diff --git a/chunk.nim b/chunk.nim deleted file mode 100644 index feb0962..0000000 --- a/chunk.nim +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index a982c56..0000000 --- a/compiler.nim +++ /dev/null @@ -1,762 +0,0 @@ -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 deleted file mode 100644 index d0a49c7..0000000 --- a/config.nim +++ /dev/null @@ -1,32 +0,0 @@ -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/perf.sh b/perf.sh deleted file mode 100644 index 585c7cc..0000000 --- a/perf.sh +++ /dev/null @@ -1,6 +0,0 @@ -# change this to the path to nds and to the path to the program profiled -sudo perf record -g -F 999 /home/user/Projects/nondescript/bin/nds benchmarks/fib.nds - -sudo perf script -F +pid > test.perf - -# https://profiler.firefox.com/ \ No newline at end of file diff --git a/pointerutils.nim b/pointerutils.nim deleted file mode 100644 index 8f37506..0000000 --- a/pointerutils.nim +++ /dev/null @@ -1,9 +0,0 @@ - -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 deleted file mode 100644 index db56c87..0000000 --- a/scanner.nim +++ /dev/null @@ -1,200 +0,0 @@ -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/value.nim b/value.nim deleted file mode 100644 index 8f67f54..0000000 --- a/value.nim +++ /dev/null @@ -1,142 +0,0 @@ -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 deleted file mode 100644 index 181e261..0000000 --- a/vm.nim +++ /dev/null @@ -1,236 +0,0 @@ -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() -