code wiki / (root) / nx_map.nx

nx_map.nx source

↩ module page · 261 lines · 7946 B

1// map.nx -- Robin-hood hashmap with FNV-1a (Phase G8). 2// 3// Research: Celis 1986 (robin-hood hashing), Noll 1991 (FNV-1a). 4// Robin-hood hashing levels probe-distance variance so worst-case 5// lookup stays close to average. FNV-1a is simple, fast, and good 6// enough for integer + byte-string keys we'll hash in practice. 7// 8// Specialised to (i64, i64) entries for now — generalises to 9// Map<K, V> once parse.nx's multi-param generics ship. Use cases: 10// * Symbol tables in nxasm/nxld (name-hash → offset) 11// * Route tables in runtime/http.nx (path-hash → handler id) 12// * JSON object parse tables (key-hash → value index) 13// * Config stores (key-hash → value) 14// 15// Open addressing with linear probing; load-factor cap 0.75; power- 16// of-2 bucket count for AND-mask indexing. Deletions use tombstone 17// markers (so subsequent probes still find displaced entries). 18 19// nx_safety_envelope: 20// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 21// sil_target: SIL1 22// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 23// verdict: NOT_YET_EVALUATED 24 25import "nx_syscalls.nx" 26 27// Per-bucket metadata: 28// kind = 0: empty 29// kind = 1: occupied with (key, val) 30// kind = 2: tombstone 31struct MapEntry { 32 kind: i64, 33 key: i64, 34 val: i64, 35 probe: i64, // distance from ideal slot (for robin-hood) 36} 37 38const MAP_ENTRY_BYTES: i64 = 32 39 40struct Map { 41 entries: *MapEntry, 42 cap: i64, // power of 2 43 len: i64, // occupied entries (not counting tombstones) 44 mask: i64, // cap - 1 45} 46 47// FNV-1a 64-bit hash of an i64 key. Treats the key as 8 bytes LE. 48// Officially defined over byte streams; for i64 we mix each byte 49// through the standard offset-basis / prime. 50const FNV_OFFSET: i64 = 0xcbf29ce484222325 51const FNV_PRIME: i64 = 1099511628211 // 0x100000001b3 52 53func map_hash_i64(k: i64) -> i64 { 54 var h: i64 = FNV_OFFSET 55 var i: i64 = 0 56 while i < 8 { 57 let byte: i64 = (k >> (i * 8)) & 0xFF 58 h = h ^ byte 59 h = h * FNV_PRIME 60 i = i + 1 61 } 62 return h 63} 64 65// FNV-1a over a byte slice. Use for string-keyed maps (route 66// tables, config). Keys get pre-hashed by the caller. 67func map_hash_bytes(buf: *u8, n: i64) -> i64 { 68 var h: i64 = FNV_OFFSET 69 var i: i64 = 0 70 while i < n { 71 h = h ^ buf[i] 72 h = h * FNV_PRIME 73 i = i + 1 74 } 75 return h 76} 77 78// Forward decl: map_insert calls map_grow which is defined after it. 79func map_grow(m: *Map) -> i64; 80 81func map_entry_at(m: *Map, i: i64) -> *MapEntry { 82 let base: i64 = m.entries as i64 83 return (base + i * MAP_ENTRY_BYTES) as *MapEntry 84} 85 86// Allocate a map with at least `cap_hint` buckets (rounded up to a 87// power of 2, min 8). All entries start in kind=0 (empty) since 88// sys_mmap zeros the region. 89func map_new(cap_hint: i64) -> *Map { 90 var cap: i64 = 8 91 while cap < cap_hint { cap = cap * 2 } 92 let raw: *u8 = sys_mmap(64 + cap * MAP_ENTRY_BYTES + 16) 93 let m: *Map = raw as *Map 94 m.entries = (raw as i64 + 64) as *MapEntry 95 m.cap = cap 96 m.len = 0 97 m.mask = cap - 1 98 return m 99} 100 101func map_len(m: *Map) -> i64 { return m.len } 102 103// Insert (key, val). If key exists, overwrite. Robin-hood swap 104// rule: if the entry we're trying to place has travelled further 105// than the one currently in this slot, displace the resident and 106// continue probing with its data. Levels worst-case probe length. 107func map_insert(m: *Map, key: i64, val: i64) -> i64 { 108 // Grow if load factor would exceed 0.75. 109 if (m.len + 1) * 4 > m.cap * 3 { 110 map_grow(m) 111 } 112 var k: i64 = key 113 var v: i64 = val 114 var probe: i64 = 0 115 var idx: i64 = map_hash_i64(k) & m.mask 116 117 var placed: i64 = 0 118 while placed == 0 { 119 let e: *MapEntry = map_entry_at(m, idx) 120 if e.kind == 0 { 121 // Empty slot — place. 122 e.kind = 1 123 e.key = k 124 e.val = v 125 e.probe = probe 126 m.len = m.len + 1 127 placed = 1 128 } 129 if placed == 0 { 130 if e.kind == 1 { 131 if e.key == k { 132 // Update existing key. 133 e.val = v 134 placed = 1 135 } 136 } 137 } 138 if placed == 0 { 139 if e.kind == 2 { 140 // Reuse tombstone for this key iff not already present 141 // further down the probe chain — but simpler path: 142 // just fill it. Rare pathological rewrites can leave 143 // duplicate keys; caller should treat as first-wins 144 // (matches most robin-hood implementations). 145 e.kind = 1 146 e.key = k 147 e.val = v 148 e.probe = probe 149 m.len = m.len + 1 150 placed = 1 151 } 152 } 153 if placed == 0 { 154 // e.kind == 1 and e.key != k — robin-hood swap? 155 if e.probe < probe { 156 // Steal this slot; displace the existing entry. 157 let tk: i64 = e.key 158 let tv: i64 = e.val 159 let tp: i64 = e.probe 160 e.key = k 161 e.val = v 162 e.probe = probe 163 k = tk 164 v = tv 165 probe = tp 166 } 167 probe = probe + 1 168 idx = (idx + 1) & m.mask 169 } 170 } 171 return 0 172} 173 174// Return 1 and write value to *out if key present; 0 otherwise. 175func map_get(m: *Map, key: i64, out: *i64) -> i64 { 176 var probe: i64 = 0 177 var idx: i64 = map_hash_i64(key) & m.mask 178 var found: i64 = 0 179 var stop: i64 = 0 180 while stop == 0 { 181 let e: *MapEntry = map_entry_at(m, idx) 182 if e.kind == 0 { 183 stop = 1 184 } 185 if stop == 0 { 186 if e.kind == 1 { 187 if e.key == key { 188 *out = e.val 189 found = 1 190 stop = 1 191 } 192 } 193 } 194 if stop == 0 { 195 // Robin-hood invariant: if we've probed further than this 196 // entry did, the key can't be further along. 197 if probe > e.probe { 198 stop = 1 199 } 200 } 201 if stop == 0 { 202 probe = probe + 1 203 idx = (idx + 1) & m.mask 204 } 205 } 206 return found 207} 208 209// Returns 1 if the key was present and removed, 0 otherwise. 210func map_remove(m: *Map, key: i64) -> i64 { 211 var probe: i64 = 0 212 var idx: i64 = map_hash_i64(key) & m.mask 213 var removed: i64 = 0 214 var stop: i64 = 0 215 while stop == 0 { 216 let e: *MapEntry = map_entry_at(m, idx) 217 if e.kind == 0 { stop = 1 } 218 if stop == 0 { 219 if e.kind == 1 { 220 if e.key == key { 221 e.kind = 2 // tombstone 222 m.len = m.len - 1 223 removed = 1 224 stop = 1 225 } 226 } 227 } 228 if stop == 0 { 229 if probe > e.probe { stop = 1 } 230 } 231 if stop == 0 { 232 probe = probe + 1 233 idx = (idx + 1) & m.mask 234 } 235 } 236 return removed 237} 238 239// Double capacity + rehash. Called transparently by map_insert when 240// load factor would exceed threshold. Preserves all keys; tombstones 241// are dropped. 242func map_grow(m: *Map) -> i64 { 243 let old_entries: *MapEntry = m.entries 244 let old_cap: i64 = m.cap 245 let new_cap: i64 = old_cap * 2 246 let raw: *u8 = sys_mmap(new_cap * MAP_ENTRY_BYTES + 64) 247 m.entries = raw as *MapEntry 248 m.cap = new_cap 249 m.mask = new_cap - 1 250 m.len = 0 251 var i: i64 = 0 252 while i < old_cap { 253 let base: i64 = old_entries as i64 254 let e: *MapEntry = (base + i * MAP_ENTRY_BYTES) as *MapEntry 255 if e.kind == 1 { 256 map_insert(m, e.key, e.val) 257 } 258 i = i + 1 259 } 260 return 0 261}