Moved for peon types to its own module, added support for negating primitives in the compiler and for floats in the VM, added some more docs to the VM and restored the old empty-jump mechanism for closure declaration. Minor changes to endScope

This commit is contained in:
Mattia Giambirtone 2022-07-10 13:19:57 +02:00
parent e38610fdbd
commit 6116c127c6
4 changed files with 106 additions and 55 deletions

View File

@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import strformat
import strutils
type type
@ -62,3 +64,44 @@ type
value*: ptr PeonObject value*: ptr PeonObject
else: else:
discard # TODO discard # TODO
proc `$`*(self: PeonObject): string =
## Returns a string representation
## of a peon object
case self.kind:
of Int64:
result = &"{self.long}'i64"
of UInt64:
result = &"{self.uLong}'u64"
of Int32:
result = &"{self.`int`}'i32"
of UInt32:
result = &"{self.uInt}'u32"
of Int16:
result = &"{self.short}'i16"
of UInt16:
result = &"{self.uShort}'u16"
of Int8:
result = &"{self.tiny}'i8"
of UInt8:
result = &"{self.uTiny}'u8"
of Float32:
result = &"{self.halfFloat}'f32"
of Float64:
result = &"{self.`float`}'f64"
of ObjectKind.Inf:
if self.positive:
result ="inf"
else:
result ="-inf"
of ObjectKind.Nan, Nil:
result =($self.kind).toLowerAscii()
of Function:
result = &"fn(ip: {self.ip})"
of CustomType:
result = "typevar"
of String:
result = self.str
else:
discard

View File

@ -23,6 +23,7 @@ import ../util/multibyte
export types export types
type type
PeonVM* = ref object PeonVM* = ref object
## The Peon Virtual Machine ## The Peon Virtual Machine
@ -58,7 +59,7 @@ proc newPeonVM*: PeonVM =
result.initCache() result.initCache()
## Getters for singleton types (they are cached!) # Getters for singleton types (they are cached!)
proc getNil*(self: PeonVM): PeonObject {.inline.} = self.cache[0] proc getNil*(self: PeonVM): PeonObject {.inline.} = self.cache[0]
@ -74,10 +75,10 @@ proc getInf*(self: PeonVM, positive: bool): PeonObject {.inline.} =
proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5] proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5]
## Stack primitives. Note: all stack accessing that goes # Stack primitives. Note: all stack accessing that goes
## through the get/set wrappers is frame-relative, meaning # through the get(c)/set(c)/peek(c) wrappers is frame-relative,
## that the index is added to the current stack frame's # meaning that the index is added to the current stack frame's
## bottom to obtain an absolute stack index. # bottom to obtain an absolute stack index.
proc push(self: PeonVM, obj: PeonObject) = proc push(self: PeonVM, obj: PeonObject) =
## Pushes a Peon object onto the ## Pushes a Peon object onto the
@ -111,7 +112,7 @@ proc popc(self: PeonVM): PeonObject =
return self.calls.pop() return self.calls.pop()
proc peekc(self: PeonVM, distance: int = 0): PeonObject = proc peekc(self: PeonVM, distance: int = 0): PeonObject {.used.} =
## Returns the Peon object at the ## Returns the Peon object at the
## given distance from the top of ## given distance from the top of
## the call stack without consuming it ## the call stack without consuming it
@ -131,8 +132,8 @@ proc setc(self: PeonVM, idx: int, val: PeonObject) =
## frames ## frames
self.calls[idx + self.frames[^1]] = val self.calls[idx + self.frames[^1]] = val
## Byte-level primitives to read/decode # Byte-level primitives to read and decode
## bytecode # bytecode
proc readByte(self: PeonVM): uint8 = proc readByte(self: PeonVM): uint8 =
## Reads a single byte from the ## Reads a single byte from the
@ -168,6 +169,9 @@ proc readUInt(self: PeonVM): uint32 =
return uint32([self.readByte(), self.readByte(), self.readByte(), self.readByte()].fromQuad()) return uint32([self.readByte(), self.readByte(), self.readByte(), self.readByte()].fromQuad())
# Functions to read primitives from the chunk's
# constants table
proc constReadInt64(self: PeonVM, idx: int): PeonObject = proc constReadInt64(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the ## Reads a constant from the
## chunk's constant table and ## chunk's constant table and
@ -287,45 +291,11 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject =
copyMem(result.`float`.addr, arr.addr, sizeof(arr)) copyMem(result.`float`.addr, arr.addr, sizeof(arr))
proc `$`(self: PeonObject): string =
case self.kind:
of Int64:
result = &"{self.long}'i64"
of UInt64:
result = &"{self.uLong}'u64"
of Int32:
result = &"{self.`int`}'i32"
of UInt32:
result = &"{self.uInt}'u32"
of Int16:
result = &"{self.short}'i16"
of UInt16:
result = &"{self.uShort}'u16"
of Int8:
result = &"{self.tiny}'i8"
of UInt8:
result = &"{self.uTiny}'u8"
of Float32:
result = &"{self.halfFloat}'f32"
of Float64:
result = &"{self.`float`}'f64"
of ObjectKind.Inf:
if self.positive:
result ="inf"
else:
result ="-inf"
of ObjectKind.Nan, Nil:
result =($self.kind).toLowerAscii()
of Function:
result = &"fn(ip: {self.ip})"
else:
discard
proc dispatch*(self: PeonVM) = proc dispatch*(self: PeonVM) =
## Main bytecode dispatch loop ## Main bytecode dispatch loop
var instruction {.register.}: OpCode var instruction {.register.}: OpCode
while true: while true:
{.computedgoto.} # https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
when DEBUG_TRACE_VM: when DEBUG_TRACE_VM:
echo &"IP: {self.ip}" echo &"IP: {self.ip}"
echo &"Instruction: {OpCode(self.chunk.code[self.ip])}" echo &"Instruction: {OpCode(self.chunk.code[self.ip])}"
@ -343,7 +313,7 @@ proc dispatch*(self: PeonVM) =
discard readLine stdin discard readLine stdin
instruction = OpCode(self.readByte()) instruction = OpCode(self.readByte())
case instruction: case instruction:
# Constant loading # Constant loading instructions
of LoadTrue: of LoadTrue:
self.push(self.getBool(true)) self.push(self.getBool(true))
of LoadFalse: of LoadFalse:
@ -381,8 +351,8 @@ proc dispatch*(self: PeonVM) =
# instruction pointer # instruction pointer
self.push(PeonObject(kind: Function, ip: self.readLong())) self.push(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress: of LoadReturnAddress:
# Loads a 32-bit unsigned integer. Used to load function # Loads a 32-bit unsigned integer onto the operand stack.
# return addresses # Used to load function return addresses
self.push(PeonObject(kind: UInt32, uInt: self.readUInt())) self.push(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call: of Call:
# Calls a function. The calling convention for peon # Calls a function. The calling convention for peon
@ -390,7 +360,9 @@ proc dispatch*(self: PeonVM) =
# frame is a function object which contains the new # frame is a function object which contains the new
# instruction pointer to jump to, followed by a 32-bit # instruction pointer to jump to, followed by a 32-bit
# return address. After that, all arguments and locals # return address. After that, all arguments and locals
# follow # follow. Note that, due to how the stack works, all
# arguments before the call are in the reverse order in
# which they are passed to the function
var argc {.used.} = self.readLong().int var argc {.used.} = self.readLong().int
let retAddr = self.peek(-argc) # Return address let retAddr = self.peek(-argc) # Return address
let fnObj = self.peek(-argc - 1) # Function object let fnObj = self.peek(-argc - 1) # Function object
@ -439,7 +411,11 @@ proc dispatch*(self: PeonVM) =
self.ip = ret.uInt self.ip = ret.uInt
of SetResult: of SetResult:
# Sets the result of the # Sets the result of the
# current function # current function. A Return
# instruction will pop this
# off the results array and
# onto the operand stack when
# the current function exits.
self.results[self.frames.high()] = self.pop() self.results[self.frames.high()] = self.pop()
of StoreVar: of StoreVar:
# Stores the value at the top of the operand stack # Stores the value at the top of the operand stack
@ -621,6 +597,10 @@ proc dispatch*(self: PeonVM) =
self.push(PeonObject(kind: Int16, short: -self.pop().short)) self.push(PeonObject(kind: Int16, short: -self.pop().short))
of NegInt8: of NegInt8:
self.push(PeonObject(kind: Int8, tiny: -self.pop().tiny)) self.push(PeonObject(kind: Int8, tiny: -self.pop().tiny))
of NegFloat64:
self.push(PeonObject(kind: Float64, `float`: -self.pop().`float`))
of NegFloat32:
self.push(PeonObject(kind: Float32, halfFloat: -self.pop().halfFloat))
else: else:
discard discard

View File

@ -874,7 +874,8 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
## Emits instructions for builtin functions ## Emits instructions for builtin functions
## such as addition or subtraction ## such as addition or subtraction
if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]: if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]:
self.expression(args[1]) if len(args) == 2:
self.expression(args[1])
self.expression(args[0]) self.expression(args[0])
case fn.valueType.builtinOp: case fn.valueType.builtinOp:
of "AddInt64": of "AddInt64":
@ -957,6 +958,18 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitByte(DivFloat32) self.emitByte(DivFloat32)
of "MulFloat32": of "MulFloat32":
self.emitByte(MulFloat32) self.emitByte(MulFloat32)
of "NegInt64":
self.emitByte(NegInt64)
of "NegInt32":
self.emitByte(NegInt32)
of "NegInt16":
self.emitByte(NegInt16)
of "NegInt8":
self.emitByte(NegInt8)
of "NegFloat64":
self.emitByte(NegFloat64)
of "NegFloat32":
self.emitByte(NegFloat32)
of "LogicalOr": of "LogicalOr":
self.expression(args[0]) self.expression(args[0])
let jump = self.emitJump(JumpIfTrue) let jump = self.emitJump(JumpIfTrue)
@ -1062,6 +1075,21 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
line: node.token.line)) line: node.token.line))
if mutable: if mutable:
self.names[^1].valueType.mutable = true self.names[^1].valueType.mutable = true
# We emit a jump of 0 because this may become a
# StoreHeap instruction. If they variable is
# not closed over, we'll sadly be wasting a
# VM cycle. The previous implementation used 4 no-op
# instructions, which wasted 4 times as many clock
# cycles.
# TODO: Optimize this. It's a bit tricky because
# deleting bytecode would render all of our
# jump offsets and other absolute indeces in the
# bytecode wrong
if self.scopeDepth > 0:
# Closure variables are only used in local
# scopes
self.emitByte(JumpForwards)
self.emitBytes(0.toTriple())
of NodeKind.funDecl: of NodeKind.funDecl:
var node = FunDecl(node) var node = FunDecl(node)
# We declare the generics before the function so we # We declare the generics before the function so we
@ -1128,8 +1156,6 @@ proc identifier(self: Compiler, node: IdentExpr) =
# no matter the scope depth # no matter the scope depth
self.emitConstant(node, self.inferType(node)) self.emitConstant(node, self.inferType(node))
else: else:
self.emitByte(JumpForwards)
self.emitBytes(0.toTriple())
self.detectClosureVariable(s) self.detectClosureVariable(s)
if s.valueType.kind == Function: if s.valueType.kind == Function:
# Functions have no runtime # Functions have no runtime
@ -1197,7 +1223,7 @@ proc endScope(self: Compiler) =
dec(self.scopeDepth) dec(self.scopeDepth)
var names: seq[Name] = @[] var names: seq[Name] = @[]
for name in self.names: for name in self.names:
if name.depth > self.scopeDepth: if name.depth > self.scopeDepth and name.valueType.kind notin {Generic, CustomType, Function}:
names.add(name) names.add(name)
if len(names) > 1: if len(names) > 1:
# If we're popping less than 65535 variables, then # If we're popping less than 65535 variables, then
@ -1409,7 +1435,7 @@ proc endFunctionBeforeReturn(self: Compiler) =
## its return instruction ## its return instruction
var popped = 0 var popped = 0
for name in self.names: for name in self.names:
if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType} and not name.isClosedOver: if name.depth == self.scopeDepth and name.valueType.kind notin {Function, Generic, CustomType}:
inc(popped) inc(popped)
if popped > 1: if popped > 1:
self.emitByte(PopN) self.emitByte(PopN)

View File

@ -95,6 +95,8 @@ type
NegInt32, NegInt32,
NegInt16, NegInt16,
NegInt8, NegInt8,
NegFloat32,
NegFloat64,
AddInt64, AddInt64,
AddUInt64, AddUInt64,
AddInt32, AddInt32,
@ -195,8 +197,8 @@ const simpleInstructions* = {Return, LoadNil,
DivInt8, DivUInt8, AddFloat64, DivInt8, DivUInt8, AddFloat64,
SubFloat64, DivFloat64, MulFloat64, SubFloat64, DivFloat64, MulFloat64,
AddFloat32, SubFloat32, DivFloat32, AddFloat32, SubFloat32, DivFloat32,
MulFloat32 MulFloat32, NegFloat32, NegFloat64,
} }
# Constant instructions are instructions that operate on the bytecode constant table # Constant instructions are instructions that operate on the bytecode constant table
const constantInstructions* = {LoadInt64, LoadUInt64, const constantInstructions* = {LoadInt64, LoadUInt64,