diff --git a/docs/bytecode.md b/docs/bytecode.md index 0879136..387ef5c 100644 --- a/docs/bytecode.md +++ b/docs/bytecode.md @@ -25,7 +25,6 @@ A peon bytecode file starts with the header, which is structured as follows: - The branch name of the repository the compiler was built from, prepended with its length as a 1 byte integer - The commit hash (encoded as a 40-byte hex-encoded string) in the aforementioned branch from which the compiler was built from (particularly useful in development builds) - An 8-byte UNIX timestamp (with Epoch 0 starting at 1/1/1970 12:00 AM) representing the exact date and time of when the file was generated -- A 32-byte, hex-encoded SHA256 hash of the source file's content, used to track file changes ## Debug information diff --git a/src/backend/types.nim b/src/backend/types.nim index c910c44..4a787d0 100644 --- a/src/backend/types.nim +++ b/src/backend/types.nim @@ -24,6 +24,8 @@ type PeonObject* = object ## A generic Peon object case kind*: ObjectKind: + of String: + str*: string of Bool: boolean*: bool of Inf: @@ -50,5 +52,11 @@ type discard of CustomType: fields*: seq[PeonObject] + of Float32: + halfFloat*: float32 + of Float64: + `float`*: float + of Function: + ip*: uint32 else: discard # TODO diff --git a/src/backend/vm.nim b/src/backend/vm.nim index 27d39ad..8409ad0 100644 --- a/src/backend/vm.nim +++ b/src/backend/vm.nim @@ -26,12 +26,14 @@ export types type PeonVM* = ref object ## The Peon Virtual Machine - stack: seq[PeonObject] - ip: int # Instruction pointer + calls: seq[PeonObject] # Our call stack + operands: seq[PeonObject] # Our operand stack + ip: uint32 # Instruction pointer cache: array[6, PeonObject] # Singletons cache chunk: Chunk # Piece of bytecode to execute frames: seq[int] # Stores the bottom of stack frames - heapVars: seq[PeonObject] # Stores variables that do not have stack semantics + closedOver: seq[PeonObject] # Stores variables that do not have stack semantics + results: seq[PeonObject] # Stores function's results proc initCache*(self: PeonVM) = @@ -51,7 +53,8 @@ proc newPeonVM*: PeonVM = new(result) result.ip = 0 result.frames = @[] - result.stack = newSeq[PeonObject]() + result.calls = newSeq[PeonObject]() + result.operands = newSeq[PeonObject]() result.initCache() @@ -78,38 +81,70 @@ proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5] proc push(self: PeonVM, obj: PeonObject) = ## Pushes a Peon object onto the - ## stack - self.stack.add(obj) + ## operand stack + self.operands.add(obj) proc pop(self: PeonVM): PeonObject = ## Pops a Peon object off the - ## stack, decreasing the stack - ## pointer. The object is returned - return self.stack.pop() + ## operand stack. The object + ## is returned + return self.operands.pop() -proc peek(self: PeonVM): PeonObject = - ## Returns the Peon object at the top - ## of the stack without consuming - ## it - return self.stack[^1] +proc peek(self: PeonVM, distance: int = 0): PeonObject = + ## Returns the Peon object at the + ## given distance from the top of + ## the operand stack without consuming it + return self.operands[self.operands.high() + distance] proc get(self: PeonVM, idx: int): PeonObject = ## Accessor method that abstracts - ## stack indexing through stack + ## indexing the through stack ## frames - return self.stack[idx + self.frames[^1]] + return self.operands[idx + self.frames[^1]] proc set(self: PeonVM, idx: int, val: PeonObject) = ## Setter method that abstracts - ## stack indexing through stack + ## indexing through stack ## frames - self.stack[idx + self.frames[^1]] = val + self.operands[idx + self.frames[^1]] = val +proc pushc(self: PeonVM, val: PeonObject) = + ## Pushes a new object to the + ## call stack + self.calls.add(val) + + +proc popc(self: PeonVM): PeonObject = + ## Pops an object off the call + ## stack and returns it + return self.calls.pop() + + +proc peekc(self: PeonVM, distance: int = 0): PeonObject = + ## Returns the Peon object at the + ## given distance from the top of + ## the call stack without consuming it + return self.calls[self.calls.high() + distance] + + +proc getc(self: PeonVM, idx: int): PeonObject = + ## Accessor method that abstracts + ## indexing our call stack through stack + ## frames + return self.calls[idx + self.frames[^1]] + + +proc setc(self: PeonVM, idx: int, val: PeonObject) = + ## Setter method that abstracts + ## indexing our call stack through stack + ## frames + self.calls[idx + self.frames[^1]] = val + ## Byte-level primitives to read/decode ## bytecode @@ -139,7 +174,15 @@ proc readLong(self: PeonVM): uint32 = return uint32([self.readByte(), self.readByte(), self.readByte()].fromTriple()) -proc readInt64(self: PeonVM, idx: int): PeonObject = +proc readUInt(self: PeonVM): uint32 = + ## Reads three bytes from the + ## bytecode and returns them + ## as an unsigned 32 bit + ## integer + return uint32([self.readByte(), self.readByte(), self.readByte(), self.readByte()].fromQuad()) + + +proc constReadInt64(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes @@ -153,7 +196,7 @@ proc readInt64(self: PeonVM, idx: int): PeonObject = copyMem(result.long.addr, arr.addr, sizeof(arr)) -proc readUInt64(self: PeonVM, idx: int): PeonObject = +proc constReadUInt64(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes @@ -167,7 +210,7 @@ proc readUInt64(self: PeonVM, idx: int): PeonObject = copyMem(result.uLong.addr, arr.addr, sizeof(arr)) -proc readUInt32(self: PeonVM, idx: int): PeonObject = +proc constReadUInt32(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes @@ -178,7 +221,7 @@ proc readUInt32(self: PeonVM, idx: int): PeonObject = copyMem(result.uInt.addr, arr.addr, sizeof(arr)) -proc readInt32(self: PeonVM, idx: int): PeonObject = +proc constReadInt32(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes @@ -189,18 +232,95 @@ proc readInt32(self: PeonVM, idx: int): PeonObject = copyMem(result.`int`.addr, arr.addr, sizeof(arr)) +proc constReadInt16(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is an Int16 + var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]] + result = PeonObject(kind: Int16) + copyMem(result.short.addr, arr.addr, sizeof(arr)) + + +proc constReadUInt16(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is an UInt16 + var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]] + result = PeonObject(kind: UInt16) + copyMem(result.uShort.addr, arr.addr, sizeof(arr)) + + +proc constReadInt8(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is an Int8 + result = PeonObject(kind: Int8, tiny: self.chunk.consts[idx]) + + +proc constReadUInt8(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is an UInt8 + result = PeonObject(kind: UInt8, uTiny: self.chunk.consts[idx]) + + +proc constReadString(self: PeonVM, idx: int): PeonObject = + ## Reads a constant from the + ## chunk's constant table and + ## returns a Peon object. Assumes + ## the constant is a string + let size = self.readLong() + result = PeonObject(kind: String, str: self.chunk.consts[idx.. 0: + echo &"Call Stack: {self.calls}" + if self.operands.len() > 0: + echo &"Operand Stack: {self.operands}" + if self.frames.len() > 0: + echo &"Current Frame: {self.calls[self.frames[^1]..^1]}" + echo &"Frames: {self.frames}" + if self.closedOver.len() > 0: + echo &"Closure Array: {self.closedOver}" + if self.results.len() > 0: + echo &"Results: {self.results}" discard readLine stdin + instruction = OpCode(self.readByte()) case instruction: # Constant loading of LoadTrue: @@ -214,73 +334,103 @@ proc dispatch*(self: PeonVM) = of LoadInf: self.push(self.getInf(true)) of LoadInt64: - self.push(self.readInt64(int(self.readLong()))) + self.push(self.constReadInt64(int(self.readLong()))) of LoadUInt64: - self.push(self.readUInt64(int(self.readLong()))) + self.push(self.constReadUInt64(int(self.readLong()))) of LoadUInt32: - self.push(self.readUInt32(int(self.readLong()))) + self.push(self.constReadUInt32(int(self.readLong()))) of LoadInt32: - self.push(self.readInt32(int(self.readLong()))) + self.push(self.constReadInt32(int(self.readLong()))) + of LoadInt16: + self.push(self.constReadInt16(int(self.readLong()))) + of LoadUInt16: + self.push(self.constReadUInt16(int(self.readLong()))) + of LoadInt8: + self.push(self.constReadInt8(int(self.readLong()))) + of LoadUInt8: + self.push(self.constReadUInt8(int(self.readLong()))) + of LoadString: + self.push(self.constReadString(int(self.readLong()))) + of LoadFloat32: + self.push(self.constReadFloat32(int(self.readLong()))) + of LoadFloat64: + self.push(self.constReadFloat64(int(self.readLong()))) + of LoadFunction: + self.pushc(PeonObject(kind: Function, ip: self.readLong())) + of LoadReturnAddress: + self.pushc(PeonObject(kind: UInt32, uInt: self.readUInt())) of Call: # Calls a function. The calling convention for peon - # functions is pretty simple: the return address sits - # at the bottom of the stack frame, then follow the - # arguments and all temporaries/local variables - let newIp = self.readLong() - # We store it because if we immediately changed - # the instruction pointer, we'd read the wrong - # value for the argument count. Storing it and - # changing it later fixes this issue - self.frames.add(int(self.readLong())) - self.ip = int(newIp) + # functions is pretty simple: the first item in the + # frame is a function object which contains the new + # instruction pointer to jump to, followed by a 32-bit + # return address. After that, all arguments and locals + # follow + var size {.used.} = self.readLong().int + self.frames.add(self.calls.len() - 2) + self.ip = self.peekc(-1).ip + self.results.add(self.getNil()) + # TODO: Use the frame size once + # we have more control over the + # memory + #[while size > 0: + dec(size) + self.pushc(self.getNil()) + ]# + of LoadArgument: + self.pushc(self.pop()) of OpCode.Return: - # Returns from a void function - let frame = self.frames.pop() - for i in 0.. 0: - discard self.pop() + # Returns from a function. + # Every peon program is wrapped + # in a hidden function, so this + # will also exit the VM if we're + # at the end of the program + let ret = self.popc() + discard self.popc() # Function object + if self.readByte() == 1: + # Function is non-void! + self.push(self.results.pop()) + else: + discard self.results.pop() discard self.frames.pop() - return - of ReturnValue: - # Returns from a function which has a return value, - # pushing it on the stack - let retVal = self.pop() - let frame = self.frames.pop() - for i in 0.. self.heapVars.high(): - self.heapVars.add(self.pop()) + if idx > self.closedOver.high(): + self.closedOver.add(self.pop()) else: - self.heapVars[idx] = self.pop() + self.closedOver[idx] = self.pop() of LoadClosure: # Loads a closed-over variable onto the # stack - self.push(self.heapVars[self.readLong()]) + self.push(self.closedOver[self.readLong()]) of PopClosure: # Pops a closed-over variable off the closure # array - discard self.heapVars.pop() + discard self.closedOver.pop() of LoadVar: - # Stores/updates the value of a local variable - self.push(self.get(int(self.readLong()))) + self.push(self.getc(int(self.readLong()))) of NoOp: continue + of PopC: + discard self.popc() of Pop: discard self.pop() of PopRepl: @@ -302,6 +452,10 @@ proc dispatch*(self: PeonVM) = echo &"{popped.tiny}'i8" of UInt8: echo &"{popped.uTiny}'u8" + of Float32: + echo &"{popped.halfFloat}'f32" + of Float64: + echo &"{popped.`float`}'f64" of ObjectKind.Inf: if popped.positive: echo "inf" @@ -313,45 +467,45 @@ proc dispatch*(self: PeonVM) = discard of PopN: for _ in 0.. 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 diff --git a/src/frontend/compiler.nim b/src/frontend/compiler.nim index 36df1d8..d1c85ce 100644 --- a/src/frontend/compiler.nim +++ b/src/frontend/compiler.nim @@ -90,6 +90,8 @@ type codePos: int # Is the name closed over (i.e. used in a closure)? isClosedOver: bool + # Is this a function argument? + isFunctionArgument: bool # Where is this node declared in the file? line: int Loop = object @@ -123,8 +125,6 @@ type # runtime to load variables that have stack # behavior more efficiently names: seq[Name] - # Beginning of stack frames for function calls - frames: seq[int] # The current scope depth. If > 0, we're # in a local scope, otherwise it's global scopeDepth: int @@ -158,6 +158,11 @@ type deferred: seq[uint8] # List of closed-over variables closedOver: seq[Name] + frames: seq[int] + + +proc `$`(self: Name): string = + result &= &"Name(name='{self.name.name.lexeme}', depth={self.depth}, codePos={self.codePos})" proc newCompiler*(enableOptimizations: bool = true, replMode: bool = false): Compiler = @@ -172,7 +177,6 @@ proc newCompiler*(enableOptimizations: bool = true, replMode: bool = false): Com result.enableOptimizations = enableOptimizations result.replMode = replMode result.currentModule = "" - result.frames = @[] ## Forward declarations @@ -187,7 +191,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) +proc patchReturnAddress(self: Compiler, pos: int) ## End of forward declarations ## Public getter for nicer error formatting @@ -261,6 +265,16 @@ proc makeConstant(self: Compiler, val: Expression, typ: Type): array[3, uint8] = result = self.chunk.writeConstant(v.toQuad()) of Int64, UInt64: result = self.chunk.writeConstant(v.toLong()) + of String: + result = self.chunk.writeConstant(v.toBytes()) + of Float32: + var f: float = 0.0 + discard parseFloat(val.token.lexeme, f) + result = self.chunk.writeConstant(cast[array[4, uint8]](float32(f))) + of Float64: + var f: float = 0.0 + discard parseFloat(val.token.lexeme, f) + result = self.chunk.writeConstant(cast[array[8, uint8]](f)) else: discard @@ -268,90 +282,58 @@ proc makeConstant(self: Compiler, val: Expression, typ: Type): array[3, uint8] = proc emitConstant(self: Compiler, obj: Expression, kind: Type) = ## Emits a constant instruction along ## with its operand - case self.inferType(obj).kind: + case kind.kind: of Int64: self.emitByte(LoadInt64) of UInt64: self.emitByte(LoadUInt64) of Int32: self.emitByte(LoadInt32) + of UInt32: + self.emitByte(LoadUInt32) + of Int16: + self.emitByte(LoadInt16) + of UInt16: + self.emitByte(LoadUInt16) + of Int8: + self.emitByte(LoadInt8) + of UInt8: + self.emitByte(LoadUInt8) + of String: + self.emitByte(LoadString) + let str = LiteralExpr(obj).literal.lexeme + if str.len() >= 16777216: + self.error("string constants cannot be larger than 16777216 bytes") + self.emitBytes(LiteralExpr(obj).literal.lexeme.len().toTriple()) + of Float32: + self.emitByte(LoadFloat32) + of Float64: + self.emitByte(LoadFloat64) else: discard # TODO self.emitBytes(self.makeConstant(obj, kind)) proc emitJump(self: Compiler, opcode: OpCode): int = - ## Emits a dummy jump offset to be patched later. Assumes - ## the largest offset (emits 4 bytes, one for the given jump - ## opcode, while the other 3 are for the jump offset, which - ## is set to the maximum unsigned 24 bit integer). If the shorter - ## 16 bit alternative is later found to be better suited, patchJump - ## will fix this. Returns the absolute index into the chunk's - ## bytecode array where the given placeholder instruction was written + ## Emits a dummy jump offset to be patched later + ## and returns the absolute index into the chunk's + ## bytecode array where the given placeholder + ## instruction was written self.emitByte(opcode) - self.emitBytes((0xffffff).toTriple()) + self.emitBytes(0.toTriple()) result = self.chunk.code.len() - 4 proc patchJump(self: Compiler, offset: int) = ## Patches a previously emitted relative - ## jump using emitJump. Since emitJump assumes - ## a long jump, this also shrinks the jump - ## offset and changes the bytecode instruction - ## if possible (i.e. jump is in 16 bit range), - ## but the converse is also true (i.e. it might - ## change a regular jump into a long one) + ## jump using emitJump var jump: int = self.chunk.code.len() - offset if jump > 16777215: self.error("cannot jump more than 16777216 bytecode instructions") - if jump < uint16.high().int: - case OpCode(self.chunk.code[offset]): - of LongJumpForwards: - self.chunk.code[offset] = JumpForwards.uint8() - # We do this because a relative jump - # does not take its argument into account - # because it is hardcoded in the bytecode - # itself - jump -= 4 - of LongJumpBackwards: - self.chunk.code[offset] = JumpBackwards.uint8() - jump -= 4 - of LongJumpIfFalse: - self.chunk.code[offset] = JumpIfFalse.uint8() - of LongJumpIfFalsePop: - self.chunk.code[offset] = JumpIfFalsePop.uint8() - of LongJumpIfFalseOrPop: - self.chunk.code[offset] = JumpIfFalseOrPop.uint8() - of JumpForwards, JumpBackwards: - jump -= 3 - else: - discard - self.chunk.code.delete(offset + 1) # Discards the first 8 bits of the jump offset (which are empty) - let offsetArray = (jump - 1).toDouble() # -1 since we got rid of 1 byte! - self.chunk.code[offset + 1] = offsetArray[0] - self.chunk.code[offset + 2] = offsetArray[1] - else: - case OpCode(self.chunk.code[offset]): - of JumpForwards: - self.chunk.code[offset] = LongJumpForwards.uint8() - jump -= 3 - of JumpBackwards: - self.chunk.code[offset] = LongJumpBackwards.uint8() - jump -= 3 - of JumpIfFalse: - self.chunk.code[offset] = LongJumpIfFalse.uint8() - of JumpIfFalsePop: - self.chunk.code[offset] = LongJumpIfFalsePop.uint8() - of JumpIfFalseOrPop: - self.chunk.code[offset] = LongJumpIfFalseOrPop.uint8() - of LongJumpForwards, LongJumpBackwards: - jump -= 4 - else: - discard - let offsetArray = jump.toTriple() - self.chunk.code[offset + 1] = offsetArray[0] - self.chunk.code[offset + 2] = offsetArray[1] - self.chunk.code[offset + 3] = offsetArray[2] + let offsetArray = (jump - 4).toTriple() + self.chunk.code[offset + 1] = offsetArray[0] + self.chunk.code[offset + 2] = offsetArray[1] + self.chunk.code[offset + 3] = offsetArray[2] proc resolve(self: Compiler, name: IdentExpr, @@ -371,34 +353,44 @@ proc resolve(self: Compiler, name: IdentExpr, return nil -proc getStackPos(self: Compiler, name: IdentExpr, - depth: int = self.scopeDepth): tuple[closedOver: bool, pos: int] = - ## Iterates the internal list of declared names backwards and - ## returns a tuple (closedOver, pos) that tells the caller whether the - ## the name is to be emitted as a closure as well as its predicted - ## stack/closure array position. Returns (false, -1) if the variable's - ## location can not be determined at compile time (this is an error!). - ## Note that private names declared in other modules will not be resolved! - var i: int = self.names.high() +proc getStackPos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int = + ## Returns the predicted call stack position of a given name, relative + ## to the current frame + result = 2 + var found = false for variable in reversed(self.names): if name.name.lexeme == variable.name.name.lexeme: if variable.isPrivate and variable.owner != self.currentModule: continue elif variable.depth == depth or variable.depth == 0: # variable.depth == 0 for globals! - return (false, i) - elif variable.depth > 0: - var j: int = self.closedOver.high() - for closure in reversed(self.closedOver): - if closure.name.token.lexeme == name.name.lexeme: - return (true, j) - inc(j) - dec(i) - return (false, -1) + found = true + break + inc(result) + if not found: + return -1 -proc detectClosureVariable(self: Compiler, name: Name, - depth: int = self.scopeDepth) = +proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int = + ## Iterates the internal list of declared closure names backwards and + ## returns the predicted closure array position of a given name. + ## Returns -1 if the name can't be found (this includes names that + ## are private in other modules) + result = self.closedOver.high() + var found = false + for variable in reversed(self.closedOver): + if name.name.lexeme == variable.name.name.lexeme: + if variable.isPrivate and variable.owner != self.currentModule: + continue + elif variable.depth == depth: + found = true + break + dec(result) + if not found: + return -1 + + +proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDepth) = ## Detects if the given name is used in a local scope deeper ## than the given one and modifies the code emitted for it ## to store it as a closure variable if it is. Does nothing if the name @@ -407,15 +399,14 @@ proc detectClosureVariable(self: Compiler, name: Name, ## each time a name is referenced in order for closed-over variables ## to be emitted properly, otherwise the runtime may behave ## unpredictably or crash - if name == nil: + if name == nil or name.depth == 0: return - if name.depth > 0 and name.depth < depth: + elif name.depth < depth and not name.isClosedOver: # Ding! The given name is closed over: we need to # change the dummy Jump instruction that self.declareName - # put in place for us into a StoreHeap. We don't need to change - # other pieces of code because self.identifier() already - # emits LoadHeap if it detects the variable is closed over, - # whether or not this function is called + # put in place for us into a StoreClosure. We also update + # the name's isClosedOver field so that self.identifier() + # can emit a LoadClosure instruction instead of a LoadVar self.closedOver.add(name) let idx = self.closedOver.high().toTriple() if self.closedOver.len() >= 16777216: @@ -694,7 +685,8 @@ proc literal(self: Compiler, node: ASTNode) = discard parseInt(y.literal.lexeme, x) except ValueError: self.error("integer value out of range") - self.emitConstant(y, Type(kind: Int64)) + + self.emitConstant(y, self.inferType(y)) of hexExpr: var x: int var y = HexExpr(node) @@ -707,7 +699,7 @@ proc literal(self: Compiler, node: ASTNode) = stop: y.token.pos.start + len($x)) ) ) - self.emitConstant(node, Type(kind: Int64)) + self.emitConstant(node, self.inferType(y)) of binExpr: var x: int var y = BinExpr(node) @@ -720,7 +712,7 @@ proc literal(self: Compiler, node: ASTNode) = stop: y.token.pos.start + len($x)) ) ) - self.emitConstant(node, Type(kind: Int64)) + self.emitConstant(node, self.inferType(y)) of octExpr: var x: int var y = OctExpr(node) @@ -733,7 +725,7 @@ proc literal(self: Compiler, node: ASTNode) = stop: y.token.pos.start + len($x)) ) ) - self.emitConstant(node, Type(kind: Int64)) + self.emitConstant(node, self.inferType(y)) of floatExpr: var x: float var y = FloatExpr(node) @@ -741,7 +733,7 @@ proc literal(self: Compiler, node: ASTNode) = discard parseFloat(y.literal.lexeme, x) except ValueError: self.error("floating point value out of range") - self.emitConstant(y, Type(kind: Float64)) + self.emitConstant(y, self.inferType(y)) of awaitExpr: var y = AwaitExpr(node) self.expression(y.expression) @@ -798,20 +790,40 @@ proc matchImpl(self: Compiler, name: string, kind: Type): Name = return impl[0] +proc emitFunction(self: Compiler, node: Name) = + ## Wrapper to emit LoadFunction instructions + self.emitByte(LoadFunction) + self.emitBytes((node.codePos + 4).toTriple()) + + proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) = ## Small wrapper that abstracts emitting a call instruction ## for a given function - self.emitByte(LoadNil) # Stack alignment - self.emitByte(LoadUInt32) - # We patch it later! - let idx = self.chunk.consts.len() - self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad())) - for argument in args: - self.expression(argument) # Pushes the arguments onto the stack - self.emitByte(Call) # Creates a stack frame - self.emitBytes(fn.codePos.toTriple()) - self.emitBytes((args.len()).toTriple()) - self.patchReturnAddress(idx) + self.emitFunction(fn) + self.emitByte(LoadReturnAddress) + let pos = self.chunk.code.len() + self.emitBytes(0.toQuad()) + for argument in reversed(args): + # We pass the arguments in reverse because + # we delegate the callee with popping them + # off the operand stack when they're invoked, + # rather than doing it ourselves. The reason + # for this is that the VM can always find the + # function object at a constant location in the + # stack frame instead of needing extra math to + # skip the arguments + self.expression(argument) + self.emitByte(Call) # Creates a new call frame + var size = 2 # We start at 2 because each call frame + # contains at least 2 elements (function + # object and return address) + for name in reversed(self.names): + # Then, for each local variable + # we increase the frame size by 1 + if name.depth == self.scopeDepth: + inc(size) + self.emitBytes(size.toTriple()) + self.patchReturnAddress(pos) proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) = @@ -881,8 +893,10 @@ proc declareName(self: Compiler, node: Declaration) = # slap myself 100 times with a sign saying "I'm dumb". Mark my words self.error("cannot declare more than 16777216 variables at a time") for name in self.findByName(node.name.token.lexeme): - if name.depth == self.scopeDepth and name.valueType.kind notin {Function, CustomType}: - # Trying to redeclare a variable in the same module is an error! + if name.depth == self.scopeDepth and name.valueType.kind notin {Function, CustomType} and not name.isFunctionArgument: + # Trying to redeclare a variable in the same module is an error, but it's okay + # if it's a function argument (for example, if you want to copy a number to + # mutate it) self.error(&"attempt to redeclare '{node.name.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}") self.names.add(Name(depth: self.scopeDepth, name: node.name, @@ -920,7 +934,7 @@ proc declareName(self: Compiler, node: Declaration) = returnType: self.inferType( node.returnType), args: @[]), - codePos: self.chunk.code.high(), + codePos: self.chunk.code.len(), name: node.name, isLet: false, isClosedOver: false, @@ -930,7 +944,7 @@ proc declareName(self: Compiler, node: Declaration) = for argument in node.arguments: if self.names.high() > 16777215: self.error("cannot declare more than 16777216 variables at a time") - # wait, no LoadVar?? Yes! That's because when calling functions, + # wait, no LoadVar? Yes! That's because when calling functions, # arguments will already be on the stack so there's no need to # load them here name = Name(depth: self.scopeDepth + 1, @@ -941,7 +955,9 @@ proc declareName(self: Compiler, node: Declaration) = valueType: nil, codePos: 0, isLet: false, - isClosedOver: false) + isClosedOver: false, + line: argument.name.token.line, + isFunctionArgument: true) self.names.add(name) name.valueType = self.inferType(argument.valueType) if argument.mutable: @@ -975,20 +991,15 @@ proc identifier(self: Compiler, node: IdentExpr) = self.emitConstant(node, self.inferType(node)) else: self.detectClosureVariable(s) - let t = self.getStackPos(node) - var index = t.pos - # We don't check if index is -1 because if it - # were, self.resolve() would have returned nil - if not t.closedOver: + if not s.isClosedOver: # Static name resolution, loads value at index in the stack. Very fast. Much wow. - if self.scopeDepth > 0: - inc(index, 1) self.emitByte(LoadVar) - self.emitBytes((index - self.frames[^1]).toTriple()) + # No need to check for -1 here: we already did a nil-check above! + self.emitBytes(self.getStackPos(s.name).toTriple()) else: - # Heap-allocated closure variable. Stored in a separate "closure array" in the VM that does not have stack semantics - # and where the no-effect invariant is not kept. This makes closures work as expected and is not much slower than - # indexing our stack (since they're both dynamic arrays at runtime anyway) + # Loads a closure variable. Stored in a separate "closure array" in the VM that does not + # align its semantics with the call stack. This makes closures work as expected and is + # not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway) self.emitByte(LoadClosure) self.emitBytes(self.closedOver.high().toTriple()) @@ -1007,16 +1018,16 @@ proc assignment(self: Compiler, node: ASTNode) = elif r.isLet: self.error(&"cannot reassign '{name.token.lexeme}'") self.expression(node.value) - let t = self.getStackPos(name) - let index = t.pos - if index != -1: - if not t.closedOver: - self.emitByte(StoreVar) - else: - self.emitByte(StoreClosure) - self.emitBytes(index.toTriple()) + self.detectClosureVariable(r) + if not r.isClosedOver: + self.emitByte(StoreVar) + self.emitBytes(self.getStackPos(name).toTriple()) else: - self.error(&"reference to undeclared name '{node.token.lexeme}'") + # Loads a closure variable. Stored in a separate "closure array" in the VM that does not + # align its semantics with the call stack. This makes closures work as expected and is + # not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway) + self.emitByte(StoreClosure) + self.emitBytes(self.getClosurePos(name).toTriple()) of setItemExpr: let node = SetItemExpr(node) let typ = self.inferType(node) @@ -1033,7 +1044,7 @@ proc beginScope(self: Compiler) = inc(self.scopeDepth) -proc endScope(self: Compiler, fromFunc: bool = false) = +proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false) = ## Ends the current local scope if self.scopeDepth < 0: self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)") @@ -1045,7 +1056,7 @@ proc endScope(self: Compiler, fromFunc: bool = false) = if not self.enableOptimizations and not fromFunc: # All variables with a scope depth larger than the current one # are now out of scope. Begone, you're now homeless! - self.emitByte(Pop) + self.emitByte(PopC) if self.enableOptimizations and len(names) > 1 and not fromFunc: # If we're popping less than 65535 variables, then # we can emit a PopN instruction. This is true for @@ -1059,27 +1070,28 @@ proc endScope(self: Compiler, fromFunc: bool = false) = if len(names) > uint16.high().int(): for i in countdown(self.names.high(), len(names) - uint16.high().int()): if self.names[i].depth > self.scopeDepth: - self.emitByte(Pop) + self.emitByte(PopC) elif len(names) == 1 and not fromFunc: # We only emit PopN if we're popping more than one value - self.emitByte(Pop) + self.emitByte(PopC) # This seems *really* slow, but # what else should I do? Nim doesn't # allow the removal of items during # seq iteration so ¯\_(ツ)_/¯ - var idx = 0 - while idx < self.names.len(): - for name in names: - if self.names[idx] == name: - self.names.delete(idx) - inc(idx) - idx = 0 - while idx < self.closedOver.len(): - for name in names: - if name.isClosedOver: - self.closedOver.delete(idx) - self.emitByte(PopClosure) - inc(idx) + if deleteNames: + var idx = 0 + while idx < self.names.len(): + for name in names: + if self.names[idx] == name: + self.names.delete(idx) + inc(idx) + idx = 0 + while idx < self.closedOver.len(): + for name in names: + if name.isClosedOver: + self.closedOver.delete(idx) + self.emitByte(PopClosure) + inc(idx) proc blockStmt(self: Compiler, node: BlockStmt) = @@ -1176,11 +1188,6 @@ proc callExpr(self: Compiler, node: CallExpr) = proc expression(self: Compiler, node: Expression) = ## Compiles all expressions - if self.inferType(node) == nil: - if node.kind != identExpr: - # So we can raise a more appropriate - # error in self.identifier() - self.error("expression has no type") case node.kind: of NodeKind.callExpr: self.callExpr(CallExpr(node)) # TODO @@ -1237,7 +1244,24 @@ proc deferStmt(self: Compiler, node: DeferStmt) = self.expression(node.expression) for i in countup(current, self.chunk.code.high()): self.deferred.add(self.chunk.code[i]) - self.chunk.code.del(i) + self.chunk.code.delete(i) # TODO: Do not change bytecode size + + +proc endFunctionBeforeReturn(self: Compiler) = + ## Emits code to clear a function's + ## stack frame right before executing + ## its return instruction + var popped = 0 + for name in self.names: + if name.depth == self.scopeDepth and name.valueType.kind != Function: + inc(popped) + if self.enableOptimizations and popped > 1: + self.emitByte(PopN) + self.emitBytes(popped.toDouble()) + dec(popped, uint16.high().int) + while popped > 0: + self.emitByte(PopC) + dec(popped) proc returnStmt(self: Compiler, node: ReturnStmt) = @@ -1247,8 +1271,11 @@ proc returnStmt(self: Compiler, node: ReturnStmt) = let typ = self.inferType(self.currentFunction) ## Having the return type if returnType == nil and typ.returnType != nil: - if node.value.kind == identExpr: - self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'") + if node.value != nil: + if node.value.kind == identExpr: + self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'") + elif node.value.kind == callExpr and CallExpr(node.value).callee.kind == identExpr: + self.error(&"call to undeclared function '{CallExpr(node.value).callee.token.lexeme}'") self.error(&"expected return value of type '{self.typeToStr(typ.returnType)}', but expression has no type") elif typ.returnType == nil and returnType != nil: self.error("non-empty return statement is not allowed in void functions") @@ -1256,9 +1283,13 @@ proc returnStmt(self: Compiler, node: ReturnStmt) = self.error(&"expected return value of type '{self.typeToStr(typ.returnType)}', got '{self.typeToStr(returnType)}' instead") if node.value != nil: self.expression(node.value) - self.emitByte(OpCode.ReturnValue) + self.emitByte(OpCode.SetResult) + self.endFunctionBeforeReturn() + self.emitByte(OpCode.Return) + if node.value != nil: + self.emitByte(1) else: - self.emitByte(OpCode.Return) + self.emitByte(0) proc yieldStmt(self: Compiler, node: YieldStmt) = @@ -1322,11 +1353,16 @@ proc statement(self: Compiler, node: Statement) = of exprStmt: var expression = ExprStmt(node).expression self.expression(expression) - # We only print top-level expressions - if self.replMode and self.scopeDepth == 0: - self.emitByte(PopRepl) + if expression.kind == callExpr and self.inferType(CallExpr(expression).callee).returnType == nil: + # The expression has no type, so we don't have to + # pop anything + discard else: - self.emitByte(Pop) # Expression statements discard their value. Their main use case is side effects in function calls + # We only print top-level expressions + if self.replMode and self.scopeDepth == 0: + self.emitByte(PopRepl) + else: + self.emitByte(Pop) of NodeKind.ifStmt: self.ifStmt(IfStmt(node)) of NodeKind.assertStmt: @@ -1371,6 +1407,8 @@ proc varDecl(self: Compiler, node: VarDecl) = let expected = self.inferType(node.valueType) let actual = self.inferType(node.value) if expected == nil and actual == nil: + if node.value.kind == identExpr: + self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'") self.error(&"'{node.name.token.lexeme}' has no type") elif expected != nil and expected.kind == Mutable: # I mean, variables *are* already mutable (some of them anyway) self.error(&"invalid type '{self.typeToStr(expected)}' for var") @@ -1379,27 +1417,24 @@ proc varDecl(self: Compiler, node: VarDecl) = self.error(&"expected value of type '{self.typeToStr(expected)}', but '{node.name.token.lexeme}' is of type '{self.typeToStr(actual)}'") self.expression(node.value) self.declareName(node) + self.emitByte(StoreVar) + self.emitBytes(self.names.high().toTriple()) proc funDecl(self: Compiler, node: FunDecl) = ## Compiles function declarations # A function's code is just compiled linearly # and then jumped over - - # While the compiler stores functions as if they - # were on the stack, in reality there is nothing - # at runtime that represents them (instead, we - # compile everything in a single blob and jump back - # and forth). If we didn't align the stack to account - # for this behavior, all of our offsets would be off - # by how many functions we have declared. We could fix - # this by storing functions in a separate list of identifiers, - # but that's rather unelegant and requires specialized functions - # for looking identifiers vs functions, which is not ideal - let jmp = self.emitJump(JumpForwards) var function = self.currentFunction self.declareName(node) - self.frames.add(self.names.len() - 1) + self.frames.add(self.names.high()) + let fn = self.names[^(node.arguments.len() + 1)] + fn.codePos = self.chunk.code.len() + let jmp = self.emitJump(LongJumpForwards) + for argument in node.arguments: + self.emitByte(LoadArgument) + if node.returnType != nil and self.inferType(node.returnType) == nil: + self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'") # TODO: Forward declarations if node.body != nil: if BlockStmt(node.body).code.len() == 0: @@ -1428,26 +1463,32 @@ proc funDecl(self: Compiler, node: FunDecl) = var deferStart = self.deferred.len() # We let our debugger know a function is starting let start = self.chunk.code.high() - self.beginScope() - # self.emitByte(LoadNil) for decl in BlockStmt(node.body).code: self.declaration(decl) - self.endScope(fromFunc=true) + var typ: Type + var hasVal: bool = false case self.currentFunction.kind: of NodeKind.funDecl: - if not self.currentFunction.hasExplicitReturn: - let typ = self.inferType(self.currentFunction) - if self.currentFunction.returnType == nil and typ.returnType != nil: - self.error("non-empty return statement is not allowed in void functions") - if self.currentFunction.returnType != nil: - self.error("function has an explicit return type, but no return statement was found") - self.emitByte(OpCode.Return) + typ = self.inferType(self.currentFunction) + hasVal = self.currentFunction.hasExplicitReturn of NodeKind.lambdaExpr: - if not LambdaExpr(Declaration(self.currentFunction)).hasExplicitReturn: - self.emitByte(OpCode.Return) + typ = self.inferType(LambdaExpr(Declaration(self.currentFunction))) + hasVal = LambdaExpr(Declaration(self.currentFunction)).hasExplicitReturn else: discard # Unreachable + if hasVal and self.currentFunction.returnType == nil and typ.returnType != nil: + self.error("non-empty return statement is not allowed in void functions") + elif not hasVal and self.currentFunction.returnType != nil: + self.error("function has an explicit return type, but no return statement was found") + # self.endFunctionBeforeReturn() + hasVal = hasVal and typ.returnType != nil + self.endScope(deleteNames=true, fromFunc=false) + self.emitByte(OpCode.Return) + if hasVal: + self.emitByte(1) + else: + self.emitByte(0) # Function is ending! self.chunk.cfi.add(start.toTriple()) self.chunk.cfi.add(self.chunk.code.high().toTriple()) @@ -1472,15 +1513,15 @@ proc funDecl(self: Compiler, node: FunDecl) = discard self.frames.pop() -proc patchReturnAddress(self: Compiler, retAddr: int) = +proc patchReturnAddress(self: Compiler, pos: int) = ## Patches the return address of a function - ## call. This is called at each iteration of - ## the compiler's loop + ## call 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] + self.chunk.code[pos] = address[0] + self.chunk.code[pos + 1] = address[1] + self.chunk.code[pos + 2] = address[2] + self.chunk.code[pos + 3] = address[3] + proc declaration(self: Compiler, node: Declaration) = @@ -1505,12 +1546,41 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk = self.currentFunction = nil self.currentModule = self.file.extractFilename() self.current = 0 - self.frames = @[0] + # Every peon program has a hidden entry point in + # which user code is wrapped. Think of it as if + # peon is implicitly writing the main() function + # of your program and putting all of your code in + # there. While we call our entry point just like + # any regular peon function, we can't use our handy + # helper generateCall() because we need to keep track + # of where our program ends (which we don't know yet). + # To fix this, we emit dummy offsets and patch them + # later, once we know the boundaries of our hidden main() + var main = Name(depth: 0, + isPrivate: true, + isConst: false, + isLet: false, + isClosedOver: false, + owner: self.currentModule, + valueType: Type(kind: Function, + name: "", + returnType: nil, + args: @[]), + codePos: 13, + name: newIdentExpr(Token(lexeme: "", kind: Identifier)), + line: -1) + self.names.add(main) + self.emitByte(LoadFunction) + self.emitBytes(main.codePos.toTriple()) + self.emitByte(LoadReturnAddress) + let pos = self.chunk.code.len() + self.emitBytes(0.toQuad()) + self.emitByte(Call) + self.emitBytes(2.toTriple()) while not self.done(): self.declaration(Declaration(self.step())) - if self.ast.len() > 0: - # *Technically* an empty program is a valid program - self.emitByte(ProgExit) + self.endScope(fromFunc=true) + self.patchReturnAddress(pos) + self.emitByte(OpCode.Return) + self.emitByte(0) result = self.chunk - if self.ast.len() > 0 and self.scopeDepth != 0: - self.error(&"invalid state: invalid scopeDepth value (expected 0, got {self.scopeDepth}), did you forget to call endScope/beginScope?") diff --git a/src/frontend/meta/bytecode.nim b/src/frontend/meta/bytecode.nim index 211fc23..ea0b340 100644 --- a/src/frontend/meta/bytecode.nim +++ b/src/frontend/meta/bytecode.nim @@ -82,6 +82,8 @@ type LoadFloat64, LoadFloat32, LoadString, + LoadFunction, + LoadReturnAddress, ## Singleton opcodes (each of them pushes a constant singleton on the stack) LoadNil, LoadTrue, @@ -118,8 +120,8 @@ type LongJumpBackwards, ## Functions Call, # Calls a function and initiates a new stack frame - Return, # Terminates the current function without popping off the stack - ReturnValue, # Pops a return value off the stack and terminates the current function + Return, # Terminates the current function + SetResult, # Sets the result of the current function ## Exception handling Raise, # Raises exception x or re-raises active exception if x is nil BeginTry, # Initiates an exception handling context @@ -131,7 +133,8 @@ type ## Misc Assert, # Raises an AssertionFailed exception if x is false NoOp, # Just a no-op - ProgExit, # Terminates the whole program + LoadArgument, + PopC # We group instructions by their operation/operand types for easier handling when debugging @@ -142,8 +145,9 @@ const simpleInstructions* = {Return, LoadNil, LoadNan, LoadInf, Pop, PopRepl, Raise, BeginTry, FinishTry, Yield, - Await, NoOp, ReturnValue, - PopClosure, ProgExit} + Await, NoOp, PopClosure, + SetResult, LoadArgument, + PopC} # Constant instructions are instructions that operate on the bytecode constant table const constantInstructions* = {LoadInt64, LoadUInt64, diff --git a/src/main.nim b/src/main.nim index 479e01f..bdd3b58 100644 --- a/src/main.nim +++ b/src/main.nim @@ -3,7 +3,6 @@ import strformat import strutils import terminal import parseopt -import nimSHA2 import times import os @@ -15,7 +14,6 @@ import jale/plugin/editor_history import jale/keycodes import jale/multiline - # Our stuff import frontend/lexer as l import frontend/parser as p @@ -32,12 +30,12 @@ proc getLineEditor: LineEditor # Handy dandy compile-time constants const debugLexer = false const debugParser = false -const debugCompiler = false +const debugCompiler = true const debugSerializer = false const debugRuntime = false - -proc repl = + +proc repl(vm: PeonVM = newPeonVM()) = styledEcho fgMagenta, "Welcome into the peon REPL!" var keep = true @@ -50,7 +48,6 @@ proc repl = compiler = newCompiler(replMode=true) debugger = newDebugger() serializer = newSerializer() - vm = newPeonVM() editor = getLineEditor() input: string current: string @@ -110,7 +107,6 @@ proc repl = 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.version.major}.{serialized.version.minor}.{serialized.version.patch}", fgBlue, " (commit ", fgYellow, serialized.commit[0..8], fgBlue, ") on branch ", fgYellow, serialized.branch stdout.styledWriteLine(fgBlue, "\t- Compilation date & time: ", fgYellow, fromUnix(serialized.compileDate).format("d/M/yyyy HH:mm:ss")) stdout.styledWrite(fgBlue, &"\t- Constants segment: ") @@ -163,7 +159,6 @@ proc repl = styledEcho fgBlue, "Source line: " , fgDefault, line styledEcho fgCyan, " ".repeat(len("Source line: ")) & "^".repeat(relPos.stop - relPos.start) except CompileError: - input = "" let exc = CompileError(getCurrentException()) let lexeme = exc.node.token.lexeme let lineNo = exc.node.token.line @@ -184,7 +179,7 @@ proc repl = quit(0) -proc runFile(f: string) = +proc runFile(f: string, interactive: bool = false, fromString: bool = false) = var tokens: seq[Token] = @[] tree: seq[Declaration] = @[] @@ -199,7 +194,13 @@ proc runFile(f: string) = input: string tokenizer.fillSymbolTable() try: - input = readFile(f) + var f = f + if not fromString: + if not f.endsWith(".pn"): + f &= ".pn" + input = readFile(f) + else: + input = f tokens = tokenizer.lex(input, f) if tokens.len() == 0: return @@ -302,6 +303,8 @@ proc runFile(f: string) = stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {getCurrentExceptionMsg()}") except OSError: stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {osErrorMsg(osLastError())} [errno {osLastError()}]") + if interactive: + repl(vm) when isMainModule: @@ -353,7 +356,7 @@ when isMainModule: if file == "": repl() else: - runFile(file) + runFile(file, interactive, fromString) proc fillSymbolTable(tokenizer: Lexer) = diff --git a/src/util/debugger.nim b/src/util/debugger.nim index 812dd2e..5270d09 100644 --- a/src/util/debugger.nim +++ b/src/util/debugger.nim @@ -71,7 +71,7 @@ proc checkFrameStart(self: Debugger, n: int) = styledEcho fgBlue, "\n==== Peon Bytecode Debugger - Begin Frame ", fgYellow, &"'{e.name}' ", fgBlue, "(", fgYellow, $i, fgBlue, ") ====" styledEcho fgGreen, "\t- Start offset: ", fgYellow, $e.start styledEcho fgGreen, "\t- End offset: ", fgYellow, $e.stop - styledEcho fgGreen, "\t- Stack bottom: ", fgYellow, $e.bottom + styledEcho fgGreen, "\t- Frame bottom: ", fgYellow, $e.bottom styledEcho fgGreen, "\t- Argument count: ", fgYellow, $e.argc @@ -83,18 +83,24 @@ proc checkFrameEnd(self: Debugger, n: int) = proc simpleInstruction(self: Debugger, instruction: OpCode) = - printInstruction(instruction) - nl() + printInstruction(instruction, true) self.current += 1 - if instruction in {Return, ReturnValue}: + if instruction == Return: + printDebug("Void: ") + if self.chunk.code[self.current] == 0: + stdout.styledWriteLine(fgYellow, "Yes") + else: + stdout.styledWriteLine(fgYellow, "No") + self.current += 1 + self.checkFrameEnd(self.current - 2) self.checkFrameEnd(self.current - 1) self.checkFrameEnd(self.current) + proc stackTripleInstruction(self: Debugger, instruction: OpCode) = ## Debugs instructions that operate on a single value on the stack using a 24-bit operand - var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[ - self.current + 3]].fromTriple() + var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple() printInstruction(instruction) stdout.styledWriteLine(fgGreen, &", points to index ", fgYellow, $slot) self.current += 4 @@ -127,25 +133,34 @@ proc argumentTripleInstruction(self: Debugger, instruction: OpCode) = proc callInstruction(self: Debugger, instruction: OpCode) = ## Debugs function calls - var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple() - var args = [self.chunk.code[self.current + 4], self.chunk.code[self.current + 5], self.chunk.code[self.current + 6]].fromTriple() + var size = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple() printInstruction(instruction) - stdout.styledWrite(fgGreen, &", jumps to address ", fgYellow, $slot, fgGreen, " with ", fgYellow, $args, fgGreen, " argument") - if args > 1 or args == 0: - stdout.styledWrite(fgGreen, "s") - nl() - self.current += 7 + styledEcho fgGreen, &", creates frame of size ", fgYellow, $size + self.current += 4 + + +proc functionInstruction(self: Debugger, instruction: OpCode) = + ## Debugs function calls + var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple() + printInstruction(instruction) + styledEcho fgGreen, &", loads function at address ", fgYellow, $address + self.current += 4 + + +proc loadAddressInstruction(self: Debugger, instruction: OpCode) = + ## Debugs LoadReturnAddress instructions + var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3], + self.chunk.code[self.current + 4]].fromQuad() + printInstruction(instruction) + styledEcho fgGreen, &" loads address ", fgYellow, $address + self.current += 5 proc constantInstruction(self: Debugger, instruction: OpCode) = ## Debugs instructions that operate on the constant table - var constant = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[ - self.current + 3]].fromTriple() + var constant = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple() printInstruction(instruction) - stdout.styledWrite(fgGreen, &", points to constant at position ", fgYellow, $constant) - nl() - printDebug("Operand: ") - stdout.styledWriteLine(fgYellow, &"{self.chunk.consts[constant]}") + stdout.styledWriteLine(fgGreen, &", points to constant at position ", fgYellow, $constant) self.current += 4 @@ -158,8 +173,7 @@ proc jumpInstruction(self: Debugger, instruction: OpCode) = jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2]].fromDouble().int() of LongJump, LongJumpIfFalse, LongJumpIfTrue, LongJumpIfFalsePop, LongJumpForwards, LongJumpBackwards: - jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[ - self.current + 3]].fromTriple().int() + jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple().int() self.current += 1 else: discard # Unreachable @@ -172,10 +186,11 @@ proc jumpInstruction(self: Debugger, instruction: OpCode) = self.checkFrameStart(i) + proc disassembleInstruction*(self: Debugger) = ## Takes one bytecode instruction and prints it printDebug("Offset: ") - stdout.styledWriteLine(fgYellow, $self.current) + stdout.styledWriteLine(fgYellow, $(self.current)) printDebug("Line: ") stdout.styledWriteLine(fgYellow, &"{self.chunk.getLine(self.current)}") var opcode = OpCode(self.chunk.code[self.current]) @@ -196,6 +211,10 @@ proc disassembleInstruction*(self: Debugger) = self.callInstruction(opcode) of jumpInstructions: self.jumpInstruction(opcode) + of LoadFunction: + self.functionInstruction(opcode) + of LoadReturnAddress: + self.loadAddressInstruction(opcode) else: echo &"DEBUG - Unknown opcode {opcode} at index {self.current}" self.current += 1 diff --git a/src/util/serializer.nim b/src/util/serializer.nim index 46ab3d3..c98f6e5 100644 --- a/src/util/serializer.nim +++ b/src/util/serializer.nim @@ -20,7 +20,6 @@ import ../config import strformat import strutils -import nimSHA2 import times @@ -36,7 +35,6 @@ type ## the Serializer.read* ## procedures to store ## metadata - fileHash*: string version*: tuple[major, minor, patch: int] branch*: string commit*: string @@ -45,7 +43,7 @@ type proc `$`*(self: Serialized): string = - result = &"Serialized(fileHash={self.fileHash}, version={self.version.major}.{self.version.minor}.{self.version.patch}, branch={self.branch}), commitHash={self.commit}, date={self.compileDate}, chunk={self.chunk[]}" + result = &"Serialized(version={self.version.major}.{self.version.minor}.{self.version.patch}, branch={self.branch}), commitHash={self.commit}, date={self.compileDate}, chunk={self.chunk[]}" proc error(self: Serializer, message: string) = @@ -72,7 +70,6 @@ proc writeHeaders(self: Serializer, stream: var seq[byte], file: string) = stream.extend(PEON_BRANCH.toBytes()) stream.extend(PEON_COMMIT_HASH.toBytes()) stream.extend(getTime().toUnixFloat().int().toBytes()) - stream.extend(computeSHA256(file).toBytes()) proc writeLineData(self: Serializer, stream: var seq[byte]) = @@ -129,8 +126,7 @@ proc readHeaders(self: Serializer, stream: seq[byte], serialized: Serialized): i stream[3], stream[4], stream[5], stream[6], stream[7]])) stream = stream[8..^1] result += 8 - serialized.fileHash = stream[0..<32].fromBytes().toHex().toLowerAscii() - result += 32 + proc readLineData(self: Serializer, stream: seq[byte]): int =