diff --git a/src/backend/types.nim b/src/backend/types.nim index 0688218..a924142 100644 --- a/src/backend/types.nim +++ b/src/backend/types.nim @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import strformat +import strutils type @@ -62,3 +64,44 @@ type value*: ptr PeonObject else: discard # TODO + + +proc `$`*(self: PeonObject): string = + ## Returns a string representation + ## of a peon object + case self.kind: + of Int64: + result = &"{self.long}'i64" + of UInt64: + result = &"{self.uLong}'u64" + of Int32: + result = &"{self.`int`}'i32" + of UInt32: + result = &"{self.uInt}'u32" + of Int16: + result = &"{self.short}'i16" + of UInt16: + result = &"{self.uShort}'u16" + of Int8: + result = &"{self.tiny}'i8" + of UInt8: + result = &"{self.uTiny}'u8" + of Float32: + result = &"{self.halfFloat}'f32" + of Float64: + result = &"{self.`float`}'f64" + of ObjectKind.Inf: + if self.positive: + result ="inf" + else: + result ="-inf" + of ObjectKind.Nan, Nil: + result =($self.kind).toLowerAscii() + of Function: + result = &"fn(ip: {self.ip})" + of CustomType: + result = "typevar" + of String: + result = self.str + else: + discard \ No newline at end of file diff --git a/src/backend/vm.nim b/src/backend/vm.nim index db55717..2b3a0ca 100644 --- a/src/backend/vm.nim +++ b/src/backend/vm.nim @@ -23,6 +23,7 @@ import ../util/multibyte export types + type PeonVM* = ref object ## The Peon Virtual Machine @@ -58,7 +59,7 @@ proc newPeonVM*: PeonVM = result.initCache() -## Getters for singleton types (they are cached!) +# Getters for singleton types (they are cached!) proc getNil*(self: PeonVM): PeonObject {.inline.} = self.cache[0] @@ -74,10 +75,10 @@ proc getInf*(self: PeonVM, positive: bool): PeonObject {.inline.} = proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5] -## Stack primitives. Note: all stack accessing that goes -## through the get/set wrappers is frame-relative, meaning -## that the index is added to the current stack frame's -## bottom to obtain an absolute stack index. +# Stack primitives. Note: all stack accessing that goes +# through the get(c)/set(c)/peek(c) wrappers is frame-relative, +# meaning that the index is added to the current stack frame's +# bottom to obtain an absolute stack index. proc push(self: PeonVM, obj: PeonObject) = ## Pushes a Peon object onto the @@ -111,7 +112,7 @@ proc popc(self: PeonVM): PeonObject = return self.calls.pop() -proc peekc(self: PeonVM, distance: int = 0): PeonObject = +proc peekc(self: PeonVM, distance: int = 0): PeonObject {.used.} = ## Returns the Peon object at the ## given distance from the top of ## the call stack without consuming it @@ -131,8 +132,8 @@ proc setc(self: PeonVM, idx: int, val: PeonObject) = ## frames self.calls[idx + self.frames[^1]] = val -## Byte-level primitives to read/decode -## bytecode +# Byte-level primitives to read and decode +# bytecode proc readByte(self: PeonVM): uint8 = ## Reads a single byte from the @@ -168,6 +169,9 @@ proc readUInt(self: PeonVM): uint32 = return uint32([self.readByte(), self.readByte(), self.readByte(), self.readByte()].fromQuad()) +# Functions to read primitives from the chunk's +# constants table + proc constReadInt64(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and @@ -287,45 +291,11 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject = copyMem(result.`float`.addr, arr.addr, sizeof(arr)) -proc `$`(self: PeonObject): string = - case self.kind: - of Int64: - result = &"{self.long}'i64" - of UInt64: - result = &"{self.uLong}'u64" - of Int32: - result = &"{self.`int`}'i32" - of UInt32: - result = &"{self.uInt}'u32" - of Int16: - result = &"{self.short}'i16" - of UInt16: - result = &"{self.uShort}'u16" - of Int8: - result = &"{self.tiny}'i8" - of UInt8: - result = &"{self.uTiny}'u8" - of Float32: - result = &"{self.halfFloat}'f32" - of Float64: - result = &"{self.`float`}'f64" - of ObjectKind.Inf: - if self.positive: - result ="inf" - else: - result ="-inf" - of ObjectKind.Nan, Nil: - result =($self.kind).toLowerAscii() - of Function: - result = &"fn(ip: {self.ip})" - else: - discard - - proc dispatch*(self: PeonVM) = ## Main bytecode dispatch loop var instruction {.register.}: OpCode while true: + {.computedgoto.} # https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma when DEBUG_TRACE_VM: echo &"IP: {self.ip}" echo &"Instruction: {OpCode(self.chunk.code[self.ip])}" @@ -343,7 +313,7 @@ proc dispatch*(self: PeonVM) = discard readLine stdin instruction = OpCode(self.readByte()) case instruction: - # Constant loading + # Constant loading instructions of LoadTrue: self.push(self.getBool(true)) of LoadFalse: @@ -381,8 +351,8 @@ proc dispatch*(self: PeonVM) = # instruction pointer self.push(PeonObject(kind: Function, ip: self.readLong())) of LoadReturnAddress: - # Loads a 32-bit unsigned integer. Used to load function - # return addresses + # Loads a 32-bit unsigned integer onto the operand stack. + # Used to load function return addresses self.push(PeonObject(kind: UInt32, uInt: self.readUInt())) of Call: # Calls a function. The calling convention for peon @@ -390,7 +360,9 @@ proc dispatch*(self: PeonVM) = # 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 + # follow. Note that, due to how the stack works, all + # arguments before the call are in the reverse order in + # which they are passed to the function var argc {.used.} = self.readLong().int let retAddr = self.peek(-argc) # Return address let fnObj = self.peek(-argc - 1) # Function object @@ -439,7 +411,11 @@ proc dispatch*(self: PeonVM) = self.ip = ret.uInt of SetResult: # Sets the result of the - # current function + # current function. A Return + # instruction will pop this + # off the results array and + # onto the operand stack when + # the current function exits. self.results[self.frames.high()] = self.pop() of StoreVar: # Stores the value at the top of the operand stack @@ -621,6 +597,10 @@ proc dispatch*(self: PeonVM) = self.push(PeonObject(kind: Int16, short: -self.pop().short)) of NegInt8: self.push(PeonObject(kind: Int8, tiny: -self.pop().tiny)) + of NegFloat64: + self.push(PeonObject(kind: Float64, `float`: -self.pop().`float`)) + of NegFloat32: + self.push(PeonObject(kind: Float32, halfFloat: -self.pop().halfFloat)) else: discard diff --git a/src/frontend/compiler.nim b/src/frontend/compiler.nim index 188ea4c..4c95162 100644 --- a/src/frontend/compiler.nim +++ b/src/frontend/compiler.nim @@ -874,7 +874,8 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) = ## Emits instructions for builtin functions ## such as addition or subtraction if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]: - self.expression(args[1]) + if len(args) == 2: + self.expression(args[1]) self.expression(args[0]) case fn.valueType.builtinOp: of "AddInt64": @@ -957,6 +958,18 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) = self.emitByte(DivFloat32) of "MulFloat32": self.emitByte(MulFloat32) + of "NegInt64": + self.emitByte(NegInt64) + of "NegInt32": + self.emitByte(NegInt32) + of "NegInt16": + self.emitByte(NegInt16) + of "NegInt8": + self.emitByte(NegInt8) + of "NegFloat64": + self.emitByte(NegFloat64) + of "NegFloat32": + self.emitByte(NegFloat32) of "LogicalOr": self.expression(args[0]) let jump = self.emitJump(JumpIfTrue) @@ -1062,6 +1075,21 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) = line: node.token.line)) if mutable: self.names[^1].valueType.mutable = true + # We emit a jump of 0 because this may become a + # StoreHeap instruction. If they variable is + # not closed over, we'll sadly be wasting a + # VM cycle. The previous implementation used 4 no-op + # instructions, which wasted 4 times as many clock + # cycles. + # TODO: Optimize this. It's a bit tricky because + # deleting bytecode would render all of our + # jump offsets and other absolute indeces in the + # bytecode wrong + if self.scopeDepth > 0: + # Closure variables are only used in local + # scopes + self.emitByte(JumpForwards) + self.emitBytes(0.toTriple()) of NodeKind.funDecl: var node = FunDecl(node) # We declare the generics before the function so we @@ -1128,8 +1156,6 @@ proc identifier(self: Compiler, node: IdentExpr) = # no matter the scope depth self.emitConstant(node, self.inferType(node)) else: - self.emitByte(JumpForwards) - self.emitBytes(0.toTriple()) self.detectClosureVariable(s) if s.valueType.kind == Function: # Functions have no runtime @@ -1197,7 +1223,7 @@ proc endScope(self: Compiler) = dec(self.scopeDepth) var names: seq[Name] = @[] for name in self.names: - if name.depth > self.scopeDepth: + if name.depth > self.scopeDepth and name.valueType.kind notin {Generic, CustomType, Function}: names.add(name) if len(names) > 1: # If we're popping less than 65535 variables, then @@ -1409,7 +1435,7 @@ proc endFunctionBeforeReturn(self: Compiler) = ## its return instruction var popped = 0 for name in self.names: - if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType} and not name.isClosedOver: + if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType}: inc(popped) if popped > 1: self.emitByte(PopN) diff --git a/src/frontend/meta/bytecode.nim b/src/frontend/meta/bytecode.nim index c8d1035..2ffd7f6 100644 --- a/src/frontend/meta/bytecode.nim +++ b/src/frontend/meta/bytecode.nim @@ -95,6 +95,8 @@ type NegInt32, NegInt16, NegInt8, + NegFloat32, + NegFloat64, AddInt64, AddUInt64, AddInt32, @@ -195,8 +197,8 @@ const simpleInstructions* = {Return, LoadNil, DivInt8, DivUInt8, AddFloat64, SubFloat64, DivFloat64, MulFloat64, AddFloat32, SubFloat32, DivFloat32, - MulFloat32 - } + MulFloat32, NegFloat32, NegFloat64, + } # Constant instructions are instructions that operate on the bytecode constant table const constantInstructions* = {LoadInt64, LoadUInt64,