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 InterpretResult* = enum irOK, irRuntimeError 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*(chunk: Chunk): InterpretResult = var ip: ptr uint8 = chunk.code[0].unsafeAddr stack: seq[NdValue] = newSeqOfCap[NdValue](256) hadError: bool globals: Table[string, NdValue] frames: seq[Frame] = newSeqOfCap[Frame](4) proc runtimeError(msg: string) = let ii = ip.pdiff(chunk.code[0].unsafeAddr) let line = chunk.lines[ii] write stderr, &"[line: {line}] {msg}\n" hadError = true template peek(stack: seq[NdValue]): NdValue = stack[stack.high] proc popn(stack: var seq[NdValue], amt: int) {.inline.} = for i in countup(1, amt): discard stack.pop() proc pushSafe(stack: var seq[NdValue], val: NdValue): bool = ## DEPRECATED ## 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(): runtimeError($val) false else: stack.add(val) true proc readUI8(): int = result = ip[].int ip = ip.padd(1) proc readUI8Noconv(): uint8 = result = ip[] ip = ip.padd(1) proc readDU8(): int = result = ip.DU8ptrToInt ip = ip.padd(argSize) proc readConstant(): NdValue = let index = readDU8() chunk.constants[index] template frameBottom: int = frames[frames.high].stackBottom while true: {.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma let ins = ip[].OpCode ip = ip.padd(1) when debugVM: let ii = ip.pdiff(chunk.code[0].unsafeAddr) let opname = ($ins) var msg = &"[{ii:4}] {opname}" msg &= " Stack: [ " for i in 0 .. stack.high: let e = stack[i] if i == frameBottom: msg &= &"<{e}> " else: msg &= &"{e} " msg &= "]" echo msg when profileInstructions: let startTime = getMonoTime() case ins: of opPop: discard stack.pop() of opPopA: let amt = readDU8() stack.popn(amt) of opPopSA: let amt = readUI8() stack.popn(amt) of opConstant: let val: NdValue = readConstant() stack.add(val) of opNegate: let res = stack.peek().negate if not res.ok: runtimeError(res.msg) break of opAdd: let right = stack.pop() let res = stack.peek().add(right) if not res.ok: runtimeError(res.msg) break of opSubtract: let right = stack.pop() let res = stack.peek().subtract(right) if not res.ok: runtimeError(res.msg) break of opMultiply: let right = stack.pop() let res = stack.peek().multiply(right) if not res.ok: runtimeError(res.msg) break of opDivide: let right = stack.pop() let res = stack.peek().divide(right) if not res.ok: runtimeError(res.msg) break of opReturn: if frames.len == 0: break else: ip = frames.pop().returnIp # remove frame that's over of opTrue: stack.add(toNdValue(true)) of opFalse: stack.add(toNdValue(false)) of opNil: stack.add(toNdValue()) of opNot: stack.add(toNdValue(stack.pop().isFalsey)) of opEqual: stack.add(toNdValue(stack.pop().equal(stack.pop()))) of opLess: if not stack.pushSafe(stack.pop() > stack.pop()): break of opGreater: if not stack.pushSafe(stack.pop() < stack.pop()): break of opPrint: echo $stack.peek() of opDefineGlobal: let name = readConstant().stringValue if not globals.hasKey(name): globals[name] = stack.pop() else: runtimeError("Attempt to redefine an existing global variable.") break of opGetGlobal: let name = readConstant().stringValue if globals.hasKey(name): stack.add(globals[name]) else: runtimeError(&"Undefined global variable {name}.") break of opSetGlobal: let name = readConstant().stringValue if globals.hasKey(name): globals[name] = stack.peek() else: runtimeError(&"Attempt to set undefined global variable {name}.") break of opGetLocal: let slot = readDU8() stack.add(stack[slot + frameBottom]) of opSetLocal: let slot = readDU8() stack[slot + frameBottom] = stack.peek() of opJumpIfFalse: let offset = readDU8() if stack.peek().isFalsey: ip = ip.padd(offset) of opJumpIfFalsePop: let offset = readDU8() if stack.pop().isFalsey: ip = ip.padd(offset) of opJump: let offset = readDU8() ip = ip.padd(offset) of opLoop: let offset = readDU8() ip = ip.psub(offset) of opCall: # create the call env # current stack before opCall: # ... # opCall converts it to this # ... let argcountUint8 = readUI8Noconv() let argcount = argcountUint8.int let funct = stack[stack.high - argcount] if funct.ndType != ndFunct: runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke break if funct.arity != argcountUint8: runtimeError(&"Wrong number of arguments, expected {funct.arity}, got {argcount}.") break stack[stack.high - argcount] = toNdValue() # replace the function with nil: this is the return value slot # create new frame frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip)) ip = chunk.code[0].unsafeAddr.padd(funct.entryII) # jump to the entry point when profileInstructions: durations[ins] += getMonoTime() - startTime when assertionsVM: if not hadError and stack.len > 0: runtimeError(&"VM Assertion failed: stack is of non-zero length {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 hadError: irRuntimeError else: irOK