diff --git a/src/backend/vm.nim b/src/backend/vm.nim index 9ba6763..b77b56a 100644 --- a/src/backend/vm.nim +++ b/src/backend/vm.nim @@ -14,6 +14,7 @@ ## The Peon runtime environment import types +import strformat import ../config import ../frontend/meta/bytecode import ../util/multibyte @@ -24,9 +25,9 @@ type ## The Peon Virtual Machine stack: seq[PeonObject] ip: int # Instruction pointer - sp: int # Stack pointer cache: array[6, PeonObject] # Singletons cache chunk: Chunk # Piece of bytecode to execute + depth: int proc initCache*(self: PeonVM) = @@ -45,11 +46,10 @@ proc newPeonVM*: PeonVM = ## for executing Peon bytecode new(result) result.ip = 0 - result.sp = 0 - result.stack = newSeqOfCap[PeonObject](INITIAL_STACK_SIZE) + result.depth = 0 + result.stack = newSeq[PeonObject]() result.initCache() - for _ in 0..= self.stack.high(): - for _ in 0..self.stack.len(): - self.stack.add(self.getNil()) - self.stack[self.sp] = obj - inc(self.sp) + self.stack.add(obj) proc pop(self: PeonVM): PeonObject = ## Pops a Peon object off the ## stack, decreasing the stack ## pointer. The object is returned - dec(self.sp) - return self.stack[self.sp] + return self.stack.pop() proc peek(self: PeonVM): PeonObject = ## Returns the element at the top ## of the stack without consuming ## it - return self.stack[self.sp] + return self.stack[^1] proc readByte(self: PeonVM): uint8 = @@ -126,7 +121,10 @@ proc readInt64(self: PeonVM, idx: int): PeonObject = ## returns a Peon object. Assumes ## the constant is an Int64 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], - self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] + self.chunk.consts[idx + 2], self.chunk.consts[idx + 3], + self.chunk.consts[idx + 4], self.chunk.consts[idx + 5], + self.chunk.consts[idx + 6], self.chunk.consts[idx + 7], + ] result = PeonObject(kind: Int64) copyMem(result.long.addr, arr.addr, sizeof(arr)) @@ -137,16 +135,36 @@ proc readUInt64(self: PeonVM, idx: int): PeonObject = ## returns a Peon object. Assumes ## the constant is an UInt64 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], - self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] + self.chunk.consts[idx + 2], self.chunk.consts[idx + 3], + self.chunk.consts[idx + 4], self.chunk.consts[idx + 5], + self.chunk.consts[idx + 6], self.chunk.consts[idx + 7], + ] result = PeonObject(kind: UInt64) copyMem(result.uLong.addr, arr.addr, sizeof(arr)) +proc readUInt32(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is an UInt32 + var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], + self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] + result = PeonObject(kind: UInt32) + copyMem(result.uInt.addr, arr.addr, sizeof(arr)) + + proc dispatch*(self: PeonVM) = ## Main bytecode dispatch loop var instruction: OpCode while true: instruction = OpCode(self.readByte()) + when DEBUG_TRACE_VM: + echo &"Stack: {self.stack}" + echo &"PC: {self.ip}" + echo &"SP: {self.stack.high()}" + echo &"Instruction: {instruction}" + discard readLine stdin case instruction: of LoadTrue: self.push(self.getBool(true)) @@ -162,15 +180,25 @@ proc dispatch*(self: PeonVM) = self.push(self.readInt64(int(self.readLong()))) of LoadUInt64: self.push(self.readUInt64(int(self.readLong()))) + of LoadUInt32: + self.push(self.readUInt32(int(self.readLong()))) of OpCode.Return: - # TODO - return + if self.depth == 0: + return + else: + self.ip = int(self.pop().uInt) + of ReturnPop: + var retVal = self.pop() + self.ip = int(self.pop().uInt) + self.push(retVal) of OpCode.LoadVar: self.push(self.stack[self.readLong()]) of NoOp: continue of Pop: discard self.pop() + of Call: + self.ip = int(self.readLong()) of Jump: self.ip = int(self.readShort()) of JumpForwards: @@ -217,6 +245,5 @@ proc dispatch*(self: PeonVM) = proc run*(self: PeonVM, chunk: Chunk) = ## Executes a piece of Peon bytecode. self.chunk = chunk - self.sp = 0 self.ip = 0 self.dispatch() diff --git a/src/config.nim b/src/config.nim index 66d9ca1..8aca4c7 100644 --- a/src/config.nim +++ b/src/config.nim @@ -27,11 +27,10 @@ when len(PEON_COMMIT_HASH) != 40: const PEON_BRANCH* = "master" when len(PEON_BRANCH) > 255: {.fatal: "The git branch name's length must be less than or equal to 255 characters".} -const DEBUG_TRACE_VM* = false # Traces VM execution +const DEBUG_TRACE_VM* = true # Traces VM execution const DEBUG_TRACE_GC* = false # Traces the garbage collector (TODO) const DEBUG_TRACE_ALLOCATION* = false # Traces memory allocation/deallocation const DEBUG_TRACE_COMPILER* = false # Traces the compiler -const INITIAL_STACK_SIZE* = 0 const PEON_VERSION_STRING* = &"Peon {PEON_VERSION.major}.{PEON_VERSION.minor}.{PEON_VERSION.patch} {PEON_RELEASE} ({PEON_BRANCH}, {CompileDate}, {CompileTime}, {PEON_COMMIT_HASH[0..8]}) [Nim {NimVersion}] on {hostOS} ({hostCPU})" const HELP_MESSAGE* = """The peon programming language, Copyright (C) 2022 Mattia Giambirtone & All Contributors diff --git a/src/frontend/compiler.nim b/src/frontend/compiler.nim index f65675b..8534e16 100644 --- a/src/frontend/compiler.nim +++ b/src/frontend/compiler.nim @@ -140,6 +140,7 @@ type closedOver: seq[IdentExpr] + proc newCompiler*(enableOptimizations: bool = true): Compiler = ## Initializes a new Compiler object new(result) @@ -165,7 +166,7 @@ proc inferType(self: Compiler, node: Expression): Type proc findByName(self: Compiler, name: string): seq[Name] proc findByType(self: Compiler, name: string, kind: Type): seq[Name] proc compareTypes(self: Compiler, a, b: Type): bool - +proc patchReturnAddress(self: Compiler, retAddr: int) ## End of forward declarations ## Public getter for nicer error formatting @@ -724,6 +725,22 @@ proc unary(self: Compiler, node: UnaryExpr) = let impl = self.findByType(node.token.lexeme, Type(kind: Function, returnType: valueType, node: nil, args: @[valueType])) if impl.len() == 0: self.error(&"cannot find a suitable implementation for '{node.token.lexeme}'") + elif impl.len() > 2: + var msg = &"multiple matching implementations of '{node.token.lexeme}' found:\n" + for fn in reversed(impl): + var node = FunDecl(fn.valueType.node) + discard self.typeToStr(fn.valueType) + msg &= &"- '{node.name.token.lexeme}' at line {node.token.line} of type {self.typeToStr(fn.valueType)}\n" + self.error(msg) + else: + # Pushes the return address + self.emitByte(LoadUInt32) + # We patch it later! + let idx = self.chunk.consts.len() + self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad())) + self.emitByte(Call) + self.emitBytes(impl[0].codePos.toTriple()) + self.patchReturnAddress(idx) proc binary(self: Compiler, node: BinaryExpr) = @@ -795,7 +812,7 @@ proc declareName(self: Compiler, node: Declaration) = returnType: self.inferType( node.returnType), args: @[]), - codePos: self.chunk.code.len(), + codePos: self.chunk.code.high(), name: node.name, isLet: false)) let fn = self.names[^1] @@ -813,8 +830,6 @@ proc declareName(self: Compiler, node: Declaration) = self.names[^1].valueType = self.inferType(argument.valueType) self.names[^1].valueType.node = argument.name fn.valueType.args.add(self.names[^1].valueType) - self.emitByte(LoadVar) - self.emitBytes(self.names.high().toTriple()) else: discard # Unreachable @@ -839,7 +854,11 @@ proc identifier(self: Compiler, node: IdentExpr) = if not t.closedOver: # Static name resolution, loads value at index in the stack. Very fast. Much wow. self.emitByte(LoadVar) - self.emitBytes(index.toTriple()) + if self.scopeDepth > 0: + # Skips function's return address + self.emitBytes((index - 1).toTriple()) + else: + self.emitBytes(index.toTriple()) else: if self.closedOver.len() == 0: self.error("error: closure variable array is empty but LoadHeap would be emitted (this is an internal error and most likely a bug)") @@ -1277,6 +1296,16 @@ proc funDecl(self: Compiler, node: FunDecl) = self.currentFunction = function +proc patchReturnAddress(self: Compiler, retAddr: int) = + ## Patches the return address of a function + ## call. This is called at each iteration of + ## the compiler's loop + let address = self.chunk.code.len().toQuad() + self.chunk.consts[retAddr] = address[0] + self.chunk.consts[retAddr + 1] = address[1] + self.chunk.consts[retAddr + 2] = address[2] + self.chunk.consts[retAddr + 3] = address[3] + proc declaration(self: Compiler, node: Declaration) = ## Compiles all declarations diff --git a/src/frontend/meta/bytecode.nim b/src/frontend/meta/bytecode.nim index a41b15b..a14d493 100644 --- a/src/frontend/meta/bytecode.nim +++ b/src/frontend/meta/bytecode.nim @@ -147,9 +147,11 @@ const stackTripleInstructions* = {StoreVar, LoadVar, LoadHeap, StoreHeap} # of 16 bit integers const stackDoubleInstructions* = {} -# Argument double argument instructions take hardcoded arguments on the stack as 16 bit integers +# Argument double argument instructions take hardcoded arguments as 16 bit integers const argumentDoubleInstructions* = {PopN, } +# Argument double argument instructions take hardcoded arguments as 24 bit integers +const argumentTripleInstructions* = {Call, } # Jump instructions jump at relative or absolute bytecode offsets const jumpInstructions* = {Jump, LongJump, JumpIfFalse, JumpIfFalsePop, JumpForwards, JumpBackwards, diff --git a/src/main.nim b/src/main.nim index e35fb2e..ba8cbb7 100644 --- a/src/main.nim +++ b/src/main.nim @@ -67,7 +67,6 @@ proc repl = input = editor.read() if input.len() == 0: continue - # Currently the parser doesn't handle these tokens well tokens = tokenizer.lex(input, "stdin") if tokens.len() == 0: continue @@ -168,13 +167,72 @@ proc repl = proc runFile(f: string) = var + tokens: seq[Token] = @[] + tree: seq[Declaration] = @[] + compiled: Chunk + serialized: Serialized tokenizer = newLexer() parser = newParser() compiler = newCompiler() + serializer = newSerializer() vm = newPeonVM() + input: string tokenizer.fillSymbolTable() try: - vm.run(compiler.compile(parser.parse(tokenizer.lex(readFile(f), f), f), f)) + input = readFile(f) + tokens = tokenizer.lex(input, f) + if tokens.len() == 0: + return + when debugLexer: + styledEcho fgCyan, "Tokenization step:" + for i, token in tokens: + if i == tokens.high(): + # Who cares about EOF? + break + styledEcho fgGreen, "\t", $token + echo "" + tree = parser.parse(tokens, f) + if tree.len() == 0: + return + when debugParser: + styledEcho fgCyan, "Parsing step:" + for node in tree: + styledEcho fgGreen, "\t", $node + echo "" + compiled = compiler.compile(tree, f) + when debugCompiler: + styledEcho fgCyan, "Compilation step:" + stdout.styledWrite(fgCyan, "\tRaw byte stream: ", fgGreen, "[", fgYellow, compiled.code.join(", "), fgGreen, "]") + styledEcho fgCyan, "\n\nBytecode disassembler output below:\n" + disassembleChunk(compiled, f) + echo "" + + serializer.dumpFile(compiled, input, f, splitFile(f).name & ".pbc") + serialized = serializer.loadFile(splitFile(f).name & ".pbc") + when debugSerializer: + var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash + styledEcho fgCyan, "Serialization step: " + styledEcho fgBlue, &"\t- File hash: ", fgYellow, serialized.fileHash, fgBlue, " (", if hashMatches: fgGreen else: fgRed, if hashMatches: "OK" else: "Fail", fgBlue, ")" + styledEcho fgBlue, "\t- Peon version: ", fgYellow, &"{serialized.peonVer.major}.{serialized.peonVer.minor}.{serialized.peonVer.patch}", fgBlue, " (commit ", fgYellow, serialized.commitHash[0..8], fgBlue, ") on branch ", fgYellow, serialized.peonBranch + stdout.styledWriteLine(fgBlue, "\t- Compilation date & time: ", fgYellow, fromUnix(serialized.compileDate).format("d/M/yyyy HH:mm:ss")) + stdout.styledWrite(fgBlue, &"\t- Constants segment: ") + if serialized.chunk.consts == compiled.consts: + styledEcho fgGreen, "OK" + else: + styledEcho fgRed, "Corrupted" + stdout.styledWrite(fgBlue, &"\t- Code segment: ") + if serialized.chunk.code == compiled.code: + styledEcho fgGreen, "OK" + else: + styledEcho fgRed, "Corrupted" + stdout.styledWrite(fgBlue, "\t- Line info segment: ") + if serialized.chunk.lines == compiled.lines: + styledEcho fgGreen, "OK" + else: + styledEcho fgRed, "Corrupted" + when debugRuntime: + styledEcho fgCyan, "\n\nExecution step: " + vm.run(serialized.chunk) except LexingError: let exc = LexingError(getCurrentException()) let relPos = tokenizer.getRelPos(exc.line) @@ -218,9 +276,9 @@ proc runFile(f: string) = let exc = SerializationError(getCurrentException()) stderr.styledWriteLine(fgRed, "A fatal error occurred while (de-)serializing", fgYellow, &"'{exc.file}'", fgGreen, ": ", getCurrentExceptionMsg()) except IOError: - stderr.styledWriteLine("An error occurred while trying to read ", fgYellow, &"'{f}'", fgRed, &": {getCurrentExceptionMsg()}") + stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {getCurrentExceptionMsg()}") except OSError: - stderr.styledWriteLine("An error occurred while trying to read ", fgYellow, &"'{f}'", fgRed, &": {osErrorMsg(osLastError())} [errno {osLastError()}]") + stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {osErrorMsg(osLastError())} [errno {osLastError()}]") when isMainModule: diff --git a/src/tests.pn b/src/tests.pn index 0352b29..0ea5253 100644 --- a/src/tests.pn +++ b/src/tests.pn @@ -3,4 +3,3 @@ operator `+`(a: int): int { } +1; # Works: defined for int64 -+1'u8; # No definition for int8: error! diff --git a/src/util/debugger.nim b/src/util/debugger.nim index 9aaf01d..85a38cd 100644 --- a/src/util/debugger.nim +++ b/src/util/debugger.nim @@ -85,6 +85,17 @@ proc argumentDoubleInstruction(instruction: OpCode, chunk: Chunk, offset: int): return offset + 3 +proc argumentTripleInstruction(instruction: OpCode, chunk: Chunk, offset: int): int = + ## Debugs instructions that operate on a hardcoded value on the stack using a 16-bit operand + var slot = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple() + printInstruction(instruction) + stdout.styledWrite(fgGreen, &", has argument ") + setForegroundColor(fgYellow) + stdout.write(&"{slot}") + nl() + return offset + 4 + + proc constantInstruction(instruction: OpCode, chunk: Chunk, offset: int): int = ## Debugs instructions that operate on the constant table var constant = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[ @@ -94,9 +105,8 @@ proc constantInstruction(instruction: OpCode, chunk: Chunk, offset: int): int = setForegroundColor(fgYellow) stdout.write(&"{constant}") nl() - let obj = chunk.consts[constant] printDebug("Operand: ") - stdout.styledWrite(fgYellow, &"{obj}\n") + stdout.styledWriteLine(fgYellow, &"{chunk.consts[constant]}") return offset + 4 @@ -138,6 +148,8 @@ proc disassembleInstruction*(chunk: Chunk, offset: int): int = result = stackTripleInstruction(opcode, chunk, offset) of argumentDoubleInstructions: result = argumentDoubleInstruction(opcode, chunk, offset) + of argumentTripleInstructions: + result = argumentTripleInstruction(opcode, chunk, offset) of jumpInstructions: result = jumpInstruction(opcode, chunk, offset) else: