448 lines
15 KiB
Nim
448 lines
15 KiB
Nim
# a new bytecode emitter for nds
|
|
# emitter: takes AST, emits bytecode. It also binds variables.
|
|
|
|
import node
|
|
import ../chunk
|
|
import ../config
|
|
import ../types/value
|
|
|
|
import strformat
|
|
import sequtils
|
|
import sugar
|
|
import bitops
|
|
|
|
type
|
|
Emitter* = ref object
|
|
root: Node
|
|
chunk*: Chunk
|
|
|
|
hadError*: bool
|
|
line: int
|
|
|
|
# binding
|
|
stackIndex: int
|
|
locals: seq[Local]
|
|
scopes: seq[Scope]
|
|
|
|
Local = ref object
|
|
name: string
|
|
index: int # index in the stack (0 - stack bottom)
|
|
depth: int # depth of the local
|
|
# -1 if cannot be referenced yet
|
|
scope: Scope # innermost scope
|
|
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
|
|
isLocal: bool
|
|
|
|
Scope = ref object
|
|
labels: seq[string]
|
|
goalStackIndex: int # stack count it started with plus 1
|
|
jumps: seq[int] # list of jumps to be patched to the end of the scope
|
|
function: bool # if true, it's a function
|
|
parentFunction: Scope # if function, itself. Else, the innermost function it's in
|
|
upvalues: seq[Upvalue] # for functions, list of upvalues
|
|
|
|
# helpers
|
|
proc newEmitter*(name: string, root: Node): Emitter =
|
|
new(result)
|
|
result.root = root
|
|
result.chunk = initChunk(name)
|
|
result.stackIndex = -1
|
|
|
|
proc error(em: Emitter, msg: string) =
|
|
write stderr, &"[line {em.line}] Error {msg}\n"
|
|
em.hadError = true
|
|
|
|
proc newScope(em: Emitter, funct: bool): Scope =
|
|
result.new()
|
|
result.function = funct
|
|
result.goalStackIndex = em.stackIndex + 1
|
|
if funct:
|
|
result.parentFunction = result
|
|
elif em.scopes.len() > 0:
|
|
result.parentFunction = em.scopes[em.scopes.high()].parentFunction
|
|
em.scopes.add(result)
|
|
|
|
# chunk writers
|
|
proc writeChunk(em: Emitter, dStackIndex: int, ch: OpCode | DoubleUint8 | uint8) =
|
|
em.stackIndex += dStackIndex
|
|
em.chunk.writeChunk(ch, em.line)
|
|
|
|
proc writePops(em: Emitter, n: int) =
|
|
if n == 0:
|
|
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:
|
|
em.writeChunk(-n, opPopSA)
|
|
em.writeChunk(0, n.uint8)
|
|
else:
|
|
em.writeChunk(-n, opPopA)
|
|
em.writeChunk(0, n.toDU8())
|
|
|
|
proc writeConstant(em: Emitter, constant: NdValue) =
|
|
em.stackIndex.inc()
|
|
let index = em.chunk.writeConstant(constant, em.line)
|
|
if index >= argMax:
|
|
em.error("Too many constants in one chunk.")
|
|
|
|
# locals helpers
|
|
|
|
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
|
|
let depth = if delta > 0: -1 else: em.scopes.high
|
|
em.locals.add(
|
|
Local(name: name, depth: depth, index: em.stackIndex + delta, scope: em.scopes[em.scopes.high()], captured: false)
|
|
)
|
|
|
|
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) =
|
|
# delta = 0 - jump does not pop
|
|
# delta = -1 - jump pops the condition from the stack
|
|
em.writeChunk(delta, op)
|
|
em.writeChunk(0, 0xffffff.toDU8())
|
|
# return where the jump target coordinate is and the starting stackindex
|
|
return (em.chunk.len() - argSize, em.stackIndex)
|
|
|
|
proc patchJump(em: Emitter, target: int, stackLen: int) =
|
|
# target points to the DU8 specifying jump size
|
|
# stackLen is the original stack length - for compiler assertions only
|
|
if em.stackIndex != stackLen:
|
|
em.error("Assertion failed: jump doesn't preserve stackindex.")
|
|
let jump = em.chunk.len - target - 2
|
|
if jump > argMax:
|
|
em.error("Too much code to jump over.")
|
|
let jumpDU8 = jump.toDU8()
|
|
em.chunk.code[target] = jumpDU8[0]
|
|
em.chunk.code[target+1] = jumpDU8[1]
|
|
|
|
proc emitLoop(em: Emitter, loopStart: int, stackLen: int) =
|
|
# loopStart is the place to jump to,
|
|
# loopStackLen is the stack len at the place it is jumping to.
|
|
# opLoop is the only looping op for now, so it is assumed
|
|
if em.stackIndex != stackLen:
|
|
em.error("Assertion failed: loop doesn't preserve stackindex.")
|
|
em.writeChunk(0, opLoop)
|
|
|
|
let offset = em.chunk.len - loopStart + argSize
|
|
if offset > argMax:
|
|
em.error("Loop body too large.")
|
|
|
|
em.writeChunk(0, offset.toDU8)
|
|
|
|
|
|
# node compilers
|
|
proc emit(em: Emitter, node: Node) =
|
|
em.line = node.line
|
|
case node.kind:
|
|
of nkFalse:
|
|
em.writeChunk(1, opFalse)
|
|
of nkTrue:
|
|
em.writeChunk(1, opTrue)
|
|
of nkNil:
|
|
em.writeChunk(1, opNil)
|
|
of nkConst:
|
|
em.writeConstant(node.constant)
|
|
of nkNegate:
|
|
em.emit(node.argument)
|
|
em.writeChunk(0, opNegate)
|
|
of nkNot:
|
|
em.emit(node.argument)
|
|
em.writeChunk(0, opNot)
|
|
of nkLen:
|
|
em.emit(node.argument)
|
|
em.writeChunk(0, opLen)
|
|
of nkProgram:
|
|
for ch in node.pChildren:
|
|
em.emit(ch)
|
|
of nkBlockExpr:
|
|
# 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:
|
|
em.emit(node.expression)
|
|
em.writePops(1)
|
|
of nkPlus, nkMinus, nkMult, nkDiv, nkEq, nkNeq, nkGreater, nkLess, nkGe, nkLe:
|
|
em.emit(node.left)
|
|
em.emit(node.right)
|
|
case node.kind:
|
|
of nkPlus: em.writeChunk(-1, opAdd)
|
|
of nkMinus: em.writeChunk(-1, opSubtract)
|
|
of nkMult: em.writeChunk(-1, opMultiply)
|
|
of nkDiv: em.writeChunk(-1, opDivide)
|
|
of nkEq: em.writeChunk(-1, opEqual)
|
|
of nkNeq:
|
|
em.writeChunk(-1, opEqual)
|
|
em.writeChunk(0, opNot)
|
|
of nkGreater:
|
|
em.writeChunk(-1, opGreater)
|
|
of nkLess:
|
|
em.writeChunk(-1, opLess)
|
|
of nkGe:
|
|
em.writeChunk(-1, opLess)
|
|
em.writeChunk(0, opNot)
|
|
of nkLe:
|
|
em.writeChunk(-1, opGreater)
|
|
em.writeChunk(0, opNot)
|
|
else:
|
|
raise newException(Defect, "Misaligned case list.") # unreachable
|
|
of nkIf:
|
|
# condition
|
|
em.emit(node.ifCondition)
|
|
let (thenJump, thenStackLen) = em.emitJump(0, opJumpIfFalse)
|
|
# jumped over conditional code must leave stack size intact
|
|
# if body:
|
|
em.writePops(1) # pop condition
|
|
em.emit(node.ifBody) # if body expression
|
|
# if body: jump over else, if there is an else body
|
|
let (elseJump, elseStackLen) = em.emitJump(0, opJump)
|
|
|
|
# start of else, so jumps here if condition is falsy
|
|
em.patchJump(thenJump, thenStackLen)
|
|
if node.elseBody != nil:
|
|
# else body:
|
|
em.writePops(1) # pop condition
|
|
em.emit(node.elseBody) # else body expression
|
|
|
|
em.patchJump(elseJump, elseStackLen) # jumps over the else end up here
|
|
of nkAnd:
|
|
em.emit(node.left)
|
|
let (andJump, andStackLen) = em.emitJump(0, opJumpIfFalse)
|
|
# condition: if left is truthy, pop left and do the right expression
|
|
em.writePops(1)
|
|
em.emit(node.right)
|
|
# jump here if falsey
|
|
em.patchJump(andJump, andStackLen)
|
|
of nkOr:
|
|
em.emit(node.left)
|
|
# if falsey, jump over the jump that jumps over the right one (that's a mouthful)
|
|
let (orJump, orStackLen) = em.emitJump(0, opJumpIfFalse)
|
|
let (endJump, endStackLen) = em.emitJump(0, opJump)
|
|
em.patchJump(orJump, orStackLen)
|
|
|
|
# right side, only executed if left side is truthy
|
|
em.writePops(1) # pop left side
|
|
em.emit(node.right) # right side
|
|
|
|
# jumps to the end point here
|
|
em.patchJump(endJump, endStackLen)
|
|
of nkWhile:
|
|
em.writeChunk(1, opNil) # default return value
|
|
|
|
# looping will go here
|
|
let loopStart = em.chunk.len()
|
|
let loopStackLen = em.stackIndex
|
|
|
|
em.emit(node.whileCondition) # condition - the default return value of while
|
|
let (exitJump, exitStackLen) = em.emitJump(-1, opJumpIfFalsePop) # pop condition
|
|
|
|
# while body:
|
|
em.writePops(1) # pop old return value
|
|
em.emit(node.whileBody) # body
|
|
|
|
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) =
|
|
em.emit(em.root)
|
|
em.writeChunk(0, opReturn) # required for the vm to exit gracefully |