diff --git a/chunk.nim b/chunk.nim new file mode 100644 index 0000000..feb0962 --- /dev/null +++ b/chunk.nim @@ -0,0 +1,116 @@ +import strformat +import strutils + +import value + +type + OpCode* = enum + opReturn, opCall, # functions + opPop, # pop + opPrint, # print + opNegate, opNot # unary + opAdd, opSubtract, opMultiply, opDivide, # math + opEqual, opGreater, opLess, # comparison + opTrue, opFalse, opNil, # literal + opConstant, # constant + opDefineGlobal, opGetGlobal, opSetGlobal, # globals (uses constants) + opGetLocal, opSetLocal, # locals + opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop, # jumps + + Chunk* = object + code*: seq[uint8] + constants*: seq[KonValue] + lines*: seq[int] + name*: string # name of the module/chunk/files + + DoubleUint8* = array[2, uint8] + +const argSize* = 2 +const argMax*: int = 256*256 + +proc initChunk*(name: string): Chunk = + Chunk(code: @[], name: name, lines: @[], constants: @[]) + +proc writeChunk*(ch: var Chunk, code: uint8, line: int) = + ch.code.add(code) + ch.lines.add(line) + +proc writeChunk*(ch: var Chunk, code: OpCode, line: int) = + ch.code.add(code.uint8) + ch.lines.add(line) + +proc writeChunk*(ch: var Chunk, code: DoubleUint8, line: int) = + for c in code: + ch.code.add(c) + ch.lines.add(line) + +proc len*(ch: Chunk): int = + ch.code.len + +proc toDU8*(integ: int): DoubleUint8 = + cast[ptr array[2, uint8]](integ.unsafeAddr)[] + +proc toInt*(du8: DoubleUint8): int = + cast[uint16](du8).int + +proc DU8ptrToInt*(du8: ptr uint8): int = + cast[ptr uint16](du8)[].int + +proc addConstant*(ch: var Chunk, constant: KonValue): int = + ch.constants.add(constant) + ch.constants.high + +proc writeConstant*(ch: var Chunk, constant: KonValue, line: int): int = + result = ch.addConstant(constant) + ch.writeChunk(opConstant, line) + ch.writeChunk(result.toDU8, line) + +const simpleInstructions = { + opReturn, + opPop, + opPrint, + opNegate, opNot, + opAdd, opSubtract, opMultiply, opDivide, + opEqual, opGreater, opLess, + opTrue, opFalse, opNil +} +const constantInstructions = { + opConstant, + opDefineGlobal, opGetGlobal, opSetGlobal, +} +const argInstructions = { + opCall, + opGetLocal, opSetLocal, + opJumpIfFalse, opJump, opLoop, opJumpIfFalsePop, +} + + +proc disassembleChunk*(ch: Chunk) = + echo &"== Chunk {ch.name} begin ==" + echo "index line instruction" + var c: int = 0 + var lastLine = -1 + while c < ch.code.len: + template instruction: uint8 = ch.code[c] + template line: int = ch.lines[c] + template double: DoubleUint8 = [ch.code[c+1], ch.code[c+2]] + let cFmt = &"{c:04}" + let lineFmt = if lastLine == line: " | " else: &"{line:04}" + try: + write stdout, &"[{cFmt}] {lineFmt} {instruction.OpCode} ({instruction.toHex(2)}" + case instruction.OpCode: + of simpleInstructions: + write stdout, ")\n" + of argInstructions: + write stdout, &" {double[0].toHex(2)} {double[1].toHex(2)})\n" + c += 2 + of constantInstructions: + let i = double.toInt + write stdout, &" {double[0].toHex(2)} {double[1].toHex(2)})\n" + echo &" points to constant {ch.constants[i]} (i: {i})" + c += 2 + except: + echo &"[{cFmt}] {lineFmt} Unknown opcode {instruction}" + c.inc + echo &"== Chunk {ch.name} end ==" + diff --git a/compiler.nim b/compiler.nim new file mode 100644 index 0000000..a982c56 --- /dev/null +++ b/compiler.nim @@ -0,0 +1,762 @@ +import strformat +import strutils +import sugar +import tables +import options + +import scanner +import chunk +import value +import config + +type + Local = object + name: string # name of this local + index: int # what is its index in the stack (0 is the stack bottom - nil in the main function) + depth: int # depth of this local + # if depth is -1, the variable cannot be referenced yet + # its depth will be set once its first ever value is determined + + Scope = ref object + labels: seq[string] + #depth: int + goalStackIndex: int # the stack count it started with plus 1 + jumps: seq[int] # jumps to be patched that jump to the end + function: bool # if true, it is a function + + Compiler = ref object + # + # input + # + scanner: Scanner + source: string + # + # state + # + previous: Token + current: Token + canAssign: bool + locals: seq[Local] + scopes: seq[Scope] + stackIndex: int # how large the stack is + # when there's an error both are set + # panic mode can be turned off e.g. at block boundaries + panicMode: bool + # + # output + # + chunk*: Chunk + hadError*: bool + + Precedence = enum + pcNone, pcAssignment, pcOr, pcAnd, pcEquality, pcComparison, + pcTerm, pcFactor, pcUnary, pcCall, pcPrimary + # pcUnary applies to all prefix operators regardless of this enum's value + # changing pcUnary's position can change the priority of all unary ops + # + # Note: unary only rules should have precedence pcNone!!! + + ParseRule = object + name: string # debug purposes only + prefix: (Compiler) -> void + infix: (Compiler) -> void + prec: Precedence # only relevant to infix, prefix always has pcUnary + +proc newScope(comp: Compiler, function: bool): Scope = + result.new() + #result.depth = comp.scopes.len + 1 + result.function = function + result.goalStackIndex = comp.stackIndex + 1 + comp.scopes.add(result) + +# HELPERS FOR THE COMPILER TYPE + +proc newCompiler*(name: string, source: string): Compiler = + result = new(Compiler) + result.chunk = initChunk(name) + result.source = source + result.hadError = false + result.panicMode = false + result.canAssign = true + result.locals = @[] + result.scopes = @[] + +proc errorAt(comp: Compiler, line: int, msg: string, at: string = "") = + if comp.panicMode: + return + write stderr, &"[line {line}] Error " + if at.len > 0: + write stderr, &"at {at} " + write stderr, msg + write stderr, "\n" + comp.hadError = true + comp.panicMode = true + +proc error(comp: Compiler, msg: string) = + ## create a simple error message + comp.errorAt(comp.previous.line, msg) + +proc errorAtCurrent(comp: Compiler, msg: string) = + comp.errorAt(comp.current.line, msg) + +proc advance(comp: Compiler) = + comp.previous = comp.current + while true: + comp.current = comp.scanner.scanToken() + when debugScanner: + comp.current.debugPrint() + if (comp.current.tokenType != tkError): + break + comp.errorAtCurrent(comp.current.text) + +proc match(comp: Compiler, tokenType: TokenType): bool = + if comp.current.tokenType == tokenType: + comp.advance() + true + else: + false + +proc consume(comp: Compiler, tokenType: TokenType, msg: string) = + if comp.current.tokenType == tokenType: + comp.advance() + else: + comp.errorAtCurrent(msg) + +proc synchronize(comp: Compiler) = + comp.panicMode = false + while comp.current.tokenType != tkEof: + if comp.previous.tokenType in {tkSemicolon, tkRightBrace}: + return + if comp.current.tokenType in {tkFunct, tkVar, tkFor, tkIf, tkWhile}: + return + comp.advance() + +proc writeChunk(comp: Compiler, dStackIndex: int, ch: OpCode | DoubleUint8) = + comp.stackIndex += dStackIndex + when debugCompiler: + debugEcho &"new stackindex: {comp.stackIndex}, delta: {dStackIndex} due to {ch.repr}" + comp.chunk.writeChunk(ch, comp.previous.line) + +proc writeConstant(comp: Compiler, constant: KonValue) = + comp.stackIndex.inc + let index = comp.chunk.writeConstant(constant, comp.previous.line) + if index >= argMax: + comp.error("Too many constants in one chunk.") + + +proc addLocal(comp: Compiler, name: string, delta: int) = + if comp.locals.len >= argMax: + comp.error("Too many local variables in function.") + + # if delta is 0 or negative, it means that it is already on the stack when addLocal is called + # if delta is positive, the first ever value of the local is to the right + comp.locals.add(Local(name: name, depth: if delta > 0: -1 else: comp.scopes.high, index: comp.stackIndex + delta)) + +proc markInitialized(comp: Compiler) = + comp.locals[comp.locals.high].depth = comp.scopes.high + +# PARSE RULE/PRECEDENCE MISC +proc nop(comp: Compiler) = discard + +var rules: Table[TokenType, ParseRule] +template genRule(ttype: TokenType, tprefix: (Compiler) -> void, tinfix: (Compiler) -> void, tprec: Precedence) = + if tprec == pcUnary: + raise newException(Exception, "pcUnary cannot be used as a rule precedence! Use pcNone for unary-only rules!") + elif tprec == pcPrimary: + raise newException(Exception, "Invalid rule: pcPrimary cannot be used for binary operators, if this rule is for a primary value, use pcNone!") + elif tprec == pcNone and tinfix != nop: + raise newException(Exception, "Invalid rule: pcNone only allowed for unary operators and primary values, not for infix ones!") + rules[ttype] = ParseRule(name: $ttype, prefix: tprefix, infix: tinfix, prec: tprec) + +proc getRule(opType: TokenType): Option[ParseRule] = + if rules.hasKey(opType): + some(rules[opType]) + else: + none(ParseRule) + +proc applyRule(rule: ParseRule): Precedence = + # returns the rule's precedence + rule.prec + +proc increment(prec: Precedence): Precedence = + # increases precedence by one + if prec == pcPrimary: + raise newException(Exception, "Invalid ruletable, pcPrimary precedence increment attempted.") + else: + Precedence(int(prec) + 1) + +proc `<=`(a, b: Precedence): bool = + int(a) <= int(b) + +# JUMP HELPERS + +proc emitJump(comp: Compiler, delta: int, op: OpCode): int = + # delta -> 0 if the jump does not pop + # delta -> -1 if the jump pops the condition from the stack + comp.writeChunk(delta, op) + comp.writeChunk(0, 0xffffff.toDU8) + comp.chunk.len - argSize + +proc patchJump(comp: Compiler, offset: int) = + let jump = (comp.chunk.len - offset - argSize) + + if (jump > argMax): + comp.error("Too much code to jump over.") + + let jumpt = jump.toDU8 + + comp.chunk.code[offset] = jumpt[0] + comp.chunk.code[offset + 1] = jumpt[1] + +proc emitLoop(comp: Compiler, loopstart: int, delta: int, op: OpCode) = + comp.writeChunk(delta, op) + + let offset = comp.chunk.len - loopstart + argSize + if offset > argMax: + comp.error("Loop body too large.") + + comp.writeChunk(0, offset.toDU8) + +# SCOPE HELPERS +proc beginScope(comp: Compiler, function: bool = false) = + let scope = comp.newScope(function) + + when debugCompiler: + debugEcho &"Begin scope called for depth {comp.scopes.len} function? {function}" + + if function: + scope.labels.add("result") + else: + while comp.match(tkLabel): + let label = comp.previous.text[1..^1] + scope.labels.add(label) + + if function: + # if it's a function scope, the frame will move + # access to outside locals is also limited to upvalues and closures + comp.stackIndex = 0 + else: + # only put the opNil if it's not a function scope, since + # function scopes are initialized by the caller + comp.writeChunk(1, opNil) + + comp.addLocal("^", delta = 0) + # note, that later added scope labels will not be accessible through ^ + # so it is better not to assign scope labels after beginScope + for label in scope.labels: + comp.addLocal(&"^{label}", delta = 0) + +proc restore(comp: Compiler, scope: Scope) = + let delta = comp.stackIndex - scope.goalStackIndex + for i in countup(1, delta): + comp.writeChunk(-1, opPop) + when assertionsCompiler: + if not comp.stackIndex == scope.goalStackIndex: + comp.error("Assertion failed in restore") + when debugCompiler: + debugEcho &"Restored scope: delta {delta}" + +proc restoreInFunct(comp: Compiler, scope: Scope) = + when debugCompiler: + var pops = 0 + while comp.stackIndex > 0: + comp.writeChunk(-1, opPop) + when debugCompiler: + inc pops + when assertionsCompiler: + if not comp.stackIndex == 0: + comp.error("Assertion failed in restoreInFunct") + + comp.stackIndex = scope.goalStackIndex + when debugCompiler: + debugEcho &"Restored function scope: delta {pops}; new stackindex: {comp.stackIndex}" + +proc jumpToEnd(comp: Compiler, scope: Scope) = + let delta = comp.stackIndex - scope.goalStackIndex + for i in countup(1, delta): + comp.writeChunk(0, opPop) + let jmp = comp.emitJump(0, opJump) + scope.jumps.add(jmp) + +proc endScope(comp: Compiler) = + # remove locals + let popped = comp.scopes.pop() + let function = popped.function + when debugCompiler: + debugEcho &"End scope called for depth {comp.scopes.len} function? {function}" + if function: + comp.restoreInFunct(popped) + else: + comp.restore(popped) + # patch jumps to after the scope (such jumps from breaks emit the pops before jumping) + for jump in popped.jumps: + comp.patchJump(jump) + if function: + comp.writeChunk(0, opReturn) + + +# EXPRESSIONS +proc parsePrecedence(comp: Compiler, prec: Precedence) = + comp.advance() + let rule = comp.previous.tokenType.getRule + + if rule.isSome and rule.get.prefix != nop: + comp.canAssign = prec <= pcAssignment + + let rule = rule.get + when debugCompiler: + debugEcho &"parsePrecedence call, valid prefix op found, rule used: {rule.name}, precedence: {prec}" + + rule.prefix(comp) + + while comp.current.tokenType.getRule.isSome and + prec <= comp.current.tokenType.getRule.get.prec: + comp.advance() + # checked for isSome in the loop + # since advance moves current to previous + if comp.previous.tokenType.getRule.get.infix == nop: + # should never happen, as having a precedence set + # means that it is a binary op + comp.error("Invalid rule table.") + return + else: + let infixRule = comp.previous.tokenType.getRule.get.infix + infixRule(comp) + else: + comp.error("Expect expression.") + +proc expression(comp: Compiler) = + ## The lowest precedence among the pratt-parsed expressions + + # DO NOT ADD ANYTHING HERE + # only use the pratt table for parsing expressions! + when assertionsVM: + let oldStackIndex = comp.stackIndex + comp.parsePrecedence(pcAssignment) + when assertionsVM: + let diff = comp.stackIndex - oldStackIndex + if diff != 1: + comp.error(&"Assertion failed: expression increased ({oldStackIndex} -> {comp.stackIndex}) the stack index by {diff} (should be 1).") + + +proc number(comp: Compiler) = + # assume the number is already advanced through + let value = comp.previous.text.parseFloat.toKonValue + comp.writeConstant(value) + when debugCompiler: + debugEcho &"Written constant (type: {value.konType}, str repr: {$value}) to chunk" + +tkNumber.genRule(number, nop, pcNone) + +proc expFalse(comp: Compiler) = + comp.writeChunk(1, opFalse) + +tkFalse.genRule(expFalse, nop, pcNone) + +proc expTrue(comp: Compiler) = + comp.writeChunk(1, opTrue) + +tkTrue.genRule(expTrue, nop, pcNone) + +proc expNil(comp: Compiler) = + comp.writeChunk(1, opNil) + +tkNil.genRule(expNil, nop, pcNone) + +proc expString(comp: Compiler) = + let value = comp.previous.text[1..^2].toKonValue() + comp.writeConstant(value) + when debugCompiler: + debugEcho &"Written constant (type: {value.konType}, str repr: {$value}) to chunk" + +tkString.genRule(expString, nop, pcNone) + +proc resolveLocal(comp: Compiler, name: string): int = + ## returns the stack index of the local of the name + var i = comp.locals.high + while i >= 0: + let local = comp.locals[i] + if local.name == name: + if local.depth == -1: + comp.error("Can't read local variable in its own initializer.") + return local.index + i.dec + return -1 + +proc variable(comp: Compiler) = + # named variable + var getOp = opGetGlobal + var setOp = opSetGlobal + + let name = comp.previous.text + + # try resolving local, set arg to the index on the stack + var arg = comp.resolveLocal(name) + + if arg != -1: + # local + getOp = opGetLocal + setOp = opSetLocal + else: + # global + arg = comp.chunk.addConstant(name.toKonValue) + + if comp.match(tkEqual): + # assignment (global/local) + if not comp.canAssign: + comp.error("Invalid assignment target.") + return + comp.expression() + comp.writeChunk(0, setOp) + else: + # get (global/local) + comp.writeChunk(1, getOp) + + comp.writeChunk(0, arg.toDU8) + +tkIdentifier.genRule(variable, nop, pcNone) + +proc grouping(comp: Compiler) = + # assume initial '(' is already consumed + comp.expression() + comp.consume(tkRightParen, "Expect ')' after expression.") + + +proc parseCall(comp: Compiler) = + # ( consumed + + # create the call env + # current stack before opCall: + # ... + # opCall converts it to this + # ... + + var argcount = 0 + # put args on stack + while comp.current.tokenType notin {tkRightParen, tkEof}: + comp.expression() + inc argcount + if comp.current.tokenType != tkRightParen: + comp.consume(tkComma, "Expected ',' between arguments in function calls.") + comp.consume(tkRightParen, "Expected ')' after arguments in function calls.") + + # emit call + comp.writeChunk(-argcount, opCall) + comp.writeChunk(0, argcount.toDU8) + + +tkLeftParen.genRule(grouping, parseCall, pcCall) + +proc unary(comp: Compiler) = + let opType = comp.previous.tokenType + + comp.parsePrecedence(pcUnary) + + case opType: + of tkMinus: + comp.writeChunk(0, opNegate) + of tkBang: + comp.writeChunk(0, opNot) + else: + discard # unreachable + +tkBang.genRule(unary, nop, pcNone) + +proc binary(comp: Compiler) = + let opType = comp.previous.tokenType + # safety checked in parsePrecedence + let rule = opType.getRule.get + comp.parsePrecedence(rule.applyRule.increment) + + case opType: + of tkPlus: + comp.writeChunk(-1, opAdd) + of tkMinus: + comp.writeChunk(-1, opSubtract) + of tkStar: + comp.writeChunk(-1, opMultiply) + of tkSlash: + comp.writeChunk(-1, opDivide) + of tkEqualEqual: + comp.writeChunk(-1, opEqual) + of tkBangEqual: + comp.writeChunk(-1, opEqual) + comp.writeChunk(0, opNot) + of tkGreater: + comp.writeChunk(-1, opGreater) + of tkLess: + comp.writeChunk(-1, opLess) + of tkGreaterEqual: + comp.writeChunk(-1, opLess) + comp.writeChunk(0, opNot) + of tkLessEqual: + comp.writeChunk(-1, opGreater) + comp.writeChunk(0, opNot) + else: + return # unreachable + +tkMinus.genRule(unary, binary, pcTerm) +tkPlus.genRule(nop, binary, pcTerm) +tkSlash.genRule(nop, binary, pcFactor) +tkStar.genRule(nop, binary, pcFactor) + +tkEqualEqual.genRule(nop, binary, pcEquality) +tkBangEqual.genRule(nop, binary, pcEquality) +tkGreater.genRule(nop, binary, pcComparison) +tkGreaterEqual.genRule(nop, binary, pcComparison) +tkLess.genRule(nop, binary, pcComparison) +tkLessEqual.genRule(nop, binary, pcComparison) + +proc ifExpr(comp: Compiler) = + # if expressions return the body if condition is truthy, + # the else expression otherwise, unless there is no else: + # if there is no else, it returns the condition if it is falsey + comp.consume(tkLeftParen, "Expect '(' after 'if'.") + comp.expression() + comp.consume(tkRightParen, "Expect ')' after condition.") + + let thenJump = comp.emitJump(0, opJumpIfFalse) + + # conditional code that can be jumped over must leave the stack in tact! + + comp.writeChunk(-1, opPop) + comp.expression() + # net change to stack: -1 + 1 = 0 + + let elseJump = comp.emitJump(0, opJump) + comp.patchJump(thenJump) + + if comp.match(tkElse): + comp.writeChunk(-1, opPop) + comp.expression() + + comp.patchJump(elseJump) + + +tkIf.genRule(ifExpr, nop, pcNone) + +proc andExpr(comp: Compiler) = + let endJump = comp.emitJump(0, opJumpIfFalse) + + comp.writeChunk(-1, opPop) + comp.parsePrecedence(pcAnd) + # net effect on stack: -1 + 1 = 0 + + comp.patchJump(endJump) + +tkAnd.genRule(nop, andExpr, pcAnd) + +proc orExpr(comp: Compiler) = + let elseJump = comp.emitJump(0, opJumpIfFalse) + let endJump = comp.emitJump(0, opJump) + + comp.patchJump(elseJump) + + comp.writeChunk(-1, opPop) + comp.parsePrecedence(pcOr) + # net effect on stack: -1 + 1 = 0 + + comp.patchJump(endJump) + +tkOr.genRule(nop, orExpr, pcOr) + +proc debugExpr(comp: Compiler) = + comp.expression() + when debugCompiler: + debugEcho &"debug expression, current stackindex: {comp.stackIndex}" + comp.writeChunk(0, opPrint) + +tkPrint.genRule(debugExpr, nop, pcNone) + +proc parseWhile(comp: Compiler) = + + comp.writeChunk(1, opNil) # return value + let loopStart = comp.chunk.len + comp.consume(tkLeftParen, "Expect '(' after 'while'.") + # condition + comp.expression() + comp.consume(tkRightParen, "Expect ')' after condition.") + + let exitJump = comp.emitJump(-1, opJumpIfFalsePop) + # this cannot be handled with just opPop, since the net change in the + # stack size inside code that is conditional must be 0! + + # body + comp.writeChunk(-1, opPop) # pop the old return value + comp.expression() + # net stack change: 1 + -1 = 0 + + comp.emitLoop(loopstart = loopStart, delta = 0, op = opLoop) + comp.patchJump(exitJump) + + +tkWhile.genRule(parseWhile, nop, pcNone) + +proc parseFunct(comp: Compiler) = + + # jump over + let jumpOverBody = comp.emitJump(0, opJump) + + comp.consume(tkLeftParen, "Expected '(' after keyword 'funct'.") + + var params: seq[string] + # parameters + while comp.current.tokenType == tkIdentifier: + comp.advance() + params.add(comp.previous.text) + if comp.current.tokenType == tkRightParen: + break + comp.consume(tkComma, "Expected ',' to separate items in the parameter list.") + + comp.consume(tkRightParen, "Expected ')' after parameter list.") + + # function body: + let functII = comp.chunk.len + comp.beginScope(function = true) # this saves the old stackindex, sets it to 0, points ^ and ^function at index 0 + # assumption: + # the caller will create the following stack for the function to run in: + # [0] = return value placeholder + # [1] = arg #1 + # [2] = arg #2 + # [3] = arg #3 + + for i in countup(1, params.len): + comp.stackIndex = i + comp.addLocal(params[i-1], 0) + comp.expression() + when assertionsCompiler: + let shouldbeStackIndex = params.len + 1 + if shouldbeStackIndex != comp.stackIndex: + comp.error(&"Assertion failed: wrong stackindex ({comp.stackIndex}) in function declaration (should be {shouldbeStackIndex}).") + comp.endScope() + dec comp.stackIndex # the previous end scope did not put anything on the stack, it is jumped over + + # get konvalue functions: + let konFunct = newKonFunction(functII, params.len) + + # end of function declaration: + comp.patchJump(jumpOverBody) + # put the fn object on the stack + comp.writeConstant(konFunct) + +tkFunct.genRule(parseFunct, nop, pcNone) + + +# below are the expressions that can contain statements in some way +# the only expressions that can contain a statement are: + # the block expression +proc statement(comp: Compiler) + +proc parseBlock(comp: Compiler) = + ## Despite the name, can be used for statements if the arg statement is true + ## Also can be used for function bodies + comp.beginScope() + while comp.current.tokenType != tkRightBrace and comp.current.tokenType != tkEof: + comp.statement() + comp.endScope() + + comp.consume(tkRightBrace, "Expect '}' after block.") + +tkLeftBrace.genRule(parseBlock, nop, pcNone) + +# statements + +proc parseVariable(comp: Compiler, msg: string): int = + ## Parses variable declarations + ## During manipulation with variables: + ## if global: + ## consume the identifier and return index to work with + ## if local: + ## register the name with the vm + comp.consume(tkIdentifier, msg) + let name = comp.previous.text + + if name[0] in {'^'}: + comp.error("Illegal variable name.") + + if comp.scopes.len > 0: # declareVariable + # local + + # check if name exists already within scope + for i in countdown(comp.locals.high, 0): + let local = comp.locals[i] + if local.depth != -1 and local.depth < comp.scopes.len: + break + + if name == local.name: + comp.error("Already a variable with this name in this scope.") + break + + comp.addLocal(name, 1) + 0 # index to the constant is irrelevant if the var is local + else: + # global + comp.chunk.addConstant(name.toKonValue()) + +proc defineVariable(comp: Compiler, index: int) = + ## Generate code that moves the variable on the stack + ## to a variable at the right place in memory + ## the right place is defined by the 3 following byte after the op + ## the thing to move is the item below it + + if comp.scopes.len > 0: + # local variable: it's already on the right place + # but we need to mark initialized + comp.markInitialized() + else: + comp.writeChunk(-1, opDefineGlobal) + comp.writeChunk(0, index.toDU8) + +proc varStatement(comp: Compiler) = + let globalIndex = comp.parseVariable("Expect variable name.") + + if comp.match(tkEqual): + comp.expression() + else: + comp.writeChunk(1, opNil) + + comp.defineVariable(globalIndex) + + +proc breakStatement(comp: Compiler) = + if not comp.match(tkLabel): + comp.error("Label expected after break.") + + let label = comp.previous.text[1..^1] + for i in countdown(comp.scopes.high, 0): + let scope = comp.scopes[i] + if scope.labels.contains(label): + comp.jumpToEnd(scope) + break + + comp.consume(tkSemicolon, "Semicolon expected after break statement.") + if comp.current.tokenType != tkRightBrace: + comp.error("Break statement must be the last element inside the innermost block it is in.") + +proc statement(comp: Compiler) = + if comp.match(tkVar): + comp.varStatement() + comp.consume(tkSemicolon, "Semicolon expected after expression statement.") + elif comp.match(tkBreak): + comp.breakStatement() + else: + comp.expression() + comp.writeChunk(-1, opPop) + comp.consume(tkSemicolon, "Semicolon expected after expression statement.") + + if comp.panicMode: + comp.synchronize() + +proc compile*(comp: Compiler) = + comp.scanner = newScanner(comp.source) + comp.writeChunk(0, opNil) + # the starting stackIndex is 0, which points to this nil + # it is correctly set to delta = 0!!! + comp.advance() + while comp.current.tokenType != tkEof: + comp.statement() + comp.writeChunk(-1, opPop) + comp.writeChunk(0, opReturn) + when debugDumpChunk: + if not comp.hadError: + comp.chunk.disassembleChunk() diff --git a/config.nim b/config.nim new file mode 100644 index 0000000..d0a49c7 --- /dev/null +++ b/config.nim @@ -0,0 +1,32 @@ +type + LineEditorChoice = enum + leBasic, leRdstdin + ReadlineInterruptedException* = object of CatchableError + +# choose debug options here +const debugVM* = false +const debugScanner* = false +const debugCompiler* = false +const debugDumpChunk* = false +const assertionsVM* = false # sanity checks in the VM, such as the stack being empty at the end +const assertionsCompiler* = false # sanity checks in the compiler +const profileInstructions* = false # if true, the time spent on every opcode is measured + +# choose a line editor for the repl +const lineEditor = leRdstdin + +when lineEditor == leRdstdin: + import rdstdin + +proc konLineEditor*: string = + proc ctrlc = + raise newException(ReadlineInterruptedException, "Ctrl+C/D pressed.") + when lineEditor == leBasic: + write stdout, "\r-> " + result = stdin.readLine() + when lineEditor == leRdstdin: + var line: string + let ok = readLineFromStdin("-> ", line) + if not ok: + ctrlc() + return line \ No newline at end of file diff --git a/main b/main new file mode 100644 index 0000000..da1af5c Binary files /dev/null and b/main differ diff --git a/main.nim b/main.nim new file mode 100644 index 0000000..2f2b992 --- /dev/null +++ b/main.nim @@ -0,0 +1,52 @@ +import vm +import compiler +import os +import config + +type Result = enum + rsOK, rsCompileError, rsRuntimeError + +proc interpret(name: string, source: string): Result = + let compiler = newCompiler(name, source) + compiler.compile() + if compiler.hadError: + return rsCompileError + let vm = newVM(compiler.chunk) + case vm.run(): + of irOK: + rsOK + of irRuntimeError: + rsRuntimeError + + +proc repl = + while true: + try: + let line = konLineEditor() + if line.len > 0: + discard interpret("repl", line) + except ReadlineInterruptedException: + break + +proc runFile(path: string) = + case interpret(path, readFile(path)): + of rsCompileError: + quit 65 + of rsRuntimeError: + quit 70 + of rsOK: + quit 0 + +const hardcodedPath* = "" + +if paramCount() == 0: + if hardcodedPath == "": + repl() + else: + runFile(hardcodedPath) +elif paramCount() == 1: + runFile(paramStr(1)) +else: + echo "Maximum param count is 1" + quit 1 + diff --git a/perf.sh b/perf.sh old mode 100755 new mode 100644 diff --git a/pointerutils.nim b/pointerutils.nim new file mode 100644 index 0000000..8f37506 --- /dev/null +++ b/pointerutils.nim @@ -0,0 +1,9 @@ + +template padd*[T](x: ptr T, num: int): ptr T = + cast[ptr T](cast[int](x) + num) + +template psub*[T](x: ptr T, num: int): ptr T = + cast[ptr T](cast[int](x) - num) + +template pdiff*[T](x: ptr T, y: ptr): int = + cast[int](x) - cast[int](y) \ No newline at end of file diff --git a/scanner.nim b/scanner.nim new file mode 100644 index 0000000..db56c87 --- /dev/null +++ b/scanner.nim @@ -0,0 +1,200 @@ +import strutils +import tables +import strformat + +type + Scanner* = ref object + start: int + current: int + line: int + source: string + + TokenType* = enum + tkNone, # the default tokentype, if encountered anywhere, erroring out is the best course of action + tkLeftParen, tkRightParen, tkLeftBrace, tkRightBrace, tkComma, tkDot, + tkMinus, tkPlus, tkSemicolon, tkSlash, tkStar, tkBang, tkBangEqual, + tkGreater, tkGreaterEqual, tkLess, tkLessEqual, tkEqual, tkEqualEqual, + tkIdentifier, tkString, + tkNumber, tkAnd, tkElse, tkFalse, tkFor, tkFunct, tkGoto, tkIf, tkNil, + tkOr, tkPrint, tkLabel, tkBreak, tkTrue, tkVar, tkWhile, + tkError, tkEof + + Token* = object + tokenType*: TokenType + text*: string + line*: int + +proc debugPrint*(token: Token) = + write stdout, &"Token of type {$token.tokenType} [{token.text}] at line {$token.line}\n" + +proc isAtEnd(scanner: Scanner): bool = + scanner.current > scanner.source.high + +proc advance(scanner: Scanner): char = + scanner.current.inc + scanner.source[scanner.current - 1] + +proc peek(scanner: Scanner): char = + if scanner.isAtEnd(): + '\0' + else: + scanner.source[scanner.current] + +proc peekNext(scanner: Scanner): char = + if scanner.current < scanner.source.high: + scanner.source[scanner.current + 1] + else: + '\0' + +proc match(scanner: Scanner, exp: char): bool = + if scanner.peek() == exp: + discard scanner.advance() + true + else: + false + +proc newScanner*(source: string): Scanner = + Scanner(source: source, line: 0, current: 0) + +proc makeToken(scanner: Scanner, tokenType: TokenType): Token = + result.tokenType = tokenType + result.text = scanner.source[scanner.start..scanner.current-1] + result.line = scanner.line + +proc errorToken(scanner: Scanner, msg: string): Token = + result.tokenType = tkError + result.text = msg + result.line = scanner.line + +proc skipWhitespace(scanner: Scanner) = + while true: + let c = scanner.peek() + case c: + of {' ', '\r', '\t'}: + discard scanner.advance() + of '\n': + scanner.line.inc + discard scanner.advance() + of '/': + if scanner.peekNext() == '/': + while scanner.peek != '\n' and not scanner.isAtEnd(): + discard scanner.advance() + else: + return + else: + return + +proc scanString(scanner: Scanner): Token = + while scanner.peek() != '\"' and not scanner.isAtEnd(): + if scanner.peek() == '\n': + scanner.line.inc + discard scanner.advance() + + if scanner.isAtEnd(): + return scanner.errorToken("Unterminated string.") + + discard scanner.advance() + scanner.makeToken(tkString) + +proc scanNumber(scanner: Scanner): Token = + while scanner.peek() in Digits: + discard scanner.advance() + + if scanner.peek() == '.' and scanner.peekNext() in Digits: + discard scanner.advance() + while scanner.peek() in Digits: + discard scanner.advance() + + return scanner.makeToken(tkNumber) + +const keywords = { + "and": tkAnd, + "break": tkBreak, + "else": tkElse, + "false": tkFalse, + "for": tkFor, + "funct": tkFunct, + # here's a language that uses funct... still waiting for the day when a good de-funct joke comes to my mind that I can abuse + "goto": tkGoto, + "if": tkIf, + "nil": tkNil, + "or": tkOr, + "print": tkPrint, + "true": tkTrue, + "var": tkVar, + "while": tkWhile +}.toTable + +proc canStartIdent(chr: char): bool = + chr in Letters or chr in {'_', '^'} + +proc canContIdent(chr: char): bool = + canStartIdent(chr) or chr in Digits + +proc scanIdentifier(scanner: Scanner): Token = + while scanner.peek.canContIdent: + discard scanner.advance() + + let text = scanner.source[scanner.start..scanner.current-1] + + if keywords.hasKey(text): + return scanner.makeToken(keywords[text]) + + return scanner.makeToken(tkIdentifier) + +proc canContLabel(chr: char): bool = + chr in Letters or chr == '_' + +proc scanLabel(scanner: Scanner): Token = + if not scanner.peek.canContLabel: + return scanner.errorToken("Labels must only contain letters and underscores.") + + while scanner.peek.canContLabel: + discard scanner.advance() + + return scanner.makeToken(tkLabel) + +proc scanToken*(scanner: Scanner): Token = + + scanner.skipWhitespace() + scanner.start = scanner.current + + if scanner.isAtEnd(): + return scanner.makeToken(tkEof) + + let c = scanner.advance() + + case c: + of '(': return scanner.makeToken(tkLeftParen) + of ')': return scanner.makeToken(tkRightParen) + of '{': return scanner.makeToken(tkLeftBrace) + of '}': return scanner.makeToken(tkRightBrace) + of ';': return scanner.makeToken(tkSemicolon) + of ',': return scanner.makeToken(tkComma) + of '.': return scanner.makeToken(tkDot) + of '-': return scanner.makeToken(tkMinus) + of '+': return scanner.makeToken(tkPlus) + of '/': return scanner.makeToken(tkSlash) + of '*': return scanner.makeToken(tkStar) + of '!': + return if scanner.match('='): scanner.makeToken(tkBangEqual) else: scanner.makeToken(tkBang) + + of '=': + return if scanner.match('='): scanner.makeToken(tkEqualEqual) else: scanner.makeToken(tkEqual) + + of '<': + return if scanner.match('='): scanner.makeToken(tkLessEqual) else: scanner.makeToken(tkLess) + + of '>': + return if scanner.match('='): scanner.makeToken(tkGreaterEqual) else: scanner.makeToken(tkGreater) + of '\"': + return scanner.scanString() + of Digits: + return scanner.scanNumber() + of '@': + return scanner.scanLabel() + else: + if c.canStartIdent(): + return scanner.scanIdentifier() + else: + return scanner.errorToken("Unexpected character.") diff --git a/src/ndspkg/compv2/compiler.nim b/src/ndspkg/compv2/compiler.nim new file mode 100644 index 0000000..ac92460 --- /dev/null +++ b/src/ndspkg/compv2/compiler.nim @@ -0,0 +1,22 @@ +import ../scanner +import ../chunk +import ../config + +import parser/parser +import emitter/emitter + +proc compile*(source: string): CompileResult = + result = CompileResult(ok: false) + let scanner = newScanner(source) + let parser = newParser(scanner) + let nodeRoot = parser.parse() + if parser.hadError: + return result + let emitter = newEmitter(nodeRoot) + let chunk = emitter.emit() + if emitter.hadError: + return result + when debugDumpChunk: + if not emitter.hadError: + chunk.disassembleChunk() + return CompileResult(ok: true, chunk: chunk) \ No newline at end of file diff --git a/src/ndspkg/compv2/emitter/emitter.nim b/src/ndspkg/compv2/emitter/emitter.nim new file mode 100644 index 0000000..e69de29 diff --git a/src/ndspkg/compv2/parser/parser.nim b/src/ndspkg/compv2/parser/parser.nim new file mode 100644 index 0000000..0abed99 --- /dev/null +++ b/src/ndspkg/compv2/parser/parser.nim @@ -0,0 +1,14 @@ +import ../types +import ../../scanner + +import utils +import statements + +proc newParser*(sc: Scanner): Parser = + result.new() + result.scanner = sc + +proc parse*(par: Parser): Node = + result = Node(kind: nkRoot) + while par.current.tokenType != tkEof: + result.children.add(par.statement()) \ No newline at end of file diff --git a/src/ndspkg/compv2/parser/statement.nim b/src/ndspkg/compv2/parser/statement.nim new file mode 100644 index 0000000..1f7185a --- /dev/null +++ b/src/ndspkg/compv2/parser/statement.nim @@ -0,0 +1,45 @@ +import ../types +import utils + +# below are the expressions that can contain statements in some way +# the only expressions that can contain a statement are: + # the block expression +proc statement*(par: Parser) + +proc parseBlock(par: Parser): Node = + ## Despite the name, can be used for statements if the arg statement is true + ## Also can be used for function bodies + result = Node(kind: nkBlockExpr) + while par.current.tokenType != tkRightBrace and par.current.tokenType != tkEof: + result.children.add(par.statement()) + + par.consume(tkRightBrace, "Expect '}' after block.") + +# statements + +proc breakStatement(par: Parser): Node = + if not par.match(tkLabel): + par.error("Label expected after break.") + + let label = comp.previous.text[1..^1] + result = Node(kind: nkBreak, label: label) + + par.consume(tkSemicolon, "Semicolon expected after break statement.") + if par.current.tokenType != tkRightBrace: + par.error("Break statement must be the last element inside the innermost block it is in.") + +proc statement*(par: Parser): Node = + if par.match(tkVar): + result = par.varStatement() + par.consume(tkSemicolon, "Semicolon expected after expression statement.") + elif par.match(tkDef): + result = par.procStatement() + par.consume(tkSemicolon, "Semicolon expected after procedure declaration.") + elif par.match(tkBreak): + result = par.breakStatement() + else: + result = Node(kind: nkExprStmt, expression: par.expression()) + par.consume(tkSemicolon, "Semicolon expected after expression statement.") + + if par.panicMode: + par.synchronize() \ No newline at end of file diff --git a/src/ndspkg/compv2/parser/utils.nim b/src/ndspkg/compv2/parser/utils.nim new file mode 100644 index 0000000..3566d1d --- /dev/null +++ b/src/ndspkg/compv2/parser/utils.nim @@ -0,0 +1,52 @@ +import ../types +import ../../config + +proc errorAt*(par: Parser, line: int, msg: string, at: string = "") = + if par.panicMode: + return + write stderr, &"[line {line}] Error " + if at.len > 0: + write stderr, &"at {at} " + write stderr, msg + write stderr, "\n" + par.hadError = true + par.panicMode = true + +proc error*(par: Parser, msg: string) = + ## create a simple error message + par.errorAt(par.previous.line, msg, par.previous.text) + +proc errorAtCurrent*(par: Parser, msg: string, supress: bool = false) = + par.errorAt(par.current.line, msg, if supress: "" else: par.current.text) + +proc advance*(par: Parser) = + par.previous = par.current + while true: + par.current = par.scanner.scanToken() + when debugScanner: + par.current.debugPrint() + if (par.current.tokenType != tkError): + break + par.errorAtCurrent(par.current.text, true) + +proc match*(par: Parser, tokenType: TokenType): bool = + if par.current.tokenType == tokenType: + par.advance() + true + else: + false + +proc consume*(par: Parser, tokenType: TokenType, msg: string) = + if par.current.tokenType == tokenType: + par.advance() + else: + par.errorAtCurrent(msg) + +proc synchronize*(par: Parser) = + par.panicMode = false + while par.current.tokenType != tkEof: + if par.previous.tokenType in {tkSemicolon, tkRightBrace}: + return + if par.current.tokenType in {tkFunct, tkVar, tkFor, tkIf, tkWhile}: + return + par.advance() diff --git a/src/ndspkg/compv2/types.nim b/src/ndspkg/compv2/types.nim new file mode 100644 index 0000000..330554d --- /dev/null +++ b/src/ndspkg/compv2/types.nim @@ -0,0 +1,35 @@ +import ../scanner +import ../chunk +import ../types/value + +type + Parser* = ref object + scanner*: Scanner + current*: Token + previous*: Token + hadError*: bool + panicMode*: bool + + NodeKind* = enum + nkBlockExpr, nkRoot, + nkExprStmt, + nkBreak, + nkConst + + Node* = ref object + case kind*: NodeKind: + of nkBlockExpr, nkRoot: + children*: seq[Node] + of nkExprStmt: + expression*: Node + of nkBreak: + label*: string + of nkConst: + constant*: NdValue + + CompileResult* = object + case ok*: bool: + of true: + chunk*: Chunk + of false: + discard diff --git a/value.nim b/value.nim new file mode 100644 index 0000000..8f67f54 --- /dev/null +++ b/value.nim @@ -0,0 +1,142 @@ +import strformat + +type + KonType* = enum + ktNil, ktBool, ktFloat, ktString, + ktFunct, + ktTypeError, + +const errorTypes = {ktTypeError} + +type + KonValue* = object + case konType*: KonType: + of ktNil: + discard + of ktBool: + boolValue*: bool + of ktFloat: + floatValue*: float64 + of ktString: + stringValue*: string + of ktFunct: + module*: string + entryII*: int # entry instruction index + arity*: int # number of arguments + of errorTypes: + message*: string + +# KON VALUE HELPERS, MUST BE DEFINED FOR EVERY KONVALUE + +proc `$`*(val: KonValue): string = + case val.konType: + of ktFloat: + return $val.floatValue + of ktBool: + return $val.boolValue + of ktNil: + return "nil" + of ktString: + return val.stringValue + of ktFunct: + return &"Function object: {val.entryII}" + of errorTypes: + let ename = $val.konType + return &"{ename[2..^1]}: {val.message}" + +proc isError*(val: KonValue): bool = + val.konType in errorTypes + +proc isFalsey*(val: KonValue): bool = + val.konType in {ktNil} or (val.konType == ktBool and not val.boolValue) + +template isTruthy*(val: KonValue): bool = + not isFalsey(val) + +proc equal*(val, right: KonValue): bool = + if val.konType != right.konType: + false + else: + case val.konType: + of ktFloat: + val.floatValue == right.floatValue + of ktBool: + val.boolValue == right.boolValue + of ktNil: + true + of ktString: + val.stringValue == right.stringValue + of ktFunct: + val.entryII == right.entryII + # same entry II/module but diff arity is a bug + of errorTypes: + false # error comparison is undefined + +# NIM VALUE TO KON VALUE WRAPPERS + +proc toKonValue*(val: float): KonValue = + KonValue(konType: ktFloat, floatValue: val) + +proc toKonValue*(val: bool): KonValue = + KonValue(konType: ktBool, boolValue: val) + +proc toKonValue*(val: string): KonValue = + KonValue(konType: ktString, stringValue: val) + +proc newKonFunction*(ii: int, arity: int): KonValue = + KonValue(konType: ktFunct, entryII: ii, arity: arity) + +proc toKonValue*: KonValue = + KonValue(konType: ktNil) + +proc konTypeError*(msg: string): KonValue = + KonValue(konType: ktTypeError, message: msg) + +# OPERATIONS +# NOTE: these operations can return ktTypeError with a message if types are invalid + +proc negate*(val: KonValue): KonValue = + if (val.konType != ktFloat): + return konTypeError("Operand must be a number.") + else: + return toKonValue(-val.floatValue) + +proc add*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue + right.floatValue) + elif val.konType == ktString and right.konType == ktString: + return toKonValue(val.stringValue & right.stringValue) + else: + return konTypeError(&"Attempt to add types {val.konType} and {right.konType}.") + +proc subtract*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue - right.floatValue) + else: + return konTypeError(&"Attempt to subtract types {val.konType} and {right.konType}.") + +proc multiply*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue * right.floatValue) + else: + return konTypeError(&"Attempt to multiply types {val.konType} and {right.konType}.") + + +proc divide*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue / right.floatValue) + else: + return konTypeError(&"Attempt to divide types {val.konType} and {right.konType}.") + +proc `<`*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue < right.floatValue) + else: + return konTypeError(&"Attempt to compare types {val.konType} and {right.konType}.") + + +proc `>`*(val: KonValue, right: KonValue): KonValue = + if val.konType == ktFloat and right.konType == ktFloat: + return toKonValue(val.floatValue > right.floatValue) + else: + return konTypeError(&"Attempt to compare types {val.konType} and {right.konType}.") \ No newline at end of file diff --git a/vm.nim b/vm.nim new file mode 100644 index 0000000..181e261 --- /dev/null +++ b/vm.nim @@ -0,0 +1,236 @@ +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 + + VM* = ref object + chunk: Chunk + ip: ptr uint8 + stack: seq[KonValue] + hadError: bool + globals: Table[string, KonValue] + frames: seq[Frame] + + InterpretResult* = enum + irOK, irRuntimeError + + +proc newVM*(ch: Chunk): VM = + result = VM(chunk: ch, stack: newSeqOfCap[KonValue](256), frames: newSeqOfCap[Frame](4)) + result.ip = ch.code[0].unsafeAddr + +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) = + let ii = vm.ip.pdiff(vm.chunk.code[0].unsafeAddr) + let line = vm.chunk.lines[ii] + write stderr, &"[line: {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 = + result = vm.ip[] + vm.ip = vm.ip.padd(1) + +proc readDU8(vm: VM): int = + result = vm.ip.DU8ptrToInt + vm.ip = vm.ip.padd(argSize) + +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 + +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*(vm: VM): InterpretResult = + + template frameBottom: int = vm.frames[vm.frames.high].stackBottom + while true: + {.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma + + 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 + + when profileInstructions: + let startTime = getMonoTime() + + case ins: + of opPop: + discard vm.pop() + 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 vm.frames.len == 0: + break + else: + vm.ip = vm.frames.pop().returnIp # remove frame that's over + 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 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.ip = vm.ip.padd(offset) + of opJumpIfFalsePop: + let offset = vm.readDU8() + if vm.pop.isFalsey: + vm.ip = vm.ip.padd(offset) + of opJump: + let offset = vm.readDU8() + vm.ip = vm.ip.padd(offset) + of opLoop: + let offset = vm.readDU8() + vm.ip = vm.ip.psub(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 + + # create new frame + vm.frames.add(Frame(stackBottom: vm.stack.high - argcount, returnIp: vm.ip)) + + vm.ip = vm.chunk.code[0].unsafeAddr.padd(funct.entryII) # jump to the entry point + + when profileInstructions: + durations[ins] += getMonoTime() - startTime + + 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.") + + when profileInstructions: + for op in OpCode: + let dur = durations[op].inMilliseconds + echo &"OpCode: {op} total duration {dur} ms" + + if vm.hadError: + irRuntimeError + else: + irOK + +proc interpretChunk*(ch: Chunk): InterpretResult = + let vm = newVM(ch) + return vm.run() +