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.
# See the License for the specific language governing permissions and
# limitations under the License.
import strformat
import strutils
type
@ -62,3 +64,44 @@ type
value*: ptr PeonObject
else:
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
type
PeonVM* = ref object
## The Peon Virtual Machine
@ -58,7 +59,7 @@ proc newPeonVM*: PeonVM =
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]
@ -74,10 +75,10 @@ proc getInf*(self: PeonVM, positive: bool): PeonObject {.inline.} =
proc getNan*(self: PeonVM): PeonObject {.inline.} = self.cache[5]
## Stack primitives. Note: all stack accessing that goes
## through the get/set wrappers is frame-relative, meaning
## that the index is added to the current stack frame's
## bottom to obtain an absolute stack index.
# Stack primitives. Note: all stack accessing that goes
# through the get(c)/set(c)/peek(c) wrappers is frame-relative,
# meaning that the index is added to the current stack frame's
# bottom to obtain an absolute stack index.
proc push(self: PeonVM, obj: PeonObject) =
## Pushes a Peon object onto the
@ -111,7 +112,7 @@ proc popc(self: PeonVM): PeonObject =
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
## given distance from the top of
## the call stack without consuming it
@ -131,8 +132,8 @@ proc setc(self: PeonVM, idx: int, val: PeonObject) =
## frames
self.calls[idx + self.frames[^1]] = val
## Byte-level primitives to read/decode
## bytecode
# Byte-level primitives to read and decode
# bytecode
proc readByte(self: PeonVM): uint8 =
## 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())
# Functions to read primitives from the chunk's
# constants table
proc constReadInt64(self: PeonVM, idx: int): PeonObject =
## Reads a constant from the
## chunk's constant table and
@ -287,45 +291,11 @@ proc constReadFloat64(self: PeonVM, idx: int): PeonObject =
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) =
## Main bytecode dispatch loop
var instruction {.register.}: OpCode
while true:
{.computedgoto.} # https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
when DEBUG_TRACE_VM:
echo &"IP: {self.ip}"
echo &"Instruction: {OpCode(self.chunk.code[self.ip])}"
@ -343,7 +313,7 @@ proc dispatch*(self: PeonVM) =
discard readLine stdin
instruction = OpCode(self.readByte())
case instruction:
# Constant loading
# Constant loading instructions
of LoadTrue:
self.push(self.getBool(true))
of LoadFalse:
@ -381,8 +351,8 @@ proc dispatch*(self: PeonVM) =
# instruction pointer
self.push(PeonObject(kind: Function, ip: self.readLong()))
of LoadReturnAddress:
# Loads a 32-bit unsigned integer. Used to load function
# return addresses
# Loads a 32-bit unsigned integer onto the operand stack.
# Used to load function return addresses
self.push(PeonObject(kind: UInt32, uInt: self.readUInt()))
of Call:
# 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
# instruction pointer to jump to, followed by a 32-bit
# 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
let retAddr = self.peek(-argc) # Return address
let fnObj = self.peek(-argc - 1) # Function object
@ -439,7 +411,11 @@ proc dispatch*(self: PeonVM) =
self.ip = ret.uInt
of SetResult:
# 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()
of StoreVar:
# 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))
of NegInt8:
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:
discard

View File

@ -874,7 +874,8 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
## Emits instructions for builtin functions
## such as addition or subtraction
if fn.valueType.builtinOp notin ["GenericLogicalOr", "GenericLogicalAnd"]:
self.expression(args[1])
if len(args) == 2:
self.expression(args[1])
self.expression(args[0])
case fn.valueType.builtinOp:
of "AddInt64":
@ -957,6 +958,18 @@ proc handleBuiltinFunction(self: Compiler, fn: Name, args: seq[Expression]) =
self.emitByte(DivFloat32)
of "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":
self.expression(args[0])
let jump = self.emitJump(JumpIfTrue)
@ -1062,6 +1075,21 @@ proc declareName(self: Compiler, node: Declaration, mutable: bool = false) =
line: node.token.line))
if mutable:
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:
var node = FunDecl(node)
# We declare the generics before the function so we
@ -1128,8 +1156,6 @@ proc identifier(self: Compiler, node: IdentExpr) =
# no matter the scope depth
self.emitConstant(node, self.inferType(node))
else:
self.emitByte(JumpForwards)
self.emitBytes(0.toTriple())
self.detectClosureVariable(s)
if s.valueType.kind == Function:
# Functions have no runtime
@ -1197,7 +1223,7 @@ proc endScope(self: Compiler) =
dec(self.scopeDepth)
var names: seq[Name] = @[]
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)
if len(names) > 1:
# If we're popping less than 65535 variables, then
@ -1409,7 +1435,7 @@ proc endFunctionBeforeReturn(self: Compiler) =
## its return instruction
var popped = 0
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)
if popped > 1:
self.emitByte(PopN)

View File

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