nondescript/vm.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()