# Copyright 2022 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # 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. ## The Peon runtime environment import std/monotimes import types import ../config import ../frontend/meta/bytecode import ../util/multibyte export types when DEBUG_TRACE_VM: import strutils import strformat type PeonVM* = ref object ## The Peon Virtual Machine 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 closedOver: seq[PeonObject] # Stores variables that do not have stack semantics results: seq[PeonObject] # Stores function's results proc initCache*(self: PeonVM) = ## Initializes the VM's ## singletons cache self.cache[0] = PeonObject(kind: Nil) self.cache[1] = PeonObject(kind: Bool, boolean: true) self.cache[2] = PeonObject(kind: Bool, boolean: false) self.cache[3] = PeonObject(kind: ObjectKind.Inf, positive: true) self.cache[4] = PeonObject(kind: ObjectKind.Inf, positive: false) self.cache[5] = PeonObject(kind: ObjectKind.Nan) proc newPeonVM*: PeonVM = ## Initializes a new, blank VM ## for executing Peon bytecode new(result) result.ip = 0 result.frames = @[] result.calls = newSeq[PeonObject]() result.operands = newSeq[PeonObject]() result.initCache() # Getters for singleton types (they are cached!) proc getNil*(self: PeonVM): PeonObject {.inline.} = self.cache[0] proc getBool*(self: PeonVM, value: bool): PeonObject {.inline.} = if value: return self.cache[1] return self.cache[2] proc getInf*(self: PeonVM, positive: bool): PeonObject {.inline.} = if positive: return self.cache[3] return self.cache[4] proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5] # Thanks to nim's *genius* idea of making x !> y a template # for y < x (which by itself is fine) together with the fact # that the order of evaluation of templates with the same # expression is fucking stupid (see https://nim-lang.org/docs/manual.html#order-of-evaluation # and https://github.com/nim-lang/Nim/issues/10425 and try not to # bang your head against the nearest wall), we need a custom operator # that preserves the natural order of evaluation proc `!>`[T](a, b: T): auto {.inline.} = b < a proc `!>=`[T](a, b: T): auto {.inline, used.} = b <= a # 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 ## operand stack self.operands.add(obj) proc pop(self: PeonVM): PeonObject = ## Pops a Peon object off the ## operand stack. The object ## is returned return self.operands.pop() 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 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 {.used.} = ## 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 and decode # bytecode proc readByte(self: PeonVM): uint8 = ## Reads a single byte from the ## bytecode and returns it as an ## unsigned 8 bit integer inc(self.ip) return self.chunk.code[self.ip - 1] proc readShort(self: PeonVM): uint16 = ## Reads two bytes from the ## bytecode and returns them ## as an unsigned 16 bit ## integer return [self.readByte(), self.readByte()].fromDouble() proc readLong(self: PeonVM): uint32 = ## Reads three bytes from the ## bytecode and returns them ## as an unsigned 32 bit ## integer. Note however that ## the boundary is capped at ## 24 bits instead of 32 return uint32([self.readByte(), self.readByte(), self.readByte()].fromTriple()) 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()) # 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 ## 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 + 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)) proc constReadUInt64(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## 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 + 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 constReadUInt32(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 constReadInt32(self: PeonVM, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes ## the constant is an Int32 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] result = PeonObject(kind: Int32) 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: int8(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, size: int, idx: int): PeonObject = ## Reads a constant from the ## chunk's constant table and ## returns a Peon object. Assumes ## the constant is a string 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 instructions of LoadTrue: self.push(self.getBool(true)) of LoadFalse: self.push(self.getBool(false)) of LoadNan: self.push(self.getNan()) of LoadNil: self.push(self.getNil()) of LoadInf: self.push(self.getInf(true)) of LoadInt64: self.push(self.constReadInt64(int(self.readLong()))) of LoadUInt64: self.push(self.constReadUInt64(int(self.readLong()))) of LoadUInt32: self.push(self.constReadUInt32(int(self.readLong()))) of LoadInt32: 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()), int(self.readLong()))) of LoadFloat32: self.push(self.constReadFloat32(int(self.readLong()))) of LoadFloat64: self.push(self.constReadFloat64(int(self.readLong()))) of LoadFunction: # Loads a function onto the operand stack by reading its # instruction pointer self.push(PeonObject(kind: Function, ip: self.readLong())) of LoadReturnAddress: # 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 # 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. 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 self.ip = fnObj.ip self.pushc(fnObj) self.pushc(retAddr) # Creates a new result slot for the # function's return value self.results.add(self.getNil()) # Creates a new call frame self.frames.add(self.calls.len() - 2) # Loads the arguments onto the stack for _ in 0.. 0: dec(argc) self.pushc(self.getNil()) ]# of Return: # 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 while self.calls.len() !> self.frames[^1] + 2: # Discards the function's local variables, # if there is any discard self.popc() let ret = self.popc() # Return address discard self.popc() # Function object if self.readByte() == 1: # Function is non-void! self.push(self.results.pop()) else: discard self.results.pop() # Discard the topmost stack frame discard self.frames.pop() if self.frames.len() == 0: # End of the program! return self.ip = ret.uInt of SetResult: # Sets the result of the # 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 # into the given call stack index let idx = int(self.readLong()) if idx + self.frames[^1] <= self.calls.high(): self.setc(idx, self.pop()) else: self.pushc(self.pop()) of StoreClosure: # Stores/updates the value of a closed-over # variable let idx = self.readLong().int if idx !> self.closedOver.high(): # Note: we *peek* the stack, but we # don't pop! self.closedOver.add(self.peek()) else: self.closedOver[idx] = self.peek() of LoadClosure: # Loads a closed-over variable onto the # stack self.push(self.closedOver[self.readLong()]) of LoadVar: # Pushes a variable onto the operand # stack self.push(self.getc(int(self.readLong()))) of NoOp: # Does nothing continue of PopC: # Pops a value off the call stack discard self.popc() of Pop: # Pops a value off the operand stack discard self.pop() of PushC: # Pushes a value from the operand stack # onto the call stack self.pushc(self.pop()) of PopRepl: # Pops a peon object off the # operand stack and prints it. # Used in interactive REPL mode if self.frames.len() !> 1: discard self.pop() continue echo self.pop() of GenericPrint: # Prints the peon object at the top # of the operand stack echo self.pop() of PopN: # Pops N elements off the call stack for _ in 0.. self.pop().long)) of EqualInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().long == self.pop().long)) of NotEqualInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().long != self.pop().long)) of LessThanUInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().uLong < self.pop().uLong)) of GreaterThanUInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().uLong !> self.pop().uLong)) of EqualUInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().uLong == self.pop().uLong)) of NotEqualUInt64: self.push(PeonObject(kind: Bool, boolean: self.pop().uLong != self.pop().uLong)) of LessThanInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().`int` < self.pop().`int`)) of GreaterThanInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().`int` !> self.pop().`int`)) of EqualInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().`int` == self.pop().`int`)) of NotEqualInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().`int` != self.pop().`int`)) of LessThanUInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().uInt < self.pop().uInt)) of GreaterThanUInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().uInt !> self.pop().uInt)) of EqualUInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().uInt == self.pop().uInt)) of NotEqualUInt32: self.push(PeonObject(kind: Bool, boolean: self.pop().uInt != self.pop().uInt)) of LessThanInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().short < self.pop().short)) of GreaterThanInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().short !> self.pop().short)) of EqualInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().short == self.pop().short)) of NotEqualInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().short != self.pop().short)) of LessThanUInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().uShort < self.pop().uShort)) of GreaterThanUInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().uShort !> self.pop().uShort)) of EqualUInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().uShort == self.pop().uShort)) of NotEqualUInt16: self.push(PeonObject(kind: Bool, boolean: self.pop().uShort != self.pop().uShort)) of LessThanInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().tiny < self.pop().tiny)) of GreaterThanInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().tiny !> self.pop().tiny)) of EqualInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().tiny == self.pop().tiny)) of NotEqualInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().tiny != self.pop().tiny)) of LessThanUInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().uTiny < self.pop().uTiny)) of GreaterThanUInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().uTiny !> self.pop().uTiny)) of EqualUInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().uTiny == self.pop().uTiny)) of NotEqualUInt8: self.push(PeonObject(kind: Bool, boolean: self.pop().uTiny != self.pop().uTiny)) of LessThanFloat64: self.push(PeonObject(kind: Bool, boolean: self.pop().`float` < self.pop().`float`)) of GreaterThanFloat64: self.push(PeonObject(kind: Bool, boolean: self.pop().`float` !> self.pop().`float`)) of EqualFloat64: self.push(PeonObject(kind: Bool, boolean: self.pop().`float` == self.pop().`float`)) of NotEqualFLoat64: self.push(PeonObject(kind: Bool, boolean: self.pop().`float` != self.pop().`float`)) of LessThanFloat32: self.push(PeonObject(kind: Bool, boolean: self.pop().halfFloat < self.pop().halfFloat)) of GreaterThanFloat32: self.push(PeonObject(kind: Bool, boolean: self.pop().halfFloat !> self.pop().halfFloat)) of EqualFloat32: self.push(PeonObject(kind: Bool, boolean: self.pop().halfFloat == self.pop().halfFloat)) of NotEqualFloat32: self.push(PeonObject(kind: Bool, boolean: self.pop().halfFloat != self.pop().halfFloat)) of SysClock64: # Pushes the value of a monotonic clock # as a 64 bit float onto the operand stack. # Used to track system runtime accurately, # but cannot be converted to a date. The number # is in seconds self.push(PeonObject(kind: Float64, `float`: getMonoTime().ticks().float() / 1_000_000_000)) else: discard proc run*(self: PeonVM, chunk: Chunk) = ## Executes a piece of Peon bytecode self.chunk = chunk self.frames = @[] self.calls = @[] self.operands = @[] self.ip = 0 self.dispatch()