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
- globals
- types
## Operator precedence

View File

@ -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) =

View File

@ -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:

View File

@ -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)