Initial work on a two-stack design
This commit is contained in:
parent
f8ab292c27
commit
099f733db6
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?")
|
||||
|
|
|
@ -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,
|
||||
|
|
25
src/main.nim
25
src/main.nim
|
@ -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) =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue