Fixed a minor bug when calling natives with variadic arguments. Improved tests/runtests.nim and integrated the test suite in the build script. Added type(), round() and toInt() builtins to JAPL as well as the identity operator (revision needed). Got rid of an unused parameter inside frame.nim and cleaned up the VM's code by no longer using var parameters for the VM. Exception objects are now added to the VM's objects stack and variables used for debugging purposes are now only declared/modified when the debug flags are turned on

This commit is contained in:
nocturn9x 2021-01-09 18:03:47 +01:00
parent 90d222a402
commit 87fa674b15
11 changed files with 298 additions and 137 deletions

View File

@ -109,7 +109,7 @@ git clone https://github.com/japl-lang/japl
## Running the build script
As a next step, you need to run the build script. This will generate the required configuration files,
compile the JAPL runtime and run tests (when we'll actually have some tests). There are some options that
compile the JAPL runtime and run tests. There are some options that
can be tweaked with command-line options, for more information, run `python3 build.py --help`.

View File

@ -23,7 +23,7 @@ import argparse
from time import time
from typing import Dict
from pprint import pformat
from subprocess import Popen, PIPE, DEVNULL
from subprocess import Popen, PIPE, DEVNULL, run
@ -75,7 +75,7 @@ Command-line options
"""'''
def build(path: str, flags: Dict[str, str] = {}, options: Dict[str, bool] = {}, override: bool = False):
def build(path: str, flags: Dict[str, str] = {}, options: Dict[str, bool] = {}, override: bool = False, skip_tests: bool = False):
"""
Compiles the JAPL runtime, generating the appropriate
configuration needed for compilation to succeed.
@ -108,17 +108,15 @@ def build(path: str, flags: Dict[str, str] = {}, options: Dict[str, bool] = {},
logging.debug(f"Generating config file at '{config_path}'")
try:
with open(config_path, "w") as build_config:
build_config.write(CONFIG_TEMPLATE.format(
**options
))
build_config.write(CONFIG_TEMPLATE.format(**options))
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
return
else:
logging.debug(f"Config file has been generated, compiling with options as follows: \n{pformat(options, indent=2)}")
logging.debug(f"Compiling '{main_path}'")
nim_flags = " ".join(f"-{name}:{value}" if len(name) == 1 else f"--{name}:{value}" for name, value in flags.items())
command = "nim {flags} compile {path}"
command = command.format(flags=nim_flags, path=main_path)
command = "nim {flags} compile {path}".format(flags=nim_flags, path=main_path)
logging.debug(f"Running '{command}'")
logging.info("Compiling JAPL")
start = time()
@ -132,6 +130,37 @@ def build(path: str, flags: Dict[str, str] = {}, options: Dict[str, bool] = {},
else:
logging.debug(f"Compilation completed in {time() - start:.2f} seconds")
logging.info("Build completed")
if skip_tests:
logging.warning("Skipping test suite")
else:
logging.info("Running tests under tests/")
logging.debug("Compiling test suite")
start = time()
tests_path = "./tests/runtests" if os.name != "nt" else ".\tests\runtests"
try:
process = Popen(shlex.split(f"nim compile {tests_path}", posix=os.name != "nt"), stdout=DEVNULL, stderr=PIPE)
_, stderr = process.communicate()
stderr = stderr.decode()
assert process.returncode == 0, f"Command '{command}' exited with non-0 exit code {process.returncode}, output below:\n{stderr}"
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
else:
logging.debug(f"Test suite compilation completed in {time() - start:.2f} seconds")
logging.debug("Running tests")
start = time()
try:
# TODO: Find a better way of running the test suite
process = run(f"{tests_path}", stdout=PIPE, stderr=PIPE, shell=True)
stderr = process.stderr.decode()
assert process.returncode == 0, f"Command '{command}' exited with non-0 exit code {process.returncode}, output below:\n{stderr}"
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
else:
logging.debug(f"Test suite ran in {time() - start:.2f} seconds")
# This way it *looks* like we're running it now when it
# actually already happened
print(process.stdout.decode().rstrip("\n"))
logging.info("Test suite completed!")
if __name__ == "__main__":
@ -142,6 +171,7 @@ if __name__ == "__main__":
parser.add_argument("--options", help="Set compile-time options and constants, pass a comma-separated list of name:value (without spaces). "
"Note that if a config.nim file exists in the destination directory, that will override any setting defined here unless --override-config is used")
parser.add_argument("--override-config", help="Overrides the setting of an already existing config.nim file in the destination directory", action="store_true")
parser.add_argument("--skip-tests", help="Skips running the JAPL test suite", action="store_true")
args = parser.parse_args()
flags = {
"gc": "markAndSweep",
@ -153,7 +183,7 @@ if __name__ == "__main__":
"debug_alloc": "false",
"map_load_factor": "0.75",
"array_grow_factor": "2",
"frames_max": "800"
"frames_max": "800",
}
level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(format="[%(levelname)s - %(asctime)s] %(message)s",
@ -179,5 +209,5 @@ if __name__ == "__main__":
except Exception:
logging.error("Invalid parameter for --options")
exit()
build(args.path, flags, options, args.override_config)
build(args.path, flags, options, args.override_config, args.skip_tests)
logging.debug("Build tool exited")

View File

@ -280,6 +280,8 @@ proc binary(self: Compiler, canAssign: bool) =
self.emitByte(OpCode.Bor)
of TokenType.BAND:
self.emitByte(OpCode.Band)
of TokenType.IS:
self.emitByte(OpCode.Is)
else:
discard # Unreachable
@ -856,7 +858,7 @@ proc parseBreak(self: Compiler) =
if not self.loop.alive:
self.parser.parseError(self.parser.previous, "'break' outside loop")
else:
self.parser.consume(TokenType.SEMICOLON, "missing semicolon after statement")
self.parser.consume(TokenType.SEMICOLON, "missing semicolon after break statement")
var i = self.localCount - 1
while i >= 0 and self.locals[i].depth > self.loop.depth:
self.emitByte(OpCode.Pop)
@ -889,7 +891,7 @@ proc continueStatement(self: Compiler) =
if not self.loop.alive:
self.parser.parseError(self.parser.previous, "'continue' outside loop")
else:
self.parser.consume(TokenType.SEMICOLON, "missing semicolon after statement")
self.parser.consume(TokenType.SEMICOLON, "missing semicolon after continue statement")
var i = self.localCount - 1
while i >= 0 and self.locals[i].depth > self.loop.depth:
self.emitByte(OpCode.Pop)
@ -924,7 +926,7 @@ proc parseFunction(self: Compiler, funType: FunctionType) =
while true:
self.function.arity += 1
if self.function.arity + self.function.optionals > 255:
self.compileError("cannot have more than 255 arguments")
self.compileError("functions cannot have more than 255 arguments")
break
var paramIdx = self.parseVariable("expecting parameter name")
if self.parser.hadError:
@ -976,7 +978,7 @@ proc argumentList(self: Compiler): uint8 =
while true:
self.expression()
if result == 255:
self.compileError("cannot have more than 255 arguments")
self.compileError("cannot pass more than 255 arguments")
return
result += 1
if not self.parser.match(COMMA):
@ -986,9 +988,8 @@ proc argumentList(self: Compiler): uint8 =
proc call(self: Compiler, canAssign: bool) =
## Emits appropriate bytecode to call
## a function
var argCount = self.argumentList()
self.emitBytes(OpCode.Call, argCount)
## a function with its arguments
self.emitBytes(OpCode.Call, self.argumentList())
proc returnStatement(self: Compiler) =
@ -1041,6 +1042,7 @@ proc declaration(self: Compiler) =
if self.parser.panicMode:
self.synchronize()
proc freeObject(self: Compiler, obj: ptr Obj) =
## Frees the associated memory
## of an object
@ -1072,12 +1074,14 @@ proc freeObject(self: Compiler, obj: ptr Obj) =
proc freeCompiler*(self: Compiler) =
## Frees all the allocated objects
## from the compiler
var objCount = len(self.objects)
var objFreed = 0
when DEBUG_TRACE_ALLOCATION:
var objCount = len(self.objects)
var objFreed = 0
for obj in reversed(self.objects):
self.freeObject(obj)
discard self.objects.pop()
objFreed += 1
when DEBUG_TRACE_ALLOCATION:
objFreed += 1
when DEBUG_TRACE_ALLOCATION:
echo &"DEBUG - Compiler: Freed {objFreed} objects out of {objCount} compile-time objects"
@ -1109,7 +1113,7 @@ var rules: array[TokenType, ParseRule] = [
makeRule(nil, nil, Precedence.None), # RS
makeRule(number, nil, Precedence.None), # NUMBER
makeRule(strVal, nil, Precedence.None), # STR
makeRule(nil, nil, Precedence.None), # semicolon
makeRule(nil, nil, Precedence.None), # SEMI
makeRule(nil, parseAnd, Precedence.And), # AND
makeRule(nil, nil, Precedence.None), # CLASS
makeRule(nil, nil, Precedence.None), # ELSE
@ -1128,7 +1132,7 @@ var rules: array[TokenType, ParseRule] = [
makeRule(nil, nil, Precedence.None), # DEL
makeRule(nil, nil, Precedence.None), # BREAK
makeRule(nil, nil, Precedence.None), # EOF
makeRule(nil, nil, Precedence.None), # TokenType.COLON
makeRule(nil, nil, Precedence.None), # COLON
makeRule(nil, nil, Precedence.None), # CONTINUE
makeRule(nil, binary, Precedence.Term), # CARET
makeRule(nil, binary, Precedence.Term), # SHL
@ -1138,6 +1142,7 @@ var rules: array[TokenType, ParseRule] = [
makeRule(nil, binary, Precedence.Term), # BAND
makeRule(nil, binary, Precedence.Term), # BOR
makeRule(unary, nil, Precedence.None), # TILDE
makeRule(nil, binary, Precedence.Term) # IS
]

View File

@ -56,7 +56,8 @@ const RESERVED = to_table({
"this": TokenType.THIS, "super": TokenType.SUPER,
"del": TokenType.DEL, "break": TokenType.BREAK,
"continue": TokenType.CONTINUE, "inf": TokenType.INF,
"nan": TokenType.NAN})
"nan": TokenType.NAN,
"is": TokenType.IS})
type
Lexer* = ref object
source*: string

View File

@ -35,6 +35,7 @@ proc clear*(self: CallFrame): int =
discard self.stack.pop()
inc result
proc getView*(self: CallFrame): seq[ptr Obj] =
result = self.stack[self.slot..self.stack.high()]
@ -43,17 +44,17 @@ proc len*(self: CallFrame): int =
result = len(self.getView())
proc `[]`*(self: CallFrame, idx: int, offset: int): ptr Obj =
proc `[]`*(self: CallFrame, idx: int): ptr Obj =
result = self.stack[idx + self.slot]
proc `[]=`*(self: CallFrame, idx: int, offset: int, val: ptr Obj) =
proc `[]=`*(self: CallFrame, idx: int, val: ptr Obj) =
if idx < self.slot:
raise newException(IndexError, "CallFrame index out of range")
raise newException(IndexDefect, "CallFrame index out of range")
self.stack[idx + self.slot] = val
proc delete*(self: CallFrame, idx: int, offset: int) =
proc delete*(self: CallFrame, idx: int) =
if idx < self.slot:
raise newException(IndexError, "CallFrame index out of range")
raise newException(IndexDefect, "CallFrame index out of range")
self.stack.delete(idx + self.slot)

View File

@ -68,7 +68,8 @@ type
Call,
Bor,
Band,
Bnot
Bnot,
Is
@ -79,7 +80,8 @@ const simpleInstructions* = {OpCode.Return, OpCode.Add, OpCode.Multiply,
OpCode.Inf, OpCode.Shl, OpCode.Shr,
OpCode.Xor, OpCode.Not, OpCode.Equal,
OpCode.Greater, OpCode.Less, OpCode.GetItem,
OpCode.Slice, OpCode.Pop, OpCode.Negate}
OpCode.Slice, OpCode.Pop, OpCode.Negate,
OpCode.Is}
const constantInstructions* = {OpCode.Constant, OpCode.DefineGlobal,
OpCode.GetGlobal, OpCode.SetGlobal,
OpCode.DeleteGlobal}

View File

@ -29,7 +29,7 @@ type
WHILE, DEL, BREAK, EOF,
COLON, CONTINUE, CARET,
SHL, SHR, NAN, INF, BAND,
BOR, TILDE
BOR, TILDE, IS
Token* = ref object
kind*: TokenType
lexeme*: string

View File

@ -20,8 +20,12 @@ import types/baseObject
import types/japlNil
import types/numbers
import types/methods
import times
import types/japlString
import types/exception
import times
import math
import strformat
proc natPrint(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
@ -38,7 +42,10 @@ proc natPrint(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
else:
res = res & arg.stringify()
echo res
return (ok: true, result: asNil())
# Note: we return nil and not asNil() because
# the VM will later use its own cached pointer
# to nil
return (ok: true, result: nil)
proc natClock(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
@ -52,6 +59,63 @@ proc natClock(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
result = (ok: true, result: getTime().toUnixFloat().asFloat())
template stdlibInit*(vm: VM) =
proc natRound(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
## Rounds a floating point number to a given
## precision (when precision == 0, this function drops the
## decimal part and returns an integer). Note that when
## precision > 0 and the value of the dropped digits
## exceeds or equals 5, the closest decimal place is
## increased by 1 (i.e. round(3.141519, 3) == 3.142)
var precision = 0
if len(args) notin 1..2:
# Here we need to return immediately to exit the procedure
return (ok: false, result: newTypeError(&"function 'round' takes from 1 to 2 arguments, got {len(args)}"))
elif len(args) == 2:
if not args[1].isInt():
return (ok: false, result: newTypeError(&"precision must be of type 'int', not '{args[1].typeName()}'"))
else:
precision = args[1].toInt()
if args[0].kind notin {ObjectType.Integer, ObjectType.Float}:
return (ok: false, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
if precision < 0:
result = (ok: false, result: newTypeError(&"precision must be positive"))
else:
if args[0].isInt():
result = (ok: true, result: args[0])
elif precision == 0:
result = (ok: true, result: int(args[0].toFloat()).asInt())
else:
result = (ok: true, result: round(args[0].toFloat(), precision).asFloat())
proc natToInt(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
## Drops the decimal part of a float and returns an integer.
## If the value is already an integer, the same object is returned
if args[0].isInt():
result = (ok: true, result: args[0])
elif args[0].isFloat():
result = (ok: true, result: int(args[0].toFloat()).asInt())
else:
result = (ok: false, result: newTypeError(&"input must be of type 'int' or 'float', not '{args[0].typeName()}'"))
proc natType(args: seq[ptr Obj]): tuple[ok: bool, result: ptr Obj] =
## Returns the type of a given object as a string
result = (ok: true, result: args[0].typeName().asStr())
proc stdlibInit*(vm: 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
vm.defineGlobal("print", newNative("print", natPrint, -1))
vm.defineGlobal("clock", newNative("clock", natClock, 0))
vm.defineGlobal("round", newNative("round", natRound, -1))
vm.defineGlobal("toInt", newNative("toInt", natToInt, 1))
vm.defineGlobal("type", newNative("type", natType, 1))

View File

@ -18,6 +18,7 @@
import ../meta/opcode
import ../types/baseObject
import ../types/methods
import strformat

View File

@ -16,12 +16,15 @@
## This is the entire runtime environment for JAPL
{.experimental: "implicitDeref".}
## Standard library imports
import algorithm
import strformat
import tables
import std/enumerate
## Our modules
import memory
import config
import compiler
import tables
import meta/opcode
import meta/frame
import types/baseObject
@ -33,8 +36,6 @@ import types/boolean
import types/methods
import types/function
import types/native
import memory
import tables
when DEBUG_TRACE_VM:
import util/debug
@ -69,7 +70,7 @@ func handleInterrupt() {.noconv.} =
raise newException(KeyboardInterrupt, "Ctrl+C")
proc resetStack*(self: var VM) =
proc resetStack*(self: VM) =
## Resets the VM stack to a blank state
self.stack = new(seq[ptr Obj])
self.frames = @[]
@ -77,7 +78,7 @@ proc resetStack*(self: var VM) =
self.stackTop = 0
proc getBoolean(self: var VM, kind: bool): ptr Obj =
proc getBoolean(self: VM, kind: bool): ptr Obj =
## Tiny little optimization for booleans
## which are pre-allocated on startup
if kind:
@ -86,11 +87,20 @@ proc getBoolean(self: var VM, kind: bool): ptr Obj =
return self.cached[1]
proc error*(self: var VM, error: ptr JAPLException) =
proc error*(self: VM, error: ptr JAPLException) =
## Reports runtime errors with a nice traceback
# TODO: Exceptions
# 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.add(error)
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
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("Traceback (most recent call last):\n")
@ -117,13 +127,13 @@ proc error*(self: var VM, error: ptr JAPLException) =
self.resetStack()
proc pop*(self: var VM): ptr Obj =
proc pop*(self: VM): ptr Obj =
## Pops an object off the stack
result = self.stack.pop()
self.stackTop -= 1
proc push*(self: var VM, obj: ptr Obj) =
proc push*(self: VM, obj: ptr Obj) =
## Pushes an object onto the stack
self.stack.add(obj)
if obj notin self.objects and obj notin self.cached:
@ -131,23 +141,14 @@ proc push*(self: var VM, obj: ptr Obj) =
self.stackTop += 1
proc peek*(self: var VM, distance: int): ptr Obj =
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.stackTop - distance - 1]
template addObject*(self: var VM, obj: ptr Obj): untyped =
## Stores an object in the VM's internal
## list of objects in order to reclaim
## its memory later
let temp = obj
self.objects.add(temp)
temp
# TODO: Move this to jobject.nim
proc slice(self: var VM): bool =
# TODO: Move this to types/
proc slice(self: VM): bool =
## Handles single-operator slice expressions
## (consider moving this to an appropriate
## slice method)
@ -177,7 +178,7 @@ proc slice(self: var VM): bool =
return false
# TODO: Move this to types/
proc sliceRange(self: var VM): bool =
proc sliceRange(self: VM): bool =
## Handles slices when there's both a start
## and an end index (even implicit ones)
var sliceEnd = self.pop()
@ -215,7 +216,7 @@ proc sliceRange(self: var VM): bool =
return false
proc call(self: var VM, function: ptr Function, argCount: int): bool =
proc call(self: VM, function: ptr Function, argCount: int): bool =
## Sets up the call frame and performs error checking
## when calling callables
if argCount != function.arity:
@ -232,7 +233,8 @@ proc call(self: var VM, function: ptr Function, argCount: int): bool =
return true
proc call(self: var VM, native: ptr Native, argCount: int): bool =
proc call(self: 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
@ -243,16 +245,23 @@ proc call(self: var VM, native: ptr Native, argCount: int): bool =
let nativeResult = native.nimproc(args)
if not nativeResult.ok:
self.error(cast[ptr JaplException](nativeResult.result))
return false
# assumes that all native procs behave well, and if not ok, they
# only return japl exceptions
for i in countup(slot - 1, self.stack.high()):
discard self.pop() # TODO once stack is a custom datatype,
# just reduce its length
self.push(nativeResult.result)
if nativeResult.result == nil:
# Since nil is a singleton, natives
# don't reallocate it and we need to
# reuse our cached object instead
self.push(self.cached[2])
else:
self.push(nativeResult.result)
return true
proc callObject(self: var VM, callee: ptr Obj, argCount: uint8): bool =
proc callObject(self: VM, callee: ptr Obj, argCount: uint8): bool =
## Wrapper around call() to do type checking
if callee.isCallable():
case callee.kind:
@ -266,9 +275,12 @@ proc callObject(self: var VM, callee: ptr Obj, argCount: uint8): bool =
self.error(newTypeError(&"object of type '{callee.typeName()}' is not callable"))
return false
proc defineGlobal*(self: VM, name: string, value: ptr Obj) =
## Adds a key-value couple to the VM's global scope
self.globals[name] = value
proc readByte(self: CallFrame): uint8 =
## Reads a single byte from the given
## frame's chunk of bytecode
@ -305,14 +317,13 @@ proc readLongConstant(self: CallFrame): ptr Obj =
result = self.function.chunk.consts[idx]
proc run(self: var VM, repl: bool): InterpretResult =
proc run(self: VM, repl: bool): InterpretResult =
## Chews trough bytecode instructions executing
## them one at a time: this is the runtime's
## main loop
var frame = self.frames[self.frameCount - 1]
var instruction: uint8
var opcode: OpCode
var stackOffset: int = 2
while true:
{.computedgoto.} # See https://nim-lang.org/docs/manual.html#pragmas-computedgoto-pragma
instruction = frame.readByte()
@ -320,10 +331,6 @@ proc run(self: var VM, repl: bool): InterpretResult =
## This offset dictates how the call frame behaves when converting
## relative frame indexes to absolute stack indexes, since the behavior
## in function local vs. global/scope-local scope is different
if frame.function.name == nil:
stackOffset = 2
else:
stackOffset = 1
when DEBUG_TRACE_VM: # Insight inside the VM
stdout.write("Current VM stack status: [")
for v in self.stack:
@ -331,23 +338,26 @@ proc run(self: var VM, repl: bool): InterpretResult =
stdout.write(", ")
stdout.write("]\n")
stdout.write("Current global scope status: {")
for k, v in self.globals.pairs():
for i, (k, v) in enumerate(self.globals.pairs()):
stdout.write(k)
stdout.write(": ")
stdout.write(stringify(v))
stdout.write(", ")
if i < self.globals.len() - 1:
stdout.write(", ")
stdout.write("}\n")
stdout.write("Current frame type:")
stdout.write("Current frame type: ")
if frame.function.name == nil:
stdout.write(" main\n")
stdout.write("main\n")
else:
stdout.write(&" function, '{frame.function.name.stringify()}'\n")
echo "Current frame count: {self.frameCount}"
stdout.write(&"function, '{frame.function.name.stringify()}'\n")
echo &"Current frame count: {self.frameCount}"
echo &"Current frame length: {frame.len}"
stdout.write("Current frame stack status: ")
stdout.write("[")
for e in frame.getView():
for i, e in frame.getView():
stdout.write(stringify(e))
stdout.write(", ")
if i < len(frame) - 1:
stdout.write(", ")
stdout.write("]\n")
discard disassembleInstruction(frame.function.chunk, frame.ip - 1)
case opcode: # Main OpCodes dispatcher
@ -494,6 +504,12 @@ proc run(self: var VM, repl: bool): InterpretResult =
except NotImplementedError:
self.error(newTypeError(&"unsupported binary operator '>' for objects of type '{left.typeName()}' and '{right.typeName()}'"))
return RuntimeError
of OpCode.Is:
# This is implemented internally for obvious
# reasons and works on any pair of objects
var right = self.pop()
var left = self.pop()
self.push(self.getBoolean(addr(left) == addr(right)))
of OpCode.GetItem:
# TODO: More generic method
if not self.slice():
@ -555,22 +571,22 @@ proc run(self: var VM, repl: bool): InterpretResult =
self.globals.del(constant)
of OpCode.GetLocal:
if frame.len > 255:
self.push(frame[frame.readBytes(), stackOffset])
self.push(frame[frame.readBytes()])
else:
self.push(frame[int frame.readByte(), stackOffset])
self.push(frame[int frame.readByte()])
of OpCode.SetLocal:
if frame.len > 255:
frame[frame.readBytes(), stackOffset] = self.peek(0)
frame[frame.readBytes()] = self.peek(0)
else:
frame[int frame.readByte(), stackOffset] = self.peek(0)
frame[int frame.readByte()] = self.peek(0)
of OpCode.DeleteLocal:
# TODO: Inspect potential issues with the GC
if frame.len > 255:
var slot = frame.readBytes()
frame.delete(slot, stackOffset)
frame.delete(slot)
else:
var slot = frame.readByte()
frame.delete(int slot, stackOffset)
frame.delete(int slot)
of OpCode.Pop:
self.lastPop = self.pop()
of OpCode.JumpIfFalse:
@ -591,7 +607,9 @@ proc run(self: var VM, repl: bool): InterpretResult =
of OpCode.Return:
var retResult = self.pop()
if repl and not self.lastPop.isNil() and self.frameCount == 1:
# This avoids unwanted output with recursive calls
# Prints the last expression to stdout as long as we're
# in REPL mode, the expression isn't nil and we're at the
# top-level code
echo stringify(self.lastPop)
self.lastPop = cast[ptr Nil](self.cached[2])
self.frameCount -= 1
@ -635,30 +653,33 @@ proc freeObject(self: VM, obj: ptr Obj) =
discard free(ObjectType.Function, fun)
proc freeObjects(self: var VM) =
proc freeObjects(self: VM) =
## Frees all the allocated objects
## from the VM
var runtimeObjCount = len(self.objects)
var cacheCount = len(self.cached)
var runtimeFreed = 0
var cachedFreed = 0
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):
self.freeObject(obj)
discard self.objects.pop()
runtimeFreed += 1
when DEBUG_TRACE_ALLOCATION:
runtimeFreed += 1
for cached_obj in self.cached:
self.freeObject(cached_obj)
cachedFreed += 1
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: var VM) =
proc freeVM*(self: VM) =
## Tears down the VM
unsetControlCHook()
try:
self.freeObjects()
except NilAccessError:
except NilAccessDefect:
stderr.write("A fatal error occurred -> could not free memory, segmentation fault\n")
quit(71)
when DEBUG_TRACE_ALLOCATION:
@ -666,9 +687,16 @@ proc freeVM*(self: var VM) =
echo &"DEBUG - VM: Warning, {self.objects.len} objects were not freed"
proc initCache(self: var VM) =
proc initCache(self: VM) =
## Initializes the static cache for singletons
## such as nil, true, false and nan
## 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
self.cached =
[
true.asBool().asObj(),
@ -687,7 +715,7 @@ proc initVM*(): VM =
result.initCache()
proc interpret*(self: var VM, source: string, repl: bool = false, file: string): InterpretResult =
proc interpret*(self: VM, source: string, repl: bool = false, file: string): InterpretResult =
## Interprets a source string containing JAPL code
self.resetStack()
self.source = source
@ -704,6 +732,10 @@ proc interpret*(self: var VM, source: string, repl: bool = false, file: string):
if compiled == nil:
compiler.freeCompiler()
return CompileError
# 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)
when DEBUG_TRACE_VM:

View File

@ -25,6 +25,10 @@
import multibyte, os, strformat, times, re
# Exceptions for tests that represent not-yet implemented behaviour
const exceptions = ["all.jpl"]
proc compileExpectedOutput(path: string): string =
for line in path.lines():
if line =~ re"^.*//output:(.*)$":
@ -38,8 +42,8 @@ proc deepComp(left, right: string): tuple[same: bool, place: int] =
for i in countup(0, left.high()):
result.place = i
if i > right.high():
# already false bc of the len check at the beginning
# already correct place bc it's updated every i
# already false because of the len check at the beginning
# already correct place because it's updated every i
return
if left[i] != right[i]:
result.same = false
@ -48,68 +52,86 @@ proc deepComp(left, right: string): tuple[same: bool, place: int] =
# Quick logging levels using procs
proc log(file: File, msg: string) =
file.writeLine(&"[LOG] {msg}")
echo msg
proc log(file: File, msg: string, toFile: bool = true) =
## Logs to stdout and to the log file unless
## toFile == false
if toFile:
file.writeLine(&"[LOG - {$getTime()}] {msg}")
echo &"[LOG - {$getTime()}] {msg}"
proc detail(file: File, msg: string) =
file.writeLine(&"[DETAIL] {msg}")
## Logs only to the log file
file.writeLine(&"[DETAIL - {$getTime()}] {msg}")
const exceptions = ["all.jpl"]
proc main(testsDir: string, japlExec: string, testResultsFile: File) =
proc main(testsDir: string, japlExec: string, testResultsFile: File): tuple[numOfTests: int, successTests: int, failedTests: int, skippedTests: int] =
var numOfTests = 0
var successTests = 0
var failedTests = 0
var skippedTests = 0
try:
testResultsFile.writeLine(&"Executing tests at {$getTime()}")
# Exceptions for tests that represent not-yet implemented behaviour
for file in walkDir(testsDir):
block singleTest:
for exc in exceptions:
if exc == file.path.extractFilename:
log(testResultsFile, &"Skipping {file.path} because it's on the exceptions list")
detail(testResultsFile, &"Skipping '{file.path}'")
numOfTests += 1
skippedTests += 1
break singleTest
if file.path.dirExists():
log(testResultsFile, "Descending into " & file.path)
main(file.path, japlExec, testResultsFile)
detail(testResultsFile, "Descending into '" & file.path & "'")
var subTestResult = main(file.path, japlExec, testResultsFile)
numOfTests += subTestResult.numOfTests
successTests += subTestResult.successTests
failedTests += subTestResult.failedTests
skippedTests += subTestResult.skippedTests
break singleTest
log(testResultsFile, &"Running test {file.path}")
detail(testResultsFile, &"Running test '{file.path}'")
if fileExists("testoutput.txt"):
removeFile("testoutput.txt") # in case this crashed
discard execShellCmd(&"{japlExec} {file.path} >>testoutput.txt")
let expectedOutput = compileExpectedOutput(file.path).replace(re"(\n*)$", "")
let realOutputFile = open("testoutput.txt", fmRead)
let realOutput = realOutputFile.readAll().replace(re"([\n\r]*)$", "")
realOutputFile.close()
removeFile("testoutput.txt")
let comparison = deepComp(expectedOutput, realOutput)
if comparison.same:
log(testResultsFile, &"Successful test {file.path}")
let retCode = execShellCmd(&"{japlExec} {file.path} >> testoutput.txt")
numOfTests += 1
if retCode != 0:
failedTests += 1
log(testResultsFile, &"Test '{file.path}' has crashed!")
else:
detail(testResultsFile, &"Expected output:\n{expectedOutput}\n")
detail(testResultsFile, &"Received output:\n{realOutput}\n")
detail(testResultsFile, &"Mismatch at pos {comparison.place}")
if comparison.place > expectedOutput.high() or
comparison.place > realOutput.high():
detail(testResultsFile, &"Length mismatch")
successTests += 1
let expectedOutput = compileExpectedOutput(file.path).replace(re"(\n*)$", "")
let realOutputFile = open("testoutput.txt", fmRead)
let realOutput = realOutputFile.readAll().replace(re"([\n\r]*)$", "")
realOutputFile.close()
removeFile("testoutput.txt")
let comparison = deepComp(expectedOutput, realOutput)
if comparison.same:
log(testResultsFile, &"Test '{file.path}' was successful")
else:
detail(testResultsFile, &"Expected is '{expectedOutput[comparison.place]}' while received '{realOutput[comparison.place]}'")
log(testResultsFile, &"Test failed {file.path}, check 'testresults.txt' for details")
detail(testResultsFile, &"Expected output:\n{expectedOutput}\n")
detail(testResultsFile, &"Received output:\n{realOutput}\n")
detail(testResultsFile, &"Mismatch at pos {comparison.place}")
if comparison.place > expectedOutput.high() or
comparison.place > realOutput.high():
detail(testResultsFile, &"Length mismatch")
else:
detail(testResultsFile, &"Expected is '{expectedOutput[comparison.place]}' while received '{realOutput[comparison.place]}'")
log(testResultsFile, &"Test '{file.path}' failed")
result = (numOfTests: numOfTests, successTests: successTests, failedTests: failedTests, skippedTests: skippedTests)
except IOError:
stderr.write(&"Fatal IO error encountered while running tesrs -> {getCurrentExceptionMsg()}")
stderr.write(&"Fatal IO error encountered while running tests -> {getCurrentExceptionMsg()}")
when isMainModule:
let testResultsFile = open("testresults.txt", fmAppend)
testResultsFile.writeLine(&"Executing tests at {$getTime()}")
# nim tests
testMultibyte()
# japl tests
let testResultsFile = open("testresults.txt", fmWrite)
log(testResultsFile, "Running Nim tests")
# Nim tests
detail(testResultsFile, "Running testMultiByte")
testMultiByte()
# JAPL tests
log(testResultsFile, "Running JAPL tests")
var testsDir = "tests" / "japl"
var japlExec = "src" / "japl"
var currentDir = getCurrentDir()
# support running from both the japl root and the tests dir where it
# resides
# Supports running from both the project root and the tests dir itself
if currentDir.lastPathPart() == "tests":
testsDir = "japl"
japlExec = ".." / japlExec
@ -121,5 +143,8 @@ when isMainModule:
if not dirExists(testsDir):
log(testResultsFile, "Tests dir not found")
quit(1)
main(testsDir, japlExec, testResultsFile)
let testResult = main(testsDir, japlExec, testResultsFile)
log(testResultsFile, &"Found {testResult.numOfTests} tests: {testResult.successTests} were successful, {testResult.failedTests} failed and {testResult.skippedTests} were skipped.")
log(testResultsFile, "Check 'testresults.txt' for details", toFile=false)
testResultsfile.close()