mirror of https://github.com/japl-lang/japl.git
861 lines
34 KiB
Nim
861 lines
34 KiB
Nim
# Copyright 2020 Mattia Giambirtone
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# 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.
|
|
|
|
## A stack-based bytecode virtual machine implementation.
|
|
## This is the entire runtime environment for JAPL
|
|
{.experimental: "implicitDeref".}
|
|
|
|
## Standard library imports
|
|
import strformat
|
|
## Our modules
|
|
import config
|
|
when not SKIP_STDLIB_INIT:
|
|
import stdlib
|
|
import compiler
|
|
import meta/opcode
|
|
import meta/frame
|
|
import types/baseObject
|
|
import types/japlString
|
|
import types/japlNil
|
|
import types/exception
|
|
import types/numbers
|
|
import types/boolean
|
|
import types/methods
|
|
import types/typeutils
|
|
import types/function
|
|
import types/native
|
|
import types/arrayList
|
|
import types/simpleHashMap
|
|
import multibyte
|
|
when DEBUG_TRACE_VM:
|
|
import util/debug
|
|
import terminal
|
|
|
|
|
|
type
|
|
KeyboardInterrupt* = object of CatchableError
|
|
## Custom exception to handle Ctrl+C
|
|
InterpretResult* = enum
|
|
## All possible interpretation results
|
|
Ok,
|
|
CompileError,
|
|
RuntimeError
|
|
VM* = object
|
|
## A wrapper around the virtual machine
|
|
## functionality. Using custom heap allocated
|
|
## types for everything might sound excessive,
|
|
## but bad things happen when nim's GC puts its
|
|
## hands on JAPL-owned objects, so it was decided
|
|
## to reduce the GC's impact to a minimal
|
|
lastPop*: ptr Obj
|
|
source*: ptr String
|
|
frames*: ptr ArrayList[CallFrame]
|
|
stack*: ptr ArrayList[ptr Obj]
|
|
objects*: ptr ArrayList[ptr Obj]
|
|
globals*: ptr SimpleHashMap
|
|
cached*: array[6, ptr Obj]
|
|
file*: ptr String
|
|
|
|
|
|
func handleInterrupt() {.noconv.} =
|
|
## Raises an appropriate exception
|
|
## to let us catch and handle
|
|
## Ctrl+C gracefully
|
|
raise newException(KeyboardInterrupt, "Ctrl+C")
|
|
|
|
|
|
proc initStack*(self: var VM) =
|
|
## Initializes the VM's stack, frame stack
|
|
## and objects arraylist
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Resetting the stack"
|
|
self.stack = newArrayList[ptr Obj]()
|
|
self.objects = newArrayList[ptr Obj]()
|
|
self.frames = newArrayList[CallFrame]()
|
|
|
|
|
|
proc resetStack*(self: VM) =
|
|
## Resets the VM's stack to a blank state
|
|
while self.stack.len() >= 1:
|
|
discard self.stack.pop()
|
|
while self.frames.len() >= 1:
|
|
discard self.frames.pop()
|
|
|
|
|
|
proc getBoolean(self: VM, kind: bool): ptr Obj =
|
|
## Tiny little optimization for booleans
|
|
## which are pre-allocated on startup
|
|
if kind:
|
|
return self.cached[0]
|
|
else:
|
|
return self.cached[1]
|
|
|
|
|
|
proc error*(self: VM, error: ptr JAPLException) =
|
|
## Reports runtime errors with a nice traceback
|
|
|
|
# TODO: Once we have proper exceptions,
|
|
# this procedure will be used to report
|
|
# those that were not catched and managed
|
|
# to climb the call stack to the first
|
|
# frame (the global code object)
|
|
|
|
# Exceptions are objects too and they need to
|
|
# be freed like any other entity in JAPL
|
|
self.objects.append(error) # TODO -> Move this somewhere else to mark exceptions even before they are raised
|
|
var previous = "" # All this stuff seems overkill, but it makes the traceback look nicer
|
|
var repCount = 0 # and if we are here we are far beyond a point where performance matters anyway
|
|
var mainReached = false
|
|
var output = ""
|
|
stderr.write("An unhandled exception occurred, traceback below:\n")
|
|
for frame in reversed(self.frames):
|
|
if mainReached:
|
|
break
|
|
var function = frame.function
|
|
var line = function.chunk.lines[frame.ip]
|
|
if function.name == nil:
|
|
output = &" File '{self.file}', line {line}, in <module>:"
|
|
mainReached = true
|
|
else:
|
|
output = &" File '{self.file}', line {line}, in {stringify(function.name)}():"
|
|
if output != previous:
|
|
if repCount > 0:
|
|
stderr.write(&" ...previous line repeated {repCount} more times...\n")
|
|
repCount = 0
|
|
previous = output
|
|
stderr.write(&"{output}\n")
|
|
else:
|
|
repCount += 1
|
|
stderr.write(error.stringify())
|
|
stderr.write("\n")
|
|
self.resetStack()
|
|
|
|
|
|
proc pop*(self: VM): ptr Obj =
|
|
## Pops an object off the stack
|
|
result = self.stack.pop()
|
|
|
|
|
|
proc push*(self: VM, obj: ptr Obj) =
|
|
## Pushes an object onto the stack
|
|
self.stack.append(obj)
|
|
if obj notin self.objects and obj notin self.cached:
|
|
self.objects.append(obj)
|
|
|
|
|
|
proc push*(self: VM, ret: returnType): bool =
|
|
## Pushes a return value from a builtin
|
|
## method onto the stack and handles errors
|
|
result = true
|
|
case ret.kind:
|
|
of returnTypes.Object:
|
|
self.push(ret.result)
|
|
of returnTypes.Exception:
|
|
self.error(cast[ptr JAPLException](ret.result))
|
|
result = false
|
|
of returnTypes.True:
|
|
self.push(self.cached[0])
|
|
of returnTypes.False:
|
|
self.push(self.cached[1])
|
|
of returnTypes.Nil:
|
|
self.push(self.cached[2])
|
|
of returnTypes.Inf:
|
|
self.push(self.cached[3])
|
|
of returnTypes.nInf:
|
|
self.push(self.cached[4])
|
|
of returnTypes.NotANumber:
|
|
self.push(self.cached[5])
|
|
|
|
|
|
proc peek*(self: VM, distance: int): ptr Obj =
|
|
## Peeks an object (at a given distance from the
|
|
## current index) from the stack
|
|
return self.stack[self.stack.high() - distance]
|
|
|
|
|
|
proc call(self: var VM, function: ptr Function, argCount: int): bool =
|
|
## Sets up the call frame and performs error checking
|
|
## when calling callables
|
|
if argCount < function.arity:
|
|
var arg: string
|
|
if function.arity > 1:
|
|
arg = "s"
|
|
self.error(newTypeError(&"function '{stringify(function.name)}' takes at least {function.arity} argument{arg}, got {argCount}"))
|
|
return false
|
|
elif argCount > function.arity and (argCount - function.arity) - function.optionals != 0:
|
|
self.error(newTypeError(&"function '{stringify(function.name)}' takes at least {function.arity} arguments and at most {function.arity + function.optionals}, got {argCount}"))
|
|
return false
|
|
if self.frames.len() == FRAMES_MAX:
|
|
self.error(newRecursionError("max recursion depth exceeded"))
|
|
return false
|
|
let slot = self.stack.high() - argCount
|
|
var frame = CallFrame(function: function, ip: 0, slot: slot, stack: self.stack)
|
|
self.frames.append(frame)
|
|
return true
|
|
|
|
|
|
proc call(self: var VM, native: ptr Native, argCount: int): bool =
|
|
## Does the same as self.call, but with native functions
|
|
if argCount != native.arity and native.arity != -1:
|
|
self.error(newTypeError(&"function '{stringify(native.name)}' takes {native.arity} argument(s), got {argCount}"))
|
|
return false
|
|
let slot = self.stack.high() - argCount + 1
|
|
var args: seq[ptr Obj]
|
|
for i in countup(slot, self.stack.high()):
|
|
args.add(self.stack[i])
|
|
let nativeResult = native.nimproc(args)
|
|
for i in countup(slot - 1, self.stack.high()):
|
|
discard self.pop() # TODO once stack is a custom datatype,
|
|
# just reduce its length
|
|
case nativeResult.kind:
|
|
of retNative.True:
|
|
self.push(self.getBoolean(true))
|
|
of retNative.False:
|
|
self.push(self.getBoolean(false))
|
|
of retNative.Object:
|
|
self.push(nativeResult.result)
|
|
of retNative.Nil:
|
|
self.push(self.cached[2])
|
|
of retNative.Inf:
|
|
self.push(self.cached[3])
|
|
of retNative.nInf:
|
|
self.push(self.cached[4])
|
|
of retNative.NotANumber:
|
|
self.push(self.cached[5])
|
|
of retNative.Exception:
|
|
self.error(cast[ptr JaplException](nativeResult.result))
|
|
return false
|
|
return true
|
|
|
|
|
|
proc callObject(self: var VM, callee: ptr Obj, argCount: uint8): bool =
|
|
## Wrapper around call() to do type checking
|
|
if callee.isCallable():
|
|
case callee.kind:
|
|
of ObjectType.Function:
|
|
return self.call(cast[ptr Function](callee), int(argCount))
|
|
of ObjectType.Native:
|
|
return self.call(cast[ptr Native](callee), int(argCount))
|
|
else: # TODO: Classes
|
|
discard # Unreachable
|
|
else:
|
|
self.error(newTypeError(&"object of type '{callee.typeName()}' is not callable"))
|
|
return false
|
|
|
|
|
|
proc defineGlobal*(self: var VM, name: string, value: ptr Obj) =
|
|
## Adds a key-value couple to the VM's global scope
|
|
self.globals[name.asStr()] = value
|
|
|
|
|
|
proc readByte(self: CallFrame): uint8 =
|
|
## Reads a single byte from the given
|
|
## frame's chunk of bytecode
|
|
inc(self.ip)
|
|
result = self.function.chunk.code[self.ip - 1]
|
|
|
|
|
|
proc readBytes(self: CallFrame): int =
|
|
## Reads and decodes 3 bytes from the
|
|
## given frame's chunk into an integer
|
|
var arr = [self.readByte(), self.readByte(), self.readByte()]
|
|
copyMem(result.addr, unsafeAddr(arr), sizeof(arr))
|
|
|
|
|
|
proc readShort(self: CallFrame): uint16 =
|
|
## Reads a 16 bit number from the
|
|
## given frame's chunk
|
|
fromDouble([self.readByte(), self.readByte()])
|
|
|
|
|
|
proc readConstant(self: CallFrame): ptr Obj =
|
|
## Reads a constant from the given
|
|
## frame's constant table
|
|
var arr = [self.readByte(), self.readByte(), self.readByte()]
|
|
var idx: int
|
|
copyMem(idx.addr, arr.addr, sizeof(arr))
|
|
result = self.function.chunk.consts[idx]
|
|
|
|
|
|
|
|
when DEBUG_TRACE_VM:
|
|
proc showRuntime*(self: VM, frame: CallFrame, iteration: uint64) =
|
|
## Shows debug information about the current
|
|
## state of the virtual machine
|
|
|
|
let view = frame.getView()
|
|
setForegroundColor(fgMagenta)
|
|
if iteration > 1:
|
|
echo "" # To separate different iterations
|
|
stdout.write("DEBUG - VM: General information\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write(&"DEBUG - VM:\tIteration -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write(&"{iteration}\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write("DEBUG - VM:\tStack -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write("[")
|
|
for i, v in self.stack:
|
|
stdout.write(stringify(v))
|
|
if i < self.stack.high():
|
|
stdout.write(", ")
|
|
stdout.write("]")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write("\nDEBUG - VM: \tGlobals -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write("{")
|
|
var i = 0
|
|
for k, v in self.globals.pairs():
|
|
stdout.write(&"'{k}': {stringify(v)}")
|
|
if i < self.globals.len() - 1:
|
|
stdout.write(", ")
|
|
i += 1
|
|
stdout.write("}")
|
|
setForegroundColor(fgMagenta)
|
|
stdout.write("\nDEBUG - VM: Frame information\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write("DEBUG - VM:\tType -> ")
|
|
setForegroundColor(fgYellow)
|
|
if frame.function.name == nil:
|
|
stdout.write("main\n")
|
|
else:
|
|
stdout.write(&"function, '{frame.function.name.stringify()}'\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write(&"DEBUG - VM:\tCount -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write(&"{self.frames.len()}\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write(&"DEBUG - VM:\tLength -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write(&"{view.len}\n")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write("DEBUG - VM:\tTable -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write("[")
|
|
for i, e in frame.function.chunk.consts:
|
|
stdout.write(stringify(e))
|
|
if i < len(frame.function.chunk.consts) - 1:
|
|
stdout.write(", ")
|
|
stdout.write("]")
|
|
setForegroundColor(fgGreen)
|
|
stdout.write("\nDEBUG - VM:\tStack view -> ")
|
|
setForegroundColor(fgYellow)
|
|
stdout.write("[")
|
|
for i, e in view:
|
|
stdout.write(stringify(e))
|
|
if i < len(view) - 1:
|
|
stdout.write(", ")
|
|
stdout.write("]\n")
|
|
setForegroundColor(fgMagenta)
|
|
echo "DEBUG - VM: Current instruction"
|
|
setForegroundColor(fgGreen)
|
|
discard disassembleInstruction(frame.function.chunk, frame.ip - 1)
|
|
setForegroundColor(fgDefault)
|
|
|
|
|
|
proc run(self: var VM): InterpretResult =
|
|
## Chews trough bytecode instructions executing
|
|
## them one at a time: this is the runtime's
|
|
## main loop
|
|
var frame = self.frames[self.frames.high()]
|
|
var instruction: OpCode
|
|
when DEBUG_TRACE_VM:
|
|
var iteration: uint64 = 0
|
|
while true:
|
|
instruction = OpCode(frame.readByte())
|
|
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
|
|
when DEBUG_TRACE_VM: # Insight inside the VM
|
|
iteration += 1
|
|
self.showRuntime(frame, iteration)
|
|
case instruction: # Main OpCodes dispatcher
|
|
of OpCode.Constant:
|
|
# Loads a constant from the chunk's constant
|
|
# table
|
|
self.push(frame.readConstant())
|
|
of OpCode.Negate:
|
|
# Performs unary negation
|
|
let operand = self.pop()
|
|
try:
|
|
if not self.push(operand.negate()):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported unary operator '-' for object of type '{operand.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Shl:
|
|
# Bitwise left-shift
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.binaryShl(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '<<' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Shr:
|
|
# Bitwise right-shift
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.binaryShr(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '>>' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Xor:
|
|
# Bitwise xor
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.binaryXor(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '^' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Bor:
|
|
# Bitwise or
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.binaryOr(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '&' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Bnot:
|
|
# Bitwise not
|
|
var operand = self.pop()
|
|
try:
|
|
if not self.push(operand.binaryNot()):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported unary operator '~' for object of type '{operand.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Band:
|
|
# Bitwise and
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.binaryAnd(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '&' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Add:
|
|
# Binary +
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.sum(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '+' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Subtract:
|
|
# Binary -
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.sub(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '-' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Divide:
|
|
# Binary /
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.trueDiv(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '/' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Multiply:
|
|
# Binary *
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.mul(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '*' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Mod:
|
|
# Binary % (modulo division)
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.divMod(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '%' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Pow:
|
|
# Binary ** (exponentiation)
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.pow(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '**' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.True:
|
|
self.push(cast[ptr Bool](self.getBoolean(true)))
|
|
of OpCode.False:
|
|
self.push(cast[ptr Bool](self.getBoolean(false)))
|
|
of OpCode.Nil:
|
|
self.push(cast[ptr Nil](self.cached[2]))
|
|
of OpCode.Nan:
|
|
self.push(cast[ptr NotANumber](self.cached[4]))
|
|
of OpCode.Inf:
|
|
self.push(cast[ptr Infinity](self.cached[3]))
|
|
of OpCode.Not:
|
|
self.push(self.getBoolean(self.pop().isFalsey()))
|
|
of OpCode.Equal:
|
|
# Compares object equality
|
|
# Here order doesn't matter, because if a == b
|
|
# then b == a (at least in *most* languages, sigh)
|
|
self.push(self.getBoolean(self.pop().eq(self.pop())))
|
|
# Doesn't this chain of calls look beautifully
|
|
# intuitive?
|
|
of OpCode.Less:
|
|
# Binary less (<)
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
var comp: tuple[result: bool, obj: ptr Obj]
|
|
try:
|
|
comp = left.lt(right)
|
|
if system.`==`(comp.obj, nil):
|
|
self.push(self.getBoolean(comp.result))
|
|
else:
|
|
self.push(comp.obj)
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '<' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Greater:
|
|
# Binary greater (>)
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
var comp: tuple[result: bool, obj: ptr Obj]
|
|
try:
|
|
comp = left.gt(right)
|
|
if system.`==`(comp.obj, nil):
|
|
self.push(self.getBoolean(comp.result))
|
|
else:
|
|
self.push(comp.obj)
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '>' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.LessOrEqual:
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
var comp: tuple[result: bool, obj: ptr Obj]
|
|
try:
|
|
comp = left.lt(right)
|
|
if not comp.result and left == right:
|
|
comp.result = true
|
|
if system.`==`(comp.obj, nil):
|
|
self.push(self.getBoolean(comp.result))
|
|
else:
|
|
self.push(comp.obj)
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '<' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.GreaterOrEqual:
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
var comp: tuple[result: bool, obj: ptr Obj]
|
|
try:
|
|
comp = left.gt(right)
|
|
if not comp.result and left == right:
|
|
comp.result = true
|
|
if system.`==`(comp.obj, nil):
|
|
self.push(self.getBoolean(comp.result))
|
|
else:
|
|
self.push(comp.obj)
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator '>' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.Is:
|
|
# Implements object identity (i.e. same pointer)
|
|
# This is implemented internally for obvious
|
|
# reasons and works on any pair of objects, which
|
|
# is why we call nim's system.== operator and NOT
|
|
# our custom one
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
self.push(self.getBoolean(system.`==`(left, right)))
|
|
of OpCode.As:
|
|
# Implements type casting (TODO: Only allow classes)
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(objAs(left, right.kind)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"unsupported binary operator 'as' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
|
|
return RuntimeError
|
|
of OpCode.GetItem:
|
|
# Implements expressions such as a[b]
|
|
# TODO: More generic method
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
try:
|
|
if not self.push(left.getItem(right)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"object of type '{left.typeName()}' does not support getItem expressions"))
|
|
return RuntimeError
|
|
of OpCode.Slice:
|
|
# Implements expressions such as a[b:c]
|
|
var right = self.pop()
|
|
var left = self.pop()
|
|
var operand = self.pop()
|
|
try:
|
|
if not self.push(operand.Slice(right, left)):
|
|
return RuntimeError
|
|
except NotImplementedError:
|
|
self.error(newTypeError(&"object of type '{operand.typeName()}' does not support slicing"))
|
|
return RuntimeError
|
|
of OpCode.DefineGlobal:
|
|
# Defines a global variable
|
|
var name = cast[ptr String](frame.readConstant())
|
|
self.globals[name] = self.peek(0)
|
|
discard self.pop()
|
|
of OpCode.GetGlobal:
|
|
# Retrieves a global variable
|
|
var constant = cast[ptr String](frame.readConstant())
|
|
if constant notin self.globals:
|
|
self.error(newReferenceError(&"undefined name '{constant}'"))
|
|
return RuntimeError
|
|
else:
|
|
self.push(self.globals[constant])
|
|
of OpCode.SetGlobal:
|
|
# Changes the value of an already defined global variable
|
|
var constant = cast[ptr String](frame.readConstant())
|
|
if constant notin self.globals:
|
|
self.error(newReferenceError(&"assignment to undeclared name '{constant}'"))
|
|
return RuntimeError
|
|
else:
|
|
self.globals[constant] = self.peek(0)
|
|
of OpCode.DeleteGlobal:
|
|
# Deletes a global variable
|
|
# TODO: Inspect potential issues with the GC
|
|
var constant = cast[ptr String](frame.readConstant())
|
|
if constant notin self.globals:
|
|
self.error(newReferenceError(&"undefined name '{constant}'"))
|
|
return RuntimeError
|
|
else:
|
|
self.globals.del(constant)
|
|
of OpCode.GetLocal:
|
|
# Retrieves a local variable
|
|
self.push(frame[frame.readBytes()])
|
|
of OpCode.SetLocal:
|
|
# Changes the value of an already defined local variable
|
|
frame[frame.readBytes()] = self.peek(0)
|
|
of OpCode.DeleteLocal:
|
|
# Deletes a local variable
|
|
# TODO: Inspect potential issues with the GC
|
|
frame.delete(frame.readBytes())
|
|
of OpCode.Pop:
|
|
# Pops an item off the stack
|
|
self.lastPop = self.pop()
|
|
of OpCode.JumpIfFalse:
|
|
# Skips a certain amount of
|
|
# bytecode instructions
|
|
# if the object at the top of
|
|
# our stack is falsey
|
|
let jmpOffset = int frame.readShort()
|
|
if isFalsey(self.peek(0)):
|
|
frame.ip += int jmpOffset
|
|
of OpCode.Jump:
|
|
# Jumps a certain amount of bytecode
|
|
# instructions, unconditionally
|
|
frame.ip += int frame.readShort()
|
|
of OpCode.Loop:
|
|
# Loops back a certain amount of
|
|
# bytecode instructions, unconditionally
|
|
frame.ip -= int frame.readShort()
|
|
of OpCode.Call:
|
|
# Implements functions call
|
|
var argCount = frame.readByte()
|
|
if not self.callObject(self.peek(int argCount), argCount):
|
|
return RuntimeError
|
|
frame = self.frames[self.frames.high()]
|
|
of OpCode.Break:
|
|
discard # Unused (the compiler converts it to other stuff before it arrives here)
|
|
of OpCode.Return:
|
|
# Handles returning values from the callee to the caller
|
|
# and sets up the stack to proceed with execution
|
|
var retResult = self.pop()
|
|
# Pops the function's frame
|
|
discard self.frames.pop()
|
|
if self.frames.len() == 0:
|
|
discard self.pop()
|
|
return OK
|
|
discard frame.clear()
|
|
self.push(retResult)
|
|
frame = self.frames[self.frames.high()]
|
|
|
|
|
|
proc freeObjects(self: VM) =
|
|
## Frees all the allocated objects
|
|
## from the VM
|
|
when DEBUG_TRACE_ALLOCATION:
|
|
var runtimeObjCount = len(self.objects)
|
|
var cacheCount = len(self.cached)
|
|
var runtimeFreed = 0
|
|
var cachedFreed = 0
|
|
for obj in reversed(self.objects):
|
|
freeObject(obj)
|
|
discard self.objects.pop()
|
|
when DEBUG_TRACE_ALLOCATION:
|
|
runtimeFreed += 1
|
|
for cached_obj in self.cached:
|
|
freeObject(cached_obj)
|
|
when DEBUG_TRACE_ALLOCATION:
|
|
cachedFreed += 1
|
|
when DEBUG_TRACE_ALLOCATION:
|
|
echo &"DEBUG - VM: Freed {runtimeFreed + cachedFreed} objects out of {runtimeObjCount + cacheCount} ({cachedFreed}/{cacheCount} cached objects, {runtimeFreed}/{runtimeObjCount} runtime objects)"
|
|
|
|
|
|
proc freeVM*(self: VM) =
|
|
## Tears down the VM
|
|
unsetControlCHook()
|
|
try:
|
|
self.freeObjects()
|
|
freeObject(self.objects)
|
|
freeObject(self.stack)
|
|
freeObject(self.frames)
|
|
except NilAccessDefect:
|
|
stderr.write("A fatal error occurred -> could not free memory, segmentation fault\n")
|
|
quit(71)
|
|
when DEBUG_TRACE_ALLOCATION:
|
|
if self.objects.len > 0:
|
|
echo &"DEBUG - VM: Warning, {self.objects.len} objects were not freed"
|
|
echo "DEBUG - VM: The virtual machine has shut down"
|
|
|
|
|
|
proc initCache(self: var VM) =
|
|
## Initializes the static cache for singletons
|
|
## such as true and false
|
|
|
|
# TODO -> Make sure that every operation
|
|
# concerning singletons ALWAYS returns
|
|
# these cached objects in order to
|
|
# implement proper object identity
|
|
# in a quicker way than it is done
|
|
# for equality
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Initializing singletons cache"
|
|
self.cached =
|
|
[
|
|
true.asBool().asObj(),
|
|
false.asBool().asObj(),
|
|
asNil().asObj(),
|
|
asInf().asObj(),
|
|
nil,
|
|
asNan().asObj()
|
|
]
|
|
# We cache -inf as well
|
|
let nInf = asInf()
|
|
nInf.isNegative = true
|
|
self.cached[4] = nInf.asObj()
|
|
|
|
|
|
proc initStdlib*(vm: var VM) =
|
|
## Initializes the VM's standard library by defining builtin
|
|
## functions that do not require imports. An arity of -1
|
|
## means that the function is variadic (or that it can
|
|
## take a different number of arguments according to
|
|
## how it's called) and should be handled by the nim
|
|
## procedure accordingly
|
|
when DEBUG_TRACE_VM and not SKIP_STDLIB_INIT or not DEBUG_TRACE_VM:
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Initializing stdlib"
|
|
vm.defineGlobal("print", newNative("print", natPrint, -1))
|
|
vm.defineGlobal("printErr", newNative("printErr", natPrintErr, -1))
|
|
vm.defineGlobal("clock", newNative("clock", natClock, 0))
|
|
vm.defineGlobal("round", newNative("round", natRound, -1))
|
|
vm.defineGlobal("toInt", newNative("toInt", natToInt, 1))
|
|
vm.defineGlobal("toString", newNative("toString", natToString, 1))
|
|
vm.defineGlobal("type", newNative("type", natType, 1))
|
|
vm.defineGlobal("readLine", newNative("readLine", natReadline, -1))
|
|
when DEBUG_TRACE_VM and SKIP_STDLIB_INIT:
|
|
echo "DEBUG - VM: Skipping stdlib initialization"
|
|
|
|
|
|
proc initVM*(): VM =
|
|
## Initializes the Virtual Machine by
|
|
## creating the cache, setting signal
|
|
## handlers, loading the standard
|
|
## library and preparing the stack
|
|
## and internal data structures
|
|
when DEBUG_TRACE_VM:
|
|
echo &"DEBUG - VM: Initializing the virtual machine, {JAPL_VERSION_STRING}"
|
|
result = VM(globals: newSimpleHashMap())
|
|
result.initStack()
|
|
result.initCache()
|
|
result.initStdlib()
|
|
setControlCHook(handleInterrupt)
|
|
result.lastPop = cast[ptr Nil](result.cached[2])
|
|
when DEBUG_TRACE_VM:
|
|
echo &"DEBUG - VM: Initialization complete, compiled with the following constants: FRAMES_MAX={FRAMES_MAX}, ARRAY_GROW_FACTOR={ARRAY_GROW_FACTOR}, MAP_LOAD_FACTOR={MAP_LOAD_FACTOR}"
|
|
|
|
|
|
proc interpret*(self: var VM, source: string, file: string): InterpretResult =
|
|
## Interprets a source string containing JAPL code
|
|
when DEBUG_TRACE_VM:
|
|
echo &"DEBUG - VM: Preparing to run '{file}'"
|
|
self.resetStack()
|
|
self.source = source.asStr()
|
|
self.file = file.asStr()
|
|
self.objects.append(self.source)
|
|
self.objects.append(self.file)
|
|
when DEBUG_TRACE_VM:
|
|
echo &"DEBUG - VM: Compiling '{file}'"
|
|
var compiler = initCompiler(SCRIPT, file=file)
|
|
var compiled = compiler.compile(source)
|
|
# Here we take into account that self.interpret() might
|
|
# get called multiple times (like in the REPL) and we don't wanna loose
|
|
# what we allocated before, so we merge everything we already
|
|
# allocated and everything the compiler allocated at compile time
|
|
self.objects.extend(compiler.objects)
|
|
if compiled == nil:
|
|
# Compile-time error
|
|
compiler.freeCompiler()
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Result -> CompileError"
|
|
return CompileError
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Compilation successful"
|
|
# Since in JAPL all code runs in some
|
|
# sort of function, we push our global
|
|
# "code object" and call it like any
|
|
# other function
|
|
self.push(compiled)
|
|
discard self.callObject(compiled, 0)
|
|
try:
|
|
result = self.run()
|
|
except KeyboardInterrupt: # TODO: Better handling
|
|
self.error(newInterruptedError(""))
|
|
when DEBUG_TRACE_VM:
|
|
echo "DEBUG - VM: Result -> RuntimeError"
|
|
return RuntimeError
|
|
when DEBUG_TRACE_VM:
|
|
echo &"DEBUG - VM: Result -> {result}"
|
|
|