peon/src/backend/vm.nim

1103 lines
41 KiB
Nim

# 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 ../config
# Sorry, but there only is enough space
# for one GC in this VM :(
{.push checks:enableVMChecks.} # The VM is a critical point where checks are deleterious
when defined(gcOrc):
GC_disableOrc()
when not defined(gcArc) and not defined(gcOrc):
GC_disable()
GC_disableMarkAndSweep()
import std/math
import std/segfaults
import std/strutils
import std/sets
import std/monotimes
import ../frontend/meta/bytecode
import ../util/multibyte
when debugVM or debugMem or debugGC:
import std/strformat
import std/sequtils
import std/terminal
when debugVM:
proc clearerr(stream: File) {.header: "stdio.h", importc.}
type
ObjectKind* = enum
## A tag for heap-allocated
## peon objects
String, List,
Dict, Tuple,
CustomType,
HeapObject* = object
## A tagged box for a heap-allocated
## peon object
marked*: bool # Used in the GC phase
case kind*: ObjectKind
of String:
str*: ptr UncheckedArray[char]
len*: int
else:
discard # TODO
PeonGC* = object
## A simple Mark&Sweep collector
## to manage peon's heap space.
## All heap allocation goes through
## this system and is not handled
## manually by the VM
bytesAllocated: tuple[total, current: int]
cycles: int
nextGC: int
pointers: HashSet[uint64]
objects: seq[ptr HeapObject]
PeonVM* = 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: everything
## is lost after the compilation
## phase
ip: uint64 # The instruction pointer
chunk: Chunk # The chunk of bytecode to execute
calls: seq[uint64] # The call stack
operands: seq[uint64] # The operand stack
cache: array[6, uint64] # The singletons cache
frames: seq[uint64] # Stores the bottom of stack frames
closures: seq[uint64] # Stores closure environment offsets
envs: seq[uint64] # Stores closure variables
results: seq[uint64] # Stores function return values
gc: PeonGC # A reference to the VM's garbage collector
breakpoints: seq[uint64] # Breakpoints where we call our debugger
debugNext: bool # Whether to debug the next instruction
lastDebugCommand: string # The last debugging command input by the user
# Implementation of peon's memory manager
proc newPeonGC*: PeonGC =
## Initializes a new, blank
## garbage collector
result.bytesAllocated = (0, 0)
result.objects = @[]
result.nextGC = FirstGC
result.cycles = 0
proc collect*(self: var PeonVM)
proc reallocate*(self: var PeonVM, p: pointer, oldSize: int, newSize: int): pointer =
## Simple wrapper around realloc with
## built-in garbage collection
self.gc.bytesAllocated.current += newSize - oldSize
try:
if newSize == 0 and not p.isNil():
when debugMem:
if oldSize > 1:
echo &"DEBUG - MM: Deallocating {oldSize} bytes of memory"
else:
echo "DEBUG - MM: Deallocating 1 byte of memory"
elif (oldSize > 0 and not p.isNil() and newSize > oldSize) or oldSize == 0:
when debugMem:
if oldSize == 0:
if newSize > 1:
echo &"DEBUG - MM: Allocating {newSize} bytes of memory"
else:
echo "DEBUG - MM: Allocating 1 byte of memory"
else:
echo &"DEBUG - M: Resizing {oldSize} bytes of memory to {newSize} bytes"
self.gc.bytesAllocated.total += newSize - oldSize
when debugStressGC:
self.collect()
else:
if self.gc.bytesAllocated.current > self.gc.nextGC:
self.collect()
result = realloc(p, newSize)
except NilAccessDefect:
stderr.writeLine("Peon: could not manage memory, segmentation fault")
quit(139) # For now, there's not much we can do if we can't get the memory we need, so we exit
template resizeArray*(self: var PeonVM, kind: untyped, p: pointer, oldCount, newCount: int): untyped =
## Handy template to resize a dynamic array
cast[ptr UncheckedArray[kind]](reallocate(self, p, sizeof(kind) * oldCount, sizeof(kind) * newCount))
template freeArray*(self: var PeonVM, kind: untyped, p: pointer, size: int): untyped =
## Frees a dynamic array
discard reallocate(self, p, sizeof(kind) * size, 0)
template free*(self: var PeonVM, kind: typedesc, p: pointer): untyped =
## Frees a pointer by reallocating its
## size to 0
discard reallocate(self, p, sizeof(kind), 0)
template setKind*[T, K](t: var T, kind: untyped, target: K) =
## Thanks to https://forum.nim-lang.org/t/8312
cast[ptr K](cast[int](addr t) + offsetOf(typeof(t), kind))[] = target
proc allocate*(self: var PeonVM, kind: ObjectKind, size: typedesc, count: int): ptr HeapObject {.inline.} =
## Allocates an object on the heap
result = cast[ptr HeapObject](self.reallocate(nil, 0, sizeof(HeapObject)))
setkind(result[], kind, kind)
result.marked = false
case kind:
of String:
result.str = cast[ptr UncheckedArray[char]](self.reallocate(nil, 0, sizeof(size) * count))
result.len = count
else:
discard # TODO
self.gc.objects.add(result)
self.gc.pointers.incl(cast[uint64](result))
when debugAlloc:
echo &"DEBUG - GC: Allocated new object: {result[]}"
echo &"DEBUG - GC: Current heap size: {self.gc.bytesAllocated.current}"
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
echo &"DEBUG - GC: Tracked objects: {self.gc.pointers.len()}"
echo &"DEBUG - GC: Completed GC cycles: {self.gc.cycles}"
proc mark(self: ptr HeapObject): bool =
## Marks a single object
if self.marked:
return false
self.marked = true
return true
proc markRoots(self: var PeonVM): seq[ptr HeapObject] =
## Marks root objects *not* to be
## collected by the GC and returns
## their addresses
when debugGC:
echo "DEBUG - GC: Starting mark phase"
# Unlike what bob does in his book,
# we keep track of objects in a different
# way due to the difference of our design.
# Specifically, we don't have neat structs for
# all peon objects: When we allocate() an object,
# we keep track of the small wrapper it created
# along with its type and other metadata. Then,
# we can go through the various sources of roots
# in the VM, see if they match any pointers we
# already know about (we store them in a hash set so
# it's really fast), and then we can be sure that
# anything that's in the difference (i.e. mathematical
# set difference) between our full list of pointers
# and the live ones is not a root object, so if it's
# not indirectly reachable through a root itself, it
# can be freed. I'm not sure if I can call this GC
# strategy precise, since technically there is a chance
# for a regular value to collide with one of the pointers
# we allocated and that would cause a memory leak, but
# with a 64-bit address-space it probably hardly matters,
# so I guess this is a mostly-precise Mark&Sweep collector
var live = initHashSet[uint64](self.gc.pointers.len())
for obj in self.calls:
if obj in self.gc.pointers:
live.incl(obj)
for obj in self.operands:
if obj in self.gc.pointers:
live.incl(obj)
for obj in self.envs:
if obj in self.gc.pointers:
live.incl(obj)
# We preallocate the space on the seq
result = newSeqOfCap[ptr HeapObject](len(live))
var obj: ptr HeapObject
for p in live:
obj = cast[ptr HeapObject](p)
if obj.mark():
result.add(obj)
when debugGC:
echo &"DEBUG - GC: Marked object: {obj[]}"
when debugGC:
echo "DEBUG - GC: Mark phase complete"
proc trace(self: var PeonVM, roots: seq[ptr HeapObject]) =
## Traces references to other
## objects starting from the
## roots. The second argument
## is the output of the mark
## phase. To speak in terms
## of the tricolor abstraction,
## this is where we blacken gray
## objects
when debugGC:
echo &"DEBUG - GC: Tracing indirect references from {len(roots)} roots"
var count = 0
for root in roots:
case root.kind:
of String:
discard # Strings hold no additional references
else:
discard # TODO: Other types
when debugGC:
echo &"DEBUG - GC: Traced {count} indirect references"
proc free(self: var PeonVM, obj: ptr HeapObject) =
## Frees a single heap-allocated
## peon object and all the memory
## it directly or indirectly owns
when debugAlloc:
echo &"DEBUG - GC: Freeing object: {obj[]}"
case obj.kind:
of String:
# Strings only own their
# underlying character array
if obj.len > 0 and not obj.str.isNil():
self.freeArray(char, obj.str, obj.len)
else:
discard # TODO
self.free(HeapObject, obj)
self.gc.pointers.excl(cast[uint64](obj))
when debugAlloc:
echo &"DEBUG - GC: Current heap size: {self.gc.bytesAllocated.current}"
echo &"DEBUG - GC: Total bytes allocated: {self.gc.bytesAllocated.total}"
echo &"DEBUG - GC: Tracked objects: {self.gc.pointers.len()}"
echo &"DEBUG - GC: Completed GC cycles: {self.gc.cycles}"
proc sweep(self: var PeonVM) =
## Sweeps unmarked objects
## that have been left behind
## during the mark phase.
## This is more convoluted
## than it needs to be because
## nim disallows changing the
## size of a sequence during
## iteration
when debugGC:
echo "DEBUG - GC: Beginning sweeping phase"
var j = -1
var idx = 0
when debugGC:
var count = 0
while j < self.gc.objects.high():
inc(j)
if self.gc.objects[j].marked:
# Object is marked: don't touch it,
# but reset its mark so that it doesn't
# stay alive forever
when debugGC:
echo &"DEBUG - GC: Unmarking object: {self.gc.objects[j][]}"
self.gc.objects[j].marked = false
inc(idx)
else:
# Object is unmarked: its memory is
# fair game
self.free(self.gc.objects[idx])
self.gc.objects.delete(idx)
inc(idx)
when debugGC:
inc(count)
when debugGC:
echo &"DEBUG - GC: Swept {count} objects"
proc collect(self: var PeonVM) =
## Attempts to reclaim some
## memory from unreachable
## objects onto the heap
when debugGC:
let before = self.gc.bytesAllocated.current
let time = getMonoTime().ticks().float() / 1_000_000
echo &"DEBUG - GC: Starting collection cycle at heap size {self.gc.bytesAllocated.current}"
inc(self.gc.cycles)
self.trace(self.markRoots())
self.sweep()
self.gc.nextGC = self.gc.bytesAllocated.current * HeapGrowFactor
when debugGC:
echo &"DEBUG - GC: Collection cycle has terminated in {getMonoTime().ticks().float() / 1_000_000 - time:.2f} ms, collected {before - self.gc.bytesAllocated.current} bytes of memory in total"
echo &"DEBUG - GC: Next cycle at {self.gc.nextGC} bytes"
# Implementation of the peon VM
proc initCache*(self: var PeonVM) =
## Initializes the VM's
## singletons cache
self.cache[0] = 0x0 # False
self.cache[1] = 0x1 # True
self.cache[2] = 0x2 # Nil
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
result.ip = 0
result.initCache()
result.gc = newPeonGC()
result.frames = @[]
result.operands = @[]
result.results = @[]
result.envs = @[]
result.calls = @[]
# Getters for singleton types
{.push inline.}
proc getNil*(self: var PeonVM): uint64 = self.cache[2]
proc getBool*(self: var PeonVM, value: bool): uint64 =
if value:
return self.cache[1]
return self.cache[0]
proc getInf*(self: var PeonVM, positive: bool): uint64 =
if positive:
return self.cache[3]
return self.cache[4]
proc getNan*(self: var 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 are frame-relative,
# meaning that the index is added to the current stack frame's
# bottom to obtain an absolute stack index
proc push(self: var PeonVM, obj: uint64) =
## Pushes a value object onto the
## operand stack
self.operands.add(obj)
proc pop(self: var 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(^(-int(distance)))
return self.operands[self.operands.high() + distance]
proc pushc(self: var PeonVM, val: uint64) =
## Pushes a value onto the
## call stack
self.calls.add(val)
proc popc(self: var 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: int): uint64 =
## Getter method that abstracts
## indexing our call stack through
## stack frames
return self.calls[idx.uint64 + self.frames[^1]]
proc setc(self: var PeonVM, idx: int, val: uint64) =
## Setter method that abstracts
## indexing our call stack through
## stack frames
self.calls[idx.uint + self.frames[^1]] = val
proc getClosure(self: PeonVM, idx: int): uint64 =
## Getter method that abstracts
## indexing closure environments
return self.envs[idx.uint + self.closures[^1]]
proc setClosure(self: var PeonVM, idx: int, val: uint64) =
## Setter method that abstracts
## indexing closure environments
if idx == self.envs.len():
self.envs.add(val)
else:
self.envs[idx.uint + self.closures[^1]] = val
proc popClosure(self: var PeonVM, idx: int): uint64 =
## Pop method that abstracts
## popping values off closure
## environments
var idx = idx.uint + self.closures[^1]
result = self.envs[idx]
self.envs.delete(idx)
# Byte-level primitives to read and decode
# bytecode
proc readByte(self: var 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: var 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: var 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: var PeonVM): uint32 {.used.} =
## 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: var 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: var 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: var 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: var 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: var 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: var 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: var 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: var 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: var 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: var 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))
proc constReadString(self: var PeonVM, size, idx: int): ptr HeapObject =
## Reads a constant from the
## chunk's constant table and
## returns it as a pointer to
## a heap-allocated string
let str = self.chunk.consts[idx..<idx + size].fromBytes()
result = self.allocate(String, char, len(str))
for i, c in str:
result.str[i] = c
{.pop.}
when debugVM: # So nim shuts up
proc debug(self: var 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, ")"
var command = ""
while true:
stdout.styledWrite(fgGreen, "=> ")
stdout.flushFile()
try:
command = readLine(stdin)
except EOFError:
styledEcho(fgYellow, "Use Ctrl+C to exit")
clearerr(stdin)
break
except IOError:
styledEcho(fgRed, "An error occurred while reading command: ", fgYellow, getCurrentExceptionMsg())
break
if command == "":
if self.lastDebugCommand == "":
command = "n"
else:
command = self.lastDebugCommand
case command:
of "n", "next":
self.debugNext = true
break
of "c", "continue":
self.debugNext = false
break
of "s", "stack":
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, "]"
of "o", "operands":
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, "]"
of "f", "frame":
stdout.styledWrite(fgCyan, "Current Frame: ", fgMagenta, "[")
if self.frames.len() > 0:
for i, e in self.calls[self.frames[^1]..^1]:
stdout.styledWrite(fgYellow, $e)
if i < (self.calls.high() - self.frames[^1].int):
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]", fgCyan
of "frames":
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, "]"
of "cl", "closures":
stdout.styledWrite(fgBlue, "Closure offsets: ", fgMagenta, "[")
for i, e in self.closures:
stdout.styledWrite(fgYellow, $e)
if i < self.closures.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
of "e", "env", "environments":
stdout.styledWrite(fgGreen, "Environments: ", fgMagenta, "[")
for i, e in self.envs:
stdout.styledWrite(fgYellow, $e)
if i < self.envs.high():
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]"
of "r", "results":
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, "]"
of "clear":
stdout.write("\x1Bc")
else:
styledEcho(fgRed, "Unknown command ", fgYellow, &"'{command}'")
proc dispatch*(self: var PeonVM) =
## Main bytecode dispatch loop
var instruction {.register.}: OpCode
while true:
{.computedgoto.} # https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
when debugVM:
if self.ip in self.breakpoints or self.breakpoints.len() == 0 or self.debugNext:
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 LoadNInf:
self.push(self.getInf(false))
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:
self.push(cast[uint64](self.constReadString(int(self.readLong()), int(self.readLong()))))
# We cast instead of converting because, unlike with integers,
# we don't want nim to touch any of the bits of the underlying
# value!
of LoadFloat32:
self.push(cast[uint64](self.constReadFloat32(int(self.readLong()))))
of LoadFloat64:
self.push(cast[uint64](self.constReadFloat64(int(self.readLong()))))
of Call:
# Calls a peon function. The calling convention here
# is pretty simple: the first value in the frame is
# the new instruction pointer to jump to, then a
# 64-bit return address follows. 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
let argc = self.readLong().int
let retAddr = self.peek(-argc - 1) # Return address
let jmpAddr = self.peek(-argc - 2) # Function address
self.ip = jmpAddr
self.pushc(jmpAddr)
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(uint64(self.calls.len() - 2))
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())
# Pops the function and return address
# off the operand stack since they're
# not needed there anymore
discard self.pop()
discard self.pop()
of CallClosure:
# Calls a peon closure. The code here is
# mostly identical to the one for Call,
# but we also create a new environment
# containing the function's closed-over variables
let argc = self.readLong().int
let offset = self.readLong().uint64
let retAddr = self.peek(-argc - 1) # Return address
let jmpAddr = self.peek(-argc - 2) # Function address
self.ip = jmpAddr
self.pushc(jmpAddr)
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(uint64(self.calls.len() - 2))
self.closures.add(offset - 1)
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())
# Pops the function and return address
# off the operand stack since they're
# not needed there anymore
discard self.pop()
discard self.pop()
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
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
# We change the instruction
# pointer just now because
# if we did it beforehand,
# our readByte() call would've
# read from the wrong offset
self.ip = ret
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 debugVM:
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.int, self.pop())
else:
self.pushc(self.pop())
of LoadClosure:
# Loads a closed-over variable from the current
# environment onto the operand stack
self.push(self.getClosure(self.readLong().int))
of PopClosure:
# Discards a closed-over variable from the
# current environment
discard self.popClosure(self.readLong().int)
of StoreClosure:
# Stores/updates the value of a closed-over
# variable
let item = self.getc(self.readLong().int)
self.setClosure(self.readLong().int, item)
of LoadVar:
# Pushes a variable from the call stack
# onto the operand stack
self.push(self.getc(self.readLong().int))
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..<int(self.readShort()):
discard self.popc()
# Jump opcodes
of Jump:
# Absolute jump
self.ip = self.readLong()
of JumpForwards:
# Relative, forward-jump
self.ip += self.readLong()
of JumpBackwards:
# Relative, backward-jump
self.ip -= self.readLong()
of JumpIfFalse:
# Conditional, forward-jump
if not self.peek().bool:
self.ip += self.readLong()
of JumpIfTrue:
# Conditional (if the top of the stack
# equals true), forward-jump
let ip = self.readLong()
if self.peek().bool:
self.ip += ip
of JumpIfFalsePop:
# Conditional (if the top of the stack
# equals false), forward-jump. Always
# pops off the operand stack
let ip = self.readLong()
if not self.pop().bool:
self.ip += ip
of JumpIfFalseOrPop:
# Conditional (if the top of the stack
# equals false), forward-jump. Pops off
# the operand stack if the value at the
# top of the operand stack is true
let ip = self.readLong()
if not self.peek().bool:
self.ip += ip
else:
discard self.pop()
# Built-in operations on primitive types.
# Note that, for operations where the order
# of the operands matters, we don't need to
# swap the order of the calls to pop: this
# is because operators are handled like peon
# functions, which means the arguments are
# already reversed on the stack when we
# execute the instruction. The beauty of the
# 2's complement system is that for most integer
# types, we don't need specialized instructions
# to operate on them
of Negate:
self.push(cast[uint64](-int64(self.pop())))
of NegateFloat64:
self.push(cast[uint64](-cast[float](self.pop())))
of NegateFloat32:
self.push(cast[uint64](-cast[float32](self.pop())))
of Add:
self.push(self.pop() + self.pop())
of Subtract:
self.push(self.pop() - self.pop())
of Multiply:
self.push(self.pop() * self.pop())
of Divide:
self.push(self.pop() div self.pop())
of SignedDivide:
self.push(uint64(int64(self.pop()) div int64(self.pop())))
of AddFloat64:
self.push(cast[uint64](cast[float](self.pop()) + cast[float](self.pop())))
of SubtractFloat64:
self.push(cast[uint64](cast[float](self.pop()) - cast[float](self.pop())))
of MultiplyFloat64:
self.push(cast[uint64](cast[float](self.pop()) * cast[float](self.pop())))
of DivideFloat64:
self.push(cast[uint64](cast[float](self.pop()) / cast[float](self.pop())))
of AddFloat32:
self.push(cast[uint64](cast[float32](self.pop()) + cast[float32](self.pop())))
of SubtractFloat32:
self.push(cast[uint64](cast[float32](self.pop()) - cast[float32](self.pop())))
of MultiplyFloat32:
self.push(cast[uint64](cast[float32](self.pop()) * cast[float32](self.pop())))
of DivideFloat32:
self.push(cast[uint64](cast[float32](self.pop()) / cast[float32](self.pop())))
of Pow:
self.push(uint64(self.pop() ^ self.pop()))
of SignedPow:
self.push(uint64(int64(self.pop()) ^ int64(self.pop())))
of PowFloat64:
self.push(cast[uint64](pow(cast[float](self.pop()), cast[float](self.pop()))))
of PowFloat32:
self.push(cast[uint64](pow(cast[float](self.pop()), cast[float](self.pop()))))
of Mod:
self.push(uint64(self.pop() mod self.pop()))
of SignedMod:
self.push(uint64(int64(self.pop()) mod int64(self.pop())))
of ModFloat64:
self.push(cast[uint64](floorMod(cast[float](self.pop()), cast[float](self.pop()))))
of ModFloat32:
self.push(cast[uint64](floorMod(cast[float](self.pop()), cast[float](self.pop()))))
of LShift:
self.push(self.pop() shl self.pop())
of RShift:
self.push(self.pop() shr self.pop())
of Xor:
self.push(self.pop() xor self.pop())
of Not:
self.push(not self.pop())
of And:
self.push(self.pop() and self.pop())
# Comparison opcodes
of Equal:
self.push(self.getBool(self.pop() == self.pop()))
of NotEqual:
self.push(self.getBool(self.pop() != self.pop()))
of GreaterThan:
self.push(self.getBool(self.pop() !> 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()))
# Print opcodes
of PrintInt64:
echo cast[int64](self.pop())
of PrintUInt64:
echo self.pop()
of PrintInt32:
echo cast[int32](self.pop())
of PrintUInt32:
echo uint32(self.pop())
of PrintInt16:
echo cast[int16](self.pop())
of PrintUInt16:
echo uint16(self.pop())
of PrintInt8:
echo cast[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:
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:
let s = cast[ptr HeapObject](self.pop())
for i in 0..<s.len:
stdout.write(s.str[i])
stdout.write("\n")
of SysClock64:
self.push(cast[uint64](getMonoTime().ticks.float() / 1_000_000_000))
of LogicalNot:
self.push(uint64(not self.pop().bool))
else:
discard
proc run*(self: var PeonVM, chunk: Chunk, breakpoints: seq[uint64] = @[]) =
## Executes a piece of Peon bytecode
self.chunk = chunk
self.frames = @[]
self.calls = @[]
self.operands = @[]
self.breakpoints = breakpoints
self.results = @[]
self.ip = 0
self.lastDebugCommand = ""
try:
self.dispatch()
except NilAccessDefect:
stderr.writeLine("Memory Access Violation: SIGSEGV")
quit(1)
# We clean up after ourselves!
self.collect()
{.pop.}