import strformat import tables import value import chunk import config type Frame = object stackBottom: int # the absolute index of where 0 inside the frame is ii: int VM* = ref object chunk: Chunk ii: int stack: seq[KonValue] line: int hadError: bool globals: Table[string, KonValue] frames: seq[Frame] InterpretResult* = enum irOK, irRuntimeError proc newVM*(ch: Chunk): VM = VM(chunk: ch, ii: 0, stack: @[], frames: @[Frame()]) 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) = write stderr, &"[line: {vm.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 = vm.ii.inc vm.line = vm.chunk.lines[vm.ii-1] vm.chunk.code[vm.ii-1] proc readDU8(vm: VM): int = [vm.advance, vm.advance].toInt 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 proc run*(vm: VM): InterpretResult = template frameBottom: int = vm.frames[vm.frames.high].stackBottom while true: 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 {.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma case ins: 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 frameBottom == 0: break else: discard vm.frames.pop() # remove frame that's over vm.ii = vm.frames[vm.frames.high].ii # get backed up ii 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 opPop: discard vm.pop() 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.ii += offset of opJumpIfFalsePop: let offset = vm.readDU8() if vm.pop.isFalsey: vm.ii += offset of opJump: let offset = vm.readDU8() vm.ii += offset of opLoop: let offset = vm.readDU8() vm.ii -= offset of opCall: # create the call env # current stack before opCall: # ... # opCall converts it to this # ... 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 # save ii vm.frames[vm.frames.high].ii = vm.ii # create new frame vm.frames.add(Frame(stackBottom: vm.stack.high - argcount)) vm.ii = funct.entryII # jump to the entry point 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.") if vm.hadError: irRuntimeError else: irOK proc interpretChunk*(ch: Chunk): InterpretResult = let vm = newVM(ch) return vm.run()