Refactored calling convention and added stack frame support to the VM as well as StoreVar

This commit is contained in:
Mattia Giambirtone 2022-05-23 10:49:38 +02:00
parent 5f43ea9490
commit 8d1699ff9e
6 changed files with 100 additions and 52 deletions

BIN
main

Binary file not shown.

View File

@ -27,7 +27,7 @@ type
ip: int # Instruction pointer
cache: array[6, PeonObject] # Singletons cache
chunk: Chunk # Piece of bytecode to execute
depth: int
frames: seq[int] # Stores the initial index of stack frames
proc initCache*(self: PeonVM) =
@ -46,7 +46,7 @@ proc newPeonVM*: PeonVM =
## for executing Peon bytecode
new(result)
result.ip = 0
result.depth = 0
result.frames = @[]
result.stack = newSeq[PeonObject]()
result.initCache()
@ -89,6 +89,20 @@ proc peek(self: PeonVM): PeonObject =
return self.stack[^1]
proc get(self: PeonVM, idx: int): PeonObject =
## Accessor method that abstracts
## stack accessing through stack
## frames
return self.stack[idx + self.frames[^1]]
proc set(self: PeonVM, idx: int, val: PeonObject) =
## Setter method that abstracts
## stack accessing through stack
## frames
self.stack[idx + self.frames[^1]] = val
proc readByte(self: PeonVM): uint8 =
## Reads a single byte from the
## bytecode and returns it as an
@ -160,9 +174,9 @@ proc dispatch*(self: PeonVM) =
while true:
instruction = OpCode(self.readByte())
when DEBUG_TRACE_VM:
echo &"Stack: {self.stack}"
echo &"PC: {self.ip}"
echo &"IP: {self.ip}"
echo &"SP: {self.stack.high()}"
echo &"Stack: {self.stack}"
echo &"Instruction: {instruction}"
discard readLine stdin
case instruction:
@ -182,23 +196,37 @@ proc dispatch*(self: PeonVM) =
self.push(self.readUInt64(int(self.readLong())))
of LoadUInt32:
self.push(self.readUInt32(int(self.readLong())))
of Call:
# We do this because if we immediately changed
# the instruction pointer, we'd read the wrong
# value for the argument count. Storing it and
# changing it later fixes this issue
let newIp = self.readLong()
self.frames.add(int(self.readLong()))
self.ip = int(newIp)
of OpCode.Return:
if self.depth == 0:
return
else:
if self.frames.len() > 1:
let frame = self.frames.pop()
for i in countdown(self.stack.high(), frame):
discard self.pop()
self.ip = int(self.pop().uInt)
of ReturnPop:
var retVal = self.pop()
else:
return
of ReturnValue:
let retVal = self.pop()
let frame = self.frames.pop()
for i in countdown(self.stack.high(), frame):
discard self.pop()
self.ip = int(self.pop().uInt)
self.push(retVal)
of OpCode.StoreVar:
self.set(int(self.readLong()), self.pop())
of OpCode.LoadVar:
self.push(self.stack[self.readLong()])
self.push(self.get(int(self.readLong())))
of NoOp:
continue
of Pop:
discard self.pop()
of Call:
self.ip = int(self.readLong())
of Jump:
self.ip = int(self.readShort())
of JumpForwards:
@ -245,5 +273,7 @@ proc dispatch*(self: PeonVM) =
proc run*(self: PeonVM, chunk: Chunk) =
## Executes a piece of Peon bytecode.
self.chunk = chunk
self.frames = @[0]
self.stack = @[]
self.ip = 0
self.dispatch()

View File

@ -110,6 +110,8 @@ type
# runtime to load variables that have stack
# behavior more efficiently
names: seq[Name]
# Beginning of stack frames for function calls
frames: seq[int]
# The current scope depth. If > 0, we're
# in a local scope, otherwise it's global
scopeDepth: int
@ -152,6 +154,7 @@ proc newCompiler*(enableOptimizations: bool = true): Compiler =
result.currentFunction = nil
result.enableOptimizations = enableOptimizations
result.currentModule = ""
result.frames = @[]
## Forward declarations
@ -720,7 +723,6 @@ proc literal(self: Compiler, node: ASTNode) =
proc unary(self: Compiler, node: UnaryExpr) =
## Compiles unary expressions such as decimal
## and bitwise negation
self.expression(node.a) # Pushes the operand onto the stack
let valueType = self.inferType(node.a)
let impl = self.findByType(node.token.lexeme, Type(kind: Function, returnType: valueType, node: nil, args: @[valueType]))
if impl.len() == 0:
@ -738,14 +740,15 @@ proc unary(self: Compiler, node: UnaryExpr) =
# We patch it later!
let idx = self.chunk.consts.len()
self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad()))
self.emitByte(Call)
self.expression(node.a) # Pushes the operand onto the stack
self.emitByte(Call) # Creates a stack frame
self.emitBytes(impl[0].codePos.toTriple())
self.emitBytes(1.toTriple())
self.patchReturnAddress(idx)
proc binary(self: Compiler, node: BinaryExpr) =
## Compiles all binary expressions
# These two lines prepare the stack by pushing the
# opcode's operands onto it
self.expression(node.a)
@ -784,6 +787,9 @@ proc declareName(self: Compiler, node: Declaration) =
# If someone ever hits this limit in real-world scenarios, I swear I'll
# slap myself 100 times with a sign saying "I'm dumb". Mark my words
self.error("cannot declare more than 16777216 variables at a time")
for name in self.findByName(node.name.token.lexeme):
if name.name.token.lexeme == node.name.token.lexeme:
self.error(&"attempt to redeclare '{node.name.token.lexeme}', which was previously defined in '{name.owner}' at line {name.valueType.node.token.line}")
self.names.add(Name(depth: self.scopeDepth,
name: node.name,
isPrivate: node.isPrivate,
@ -819,6 +825,9 @@ proc declareName(self: Compiler, node: Declaration) =
for argument in node.arguments:
if self.names.high() > 16777215:
self.error("cannot declare more than 16777216 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
# load them here
self.names.add(Name(depth: self.scopeDepth + 1,
isPrivate: true,
owner: self.currentModule,
@ -854,11 +863,7 @@ proc identifier(self: Compiler, node: IdentExpr) =
if not t.closedOver:
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
self.emitByte(LoadVar)
if self.scopeDepth > 0:
# Skips function's return address
self.emitBytes((index - 1).toTriple())
else:
self.emitBytes(index.toTriple())
self.emitBytes((index - self.frames[^1]).toTriple())
else:
if self.closedOver.len() == 0:
self.error("error: closure variable array is empty but LoadHeap would be emitted (this is an internal error and most likely a bug)")
@ -1106,9 +1111,10 @@ proc returnStmt(self: Compiler, node: ReturnStmt) =
self.error(&"expected return value of type '{self.typeToStr(typ.returnType)}', got '{self.typeToStr(returnType)}' instead")
if node.value != nil:
self.expression(node.value)
self.emitByte(OpCode.ReturnPop)
self.emitByte(OpCode.ReturnValue)
else:
self.emitByte(OpCode.Return)
discard self.frames.pop()
proc yieldStmt(self: Compiler, node: YieldStmt) =
@ -1232,6 +1238,7 @@ proc funDecl(self: Compiler, node: FunDecl) =
let jmp = self.emitJump(Jump)
var function = self.currentFunction
self.declareName(node)
self.frames.add(self.names.high())
# TODO: Forward declarations
if node.body != nil:
if BlockStmt(node.body).code.len() == 0:
@ -1329,6 +1336,7 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk =
self.currentFunction = nil
self.currentModule = self.file.extractFilename()
self.current = 0
self.frames = @[0]
while not self.done():
self.declaration(Declaration(self.step()))
if self.ast.len() > 0:

View File

@ -23,7 +23,7 @@ import ../../util/multibyte
type
Chunk* = ref object
## A piece of bytecode.
## byteConsts is used when serializing to/from a bytecode stream.
## consts is used when serializing to/from a bytecode stream.
## code is the linear sequence of compiled bytecode instructions.
## lines maps bytecode instructions to line numbers using Run
## Length Encoding. Instructions are encoded in groups whose structure
@ -41,7 +41,6 @@ type
consts*: seq[uint8]
code*: seq[uint8]
lines*: seq[int]
reuseConsts*: bool
OpCode* {.pure.} = enum
## Enum of Peon's bytecode opcodes
@ -105,7 +104,7 @@ type
## Functions
Call, # Calls a function and initiates a new stack frame
Return, # Terminates the current function without popping off the stack
ReturnPop, # Pops a return value off the stack and terminates the current function
ReturnValue, # Pops a return value off the stack and terminates the current function
## Exception handling
Raise, # Raises exception x or re-raises active exception if x is nil
BeginTry, # Initiates an exception handling context
@ -129,7 +128,7 @@ const simpleInstructions* = {OpCode.Return, LoadNil,
BeginTry, FinishTry,
OpCode.Yield, OpCode.Await,
OpCode.NoOp, OpCode.Return,
OpCode.ReturnPop}
OpCode.ReturnValue}
# Constant instructions are instructions that operate on the bytecode constant table
const constantInstructions* = {LoadInt64, LoadUInt64,
@ -151,7 +150,11 @@ const stackDoubleInstructions* = {}
const argumentDoubleInstructions* = {PopN, }
# Argument double argument instructions take hardcoded arguments as 24 bit integers
const argumentTripleInstructions* = {Call, }
const argumentTripleInstructions* = {}
# Instructions that call functions
const callInstructions* = {Call, }
# Jump instructions jump at relative or absolute bytecode offsets
const jumpInstructions* = {Jump, LongJump, JumpIfFalse, JumpIfFalsePop,
JumpForwards, JumpBackwards,
@ -160,9 +163,9 @@ const jumpInstructions* = {Jump, LongJump, JumpIfFalse, JumpIfFalsePop,
JumpIfTrue, LongJumpIfTrue}
proc newChunk*(reuseConsts: bool = true): Chunk =
proc newChunk*: Chunk =
## Initializes a new, empty chunk
result = Chunk(consts: @[], code: @[], lines: @[], reuseConsts: reuseConsts)
result = Chunk(consts: @[], code: @[], lines: @[])
proc `$`*(self: Chunk): string = &"""Chunk(consts=[{self.consts.join(", ")}], code=[{self.code.join(", ")}], lines=[{self.lines.join(", ")}])"""

View File

@ -26,7 +26,7 @@ proc getLineEditor: LineEditor
# Handy dandy compile-time constants
const debugLexer = false
const debugParser = false
const debugCompiler = false
const debugCompiler = true
const debugSerializer = false
const debugRuntime = false
@ -89,8 +89,9 @@ proc repl =
compiled = compiler.compile(tree, "stdin")
when debugCompiler:
styledEcho fgCyan, "Compilation step:"
stdout.styledWrite(fgCyan, "\tRaw byte stream: ", fgGreen, "[", fgYellow, compiled.code.join(", "), fgGreen, "]")
styledEcho fgCyan, "\n\nBytecode disassembler output below:\n"
styledEcho fgCyan, "\tRaw byte stream: ", fgGreen, "[", fgYellow, compiled.code.join(", "), fgGreen, "]"
styledEcho fgCyan, "\tConstant table: ", fgGreen, "[", fgYellow, compiled.consts.join(", "), fgGreen, "]"
styledEcho fgCyan, "\nBytecode disassembler output below:\n"
disassembleChunk(compiled, "stdin")
echo ""
@ -202,8 +203,9 @@ proc runFile(f: string) =
compiled = compiler.compile(tree, f)
when debugCompiler:
styledEcho fgCyan, "Compilation step:"
stdout.styledWrite(fgCyan, "\tRaw byte stream: ", fgGreen, "[", fgYellow, compiled.code.join(", "), fgGreen, "]")
styledEcho fgCyan, "\n\nBytecode disassembler output below:\n"
styledEcho fgCyan, "\tRaw byte stream: ", fgGreen, "[", fgYellow, compiled.code.join(", "), fgGreen, "]"
styledEcho fgCyan, "\tConstant table: ", fgGreen, "[", fgYellow, compiled.consts.join(", "), fgGreen, "]"
styledEcho fgCyan, "\nBytecode disassembler output below:\n"
disassembleChunk(compiled, f)
echo ""

View File

@ -31,15 +31,15 @@ proc printDebug(s: string, newline: bool = false) =
nl()
proc printName(name: string, newline: bool = false) =
stdout.styledWrite(fgRed, name)
proc printName(opcode: OpCode, newline: bool = false) =
stdout.styledWrite(fgRed, $opcode, " (", fgYellow, $uint8(opcode), fgRed, ")")
if newline:
nl()
proc printInstruction(instruction: OpCode, newline: bool = false) =
printDebug("Instruction: ")
printName($instruction)
printName(instruction)
if newline:
nl()
@ -57,8 +57,7 @@ proc stackTripleInstruction(instruction: OpCode, chunk: Chunk,
offset + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", points to index ")
stdout.styledWrite(fgYellow, &"{slot}")
nl()
stdout.styledWriteLine(fgYellow, &"{slot}")
return offset + 4
@ -69,8 +68,7 @@ proc stackDoubleInstruction(instruction: OpCode, chunk: Chunk,
printInstruction(instruction)
stdout.write(&", points to index ")
stdout.styledWrite(fgGreen, &", points to index ")
stdout.styledWrite(fgYellow, &"{slot}")
nl()
stdout.styledWriteLine(fgYellow, &"{slot}")
return offset + 3
@ -79,31 +77,37 @@ proc argumentDoubleInstruction(instruction: OpCode, chunk: Chunk, offset: int):
var slot = [chunk.code[offset + 1], chunk.code[offset + 2]].fromDouble()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", has argument ")
setForegroundColor(fgYellow)
stdout.write(&"{slot}")
nl()
stdout.styledWriteLine(fgYellow, $slot)
return offset + 3
proc argumentTripleInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
## Debugs instructions that operate on a hardcoded value on the stack using a 16-bit operand
## Debugs instructions that operate on a hardcoded value on the stack using a 24-bit operand
var slot = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", has argument ")
setForegroundColor(fgYellow)
stdout.write(&"{slot}")
nl()
stdout.styledWrite(fgGreen, ", has argument ")
stdout.styledWriteLine(fgYellow, $slot)
return offset + 4
proc callInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
## Debugs function calls
var slot = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[offset + 3]].fromTriple()
var args = [chunk.code[offset + 4], chunk.code[offset + 5], chunk.code[offset + 6]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", jumps to address ", fgYellow, $slot, fgGreen, " with ", fgYellow, $args, fgGreen, " argument")
if args > 1:
stdout.styledWrite(fgYellow, "s")
nl()
return offset + 7
proc constantInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
## Debugs instructions that operate on the constant table
var constant = [chunk.code[offset + 1], chunk.code[offset + 2], chunk.code[
offset + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", points to constant at position ")
setForegroundColor(fgYellow)
stdout.write(&"{constant}")
stdout.styledWrite(fgGreen, &", points to constant at position ", fgYellow, $constant)
nl()
printDebug("Operand: ")
stdout.styledWriteLine(fgYellow, &"{chunk.consts[constant]}")
@ -132,7 +136,6 @@ proc jumpInstruction(instruction: OpCode, chunk: Chunk, offset: int): int =
proc disassembleInstruction*(chunk: Chunk, offset: int): int =
## Takes one bytecode instruction and prints it
printDebug("Offset: ")
setForegroundColor(fgYellow)
stdout.styledWriteLine(fgYellow, $offset)
printDebug("Line: ")
stdout.styledWriteLine(fgYellow, &"{chunk.getLine(offset)}")
@ -150,6 +153,8 @@ proc disassembleInstruction*(chunk: Chunk, offset: int): int =
result = argumentDoubleInstruction(opcode, chunk, offset)
of argumentTripleInstructions:
result = argumentTripleInstruction(opcode, chunk, offset)
of callInstructions:
result = callInstruction(opcode, chunk, offset)
of jumpInstructions:
result = jumpInstruction(opcode, chunk, offset)
else: