ludic/runtime/native/dict.ludic
Orkuncakilkaya b0b0b62bce feat(lang): L7 memory is safe unless it says unsafe
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>
2026-09-24 12:53:27 +03:00

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