import strformat import chunk import config import pointerutils import types/stack import types/ndstring import bitops # needed for value's templates import types/value import types/hashtable import types/ndlist 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[NdValue, 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 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 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) - 1 let opname = ($ins) var msg = &"[{ii:04}] {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() runcounts[ins] += 1f 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(fromBool(true)) of opFalse: stack.add(fromBool(false)) of opNil: stack.add(fromNil()) of opNot: # TODO hayago += optimization stack.add(fromBool(stack.pop().isFalsey())) of opEqual: stack.add(fromBool(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().asString() let existed = globals.tableSet(name.fromNdString(), stack.pop()) if existed: runtimeError("Attempt to redefine an existing global variable.") break of opGetGlobal: let name = readConstant().asString() var val: NdValue let existed = globals.tableGet(name.fromNdString(), val) if existed: stack.add(val) else: runtimeError(&"Undefined global variable {name}.") break of opSetGlobal: let name = readConstant().asString() let existed = globals.tableSet(name.fromNdString(), stack.peek()) if not existed: runtimeError("Attempt to redefine an existing global variable.") 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 opFunctionDef: let offset = readDU8() let faddr: ptr uint8 = ip ip = ip.padd(offset) stack.push(faddr.fromFunct()) 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 not funct.isFunct(): runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke break stack.setIndexNeg(argcount, fromNil()) # replace the function with nil: this is the return value slot # create new frame frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip)) ip = funct.asFunct() # jump to the entry point of opCreateList: let listLen = readDU8() if listLen == 0: stack.push(newList[NdValue]().fromList()) else: let start = stack.getIndexNeg(listLen - 1).addr var list = newListCopymem[NdValue](start, listLen) stack.deleteTopN(listLen) stack.push(list.fromList()) of opCreateTable: let tblLen = readDU8() var tbl = newNdTable[NdValue, NdValue](tblLen) for i in countup(0, tblLen - 1): let val = stack.pop() let key = stack.pop() if tbl[].tableSet(key, val): runtimeError("Attempt to redefine an existing value inside table declaration.") break stack.deleteTopN(tblLen * 2) stack.push(tbl.fromTable()) of opLen: let res = stack.peek().getLength() if not res.ok: runtimeError(res.msg) break of opGetIndex: let index = stack.pop() let res = stack.peek().getIndex(index) # getIndex modifies the top value of the stack if not res.ok: runtimeError(res.msg) break of opSetIndex: let value = stack.pop() let index = stack.pop() let res = stack.peek().setIndex(index, value) if not res.ok: runtimeError(res.msg) break of opChr: let val = stack.peek() if not val.isFloat(): runtimeError("chr on not float") break let floatval = val.asFloat() if floatval > 255f or floatval < 0f: runtimeError("chr on a float out of range") break let chr = floatval.char() stack.settip((chr).newString().fromNdString()) of opInt: let val = stack.peek() if not val.isString(): runtimeError("int on non string") break let strval = val.asString() if strval.getLength() == 0: runtimeError("int on empty string") break let code = val.asString().getIndexAsChar(0).float() stack.settip(code.fromFloat()) of opPutchar: write stdout, $stack.peek() 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() globals.free() if hadError: irRuntimeError else: irOK