nondescript/src/ndspkg/compv2/emitter.nim

633 lines
20 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)
# jump helper for break
proc jumpToEnd(em: Emitter, scope: Scope) =
## jumps to end of scope and emits enough pops before doing so to match goal stack index
## However, does not affect stack Index during compilation
var delta: int
if scope.function:
delta = em.stackIndex
else:
delta = em.stackIndex - scope.goalStackIndex
em.writePops(delta)
let (jump, _) = em.emitJump(delta, opJump)
# compensate for minus delta from writePops by plus delta at this opJump
scope.jumps.add(jump)
# 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)
if ch.kind == nkExpr: # implicit return if no ;
em.writeChunk(0, opSetLocal)
em.writeChunk(0, scope.goalStackIndex.toDU8())
# 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)
# close upvalues - old compiler endScope
var i = em.locals.high()
while i >= 0:
let local = em.locals[i]
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
i.dec()
# old compiler restore()
# pop new locals in this scope
let delta = em.stackIndex - scope.goalStackIndex
em.writePops(delta)
# remove those locals that are too deep
em.locals.keepIf((it) => it.depth < em.scopes.len())
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 nkProc:
# proc declaration
# first, it should be jumped over during declaration
let (jumpOverBody, jumpStackLen) = em.emitJump(1, opFunctionDef)
# function body
# begin scope
let scope = em.newScope(true) # true for function
em.stackIndex = 0 # it's function, so reset it
em.addLocal(":result", 0) # pointing to the return value
if node.parameters.len() > shortArgMax:
em.error("Too many parameters.")
# first thing in the function body is checking arity
em.writeChunk(0, opCheckArity)
em.writeChunk(0, node.parameters.len().uint8())
# adding locals
for i in countup(1, node.parameters.len()):
em.stackIndex = i
em.addLocal(node.parameters[i-1], 0)
# the body
em.emit(node.procBody)
# checking stack integrity (body should increase stack index by one)
let shouldbeStackIndex = node.parameters.len() + 1
if shouldbeStackIndex != em.stackIndex:
em.error("Assertion failed: wrong stackindex in function declaration.")
# end scope start
discard em.scopes.pop()
scope.parentFunction = nil # cleanup cycles, since for functions
# parentFunction points to itself
# end scope further stuff
# closing upvalues - also see tkBlockExpression
var i = em.locals.high()
while i >= 0:
let local = em.locals[i]
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
i.dec()
# restore stackindex
em.stackIndex = scope.goalStackIndex - 1
# old stack index is always goal - 1
# and the opFunctionDef already increased the stack by 1
# return
em.writeChunk(0, opReturn)
# remove locals that were in this scope
em.locals.keepIf((it) => it.depth < em.scopes.len())
# END SCOPE END
# body is over and checks if stack index is restored
em.patchJump(jumpOverBody, jumpStackLen)
# closures are implemented in a way, where the vm
# pops the function from the stack and reads the
# upvalue details from the following bytes
if scope.upvalues.len() > 0:
em.writeChunk(0, opClosure)
em.writeChunk(0, scope.upvalues.len().toDU8())
for upval in scope.upvalues:
em.writeChunk(0, upval.index.toDU8())
em.writeChunk(0, if upval.isLocal: 0'u8 else: 1'u8)
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())
of nkBreak:
if node.label == "":
em.error("Label expected after break.")
let label = node.label
block ensure:
for i in countdown(em.scopes.high, 0):
let scope = em.scopes[i]
if scope.labels.contains(label):
em.jumpToEnd(scope)
break ensure
em.error(&"Couldn't find label {label}.")
of nkCall:
# get the callee
em.emit(node.function)
# first put all args on the stack
for arg in node.arguments:
em.emit(arg)
let argCount = node.arguments.len()
if argCount > shortArgMax:
em.error("Too many arguments.")
# emit the call opcode
em.writeChunk(-argCount, opCall)
em.writeChunk(0, argCount.uint8)
of nkColonCall:
# lua style table:method()
# put table on stack
em.emit(node.cCollection)
# it should be duplicated
em.writeChunk(1, opDup)
# the index
em.emit(node.cIndex)
# get index
em.writeChunk(-1, opGetIndex)
# now the stack is: table, method
# invert for function call alignment
em.writeChunk(0, opSwap)
# now similar to nkCall
for arg in node.cArguments:
em.emit(arg)
let argCount = node.cArguments.len() + 1
if argCount > shortArgMax:
em.error("Too many arguments.")
em.writeChunk(-argCount, opCall)
em.writeChunk(0, argCount.uint8)
of nkGetIndex:
# collection
em.emit(node.gCollection)
# index
em.emit(node.gIndex)
# get op
em.writeChunk(-1, opGetIndex)
of nkSetIndex:
# collection
em.emit(node.sCollection)
# index
em.emit(node.sIndex)
# new val
em.emit(node.sValue)
# set op
em.writeChunk(-2, opSetIndex)
of nkList:
# put all elements on stack
for ch in node.elems:
em.emit(ch)
let count = node.elems.len()
if count > argMax:
em.error("Maximum list length exceeded.")
em.writeChunk(1-count, opCreateList)
em.writeChunk(0, count.toDU8())
of nkTable:
# key val alternating put on stack
for i in countup(0, node.keys.high()):
em.emit(node.keys[i])
em.emit(node.values[i])
let count = node.keys.len()
if count > argMax:
em.error("Maximum table length exceeded.")
em.writeChunk(1 - 2 * count, opCreateTable)
em.writeChunk(0, count.toDU8())
proc emit*(em: Emitter) =
em.emit(em.root)
em.writeChunk(0, opReturn) # required for the vm to exit gracefully