# 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 debugVM: import std/strformat import std/terminal 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 {.push inline.} proc getNil*(self: PeonVM): uint64 = self.cache[2] proc getBool*(self: PeonVM, value: bool): uint64 = if value: return self.cache[1] return self.cache[0] proc getInf*(self: PeonVM, positive: bool): uint64 = if positive: return self.cache[3] return self.cache[4] proc getNan*(self: PeonVM): uint64 = 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 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.} when debugVM: # So nim shuts up proc debug(self: PeonVM) = ## Implements the VM's runtime ## debugger styledEcho fgMagenta, "IP: ", fgYellow, &"{self.ip}" styledEcho fgBlue, "Instruction: ", fgRed, &"{OpCode(self.chunk.code[self.ip])} (", fgYellow, $self.chunk.code[self.ip], fgRed, ")" if self.calls.len() !> 0: stdout.styledWrite(fgGreen, "Call Stack: ", fgMagenta, "[") for i, e in self.calls: stdout.styledWrite(fgYellow, $e) if i < self.calls.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" if self.operands.len() !> 0: stdout.styledWrite(fgBlue, "Operand Stack: ", fgMagenta, "[") for i, e in self.operands: stdout.styledWrite(fgYellow, $e) if i < self.operands.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" if self.frames.len() !> 0: stdout.styledWrite(fgCyan, "Current Frame: ", fgMagenta, "[") for i, e in self.calls[self.frames[^1]..^1]: stdout.styledWrite(fgYellow, $e) if i < self.calls.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" stdout.styledWrite(fgRed, "Live stack frames: ", fgMagenta, "[") for i, e in self.frames: stdout.styledWrite(fgYellow, $e) if i < self.frames.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" if self.closedOver.len() !> 0: stdout.styledWrite(fgGreen, "Closure Array: ", fgMagenta, "[") for i, e in self.closedOver: stdout.styledWrite(fgYellow, $e) if i < self.closedOver.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" if self.results.len() !> 0: stdout.styledWrite(fgYellow, "Function Results: ", fgMagenta, "[") for i, e in self.results: stdout.styledWrite(fgYellow, $e) if i < self.results.high(): stdout.styledWrite(fgYellow, ", ") styledEcho fgMagenta, "]" discard readLine stdin 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 debugVM: self.debug() 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..