Refactored calling convention and added stack frame support to the VM as well as StoreVar
This commit is contained in:
parent
5f43ea9490
commit
8d1699ff9e
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(", ")}])"""
|
||||
|
|
12
src/main.nim
12
src/main.nim
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue