Stopped using ref objects and removed recursive dependency between gc and VM

This commit is contained in:
Mattia Giambirtone 2022-11-02 13:16:43 +01:00
parent e046981f4b
commit c1deebf43b
3 changed files with 141 additions and 127 deletions

View File

@ -45,7 +45,33 @@ when debugVM:
type type
PeonVM* = ref object 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]
nextGC: int
pointers: HashSet[uint64]
objects: seq[ptr HeapObject]
PeonVM* = object
## The Peon Virtual Machine. ## The Peon Virtual Machine.
## Note how the only data ## Note how the only data
## type we handle here is ## type we handle here is
@ -72,34 +98,6 @@ type
breakpoints: seq[uint64] # Breakpoints where we call our debugger breakpoints: seq[uint64] # Breakpoints where we call our debugger
debugNext: bool # Whether to debug the next instruction debugNext: bool # Whether to debug the next instruction
lastDebugCommand: string # The last debugging command input by the user lastDebugCommand: string # The last debugging command input by the user
ObjectKind* = enum
## A tag for heap-allocated
## peon objects
String, List,
Dict, Tuple,
CustomType,
Closure
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* = ref 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
vm: PeonVM
bytesAllocated: tuple[total, current: int]
nextGC: int
pointers: HashSet[uint64]
objects: seq[ptr HeapObject]
# Implementation of peon's memory manager # Implementation of peon's memory manager
@ -107,19 +105,18 @@ type
proc newPeonGC*: PeonGC = proc newPeonGC*: PeonGC =
## Initializes a new, blank ## Initializes a new, blank
## garbage collector ## garbage collector
new(result)
result.bytesAllocated = (0, 0) result.bytesAllocated = (0, 0)
result.objects = @[] result.objects = @[]
result.nextGC = FirstGC result.nextGC = FirstGC
proc collect*(self: PeonGC) proc collect*(self: var PeonVM)
proc reallocate*(self: PeonGC, p: pointer, oldSize: int, newSize: int): pointer = proc reallocate*(self: var PeonVM, p: pointer, oldSize: int, newSize: int): pointer =
## Simple wrapper around realloc/dealloc with ## Simple wrapper around realloc/dealloc with
## built-in garbage collection ## built-in garbage collection
self.bytesAllocated.current += newSize - oldSize self.gc.bytesAllocated.current += newSize - oldSize
try: try:
if newSize == 0 and not p.isNil(): if newSize == 0 and not p.isNil():
when debugMem: when debugMem:
@ -129,11 +126,11 @@ proc reallocate*(self: PeonGC, p: pointer, oldSize: int, newSize: int): pointer
echo "DEBUG - Memory manager: Deallocating 1 byte of memory" echo "DEBUG - Memory manager: Deallocating 1 byte of memory"
dealloc(p) dealloc(p)
elif (oldSize > 0 and not p.isNil() and newSize > oldSize) or oldSize == 0: elif (oldSize > 0 and not p.isNil() and newSize > oldSize) or oldSize == 0:
self.bytesAllocated.total += newSize - oldSize self.gc.bytesAllocated.total += newSize - oldSize
when debugStressGC: when debugStressGC:
self.collect() self.collect()
else: else:
if self.bytesAllocated.current > self.nextGC: if self.gc.bytesAllocated.current > self.gc.nextGC:
self.collect() self.collect()
when debugMem: when debugMem:
if oldSize == 0: if oldSize == 0:
@ -154,37 +151,38 @@ proc reallocate*(self: PeonGC, p: pointer, oldSize: int, newSize: int): pointer
quit(139) # For now, there's not much we can do if we can't get the memory we need, so we exit 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: PeonGC, kind: untyped, p: pointer, oldCount, newCount: int): untyped =
template resizeArray*(self: var PeonVM, kind: untyped, p: pointer, oldCount, newCount: int): untyped =
## Handy template to resize a dynamic array ## Handy template to resize a dynamic array
cast[ptr UncheckedArray[kind]](reallocate(self, p, sizeof(kind) * oldCount, sizeof(kind) * newCount)) cast[ptr UncheckedArray[kind]](reallocate(self, p, sizeof(kind) * oldCount, sizeof(kind) * newCount))
template freeArray*(self: PeonGC, kind: untyped, p: pointer, size: int): untyped = template freeArray*(self: var PeonVM, kind: untyped, p: pointer, size: int): untyped =
## Frees a dynamic array ## Frees a dynamic array
discard reallocate(self, p, sizeof(kind) * size, 0) discard reallocate(self, p, sizeof(kind) * size, 0)
template free*(self: PeonGC, kind: typedesc, p: pointer): untyped = template free*(self: var PeonVM, kind: typedesc, p: pointer): untyped =
## Frees a pointer by reallocating its ## Frees a pointer by reallocating its
## size to 0 ## size to 0
discard reallocate(self, p, sizeof(kind), 0) discard reallocate(self, p, sizeof(kind), 0)
proc allocate*(self: PeonGC, kind: ObjectKind, size: typedesc, count: int): ptr HeapObject {.inline.} = proc allocate*(self: var PeonVM, kind: ObjectKind, size: typedesc, count: int): ptr HeapObject {.inline.} =
## Allocates aobject on the heap ## Allocates aobject on the heap
result = cast[ptr HeapObject](self.reallocate(nil, 0, sizeof(HeapObject))) result = cast[ptr HeapObject](self.reallocate(nil, 0, sizeof(HeapObject)))
result.marked = false result.marked = false
self.bytesAllocated.total += sizeof(result) self.gc.bytesAllocated.total += sizeof(HeapObject)
self.bytesAllocated.current += sizeof(result) self.gc.bytesAllocated.current += sizeof(HeapObject)
case kind: case kind:
of String: of String:
result.str = cast[ptr UncheckedArray[char]](self.reallocate(nil, 0, sizeof(size) * count)) result.str = cast[ptr UncheckedArray[char]](self.reallocate(nil, 0, sizeof(size) * count))
result.len = count result.len = count
self.bytesAllocated.current += sizeof(size) * count self.gc.bytesAllocated.current += sizeof(size) * count
else: else:
discard # TODO discard # TODO
self.objects.add(result) self.gc.objects.add(result)
self.pointers.incl(cast[uint64](result)) self.gc.pointers.incl(cast[uint64](result))
proc mark(self: ptr HeapObject): bool = proc mark(self: ptr HeapObject): bool =
@ -195,7 +193,7 @@ proc mark(self: ptr HeapObject): bool =
return true return true
proc markRoots(self: PeonGC): seq[ptr HeapObject] = proc markRoots(self: var PeonVM): seq[ptr HeapObject] =
## Marks root objects *not* to be ## Marks root objects *not* to be
## collected by the GC and returns ## collected by the GC and returns
## their addresses ## their addresses
@ -222,15 +220,15 @@ proc markRoots(self: PeonGC): seq[ptr HeapObject] =
# we allocated and that would cause a memory leak, but # we allocated and that would cause a memory leak, but
# with a 64-bit address-space it probably hardly matters, # with a 64-bit address-space it probably hardly matters,
# so I guess this is a mostly-precise Mark&Sweep collector # so I guess this is a mostly-precise Mark&Sweep collector
var live = initHashSet[uint64](self.pointers.len()) var live = initHashSet[uint64](self.gc.pointers.len())
for obj in self.vm.calls: for obj in self.calls:
if obj in self.pointers: if obj in self.gc.pointers:
live.incl(obj) live.incl(obj)
for obj in self.vm.operands: for obj in self.operands:
if obj in self.pointers: if obj in self.gc.pointers:
live.incl(obj) live.incl(obj)
for obj in self.vm.envs: for obj in self.envs:
if obj in self.pointers: if obj in self.gc.pointers:
live.incl(obj) live.incl(obj)
# We preallocate the space on the seq # We preallocate the space on the seq
result = newSeqOfCap[ptr HeapObject](len(live)) result = newSeqOfCap[ptr HeapObject](len(live))
@ -245,7 +243,7 @@ proc markRoots(self: PeonGC): seq[ptr HeapObject] =
echo "DEBUG - GC: Mark phase complete" echo "DEBUG - GC: Mark phase complete"
proc trace(self: PeonGC, roots: seq[ptr HeapObject]) = proc trace(self: var PeonVM, roots: seq[ptr HeapObject]) =
## Traces references to other ## Traces references to other
## objects starting from the ## objects starting from the
## roots. The second argument ## roots. The second argument
@ -267,7 +265,7 @@ proc trace(self: PeonGC, roots: seq[ptr HeapObject]) =
echo &"DEBUG - GC: Traced {count} indirect references" echo &"DEBUG - GC: Traced {count} indirect references"
proc free(self: PeonGC, obj: ptr HeapObject) = proc free(self: var PeonVM, obj: ptr HeapObject) =
## Frees a single heap-allocated ## Frees a single heap-allocated
## peon object and all the memory ## peon object and all the memory
## it directly or indirectly owns ## it directly or indirectly owns
@ -282,10 +280,10 @@ proc free(self: PeonGC, obj: ptr HeapObject) =
else: else:
discard # TODO discard # TODO
self.free(HeapObject, obj) self.free(HeapObject, obj)
self.pointers.excl(cast[uint64](obj)) self.gc.pointers.excl(cast[uint64](obj))
proc sweep(self: PeonGC) = proc sweep(self: var PeonVM) =
## Sweeps unmarked objects ## Sweeps unmarked objects
## that have been left behind ## that have been left behind
## during the mark phase. ## during the mark phase.
@ -300,21 +298,21 @@ proc sweep(self: PeonGC) =
var idx = 0 var idx = 0
when debugGC: when debugGC:
var count = 0 var count = 0
while j < self.objects.high(): while j < self.gc.objects.high():
inc(j) inc(j)
if self.objects[j].marked: if self.gc.objects[j].marked:
# Object is marked: don't touch it, # Object is marked: don't touch it,
# but reset its mark so that it doesn't # but reset its mark so that it doesn't
# stay alive forever # stay alive forever
when debugGC: when debugGC:
echo &"DEBUG - GC: Unmarking object: {self.objects[j][]}" echo &"DEBUG - GC: Unmarking object: {self.gc.objects[j][]}"
self.objects[j].marked = false self.gc.objects[j].marked = false
inc(idx) inc(idx)
else: else:
# Object is unmarked: its memory is # Object is unmarked: its memory is
# fair game # fair game
self.free(self.objects[idx]) self.free(self.gc.objects[idx])
self.objects.delete(idx) self.gc.objects.delete(idx)
inc(idx) inc(idx)
when debugGC: when debugGC:
inc(count) inc(count)
@ -322,25 +320,25 @@ proc sweep(self: PeonGC) =
echo &"DEBUG - GC: Swept {count} objects" echo &"DEBUG - GC: Swept {count} objects"
proc collect(self: PeonGC) = proc collect(self: var PeonVM) =
## Attempts to reclaim some ## Attempts to reclaim some
## memory from unreachable ## memory from unreachable
## objects onto the heap ## objects onto the heap
when debugGC: when debugGC:
let before = self.bytesAllocated.current let before = self.gc.bytesAllocated.current
let time = getMonoTime().ticks().float() / 1_000_000 let time = getMonoTime().ticks().float() / 1_000_000
echo &"DEBUG - GC: Starting collection cycle at heap size {self.bytesAllocated.current}" echo &"DEBUG - GC: Starting collection cycle at heap size {self.gc.bytesAllocated.current}"
self.trace(self.markRoots()) self.trace(self.markRoots())
self.sweep() self.sweep()
self.nextGC = self.bytesAllocated.current * HeapGrowFactor self.gc.nextGC = self.gc.bytesAllocated.current * HeapGrowFactor
when debugGC: when debugGC:
echo &"DEBUG - GC: Collection cycle has terminated in {getMonoTime().ticks().float() / 1_000_000 - time:.2f} ms, collected {before - self.bytesAllocated.current} bytes of memory in total" 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.nextGC} bytes" echo &"DEBUG - GC: Next cycle at {self.gc.nextGC} bytes"
# Implementation of the peon VM # Implementation of the peon VM
proc initCache*(self: PeonVM) = proc initCache*(self: var PeonVM) =
## Initializes the VM's ## Initializes the VM's
## singletons cache ## singletons cache
self.cache[0] = 0x0 # False self.cache[0] = 0x0 # False
@ -354,7 +352,6 @@ proc initCache*(self: PeonVM) =
proc newPeonVM*: PeonVM = proc newPeonVM*: PeonVM =
## Initializes a new, blank VM ## Initializes a new, blank VM
## for executing Peon bytecode ## for executing Peon bytecode
new(result)
result.ip = 0 result.ip = 0
result.initCache() result.initCache()
result.gc = newPeonGC() result.gc = newPeonGC()
@ -363,25 +360,24 @@ proc newPeonVM*: PeonVM =
result.results = @[] result.results = @[]
result.envs = @[] result.envs = @[]
result.calls = @[] result.calls = @[]
result.gc.vm = result
# Getters for singleton types # Getters for singleton types
{.push inline.} {.push inline.}
proc getNil*(self: PeonVM): uint64 = self.cache[2] proc getNil*(self: var PeonVM): uint64 = self.cache[2]
proc getBool*(self: PeonVM, value: bool): uint64 = proc getBool*(self: var PeonVM, value: bool): uint64 =
if value: if value:
return self.cache[1] return self.cache[1]
return self.cache[0] return self.cache[0]
proc getInf*(self: PeonVM, positive: bool): uint64 = proc getInf*(self: var PeonVM, positive: bool): uint64 =
if positive: if positive:
return self.cache[3] return self.cache[3]
return self.cache[4] return self.cache[4]
proc getNan*(self: PeonVM): uint64 = self.cache[5] proc getNan*(self: var PeonVM): uint64 = self.cache[5]
# Thanks to nim's *genius* idea of making x !> y a template # Thanks to nim's *genius* idea of making x !> y a template
@ -403,13 +399,13 @@ proc `!>=`[T](a, b: T): auto {.inline, used.} =
# that go through the getc/setc wrappers are frame-relative, # that go through the getc/setc wrappers are frame-relative,
# meaning that the index is added to the current stack frame's # meaning that the index is added to the current stack frame's
# bottom to obtain an absolute stack index # bottom to obtain an absolute stack index
proc push(self: PeonVM, obj: uint64) = proc push(self: var PeonVM, obj: uint64) =
## Pushes a value object onto the ## Pushes a value object onto the
## operand stack ## operand stack
self.operands.add(obj) self.operands.add(obj)
proc pop(self: PeonVM): uint64 = proc pop(self: var PeonVM): uint64 =
## Pops a value off the operand ## Pops a value off the operand
## stack and returns it ## stack and returns it
return self.operands.pop() return self.operands.pop()
@ -431,13 +427,13 @@ proc peek(self: PeonVM, distance: int = 0): uint64 =
return self.operands[self.operands.high() + distance] return self.operands[self.operands.high() + distance]
proc pushc(self: PeonVM, val: uint64) = proc pushc(self: var PeonVM, val: uint64) =
## Pushes a value onto the ## Pushes a value onto the
## call stack ## call stack
self.calls.add(val) self.calls.add(val)
proc popc(self: PeonVM): uint64 = proc popc(self: var PeonVM): uint64 =
## Pops a value off the call ## Pops a value off the call
## stack and returns it ## stack and returns it
return self.calls.pop() return self.calls.pop()
@ -457,7 +453,7 @@ proc getc(self: PeonVM, idx: int): uint64 =
return self.calls[idx.uint64 + self.frames[^1]] return self.calls[idx.uint64 + self.frames[^1]]
proc setc(self: PeonVM, idx: int, val: uint64) = proc setc(self: var PeonVM, idx: int, val: uint64) =
## Setter method that abstracts ## Setter method that abstracts
## indexing our call stack through ## indexing our call stack through
## stack frames ## stack frames
@ -470,7 +466,7 @@ proc getClosure(self: PeonVM, idx: int): uint64 =
return self.envs[idx.uint + self.closures[^1]] return self.envs[idx.uint + self.closures[^1]]
proc setClosure(self: PeonVM, idx: int, val: uint64) = proc setClosure(self: var PeonVM, idx: int, val: uint64) =
## Setter method that abstracts ## Setter method that abstracts
## indexing closure environments ## indexing closure environments
if idx == self.envs.len(): if idx == self.envs.len():
@ -479,7 +475,7 @@ proc setClosure(self: PeonVM, idx: int, val: uint64) =
self.envs[idx.uint + self.closures[^1]] = val self.envs[idx.uint + self.closures[^1]] = val
proc popClosure(self: PeonVM, idx: int): uint64 = proc popClosure(self: var PeonVM, idx: int): uint64 =
## Pop method that abstracts ## Pop method that abstracts
## popping values off closure ## popping values off closure
## environments ## environments
@ -491,7 +487,7 @@ proc popClosure(self: PeonVM, idx: int): uint64 =
# Byte-level primitives to read and decode # Byte-level primitives to read and decode
# bytecode # bytecode
proc readByte(self: PeonVM): uint8 = proc readByte(self: var PeonVM): uint8 =
## Reads a single byte from the ## Reads a single byte from the
## bytecode and returns it as an ## bytecode and returns it as an
## unsigned 8 bit integer ## unsigned 8 bit integer
@ -499,7 +495,7 @@ proc readByte(self: PeonVM): uint8 =
return self.chunk.code[self.ip - 1] return self.chunk.code[self.ip - 1]
proc readShort(self: PeonVM): uint16 = proc readShort(self: var PeonVM): uint16 =
## Reads two bytes from the ## Reads two bytes from the
## bytecode and returns them ## bytecode and returns them
## as an unsigned 16 bit ## as an unsigned 16 bit
@ -507,7 +503,7 @@ proc readShort(self: PeonVM): uint16 =
return [self.readByte(), self.readByte()].fromDouble() return [self.readByte(), self.readByte()].fromDouble()
proc readLong(self: PeonVM): uint32 = proc readLong(self: var PeonVM): uint32 =
## Reads three bytes from the ## Reads three bytes from the
## bytecode and returns them ## bytecode and returns them
## as an unsigned 32 bit ## as an unsigned 32 bit
@ -517,7 +513,7 @@ proc readLong(self: PeonVM): uint32 =
return uint32([self.readByte(), self.readByte(), self.readByte()].fromTriple()) return uint32([self.readByte(), self.readByte(), self.readByte()].fromTriple())
proc readUInt(self: PeonVM): uint32 {.used.} = proc readUInt(self: var PeonVM): uint32 {.used.} =
## Reads three bytes from the ## Reads three bytes from the
## bytecode and returns them ## bytecode and returns them
## as an unsigned 32 bit ## as an unsigned 32 bit
@ -528,7 +524,7 @@ proc readUInt(self: PeonVM): uint32 {.used.} =
# Functions to read primitives from the chunk's # Functions to read primitives from the chunk's
# constants table # constants table
proc constReadInt64(self: PeonVM, idx: int): int64 = proc constReadInt64(self: var PeonVM, idx: int): int64 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an int64 ## returns it as an int64
@ -540,7 +536,7 @@ proc constReadInt64(self: PeonVM, idx: int): int64 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadUInt64(self: PeonVM, idx: int): uint64 = proc constReadUInt64(self: var PeonVM, idx: int): uint64 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an uint64 ## returns it as an uint64
@ -552,7 +548,7 @@ proc constReadUInt64(self: PeonVM, idx: int): uint64 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadUInt32(self: PeonVM, idx: int): uint32 = proc constReadUInt32(self: var PeonVM, idx: int): uint32 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an int32 ## returns it as an int32
@ -561,7 +557,7 @@ proc constReadUInt32(self: PeonVM, idx: int): uint32 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadInt32(self: PeonVM, idx: int): int32 = proc constReadInt32(self: var PeonVM, idx: int): int32 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an uint32 ## returns it as an uint32
@ -570,7 +566,7 @@ proc constReadInt32(self: PeonVM, idx: int): int32 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadInt16(self: PeonVM, idx: int): int16 = proc constReadInt16(self: var PeonVM, idx: int): int16 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an int16 ## returns it as an int16
@ -578,7 +574,7 @@ proc constReadInt16(self: PeonVM, idx: int): int16 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadUInt16(self: PeonVM, idx: int): uint16 = proc constReadUInt16(self: var PeonVM, idx: int): uint16 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an uint16 ## returns it as an uint16
@ -586,21 +582,21 @@ proc constReadUInt16(self: PeonVM, idx: int): uint16 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadInt8(self: PeonVM, idx: int): int8 = proc constReadInt8(self: var PeonVM, idx: int): int8 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an int8 ## returns it as an int8
result = int8(self.chunk.consts[idx]) result = int8(self.chunk.consts[idx])
proc constReadUInt8(self: PeonVM, idx: int): uint8 = proc constReadUInt8(self: var PeonVM, idx: int): uint8 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as an uint8 ## returns it as an uint8
result = self.chunk.consts[idx] result = self.chunk.consts[idx]
proc constReadFloat32(self: PeonVM, idx: int): float32 = proc constReadFloat32(self: var PeonVM, idx: int): float32 =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as a float32 ## returns it as a float32
@ -609,7 +605,7 @@ proc constReadFloat32(self: PeonVM, idx: int): float32 =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadFloat64(self: PeonVM, idx: int): float = proc constReadFloat64(self: var PeonVM, idx: int): float =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as a float ## returns it as a float
@ -620,13 +616,13 @@ proc constReadFloat64(self: PeonVM, idx: int): float =
copyMem(result.addr, arr.addr, sizeof(arr)) copyMem(result.addr, arr.addr, sizeof(arr))
proc constReadString(self: PeonVM, size, idx: int): ptr HeapObject = proc constReadString(self: var PeonVM, size, idx: int): ptr HeapObject =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
## returns it as a pointer to ## returns it as a pointer to
## a heap-allocated string ## a heap-allocated string
let str = self.chunk.consts[idx..<idx + size].fromBytes() let str = self.chunk.consts[idx..<idx + size].fromBytes()
result = self.gc.allocate(String, char, len(str)) result = self.allocate(String, char, len(str))
for i, c in str: for i, c in str:
result.str[i] = c result.str[i] = c
when debugAlloc: when debugAlloc:
@ -636,7 +632,7 @@ proc constReadString(self: PeonVM, size, idx: int): ptr HeapObject =
when debugVM: # So nim shuts up when debugVM: # So nim shuts up
proc debug(self: PeonVM) = proc debug(self: var PeonVM) =
## Implements the VM's runtime ## Implements the VM's runtime
## debugger ## debugger
styledEcho fgMagenta, "IP: ", fgYellow, &"{self.ip}" styledEcho fgMagenta, "IP: ", fgYellow, &"{self.ip}"
@ -721,7 +717,7 @@ when debugVM: # So nim shuts up
styledEcho(fgRed, "Unknown command ", fgYellow, &"'{command}'") styledEcho(fgRed, "Unknown command ", fgYellow, &"'{command}'")
proc dispatch*(self: PeonVM) = proc dispatch*(self: var PeonVM) =
## Main bytecode dispatch loop ## Main bytecode dispatch loop
var instruction {.register.}: OpCode var instruction {.register.}: OpCode
while true: while true:
@ -1074,7 +1070,7 @@ proc dispatch*(self: PeonVM) =
discard discard
proc run*(self: PeonVM, chunk: Chunk, breakpoints: seq[uint64] = @[]) = proc run*(self: var PeonVM, chunk: Chunk, breakpoints: seq[uint64] = @[]) =
## Executes a piece of Peon bytecode ## Executes a piece of Peon bytecode
self.chunk = chunk self.chunk = chunk
self.frames = @[] self.frames = @[]
@ -1090,7 +1086,7 @@ proc run*(self: PeonVM, chunk: Chunk, breakpoints: seq[uint64] = @[]) =
stderr.writeLine("Memory Access Violation: SIGSEGV") stderr.writeLine("Memory Access Violation: SIGSEGV")
quit(1) quit(1)
# We clean up after ourselves! # We clean up after ourselves!
self.gc.collect() self.collect()
{.pop.} {.pop.}

View File

@ -161,7 +161,7 @@ type
names: seq[Name] names: seq[Name]
# The current scope depth. If > 0, we're # The current scope depth. If > 0, we're
# in a local scope, otherwise it's global # in a local scope, otherwise it's global
scopeDepth: int depth: int
# Scope ownership data # Scope ownership data
scopeOwners: seq[tuple[owner: Name, depth: int]] scopeOwners: seq[tuple[owner: Name, depth: int]]
# The current function being compiled # The current function being compiled
@ -254,7 +254,7 @@ proc newCompiler*(replMode: bool = false): Compiler =
result.current = 0 result.current = 0
result.file = "" result.file = ""
result.names = @[] result.names = @[]
result.scopeDepth = 0 result.depth = 0
result.lines = @[] result.lines = @[]
result.jumps = @[] result.jumps = @[]
result.currentFunction = nil result.currentFunction = nil
@ -567,21 +567,38 @@ proc getStackPos(self: Compiler, name: Name): int =
result = 2 result = 2
for variable in self.names: for variable in self.names:
if variable.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum, NameKind.Function, NameKind.None]: if variable.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum, NameKind.Function, NameKind.None]:
# These names don't have a runtime representation on the call stack, so we skip them
continue continue
elif variable.kind == NameKind.Argument and variable.depth > self.scopeDepth: elif variable.kind == NameKind.Argument and variable.depth > self.depth:
# Argument of a function we haven't compiled yet (or one that we're
# not in). Ignore it, as it won't exist at runtime
continue continue
elif not variable.belongsTo.isNil() and variable.belongsTo.valueType.isBuiltinFunction: elif not variable.belongsTo.isNil() and variable.belongsTo.valueType.isBuiltinFunction:
# Builtin functions don't exist at runtime either, so variables belonging to them
# are not present in the stack
continue continue
elif not variable.valueType.isNil() and variable.valueType.kind == Generic: elif not variable.valueType.isNil() and variable.valueType.kind == Generic:
# Generics are also a purely compile-time construct and are therefore
# ignored as far as stack positioning goes
continue continue
elif variable.owner != self.currentModule: elif variable.owner != self.currentModule:
continue # We don't own this variable, so we check
if name.ident == variable.ident: # if the owner exported it to us. If not,
# we skip it and pretend it doesn't exist
if variable.isPrivate or not variable.exported:
continue
elif not variable.resolved:
dec(result)
elif name.ident == variable.ident:
# After all of these checks, we can
# finally check whether the name of
# the two variables match and if they
# do, bingo: We got our name object
found = true found = true
break break
inc(result) inc(result)
if not found: if not found:
return -1 result = -1
proc getClosurePos(self: Compiler, name: Name): int = proc getClosurePos(self: Compiler, name: Name): int =
@ -1088,8 +1105,8 @@ proc handleBuiltinFunction(self: Compiler, fn: Type, args: seq[Expression], line
proc beginScope(self: Compiler) = proc beginScope(self: Compiler) =
## Begins a new local scope by incrementing the current ## Begins a new local scope by incrementing the current
## scope's depth ## scope's depth
inc(self.scopeDepth) inc(self.depth)
self.scopeOwners.add((self.currentFunction, self.scopeDepth)) self.scopeOwners.add((self.currentFunction, self.depth))
# Flattens our weird function tree into a linear # Flattens our weird function tree into a linear
@ -1106,26 +1123,26 @@ proc flatten(self: Type): seq[Type] = flattenImpl(self, result)
proc endScope(self: Compiler) = proc endScope(self: Compiler) =
## Ends the current local scope ## Ends the current local scope
if self.scopeDepth < 0: if self.depth < 0:
self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)") self.error("cannot call endScope with depth < 0 (This is an internal error and most likely a bug)")
discard self.scopeOwners.pop() discard self.scopeOwners.pop()
dec(self.scopeDepth) dec(self.depth)
var names: seq[Name] = @[] var names: seq[Name] = @[]
var popCount = 0 var popCount = 0
if self.scopeDepth == -1 and not self.isMainModule: if self.depth == -1 and not self.isMainModule:
# When we're compiling another module, we don't # When we're compiling another module, we don't
# close its global scope because self.compileModule() # close its global scope because self.compileModule()
# needs access to it # needs access to it
return return
for name in self.names: for name in self.names:
if name.depth > self.scopeDepth: if name.depth > self.depth:
if not name.belongsTo.isNil() and not name.belongsTo.resolved: if not name.belongsTo.isNil() and not name.belongsTo.resolved:
continue continue
names.add(name) names.add(name)
#[if not name.resolved: #[if not name.resolved:
# TODO: Emit a warning? # TODO: Emit a warning?
continue]# continue]#
if name.owner != self.currentModule and self.scopeDepth > -1: if name.owner != self.currentModule and self.depth > -1:
# Names coming from other modules only go out of scope # Names coming from other modules only go out of scope
# when the global scope is closed (i.e. at the end of # when the global scope is closed (i.e. at the end of
# the module) # the module)
@ -1229,7 +1246,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
# slap myself 100 times with a sign saying "I'm dumb". Mark my words # slap myself 100 times with a sign saying "I'm dumb". Mark my words
self.error("cannot declare more than 16777215 variables at a time") self.error("cannot declare more than 16777215 variables at a time")
declaredName = node.name.token.lexeme declaredName = node.name.token.lexeme
self.names.add(Name(depth: self.scopeDepth, self.names.add(Name(depth: self.depth,
ident: node.name, ident: node.name,
isPrivate: node.isPrivate, isPrivate: node.isPrivate,
owner: self.currentModule, owner: self.currentModule,
@ -1246,7 +1263,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
result = self.names[^1] result = self.names[^1]
of NodeKind.funDecl: of NodeKind.funDecl:
var node = FunDecl(node) var node = FunDecl(node)
result = Name(depth: self.scopeDepth, result = Name(depth: self.depth,
isPrivate: node.isPrivate, isPrivate: node.isPrivate,
isConst: false, isConst: false,
owner: self.currentModule, owner: self.currentModule,
@ -1304,7 +1321,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
var node = ImportStmt(node) var node = ImportStmt(node)
var name = node.moduleName.token.lexeme.extractFilename().replace(".pn", "") var name = node.moduleName.token.lexeme.extractFilename().replace(".pn", "")
declaredName = name declaredName = name
self.names.add(Name(depth: self.scopeDepth, self.names.add(Name(depth: self.depth,
owner: self.currentModule, owner: self.currentModule,
ident: newIdentExpr(Token(kind: Identifier, lexeme: name, line: node.moduleName.token.line)), ident: newIdentExpr(Token(kind: Identifier, lexeme: name, line: node.moduleName.token.line)),
line: node.moduleName.token.line, line: node.moduleName.token.line,
@ -1317,7 +1334,7 @@ proc declareName(self: Compiler, node: ASTNode, mutable: bool = false): Name =
for name in self.findByName(declaredName): for name in self.findByName(declaredName):
if name == result: if name == result:
continue continue
elif (name.kind == NameKind.Var and name.depth == self.scopeDepth) or name.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum]: elif (name.kind == NameKind.Var and name.depth == self.depth) or name.kind in [NameKind.Module, NameKind.CustomType, NameKind.Enum]:
self.error(&"attempt to redeclare '{name.ident.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}") self.error(&"attempt to redeclare '{name.ident.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}")
@ -1576,7 +1593,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
# they're referenced # they're referenced
self.emitByte(LoadUInt64, node.token.line) self.emitByte(LoadUInt64, node.token.line)
self.emitBytes(self.chunk.writeConstant(s.codePos.toLong()), node.token.line) self.emitBytes(self.chunk.writeConstant(s.codePos.toLong()), node.token.line)
elif s.depth > 0 and self.scopeDepth > 0 and not self.currentFunction.isNil() and s.depth != self.scopeDepth and self.scopeOwners[s.depth].owner != self.currentFunction: elif s.depth > 0 and self.depth > 0 and not self.currentFunction.isNil() and s.depth != self.depth and self.scopeOwners[s.depth].owner != self.currentFunction:
# Loads a closure variable. Stored in a separate "closure array" in the VM that does not # Loads a closure variable. Stored in a separate "closure array" in the VM that does not
# align its semantics with the call stack. This makes closures work as expected and is # align its semantics with the call stack. This makes closures work as expected and is
# not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway) # not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway)
@ -1953,7 +1970,7 @@ proc breakStmt(self: Compiler, node: BreakStmt) =
## Compiles break statements. A break statement ## Compiles break statements. A break statement
## jumps to the end of the loop ## jumps to the end of the loop
self.currentLoop.breakJumps.add(self.emitJump(OpCode.JumpForwards, node.token.line)) self.currentLoop.breakJumps.add(self.emitJump(OpCode.JumpForwards, node.token.line))
if self.currentLoop.depth > self.scopeDepth: if self.currentLoop.depth > self.depth:
# Breaking out of a loop closes its scope # Breaking out of a loop closes its scope
self.endScope() self.endScope()
@ -2076,7 +2093,7 @@ proc statement(self: Compiler, node: Statement) =
# for loops to while loops # for loops to while loops
let loop = self.currentLoop let loop = self.currentLoop
self.currentLoop = Loop(start: self.chunk.code.len(), self.currentLoop = Loop(start: self.chunk.code.len(),
depth: self.scopeDepth, breakJumps: @[]) depth: self.depth, breakJumps: @[])
self.whileStmt(WhileStmt(node)) self.whileStmt(WhileStmt(node))
self.patchBreaks() self.patchBreaks()
self.currentLoop = loop self.currentLoop = loop
@ -2104,7 +2121,7 @@ proc varDecl(self: Compiler, node: VarDecl, name: Name) =
var typ: Type var typ: Type
if node.value.isNil(): if node.value.isNil():
# Variable has no value: the type declaration # Variable has no value: the type declaration
# takes over # takes over
typ = self.inferOrError(node.valueType) typ = self.inferOrError(node.valueType)
elif node.valueType.isNil: elif node.valueType.isNil:
# Variable has no type declaration: the type # Variable has no type declaration: the type
@ -2241,7 +2258,7 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string, lines: seq[tu
self.chunk = chunk self.chunk = chunk
self.ast = ast self.ast = ast
self.file = file self.file = file
self.scopeDepth = 0 self.depth = 0
self.currentFunction = nil self.currentFunction = nil
self.currentModule = self.file.extractFilename().replace(".pn", "") self.currentModule = self.file.extractFilename().replace(".pn", "")
self.current = 0 self.current = 0
@ -2284,7 +2301,7 @@ proc compileModule(self: Compiler, moduleName: string) =
source, persist=true), source, persist=true),
path, self.lexer.getLines(), source, chunk=self.chunk, incremental=true, path, self.lexer.getLines(), source, chunk=self.chunk, incremental=true,
isMainModule=false) isMainModule=false)
self.scopeDepth = 0 self.depth = 0
self.current = current self.current = current
self.ast = ast self.ast = ast
self.file = file self.file = file

View File

@ -1,6 +1,7 @@
import std; import std;
var x: uint64 = 1000000'u64;
var x: uint64 = 100000'u64;
var y = "just a test"; var y = "just a test";
print(y); print(y);
print("Starting GC torture test"); print("Starting GC torture test");