# 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 {.push checks:off.} # The VM is a critical point where checks are deleterious import std/monotimes import std/math import ../config import ../frontend/meta/bytecode import ../util/multibyte import strutils when DEBUG_TRACE_VM: import std/strformat type PeonVM* = ref object ## The Peon Virtual Machine. ## Note how the only data ## type we handle here is ## a 64-bit unsigned integer: ## This is to allow the use ## of unboxed primitive types. ## For more complex types, the ## value represents a pointer to ## some stack- or heap-allocated ## object. The VM has no concept ## of type by itself and it relies ## on the compiler to produce the ## correct results ip: uint64 # Instruction pointer chunk: Chunk # Piece of bytecode to execute calls: seq[uint64] # Our call stack operands: seq[uint64] # Our operand stack cache: array[6, uint64] # Singletons cache frames: seq[uint64] # Stores the bottom of stack frames closedOver: seq[uint64] # Stores variables that do not have stack semantics results: seq[uint64] # Stores function's results (return values) proc initCache*(self: PeonVM) = ## Initializes the VM's ## singletons cache self.cache[0] = 0x0 # Nil self.cache[1] = 0x1 # True self.cache[2] = 0x2 # False self.cache[3] = 0x3 # Positive inf self.cache[4] = 0x4 # Negative inf self.cache[5] = 0x5 # NaN proc newPeonVM*: PeonVM = ## Initializes a new, blank VM ## for executing Peon bytecode new(result) result.ip = 0 result.frames = @[] result.calls = newSeq[uint64]() result.operands = newSeq[uint64]() result.initCache() # Getters for singleton types (they are cached!) proc getNil*(self: PeonVM): uint64 {.inline.} = self.cache[2] proc getBool*(self: PeonVM, value: bool): uint64 {.inline.} = if value: return self.cache[1] return self.cache[0] proc getInf*(self: PeonVM, positive: bool): uint64 {.inline.} = if positive: return self.cache[3] return self.cache[4] proc getNan*(self: PeonVM): uint64 {.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 accesses to the call stack # that go through the getc/setc wrappers is frame-relative, # meaning that the index is added to the current stack frame's # bottom to obtain an absolute stack index {.push inline.} proc push(self: PeonVM, obj: uint64) = ## Pushes a value object onto the ## operand stack self.operands.add(obj) proc pop(self: PeonVM): uint64 = ## Pops a value off the ## operand stack and ## returns it return self.operands.pop() proc peekb(self: PeonVM, distance: BackwardsIndex = ^1): uint64 = ## Returns the value at the ## given (backwards) distance from the top of ## the operand stack without consuming it return self.operands[distance] proc peek(self: PeonVM, distance: int = 0): uint64 = ## Returns the value at the ## given distance from the top of ## the operand stack without consuming it if distance < 0: return self.peekb(^(-distance)) return self.operands[self.operands.high() + distance] proc pushc(self: PeonVM, val: uint64) = ## Pushes a value to the ## call stack self.calls.add(val) proc popc(self: PeonVM): uint64 = ## Pops a value off the call ## stack and returns it return self.calls.pop() proc peekc(self: PeonVM, distance: int = 0): uint64 {.used.} = ## Returns the value 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: uint): uint64 = ## Accessor method that abstracts ## indexing our call stack through stack ## frames return self.calls[idx + self.frames[^1]] proc setc(self: PeonVM, idx: uint, val: uint64) = ## 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): int64 = ## Reads a constant from the ## chunk's constant table and ## returns it as 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], ] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadUInt64(self: PeonVM, idx: int): uint64 = ## Reads a constant from the ## chunk's constant table and ## returns it as 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], ] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadUInt32(self: PeonVM, idx: int): uint32 = ## Reads a constant from the ## chunk's constant table and ## returns it as an int32 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadInt32(self: PeonVM, idx: int): int32 = ## Reads a constant from the ## chunk's constant table and ## returns it as an uint32 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadInt16(self: PeonVM, idx: int): int16 = ## Reads a constant from the ## chunk's constant table and ## returns it as an int16 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadUInt16(self: PeonVM, idx: int): uint16 = ## Reads a constant from the ## chunk's constant table and ## returns it as an uint16 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadInt8(self: PeonVM, idx: int): int8 = ## Reads a constant from the ## chunk's constant table and ## returns it as an int8 result = int8(self.chunk.consts[idx]) proc constReadUInt8(self: PeonVM, idx: int): uint8 = ## Reads a constant from the ## chunk's constant table and ## returns it as an uint8 result = self.chunk.consts[idx] proc constReadFloat32(self: PeonVM, idx: int): float32 = ## Reads a constant from the ## chunk's constant table and ## returns it as a float32 var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1], self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]] copyMem(result.addr, arr.addr, sizeof(arr)) proc constReadFloat64(self: PeonVM, idx: int): float = ## Reads a constant from the ## chunk's constant table and ## returns it as a float 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]] copyMem(result.addr, arr.addr, sizeof(arr)) {.pop.} 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])}" if self.calls.len() !> 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(uint64(self.constReadInt64(int(self.readLong())))) of LoadUInt64: self.push(uint64(self.constReadUInt64(int(self.readLong())))) of LoadUInt32: self.push(uint64(self.constReadUInt32(int(self.readLong())))) of LoadInt32: self.push(uint64(self.constReadInt32(int(self.readLong())))) of LoadInt16: self.push(uint64(self.constReadInt16(int(self.readLong())))) of LoadUInt16: self.push(uint64(self.constReadUInt16(int(self.readLong())))) of LoadInt8: self.push(uint64(self.constReadInt8(int(self.readLong())))) of LoadUInt8: self.push(uint64(self.constReadUInt8(int(self.readLong())))) of LoadString: # TODO: Use constReadString with own memory manager # Strings are broken rn!! let size = int(self.readLong()) let idx = int(self.readLong()) var str = self.chunk.consts[idx.. 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().uint64 !> self.frames[^1] + 2'u64: # Discards the function's local variables, # if there is any discard self.popc() let ret = self.popc() # Return address discard self.popc() # Function address 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 = self.readLong() when DEBUG_TRACE_VM: assert idx.int - self.calls.high() <= 1, "StoreVar index is bigger than the length of the call stack" if idx + self.frames[^1] <= self.calls.high().uint: 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(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 PopN: # Pops N elements off the call stack for _ in 0.. self.pop())) of LessThan: self.push(self.getBool(self.pop() < self.pop())) of GreaterOrEqual: self.push(self.getBool(self.pop() !>= self.pop())) of LessOrEqual: self.push(self.getBool(self.pop() <= self.pop())) of PrintInt64: # Prints the value at the top of the stack # as an int64 echo int64(self.pop()) of PrintUInt64: # Prints the value at the top of the stack echo self.pop() of PrintInt32: echo int32(self.pop()) of PrintUInt32: echo uint32(self.pop()) of PrintInt16: echo int16(self.pop()) of PrintUInt16: echo uint16(self.pop()) of PrintInt8: echo int8(self.pop()) of PrintUInt8: echo uint8(self.pop()) of PrintFloat32: echo cast[float32](self.pop()) of PrintFloat64: echo cast[float](self.pop()) of PrintHex: # Prints the value at the top of the stack # as a hexadecimal integer echo "0x" & self.pop().toHex().strip(chars={'0'}) of PrintBool: if self.pop().bool: echo "true" else: echo "false" of PrintInf: if self.pop() == 0x3: echo "-inf" else: echo "inf" of PrintNan: echo "nan" of PrintString: echo cast[ptr string](self.pop())[] # TODO of SysClock64: # Pushes the value of a monotonic clock # onto the operand stack. This can be used # to track system time accurately, but it # cannot be converted to a date. The number # is in seconds self.push(cast[uint64](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() {.pop.}