The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.
What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
5.5 KiB
Text
162 lines
5.5 KiB
Text
# ============================================================================
|
|
# dict.ludic — string-keyed containers: a hash map (`Dict.*`) from string keys
|
|
# to int values, and a set (`Set.*`) of string members. Written in Ludic.
|
|
#
|
|
# The everyday lookups a game needs: counts and registries by name (Dict) and
|
|
# membership by name — tags, visited tiles, unlocked achievements (Set). Both
|
|
# are backed by one open-addressing hash table (FNV-1a hash, linear probing,
|
|
# tombstone deletes, grow at load factor 0.7), so lookup and insert are O(1)
|
|
# average, not the linear scan a plain list would give. Keys are compared by
|
|
# content (the language's `==` on strings). Values are `int`, which also holds
|
|
# an `entity` handle or any small id; for richer values, use the Value.* tree.
|
|
#
|
|
# ludicc splices this file into any program that mentions `Dict.*` or `Set.*`;
|
|
# it is a self-contained fragment (only compiler intrinsics), so a plain tool
|
|
# works as well as a game. The namespaces (emit_call.ludic) alias each method to
|
|
# the matching `dict_*` / `set_*` function below. A Set is just a Dict whose
|
|
# values are ignored, so `Set.*` delegates to the same table.
|
|
# ============================================================================
|
|
|
|
const DICT_EMPTY: int = 0 # slot never used
|
|
const DICT_USED: int = 1 # slot holds a live key
|
|
const DICT_TOMB: int = 2 # slot held a key that was removed
|
|
|
|
# keys: one 8-byte pointer slot each (null when empty); vals/used: one word each.
|
|
property Dict { keys: pointers, vals: words, used: words, cap: int = 0, count: int = 0 }
|
|
|
|
# (re)allocate the three parallel arrays to `cap` slots, all cleared
|
|
function dict_init(d: Dict, cap: int) -> void {
|
|
d.cap = cap; d.count = 0
|
|
d.keys = pointers(cap); fill(d.keys, 0, cap * 8) # null slots
|
|
d.vals = words(cap); fill(d.vals, 0, cap * 4)
|
|
d.used = words(cap); fill(d.used, 0, cap * 4)
|
|
}
|
|
|
|
function dict_new() -> Dict {
|
|
let d = new Dict
|
|
dict_init(d, 16)
|
|
return d
|
|
}
|
|
|
|
# FNV-1a over the key's bytes -> a non-negative slot index in [0, cap)
|
|
function dict_slot(key: pointer, cap: int) -> int {
|
|
var h = -2128831035 # FNV-1a offset basis, as a signed i32
|
|
var i = 0
|
|
while key[i] != 0 { h = (h ^ key[i]) * 16777619; i += 1 }
|
|
h = h & 2147483647 # clear the sign bit -> non-negative
|
|
return h - (h / cap) * cap # h % cap
|
|
}
|
|
|
|
# slot index of `key`, or -1 if absent
|
|
function dict_find(d: Dict, key: pointer) -> int {
|
|
if d.cap == 0 { return -1 }
|
|
var idx = dict_slot(key, d.cap)
|
|
var probes = 0
|
|
while probes < d.cap {
|
|
let u = d.used[idx]
|
|
if u == DICT_EMPTY { return -1 }
|
|
if u == DICT_USED and d.keys[idx] == key { return idx }
|
|
idx += 1
|
|
if idx >= d.cap { idx = 0 }
|
|
probes += 1
|
|
}
|
|
return -1
|
|
}
|
|
|
|
# grow to double capacity and reinsert every live key
|
|
function dict_grow(d: Dict) -> void {
|
|
let oldcap = d.cap
|
|
let oldkeys = d.keys
|
|
let oldvals = d.vals
|
|
let oldused = d.used
|
|
var nc = oldcap * 2
|
|
if nc < 16 { nc = 16 }
|
|
dict_init(d, nc)
|
|
var i = 0
|
|
while i < oldcap {
|
|
if oldused[i] == DICT_USED { dict_set(d, oldkeys[i], oldvals[i]) }
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
# insert or update key -> value
|
|
function dict_set(d: Dict, key: pointer, value: int) -> void {
|
|
if d.cap == 0 { dict_init(d, 16) }
|
|
if (d.count + 1) * 10 >= d.cap * 7 { dict_grow(d) } # load factor 0.7
|
|
var idx = dict_slot(key, d.cap)
|
|
var tomb = -1
|
|
var probes = 0
|
|
while probes < d.cap {
|
|
let u = d.used[idx]
|
|
if u == DICT_EMPTY {
|
|
var slot = idx
|
|
if tomb >= 0 { slot = tomb }
|
|
d.keys[slot] = key; d.vals[slot] = value; d.used[slot] = DICT_USED
|
|
d.count += 1
|
|
return
|
|
}
|
|
if u == DICT_TOMB { if tomb < 0 { tomb = idx } }
|
|
if u == DICT_USED and d.keys[idx] == key { d.vals[idx] = value; return }
|
|
idx += 1
|
|
if idx >= d.cap { idx = 0 }
|
|
probes += 1
|
|
}
|
|
if tomb >= 0 {
|
|
d.keys[tomb] = key; d.vals[tomb] = value; d.used[tomb] = DICT_USED
|
|
d.count += 1
|
|
}
|
|
}
|
|
|
|
# the value for key, or 0 if absent (use dict_get_or to distinguish a stored 0)
|
|
function dict_get(d: Dict, key: pointer) -> int {
|
|
let i = dict_find(d, key)
|
|
if i < 0 { return 0 }
|
|
return d.vals[i]
|
|
}
|
|
|
|
function dict_get_or(d: Dict, key: pointer, fallback: int) -> int {
|
|
let i = dict_find(d, key)
|
|
if i < 0 { return fallback }
|
|
return d.vals[i]
|
|
}
|
|
|
|
function dict_has(d: Dict, key: pointer) -> bool { return dict_find(d, key) >= 0 }
|
|
|
|
# remove key (a no-op if absent); leaves a tombstone so probes still find later keys
|
|
function dict_remove(d: Dict, key: pointer) -> void {
|
|
let i = dict_find(d, key)
|
|
if i < 0 { return }
|
|
d.used[i] = DICT_TOMB
|
|
d.keys[i] = null
|
|
d.count -= 1
|
|
}
|
|
|
|
function dict_size(d: Dict) -> int { return d.count }
|
|
|
|
function dict_clear(d: Dict) -> void {
|
|
if d.cap == 0 { return }
|
|
fill(d.keys, 0, d.cap * 8)
|
|
fill(d.used, 0, d.cap * 4)
|
|
d.count = 0
|
|
}
|
|
|
|
# every live key, in unspecified order
|
|
function dict_keys(d: Dict) -> []pointer {
|
|
let out = new []pointer
|
|
var i = 0
|
|
while i < d.cap {
|
|
if d.used[i] == DICT_USED { push(out, d.keys[i]) }
|
|
i += 1
|
|
}
|
|
return out
|
|
}
|
|
|
|
# ---- Set: a Dict whose values are ignored ----------------------------------
|
|
|
|
function set_new() -> Dict { return dict_new() }
|
|
function set_add(s: Dict, key: pointer) -> void { dict_set(s, key, 1) }
|
|
function set_has(s: Dict, key: pointer) -> bool { return dict_has(s, key) }
|
|
function set_remove(s: Dict, key: pointer) -> void { dict_remove(s, key) }
|
|
function set_size(s: Dict) -> int { return dict_size(s) }
|
|
function set_clear(s: Dict) -> void { dict_clear(s) }
|
|
function set_members(s: Dict) -> []pointer { return dict_keys(s) }
|