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 import types/closure import types/native import lib/main when debugVM: import terminal 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 # closure: Closure[NdValue] 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) openUpvalues: Upvalue[NdValue] = nil 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) template readUI8(ip: var ptr uint8): int = let res = ip[].int ip = ip.padd(1) res template readDU8(ip: var ptr uint8): int = let res = ip.DU8ptrToInt ip = ip.padd(argSize) res template readConstant(ip: var ptr uint8, chunk: Chunk): NdValue = chunk.constants[ip.readDU8()] template frameBottom: int = frames.peek().stackBottom template cclosure: Closure[NdValue] = stack.getIndex(frameBottom()).asClosure() template call(funct: NdValue, argcount: int, error: untyped) = if funct.isFunct(): # create new frame frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip)) ip = funct.asFunct() # jump to the entry point elif funct.isClosure(): frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip)) #frames.add(Frame(stackBottom: stack.high - argcount, returnIp: ip, closure: funct.asClosure())) ip = funct.asClosure().getIp() elif funct.isNative(): var args: seq[NdValue] = newSeq[NdValue](argcount) if argcount > 0: copyMem(args[0].unsafeAddr, stack.getIndexNeg(argcount - 1).addr, argcount * sizeof(NdValue)) stack.deleteTopN(argcount) let res = callNative(funct.asNative(), args, stack.peek()) if not res.ok: runtimeError(res.msg) error else: runtimeError("Attempt to call a non-funct (a defunct?).") # here is a bad defunct joke error proc captureUpvalue(location: ptr NdValue): Upvalue[NdValue] = when debugClosures: write stdout, "CLOSURES - captureUpvalue: " var prev: Upvalue[NdValue] var upvalue = openUpvalues while upvalue != nil and upvalue.location.pgreater(location): prev = upvalue upvalue = upvalue.next # existing upvalue if upvalue != nil and upvalue.location == location: when debugClosures: write stdout, "found existing, returning that.\n" return upvalue # new upvalue when debugClosures: write stdout, "creating new.\n" result = newUpvalue(location) result.next = upvalue if prev == nil: openUpvalues = result else: prev.next = result proc closeUpvalues(last: ptr NdValue) = while openUpvalues != nil and openUpvalues.location.pge(last): let upval = openUpvalues upval.closed = upval.location[] when debugClosures: echo &"CLOSURES - closeUpvalues: closing on {upval.closed}" upval.location = upval.closed.addr openUpvalues = upval.next # initialize globals constructStdlib() for i in 0 .. natives.high(): let native = i.uint32.fromNative() let name = nativeNames[i].fromNimString() discard globals.tableSet(name, native) let globalsKey = "_G".fromNimString() let globalsVal = cast[NdTable[NdValue, NdValue]](globals.addr) discard globals.tableSet(globalsKey, globalsVal.fromTable()) while true: {.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma let ins = ip[].OpCode ip = ip.padd(1) when debugVM: var msg = "" msg &= " Stack: [ " for i in 0 .. stack.high(): let e = stack[i] if i == frameBottom: msg &= &"<{e}> " else: msg &= &"{e} " msg &= "]" echo msg var ii = ip.pdiff(chunk.code[0].unsafeAddr) - 1 var ll = -1 setForegroundColor(fgYellow) disassembleInstruction(chunk, ii, ll) setForegroundColor(fgDefault) write stdout, " " when profileInstructions: let startTime = getMonoTime() runcounts[ins] += 1f case ins: of opPop: discard stack.pop() of opPopA: let amt = ip.readDU8() stack.popn(amt) of opPopSA: let amt = ip.readUI8() stack.popn(amt) of opSwap: stack.swap() of opDup: stack.push(stack.peek()) of opConstant: let val: NdValue = ip.readConstant(chunk) 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: let retval = stack.pop() let oldframe = frames.pop() ip = oldframe.returnIp # remove frame that's over let length = stack.len() - oldframe.stackBottom stack.deleteTopN(length) stack.push(retval) 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 opDefineGlobal: let name = ip.readConstant(chunk) let existed = globals.tableSet(name, stack.pop()) if existed: runtimeError("Attempt to redefine an existing global variable.") break of opGetGlobal: let name = ip.readConstant(chunk) var val: NdValue let existed = globals.tableGet(name, val) if existed: stack.add(val) else: runtimeError(&"Undefined global variable {name}.") break of opSetGlobal: let name = ip.readConstant(chunk) let existed = globals.tableSet(name, stack.peek()) if not existed: runtimeError("Attempt to assign to undefined global variable.") break of opGetLocal: let slot = ip.readDU8() stack.add(stack[slot + frameBottom]) of opSetLocal: let slot = ip.readDU8() stack[slot + frameBottom] = stack.peek() of opJumpIfFalse: let offset = ip.readDU8() if stack.peek().isFalsey(): ip = ip.padd(offset) of opJumpIfFalsePop: let offset = ip.readDU8() if stack.pop().isFalsey(): ip = ip.padd(offset) of opJump: let offset = ip.readDU8() ip = ip.padd(offset) of opLoop: let offset = ip.readDU8() ip = ip.psub(offset) of opFunctionDef: let offset = ip.readDU8() let faddr: ptr uint8 = ip ip = ip.padd(offset) stack.push(faddr.fromFunct()) of opCheckArity: let arity = ip.readUI8() let argcount = stack.high() - frameBottom if arity != argcount: runtimeError(&"Wrong number of arguments, expected {arity}, got {argcount}.") break of opCall: let argcount = ip.readUI8() let funct = stack.getIndexNeg(argcount) funct.call(argcount): break of opCreateList: let listLen = ip.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 = ip.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) # THIS IS NOT NEEDED, pops are done 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 opGetUpvalue: let slot = ip.readDU8() let val = cclosure.get(slot).read() when debugClosures: echo &"CLOSURES - getupvalue got {val} from slot {slot}" stack.push(val) of opSetUpvalue: let slot = ip.readDU8() when debugClosures: echo &"CLOSURES - setupvalue is setting {$stack.peek} to slot {slot}, number of slots: {frames.peek().closure.upvalueCount}" cclosure.get(slot).write(stack.peek()) of opCloseUpvalue: let slot = ip.readDU8() stack[slot + frameBottom].addr.closeUpvalues() of opClosure: let upvalCount = ip.readDU8() let funct = stack.peek() let closure = newClosure[NdValue](funct.asFunct(), upvalCount) stack.settip(closure.fromClosure()) for i in countup(0, upvalCount - 1): let slot = ip.readDU8() let isLocal = ip.readUI8() == 0 if isLocal: let loc = stack[slot + frameBottom].addr when debugClosures: echo &"CLOSURES - opClosure: local upvalue {loc[]} from local slot {slot} to slot {i}" closure.set(i, loc.captureUpvalue()) else: let val = cclosure.get(slot) when debugClosures: echo &"CLOSURES - opClosure: non local upvalue {val.location[]} from slot {slot} to slot {i}" closure.set(i, val) 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