diff --git a/docs/reference.md b/docs/reference.md index f31af14..e8edc8d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -222,6 +222,7 @@ TODO - locals - globals +- types ## Operator precedence diff --git a/src/ndspkg/compv2/emitter.nim b/src/ndspkg/compv2/emitter.nim index 9aaaf98..b72f12a 100644 --- a/src/ndspkg/compv2/emitter.nim +++ b/src/ndspkg/compv2/emitter.nim @@ -7,6 +7,9 @@ import ../config import ../types/value import strformat +import sequtils +import sugar +import bitops type Emitter* = ref object @@ -27,7 +30,9 @@ type depth: int # depth of the local # -1 if cannot be referenced yet scope: Scope # innermost scope - captured: bool + captured: bool # if true, it was captured from a more inner function + # and it will need to outlive its scope and become a GC'd thing + # (it becomes an upvalue eventually) Upvalue* = ref object index: int @@ -72,6 +77,8 @@ proc writePops(em: Emitter, n: int) = return elif n > argMax: em.error("Too many local variables in block.") + elif n < 0: + raise newException(Defect, "writePops call with negative amount of pops.") elif n == 1: em.writeChunk(-1, opPop) elif n < shortArgMax: @@ -93,6 +100,7 @@ proc addLocal(em: Emitter, name: string, delta: int) = if em.locals.len >= argMax: em.error("Too many local variables in function.") # TODO: check if it can be increased + # TODO 2: check if delta can be assumed that it is 0, and the depth == -1 thing completely removed # if delta is 0 or negative - means that the var is already on stack when addLocal is called # if delta is +, the first ever value of the local is about to be added # so it should be -1, (see Local typedef) to indicate that it cannot be referenced yet @@ -104,6 +112,77 @@ proc addLocal(em: Emitter, name: string, delta: int) = proc markInitialized(em: Emitter) = em.locals[em.locals.high()].depth = em.scopes.high() +proc addUpvalue(em: Emitter, local: Local): int = + ## argument: local + ## This proc takes an index to a local in the locals table + ## and creates an upvalue in every function up until the one + ## including this local, so that all of the function scopes + ## in between have the right upvalue in them (compile time) + ## + ## does not create duplicates: at each layer it will first + ## find existing ones and create references to that + ## further down the line + + template lenCheck(scope: Scope, blk: untyped) = + if scope.upvalues.len() >= argMax: + em.error("Too many closure variables in function.") + blk + + var scopeIndex = local.depth + 1 + # if the scope it is directly in is a function, then upvalues + # are in more inner functions only. If it is a block, it doesn't matter + # therefore there is a +1 here + + var isLocal = true # if isLocal it means that it is the outermost function closing it + var upvalIndex: int = local.index + + while scopeIndex < em.scopes.len(): + let scope = em.scopes[scopeIndex] + if scope.function: + scope.lenCheck(): + return 0 + block ensure: + # exiting this block means upvalueIndex is updated + # and points to the upvalue within scope + for i in countup(0, scope.upvalues.high()): + let upval = scope.upvalues[i] + if upval.index == upvalIndex and upval.isLocal == isLocal: + upvalIndex = i + break ensure + scope.upvalues.add(Upvalue(index: upvalIndex, isLocal: isLocal)) + upvalIndex = scope.upvalues.high() + isLocal = false # after the first function scope is done through, it's not local anymore + scopeIndex.inc() + return upvalIndex + +proc resolveLocal(em: Emitter, name: string): tuple[index: int, upvalue: bool] = + ## Tries to find local with name + ## + ## returns: + ## upvalue - if it is an upvalue + ## index - stack index of the local + ## - if upvalue, the upvalue index + ## - if it's -1, the name cannot be resolved + + var i = em.locals.high() + let cfunc: Scope = if em.scopes.len() > 0: em.scopes[em.scopes.high()].parentFunction else: nil + while i >= 0: + let local = em.locals[i] + i.dec() + + if local.name == name: + if local.depth == -1: + continue # not initialized yet, so it is a var x = x situation => look for x in outer scopes + # from here, it's definitely a match + let upvalue = local.scope.parentFunction != cfunc # defined in a diff. function, only possible if more outer, so it's an upvalue + if upvalue: + local.captured = true # mark it as something that needs to outlive its scope + return (em.addUpvalue(local), true) + else: + return (local.index, false) + return (-1, false) + + # jump helpers proc emitJump(em: Emitter, delta: int, op: OpCode): (int, int) = @@ -162,10 +241,50 @@ proc emit(em: Emitter, node: Node) = of nkLen: em.emit(node.argument) em.writeChunk(0, opLen) + of nkProgram: + for ch in node.pChildren: + em.emit(ch) of nkBlockExpr: - # TODO: scopes + # new scope is started, which also saves the target stack len as the one here + 1 + let scope = em.newScope(false) + + # emit opNil in place of return value + em.writeChunk(1, opNil) + + for label in node.labels: + scope.labels.add(label) + # add the return value as all labels + em.addLocal(&":{label}", delta = 0) + for ch in node.children: em.emit(ch) + + # all the children statements are complete, scope cleanup + # old compiler endScope(), with function is false + discard em.scopes.pop() # pop scope (still accessible in scope) + + # old compiler restore() + # pop new locals in this scope + let delta = em.stackIndex - scope.goalStackIndex + em.writePops(delta) + + # close upvalues - old compiler endScope again + while em.locals.len() > 0: + let local = em.locals.pop() + if local.scope == scope and local.captured: + em.writeChunk(0, opCloseUpvalue) + em.writeChunk(0, local.index.toDU8()) + if local.depth < em.scopes.high() + 1: + break + + if not em.stackIndex == scope.goalStackIndex: + # kept for good measure, even if the above should mathematically always return this + em.error("Assertion failed: can't restore scope after block expression.") + + # patch jumps to end of scope + for jump in scope.jumps: + em.patchJump(jump, scope.goalStackIndex) + of nkExpr: em.emit(node.expression) of nkExprStmt: @@ -252,6 +371,76 @@ proc emit(em: Emitter, node: Node) = em.emitLoop(loopStart, loopStackLen) em.patchJump(exitJump, exitStackLen) + of nkVarGet: + var getOp = opGetGlobal # default get op + let name = node.gVarName + # use resolve helper to find it + var (arg, upval) = em.resolveLocal(name) + if arg == -1: + # global, its name should be added as a constant + # since we don't need arg anymore, replace arg with + # constant index + arg = em.chunk.addConstant(name.fromNimString()) + else: + # local or upvalue + if upval: + # upvalue + getOp = opGetUpvalue + else: + # normal local + getOp = opGetLocal + em.writeChunk(1, getOp) # write the getop + # checking for argmax shouldn't be needed as the arg is + # returned from helpers that should check for it already + em.writeChunk(0, arg.toDU8()) # write its arg + of nkVarSet: + # first emit the value it's being set to + em.emit(node.newVal) + + # assign value to var + var setOp = opSetGlobal # default set op + let name = node.sVarName + # try to resolve it + var (arg, upval) = em.resolveLocal(name) + if arg == -1: + arg = em.chunk.addConstant(name.fromNimString()) + else: + if upval: + setOp = opSetUpvalue + else: + setOp = opSetLocal + em.writeChunk(0, setOp) + em.writeChunk(0, arg.toDU8()) + of nkVarDecl: + # first, if there is a value to save to this variable, put it on the stack. If not, put a nil on the stack. + if node.value != nil: + em.emit(node.value) + else: + em.writeChunk(1, opNil) + + # now emit the relevant assigning stuff + if em.scopes.len() > 0: + # local since is within scope + # check if name already exists in the same scope + for i in countdown(em.locals.high(), 0): + let local = em.locals[i] + if local.depth != -1 and local.depth < em.scopes.len(): + break # would be more outer scope, so break + if node.name == local.name: + em.error("Already a variable with this name in this scope.") + break # redeclaring variables not allowed + + em.addLocal(node.name, 0) + # number argument is delta, and it is one indicating that the value is on the stack + # leaving it on the stack is what defines locals + # so no instruction is needed here + else: + # global + # put name as constant, and save to it + let index = em.chunk.addConstant(node.name.fromNimString()) + em.writeChunk(-1, opDefineGlobal) + em.writeChunk(0, index.toDU8()) + else: raise newException(Defect, &"Unsupported node kind: {$node.kind}") proc emit*(em: Emitter) = diff --git a/src/ndspkg/compv2/node.nim b/src/ndspkg/compv2/node.nim index 8e5439b..16ae34b 100644 --- a/src/ndspkg/compv2/node.nim +++ b/src/ndspkg/compv2/node.nim @@ -7,7 +7,7 @@ import sequtils type NodeKind* = enum - nkBlockExpr, nkExprStmt, nkBreak, nkExpr, + nkBlockExpr, nkExprStmt, nkProgram, nkBreak, nkExpr, nkConst, nkList, nkTable, nkGetIndex, nkSetIndex, nkLen, nkIf, nkWhile, nkOr, nkAnd, nkNegate, nkNot, nkPlus, nkMinus, nkMult, nkDiv, nkEq, nkNeq, nkLess, nkGreater, @@ -22,6 +22,8 @@ type of nkBlockExpr: children*: seq[Node] labels*: seq[string] + of nkProgram: + pChildren*: seq[Node] of nkExprStmt, nkExpr: # only when there is a ; does it compile to expr stmt; expr used for parentheses, & expression*: Node of nkBreak: @@ -135,6 +137,8 @@ proc `$`*(node: Node): string = of nkProc: let params = node.parameters.join(", ") result = &"(proc params: {params} body: {node.procBody})" + of nkProgram: + result = node.pChildren.map(`$`).join(";\n") of nkSetIndex: result = &"({node.sCollection}[{node.sIndex}] = {node.sValue})" of nkTable: diff --git a/src/ndspkg/compv2/parser.nim b/src/ndspkg/compv2/parser.nim index 0a3271d..fc24dfe 100644 --- a/src/ndspkg/compv2/parser.nim +++ b/src/ndspkg/compv2/parser.nim @@ -473,6 +473,8 @@ proc statement(parser: Parser, inBlock: bool = false): Node = elif parser.match(tkVar): discard parser.consume(tkIdentifier, "Identifier expected after 'var'.") let name = parser.previous.get().text + if name[0] == ':': + parser.errorAtCurrent("Attempt to declare variable with name starting with ':'.") if parser.match(tkEqual): let val = parser.expression() result = Node(kind: nkVarDecl, name: name, value: val, line: parser.line) @@ -487,10 +489,10 @@ proc statement(parser: Parser, inBlock: bool = false): Node = proc parse*(parser: Parser): Node = parser.scanner = newScanner(parser.source) - result = Node(kind: nkBlockExpr, children: @[], line: parser.line) + result = Node(kind: nkProgram, pChildren: @[], line: parser.line) parser.advance() while not parser.isAtEnd(): let statement = parser.statement() - result.children.add(statement) + result.pChildren.add(statement)