function implementation prototype

This commit is contained in:
prod2 2022-01-21 01:51:55 +01:00
parent e5f2e213f0
commit 35d2b96651
8 changed files with 231 additions and 40 deletions

View File

@ -1 +1,10 @@
# nondescript
Nondescript is an experiment with making a more expressive version of lox with more optimalized function calls.
https://craftinginterpreters.com
Codebase note:
kon* might be a remnant of an older naming attempt... but konScript seems to be taken. The prefixes will be changed to nd* later.

View File

@ -5,7 +5,7 @@ import value
type
OpCode* = enum
opReturn, # return
opReturn, opCall, # functions
opPop, # pop
opPrint, # print
opNegate, opNot # unary
@ -65,7 +65,7 @@ proc writeConstant*(ch: var Chunk, constant: KonValue, line: int): int =
ch.writeChunk(result.toTriple, line)
const simpleInstructions = {
opReturn,
opReturn,
opPop,
opPrint,
opNegate, opNot,
@ -78,6 +78,7 @@ const constantInstructions = {
opDefineGlobal, opGetGlobal, opSetGlobal,
}
const argInstructions = {
opCall,
opGetLocal, opSetLocal,
opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop,
}

View File

@ -1,6 +1,5 @@
import strformat
import strutils
import sequtils
import sugar
import tables
import options
@ -17,14 +16,13 @@ type
depth: int # depth of this local
# if depth is -1, the variable cannot be referenced yet
# its depth will be set once its first ever value is determined
owning: bool # whether this reference owns the local on the stack
# always true except when it's the "result" value
Scope = ref object
labels: seq[string]
depth: int
#depth: int
goalStackIndex: int # the stack count it started with plus 1
jumps: seq[int] # jumps to be patched that jump to the end
function: bool # if true, it is a function
Compiler = ref object
#
@ -64,9 +62,10 @@ type
infix: (Compiler) -> void
prec: Precedence # only relevant to infix, prefix always has pcUnary
proc newScope(comp: Compiler): Scope =
proc newScope(comp: Compiler, function: bool): Scope =
result.new()
result.depth = comp.scopes.len + 1
#result.depth = comp.scopes.len + 1
result.function = function
result.goalStackIndex = comp.stackIndex + 1
comp.scopes.add(result)
@ -128,7 +127,7 @@ proc synchronize(comp: Compiler) =
while comp.current.tokenType != tkEof:
if comp.previous.tokenType in {tkSemicolon, tkRightBrace}:
return
if comp.current.tokenType in {tkFun, tkVar, tkFor, tkIf, tkWhile}:
if comp.current.tokenType in {tkFunct, tkVar, tkFor, tkIf, tkWhile}:
return
comp.advance()
@ -145,13 +144,13 @@ proc writeConstant(comp: Compiler, constant: KonValue) =
comp.error("Too many constants in one chunk.")
proc addLocal(comp: Compiler, name: string, delta: int, owning: bool = true) =
proc addLocal(comp: Compiler, name: string, delta: int) =
if comp.locals.len >= tripleMax:
comp.error("Too many local variables in function.")
# if delta is 0 or negative, it means that it is already on the stack when addLocal is called
# if delta is positive, the first ever value of the local is to the right
comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta, owning: owning))
comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta))
proc markInitialized(comp: Compiler) =
comp.locals[comp.locals.high].depth = comp.scopes.high
@ -220,22 +219,58 @@ proc emitLoop(comp: Compiler, loopstart: int, delta: int, op: OpCode) =
comp.writeChunk(0, offset.toTriple)
# SCOPE HELPERS
proc beginScope(comp: Compiler, consumeLabels: bool = true) =
# owning false because it is the return value
let scope = comp.newScope()
proc beginScope(comp: Compiler, function: bool = false) =
let scope = comp.newScope(function)
while (consumeLabels and comp.match(tkLabel)):
let label = comp.previous.text[1..^1]
scope.labels.add(label)
when debugCompiler:
debugEcho &"Begin scope called for depth {comp.scopes.len} function? {function}"
comp.writeChunk(1, opNil)
comp.addLocal("^", delta = 0, owning = false)
if function:
scope.labels.add("result")
else:
while comp.match(tkLabel):
let label = comp.previous.text[1..^1]
scope.labels.add(label)
if function:
# if it's a function scope, the frame will move
# access to outside locals is also limited to upvalues and closures
comp.stackIndex = 0
else:
# only put the opNil if it's not a function scope, since
# function scopes are initialized by the caller
comp.writeChunk(1, opNil)
comp.addLocal("^", delta = 0)
# note, that later added scope labels will not be accessible through ^
# so it is better not to assign scope labels after beginScope
for label in scope.labels:
comp.addLocal(&"^{label}", delta = 0)
proc restore(comp: Compiler, scope: Scope) =
let delta = comp.stackIndex - scope.goalStackIndex
for i in countup(1, delta):
comp.writeChunk(0, opPop)
comp.writeChunk(-1, opPop)
when assertionsCompiler:
if not comp.stackIndex == scope.goalStackIndex:
comp.error("Assertion failed in restore")
when debugCompiler:
debugEcho &"Restored scope: delta {delta}"
proc restoreInFunct(comp: Compiler, scope: Scope) =
when debugCompiler:
var pops = 0
while comp.stackIndex > 0:
comp.writeChunk(-1, opPop)
when debugCompiler:
inc pops
when assertionsCompiler:
if not comp.stackIndex == 0:
comp.error("Assertion failed in restoreInFunct")
comp.stackIndex = scope.goalStackIndex
when debugCompiler:
debugEcho &"Restored function scope: delta {pops}; new stackindex: {comp.stackIndex}"
proc jumpToEnd(comp: Compiler, scope: Scope) =
let delta = comp.stackIndex - scope.goalStackIndex
@ -247,10 +282,19 @@ proc jumpToEnd(comp: Compiler, scope: Scope) =
proc endScope(comp: Compiler) =
# remove locals
let popped = comp.scopes.pop()
comp.restore(popped)
let function = popped.function
when debugCompiler:
debugEcho &"End scope called for depth {comp.scopes.len} function? {function}"
if function:
comp.restoreInFunct(popped)
else:
comp.restore(popped)
# patch jumps to after the scope (such jumps from breaks emit the pops before jumping)
for jump in popped.jumps:
comp.patchJump(jump)
if function:
comp.writeChunk(0, opReturn)
# EXPRESSIONS
proc parsePrecedence(comp: Compiler, prec: Precedence) =
@ -287,7 +331,13 @@ proc expression(comp: Compiler) =
# DO NOT ADD ANYTHING HERE
# only use the pratt table for parsing expressions!
when assertionsVM:
let oldStackIndex = comp.stackIndex
comp.parsePrecedence(pcAssignment)
when assertionsVM:
let diff = comp.stackIndex - oldStackIndex
if diff != 1:
comp.error(&"Assertion failed: expression increased ({oldStackIndex} -> {comp.stackIndex}) the stack index by {diff} (should be 1).")
proc number(comp: Compiler) =
@ -372,7 +422,31 @@ proc grouping(comp: Compiler) =
comp.expression()
comp.consume(tkRightParen, "Expect ')' after expression.")
tkLeftParen.genRule(grouping, nop, pcNone)
proc parseCall(comp: Compiler) =
# ( consumed
# create the call env
# current stack before opCall:
# ... <funct obj> <arg1> <arg2> <arg3>
# opCall converts it to this
# ... <ret val> <arg1> <arg2> <arg3>
var argcount = 0
# put args on stack
while comp.current.tokenType notin {tkRightParen, tkEof}:
comp.expression()
inc argcount
if comp.current.tokenType != tkRightParen:
comp.consume(tkComma, "Expected ',' between arguments in function calls.")
comp.consume(tkRightParen, "Expected ')' after arguments in function calls.")
# emit call
comp.writeChunk(-argcount, opCall)
comp.writeChunk(0, argcount.toTriple)
tkLeftParen.genRule(grouping, parseCall, pcCall)
proc unary(comp: Compiler) =
let opType = comp.previous.tokenType
@ -519,6 +593,56 @@ proc parseWhile(comp: Compiler) =
tkWhile.genRule(parseWhile, nop, pcNone)
proc parseFunct(comp: Compiler) =
# jump over
let jumpOverBody = comp.emitJump(0, opJump)
comp.consume(tkLeftParen, "Expected '(' after keyword 'funct'.")
var params: seq[string]
# parameters
while comp.current.tokenType == tkIdentifier:
comp.advance()
params.add(comp.previous.text)
if comp.current.tokenType == tkRightParen:
break
comp.consume(tkComma, "Expected ',' to separate items in the parameter list.")
comp.consume(tkRightParen, "Expected ')' after parameter list.")
# function body:
let functII = comp.chunk.len
comp.beginScope(function = true) # this saves the old stackindex, sets it to 0, points ^ and ^function at index 0
# assumption:
# the caller will create the following stack for the function to run in:
# [0] = return value placeholder
# [1] = arg #1
# [2] = arg #2
# [3] = arg #3
for i in countup(1, params.len):
comp.stackIndex = i
comp.addLocal(params[i-1], 0)
comp.expression()
when assertionsCompiler:
let shouldbeStackIndex = params.len + 1
if shouldbeStackIndex != comp.stackIndex:
comp.error(&"Assertion failed: wrong stackindex ({comp.stackIndex}) in function declaration (should be {shouldbeStackIndex}).")
comp.endScope()
dec comp.stackIndex # the previous end scope did not put anything on the stack, it is jumped over
# get konvalue functions:
let konFunct = newKonFunction(comp.chunk.name, functII, params.len)
# end of function declaration:
comp.patchJump(jumpOverBody)
# put the fn object on the stack
comp.writeConstant(konFunct)
tkFunct.genRule(parseFunct, nop, pcNone)
# below are the expressions that can contain statements in some way
# the only expressions that can contain a statement are:
# the block expression
@ -632,7 +756,8 @@ proc compile*(comp: Compiler) =
comp.advance()
while comp.current.tokenType != tkEof:
comp.statement()
comp.writeChunk(-1, opReturn)
comp.writeChunk(-1, opPop)
comp.writeChunk(0, opReturn)
when debugDumpChunk:
if not comp.hadError:
comp.chunk.disassembleChunk()

View File

@ -4,11 +4,12 @@ type
ReadlineInterruptedException* = object of CatchableError
# choose debug options here
const debugVM* = true
const debugVM* = false
const debugScanner* = false
const debugCompiler* = false
const debugDumpChunk* = true
const debugDumpChunk* = false
const assertionsVM* = true # sanity checks in the VM, such as the stack being empty at the end
const assertionsCompiler* = true # sanity checks in the compiler
# choose a line editor for the repl
const lineEditor = leRdstdin

7
fib.nds Normal file
View File

@ -0,0 +1,7 @@
var fib = funct(n)
if (n < 2) ^result = 1
else ^result = fib(n-1) + fib(n-2)
;
print fib(25);

View File

@ -15,7 +15,7 @@ type
tkMinus, tkPlus, tkSemicolon, tkSlash, tkStar, tkBang, tkBangEqual,
tkGreater, tkGreaterEqual, tkLess, tkLessEqual, tkEqual, tkEqualEqual,
tkIdentifier, tkString,
tkNumber, tkAnd, tkElse, tkFalse, tkFor, tkFun, tkIf, tkNil,
tkNumber, tkAnd, tkElse, tkFalse, tkFor, tkFunct, tkGoto, tkIf, tkNil,
tkOr, tkPrint, tkLabel, tkBreak, tkTrue, tkVar, tkWhile,
tkError, tkEof
@ -113,7 +113,9 @@ const keywords = {
"else": tkElse,
"false": tkFalse,
"for": tkFor,
"fun": tkFun,
"funct": tkFunct,
# here's a language that uses funct... still waiting for the day when a good de-funct joke comes to my mind that I can abuse
"goto": tkGoto,
"if": tkIf,
"nil": tkNil,
"or": tkOr,

View File

@ -3,7 +3,7 @@ import strformat
type
KonType* = enum
ktNil, ktBool, ktFloat, ktString,
ktFunc,
ktFunct,
ktTypeError,
const errorTypes = {ktTypeError}
@ -19,7 +19,7 @@ type
floatValue*: float64
of ktString:
stringValue*: string
of ktFunc:
of ktFunct:
module*: string # which chunk the function is compiled in
entryII*: int # entry instruction index
arity*: int # number of arguments
@ -38,7 +38,7 @@ proc `$`*(val: KonValue): string =
return "nil"
of ktString:
return val.stringValue
of ktFunc:
of ktFunct:
return &"Function object: {val.module}/{val.entryII}"
of errorTypes:
let ename = $val.konType
@ -66,7 +66,7 @@ proc equal*(val, right: KonValue): bool =
true
of ktString:
val.stringValue == right.stringValue
of ktFunc:
of ktFunct:
val.module == right.module and val.entryII == right.entryII
# same entry II/module but diff arity is a bug
of errorTypes:
@ -83,6 +83,9 @@ proc toKonValue*(val: bool): KonValue =
proc toKonValue*(val: string): KonValue =
KonValue(konType: ktString, stringValue: val)
proc newKonFunction*(module: string, ii: int, arity: int): KonValue =
KonValue(konType: ktFunct, module: module, entryII: ii, arity: arity)
proc toKonValue*: KonValue =
KonValue(konType: ktNil)

61
vm.nim
View File

@ -6,6 +6,10 @@ import chunk
import config
type
Frame = object
stackBottom: int # the absolute index of where 0 inside the frame is
ii: int
VM* = ref object
chunk: Chunk
ii: int
@ -13,13 +17,14 @@ type
line: int
hadError: bool
globals: Table[string, KonValue]
frames: seq[Frame]
InterpretResult* = enum
irOK, irRuntimeError
proc newVM*(ch: Chunk): VM =
VM(chunk: ch, ii: 0, stack: @[])
VM(chunk: ch, ii: 0, stack: @[], frames: @[Frame()])
proc push(vm: VM, val: KonValue) =
vm.stack.add(val)
@ -72,16 +77,24 @@ proc binary(op: OpCode, left: KonValue, right: KonValue): KonValue =
proc run*(vm: VM): InterpretResult =
template frameBottom: int = vm.frames[vm.frames.high].stackBottom
while true:
let ins = vm.advance.OpCode
when debugVM:
var msg = &"[{vm.ii}]"
let opname = ($ins)
var msg = &"[{vm.ii:4}] {opname}"
msg &= " Stack: [ "
for e in vm.stack:
msg &= &"{e} "
for i in 0 .. vm.stack.high:
let e = vm.stack[i]
if i == frameBottom:
msg &= &"<{e}> "
else:
msg &= &"{e} "
msg &= "]"
echo msg
let ins = vm.advance.OpCode
case ins:
of opConstant:
let val: KonValue = vm.readConstant()
@ -96,8 +109,11 @@ proc run*(vm: VM): InterpretResult =
if not vm.pushSafe(binary(ins, left, right)):
break
of opReturn:
discard vm.pop()
break
if frameBottom == 0:
break
else:
discard vm.frames.pop() # remove frame that's over
vm.ii = vm.frames[vm.frames.high].ii # get backed up ii
of opTrue:
vm.push(toKonValue(true))
of opFalse:
@ -141,10 +157,10 @@ proc run*(vm: VM): InterpretResult =
discard vm.pop()
of opGetLocal:
let slot = vm.readTriple()
vm.push(vm.stack[slot])
vm.push(vm.stack[slot + frameBottom])
of opSetLocal:
let slot = vm.readTriple()
vm.stack[slot] = vm.peek()
vm.stack[slot + frameBottom] = vm.peek()
of opJumpIfFalse:
let offset = vm.readTriple()
if vm.peek.isFalsey:
@ -159,6 +175,33 @@ proc run*(vm: VM): InterpretResult =
of opLoop:
let offset = vm.readTriple()
vm.ii -= offset
of opCall:
# create the call env
# current stack before opCall:
# ... <funct obj> <arg1> <arg2> <arg3>
# opCall converts it to this
# ... <ret val> <arg1> <arg2> <arg3>
let argcount = vm.readTriple()
let funct = vm.stack[vm.stack.high - argcount]
if funct.konType != ktFunct:
vm.runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke
break
if funct.arity != argcount:
vm.runtimeError(&"Wrong number of arguments, expected {funct.arity}, got {argcount}.")
break
# if funct.module ... TODO
vm.stack[vm.stack.high - argcount] = toKonValue() # replace the function with nil: this is the return value slot
# save ii
vm.frames[vm.frames.high].ii = vm.ii
# create new frame
vm.frames.add(Frame(stackBottom: vm.stack.high - argcount))
vm.ii = funct.entryII # jump to the entry point
when assertionsVM:
if not vm.hadError and vm.stack.len > 0: