emitting variables, scopes

This commit is contained in:
prod2 2022-12-03 15:55:47 +01:00
parent 6fc8514a6b
commit 8c90b669e8
4 changed files with 201 additions and 5 deletions

View File

@ -222,6 +222,7 @@ TODO
- locals - locals
- globals - globals
- types
## Operator precedence ## Operator precedence

View File

@ -7,6 +7,9 @@ import ../config
import ../types/value import ../types/value
import strformat import strformat
import sequtils
import sugar
import bitops
type type
Emitter* = ref object Emitter* = ref object
@ -27,7 +30,9 @@ type
depth: int # depth of the local depth: int # depth of the local
# -1 if cannot be referenced yet # -1 if cannot be referenced yet
scope: Scope # innermost scope 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 Upvalue* = ref object
index: int index: int
@ -72,6 +77,8 @@ proc writePops(em: Emitter, n: int) =
return return
elif n > argMax: elif n > argMax:
em.error("Too many local variables in block.") em.error("Too many local variables in block.")
elif n < 0:
raise newException(Defect, "writePops call with negative amount of pops.")
elif n == 1: elif n == 1:
em.writeChunk(-1, opPop) em.writeChunk(-1, opPop)
elif n < shortArgMax: elif n < shortArgMax:
@ -93,6 +100,7 @@ proc addLocal(em: Emitter, name: string, delta: int) =
if em.locals.len >= argMax: if em.locals.len >= argMax:
em.error("Too many local variables in function.") em.error("Too many local variables in function.")
# TODO: check if it can be increased # 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 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 # 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 # 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) = proc markInitialized(em: Emitter) =
em.locals[em.locals.high()].depth = em.scopes.high() 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 # jump helpers
proc emitJump(em: Emitter, delta: int, op: OpCode): (int, int) = proc emitJump(em: Emitter, delta: int, op: OpCode): (int, int) =
@ -162,10 +241,50 @@ proc emit(em: Emitter, node: Node) =
of nkLen: of nkLen:
em.emit(node.argument) em.emit(node.argument)
em.writeChunk(0, opLen) em.writeChunk(0, opLen)
of nkProgram:
for ch in node.pChildren:
em.emit(ch)
of nkBlockExpr: 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: for ch in node.children:
em.emit(ch) 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: of nkExpr:
em.emit(node.expression) em.emit(node.expression)
of nkExprStmt: of nkExprStmt:
@ -252,6 +371,76 @@ proc emit(em: Emitter, node: Node) =
em.emitLoop(loopStart, loopStackLen) em.emitLoop(loopStart, loopStackLen)
em.patchJump(exitJump, exitStackLen) 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: else:
raise newException(Defect, &"Unsupported node kind: {$node.kind}") raise newException(Defect, &"Unsupported node kind: {$node.kind}")
proc emit*(em: Emitter) = proc emit*(em: Emitter) =

View File

@ -7,7 +7,7 @@ import sequtils
type type
NodeKind* = enum NodeKind* = enum
nkBlockExpr, nkExprStmt, nkBreak, nkExpr, nkBlockExpr, nkExprStmt, nkProgram, nkBreak, nkExpr,
nkConst, nkList, nkTable, nkGetIndex, nkSetIndex, nkLen, nkConst, nkList, nkTable, nkGetIndex, nkSetIndex, nkLen,
nkIf, nkWhile, nkOr, nkAnd, nkNegate, nkNot, nkIf, nkWhile, nkOr, nkAnd, nkNegate, nkNot,
nkPlus, nkMinus, nkMult, nkDiv, nkEq, nkNeq, nkLess, nkGreater, nkPlus, nkMinus, nkMult, nkDiv, nkEq, nkNeq, nkLess, nkGreater,
@ -22,6 +22,8 @@ type
of nkBlockExpr: of nkBlockExpr:
children*: seq[Node] children*: seq[Node]
labels*: seq[string] 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, & of nkExprStmt, nkExpr: # only when there is a ; does it compile to expr stmt; expr used for parentheses, &
expression*: Node expression*: Node
of nkBreak: of nkBreak:
@ -135,6 +137,8 @@ proc `$`*(node: Node): string =
of nkProc: of nkProc:
let params = node.parameters.join(", ") let params = node.parameters.join(", ")
result = &"(proc params: {params} body: {node.procBody})" result = &"(proc params: {params} body: {node.procBody})"
of nkProgram:
result = node.pChildren.map(`$`).join(";\n")
of nkSetIndex: of nkSetIndex:
result = &"({node.sCollection}[{node.sIndex}] = {node.sValue})" result = &"({node.sCollection}[{node.sIndex}] = {node.sValue})"
of nkTable: of nkTable:

View File

@ -473,6 +473,8 @@ proc statement(parser: Parser, inBlock: bool = false): Node =
elif parser.match(tkVar): elif parser.match(tkVar):
discard parser.consume(tkIdentifier, "Identifier expected after 'var'.") discard parser.consume(tkIdentifier, "Identifier expected after 'var'.")
let name = parser.previous.get().text let name = parser.previous.get().text
if name[0] == ':':
parser.errorAtCurrent("Attempt to declare variable with name starting with ':'.")
if parser.match(tkEqual): if parser.match(tkEqual):
let val = parser.expression() let val = parser.expression()
result = Node(kind: nkVarDecl, name: name, value: val, line: parser.line) 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 = proc parse*(parser: Parser): Node =
parser.scanner = newScanner(parser.source) parser.scanner = newScanner(parser.source)
result = Node(kind: nkBlockExpr, children: @[], line: parser.line) result = Node(kind: nkProgram, pChildren: @[], line: parser.line)
parser.advance() parser.advance()
while not parser.isAtEnd(): while not parser.isAtEnd():
let statement = parser.statement() let statement = parser.statement()
result.children.add(statement) result.pChildren.add(statement)