Improved VM debugger, fixed bugs with fixing CFI offsets, minor docs and code changes

This commit is contained in:
Mattia Giambirtone 2022-10-24 13:53:27 +02:00
parent 6c305d6382
commit dc626a90d9
3 changed files with 126 additions and 94 deletions

View File

@ -40,6 +40,9 @@ when debugVM or debugMem or debugGC:
import std/sequtils
import std/terminal
when debugVM:
proc clearerr(stream: File) {.header: "stdio.h", importc.}
type
PeonVM* = ref object
@ -53,20 +56,22 @@ type
## 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
## 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 offsets
envs: seq[uint64] # Stores variables that do not have stack semantics
results: seq[uint64] # Stores function's results (return values)
gc: PeonGC # Our memory manager
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
ObjectKind* = enum
## A tag for heap-allocated
## peon objects
@ -198,7 +203,7 @@ proc markRoots(self: PeonGC): seq[ptr HeapObject] =
echo "DEBUG - GC: Starting mark phase"
# Unlike what bob does in his book,
# we keep track of objects in a different
# way due to how the whole thing is designed.
# 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
@ -251,6 +256,7 @@ proc trace(self: PeonGC, roots: seq[ptr HeapObject]) =
## 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:
@ -258,7 +264,7 @@ proc trace(self: PeonGC, roots: seq[ptr HeapObject]) =
else:
discard # TODO: Other types
when debugGC:
echo &"DEBUG - GC: Tracing phase complete"
echo &"DEBUG - GC: Traced {count} indirect references"
proc free(self: PeonGC, obj: ptr HeapObject) =
@ -332,6 +338,8 @@ proc collect(self: PeonGC) =
echo &"DEBUG - GC: Next cycle at {self.nextGC} bytes"
# Implementation of the peon VM
proc initCache*(self: PeonVM) =
## Initializes the VM's
## singletons cache
@ -633,55 +641,84 @@ when debugVM: # So nim shuts up
## 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() - self.frames[^1].int):
stdout.styledWrite(fgYellow, ", ")
styledEcho fgMagenta, "]", fgCyan
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.closures.len() !> 0:
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, "]"
if self.envs.len() !> 0:
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, "]"
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
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 "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, "[")
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: PeonVM) =
@ -690,7 +727,7 @@ proc dispatch*(self: PeonVM) =
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:
if self.ip in self.breakpoints or self.breakpoints.len() == 0 or self.debugNext:
self.debug()
instruction = OpCode(self.readByte())
case instruction:
@ -1046,6 +1083,7 @@ proc run*(self: PeonVM, chunk: Chunk, breakpoints: seq[uint64] = @[]) =
self.breakpoints = breakpoints
self.results = @[]
self.ip = 0
self.lastDebugCommand = ""
try:
self.dispatch()
except NilAccessDefect:

View File

@ -205,7 +205,7 @@ type
# Stores the position of all jumps
jumps: seq[tuple[patched: bool, offset: int]]
# List of CFI start offsets into our CFI data
cfiOffsets: seq[tuple[start, stop: int, fn: Name]]
cfiOffsets: seq[tuple[start, stop, pos: int, fn: Name]]
# We store these objects to compile modules
lexer: Lexer
parser: Parser
@ -450,20 +450,22 @@ proc fixCFIOffsets(self: Compiler, oldLen: int, modifiedAt: int) =
let offset = self.chunk.code.len() - oldLen
var newCFI: array[3, uint8]
var tmp: int
var i = 0
for cfi in self.cfiOffsets.mitems():
if cfi.start >= modifiedAt:
newCFI = (cfi.start + offset).toTriple()
self.chunk.cfi[cfi.start] = newCFI[0]
self.chunk.cfi[cfi.start + 1] = newCFI[1]
self.chunk.cfi[cfi.start + 2] = newCFI[2]
tmp = [self.chunk.cfi[cfi.start + 3], self.chunk.cfi[cfi.start + 4], self.chunk.cfi[cfi.start + 5]].fromTriple().int
self.chunk.cfi[cfi.pos] = newCFI[0]
self.chunk.cfi[cfi.pos + 1] = newCFI[1]
self.chunk.cfi[cfi.pos + 2] = newCFI[2]
tmp = [self.chunk.cfi[cfi.pos + 3], self.chunk.cfi[cfi.pos + 4], self.chunk.cfi[cfi.pos + 5]].fromTriple().int
newCFI = (tmp + offset).toTriple()
self.chunk.cfi[cfi.start + 3] = newCFI[0]
self.chunk.cfi[cfi.start + 4] = newCFI[1]
self.chunk.cfi[cfi.start + 5] = newCFI[2]
self.chunk.cfi[cfi.pos + 3] = newCFI[0]
self.chunk.cfi[cfi.pos + 4] = newCFI[1]
self.chunk.cfi[cfi.pos + 5] = newCFI[2]
cfi.fn.codePos += offset
cfi.start += offset
cfi.stop += offset
inc(i)
proc fixJumps(self: Compiler, oldLen: int, modifiedAt: int) =
@ -572,7 +574,6 @@ proc getStackPos(self: Compiler, name: Name): int =
found = true
break
inc(result)
echo variable
if not found:
return -1
@ -637,11 +638,9 @@ proc compare(self: Compiler, a, b: Type): bool =
# Functions are a bit trickier
if a.args.len() != b.args.len():
return false
if a.isClosure != b.isClosure:
return false
if a.isCoroutine != b.isCoroutine:
return false
elif not self.compare(a.returnType, b.returnType):
if not self.compare(a.returnType, b.returnType):
return false
for (argA, argB) in zip(a.args, b.args):
if not self.compare(argA.kind, argB.kind):
@ -1097,10 +1096,12 @@ proc endScope(self: Compiler) =
continue
if name.depth > self.scopeDepth:
names.add(name)
if not name.resolved:
#[if not name.resolved:
# TODO: Emit a warning?
continue]#
if name.owner != self.currentModule and self.scopeDepth > -1:
continue
elif name.kind in [NameKind.Var, NameKind.Argument]:
if name.kind in [NameKind.Var, NameKind.Argument]:
# We don't increase the pop count for some kinds of objects
# because they're not stored the same way as regular variables
# (for types, generics and function declarations)
@ -2128,7 +2129,8 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
name.codePos = self.chunk.code.len()
# We let our debugger know this function's boundaries
self.chunk.cfi.add(self.chunk.code.high().toTriple())
self.cfiOffsets.add((start: self.chunk.code.high(), stop: 0, fn: name))
self.cfiOffsets.add((start: self.chunk.code.high(), stop: 0, pos: self.chunk.cfi.len() - 3, fn: name))
var cfiOffset = self.cfiOffsets[^1]
let idx = self.chunk.cfi.len()
self.chunk.cfi.add(0.toTriple()) # Patched it later
self.chunk.cfi.add(uint8(node.arguments.len()))
@ -2161,7 +2163,7 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
of NodeKind.funDecl:
hasVal = self.currentFunction.valueType.fun.hasExplicitReturn
of NodeKind.lambdaExpr:
hasVal = LambdaExpr(Declaration(self.currentFunction.valueType.fun)).hasExplicitReturn
hasVal = LambdaExpr(self.currentFunction.node).hasExplicitReturn
else:
discard # Unreachable
if not hasVal and not typ.isNil():
@ -2184,7 +2186,7 @@ proc funDecl(self: Compiler, node: FunDecl, name: Name) =
self.chunk.cfi[idx] = stop[0]
self.chunk.cfi[idx + 1] = stop[1]
self.chunk.cfi[idx + 2] = stop[2]
self.cfiOffsets[^1].stop = self.chunk.code.len()
cfiOffset.stop = self.chunk.code.len()
# Currently defer is not functional, so we
# just pop the instructions
for _ in deferStart..self.deferred.high():

View File

@ -1,21 +1,13 @@
# Tests closures
import std;
fn makeClosure(n: int): fn: fn: int {
fn inner: fn: int {
fn deep: int {
return n;
}
return deep;
fn makeClosure(n: int): fn: int {
fn inner: int {
return n;
}
return inner;
}
var closure = makeClosure(38);
var inner = closure();
print(inner()); # 38
print(inner()); # 38
print(makeClosure(420)()()); # 420
var c = makeClosure(38);
c();