mirror of https://github.com/japl-lang/japl.git
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:
parent
90d222a402
commit
87fa674b15
|
@ -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`.
|
||||
|
||||
|
||||
|
|
48
build.py
48
build.py
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import ../meta/opcode
|
||||
import ../types/baseObject
|
||||
import ../types/methods
|
||||
|
||||
import strformat
|
||||
|
||||
|
||||
|
|
154
src/vm.nim
154
src/vm.nim
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue