Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unfindable entry in sparsemap #93

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
strategy:
matrix:
nim-version: [ 2.0.8 ]
build-flags: [ noflag, memProfiler, memtrace, memrecord ]
steps:

- uses: actions/checkout@v3
Expand All @@ -63,6 +64,9 @@ jobs:
pulseaudio -D --exit-idle-time=-1
pactl load-module module-null-sink sink_name=SpeakerOutput sink_properties=device.description="Dummy_Output"

- run: echo 'switch("d", "${{ matrix.build-flags }}")' >> configs.nims
working-directory: ./tests

- run: nimble simulator
working-directory: ./tests

Expand Down
14 changes: 6 additions & 8 deletions src/playdate/bindings/memtrace.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import system/ansi_c, sparsemap
import system/ansi_c, ../util/sparsemap

proc mprotect(a1: pointer, a2: int, a3: cint): cint {.importc, header: "<sys/mman.h>".}

Expand Down Expand Up @@ -269,21 +269,19 @@ proc realloc*(trace: var MemTrace, alloc: Allocator, p: pointer, newSize: Natura
proc traceDealloc(trace: var MemTrace, alloc: Allocator, p: pointer) {.inline.} =
trace.check
let realPointer = p.input
let entry = trace.allocs[realPointer.ord]
if entry == nil:
if realPointer.ord notin trace.allocs:
cfprintf(cstderr, "Attempting to dealloc unmanaged memory! %p\n", p)
createStackFrame[STACK_SIZE](getFrame()).printStack()
let deleted = trace.deleted[realPointer.ord]
if deleted == nil:
if realPointer.ord notin trace.deleted:
trace.printPrior(p)
else:
deleted[].print("Previously deallocated", printMem = false)
trace.deleted[realPointer.ord].print("Previously deallocated", printMem = false)
return
else:
var local = entry[]
var local = trace.allocs[realPointer.ord]
local.stack = createStackFrame[STACK_SIZE](getFrame())

unprotect(realPointer, entry.realSize)
unprotect(realPointer, local.realSize)
discard alloc(realPointer, 0)
trace.deleted[realPointer.ord] = local
trace.allocs.delete(realPointer.ord)
Expand Down
69 changes: 0 additions & 69 deletions src/playdate/bindings/sparsemap.nim

This file was deleted.

113 changes: 113 additions & 0 deletions src/playdate/util/sparsemap.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
##
## A very basic sparse hashmap implementation. This implementation...
##
## * ...stores all memory on the stack, so it is non-resizeable
## * ...uses a very simple linear open addressing scheme for managing collisions
## * ...tombstones deleted entries
## * ...silently rejects adding new values when at capacity
##
## An artical about the basic strategy for this implementation can be found here:
## https://tristanpenman.com/blog/posts/2017/10/11/sparsehash-internals/
##

type
Pair*[K, V] = tuple[key: K, value: V]

Entry[K, V] = object
pair: Pair[K, V]
sparseIdx: uint32

DenseIdx = distinct uint32

StaticSparseMap*[N : static int, K, V] = object
## A sparse map implemented on the stack
## * `N` is the maximum capacity of the map
## * `K` is the type of the key
## * `V` is the type for values stored in the map
dense: array[N, Entry[K, V]]
sparse: array[N, DenseIdx]
size: uint32

const UnusedDenseIdx = DenseIdx(0)

const TombstonedIdx = DenseIdx(1)

proc toUInt(idx: DenseIdx): auto {.inline.} = uint32(idx) - 2

proc toDenseIdx(num: uint32): auto {.inline.} = DenseIdx(num + 2)

proc `==`(a, b: DenseIdx): bool {.inline.} = uint32(a) == uint32(b)

proc `=copy`[N : static int, K, V](a: var StaticSparseMap[N, K, V], b: StaticSparseMap[N, K, V]) {.error.}

proc size*[N : static int, K, V](m: var StaticSparseMap[N, K, V]): auto {.inline.} = m.size

iterator possibleSparseIdxs[N : static int, K](key: K): uint32 =
## Iterates through the possible indexes at which a key could be set
let start = (ord(key).uint32 * 7) mod N.uint32
for i in start..<N:
yield i
for i in 0'u32..<start:
yield i

proc `[]=`*[N : static int, K, V](m: var StaticSparseMap[N, K, V], key: K, value: V) =
## Add an element into the map
if m.size < N:
for i in possibleSparseIdxs[N, K](key):
let denseIdx = m.sparse[i]
if denseIdx == UnusedDenseIdx or denseIdx == TombstonedIdx:
m.dense[m.size] = Entry[K, V](pair: (key, value), sparseIdx: i)
m.sparse[i] = m.size.toDenseIdx
m.size.inc
return

iterator items*[N : static int, K, V](m: var StaticSparseMap[N, K, V]): var Pair[K, V] =
## Iterates through all entries in this map
for i in 0..<m.size:
yield m.dense[i].pair

template find[N, K, V](m: var StaticSparseMap[N, K, V], key: K, exec: untyped): untyped =
## Internal template for walking the Open Addressing indexes internal used to organize the keys
for sparseIdx {.inject.} in possibleSparseIdxs[N, K](key):
let denseIdx {.inject.} = m.sparse[sparseIdx]

if denseIdx == UnusedDenseIdx:
break
elif not(denseIdx == TombstonedIdx):
var entry {.inject.} = m.dense[denseIdx.toUInt]
if entry.pair.key == key:
exec

proc contains*[N : static int, K, V](m: var StaticSparseMap[N, K, V], key: K): bool =
## Whether a key is in this table
m.find(key):
return true

proc `[]`*[N : static int, K, V](m: var StaticSparseMap[N, K, V], key: K): V =
## Get a pointer to a key in this map
m.find(key):
return entry.pair.value

proc delete*[N : static int, K, V](m: var StaticSparseMap[N, K, V], key: K) =
## Remove a key and its value from this map
if m.size > 0:
m.find(key):

# Invalidate the existing index
m.sparse[sparseIdx] = TombstonedIdx

# Reduce the total number of stored values
m.size.dec

# If the dense index is already at the end of the list, we just need to clear it
if denseIdx.toUInt == m.size:
m.dense[m.size] = Entry[K, V]()

else:
# Move the last dense value to ensure everything is tightly packed
m.dense[denseIdx.toUInt] = move(m.dense[m.size])

# Updated the sparse index of the moved value to point to its new location
m.sparse[m.dense[denseIdx.toUInt].sparseIdx] = denseIdx

return
Loading
Loading