Initial work on a two-stack design

This commit is contained in:
Mattia Giambirtone 2022-06-02 01:33:56 +02:00
parent f8ab292c27
commit 099f733db6
9 changed files with 592 additions and 338 deletions

View File

@ -25,7 +25,6 @@ A peon bytecode file starts with the header, which is structured as follows:
- The branch name of the repository the compiler was built from, prepended with its length as a 1 byte integer
- The commit hash (encoded as a 40-byte hex-encoded string) in the aforementioned branch from which the compiler was built from (particularly useful in development builds)
- An 8-byte UNIX timestamp (with Epoch 0 starting at 1/1/1970 12:00 AM) representing the exact date and time of when the file was generated
- A 32-byte, hex-encoded SHA256 hash of the source file's content, used to track file changes
## Debug information

View File

@ -24,6 +24,8 @@ type
PeonObject* = object
## A generic Peon object
case kind*: ObjectKind:
of String:
str*: string
of Bool:
boolean*: bool
of Inf:
@ -50,5 +52,11 @@ type
discard
of CustomType:
fields*: seq[PeonObject]
of Float32:
halfFloat*: float32
of Float64:
`float`*: float
of Function:
ip*: uint32
else:
discard # TODO

View File

@ -26,12 +26,14 @@ export types
type
PeonVM* = ref object
## The Peon Virtual Machine
stack: seq[PeonObject]
ip: int # Instruction pointer
calls: seq[PeonObject] # Our call stack
operands: seq[PeonObject] # Our operand stack
ip: uint32 # Instruction pointer
cache: array[6, PeonObject] # Singletons cache
chunk: Chunk # Piece of bytecode to execute
frames: seq[int] # Stores the bottom of stack frames
heapVars: seq[PeonObject] # Stores variables that do not have stack semantics
closedOver: seq[PeonObject] # Stores variables that do not have stack semantics
results: seq[PeonObject] # Stores function's results
proc initCache*(self: PeonVM) =
@ -51,7 +53,8 @@ proc newPeonVM*: PeonVM =
new(result)
result.ip = 0
result.frames = @[]
result.stack = newSeq[PeonObject]()
result.calls = newSeq[PeonObject]()
result.operands = newSeq[PeonObject]()
result.initCache()
@ -78,38 +81,70 @@ proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5]
proc push(self: PeonVM, obj: PeonObject) =
## Pushes a Peon object onto the
## stack
self.stack.add(obj)
## operand stack
self.operands.add(obj)
proc pop(self: PeonVM): PeonObject =
## Pops a Peon object off the
## stack, decreasing the stack
## pointer. The object is returned
return self.stack.pop()
## operand stack. The object
## is returned
return self.operands.pop()
proc peek(self: PeonVM): PeonObject =
## Returns the Peon object at the top
## of the stack without consuming
## it
return self.stack[^1]
proc peek(self: PeonVM, distance: int = 0): PeonObject =
## Returns the Peon object at the
## given distance from the top of
## the operand stack without consuming it
return self.operands[self.operands.high() + distance]
proc get(self: PeonVM, idx: int): PeonObject =
## Accessor method that abstracts
## stack indexing through stack
## indexing the through stack
## frames
return self.stack[idx + self.frames[^1]]
return self.operands[idx + self.frames[^1]]
proc set(self: PeonVM, idx: int, val: PeonObject) =
## Setter method that abstracts
## stack indexing through stack
## indexing through stack
## frames
self.stack[idx + self.frames[^1]] = val
self.operands[idx + self.frames[^1]] = val
proc pushc(self: PeonVM, val: PeonObject) =
## Pushes a new object to the
## call stack
self.calls.add(val)
proc popc(self: PeonVM): PeonObject =
## Pops an object off the call
## stack and returns it
return self.calls.pop()
proc peekc(self: PeonVM, distance: int = 0): PeonObject =
## Returns the Peon object at the
## given distance from the top of
## the call stack without consuming it
return self.calls[self.calls.high() + distance]
proc getc(self: PeonVM, idx: int): PeonObject =
## Accessor method that abstracts
## indexing our call stack through stack
## frames
return self.calls[idx + self.frames[^1]]
proc setc(self: PeonVM, idx: int, val: PeonObject) =
## Setter method that abstracts
## indexing our call stack through stack
## frames
self.calls[idx + self.frames[^1]] = val
## Byte-level primitives to read/decode
## bytecode
@ -139,7 +174,15 @@ proc readLong(self: PeonVM): uint32 =
return uint32([self.readByte(), self.readByte(), self.readByte()].fromTriple())
proc readInt64(self: PeonVM, idx: int): PeonObject =
proc readUInt(self: PeonVM): uint32 =
## Reads three bytes from the
## bytecode and returns them
## as an unsigned 32 bit
## integer
return uint32([self.readByte(), self.readByte(), self.readByte(), self.readByte()].fromQuad())
proc constReadInt64(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
@ -153,7 +196,7 @@ proc readInt64(self: PeonVM, idx: int): PeonObject =
copyMem(result.long.addr, arr.addr, sizeof(arr))
proc readUInt64(self: PeonVM, idx: int): PeonObject =
proc constReadUInt64(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
@ -167,7 +210,7 @@ proc readUInt64(self: PeonVM, idx: int): PeonObject =
copyMem(result.uLong.addr, arr.addr, sizeof(arr))
proc readUInt32(self: PeonVM, idx: int): PeonObject =
proc constReadUInt32(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
@ -178,7 +221,7 @@ proc readUInt32(self: PeonVM, idx: int): PeonObject =
copyMem(result.uInt.addr, arr.addr, sizeof(arr))
proc readInt32(self: PeonVM, idx: int): PeonObject =
proc constReadInt32(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
@ -189,18 +232,95 @@ proc readInt32(self: PeonVM, idx: int): PeonObject =
copyMem(result.`int`.addr, arr.addr, sizeof(arr))
proc constReadInt16(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is an Int16
var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]]
result = PeonObject(kind: Int16)
copyMem(result.short.addr, arr.addr, sizeof(arr))
proc constReadUInt16(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is an UInt16
var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1]]
result = PeonObject(kind: UInt16)
copyMem(result.uShort.addr, arr.addr, sizeof(arr))
proc constReadInt8(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is an Int8
result = PeonObject(kind: Int8, tiny: self.chunk.consts[idx])
proc constReadUInt8(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is an UInt8
result = PeonObject(kind: UInt8, uTiny: self.chunk.consts[idx])
proc constReadString(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is a string
let size = self.readLong()
result = PeonObject(kind: String, str: self.chunk.consts[idx..<size].fromBytes())
proc constReadFloat32(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is a Float32
var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1],
self.chunk.consts[idx + 2], self.chunk.consts[idx + 3]]
result = PeonObject(kind: Float32)
copyMem(result.halfFloat.addr, arr.addr, sizeof(arr))
proc constReadFloat64(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
## returns a Peon object. Assumes
## the constant is a Float64
var arr = [self.chunk.consts[idx], self.chunk.consts[idx + 1],
self.chunk.consts[idx + 2], self.chunk.consts[idx + 3],
self.chunk.consts[idx + 4], self.chunk.consts[idx + 5],
self.chunk.consts[idx + 6], self.chunk.consts[idx + 7]]
result = PeonObject(kind: Float64)
copyMem(result.`float`.addr, arr.addr, sizeof(arr))
proc dispatch*(self: PeonVM) =
## Main bytecode dispatch loop
var instruction: OpCode
var instruction {.register.}: OpCode
while true:
instruction = OpCode(self.readByte())
when DEBUG_TRACE_VM:
echo &"IP: {self.ip}"
echo &"Instruction: {instruction}"
echo &"Stack: {self.stack}"
echo &"Current Frame: {self.stack[self.frames[^1]..^1]}"
echo &"Heap Vars: {self.heapVars}"
echo &"Instruction: {OpCode(self.chunk.code[self.ip])}"
if self.calls.len() > 0:
echo &"Call Stack: {self.calls}"
if self.operands.len() > 0:
echo &"Operand Stack: {self.operands}"
if self.frames.len() > 0:
echo &"Current Frame: {self.calls[self.frames[^1]..^1]}"
echo &"Frames: {self.frames}"
if self.closedOver.len() > 0:
echo &"Closure Array: {self.closedOver}"
if self.results.len() > 0:
echo &"Results: {self.results}"
discard readLine stdin
instruction = OpCode(self.readByte())
case instruction:
# Constant loading
of LoadTrue:
@ -214,73 +334,103 @@ proc dispatch*(self: PeonVM) =
of LoadInf:
self.push(self.getInf(true))
of LoadInt64:
self.push(self.readInt64(int(self.readLong())))
self.push(self.constReadInt64(int(self.readLong())))
of LoadUInt64:
self.push(self.readUInt64(int(self.readLong())))
self.push(self.constReadUInt64(int(self.readLong())))
of LoadUInt32:
self.push(self.readUInt32(int(self.readLong())))
self.push(self.constReadUInt32(int(self.readLong())))
of LoadInt32:
self.push(self.readInt32(int(self.readLong())))
self.push(self.constReadInt32(int(self.readLong())))
of LoadInt16:
self.push(self.constReadInt16(int(self.readLong())))
of LoadUInt16:
self.push(self.constReadUInt16(int(self.readLong())))
of LoadInt8:
self.push(self.constReadInt8(int(self.readLong())))
of LoadUInt8:
self.push(self.constReadUInt8(int(self.readLong())))
of LoadString:
self.push(self.constReadString(int(self.readLong())))
of LoadFloat32:
self.push(self.constReadFloat32(int(self.readLong())))
of LoadFloat64:
self.push(self.constReadFloat64(int(self.readLong())))
of LoadFunction:
self.pushc(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress:
self.pushc(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call:
# Calls a function. The calling convention for peon
# functions is pretty simple: the return address sits
# at the bottom of the stack frame, then follow the
# arguments and all temporaries/local variables
let newIp = self.readLong()
# We store it 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
self.frames.add(int(self.readLong()))
self.ip = int(newIp)
# functions is pretty simple: the first item in the
# frame is a function object which contains the new
# 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
self.results.add(self.getNil())
# TODO: Use the frame size once
# we have more control over the
# memory
#[while size > 0:
dec(size)
self.pushc(self.getNil())
]#
of LoadArgument:
self.pushc(self.pop())
of OpCode.Return:
# Returns from a void function
let frame = self.frames.pop()
for i in 0..<frame - 1:
discard self.pop()
self.ip = self.pop().uInt.int
discard self.pop() # Nil
of ProgExit:
# Exits the VM's loop
while self.stack.len() > 0:
discard self.pop()
# Returns from a function.
# Every peon program is wrapped
# in a hidden function, so this
# will also exit the VM if we're
# at the end of the program
let ret = self.popc()
discard self.popc() # Function object
if self.readByte() == 1:
# Function is non-void!
self.push(self.results.pop())
else:
discard self.results.pop()
discard self.frames.pop()
return
of ReturnValue:
# Returns from a function which has a return value,
# pushing it on the stack
let retVal = self.pop()
let frame = self.frames.pop()
for i in 0..<frame:
discard self.pop()
self.ip = int(self.pop().uInt)
discard self.pop() # Nil
self.push(retVal)
if self.frames.len() == 0:
# End of the program!
return
self.ip = ret.uInt
of SetResult:
# Sets the result of the
# current function
self.results[self.frames.high()] = self.pop()
of StoreVar:
# Stores the value at the top of the stack
# into the given stack index
self.set(int(self.readLong()), self.pop())
# Stores the value at the top of the operand stack
# into the given call stack index
let idx = int(self.readLong())
if idx <= self.calls.high():
self.setc(idx, self.pop())
else:
self.calls.add(self.pop())
of StoreClosure:
# Stores/updates the value of a closed-over
# variable
let idx = self.readLong().int
if idx > self.heapVars.high():
self.heapVars.add(self.pop())
if idx > self.closedOver.high():
self.closedOver.add(self.pop())
else:
self.heapVars[idx] = self.pop()
self.closedOver[idx] = self.pop()
of LoadClosure:
# Loads a closed-over variable onto the
# stack
self.push(self.heapVars[self.readLong()])
self.push(self.closedOver[self.readLong()])
of PopClosure:
# Pops a closed-over variable off the closure
# array
discard self.heapVars.pop()
discard self.closedOver.pop()
of LoadVar:
# Stores/updates the value of a local variable
self.push(self.get(int(self.readLong())))
self.push(self.getc(int(self.readLong())))
of NoOp:
continue
of PopC:
discard self.popc()
of Pop:
discard self.pop()
of PopRepl:
@ -302,6 +452,10 @@ proc dispatch*(self: PeonVM) =
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"
@ -313,45 +467,45 @@ proc dispatch*(self: PeonVM) =
discard
of PopN:
for _ in 0..<int(self.readShort()):
discard self.pop()
discard self.popc()
# Jump opcodes
of Jump:
self.ip = int(self.readShort())
self.ip = self.readShort()
of JumpForwards:
self.ip += int(self.readShort())
self.ip += self.readShort()
of JumpBackwards:
self.ip -= int(self.readShort())
self.ip -= self.readShort()
of JumpIfFalse:
if not self.peek().boolean:
self.ip += int(self.readShort())
self.ip += self.readShort()
of JumpIfTrue:
if self.peek().boolean:
self.ip += int(self.readShort())
self.ip += self.readShort()
of JumpIfFalsePop:
if not self.peek().boolean:
self.ip += int(self.readShort())
self.ip += self.readShort()
discard self.pop()
of JumpIfFalseOrPop:
if not self.peek().boolean:
self.ip += int(self.readShort())
self.ip += self.readShort()
else:
discard self.pop()
of LongJumpIfFalse:
if not self.peek().boolean:
self.ip += int(self.readLong())
self.ip += self.readLong()
of LongJumpIfFalsePop:
if not self.peek().boolean:
self.ip += int(self.readLong())
self.ip += self.readLong()
discard self.pop()
of LongJumpForwards:
self.ip += int(self.readLong())
self.ip += self.readLong()
of LongJumpBackwards:
self.ip -= int(self.readLong())
self.ip -= self.readLong()
of LongJump:
self.ip = int(self.readLong())
self.ip = self.readLong()
of LongJumpIfFalseOrPop:
if not self.peek().boolean:
self.ip += int(self.readLong())
self.ip += self.readLong()
else:
discard self.pop()
else:
@ -361,7 +515,8 @@ 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.frames = @[]
self.calls = @[]
self.operands = @[]
self.ip = 0
self.dispatch()

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

@ -90,6 +90,8 @@ type
codePos: int
# Is the name closed over (i.e. used in a closure)?
isClosedOver: bool
# Is this a function argument?
isFunctionArgument: bool
# Where is this node declared in the file?
line: int
Loop = object
@ -123,8 +125,6 @@ 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
@ -158,6 +158,11 @@ type
deferred: seq[uint8]
# List of closed-over variables
closedOver: seq[Name]
frames: seq[int]
proc `$`(self: Name): string =
result &= &"Name(name='{self.name.name.lexeme}', depth={self.depth}, codePos={self.codePos})"
proc newCompiler*(enableOptimizations: bool = true, replMode: bool = false): Compiler =
@ -172,7 +177,6 @@ proc newCompiler*(enableOptimizations: bool = true, replMode: bool = false): Com
result.enableOptimizations = enableOptimizations
result.replMode = replMode
result.currentModule = ""
result.frames = @[]
## Forward declarations
@ -187,7 +191,7 @@ proc inferType(self: Compiler, node: Expression): Type
proc findByName(self: Compiler, name: string): seq[Name]
proc findByType(self: Compiler, name: string, kind: Type): seq[Name]
proc compareTypes(self: Compiler, a, b: Type): bool
proc patchReturnAddress(self: Compiler, retAddr: int)
proc patchReturnAddress(self: Compiler, pos: int)
## End of forward declarations
## Public getter for nicer error formatting
@ -261,6 +265,16 @@ proc makeConstant(self: Compiler, val: Expression, typ: Type): array[3, uint8] =
result = self.chunk.writeConstant(v.toQuad())
of Int64, UInt64:
result = self.chunk.writeConstant(v.toLong())
of String:
result = self.chunk.writeConstant(v.toBytes())
of Float32:
var f: float = 0.0
discard parseFloat(val.token.lexeme, f)
result = self.chunk.writeConstant(cast[array[4, uint8]](float32(f)))
of Float64:
var f: float = 0.0
discard parseFloat(val.token.lexeme, f)
result = self.chunk.writeConstant(cast[array[8, uint8]](f))
else:
discard
@ -268,90 +282,58 @@ proc makeConstant(self: Compiler, val: Expression, typ: Type): array[3, uint8] =
proc emitConstant(self: Compiler, obj: Expression, kind: Type) =
## Emits a constant instruction along
## with its operand
case self.inferType(obj).kind:
case kind.kind:
of Int64:
self.emitByte(LoadInt64)
of UInt64:
self.emitByte(LoadUInt64)
of Int32:
self.emitByte(LoadInt32)
of UInt32:
self.emitByte(LoadUInt32)
of Int16:
self.emitByte(LoadInt16)
of UInt16:
self.emitByte(LoadUInt16)
of Int8:
self.emitByte(LoadInt8)
of UInt8:
self.emitByte(LoadUInt8)
of String:
self.emitByte(LoadString)
let str = LiteralExpr(obj).literal.lexeme
if str.len() >= 16777216:
self.error("string constants cannot be larger than 16777216 bytes")
self.emitBytes(LiteralExpr(obj).literal.lexeme.len().toTriple())
of Float32:
self.emitByte(LoadFloat32)
of Float64:
self.emitByte(LoadFloat64)
else:
discard # TODO
self.emitBytes(self.makeConstant(obj, kind))
proc emitJump(self: Compiler, opcode: OpCode): int =
## Emits a dummy jump offset to be patched later. Assumes
## the largest offset (emits 4 bytes, one for the given jump
## opcode, while the other 3 are for the jump offset, which
## is set to the maximum unsigned 24 bit integer). If the shorter
## 16 bit alternative is later found to be better suited, patchJump
## will fix this. Returns the absolute index into the chunk's
## bytecode array where the given placeholder instruction was written
## Emits a dummy jump offset to be patched later
## and returns the absolute index into the chunk's
## bytecode array where the given placeholder
## instruction was written
self.emitByte(opcode)
self.emitBytes((0xffffff).toTriple())
self.emitBytes(0.toTriple())
result = self.chunk.code.len() - 4
proc patchJump(self: Compiler, offset: int) =
## Patches a previously emitted relative
## jump using emitJump. Since emitJump assumes
## a long jump, this also shrinks the jump
## offset and changes the bytecode instruction
## if possible (i.e. jump is in 16 bit range),
## but the converse is also true (i.e. it might
## change a regular jump into a long one)
## jump using emitJump
var jump: int = self.chunk.code.len() - offset
if jump > 16777215:
self.error("cannot jump more than 16777216 bytecode instructions")
if jump < uint16.high().int:
case OpCode(self.chunk.code[offset]):
of LongJumpForwards:
self.chunk.code[offset] = JumpForwards.uint8()
# We do this because a relative jump
# does not take its argument into account
# because it is hardcoded in the bytecode
# itself
jump -= 4
of LongJumpBackwards:
self.chunk.code[offset] = JumpBackwards.uint8()
jump -= 4
of LongJumpIfFalse:
self.chunk.code[offset] = JumpIfFalse.uint8()
of LongJumpIfFalsePop:
self.chunk.code[offset] = JumpIfFalsePop.uint8()
of LongJumpIfFalseOrPop:
self.chunk.code[offset] = JumpIfFalseOrPop.uint8()
of JumpForwards, JumpBackwards:
jump -= 3
else:
discard
self.chunk.code.delete(offset + 1) # Discards the first 8 bits of the jump offset (which are empty)
let offsetArray = (jump - 1).toDouble() # -1 since we got rid of 1 byte!
self.chunk.code[offset + 1] = offsetArray[0]
self.chunk.code[offset + 2] = offsetArray[1]
else:
case OpCode(self.chunk.code[offset]):
of JumpForwards:
self.chunk.code[offset] = LongJumpForwards.uint8()
jump -= 3
of JumpBackwards:
self.chunk.code[offset] = LongJumpBackwards.uint8()
jump -= 3
of JumpIfFalse:
self.chunk.code[offset] = LongJumpIfFalse.uint8()
of JumpIfFalsePop:
self.chunk.code[offset] = LongJumpIfFalsePop.uint8()
of JumpIfFalseOrPop:
self.chunk.code[offset] = LongJumpIfFalseOrPop.uint8()
of LongJumpForwards, LongJumpBackwards:
jump -= 4
else:
discard
let offsetArray = jump.toTriple()
self.chunk.code[offset + 1] = offsetArray[0]
self.chunk.code[offset + 2] = offsetArray[1]
self.chunk.code[offset + 3] = offsetArray[2]
let offsetArray = (jump - 4).toTriple()
self.chunk.code[offset + 1] = offsetArray[0]
self.chunk.code[offset + 2] = offsetArray[1]
self.chunk.code[offset + 3] = offsetArray[2]
proc resolve(self: Compiler, name: IdentExpr,
@ -371,34 +353,44 @@ proc resolve(self: Compiler, name: IdentExpr,
return nil
proc getStackPos(self: Compiler, name: IdentExpr,
depth: int = self.scopeDepth): tuple[closedOver: bool, pos: int] =
## Iterates the internal list of declared names backwards and
## returns a tuple (closedOver, pos) that tells the caller whether the
## the name is to be emitted as a closure as well as its predicted
## stack/closure array position. Returns (false, -1) if the variable's
## location can not be determined at compile time (this is an error!).
## Note that private names declared in other modules will not be resolved!
var i: int = self.names.high()
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):
if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule:
continue
elif variable.depth == depth or variable.depth == 0:
# variable.depth == 0 for globals!
return (false, i)
elif variable.depth > 0:
var j: int = self.closedOver.high()
for closure in reversed(self.closedOver):
if closure.name.token.lexeme == name.name.lexeme:
return (true, j)
inc(j)
dec(i)
return (false, -1)
found = true
break
inc(result)
if not found:
return -1
proc detectClosureVariable(self: Compiler, name: Name,
depth: int = self.scopeDepth) =
proc getClosurePos(self: Compiler, name: IdentExpr, depth: int = self.scopeDepth): 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
## are private in other modules)
result = self.closedOver.high()
var found = false
for variable in reversed(self.closedOver):
if name.name.lexeme == variable.name.name.lexeme:
if variable.isPrivate and variable.owner != self.currentModule:
continue
elif variable.depth == depth:
found = true
break
dec(result)
if not found:
return -1
proc detectClosureVariable(self: Compiler, name: Name, depth: int = self.scopeDepth) =
## 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
@ -407,15 +399,14 @@ proc detectClosureVariable(self: Compiler, name: Name,
## each time a name is referenced in order for closed-over variables
## to be emitted properly, otherwise the runtime may behave
## unpredictably or crash
if name == nil:
if name == nil or name.depth == 0:
return
if name.depth > 0 and name.depth < depth:
elif name.depth < depth and not name.isClosedOver:
# 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 StoreHeap. We don't need to change
# other pieces of code because self.identifier() already
# emits LoadHeap if it detects the variable is closed over,
# whether or not this function is called
# 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 self.closedOver.len() >= 16777216:
@ -694,7 +685,8 @@ proc literal(self: Compiler, node: ASTNode) =
discard parseInt(y.literal.lexeme, x)
except ValueError:
self.error("integer value out of range")
self.emitConstant(y, Type(kind: Int64))
self.emitConstant(y, self.inferType(y))
of hexExpr:
var x: int
var y = HexExpr(node)
@ -707,7 +699,7 @@ proc literal(self: Compiler, node: ASTNode) =
stop: y.token.pos.start + len($x))
)
)
self.emitConstant(node, Type(kind: Int64))
self.emitConstant(node, self.inferType(y))
of binExpr:
var x: int
var y = BinExpr(node)
@ -720,7 +712,7 @@ proc literal(self: Compiler, node: ASTNode) =
stop: y.token.pos.start + len($x))
)
)
self.emitConstant(node, Type(kind: Int64))
self.emitConstant(node, self.inferType(y))
of octExpr:
var x: int
var y = OctExpr(node)
@ -733,7 +725,7 @@ proc literal(self: Compiler, node: ASTNode) =
stop: y.token.pos.start + len($x))
)
)
self.emitConstant(node, Type(kind: Int64))
self.emitConstant(node, self.inferType(y))
of floatExpr:
var x: float
var y = FloatExpr(node)
@ -741,7 +733,7 @@ proc literal(self: Compiler, node: ASTNode) =
discard parseFloat(y.literal.lexeme, x)
except ValueError:
self.error("floating point value out of range")
self.emitConstant(y, Type(kind: Float64))
self.emitConstant(y, self.inferType(y))
of awaitExpr:
var y = AwaitExpr(node)
self.expression(y.expression)
@ -798,20 +790,40 @@ proc matchImpl(self: Compiler, name: string, kind: Type): Name =
return impl[0]
proc emitFunction(self: Compiler, node: Name) =
## Wrapper to emit LoadFunction instructions
self.emitByte(LoadFunction)
self.emitBytes((node.codePos + 4).toTriple())
proc generateCall(self: Compiler, fn: Name, args: seq[Expression]) =
## Small wrapper that abstracts emitting a call instruction
## for a given function
self.emitByte(LoadNil) # Stack alignment
self.emitByte(LoadUInt32)
# We patch it later!
let idx = self.chunk.consts.len()
self.emitBytes(self.chunk.writeConstant((0xffffffff'u32).toQuad()))
for argument in args:
self.expression(argument) # Pushes the arguments onto the stack
self.emitByte(Call) # Creates a stack frame
self.emitBytes(fn.codePos.toTriple())
self.emitBytes((args.len()).toTriple())
self.patchReturnAddress(idx)
self.emitFunction(fn)
self.emitByte(LoadReturnAddress)
let pos = self.chunk.code.len()
self.emitBytes(0.toQuad())
for argument in reversed(args):
# We pass the arguments in reverse because
# we delegate the callee with popping them
# off the operand stack when they're invoked,
# rather than doing it ourselves. The reason
# for this is that the VM can always find the
# function object at a constant location in the
# stack frame instead of needing extra math to
# skip the arguments
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 callUnaryOp(self: Compiler, fn: Name, op: UnaryExpr) =
@ -881,8 +893,10 @@ proc declareName(self: Compiler, node: Declaration) =
# 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.depth == self.scopeDepth and name.valueType.kind notin {Function, CustomType}:
# Trying to redeclare a variable in the same module is an error!
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 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}")
self.names.add(Name(depth: self.scopeDepth,
name: node.name,
@ -920,7 +934,7 @@ proc declareName(self: Compiler, node: Declaration) =
returnType: self.inferType(
node.returnType),
args: @[]),
codePos: self.chunk.code.high(),
codePos: self.chunk.code.len(),
name: node.name,
isLet: false,
isClosedOver: false,
@ -930,7 +944,7 @@ 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,
# 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
name = Name(depth: self.scopeDepth + 1,
@ -941,7 +955,9 @@ proc declareName(self: Compiler, node: Declaration) =
valueType: nil,
codePos: 0,
isLet: false,
isClosedOver: false)
isClosedOver: false,
line: argument.name.token.line,
isFunctionArgument: true)
self.names.add(name)
name.valueType = self.inferType(argument.valueType)
if argument.mutable:
@ -975,20 +991,15 @@ proc identifier(self: Compiler, node: IdentExpr) =
self.emitConstant(node, self.inferType(node))
else:
self.detectClosureVariable(s)
let t = self.getStackPos(node)
var index = t.pos
# We don't check if index is -1 because if it
# were, self.resolve() would have returned nil
if not t.closedOver:
if not s.isClosedOver:
# Static name resolution, loads value at index in the stack. Very fast. Much wow.
if self.scopeDepth > 0:
inc(index, 1)
self.emitByte(LoadVar)
self.emitBytes((index - self.frames[^1]).toTriple())
# No need to check for -1 here: we already did a nil-check above!
self.emitBytes(self.getStackPos(s.name).toTriple())
else:
# Heap-allocated closure variable. Stored in a separate "closure array" in the VM that does not have stack semantics
# and where the no-effect invariant is not kept. This makes closures work as expected and is not much slower than
# indexing our stack (since they're both dynamic arrays at runtime anyway)
# Loads a closure variable. Stored in a separate "closure array" in the VM that does not
# align its semantics with the call stack. This makes closures work as expected and is
# 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())
@ -1007,16 +1018,16 @@ proc assignment(self: Compiler, node: ASTNode) =
elif r.isLet:
self.error(&"cannot reassign '{name.token.lexeme}'")
self.expression(node.value)
let t = self.getStackPos(name)
let index = t.pos
if index != -1:
if not t.closedOver:
self.emitByte(StoreVar)
else:
self.emitByte(StoreClosure)
self.emitBytes(index.toTriple())
self.detectClosureVariable(r)
if not r.isClosedOver:
self.emitByte(StoreVar)
self.emitBytes(self.getStackPos(name).toTriple())
else:
self.error(&"reference to undeclared name '{node.token.lexeme}'")
# Loads a closure variable. Stored in a separate "closure array" in the VM that does not
# align its semantics with the call stack. This makes closures work as expected and is
# not much slower than indexing our stack (since they're both dynamic arrays at runtime anyway)
self.emitByte(StoreClosure)
self.emitBytes(self.getClosurePos(name).toTriple())
of setItemExpr:
let node = SetItemExpr(node)
let typ = self.inferType(node)
@ -1033,7 +1044,7 @@ proc beginScope(self: Compiler) =
inc(self.scopeDepth)
proc endScope(self: Compiler, fromFunc: bool = false) =
proc endScope(self: Compiler, deleteNames: bool = true, fromFunc: bool = false) =
## 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)")
@ -1045,7 +1056,7 @@ proc endScope(self: Compiler, fromFunc: bool = false) =
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(Pop)
self.emitByte(PopC)
if self.enableOptimizations and len(names) > 1 and not fromFunc:
# If we're popping less than 65535 variables, then
# we can emit a PopN instruction. This is true for
@ -1059,27 +1070,28 @@ proc endScope(self: Compiler, fromFunc: bool = false) =
if len(names) > uint16.high().int():
for i in countdown(self.names.high(), len(names) - uint16.high().int()):
if self.names[i].depth > self.scopeDepth:
self.emitByte(Pop)
self.emitByte(PopC)
elif len(names) == 1 and not fromFunc:
# We only emit PopN if we're popping more than one value
self.emitByte(Pop)
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 ¯\_(ツ)_/¯
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)
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)
proc blockStmt(self: Compiler, node: BlockStmt) =
@ -1176,11 +1188,6 @@ proc callExpr(self: Compiler, node: CallExpr) =
proc expression(self: Compiler, node: Expression) =
## Compiles all expressions
if self.inferType(node) == nil:
if node.kind != identExpr:
# So we can raise a more appropriate
# error in self.identifier()
self.error("expression has no type")
case node.kind:
of NodeKind.callExpr:
self.callExpr(CallExpr(node)) # TODO
@ -1237,7 +1244,24 @@ proc deferStmt(self: Compiler, node: DeferStmt) =
self.expression(node.expression)
for i in countup(current, self.chunk.code.high()):
self.deferred.add(self.chunk.code[i])
self.chunk.code.del(i)
self.chunk.code.delete(i) # TODO: Do not change bytecode size
proc endFunctionBeforeReturn(self: Compiler) =
## Emits code to clear a function's
## stack frame right before executing
## its return instruction
var popped = 0
for name in self.names:
if name.depth == self.scopeDepth and name.valueType.kind != Function:
inc(popped)
if self.enableOptimizations and popped > 1:
self.emitByte(PopN)
self.emitBytes(popped.toDouble())
dec(popped, uint16.high().int)
while popped > 0:
self.emitByte(PopC)
dec(popped)
proc returnStmt(self: Compiler, node: ReturnStmt) =
@ -1247,8 +1271,11 @@ proc returnStmt(self: Compiler, node: ReturnStmt) =
let typ = self.inferType(self.currentFunction)
## Having the return type
if returnType == nil and typ.returnType != nil:
if node.value.kind == identExpr:
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
if node.value != nil:
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(typ.returnType)}', but expression has no type")
elif typ.returnType == nil and returnType != nil:
self.error("non-empty return statement is not allowed in void functions")
@ -1256,9 +1283,13 @@ 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.ReturnValue)
self.emitByte(OpCode.SetResult)
self.endFunctionBeforeReturn()
self.emitByte(OpCode.Return)
if node.value != nil:
self.emitByte(1)
else:
self.emitByte(OpCode.Return)
self.emitByte(0)
proc yieldStmt(self: Compiler, node: YieldStmt) =
@ -1322,11 +1353,16 @@ proc statement(self: Compiler, node: Statement) =
of exprStmt:
var expression = ExprStmt(node).expression
self.expression(expression)
# We only print top-level expressions
if self.replMode and self.scopeDepth == 0:
self.emitByte(PopRepl)
if expression.kind == callExpr and self.inferType(CallExpr(expression).callee).returnType == nil:
# The expression has no type, so we don't have to
# pop anything
discard
else:
self.emitByte(Pop) # Expression statements discard their value. Their main use case is side effects in function calls
# We only print top-level expressions
if self.replMode and self.scopeDepth == 0:
self.emitByte(PopRepl)
else:
self.emitByte(Pop)
of NodeKind.ifStmt:
self.ifStmt(IfStmt(node))
of NodeKind.assertStmt:
@ -1371,6 +1407,8 @@ proc varDecl(self: Compiler, node: VarDecl) =
let expected = self.inferType(node.valueType)
let actual = self.inferType(node.value)
if expected == nil and actual == nil:
if node.value.kind == identExpr:
self.error(&"reference to undeclared identifier '{node.value.token.lexeme}'")
self.error(&"'{node.name.token.lexeme}' has no type")
elif expected != nil and expected.kind == Mutable: # I mean, variables *are* already mutable (some of them anyway)
self.error(&"invalid type '{self.typeToStr(expected)}' for var")
@ -1379,27 +1417,24 @@ 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)
self.emitByte(StoreVar)
self.emitBytes(self.names.high().toTriple())
proc funDecl(self: Compiler, node: FunDecl) =
## Compiles function declarations
# A function's code is just compiled linearly
# and then jumped over
# While the compiler stores functions as if they
# were on the stack, in reality there is nothing
# at runtime that represents them (instead, we
# compile everything in a single blob and jump back
# and forth). If we didn't align the stack to account
# for this behavior, all of our offsets would be off
# by how many functions we have declared. We could fix
# this by storing functions in a separate list of identifiers,
# but that's rather unelegant and requires specialized functions
# for looking identifiers vs functions, which is not ideal
let jmp = self.emitJump(JumpForwards)
var function = self.currentFunction
self.declareName(node)
self.frames.add(self.names.len() - 1)
self.frames.add(self.names.high())
let fn = self.names[^(node.arguments.len() + 1)]
fn.codePos = self.chunk.code.len()
let jmp = self.emitJump(LongJumpForwards)
for argument in node.arguments:
self.emitByte(LoadArgument)
if node.returnType != nil and self.inferType(node.returnType) == nil:
self.error(&"cannot infer the type of '{node.returnType.token.lexeme}'")
# TODO: Forward declarations
if node.body != nil:
if BlockStmt(node.body).code.len() == 0:
@ -1428,26 +1463,32 @@ proc funDecl(self: Compiler, node: FunDecl) =
var deferStart = self.deferred.len()
# We let our debugger know a function is starting
let start = self.chunk.code.high()
self.beginScope()
# self.emitByte(LoadNil)
for decl in BlockStmt(node.body).code:
self.declaration(decl)
self.endScope(fromFunc=true)
var typ: Type
var hasVal: bool = false
case self.currentFunction.kind:
of NodeKind.funDecl:
if not self.currentFunction.hasExplicitReturn:
let typ = self.inferType(self.currentFunction)
if self.currentFunction.returnType == nil and typ.returnType != nil:
self.error("non-empty return statement is not allowed in void functions")
if self.currentFunction.returnType != nil:
self.error("function has an explicit return type, but no return statement was found")
self.emitByte(OpCode.Return)
typ = self.inferType(self.currentFunction)
hasVal = self.currentFunction.hasExplicitReturn
of NodeKind.lambdaExpr:
if not LambdaExpr(Declaration(self.currentFunction)).hasExplicitReturn:
self.emitByte(OpCode.Return)
typ = self.inferType(LambdaExpr(Declaration(self.currentFunction)))
hasVal = LambdaExpr(Declaration(self.currentFunction)).hasExplicitReturn
else:
discard # Unreachable
if hasVal and self.currentFunction.returnType == nil and typ.returnType != nil:
self.error("non-empty return statement is not allowed in void functions")
elif not hasVal and self.currentFunction.returnType != nil:
self.error("function has an explicit return type, but no return statement was found")
# self.endFunctionBeforeReturn()
hasVal = hasVal and typ.returnType != nil
self.endScope(deleteNames=true, fromFunc=false)
self.emitByte(OpCode.Return)
if hasVal:
self.emitByte(1)
else:
self.emitByte(0)
# Function is ending!
self.chunk.cfi.add(start.toTriple())
self.chunk.cfi.add(self.chunk.code.high().toTriple())
@ -1472,15 +1513,15 @@ proc funDecl(self: Compiler, node: FunDecl) =
discard self.frames.pop()
proc patchReturnAddress(self: Compiler, retAddr: int) =
proc patchReturnAddress(self: Compiler, pos: int) =
## Patches the return address of a function
## call. This is called at each iteration of
## the compiler's loop
## call
let address = self.chunk.code.len().toQuad()
self.chunk.consts[retAddr] = address[0]
self.chunk.consts[retAddr + 1] = address[1]
self.chunk.consts[retAddr + 2] = address[2]
self.chunk.consts[retAddr + 3] = address[3]
self.chunk.code[pos] = address[0]
self.chunk.code[pos + 1] = address[1]
self.chunk.code[pos + 2] = address[2]
self.chunk.code[pos + 3] = address[3]
proc declaration(self: Compiler, node: Declaration) =
@ -1505,12 +1546,41 @@ proc compile*(self: Compiler, ast: seq[Declaration], file: string): Chunk =
self.currentFunction = nil
self.currentModule = self.file.extractFilename()
self.current = 0
self.frames = @[0]
# Every peon program has a hidden entry point in
# which user code is wrapped. Think of it as if
# peon is implicitly writing the main() function
# of your program and putting all of your code in
# there. While we call our entry point just like
# any regular peon function, we can't use our handy
# helper generateCall() because we need to keep track
# of where our program ends (which we don't know yet).
# To fix this, we emit dummy offsets and patch them
# later, once we know the boundaries of our hidden main()
var main = Name(depth: 0,
isPrivate: true,
isConst: false,
isLet: false,
isClosedOver: false,
owner: self.currentModule,
valueType: Type(kind: Function,
name: "",
returnType: nil,
args: @[]),
codePos: 13,
name: newIdentExpr(Token(lexeme: "", kind: Identifier)),
line: -1)
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())
while not self.done():
self.declaration(Declaration(self.step()))
if self.ast.len() > 0:
# *Technically* an empty program is a valid program
self.emitByte(ProgExit)
self.endScope(fromFunc=true)
self.patchReturnAddress(pos)
self.emitByte(OpCode.Return)
self.emitByte(0)
result = self.chunk
if self.ast.len() > 0 and self.scopeDepth != 0:
self.error(&"invalid state: invalid scopeDepth value (expected 0, got {self.scopeDepth}), did you forget to call endScope/beginScope?")

View File

@ -82,6 +82,8 @@ type
LoadFloat64,
LoadFloat32,
LoadString,
LoadFunction,
LoadReturnAddress,
## Singleton opcodes (each of them pushes a constant singleton on the stack)
LoadNil,
LoadTrue,
@ -118,8 +120,8 @@ type
LongJumpBackwards,
## Functions
Call, # Calls a function and initiates a new stack frame
Return, # Terminates the current function without popping off the stack
ReturnValue, # Pops a return value off the stack and terminates the current function
Return, # Terminates the current function
SetResult, # Sets the result of the current function
## Exception handling
Raise, # Raises exception x or re-raises active exception if x is nil
BeginTry, # Initiates an exception handling context
@ -131,7 +133,8 @@ type
## Misc
Assert, # Raises an AssertionFailed exception if x is false
NoOp, # Just a no-op
ProgExit, # Terminates the whole program
LoadArgument,
PopC
# We group instructions by their operation/operand types for easier handling when debugging
@ -142,8 +145,9 @@ const simpleInstructions* = {Return, LoadNil,
LoadNan, LoadInf,
Pop, PopRepl, Raise,
BeginTry, FinishTry, Yield,
Await, NoOp, ReturnValue,
PopClosure, ProgExit}
Await, NoOp, PopClosure,
SetResult, LoadArgument,
PopC}
# Constant instructions are instructions that operate on the bytecode constant table
const constantInstructions* = {LoadInt64, LoadUInt64,

View File

@ -3,7 +3,6 @@ import strformat
import strutils
import terminal
import parseopt
import nimSHA2
import times
import os
@ -15,7 +14,6 @@ import jale/plugin/editor_history
import jale/keycodes
import jale/multiline
# Our stuff
import frontend/lexer as l
import frontend/parser as p
@ -32,12 +30,12 @@ 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
proc repl =
proc repl(vm: PeonVM = newPeonVM()) =
styledEcho fgMagenta, "Welcome into the peon REPL!"
var
keep = true
@ -50,7 +48,6 @@ proc repl =
compiler = newCompiler(replMode=true)
debugger = newDebugger()
serializer = newSerializer()
vm = newPeonVM()
editor = getLineEditor()
input: string
current: string
@ -110,7 +107,6 @@ proc repl =
when debugSerializer:
var hashMatches = computeSHA256(input).toHex().toLowerAscii() == serialized.fileHash
styledEcho fgCyan, "Serialization step: "
styledEcho fgBlue, &"\t- File hash: ", fgYellow, serialized.fileHash, fgBlue, " (", if hashMatches: fgGreen else: fgRed, if hashMatches: "OK" else: "Fail", fgBlue, ")"
styledEcho fgBlue, "\t- Peon version: ", fgYellow, &"{serialized.version.major}.{serialized.version.minor}.{serialized.version.patch}", fgBlue, " (commit ", fgYellow, serialized.commit[0..8], fgBlue, ") on branch ", fgYellow, serialized.branch
stdout.styledWriteLine(fgBlue, "\t- Compilation date & time: ", fgYellow, fromUnix(serialized.compileDate).format("d/M/yyyy HH:mm:ss"))
stdout.styledWrite(fgBlue, &"\t- Constants segment: ")
@ -163,7 +159,6 @@ proc repl =
styledEcho fgBlue, "Source line: " , fgDefault, line
styledEcho fgCyan, " ".repeat(len("Source line: ")) & "^".repeat(relPos.stop - relPos.start)
except CompileError:
input = ""
let exc = CompileError(getCurrentException())
let lexeme = exc.node.token.lexeme
let lineNo = exc.node.token.line
@ -184,7 +179,7 @@ proc repl =
quit(0)
proc runFile(f: string) =
proc runFile(f: string, interactive: bool = false, fromString: bool = false) =
var
tokens: seq[Token] = @[]
tree: seq[Declaration] = @[]
@ -199,7 +194,13 @@ proc runFile(f: string) =
input: string
tokenizer.fillSymbolTable()
try:
input = readFile(f)
var f = f
if not fromString:
if not f.endsWith(".pn"):
f &= ".pn"
input = readFile(f)
else:
input = f
tokens = tokenizer.lex(input, f)
if tokens.len() == 0:
return
@ -302,6 +303,8 @@ proc runFile(f: string) =
stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {getCurrentExceptionMsg()}")
except OSError:
stderr.styledWriteLine(fgRed, "An error occurred while trying to read ", fgYellow, &"'{f}'", fgGreen, &": {osErrorMsg(osLastError())} [errno {osLastError()}]")
if interactive:
repl(vm)
when isMainModule:
@ -353,7 +356,7 @@ when isMainModule:
if file == "":
repl()
else:
runFile(file)
runFile(file, interactive, fromString)
proc fillSymbolTable(tokenizer: Lexer) =

View File

@ -71,7 +71,7 @@ proc checkFrameStart(self: Debugger, n: int) =
styledEcho fgBlue, "\n==== Peon Bytecode Debugger - Begin Frame ", fgYellow, &"'{e.name}' ", fgBlue, "(", fgYellow, $i, fgBlue, ") ===="
styledEcho fgGreen, "\t- Start offset: ", fgYellow, $e.start
styledEcho fgGreen, "\t- End offset: ", fgYellow, $e.stop
styledEcho fgGreen, "\t- Stack bottom: ", fgYellow, $e.bottom
styledEcho fgGreen, "\t- Frame bottom: ", fgYellow, $e.bottom
styledEcho fgGreen, "\t- Argument count: ", fgYellow, $e.argc
@ -83,18 +83,24 @@ proc checkFrameEnd(self: Debugger, n: int) =
proc simpleInstruction(self: Debugger, instruction: OpCode) =
printInstruction(instruction)
nl()
printInstruction(instruction, true)
self.current += 1
if instruction in {Return, ReturnValue}:
if instruction == Return:
printDebug("Void: ")
if self.chunk.code[self.current] == 0:
stdout.styledWriteLine(fgYellow, "Yes")
else:
stdout.styledWriteLine(fgYellow, "No")
self.current += 1
self.checkFrameEnd(self.current - 2)
self.checkFrameEnd(self.current - 1)
self.checkFrameEnd(self.current)
proc stackTripleInstruction(self: Debugger, instruction: OpCode) =
## Debugs instructions that operate on a single value on the stack using a 24-bit operand
var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[
self.current + 3]].fromTriple()
var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWriteLine(fgGreen, &", points to index ", fgYellow, $slot)
self.current += 4
@ -127,25 +133,34 @@ proc argumentTripleInstruction(self: Debugger, instruction: OpCode) =
proc callInstruction(self: Debugger, instruction: OpCode) =
## Debugs function calls
var slot = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
var args = [self.chunk.code[self.current + 4], self.chunk.code[self.current + 5], self.chunk.code[self.current + 6]].fromTriple()
var size = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", jumps to address ", fgYellow, $slot, fgGreen, " with ", fgYellow, $args, fgGreen, " argument")
if args > 1 or args == 0:
stdout.styledWrite(fgGreen, "s")
nl()
self.current += 7
styledEcho fgGreen, &", creates frame of size ", fgYellow, $size
self.current += 4
proc functionInstruction(self: Debugger, instruction: OpCode) =
## Debugs function calls
var address = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
printInstruction(instruction)
styledEcho fgGreen, &", loads function at address ", fgYellow, $address
self.current += 4
proc loadAddressInstruction(self: Debugger, instruction: OpCode) =
## Debugs LoadReturnAddress instructions
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
self.current += 5
proc constantInstruction(self: Debugger, instruction: OpCode) =
## Debugs instructions that operate on the constant table
var constant = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[
self.current + 3]].fromTriple()
var constant = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple()
printInstruction(instruction)
stdout.styledWrite(fgGreen, &", points to constant at position ", fgYellow, $constant)
nl()
printDebug("Operand: ")
stdout.styledWriteLine(fgYellow, &"{self.chunk.consts[constant]}")
stdout.styledWriteLine(fgGreen, &", points to constant at position ", fgYellow, $constant)
self.current += 4
@ -158,8 +173,7 @@ proc jumpInstruction(self: Debugger, instruction: OpCode) =
jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2]].fromDouble().int()
of LongJump, LongJumpIfFalse, LongJumpIfTrue, LongJumpIfFalsePop,
LongJumpForwards, LongJumpBackwards:
jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[
self.current + 3]].fromTriple().int()
jump = [self.chunk.code[self.current + 1], self.chunk.code[self.current + 2], self.chunk.code[self.current + 3]].fromTriple().int()
self.current += 1
else:
discard # Unreachable
@ -172,10 +186,11 @@ proc jumpInstruction(self: Debugger, instruction: OpCode) =
self.checkFrameStart(i)
proc disassembleInstruction*(self: Debugger) =
## Takes one bytecode instruction and prints it
printDebug("Offset: ")
stdout.styledWriteLine(fgYellow, $self.current)
stdout.styledWriteLine(fgYellow, $(self.current))
printDebug("Line: ")
stdout.styledWriteLine(fgYellow, &"{self.chunk.getLine(self.current)}")
var opcode = OpCode(self.chunk.code[self.current])
@ -196,6 +211,10 @@ proc disassembleInstruction*(self: Debugger) =
self.callInstruction(opcode)
of jumpInstructions:
self.jumpInstruction(opcode)
of LoadFunction:
self.functionInstruction(opcode)
of LoadReturnAddress:
self.loadAddressInstruction(opcode)
else:
echo &"DEBUG - Unknown opcode {opcode} at index {self.current}"
self.current += 1

View File

@ -20,7 +20,6 @@ import ../config
import strformat
import strutils
import nimSHA2
import times
@ -36,7 +35,6 @@ type
## the Serializer.read*
## procedures to store
## metadata
fileHash*: string
version*: tuple[major, minor, patch: int]
branch*: string
commit*: string
@ -45,7 +43,7 @@ type
proc `$`*(self: Serialized): string =
result = &"Serialized(fileHash={self.fileHash}, version={self.version.major}.{self.version.minor}.{self.version.patch}, branch={self.branch}), commitHash={self.commit}, date={self.compileDate}, chunk={self.chunk[]}"
result = &"Serialized(version={self.version.major}.{self.version.minor}.{self.version.patch}, branch={self.branch}), commitHash={self.commit}, date={self.compileDate}, chunk={self.chunk[]}"
proc error(self: Serializer, message: string) =
@ -72,7 +70,6 @@ proc writeHeaders(self: Serializer, stream: var seq[byte], file: string) =
stream.extend(PEON_BRANCH.toBytes())
stream.extend(PEON_COMMIT_HASH.toBytes())
stream.extend(getTime().toUnixFloat().int().toBytes())
stream.extend(computeSHA256(file).toBytes())
proc writeLineData(self: Serializer, stream: var seq[byte]) =
@ -129,8 +126,7 @@ proc readHeaders(self: Serializer, stream: seq[byte], serialized: Serialized): i
stream[3], stream[4], stream[5], stream[6], stream[7]]))
stream = stream[8..^1]
result += 8
serialized.fileHash = stream[0..<32].fromBytes().toHex().toLowerAscii()
result += 32
proc readLineData(self: Serializer, stream: seq[byte]): int =