import strformat import tables import value import chunk import config import pointerutils import types/stack import types/ndstring import types/stringutils when profileInstructions: import times import std/monotimes # 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] var runcounts: array[OpCode, float64] type Frame = object stackBottom: int # the absolute index of where 0 inside the frame is returnIp: ptr uint8 InterpretResult* = enum irOK, irRuntimeError proc run*(chunk: Chunk): InterpretResult = var ip: ptr uint8 = chunk.code[0].unsafeAddr stack: Stack[NdValue] = newStack[NdValue](256) hadError: bool globals: Table[string, NdValue] frames: Stack[Frame] = newStack[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 frames.add(Frame(stackBottom: 0)) #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() template popn(stack: var Stack[NdValue], amt: int) = stack.deleteTopN(amt) proc readUI8(): int = result = ip[].int 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.peek().stackBottom 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() runtimes[ins].inc 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() == 1: 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: let right = stack.pop() let res = stack.peek().less(right) if not res.ok: runtimeError(res.msg) break of opGreater: let right = stack.pop() let res = stack.peek().greater(right) if not res.ok: runtimeError(res.msg) 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 opCheckArity: let arity = readUI8() let argcount = stack.high() - frameBottom if arity != argcount: runtimeError(&"Wrong number of arguments, expected {arity}, got {argcount}.") break of opCall: # create the call env # current stack before opCall: # ... # opCall converts it to this # ... let argcount = readUI8() let funct = stack.getIndexNeg(argcount) if funct.ndType != ntFunct: runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke break stack.setIndexNeg(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 let times = runcounts[op] echo &"OpCode: {op} total duration {dur} ms {times} times" stack.free() frames.free() if hadError: irRuntimeError else: irOK