function implementation prototype
This commit is contained in:
parent
e5f2e213f0
commit
35d2b96651
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
167
compiler.nim
167
compiler.nim
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
var fib = funct(n)
|
||||
if (n < 2) ^result = 1
|
||||
else ^result = fib(n-1) + fib(n-2)
|
||||
;
|
||||
|
||||
print fib(25);
|
||||
|
|
@ -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,
|
||||
|
|
11
value.nim
11
value.nim
|
@ -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
61
vm.nim
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue