diff --git a/.gitignore b/.gitignore index f8dff7d..a2bbda7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ nds *.txt callgrind* -test.nim -test .vscode \ No newline at end of file diff --git a/README.md b/README.md index 0f4b641..51bc0b3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,6 @@ The 4 steps to a REPL: ``` git clone https://github.com/prod2/nondescript cd nondescript -nim c main +./build.sh ./nds ``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..53c4640 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +nim c --gc:arc -d:danger --out:nds main \ No newline at end of file diff --git a/config.nim b/config.nim index fefc760..5cd0f1a 100644 --- a/config.nim +++ b/config.nim @@ -6,13 +6,17 @@ type # compiler debug options const debugScanner* = false const debugCompiler* = false -const debugDumpChunk* = false +const debugDumpChunk* = defined(debug) const assertionsCompiler* = true # sanity checks in the compiler # vm debug options (setting any to true will slow runtime down!) const debugVM* = false const assertionsVM* = false # sanity checks in the VM, such as the stack being empty at the end const profileInstructions* = false # if true, the time spent on every opcode is measured +const compileTests* = defined(debug) +# if true, the nim part of the test suite will be compiled in main +# it will be possible to start it using ./nds --test + # choose a line editor for the repl const lineEditor = leRdstdin diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..a3ca3be --- /dev/null +++ b/debug.sh @@ -0,0 +1 @@ +nim c --gc:arc -d:debug --out:nds main \ No newline at end of file diff --git a/main.nim b/main.nim index a082e3d..b698bdd 100644 --- a/main.nim +++ b/main.nim @@ -1,8 +1,13 @@ import vm import compiler -import os import config +import os +import strformat + +when compileTests: + import tests/hashtable + type Result = enum rsOK, rsCompileError, rsRuntimeError @@ -36,6 +41,21 @@ proc runFile(path: string) = of rsOK: quit 0 +proc runTests = + when compileTests: + testHashtables() + else: + echo "Nds was compiled without nim tests. Please change the flag and recompile, then tests will be available." + quit 1 + +proc printUsage = + echo """Usage: +- Start a repl: nds +- Run a file: nds path/to/script.nds +- Run the nim half of the test suite: nds --test +- Print this message: nds --help or nds -h +""" + const hardcodedPath* = "" if paramCount() == 0: @@ -44,7 +64,22 @@ if paramCount() == 0: else: runFile(hardcodedPath) elif paramCount() == 1: - runFile(paramStr(1)) + let arg = paramStr(1) + if arg[0] != '-': + runFile(paramStr(1)) + else: + case arg: + of "--test": + when defined(danger) or defined(release): + echo "WARNING: nds was compiled with -d:danger or -d:release, tests are only supported on -d:debug!" + runTests() + of "--help": + printUsage() + quit 0 + else: + echo &"Unsupported flag {arg}." + printUsage() + quit 1 else: echo "Maximum param count is 1" quit 1 diff --git a/nim.cfg b/nim.cfg deleted file mode 100644 index ff13d7a..0000000 --- a/nim.cfg +++ /dev/null @@ -1,3 +0,0 @@ -gc:arc -d:danger -out:nds \ No newline at end of file diff --git a/tests/hashtable.nim b/tests/hashtable.nim new file mode 100644 index 0000000..941fee5 --- /dev/null +++ b/tests/hashtable.nim @@ -0,0 +1,53 @@ +import ../types/hashtable + +import strformat + +proc hash*(str: string): int = + var hash = 2166136261'u32 + for i in countup(0, str.len - 1): + hash = hash xor (str[i]).uint32 + hash *= 16777619 + return hash.int + +proc testHashtables* = + var tbl = newTable[string, int]() + + var val: int + + assert tbl.tableSet("hello", 1) == false + assert tbl.tableGet("hello", val) == true + assert val == 1 + assert tbl.tableSet("hello", 4) == true + assert tbl.tableGet("hello", val) == true + assert val == 4 + assert tbl.tableGet("hellw", val) == false + assert val == 4 + assert tbl.tableDelete("hello") == true + val = 0 + assert tbl.tableGet("hello", val) == false + assert val == 0 + + for i in countup(0, 10000): + assert tbl.tableSet($i, i) == false + assert tbl.tableget($i, val) == true + assert val == i + assert tbl.tableSet($i, i * 2) == true + assert tbl.tableget($i, val) == true + assert val == i * 2 + assert tbl.tableSet($i, i * 4) == true + assert tbl.tableget($i, val) == true + assert val == i * 4 + if i mod 5 == 0: + assert tbl.tableDelete($i) == true + assert tbl.tableDelete($i) == false + + for i in countup(0, 10000): + if i mod 5 == 0: + assert tbl.tableGet($i, val) == false + else: + assert tbl.tableGet($i, val) == true + assert val == i * 4 + + tbl.free() + + echo "Hashtable test finished" diff --git a/types/hashtable.nim b/types/hashtable.nim new file mode 100644 index 0000000..47278bf --- /dev/null +++ b/types/hashtable.nim @@ -0,0 +1,120 @@ +# The hash table implementation for string interning + +import strformat + +const tableMaxLoad = 0.75 +const tableInitSize = 8 + +type + EntryStatus = enum + esNil, esAlive, esTombstone + + Entry*[U, V] = object + entryStatus: EntryStatus + key: U + value: V + + Table*[U, V] = object + count: int + cap: int + entries: ptr UncheckedArray[Entry[U, V]] + +proc newTable*[U, V]: Table[U, V] = + result.cap = 0 + result.count = 0 + +proc free*[U, V](tbl: var Table[U, V]) = + if tbl.entries != nil: + dealloc(tbl.entries) + +proc isNil[U, V](entry: ptr Entry[U, V]): bool {.inline.} = + entry[].entryStatus == esNil + +proc isTombstone[U, V](entry: ptr Entry[U, V]): bool {.inline.} = + entry[].entryStatus == esTombstone + +proc isAlive[U, V](entry: ptr Entry[U, V]): bool {.inline.} = + entry[].entryStatus == esAlive + +proc findEntry[U, V](entries: ptr UncheckedArray[Entry[U, V]], cap: int, key: U): ptr Entry[U, V] = + var index = key.hash() mod cap # TODO replace mod with sth better + var tombstone: ptr Entry[U, V] = nil + while true: + let entry: ptr Entry[U, V] = entries[index].addr # TODO: check the performance impact of this line + if entry.isNil(): + return if tombstone != nil: tombstone else: entry + elif entry.isTombstone(): # TODO: optimalization: case statement + if tombstone == nil: + tombstone = entry + elif entry[].key == key: + return entry + index = (index + 1) mod cap # TODO replace mod with sth better + +proc grow[U, V](tbl: var Table[U, V]): int {.inline.} = + ## Calculates the new capacity + if tbl.cap > 0: + tbl.cap * 2 + else: + tableInitSize + +proc adjustCapacity[U, V](tbl: var Table[U, V], newcap: int) = + let entries: ptr UncheckedArray[Entry[U, V]] = cast[ptr UncheckedArray[Entry[U, V]]](alloc0(newcap * sizeof(Entry[U, V]))) + tbl.count = 0 + + for i in countup(0, tbl.cap-1): + let entry = tbl.entries[i] + if entry.entryStatus == esAlive: + var dest = findEntry(entries, newcap, entry.key) + dest[].key = entry.key + dest[].value = entry.value + dest[].entryStatus = esAlive + tbl.count.inc + + if tbl.entries != nil: + dealloc(tbl.entries) + + tbl.entries = entries + tbl.cap = newcap + +proc tableSet*[U, V](tbl: var Table[U, V], key: U, val: V): bool = + ## Returns false if new value is entered + ## True if the value entered already existed and is overwritten + if tbl.count + 1 > int(tbl.cap.float * tableMaxLoad): + let cap = tbl.grow() + tbl.adjustCapacity(cap) + let entry: ptr Entry[U, V] = findEntry(tbl.entries, tbl.cap, key) + let status = entry[].entryStatus + if status == esNil: + tbl.count.inc + elif status == esAlive: + result = true + entry[].key = key + entry[].value = val + entry[].entryStatus = esAlive + +proc tableGet*[U, V](tbl: Table[U, V], key: U, val: var V): bool = + ## Returns false if not in table + ## Returns true if in the table, sets value to val + if tbl.count == 0: + return false + + let entry = findEntry(tbl.entries, tbl.cap, key) + if not entry.isAlive(): + return false + + val = entry[].value + return true + + + +proc tableDelete*[U, V](tbl: Table[U, V], key: U): bool = + if tbl.count == 0: + return false + + let entry = findEntry(tbl.entries, tbl.cap, key) + if not entry.isAlive(): + return false + + entry[].entryStatus = esTombstone + return true + diff --git a/types/ndstring.nim b/types/ndstring.nim index 2642b8c..952d193 100644 --- a/types/ndstring.nim +++ b/types/ndstring.nim @@ -16,4 +16,14 @@ proc `$`*(ndStr: NdString): string = proc `&`*(left, right: NdString): NdString = # TODO optimize this later when strings will be benchmarked - newString($left & $right) \ No newline at end of file + newString($left & $right) + +proc free*(ndStr: var NdString) = + dealloc(ndStr) + +proc hash*(ndStr: NdString): int = + var hash = 2166136261'u32 + for i in countup(0, ndStr.len.int - 1): + hash = hash xor (ndStr.chars[i]).uint32 + hash *= 16777619 + return hash.int \ No newline at end of file diff --git a/types/stack.nim b/types/stack.nim index 13cd51e..45d0287 100644 --- a/types/stack.nim +++ b/types/stack.nim @@ -18,7 +18,7 @@ proc newStack*[T](startingCap: int): Stack[T] = result.top = result.start.psub(sizeof(T)) result.cap = startingCap -proc destroyStack*[T](stack: var Stack[T]) = +proc free*[T](stack: var Stack[T]) = ## dealloc's the stack object ## if the stack contains pointers, those should be freed before destroying the stack stack.cap = 0 diff --git a/value.nim b/value.nim index 01f9611..cdf6d8b 100644 --- a/value.nim +++ b/value.nim @@ -57,6 +57,7 @@ proc equal*(val, right: NdValue): bool = of ntNil: true of ntString: + # TODO this was meant for nim strings, not ndStrings, FIXME! val.stringValue == right.stringValue of ntFunct: val.entryII == right.entryII diff --git a/vm.nim b/vm.nim index 384bf6c..1d5cf80 100644 --- a/vm.nim +++ b/vm.nim @@ -241,8 +241,8 @@ proc run*(chunk: Chunk): InterpretResult = let times = runcounts[op] echo &"OpCode: {op} total duration {dur} ms {times} times" - stack.destroyStack() - frames.destroyStack() + stack.free() + frames.free() if hadError: irRuntimeError