Simplified calling convention, added for PeonObject, added some comments, fixed bug with StoreVar in stack frames, fixed issues with functions assigned to variables, changed the way closures are emitted so that empty jumps are no longer needed

This commit is contained in:
Mattia Giambirtone 2022-07-09 12:47:53 +02:00
parent 2c6325d33b
commit cc0aab850e
7 changed files with 283 additions and 316 deletions

View File

@ -287,6 +287,41 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject =
copyMem(result.`float`.addr, arr.addr, sizeof(arr))
proc `$`(self: PeonObject): string =
case self.kind:
of Int64:
result = &"{self.long}'i64"
of UInt64:
result = &"{self.uLong}'u64"
of Int32:
result = &"{self.`int`}'i32"
of UInt32:
result = &"{self.uInt}'u32"
of Int16:
result = &"{self.short}'i16"
of UInt16:
result = &"{self.uShort}'u16"
of Int8:
result = &"{self.tiny}'i8"
of UInt8:
result = &"{self.uTiny}'u8"
of Float32:
result = &"{self.halfFloat}'f32"
of Float64:
result = &"{self.`float`}'f64"
of ObjectKind.Inf:
if self.positive:
result ="inf"
else:
result ="-inf"
of ObjectKind.Nan, Nil:
result =($self.kind).toLowerAscii()
of Function:
result = &"fn(ip: {self.ip})"
else:
discard
proc dispatch*(self: PeonVM) =
## Main bytecode dispatch loop
var instruction {.register.}: OpCode
@ -342,11 +377,13 @@ proc dispatch*(self: PeonVM) =
of LoadFloat64:
self.push(self.constReadFloat64(int(self.readLong())))
of LoadFunction:
self.pushc(PeonObject(kind: Function, ip: self.readLong()))
of LoadFunctionObj:
# Loads a function onto the operand stack by reading its
# instruction pointer
self.push(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress:
self.pushc(PeonObject(kind: UInt32, uInt: self.readUInt()))
# Loads a 32-bit unsigned integer. Used to load function
# return addresses
self.push(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call:
# Calls a function. The calling convention for peon
# functions is pretty simple: the first item in the
@ -354,20 +391,34 @@ proc dispatch*(self: PeonVM) =
# instruction pointer to jump to, followed by a 32-bit
# return address. After that, all arguments and locals
# follow
var size {.used.} = self.readLong().int
self.frames.add(self.calls.len() - 2)
self.ip = self.peekc(-1).ip
var argc {.used.} = self.readLong().int
let retAddr = self.peek(-argc) # Return address
let fnObj = self.peek(-argc - 1) # Function object
self.ip = fnObj.ip
self.pushc(fnObj)
self.pushc(retAddr)
# Creates a new result slot for the
# function's return value
self.results.add(self.getNil())
# TODO: Use the frame size once
# Creates a new call frame
self.frames.add(self.calls.len() - 2)
# Loads the arguments onto the stack
for _ in 0..<argc:
self.pushc(self.pop())
# Pops the function object and
# return address off the operand
# stack since they're not needed
# there anymore
discard self.pop()
discard self.pop()
# TODO: Use the frame's initial size once
# we have more control over the
# memory
#[while size > 0:
dec(size)
#[while argc > 0:
dec(argc)
self.pushc(self.getNil())
]#
of LoadArgument:
self.pushc(self.pop())
of OpCode.Return:
of Return:
# Returns from a function.
# Every peon program is wrapped
# in a hidden function, so this
@ -380,6 +431,7 @@ proc dispatch*(self: PeonVM) =
self.push(self.results.pop())
else:
discard self.results.pop()
# Discard a stack frame
discard self.frames.pop()
if self.frames.len() == 0:
# End of the program!
@ -392,11 +444,11 @@ proc dispatch*(self: PeonVM) =
of StoreVar:
# Stores the value at the top of the operand stack
# into the given call stack index
let idx = int(self.readLong()) + self.frames[^1]
if idx <= self.calls.high():
let idx = int(self.readLong())
if idx + self.frames[^1] <= self.calls.high():
self.setc(idx, self.pop())
else:
self.calls.add(self.pop())
self.pushc(self.pop())
of StoreClosure:
# Stores/updates the value of a closed-over
# variable
@ -411,56 +463,30 @@ proc dispatch*(self: PeonVM) =
# Loads a closed-over variable onto the
# stack
self.push(self.closedOver[self.readLong()])
of PopClosure:
# Removes a closed-over variable from the closure
# array
discard self.closedOver.pop()
of LoadVar:
# Pushes a variable onto the operand
# stack
self.push(self.getc(int(self.readLong())))
of NoOp:
# Does nothing
continue
of PopC:
# Pops a value off the call stack
discard self.popc()
of Pop:
# Pops a value off the operand stack
discard self.pop()
of PushC:
# Pushes a value from the operand stack
# onto the call stack
self.pushc(self.pop())
of PopRepl:
if self.frames.len() > 1:
discard self.pop()
continue
let popped = self.pop()
case popped.kind:
of Int64:
echo &"{popped.long}'i64"
of UInt64:
echo &"{popped.uLong}'u64"
of Int32:
echo &"{popped.`int`}'i32"
of UInt32:
echo &"{popped.uInt}'u32"
of Int16:
echo &"{popped.short}'i16"
of UInt16:
echo &"{popped.uShort}'u16"
of Int8:
echo &"{popped.tiny}'i8"
of UInt8:
echo &"{popped.uTiny}'u8"
of Float32:
echo &"{popped.halfFloat}'f32"
of Float64:
echo &"{popped.`float`}'f64"
of ObjectKind.Inf:
if popped.positive:
echo "inf"
else:
echo "-inf"
of ObjectKind.Nan, Nil:
echo ($popped.kind).toLowerAscii()
else:
discard
echo self.pop()
of PopN:
# Pops N elements off the call stack
for _ in 0..<int(self.readShort()):
discard self.popc()
# Jump opcodes

View File

@ -27,7 +27,7 @@ when len(PEON_COMMIT_HASH) != 40:
const PEON_BRANCH* = "master"
when len(PEON_BRANCH) > 255:
{.fatal: "The git branch name's length must be less than or equal to 255 characters".}
const DEBUG_TRACE_VM* = false # Traces VM execution
const DEBUG_TRACE_VM* = true # Traces VM execution
const DEBUG_TRACE_GC* = false # Traces the garbage collector (TODO)
const DEBUG_TRACE_ALLOCATION* = false # Traces memory allocation/deallocation
const DEBUG_TRACE_COMPILER* = false # Traces the compiler

View File

@ -102,6 +102,10 @@ type
isFunctionArgument: bool
# Where is this node declared in the file?
line: int
# Is this a function declaration or a variable
# with a function as value? (The distinction *is*
# important! Check emitFunction())
isFunDecl: bool
Loop = object
## A "loop object" used
## by the compiler to emit
@ -181,11 +185,11 @@ proc declaration(self: Compiler, node: Declaration)
proc peek(self: Compiler, distance: int = 0): ASTNode
proc identifier(self: Compiler, node: IdentExpr)
proc varDecl(self: Compiler, node: VarDecl)
proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): Type
proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Type
proc inferType(self: Compiler, node: LiteralExpr): Type
proc inferType(self: Compiler, node: Expression): Type
proc findByName(self: Compiler, name: string): seq[Name]
proc findByType(self: Compiler, name: string, kind: Type, strictMutable: bool = true): seq[Name]
proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool
proc findByType(self: Compiler, name: string, kind: Type): seq[Name]
proc compareTypes(self: Compiler, a, b: Type): bool
proc patchReturnAddress(self: Compiler, pos: int)
proc handleMagicPragma(self: Compiler, pragma: Pragma, node: ASTnode)
proc handlePurePragma(self: Compiler, pragma: Pragma, node: ASTnode)
@ -372,22 +376,25 @@ proc resolve(self: Compiler, name: IdentExpr,
proc getStackPos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int =
## Returns the predicted call stack position of a given name, relative
## to the current frame
result = 2
var found = false
for variable in reversed(self.names):
result = 2
for variable in self.names:
if variable.valueType.kind == Function:
continue
inc(result)
if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule:
continue
if variable.depth == depth or variable.depth == 0:
elif variable.depth == depth or variable.depth == 0:
# variable.depth == 0 for globals!
found = true
dec(result)
break
inc(result)
if not found:
return -1
proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): int =
proc getClosurePos(self: Compiler, name: IdentExpr): int =
## Iterates the internal list of declared closure names backwards and
## returns the predicted closure array position of a given name.
## Returns -1 if the name can't be found (this includes names that
@ -398,7 +405,7 @@ proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth
if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule:
continue
elif variable.depth == depth:
else:
found = true
break
dec(result)
@ -423,7 +430,7 @@ proc resolve(self: Compiler, name: string,
return nil
proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDepth) =
proc detectClosureVariable(self: Compiler, name: var Name, depth: int = self.scopeDepth, decl: bool = false) =
## Detects if the given name is used in a local scope deeper
## than the given one and modifies the code emitted for it
## to store it as a closure variable if it is. Does nothing if the name
@ -434,27 +441,25 @@ proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDe
## unpredictably or crash
if name.isNil() or name.depth == 0:
return
elif name.depth < depth and not name.isClosedOver:
elif name.depth < depth:
# Ding! The given name is closed over: we need to
# change the dummy Jump instruction that self.declareName
# put in place for us into a StoreClosure. We also update
# the name's isClosedOver field so that self.identifier()
# can emit a LoadClosure instruction instead of a LoadVar
self.closedOver.add(name)
let idx = self.closedOver.high().toTriple()
if not name.isClosedOver:
self.closedOver.add(name)
if self.closedOver.len() >= 16777216:
self.error("too many consecutive closed-over variables (max is 16777215)")
self.chunk.code[name.codePos] = StoreClosure.uint8
self.chunk.code[name.codePos + 1] = idx[0]
self.chunk.code[name.codePos + 2] = idx[1]
self.chunk.code[name.codePos + 3] = idx[2]
name.isClosedOver = true
if decl:
self.emitByte(StoreClosure)
self.emitBytes(self.closedOver.high().toTriple())
proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool =
proc compareTypes(self: Compiler, a, b: Type): bool =
## Compares two type objects
## for equality (works with nil!)
# The nil code here is for void functions (when
# we compare their return types)
if a.isNil():
@ -477,10 +482,6 @@ proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool
# If they're different, then they can't
# be the same type!
return false
elif a.mutable != b.mutable and strictMutable:
# Are they both (im)mutable? If not,
# they're different
return false
case a.kind:
# If all previous checks pass, it's time
# to go through each possible type peon
@ -504,10 +505,11 @@ proc compareTypes(self: Compiler, a, b: Type, strictMutable: bool = true): bool
elif not self.compareTypes(a.returnType, b.returnType):
return false
for (argA, argB) in zip(a.args, b.args):
if not self.compareTypes(argA.kind, argB.kind, strictMutable):
if not self.compareTypes(argA.kind, argB.kind):
return false
return true
else:
# TODO: Custom types
discard
@ -553,7 +555,7 @@ proc toIntrinsic(name: string): Type =
return nil
proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): Type =
proc inferType(self: Compiler, node: LiteralExpr): Type =
## Infers the type of a given literal expression
if node.isNil():
return nil
@ -565,7 +567,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
if size.len() == 1:
return Type(kind: Int64)
let typ = size[1].toIntrinsic()
if not self.compareTypes(typ, nil, strictMutable):
if not self.compareTypes(typ, nil):
return typ
else:
self.error(&"invalid type specifier '{size[1]}' for int")
@ -576,7 +578,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
if size.len() == 1 or size[1] == "f64":
return Type(kind: Float64)
let typ = size[1].toIntrinsic()
if not self.compareTypes(typ, nil, strictMutable):
if not self.compareTypes(typ, nil):
return typ
else:
self.error(&"invalid type specifier '{size[1]}' for float")
@ -594,7 +596,7 @@ proc inferType(self: Compiler, node: LiteralExpr, strictMutable: bool = true): T
discard # TODO
proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Type =
proc inferType(self: Compiler, node: Expression): Type =
## Infers the type of a given expression and
## returns it
if node.isNil():
@ -611,9 +613,9 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
return self.inferType(UnaryExpr(node).a)
of binaryExpr:
let node = BinaryExpr(node)
var a = self.inferType(node.a, strictMutable)
var b = self.inferType(node.b, strictMutable)
if not self.compareTypes(a, b, strictMutable):
var a = self.inferType(node.a)
var b = self.inferType(node.b)
if not self.compareTypes(a, b):
return nil
return a
of {intExpr, hexExpr, binExpr, octExpr,
@ -627,7 +629,7 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
if not node.returnType.isNil():
result.returnType = self.inferType(node.returnType)
for argument in node.arguments:
result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType, strictMutable)))
result.args.add((argument.name.token.lexeme, self.inferType(argument.valueType)))
of callExpr:
var node = CallExpr(node)
case node.callee.kind:
@ -640,16 +642,16 @@ proc inferType(self: Compiler, node: Expression, strictMutable: bool = true): Ty
else:
result = nil
of lambdaExpr:
result = self.inferType(LambdaExpr(node.callee).returnType, strictMutable)
result = self.inferType(LambdaExpr(node.callee).returnType)
else:
discard # Unreachable
of varExpr:
result = self.inferType(Var(node).value)
result.mutable = true
of refExpr:
result = Type(kind: Reference, value: self.inferType(Ref(node).value, strictMutable))
result = Type(kind: Reference, value: self.inferType(Ref(node).value))
of ptrExpr:
result = Type(kind: Pointer, value: self.inferType(Ptr(node).value, strictMutable))
result = Type(kind: Pointer, value: self.inferType(Ptr(node).value))
else:
discard # Unreachable
@ -715,19 +717,29 @@ proc findByName(self: Compiler, name: string): seq[Name] =
result.add(obj)
proc findByType(self: Compiler, name: string, kind: Type, strictMutable: bool = true): seq[Name] =
proc findByType(self: Compiler, name: string, kind: Type): seq[Name] =
## Looks for objects that have already been declared
## with the given name and type
for obj in self.findByName(name):
if self.compareTypes(obj.valueType, kind, strictMutable):
if self.compareTypes(obj.valueType, kind):
result.add(obj)
#[
proc findAtDepth(self: Compiler, name: string, depth: int): seq[Name] =
## Looks for objects that have been already declared
## with the given name at the given scope depth.
## Returns all objects that apply
for obj in self.findByName(name):
if obj.depth == depth:
result.add(obj)
]#
proc matchImpl(self: Compiler, name: string, kind: Type, strictMutable: bool = true): Name =
proc matchImpl(self: Compiler, name: string, kind: Type): Name =
## Tries to find a matching function implementation
## compatible with the given type and returns its
## name object
let impl = self.findByType(name, kind, strictMutable)
let impl = self.findByType(name, kind)
if impl.len() == 0:
var msg = &"cannot find a suitable implementation for '{name}'"
let names = self.findByName(name)
@ -761,9 +773,20 @@ proc matchImpl(self: Compiler, name: string, kind: Type, strictMutable: bool = t
proc emitFunction(self: Compiler, name: Name) =
## Wrapper to emit LoadFunction instructions
self.emitByte(LoadFunction)
self.emitBytes(name.codePos.toTriple())
if name.isFunDecl:
self.emitByte(LoadFunction)
self.emitBytes(name.codePos.toTriple())
# If we're not loading a statically declared
# function, then it must be a function object
# created by previous LoadFunction instructions
# that is now bound to some variable, so we just
# load it
elif not name.isClosedOver:
self.emitByte(LoadVar)
self.emitBytes(self.getStackPos(name.name).toTriple())
else:
self.emitByte(LoadClosure)
self.emitBytes(self.getClosurePos(name.name).toTriple())
## End of utility functions
@ -850,11 +873,11 @@ proc literal(self: Compiler, node: ASTNode) =
proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
## Emits single instructions for builtin functions
## Emits instructions for builtin functions
## such as addition or subtraction
if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]:
for argument in args:
self.expression(argument)
self.expression(args[1])
self.expression(args[0])
case fn.valueType.builtinOp:
of "AddInt64":
self.emitByte(AddInt64)
@ -936,29 +959,27 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitByte(DivFloat32)
of "MulFloat32":
self.emitByte(MulFloat32)
of "GenericLogicalOr":
of "LogicalOr":
self.expression(args[0])
let jump = self.emitJump(JumpIfTrue)
self.expression(args[1])
self.patchJump(jump)
of "GenericLogicalAnd":
of "LogicalAnd":
self.expression(args[0])
var jump: int
if self.enableOptimizations:
jump = self.emitJump(JumpIfFalseOrPop)
else:
jump = self.emitJump(JumpIfFalse)
self.emitByte(Pop)
var jump = self.emitJump(JumpIfFalseOrPop)
self.expression(args[1])
self.patchJump(jump)
else:
discard # Unreachable
self.error(&"unknown built-in: '{fn.valueType.builtinOp}'")
proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
## Small wrapper that abstracts emitting a call instruction
## for a given function
if fn.valueType.isBuiltinFunction:
# Builtins map to individual instructions
# (usually 1, but some use more) so we handle
# them differently
self.handleBuiltinFunction(fn, args)
return
if any(fn.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic):
@ -968,41 +989,19 @@ proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitFunction(fn)
self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len()
# We initially emit a dummy return
# address. It is patched later
self.emitBytes(0.toQuad())
for argument in args:
for argument in reversed(args):
# We pass the arguments in reverse
# because of how stack semantics
# work. They'll be fixed at runtime
self.expression(argument)
self.emitByte(Call) # Creates a new call frame
var size = 2 # We start at 2 because each call frame
# contains at least 2 elements (function
# object and return address)
for name in reversed(self.names):
# Then, for each local variable
# we increase the frame size by 1
if name.depth == self.scopeDepth:
inc(size)
self.emitBytes(size.toTriple())
self.patchReturnAddress(pos)
proc generateObjCall(self: Compiler, args: seq[Expression]) =
## Small wrapper that abstracts emitting a call instruction
## for a given function already loaded on the operand stack
self.emitByte(PushC) # Pops the function off the operand stack onto the call stack
self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len()
self.emitBytes(0.toQuad())
for argument in args:
self.expression(argument)
self.emitByte(Call) # Creates a new call frame
var size = 2 # We start at 2 because each call frame
# contains at least 2 elements (function
# object and return address)
for name in reversed(self.names):
# Then, for each local variable
# we increase the frame size by 1
if name.depth == self.scopeDepth:
inc(size)
self.emitBytes(size.toTriple())
# Creates a new call frame and jumps
# to the function's first instruction
# in the code
self.emitByte(Call)
self.emitBytes(fn.valueType.args.len().toTriple())
self.patchReturnAddress(pos)
@ -1013,23 +1012,22 @@ proc callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
proc callBinaryOp(self: Compiler, fn: Name, op: BinaryExpr) =
## Emits the code to call a binary operator
# Pushes the return address
self.generateCall(fn, @[op.a, op.b])
proc unary(self: Compiler, node: UnaryExpr) =
## Compiles unary expressions such as decimal
## and bitwise negation
let valueType = self.inferType(node.a, strictMutable=false)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)]), strictMutable=false)
let valueType = self.inferType(node.a)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", valueType)]))
self.callUnaryOp(funct, node)
proc binary(self: Compiler, node: BinaryExpr) =
## Compiles all binary expressions
let typeOfA = self.inferType(node.a, strictMutable=false)
let typeOfB = self.inferType(node.b, strictMutable=false)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)]), strictMutable=false)
let typeOfA = self.inferType(node.a)
let typeOfB = self.inferType(node.b)
let funct = self.matchImpl(node.token.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: @[("", typeOfA), ("", typeOfB)]))
self.callBinaryOp(funct, node)
@ -1049,8 +1047,8 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
# 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")
for name in self.findByName(node.name.token.lexeme):
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, CustomType} and not name.isFunctionArgument:
# Trying to redeclare a variable in the same module is an error, but it's okay
if name.depth == self.scopeDepth and not name.isFunctionArgument:
# Trying to redeclare a variable in the same scope/context is an error, but it's okay
# if it's a function argument (for example, if you want to copy a number to
# mutate it)
self.error(&"attempt to redeclare '{node.name.token.lexeme}', which was previously defined in '{name.owner}' at line {name.line}")
@ -1066,21 +1064,6 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
line: node.token.line))
if mutable:
self.names[^1].valueType.mutable = true
# We emit a jump of 0 because this may become a
# StoreHeap instruction. If they variable is
# not closed over, we'll sadly be wasting a
# VM cycle. The previous implementation used 4 no-op
# instructions, which wasted 4 times as many clock
# cycles.
# TODO: Optimize this. It's a bit tricky because
# deleting bytecode would render all of our
# jump offsets and other absolute indeces in the
# bytecode wrong
if self.scopeDepth > 0:
# Closure variables are only used in local
# scopes
self.emitByte(JumpForwards)
self.emitBytes(0.toTriple())
of NodeKind.funDecl:
var node = FunDecl(node)
# We declare the generics before the function so we
@ -1106,14 +1089,15 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
name: node.name,
isLet: false,
isClosedOver: false,
line: node.token.line))
line: node.token.line,
isFunDecl: true))
let fn = self.names[^1]
var name: Name
for argument in node.arguments:
if self.names.high() > 16777215:
self.error("cannot declare more than 16777215 variables at a time")
# wait, no LoadVar? Yes! That's because when calling functions,
# arguments will already be on the stack so there's no need to
# arguments will already be on the stack, so there's no need to
# load them here
name = Name(depth: self.scopeDepth + 1,
isPrivate: true,
@ -1138,7 +1122,7 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
proc identifier(self: Compiler, node: IdentExpr) =
## Compiles access to identifiers
let s = self.resolve(node)
var s = self.resolve(node)
if s.isNil():
self.error(&"reference to undeclared name '{node.token.lexeme}'")
elif s.isConst:
@ -1148,11 +1132,11 @@ proc identifier(self: Compiler, node: IdentExpr) =
else:
self.detectClosureVariable(s)
if s.valueType.kind == Function:
if not s.valueType.isBuiltinFunction:
self.emitByte(LoadFunctionObj)
self.emitBytes(s.codePos.toTriple())
else:
self.emitByte(LoadNil)
# Functions have no runtime
# representation, so we need
# to create one on the fly
self.emitByte(LoadFunction)
self.emitBytes(s.codePos.toTriple())
elif not s.isClosedOver:
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
self.emitByte(LoadVar)
@ -1163,7 +1147,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
# 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)
self.emitByte(LoadClosure)
self.emitBytes(self.closedOver.high().toTriple())
self.emitBytes(self.getClosurePos(s.name).toTriple())
@ -1173,7 +1157,7 @@ proc assignment(self: Compiler, node: ASTNode) =
of assignExpr:
let node = AssignExpr(node)
let name = IdentExpr(node.name)
let r = self.resolve(name)
var r = self.resolve(name)
if r.isNil():
self.error(&"assignment to undeclared name '{name.token.lexeme}'")
elif r.isConst:
@ -1207,7 +1191,7 @@ proc beginScope(self: Compiler) =
inc(self.scopeDepth)
proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false) =
proc endScope(self: Compiler) =
## Ends the current local scope
if self.scopeDepth < 0:
self.error("cannot call endScope with scopeDepth < 0 (This is an internal error and most likely a bug)")
@ -1216,11 +1200,7 @@ proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false)
for name in self.names:
if name.depth > self.scopeDepth:
names.add(name)
if not self.enableOptimizations and not fromFunc:
# All variables with a scope depth larger than the current one
# are now out of scope. Begone, you're now homeless!
self.emitByte(PopC)
if self.enableOptimizations and len(names) > 1 and not fromFunc:
if len(names) > 1:
# If we're popping less than 65535 variables, then
# we can emit a PopN instruction. This is true for
# 99.99999% of the use cases of the language (who the
@ -1234,32 +1214,25 @@ proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false)
for i in countdown(self.names.high(), len(names) - uint16.high().int()):
if self.names[i].depth > self.scopeDepth:
self.emitByte(PopC)
elif len(names) == 1 and not fromFunc:
elif len(names) == 1:
# We only emit PopN if we're popping more than one value
self.emitByte(PopC)
# This seems *really* slow, but
# what else should I do? Nim doesn't
# allow the removal of items during
# seq iteration so ¯\_(ツ)_/¯
if deleteNames:
var idx = 0
while idx < self.names.len():
for name in names:
if self.names[idx] == name:
self.names.delete(idx)
inc(idx)
idx = 0
while idx < self.closedOver.len():
for name in names:
if name.isClosedOver:
self.closedOver.delete(idx)
self.emitByte(PopClosure)
inc(idx)
var idx = 0
while idx < self.names.len():
for name in names:
if self.names[idx] == name:
self.names.delete(idx)
inc(idx)
proc blockStmt(self: Compiler, node: BlockStmt) =
## Compiles block statements, which create a new
## local scope.
## local scope
self.beginScope()
for decl in node.code:
self.declaration(decl)
@ -1270,25 +1243,13 @@ proc ifStmt(self: Compiler, node: IfStmt) =
## Compiles if/else statements for conditional
## execution of code
var cond = self.inferType(node.condition)
self.expression(node.condition)
if not self.compareTypes(cond, Type(kind: Bool)):
if cond.isNil():
if node.condition.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(node.condition).name.lexeme}'")
elif node.condition.kind == callExpr and CallExpr(node.condition).callee.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(CallExpr(node.condition).callee).name.lexeme}'")
else:
self.error(&"expecting value of type 'bool', but expression has no type")
self.error(&"expecting value of type 'bool', but expression has no type")
else:
self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead")
self.expression(node.condition)
var jumpCode: OpCode
if self.enableOptimizations:
jumpCode = JumpIfFalsePop
else:
jumpCode = JumpIfFalse
let jump = self.emitJump(jumpCode)
if not self.enableOptimizations:
self.emitByte(Pop)
let jump = self.emitJump(JumpIfFalsePop)
self.statement(node.thenBranch)
let jump2 = self.emitJump(JumpForwards)
self.patchJump(jump)
@ -1310,51 +1271,23 @@ proc emitLoop(self: Compiler, begin: int) =
proc whileStmt(self: Compiler, node: WhileStmt) =
## Compiles C-style while loops and
## desugared C-style for loops
let start = self.chunk.code.len()
var cond = self.inferType(node.condition)
self.expression(node.condition)
var jump: int
if self.enableOptimizations:
jump = self.emitJump(JumpIfFalsePop)
else:
jump = self.emitJump(JumpIfFalse)
self.emitByte(Pop)
if not self.compareTypes(cond, Type(kind: Bool)):
if cond.isNil():
self.error(&"expecting value of type 'bool', but expression has no type")
else:
self.error(&"expecting value of type 'bool', got '{self.typeToStr(cond)}' instead")
let start = self.chunk.code.len()
var jump = self.emitJump(JumpIfFalsePop)
self.statement(node.body)
self.patchJump(jump)
self.emitLoop(start)
proc isPure(self: Compiler, node: ASTNode): bool =
## Checks if a function has any side effects
var pragmas: seq[Pragma]
case node.kind:
of lambdaExpr:
pragmas = LambdaExpr(node).pragmas
else:
pragmas = Declaration(node).pragmas
if pragmas.len() == 0:
return false
for pragma in pragmas:
if pragma.name.name.lexeme == "pure":
return true
return false
proc checkCallIsPure(self: Compiler, node: ASTnode): bool =
## Checks if a call has any side effects
if not self.isPure(node):
return true
var pragmas: seq[Pragma]
case node.kind:
of lambdaExpr:
pragmas = LambdaExpr(node).pragmas
else:
pragmas = Declaration(node).pragmas
if pragmas.len() == 0:
return false
for pragma in pragmas:
if pragma.name.name.lexeme == "pure":
return true
return false
return true # TODO
proc callExpr(self: Compiler, node: CallExpr) =
@ -1362,7 +1295,6 @@ proc callExpr(self: Compiler, node: CallExpr) =
var args: seq[tuple[name: string, kind: Type]] = @[]
var argExpr: seq[Expression] = @[]
var kind: Type
var strictMutable = true
# TODO: Keyword arguments
for i, argument in node.arguments.positionals:
kind = self.inferType(argument)
@ -1370,8 +1302,6 @@ proc callExpr(self: Compiler, node: CallExpr) =
if argument.kind == identExpr:
self.error(&"reference to undeclared identifier '{IdentExpr(argument).name.lexeme}'")
self.error(&"cannot infer the type of argument {i + 1} in function call")
if kind.mutable:
strictMutable = false
args.add(("", kind))
argExpr.add(argument)
for argument in node.arguments.keyword:
@ -1381,7 +1311,7 @@ proc callExpr(self: Compiler, node: CallExpr) =
var funct: Name
case node.callee.kind:
of identExpr:
funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args), strictMutable)
funct = self.matchImpl(IdentExpr(node.callee).name.lexeme, Type(kind: Function, returnType: Type(kind: Any), args: args))
of NodeKind.callExpr:
var node = node.callee
while node.kind == callExpr:
@ -1394,13 +1324,15 @@ proc callExpr(self: Compiler, node: CallExpr) =
self.error(&"expression has no type")
else:
self.error(&"object of type '{self.typeToStr(typ)}' is not callable")
if not funct.isNil():
if funct.valueType.isBuiltinFunction:
self.handleBuiltinFunction(funct, argExpr)
else:
self.generateCall(funct, argExpr)
if any(funct.valueType.args, proc (arg: tuple[name: string, kind: Type]): bool = arg[1].kind == Generic):
# The function has generic arguments! We need to compile a version
# of it with the right type data
self.funDecl(nil, funct, argExpr)
# TODO: What next?
elif funct.valueType.isBuiltinFunction:
self.handleBuiltinFunction(funct, argExpr)
else:
self.generateObjCall(argExpr)
self.generateCall(funct, argExpr)
if self.scopeDepth > 0 and not self.checkCallIsPure(node.callee):
if self.currentFunction.name != "":
self.error(&"cannot make sure that calls to '{self.currentFunction.name}' are side-effect free")
@ -1477,9 +1409,9 @@ proc endFunctionBeforeReturn(self: Compiler) =
## its return instruction
var popped = 0
for name in self.names:
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic}:
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType} and not name.isClosedOver:
inc(popped)
if self.enableOptimizations and popped > 1:
if popped > 1:
self.emitByte(PopN)
self.emitBytes(popped.toDouble())
dec(popped, uint16.high().int)
@ -1491,17 +1423,19 @@ proc endFunctionBeforeReturn(self: Compiler) =
proc returnStmt(self: Compiler, node: ReturnStmt) =
## Compiles return statements
let actual = self.inferType(node.value)
let expected = self.currentFunction
if actual.isNil() and not expected.returnType.isNil():
var expected = self.currentFunction.returnType
if not expected.isNil() and expected.kind == Generic:
expected = actual
if actual.isNil() and not expected.isNil():
if not node.value.isNil():
if node.value.kind == identExpr:
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
elif node.value.kind == callExpr and CallExpr(node.value).callee.kind == identExpr:
self.error(&"call to undeclared function '{CallExpr(node.value).callee.token.lexeme}'")
self.error(&"expected return value of type '{self.typeToStr(expected.returnType)}', but expression has no type")
elif expected.returnType.isNil() and not actual.isNil():
self.error("non-empty return statement is not allowed in void functions")
elif not self.compareTypes(actual, expected.returnType):
self.error(&"expected return value of type '{self.typeToStr(expected)}', but expression has no type")
elif expected.isNil() and not actual.isNil():
self.error("empty return statement is only allowed in void functions")
elif not self.compareTypes(actual, expected):
self.error(&"expected return value of type '{self.typeToStr(expected)}', got '{self.typeToStr(actual)}' instead")
if not node.value.isNil():
self.expression(node.value)
@ -1653,8 +1587,10 @@ proc varDecl(self: Compiler, node: VarDecl) =
self.error(&"expected value of type '{self.typeToStr(expected)}', but '{node.name.token.lexeme}' is of type '{self.typeToStr(actual)}'")
self.expression(node.value)
self.declareName(node, mutable=node.token.kind == TokenType.Var)
var r = self.names[^1]
self.detectClosureVariable(r, decl=true)
self.emitByte(StoreVar)
self.emitBytes(self.names.len().toTriple())
self.emitBytes((self.getStackPos(r.name) + 1).toTriple())
proc typeDecl(self: Compiler, node: TypeDecl) =
@ -1708,12 +1644,16 @@ proc dispatchPragmas(self: Compiler, node: ASTnode) =
proc fixGenericFunc(self: Compiler, name: Name, args: seq[Expression]): Type =
## Specializes generic arguments in functions
var fn = name.valueType
result = fn.deepCopy()
var node = fn.fun
var fn = name.valueType.deepCopy()
result = fn
var typ: Type
for i in 0..args.high():
if fn.args[i].kind.kind == Generic:
self.resolve(fn.args[i].name).valueType = self.inferType(args[i])
typ = self.inferType(args[i])
fn.args[i].kind = typ
self.resolve(fn.args[i].name).valueType = typ
if fn.args[i].kind.isNil():
self.error(&"cannot specialize generic function: argument {i + 1} has no type")
proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression] = @[]) =
@ -1728,9 +1668,12 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
self.dispatchPragmas(node)
var node = node
var fn = if fn.isNil(): self.names[^(node.arguments.len() + 1)] else: fn
if fn.valueType.returnType.isNil():
self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'")
if not fn.valueType.isBuiltinFunction:
if fn.valueType.isBuiltinFunction:
# We take the arguments off of our name list
# because they become temporaries on the stack
for i in self.names.high() - node.arguments.high()..self.names.high():
self.names.delete(i)
else:
var function = self.currentFunction
var jmp: int
# Builtin functions map to a single
@ -1747,11 +1690,6 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
fn.codePos = self.chunk.code.len()
# We store the current function
self.currentFunction = fn.valueType
let argLen = if node.isNil(): fn.valueType.args.len() else: node.arguments.len()
for _ in 0..<argLen:
# Pops off the operand stack onto the
# call stack
self.emitByte(LoadArgument)
if node.isNil():
# We got called back with more specific type
# arguments: time to fix them!
@ -1795,13 +1733,14 @@ proc funDecl(self: Compiler, node: FunDecl, fn: Name = nil, args: seq[Expression
hasVal = LambdaExpr(Declaration(self.currentFunction.fun)).hasExplicitReturn
else:
discard # Unreachable
if hasVal and self.currentFunction.returnType.isNil() and not typ.returnType.isNil():
self.error("non-empty return statement is not allowed in void functions")
elif not hasVal and not self.currentFunction.returnType.isNil():
if not hasVal and not typ.isNil():
# There is no explicit return statement anywhere in the function's
# body: while this is not a tremendously useful piece of information (since
# the presence of at least one doesn't mean all control flow cases are
# covered), it definitely is an error worth reporting
self.error("function has an explicit return type, but no return statement was found")
self.endFunctionBeforeReturn()
hasVal = hasVal and not typ.isNil()
self.endScope(deleteNames=true, fromFunc=true)
self.endScope()
# Terminates the function's context
self.emitByte(OpCode.Return)
if hasVal:
@ -1895,15 +1834,16 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk =
self.names.add(main)
self.emitByte(LoadFunction)
self.emitBytes(main.codePos.toTriple())
self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len()
self.emitBytes(0.toQuad())
self.emitByte(Call)
self.emitBytes(2.toTriple())
self.emitBytes(0.toTriple())
while not self.done():
self.declaration(Declaration(self.step()))
self.endScope(fromFunc=true)
self.endFunctionBeforeReturn()
self.patchReturnAddress(pos)
self.emitByte(OpCode.Return)
self.emitByte(0)
self.emitByte(0) # Entry point has no return value
result = self.chunk

View File

@ -138,15 +138,13 @@ type
## Basic stack operations
Pop, # Pops an element off the stack and discards it
PopRepl, # Same as Pop, but also prints the value of what's popped (used in REPL mode)
Push, # Pushes x onto the stack
PopN, # Pops x elements off the stack (optimization for exiting local scopes which usually pop many elements)
PopN, # Pops x elements off the call stack (optimization for exiting local scopes which usually pop many elements)
## Name resolution/handling
LoadAttribute, # Pushes the attribute b of object a onto the stack
LoadVar, # Pushes the object at position x in the stack onto the stack
StoreVar, # Stores the value of b at position a in the stack
LoadClosure, # Pushes the object position x in the closure array onto the stack
StoreClosure, # Stores the value of b at position a in the closure array
PopClosure, # Pops a closed-over variable from the closure array
## Looping and jumping
Jump, # Absolute, unconditional jump into the bytecode
JumpForwards, # Relative, unconditional, positive jump in the bytecode
@ -168,10 +166,8 @@ type
## Coroutines
Await, # Calls an asynchronous function
## Misc
Assert, # Raises an AssertionFailed exception if x is false
NoOp, # Just a no-op
LoadArgument, # The caller uses this to pop off the argument from the operand stack onto the call stack
LoadFunctionObj, # Creates and pushes a function object onto the stack with ip X
Assert, # Raises an AssertionFailed exception if x is false
NoOp, # Just a no-op
PopC, # Pop off the call stack onto the operand stack
PushC # Pop off the operand stack onto the call stack
@ -185,7 +181,7 @@ const simpleInstructions* = {Return, LoadNil,
Pop, PopRepl, Raise,
BeginTry, FinishTry, Yield,
Await, NoOp, SetResult,
LoadArgument, PopC, PushC,
PopC, PushC,
AddInt64, AddUInt64, AddInt32,
AddUInt32, AddInt16, AddUInt16,
AddInt8, AddUInt8, SubInt64,
@ -222,7 +218,7 @@ const stackDoubleInstructions* = {}
const argumentDoubleInstructions* = {PopN, }
# Argument double argument instructions take hardcoded arguments as 24 bit integers
const argumentTripleInstructions* = {LoadFunctionObj, StoreClosure, PopClosure}
const argumentTripleInstructions* = {StoreClosure}
# Instructions that call functions
const callInstructions* = {Call, }

View File

@ -314,7 +314,7 @@ proc primary(self: Parser): Expression =
self.expect(RightParen, "unterminated parenthesized expression")
of Yield:
let tok = self.step()
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'yield' cannot be used outside functions")
elif self.currentFunction.token.kind != Generator:
# It's easier than doing conversions for lambda/funDecl
@ -327,7 +327,7 @@ proc primary(self: Parser): Expression =
result = newYieldExpr(newNilExpr(Token()), tok)
of Await:
let tok = self.step()
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions")
if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines")
@ -549,7 +549,7 @@ proc blockStmt(self: Parser): Statement =
var code: seq[Declaration] = @[]
while not self.check(RightBrace) and not self.done():
code.add(self.declaration())
if code[^1] == nil:
if code[^1].isNil():
code.delete(code.high())
self.expect(RightBrace, "expecting '}'")
result = newBlockStmt(code, tok)
@ -568,7 +568,7 @@ proc breakStmt(self: Parser): Statement =
proc deferStmt(self: Parser): Statement =
## Parses defer statements
let tok = self.peek(-1)
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'defer' cannot be used outside functions")
result = newDeferStmt(self.expression(), tok)
endOfLine("missing semicolon after defer statement")
@ -586,7 +586,7 @@ proc continueStmt(self: Parser): Statement =
proc returnStmt(self: Parser): Statement =
## Parses return statements
let tok = self.peek(-1)
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'return' cannot be used outside functions")
var value: Expression
if not self.check(Semicolon):
@ -606,7 +606,7 @@ proc returnStmt(self: Parser): Statement =
proc yieldStmt(self: Parser): Statement =
## Parses yield statements
let tok = self.peek(-1)
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'yield' cannot be outside functions")
elif self.currentFunction.token.kind != Generator:
self.error("'yield' can only be used inside generators")
@ -620,7 +620,7 @@ proc yieldStmt(self: Parser): Statement =
proc awaitStmt(self: Parser): Statement =
## Parses await statements
let tok = self.peek(-1)
if self.currentFunction == nil:
if self.currentFunction.isNil():
self.error("'await' cannot be used outside functions")
if self.currentFunction.token.kind != Coroutine:
self.error("'await' can only be used inside coroutines")
@ -690,10 +690,10 @@ proc tryStmt(self: Parser): Statement =
elseClause = self.statement()
if self.match(Finally):
finallyClause = self.statement()
if handlers.len() == 0 and elseClause == nil and finallyClause == nil:
if handlers.len() == 0 and elseClause.isNil() and finallyClause.isNil():
self.error("expecting 'except', 'finally' or 'else' statement after 'try' block")
for i, handler in handlers:
if handler.exc == nil and i != handlers.high():
if handler.exc.isNil() and i != handlers.high():
self.error("catch-all exception handler with bare 'except' must come last in try statement")
result = newTryStmt(body, handlers, finallyClause, elseClause, tok)
@ -737,18 +737,18 @@ proc forStmt(self: Parser): Statement =
increment = self.expression()
self.expect(RightParen, "unterminated for loop increment")
var body = self.statement()
if increment != nil:
if not increment.isNil():
# The increment runs after each iteration, so we
# inject it into the block as the last statement
body = newBlockStmt(@[Declaration(body), newExprStmt(increment,
increment.token)], tok)
if condition == nil:
if condition.isNil():
## An empty condition is functionally
## equivalent to "true"
condition = newTrueExpr(Token(lexeme: "true"))
# We can use a while loop, which in this case works just as well
body = newWhileStmt(condition, body, tok)
if initializer != nil:
if not initializer.isNil():
# Nested blocks, so the initializer is
# only executed once
body = newBlockStmt(@[Declaration(initializer), Declaration(body)], tok)
@ -777,7 +777,7 @@ proc ifStmt(self: Parser): Statement =
var condition = self.expression()
self.expect(RightParen, "expecting ')' after if condition")
var thenBranch = self.statement()
var elseBranch: Statement = nil
var elseBranch: Statement
if self.match(Else):
elseBranch = self.statement()
result = newIfStmt(condition, thenBranch, elseBranch, tok)
@ -790,7 +790,6 @@ template checkDecl(self: Parser, isPrivate: bool) =
self.error("cannot bind public names inside local scopes")
proc parsePragmas(self: Parser): seq[Pragma] =
## Parses pragmas
var
@ -871,7 +870,7 @@ proc varDecl(self: Parser, isLet: bool = false,
isLet = isLet, valueType = valueType, pragmas = (@[]))
else:
discard # Unreachable
if not hasInit and VarDecl(result).valueType == nil:
if not hasInit and VarDecl(result).valueType.isNil():
self.error("expecting initializer or explicit type declaration, but neither was found")
result.pragmas = pragmas
@ -904,7 +903,7 @@ proc parseDeclArguments(self: Parser, arguments: var seq[tuple[name: IdentExpr,
break
self.expect(RightParen)
for argument in arguments:
if argument.valueType == nil:
if argument.valueType.isNil():
self.error(&"missing type declaration for '{argument.name.token.lexeme}' in function declaration")

View File

@ -227,9 +227,12 @@ proc runFile(f: string, interactive: bool = false, fromString: bool = false) =
styledEcho fgCyan, "Compilation step:\n"
debugger.disassembleChunk(compiled, f)
echo ""
serializer.dumpFile(compiled, f, splitFile(f).dir & "/" & splitFile(f).name & ".pbc")
serialized = serializer.loadFile(splitFile(f).dir & "/" & splitFile(f).name & ".pbc")
var path = splitFile(f).dir
if path.len() > 0:
path &= "/"
path &= splitFile(f).name & ".pbc"
serializer.dumpFile(compiled, f, path)
serialized = serializer.loadFile(path)
when debugSerializer:
var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash
styledEcho fgCyan, "Serialization step: "

View File

@ -63,8 +63,8 @@ proc printInstruction(instruction: OpCode, newline: bool = false) =
proc checkFrameStart(self: Debugger, n: int) =
## Todo: Checking frame end offsets needs
## fixes
## Checks if a call frame begins at the given
## bytecode offset
for i, e in self.cfiData:
if n == e.start and not (e.started or e.stopped):
e.started = true
@ -76,6 +76,8 @@ proc checkFrameStart(self: Debugger, n: int) =
proc checkFrameEnd(self: Debugger, n: int) =
## Checks if a call frame ends at the given
## bytecode offset
for i, e in self.cfiData:
if n == e.stop and e.started and not e.stopped:
e.stopped = true
@ -83,6 +85,7 @@ proc checkFrameEnd(self: Debugger, n: int) =
proc simpleInstruction(self: Debugger, instruction: OpCode) =
## Debugs simple instructions
printInstruction(instruction, true)
self.current += 1
if instruction == Return:
@ -152,7 +155,7 @@ proc loadAddressInstruction(self: Debugger, instruction: OpCode) =
var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3],
self.chunk.code[self.current + 4]].fromQuad()
printInstruction(instruction)
styledEcho fgGreen, &" loads address ", fgYellow, $address
styledEcho fgGreen, &", loads address ", fgYellow, $address
self.current += 5