Separated string interning tests in compile-time vs runtime, fixed a bug in the build script and added some comments to the VM as well as a toStr function to obtain objects representations as strings

This commit is contained in:
nocturn9x 2021-01-14 22:37:11 +01:00
parent 3a55cbca38
commit 882efd28da
7 changed files with 128 additions and 70 deletions

View File

@ -170,48 +170,48 @@ def build(path: str, flags: Dict[str, str] = {}, options: Dict[str, bool] = {},
else:
logging.debug(f"Test suite ran in {time() - start:.2f} seconds")
logging.info("Test suite completed!")
if args.install:
if os.name == "nt":
logging.warning("Sorry, but automatically installing JAPL is not yet supported on windows")
else:
# TODO -> Is PATH defined on all linux distros?
logging.info(f"Installing JAPL at PATH")
if any(os.path.exists(os.path.join(path, "jpl")) for path in os.getenv("PATH").split(":")) and not ignore_binary:
logging.error("Could not install JAPL because a binary already exists in PATH")
return
install_path = os.path.join(os.getenv("PATH").split(":")[0], "jpl")
for path in os.getenv("PATH").split(":"):
install_path = os.path.join(path, "jpl")
logging.debug(f"Attempting to install JAPL at '{install_path}'")
try:
shutil.move(main_path.strip(".nim"), install_path)
except PermissionError:
logging.debug(f"Path '{path}' is not writable, attempting next entry in PATH")
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
else:
logging.debug(f"JAPL installed at '{path}', setting executable permissions")
# TODO: Use external oschmod library once we support windows!
try:
perms = os.stat(install_path)
os.chmod(install_path, perms.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
break
if args.install:
if os.name == "nt":
logging.warning("Sorry, but automatically installing JAPL is not yet supported on windows")
else:
# TODO -> Is PATH defined on all linux distros?
logging.info(f"Installing JAPL at PATH")
if any(os.path.exists(os.path.join(path, "jpl")) for path in os.getenv("PATH").split(":")) and not ignore_binary:
logging.error("Could not install JAPL because a binary already exists in PATH")
return
install_path = os.path.join(os.getenv("PATH").split(":")[0], "jpl")
for path in os.getenv("PATH").split(":"):
install_path = os.path.join(path, "jpl")
logging.debug(f"Attempting to install JAPL at '{install_path}'")
try:
shutil.move(main_path.strip(".nim"), install_path)
except PermissionError:
logging.debug(f"Path '{path}' is not writable, attempting next entry in PATH")
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
else:
logging.debug(f"JAPL installed at '{path}', setting executable permissions")
# TODO: Use external oschmod library once we support windows!
try:
perms = os.stat(install_path)
os.chmod(install_path, perms.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
except Exception as fatal:
logging.error(f"A fatal unhandled exception occurred -> {type(fatal).__name__}: {fatal}")
break
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("path", help="The path to JAPL's source directory")
parser.add_argument("--verbose", help="Prints debug information to stdout", action="store_true", default=os.environ.get("JAPL_VERBOSE"))
parser.add_argument("--flags", help="Optional flags to be passed to the nim compiler. Must be a comma-separated list of name:value (without spaces)", default=os.environ.get("JAPL_FLAGS"))
parser.add_argument("--verbose", help="Prints debug information to stdout", action="store_true", default=os.getenv("JAPL_VERBOSE"))
parser.add_argument("--flags", help="Optional flags to be passed to the nim compiler. Must be a comma-separated list of name:value (without spaces)", default=os.getenv("JAPL_FLAGS"))
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", default=os.environ.get("JAPL_OPTIONS"))
parser.add_argument("--override-config", help="Overrides the setting of an already existing config.nim file in the destination directory", action="store_true", default=os.environ.get("JAPL_OVERRIDE_CONFIG"))
parser.add_argument("--skip-tests", help="Skips running the JAPL test suite, useful for debug builds", action="store_true", default=os.environ.get("JAPL_SKIP_TESTS"))
parser.add_argument("--keep-results", help="Instructs the build tool not to delete the testresults.txt file from the test suite, useful for debugging", action="store_true", default=os.environ.get("JAPL_KEEP_RESULTS"))
"Note that if a config.nim file exists in the destination directory, that will override any setting defined here unless --override-config is used", default=os.getenv("JAPL_OPTIONS"))
parser.add_argument("--override-config", help="Overrides the setting of an already existing config.nim file in the destination directory", action="store_true", default=os.getenv("JAPL_OVERRIDE_CONFIG"))
parser.add_argument("--skip-tests", help="Skips running the JAPL test suite, useful for debug builds", action="store_true", default=os.getenv("JAPL_SKIP_TESTS"))
parser.add_argument("--keep-results", help="Instructs the build tool not to delete the testresults.txt file from the test suite, useful for debugging", action="store_true", default=os.getenv("JAPL_KEEP_RESULTS"))
parser.add_argument("--install", help="Tries to move the compiled binary to PATH (this is always disabled on windows)", action="store_true", default=os.environ.get("JAPL_INSTALL"))
parser.add_argument("--ignore-binary", help="Ignores an already existing 'jpl' binary in any installation directory and overwrites it, use (with care!) with --install", action="store_true", default=os.environ.get("JAPL_IGNORE_BINARY"))
parser.add_argument("--ignore-binary", help="Ignores an already existing 'jpl' binary in any installation directory and overwrites it, use (with care!) with --install", action="store_true", default=os.getenv("JAPL_IGNORE_BINARY"))
args = parser.parse_args()
flags = {
"gc": "markAndSweep", # Because refc is broken ¯\_(ツ)_/¯

View File

@ -103,4 +103,6 @@ proc natType*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
result = (kind: retNative.Object, result: args[0].typeName().asStr())
proc natToString*(args: seq[ptr Obj]): tuple[kind: retNative, result: ptr Obj] =
## Returns the string representation of an object
result = (kind: retNative.Object, result: args[0].stringify().asStr())

View File

@ -57,10 +57,11 @@ proc typeName*(self: ptr Function): string =
proc stringify*(self: ptr Function): string =
if self.name != nil and self.name.toStr() != "<lambda function>":
result = "<function '" & self.name.toStr() & "'>"
elif self.name.toStr() == "<lambda function>":
return self.name.toStr()
if self.name != nil:
if self.name.toStr() == "<lambda function>":
result = self.name.toStr()
else:
result = "<function '" & self.name.toStr() & "'>"
else:
result = "<code object>"

View File

@ -379,17 +379,23 @@ proc run(self: VM, repl: bool): InterpretResult =
self.showRuntime(frame, iteration)
case opcode: # Main OpCodes dispatcher
of OpCode.Constant:
# Loads a constant from the chunk's constant
# table
self.push(frame.readConstant())
of OpCode.ConstantLong:
# Loads a constant from the chunk's constant
# table when its size exceeds 256
self.push(frame.readLongConstant())
of OpCode.Negate:
# Performs unary negation
let operand = self.pop()
try:
self.push(operand.negate())
except NotImplementedError:
self.error(newTypeError(&"unsupported unary operator '-' for object of type '{operand.typeName()}'"))
return RuntimeError
of OpCode.Shl: # Bitwise left-shift
of OpCode.Shl:
# Bitwise left-shift
var right = self.pop()
var left = self.pop()
try:
@ -397,7 +403,8 @@ proc run(self: 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.Shr: # Bitwise right-shift
of OpCode.Shr:
# Bitwise right-shift
var right = self.pop()
var left = self.pop()
try:
@ -405,7 +412,8 @@ proc run(self: 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.Xor: # Bitwise xor
of OpCode.Xor:
# Bitwise xor
var right = self.pop()
var left = self.pop()
try:
@ -413,7 +421,8 @@ proc run(self: 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.Bor: # Bitwise or
of OpCode.Bor:
# Bitwise or
var right = self.pop()
var left = self.pop()
try:
@ -421,14 +430,16 @@ proc run(self: 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.Bnot: # Bitwise not
of OpCode.Bnot:
# Bitwise not
var operand = self.pop()
try:
self.push(operand.binaryNot())
except NotImplementedError:
self.error(newTypeError(&"unsupported unary operator '~' for object of type '{operand.typeName()}'"))
return RuntimeError
of OpCode.Band: # Bitwise and
of OpCode.Band:
# Bitwise and
var right = self.pop()
var left = self.pop()
try:
@ -437,6 +448,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -445,6 +457,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -453,6 +466,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -461,6 +475,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -469,6 +484,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -477,6 +493,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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:
@ -500,12 +517,14 @@ proc run(self: VM, repl: bool): InterpretResult =
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()
try:
@ -514,6 +533,7 @@ proc run(self: VM, repl: bool): InterpretResult =
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()
try:
@ -522,12 +542,14 @@ proc run(self: VM, repl: bool): InterpretResult =
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
var right = self.pop()
var left = self.pop()
self.push(self.getBoolean(left == right))
of OpCode.As:
# Implements type casting (TODO: Only allow classes)
var right = self.pop()
var left = self.pop()
try:
@ -536,13 +558,17 @@ proc run(self: VM, repl: bool): InterpretResult =
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
if not self.slice():
return RuntimeError
of OpCode.Slice:
# Implements expressions such as a[b:c]
# TODO: More generic method
if not self.sliceRange():
return RuntimeError
of OpCode.DefineGlobal:
# Defines a global variable
var name: string
if frame.function.chunk.consts.len > 255:
name = frame.readLongConstant().toStr()
@ -551,6 +577,7 @@ proc run(self: VM, repl: bool): InterpretResult =
self.globals[name] = self.peek(0)
discard self.pop()
of OpCode.GetGlobal:
# Retrieves a global variable
var constant: string
if frame.function.chunk.consts.len > 255:
constant = frame.readLongConstant().toStr()
@ -562,6 +589,7 @@ proc run(self: VM, repl: bool): InterpretResult =
else:
self.push(self.globals[constant])
of OpCode.SetGlobal:
# Changes the value of an already defined global variable
var constant: string
if frame.function.chunk.consts.len > 255:
constant = frame.readLongConstant().toStr()
@ -573,6 +601,7 @@ proc run(self: VM, repl: bool): InterpretResult =
else:
self.globals[constant] = self.peek(0)
of OpCode.DeleteGlobal:
# Deletes a global variable
# TODO: Inspect potential issues with the GC
var constant: string
if frame.function.chunk.consts.len > 255:
@ -585,6 +614,7 @@ proc run(self: VM, repl: bool): InterpretResult =
else:
self.globals.del(constant)
of OpCode.GetLocal:
# Retrieves a local variable
var slot: int
if frame.len > 255:
slot = frame.readBytes()
@ -592,6 +622,7 @@ proc run(self: VM, repl: bool): InterpretResult =
slot = int frame.readByte()
self.push(frame[slot])
of OpCode.SetLocal:
# Changes the value of an already defined local variable
var slot: int
if frame.len > 255:
slot = frame.readBytes()
@ -599,6 +630,7 @@ proc run(self: VM, repl: bool): InterpretResult =
slot = int frame.readByte()
frame[slot] = self.peek(0)
of OpCode.DeleteLocal:
# Deletes a global variable
# TODO: Inspect potential issues with the GC
var slot: int
if frame.len > 255:
@ -607,16 +639,26 @@ proc run(self: VM, repl: bool): InterpretResult =
slot = int frame.readByte()
frame.delete(slot)
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
@ -624,6 +666,8 @@ proc run(self: VM, repl: bool): InterpretResult =
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()
if repl and not self.lastPop.isNil() and self.frameCount == 1:
# TODO -> Make this more efficient (move into japl.nim?)
@ -743,16 +787,24 @@ proc stdlibInit*(vm: VM) =
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))
proc initVM*(): VM =
## Initializes the VM
setControlCHook(handleInterrupt)
var globals: Table[string, ptr Obj] = initTable[string, ptr Obj]()
result = VM(lastPop: asNil(), objects: @[], globals: globals, source: "", file: "")
## Initializes the Virtual Machine by
## creating the cache, setting signal
## handlers, loading the standard
## library and preparing the stack
## and internal data structures
result = VM(objects: @[], globals: initTable[string, ptr Obj](), source: "", file: "")
result.initCache()
result.stdlibInit()
result.resetStack()
setControlCHook(handleInterrupt)
result.lastPop = cast[ptr Nil](result.cached[2])
proc interpret*(self: VM, source: string, repl: bool = false, file: string): InterpretResult =
@ -763,13 +815,13 @@ proc interpret*(self: VM, source: string, repl: bool = false, file: string): Int
var compiler = initCompiler(SCRIPT, file=file)
var compiled = compiler.compile(source)
# Here we take into account that self.interpret() might
# get called multiple times and we don't wanna loose
# what we allocated before, so we merge everything we
# allocated + everything the compiler allocated at compile time
self.objects = self.objects & compiler.objects # TODO:
# revisit the best way to transfer marked objects from the compiler
# to the vm
# 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 = self.objects & compiler.objects
# TODO: revisit the best way to transfer marked objects from the compiler to the vm
if compiled == nil:
# Compile-time error
compiler.freeCompiler()
return CompileError
# Since in JAPL all code runs in some
@ -782,7 +834,7 @@ proc interpret*(self: VM, source: string, repl: bool = false, file: string): Int
echo "==== VM debugger starts ====\n"
try:
result = self.run(repl)
except KeyboardInterrupt:
except KeyboardInterrupt: # TODO: Better handling
self.error(newInterruptedError(""))
return RuntimeError
when DEBUG_TRACE_VM:

View File

@ -0,0 +1,13 @@
//compile time interning
var a = "hello";
var b = "hello";
print(a is b);//output:true
//different strings
var x = "ex";
var y = "ey";
print(x is y);//output:false

View File

@ -1,9 +1,3 @@
//compile time interning
var a = "hello";
var b = "hello";
print(a is b);//output:true
//runtime interning
var f = "leafy";

View File

@ -29,7 +29,7 @@ const testResultsPath = "testresults.txt"
# Exceptions for tests that represent not-yet implemented behaviour
const exceptions = ["all.jpl", "for_with_function.jpl"]
const exceptions = ["all.jpl", "for_with_function.jpl", "runtime_interning.jpl"]
# for_with_function.jpl probably contains an algorithmic error too
# TODO: fix that test
@ -40,8 +40,8 @@ type LogLevel {.pure.} = enum
Stdout, # always printed to stdout only (for cli experience)
const echoedLogs = { LogLevel.Info, LogLevel.Error, LogLevel.Stdout }
const savedLogs = { LogLevel.Debug, LogLevel.Info, LogLevel.Error }
const echoedLogs = {LogLevel.Info, LogLevel.Error, LogLevel.Stdout}
const savedLogs = {LogLevel.Debug, LogLevel.Info, LogLevel.Error}
proc compileExpectedOutput(path: string): string =
@ -75,7 +75,6 @@ proc deepComp(left, right: string, path: string): tuple[same: bool, place: int]
proc logWithLevel(level: LogLevel, file: File, msg: string) =
let msg = &"[{$level} - {$getTime()}] {msg}"
if level in savedLogs:
file.writeLine(msg)
if level in echoedLogs:
@ -86,8 +85,6 @@ proc logWithLevel(level: LogLevel, file: File, msg: string) =
setForegroundColor(fgDefault)
proc main(testsDir: string, japlExec: string, testResultsFile: File): tuple[numOfTests: int, successTests: int, failedTests: int, skippedTests: int] =
template detail(msg: string) =
logWithLevel(LogLevel.Debug, testResultsFile, msg)
@ -95,7 +92,6 @@ proc main(testsDir: string, japlExec: string, testResultsFile: File): tuple[numO
logWithLevel(LogLevel.Info, testResultsFile, msg)
template error(msg: string) =
logWithLevel(LogLevel.Error, testResultsFile, msg)
var numOfTests = 0
var successTests = 0
var failedTests = 0