237 lines
6.5 KiB
Nim
237 lines
6.5 KiB
Nim
import strformat
|
|
import tables
|
|
|
|
import value
|
|
import chunk
|
|
import config
|
|
import pointerutils
|
|
when profileInstructions:
|
|
import times
|
|
import std/monotimes
|
|
|
|
type
|
|
Frame = object
|
|
stackBottom: int # the absolute index of where 0 inside the frame is
|
|
returnIp: ptr uint8
|
|
|
|
VM* = ref object
|
|
chunk: Chunk
|
|
ip: ptr uint8
|
|
stack: seq[KonValue]
|
|
hadError: bool
|
|
globals: Table[string, KonValue]
|
|
frames: seq[Frame]
|
|
|
|
InterpretResult* = enum
|
|
irOK, irRuntimeError
|
|
|
|
|
|
proc newVM*(ch: Chunk): VM =
|
|
result = VM(chunk: ch, stack: newSeqOfCap[KonValue](256), frames: newSeqOfCap[Frame](4))
|
|
result.ip = ch.code[0].unsafeAddr
|
|
|
|
proc push(vm: VM, val: KonValue) =
|
|
vm.stack.add(val)
|
|
|
|
proc pop(vm: VM): KonValue =
|
|
vm.stack.pop()
|
|
|
|
proc peek(vm: VM): KonValue =
|
|
vm.stack[vm.stack.high]
|
|
|
|
proc runtimeError(vm: VM, msg: string) =
|
|
let ii = vm.ip.pdiff(vm.chunk.code[0].unsafeAddr)
|
|
let line = vm.chunk.lines[ii]
|
|
write stderr, &"[line: {line}] {msg}\n"
|
|
vm.hadError = true
|
|
|
|
proc pushSafe(vm: VM, val: KonValue): bool =
|
|
## returns if the value is not a runtime error
|
|
## prints the error if it is a runtime error
|
|
## pushes it to the stack if it is not an error
|
|
if val.isError():
|
|
vm.runtimeError($val)
|
|
return false
|
|
else:
|
|
vm.push(val)
|
|
return true
|
|
|
|
proc advance(vm: VM): uint8 =
|
|
result = vm.ip[]
|
|
vm.ip = vm.ip.padd(1)
|
|
|
|
proc readDU8(vm: VM): int =
|
|
result = vm.ip.DU8ptrToInt
|
|
vm.ip = vm.ip.padd(argSize)
|
|
|
|
proc readConstant(vm: VM): KonValue =
|
|
let index = vm.readDU8()
|
|
vm.chunk.constants[index]
|
|
|
|
proc binary(op: OpCode, left: KonValue, right: KonValue): KonValue =
|
|
case op:
|
|
of opAdd:
|
|
return left.add(right)
|
|
of opSubtract:
|
|
return left.subtract(right)
|
|
of opMultiply:
|
|
return left.multiply(right)
|
|
of opDivide:
|
|
return left.divide(right)
|
|
else:
|
|
discard #unreachable
|
|
|
|
when profileInstructions:
|
|
# not a perfect profiling approach, doing random stacktrace dumps
|
|
# x amount of times per second to get the current source line
|
|
# would be better
|
|
var durations: array[OpCode, Duration]
|
|
|
|
proc run*(vm: VM): InterpretResult =
|
|
|
|
template frameBottom: int = vm.frames[vm.frames.high].stackBottom
|
|
while true:
|
|
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
|
|
|
|
let ins = vm.advance.OpCode
|
|
|
|
when debugVM:
|
|
let opname = ($ins)
|
|
var msg = &"[{vm.ii:4}] {opname}"
|
|
msg &= " Stack: [ "
|
|
for i in 0 .. vm.stack.high:
|
|
let e = vm.stack[i]
|
|
if i == frameBottom:
|
|
msg &= &"<{e}> "
|
|
else:
|
|
msg &= &"{e} "
|
|
msg &= "]"
|
|
echo msg
|
|
|
|
when profileInstructions:
|
|
let startTime = getMonoTime()
|
|
|
|
case ins:
|
|
of opPop:
|
|
discard vm.pop()
|
|
of opConstant:
|
|
let val: KonValue = vm.readConstant()
|
|
vm.push(val)
|
|
of opNegate:
|
|
let val = vm.pop.negate
|
|
if not vm.pushSafe(val):
|
|
break
|
|
of opAdd, opSubtract, opMultiply, opDivide:
|
|
let right = vm.pop()
|
|
let left = vm.pop()
|
|
if not vm.pushSafe(binary(ins, left, right)):
|
|
break
|
|
of opReturn:
|
|
if vm.frames.len == 0:
|
|
break
|
|
else:
|
|
vm.ip = vm.frames.pop().returnIp # remove frame that's over
|
|
of opTrue:
|
|
vm.push(toKonValue(true))
|
|
of opFalse:
|
|
vm.push(toKonValue(false))
|
|
of opNil:
|
|
vm.push(toKonValue())
|
|
of opNot:
|
|
vm.push(toKonValue(vm.pop.isFalsey))
|
|
of opEqual:
|
|
vm.push(toKonValue(vm.pop.equal(vm.pop())))
|
|
of opLess:
|
|
if not vm.pushSafe(vm.pop() > vm.pop()):
|
|
break
|
|
of opGreater:
|
|
if not vm.pushSafe(vm.pop() < vm.pop()):
|
|
break
|
|
of opPrint:
|
|
echo $vm.peek()
|
|
of opDefineGlobal:
|
|
let name = vm.readConstant().stringValue
|
|
if not vm.globals.hasKey(name):
|
|
vm.globals[name] = vm.pop()
|
|
else:
|
|
vm.runtimeError("Attempt to redefine an existing global variable.")
|
|
break
|
|
of opGetGlobal:
|
|
let name = vm.readConstant().stringValue
|
|
if vm.globals.hasKey(name):
|
|
vm.push(vm.globals[name])
|
|
else:
|
|
vm.runtimeError(&"Undefined global variable {name}.")
|
|
break
|
|
of opSetGlobal:
|
|
let name = vm.readConstant().stringValue
|
|
if vm.globals.hasKey(name):
|
|
vm.globals[name] = vm.peek()
|
|
else:
|
|
vm.runtimeError(&"Attempt to set undefined global variable {name}.")
|
|
break
|
|
of opGetLocal:
|
|
let slot = vm.readDU8()
|
|
vm.push(vm.stack[slot + frameBottom])
|
|
of opSetLocal:
|
|
let slot = vm.readDU8()
|
|
vm.stack[slot + frameBottom] = vm.peek()
|
|
of opJumpIfFalse:
|
|
let offset = vm.readDU8()
|
|
if vm.peek.isFalsey:
|
|
vm.ip = vm.ip.padd(offset)
|
|
of opJumpIfFalsePop:
|
|
let offset = vm.readDU8()
|
|
if vm.pop.isFalsey:
|
|
vm.ip = vm.ip.padd(offset)
|
|
of opJump:
|
|
let offset = vm.readDU8()
|
|
vm.ip = vm.ip.padd(offset)
|
|
of opLoop:
|
|
let offset = vm.readDU8()
|
|
vm.ip = vm.ip.psub(offset)
|
|
of opCall:
|
|
# create the call env
|
|
# current stack before opCall:
|
|
# ... <funct obj> <arg1> <arg2> <arg3>
|
|
# opCall converts it to this
|
|
# ... <ret val> <arg1> <arg2> <arg3>
|
|
let argcount = vm.readDU8()
|
|
let funct = vm.stack[vm.stack.high - argcount]
|
|
if funct.konType != ktFunct:
|
|
vm.runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke
|
|
break
|
|
if funct.arity != argcount:
|
|
vm.runtimeError(&"Wrong number of arguments, expected {funct.arity}, got {argcount}.")
|
|
break
|
|
# if funct.module ... TODO
|
|
|
|
vm.stack[vm.stack.high - argcount] = toKonValue() # replace the function with nil: this is the return value slot
|
|
|
|
# create new frame
|
|
vm.frames.add(Frame(stackBottom: vm.stack.high - argcount, returnIp: vm.ip))
|
|
|
|
vm.ip = vm.chunk.code[0].unsafeAddr.padd(funct.entryII) # jump to the entry point
|
|
|
|
when profileInstructions:
|
|
durations[ins] += getMonoTime() - startTime
|
|
|
|
when assertionsVM:
|
|
if not vm.hadError and vm.stack.len > 0:
|
|
vm.runtimeError(&"VM Assertion failed: stack is of non-zero length {vm.stack.len} after execution has finished.")
|
|
|
|
when profileInstructions:
|
|
for op in OpCode:
|
|
let dur = durations[op].inMilliseconds
|
|
echo &"OpCode: {op} total duration {dur} ms"
|
|
|
|
if vm.hadError:
|
|
irRuntimeError
|
|
else:
|
|
irOK
|
|
|
|
proc interpretChunk*(ch: Chunk): InterpretResult =
|
|
let vm = newVM(ch)
|
|
return vm.run()
|
|
|