hashtable implementation and test

This commit is contained in:
prod2 2022-01-28 22:00:21 +01:00
parent 8031c7720d
commit ded7fd3211
13 changed files with 233 additions and 13 deletions

2
.gitignore vendored
View File

@ -1,6 +1,4 @@
nds
*.txt
callgrind*
test.nim
test
.vscode

View File

@ -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
```

1
build.sh Executable file
View File

@ -0,0 +1 @@
nim c --gc:arc -d:danger --out:nds main

View File

@ -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

1
debug.sh Executable file
View File

@ -0,0 +1 @@
nim c --gc:arc -d:debug --out:nds main

View File

@ -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

View File

@ -1,3 +0,0 @@
gc:arc
d:danger
out:nds

53
tests/hashtable.nim Normal file
View File

@ -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"

120
types/hashtable.nim Normal file
View File

@ -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

View File

@ -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)
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

View File

@ -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

View File

@ -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

4
vm.nim
View File

@ -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