From 88cc276100b83777d3e6f70caf950358d8c9b143 Mon Sep 17 00:00:00 2001 From: prod2 <95874442+prod2@users.noreply.github.com> Date: Fri, 2 Dec 2022 12:15:10 +0100 Subject: [PATCH] cleanup --- chunk.nim | 116 -------- compiler.nim | 762 ----------------------------------------------- config.nim | 32 -- perf.sh | 6 - pointerutils.nim | 9 - scanner.nim | 200 ------------- value.nim | 142 --------- vm.nim | 236 --------------- 8 files changed, 1503 deletions(-) delete mode 100644 chunk.nim delete mode 100644 compiler.nim delete mode 100644 config.nim delete mode 100644 perf.sh delete mode 100644 pointerutils.nim delete mode 100644 scanner.nim delete mode 100644 value.nim delete mode 100644 vm.nim 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() -