From 66a542277f26a09fa3986bc2ffdb4338a2f35e89 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 15:20:53 +0700 Subject: [PATCH] The Map runtime, and the compiler scaffolding it needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Work in progress: it builds and the runtime is exercised and green, but no Flan program can reach it yet — the checker half is not written, so (Map K V) is still refused where it is resolved. runtime/flan_rt.c is Odin's map, followed deliberately: open-addressed Robin Hood hashing at a 75% load factor, cache-line cell packing so no key or value straddles a line, and the probe loop kept to pointer-width integers. One type-erased runtime over (key size, value size) plus a hash and equality pair, the same arrangement the Vec runtime has over (size, align). Two departures from Odin, both deliberate and both commented where they are made. There are no tombstones, because removal is deferred by spec-memory.md, and that deletes the backward-shift loop entirely — it is the single largest reason this is shorter than the original. And the header does not stuff log2cap into the low bits of the data pointer: Odin does that because Raw_Map must be three words, whereas this header already carries an allocator, a generation and an epoch, so the tagging would buy nothing, cost a mask on every access, and make correctness depend on the block being 64-byte aligned rather than merely faster when it is. The scaffolding around it: a Map is 48 bytes and six words like a Vec, it crosses to the runtime by address because it is move-only and must be mutated in place, and it has a DWARF type showing all six fields. Tast.FnAddr is new — the address of a function, either one this compiler emitted or a runtime C symbol. It is not a function value: nothing in the surface language can produce one, name its type or call through it. Odin's Map_Info reaches its hash and equality pair exactly this way. reach.ml learns that edge, because a function reached only by address is invisible to the reachability walk otherwise, which is the same hazard handler-bind clauses already had. The hash and equality pair carries the transfer channel as its last parameter, because a pair emitted for a struct key is an ordinary Flan function and every Flan function's signature ends with one. --- lib/emit.ml | 60 ++++- lib/reach.ml | 5 + lib/tast.ml | 18 ++ lib/types.ml | 22 +- runtime/flan_rt.c | 581 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 677 insertions(+), 9 deletions(-) diff --git a/lib/emit.ml b/lib/emit.ml index 869ff68..e6a0206 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -102,8 +102,14 @@ let rec ll (t : Types.t) = the Vec's address — so the shape is here only so that a slot, a struct field and a copy in the IR are the right number of bytes. *) | Types.Vec _ -> "%vec" + (* data + len + log2cap + allocator + gen + epoch. Six words, exactly as the + Vec's, and read here for exactly the same reason: nothing in this file + touches a field of one — every operation is a runtime call taking the + map's address — so the shape exists only so that a slot, a struct field + and a copy in the IR are the right number of bytes. *) + | Types.Map _ -> "%map" | Types.Option e -> Printf.sprintf "{ i8, %s }" (ll e) - | Types.Map _ | Types.Fn _ | Types.Var _ -> + | Types.Fn _ | Types.Var _ -> (* The checker rejects each of these by name — nothing reaches here. *) failwith ("no layout for " ^ Types.to_string t) @@ -248,7 +254,7 @@ let rec lay m (t : Types.t) : int * int = | Types.Enum _ -> 4, 4 | Types.Ptr _ -> 8, 8 | Types.Alloc -> 8, 8 - | Types.Vec _ -> 48, 8 + | Types.Vec _ | Types.Map _ -> 48, 8 (* [n x T] adds no padding of its own: T's size already carries its tail. *) | Types.Array (n, e) -> let s, a = lay m e in Int64.to_int n * s, a | Types.Option e -> let s, a, _ = lay_fields m [ Types.Int Types.I8; e ] in s, a @@ -260,7 +266,7 @@ let rec lay m (t : Types.t) : int * int = in s, a | None -> failwith ("no layout for struct " ^ n)) - | Types.Map _ | Types.Fn _ | Types.Var _ -> + | Types.Fn _ | Types.Var _ -> failwith ("no layout for " ^ Types.to_string t) (* Size, alignment, and the offset of every member. *) @@ -378,7 +384,19 @@ let rec dty m d (t : Types.t) : int = [ ("ptr", Types.Ptr e); ("len", Types.Int Types.I64); ("cap", Types.Int Types.I64); ("allocator", Types.Alloc); ("gen", Types.Int Types.I64); ("epoch", Types.Int Types.I64) ] - | Types.Map _ | Types.Fn _ | Types.Var _ -> + (* Six fields again, and shown as six for the same reason: a debugger + that showed fewer would put the reader's offsets out. [log2cap] is + shown rather than a capacity because that is what is stored — the + capacity is 1 << it, and a debugger that invented the shift would be + describing a field that is not there. *) + | Types.Map (k, v) -> + composite (Types.to_string t) + [ ("data", Types.Ptr (Types.Int Types.U8)); + ("len", Types.Int Types.I64); ("log2cap", Types.Int Types.I64); + ("allocator", Types.Alloc); ("gen", Types.Int Types.I64); + ("epoch", Types.Int Types.I64) ] + |> fun n -> ignore k; ignore v; n + | Types.Fn _ | Types.Var _ -> failwith ("no debug type for " ^ Types.to_string t) in Hashtbl.replace d.dtys key n; @@ -709,6 +727,10 @@ and value_at f (e : Tast.expr) : string = | Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ -> (* Everything that denotes a location is a load from its address. *) load f (addr f e) e.Tast.ty + (* The symbol itself, not a load from it: a function's address is a link-time + constant. The same spelling the handler frames use for a lifted clause. *) + | Tast.FnAddr (Tast.Flanfn n) -> fname n + | Tast.FnAddr (Tast.Rtfn n) -> "@" ^ n | Tast.Addr p -> fst (place f p) | Tast.Prim (p, args) -> prim f e p args | Tast.Call (name, args) -> @@ -1555,10 +1577,12 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = let p, n = explode f a in [ "ptr " ^ p; "i64 " ^ n ] | Types.Unit | Types.Never -> [] - (* A Vec is move-only and never copied, so it crosses to the - runtime as its address — which is also what lets an operation - mutate the caller's Vec in place. *) - | Types.Vec _ -> [ "ptr " ^ addr f a ] + (* A Vec and a Map are move-only and never copied, so each + crosses to the runtime as its address — which is also what + lets an operation mutate the caller's container in place. + Passing the header by value here would hand the runtime a + copy to grow and leave the caller's untouched. *) + | Types.Vec _ | Types.Map _ -> [ "ptr " ^ addr f a ] | t -> [ ll t ^ " " ^ value f a ]) args) in @@ -1955,6 +1979,10 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher ; (Vec T), spec-memory.md. The element type is nowhere in it: the runtime is ; type-erased and every operation is handed size and align at its call site. %vec = type { ptr, i64, i64, ptr, i64, i64 } +; (Map K V), spec-memory.md — Odin's open-addressed Robin Hood map. Neither key +; nor value type appears in it, for the same reason: one type-erased runtime, +; handed the two sizes and a hash/equality pair at each call site. +%map = type { ptr, i64, i64, ptr, i64, i64 } ; A handler frame: the one it displaced, the condition type it matches, and ; the lifted function that runs. Allocated on the establishing frame's stack. %handler = type { ptr, i32, ptr } @@ -2027,6 +2055,22 @@ declare i64 @flan_vec_len(ptr, ptr, i64) declare ptr @flan_vec_at(ptr, i32, i64, ptr, i64) declare void @flan_vec_as_slice(ptr, ptr, i32, i32, i64, ptr, i64) declare void @flan_vec_free(ptr, i64, i64, ptr, i64) +; (Map K V). The two ptr arguments before the location on put/get/clone are the +; hash and equality pair, which the checker emits per key type and passes here +; the way Odin hangs them off Map_Info. +declare i8 @flan_map_init(ptr, ptr, i64, i64, ptr, i64) +declare i8 @flan_map_put(ptr, ptr, ptr, i64, i64, ptr, ptr, ptr, i64) +declare i8 @flan_map_get(ptr, ptr, ptr, i64, i64, ptr, ptr, ptr, i64) +declare i8 @flan_map_has(ptr, ptr, i64, i64, ptr, ptr, ptr, i64) +declare i8 @flan_map_reserve(ptr, i64, i64, i64, ptr, ptr, i64) +declare i8 @flan_map_clone(ptr, ptr, ptr, i64, i64, ptr, ptr, i64) +declare i64 @flan_map_len(ptr, ptr, i64) +declare void @flan_map_free(ptr, i64, i64, ptr, i64) +declare i64 @flan_hash_flat(ptr, i64, i64) +declare i8 @flan_eq_flat(ptr, ptr, i64) +declare i64 @flan_hash_str(ptr, i64, i64) +declare i8 @flan_eq_str(ptr, ptr, i64) +declare i64 @flan_hash_combine(i64, i64) ; The filesystem. flan_file_read is not here: nothing Flan emits calls it — ; only flan_slurp_into does, from C — and flan_slurp_into is runtime glue ; rather than a fourth host call. See flan_rt.c for why the widening stops diff --git a/lib/reach.ml b/lib/reach.ml index 03f9969..50a3709 100644 --- a/lib/reach.ml +++ b/lib/reach.ml @@ -40,6 +40,11 @@ let rec expr_refs f (e : Tast.expr) = | Tast.Zero _ | Tast.Uninit _ | Tast.Local _ | Tast.None_ | Tast.InvokeRestart _ -> () | Tast.Global n -> f n + (* The other edge reached by address rather than by a call: a Map's hash and + equality pair. Same hazard as [Handled] below — miss it and a program with + a map loses the two functions its every lookup calls through. *) + | Tast.FnAddr (Tast.Flanfn n) -> f n + | Tast.FnAddr (Tast.Rtfn _) -> () | Tast.Prim (_, es) -> gos es | Tast.Call (n, es) -> f n; gos es | Tast.Do es -> gos es diff --git a/lib/tast.ml b/lib/tast.ml index 709d2e2..a81b448 100644 --- a/lib/tast.ml +++ b/lib/tast.ml @@ -73,6 +73,16 @@ and expr_kind = | Global of string | Prim of prim * expr list | Call of string * expr list (* direct call; no first-class fns yet *) + (* The address of a function the compiler emitted, by symbol. Not a function + *value*: nothing in the surface language can produce one, name its type or + call through it, and its only consumers are runtime entry points that take + a procedure the way spec-memory.md's type-erased allocator does. The Map's + hash and equality pair is what wanted it — Odin's [Map_Info] is two + contextless [proc] fields reached exactly this way — and a handler-bind + clause is the same arrangement with the symbol carried on [hframe] + instead. Its Flan type is [Alloc]: an opaque pointer-width value with no + user-writable constructor, which is all any backend needs to know. *) + | FnAddr of fnref | Do of expr list | Let of (int * expr) list * expr list | If of expr * expr * expr @@ -125,6 +135,14 @@ and expr_kind = (* [Serror] is §2's diverging variant: the same lookup, type Never, and with nothing transferring the program stops rather than carrying on. *) +(* Which symbol table the address comes out of. [Flanfn] is a function this + compiler emitted and is therefore name-mangled and reachability-tracked; + [Rtfn] is a C entry point in flan_rt.c, spelled as written. The two are + interchangeable at the call site because a Flan function's emitted signature + is its parameters followed by the transfer channel, and the runtime's + matching typedef spells that last pointer out. *) +and fnref = Flanfn of string | Rtfn of string + and sigkind = Ssignal | Serror and place = diff --git a/lib/types.ml b/lib/types.ml index 293c476..ff3918c 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -127,11 +127,31 @@ let is_numeric = function Int _ | Float _ -> true | _ -> false [free] needs no analysis of its own. A struct that owns one is move-only too; that arrives with [drop], which is the step after this one. *) let rec is_move_only = function - | Vec _ -> true + | Vec _ | Map _ -> true | Option t -> is_move_only t | Array (_, t) -> is_move_only t | _ -> false +(* The key types the first Map implementation admits (spec-memory.md, "Maps — + first implementation"): integers, enums, strings, fixed arrays, and value + structs composed recursively from those. Equality and hashing for them are + compiler-provided structural operations, so this is the whole of what the + emitted hash and equality pair has to cover — there is no dispatch to design + and no type class anywhere. + + A struct is [Named], and whether its fields qualify cannot be decided here: + this module has no field table. [Check] finishes the job by walking them, + which is also where it emits the pair. Everything this does say no to says + no for a reason that will not change with a milestone: a [Ptr] or a [Slice] + key would hash an address, and hashing an address is a different operation + from hashing what it points at. *) +let rec keyable = function + | Int _ | Enum _ | Bool | String -> true + | Float _ -> false (* NaN /= NaN, and 0.0 and -0.0 differ bytewise *) + | Array (_, t) -> keyable t + | Named _ -> true (* [Check] decides, by walking the fields *) + | _ -> false + (* Ordering and equality are defined on machine types and on nothing else at milestone 2 — strings, structs and slices have no built-in [=], because an unconstrained type supports only what every type supports (plan.org, Types). *) diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index df6b8c1..957b3e4 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -996,6 +996,587 @@ int8_t flan_vec_clone(flan_vec *dst, flan_vec *src, flan_allocator *a, return 1; } +/* ── (Map K V), spec-memory.md ────────────────────────────────────────── + * + * Odin's map, followed deliberately: open-addressed Robin Hood hashing at a + * 75% load factor, cache-line cell packing, and pointer-width integers through + * the probe loop (base/runtime/dynamic_map_internal.odin, whose header states + * the same three). One type-erased runtime over (key size, value size) and a + * compiler-emitted hash and equality pair, exactly as the Vec runtime is one + * over (size, align). + * + * Why the shape matters, since the obvious question is whether this is another + * Python dict. Python's algorithm is fine. What makes it slow is that every + * key and every value is a separately allocated, reference-counted object, and + * hashing goes through __hash__ and __eq__ calls that cannot be inlined. Here + * a key is raw bytes inside the block and the hash and comparison are compiled + * concretely per key type. That is most of the gap before any cleverness. + * + * Robin Hood, in one paragraph. Every occupied slot has a probe distance: how + * far it sits from the slot its hash wanted. On insert, if the element already + * in a slot is closer to its desired slot than the element being placed, the + * two swap and the poorer one carries on down the run. Distances even out, the + * worst case collapses towards the average, and a lookup may stop the moment + * it is further from home than the occupant it is looking at — which is the + * early exit in flan_map_find and is why a miss costs about what a hit does. + * + * Cache-line cells, in one more. A flat [capacity]K array lets one key straddle + * two cache lines, so a probe that walks four slots can touch five lines. A + * Map_Cell packs as many Ks as fit in 64 bytes and pads the remainder, so no + * key ever straddles a line and a linear probe walks memory in the order the + * prefetcher expects. Keys, values and hashes are three separate blocks, so a + * probe — which reads hashes and only then one key — touches hash lines and + * nothing else until it has a candidate. + * + * Header, six words, the same as flan_vec's and for the same reason (a layout + * that changes with a build flag can disagree across the reload boundary): + * + * data one allocation: keys | values | hashes | scratch + * len live entries + * log2cap 0 until something is allocated; never 1 or 2 after + * allocator gen epoch as on a Vec, and checked the same way + * + * Odin stuffs log2cap into the low six bits of the data pointer because its + * Raw_Map must be three words. This header already carries an allocator, a + * generation and an epoch, so the bit-stuffing would buy nothing and cost a + * mask on every access — and, more usefully, not tagging means correctness + * never depends on the block being 64-byte aligned. It is requested as 64, and + * cell packing pays off when the request is honoured, but an arena whose base + * is not cache-aligned gives a slower map rather than a wrong one. + * + * Every entry point returns int8_t 1/0 for "did it fit", never reporting + * failure any other way — the condition, the restart and the message are the + * compiler's job (Check's alloc_guard). */ + +#define FLAN_MAP_CACHE_LINE 64 +#define FLAN_MAP_LOAD_FACTOR 75 +#define FLAN_MAP_MIN_LOG2 3 /* 8 slots */ + +/* The hash word. Zero means the slot is empty, which is what makes a + * zeroed hash block an empty map. There is no tombstone: removal is deferred + * (spec-memory.md defers move-aware lookup, removal and owned entries), so the + * only two states a slot has are empty and occupied. That deletes Odin's + * backward-shift loop from this file entirely, and it is the single largest + * reason this is shorter than the Odin original. + * + * The top bit is set on every stored hash so that a hasher answering 0 does + * not read as an empty slot. It is the highest bit, so the desired slot and + * the probe distance — which use only the low log2cap bits — are unchanged by + * it, and no hash needs rewriting when the capacity changes. */ +typedef uint64_t flan_map_hash; +#define FLAN_MAP_OCCUPIED ((uint64_t)1 << 63) + +/* The pair the compiler emits per key type. [size] is the key's size, passed + * so that the flat hasher and comparator below can serve every key whose + * equality is bytewise and need no per-type function at all. + * + * The trailing pointer is the transfer channel. Every Flan function's emitted + * signature ends with one (Emit's xfer_param), and a hash function emitted for + * a struct key is an ordinary Flan function — so the typedef spells it out + * rather than hoping nothing ever writes through it. Nothing does: neither a + * hasher nor a comparator can signal, because the only things either can call + * are the leaf C entry points below. It is passed as a real address, never + * NULL, so that a store through it would be a store and not a crash. */ +typedef uint64_t (*flan_hash_fn)(const void *key, uint64_t seed, int64_t size, + void *xfer); +typedef int8_t (*flan_eq_fn)(const void *a, const void *b, int64_t size, + void *xfer); + +typedef struct flan_map { + void *data; + int64_t len; + int64_t log2cap; + flan_allocator *alloc; + int64_t gen; + int64_t epoch; +} flan_map; + +/* ── Hashing ────────────────────────────────────────────────────────── + * + * FNV-1a over the bytes, then a final avalanche. FNV alone leaves the low bits + * poorly mixed and the low bits are exactly what selects the slot, so the + * splitmix64 finaliser is not decoration: without it consecutive small integer + * keys collide in long runs. The seed is mixed in first so that two maps do not + * agree on the same pathological ordering. */ +static uint64_t flan_mix64(uint64_t x) { + x ^= x >> 30; + x *= 0xbf58476d1ce4e5b9ULL; + x ^= x >> 27; + x *= 0x94d049bb133111ebULL; + x ^= x >> 31; + return x; +} + +static uint64_t flan_hash_mem(const uint8_t *p, int64_t n, uint64_t seed) { + uint64_t h = 0xcbf29ce484222325ULL ^ seed; + int64_t i; + for (i = 0; i < n; i++) { + h ^= (uint64_t)p[i]; + h *= 0x100000001b3ULL; + } + return flan_mix64(h); +} + +/* The key is [size] bytes and its equality is bytewise. Every integer, enum, + * bool, float and fixed array of those is served by this one pair, so the + * compiler emits a function only for a key type that needs one. */ +uint64_t flan_hash_flat(const void *key, uint64_t seed, int64_t size, + void *xfer) { + (void)xfer; + return flan_hash_mem((const uint8_t *)key, size, seed); +} + +int8_t flan_eq_flat(const void *a, const void *b, int64_t size, void *xfer) { + (void)xfer; + return (int8_t)(memcmp(a, b, (size_t)size) == 0); +} + +/* A string is ptr+len and its bytes are elsewhere, so neither the flat hasher + * nor memcmp is correct for it: two equal strings at different addresses must + * hash the same. [size] is ignored; the shape is fixed. */ +uint64_t flan_hash_str(const void *key, uint64_t seed, int64_t size, + void *xfer) { + const flan_slice *s = (const flan_slice *)key; + (void)size; (void)xfer; + return flan_hash_mem(s->ptr, s->len, seed); +} + +int8_t flan_eq_str(const void *a, const void *b, int64_t size, void *xfer) { + const flan_slice *x = (const flan_slice *)a, *y = (const flan_slice *)b; + (void)size; (void)xfer; + if (x->len != y->len) return 0; + if (x->len == 0) return 1; + return (int8_t)(memcmp(x->ptr, y->ptr, (size_t)x->len) == 0); +} + +/* Combining, for a key type the compiler does emit a function for: a struct + * with padding (whose padding bytes are indeterminate and must not be hashed) + * or one with a string field (whose bytes are elsewhere). The emitted function + * hashes each field with the right pair and folds the results through here. */ +uint64_t flan_hash_combine(uint64_t acc, uint64_t h) { + return flan_mix64(acc ^ (h + 0x9e3779b97f4a7c15ULL + (acc << 6) + (acc >> 2))); +} + +/* ── Cell geometry ──────────────────────────────────────────────────── + * + * Odin precomputes these into a Map_Cell_Info so the probe loop never divides. + * They are derived from the element size alone — alignment cannot matter, + * because a cell starts on a 64-byte boundary and no Flan type is aligned + * above that — so they are derived once on entry to each operation and kept in + * locals, which is the same trade with one less thing for the checker to pass + * and get wrong. */ +static int64_t flan_cell_epc(int64_t size) { + if (size <= 0 || size >= FLAN_MAP_CACHE_LINE) return 1; + return FLAN_MAP_CACHE_LINE / size; +} + +static int64_t flan_cell_size(int64_t size) { + int64_t n = flan_cell_epc(size) * size; + return flan_align_up(n, FLAN_MAP_CACHE_LINE); +} + +/* The bytes a [count]-element run of cells occupies, rounded to a cache line + * so the next block starts on one too. */ +static int64_t flan_cells_bytes(int64_t size, int64_t count) { + int64_t epc = flan_cell_epc(size); + int64_t cells = (count + epc - 1) / epc; + return cells * flan_cell_size(size); +} + +/* Slot [i] of a cell-packed run. [epc] and [cell] are hoisted by every caller + * that walks, which is why they are parameters rather than recomputed here. */ +static uint8_t *flan_cell_at(uint8_t *base, int64_t size, int64_t epc, + int64_t cell, int64_t i) { + if (epc == 1) return base + i * cell; + return base + (i / epc) * cell + (i % epc) * size; +} + +/* The four blocks. Keys, values and hashes get one run each; the scratch is + * two more keys and two more values, which is where the Robin Hood swap keeps + * the element in flight. Odin allocates the same two, for the same reason: the + * swap is a memcpy between type-erased buffers and there is no local of the + * right type to hold one. */ +static int64_t flan_map_block_size(int64_t ksize, int64_t vsize, int64_t cap) { + return flan_cells_bytes(ksize, cap) + flan_cells_bytes(vsize, cap) + + flan_cells_bytes((int64_t)sizeof(flan_map_hash), cap) + + flan_cells_bytes(ksize, 2) + flan_cells_bytes(vsize, 2); +} + +static void flan_map_blocks(const flan_map *m, int64_t ksize, int64_t vsize, + int64_t cap, uint8_t **ks, uint8_t **vs, + flan_map_hash **hs, uint8_t **sk, uint8_t **sv) { + uint8_t *p = (uint8_t *)m->data; + *ks = p; p += flan_cells_bytes(ksize, cap); + *vs = p; p += flan_cells_bytes(vsize, cap); + *hs = (flan_map_hash *)p; + p += flan_cells_bytes((int64_t)sizeof(flan_map_hash), cap); + *sk = p; p += flan_cells_bytes(ksize, 2); + *sv = p; +} + +/* The same epoch check a Vec does, and it runs in every build for the same + * reason. A map that never allocated has no allocator and nothing to check. */ +static void flan_map_check(flan_map *m, const uint8_t *loc, int64_t loclen) { + if (m->alloc) { + int64_t now = (int64_t)m->alloc->epoch; + if (now != m->epoch) flan_vec_stale_fail(loc, loclen, m->epoch, now); + } +} + +static flan_allocator *flan_map_adopt(flan_map *m) { + if (!m->alloc) { + m->alloc = flan_context_allocator(); + m->epoch = (int64_t)m->alloc->epoch; + } + return m->alloc; +} + +/* The seed, derived from the block address exactly as Odin derives it: two + * maps with the same keys then disagree about which slot is which, so an + * adversarial insertion order against one is not an insertion order against + * the other. It changes on every grow, which is why hashes are recomputed + * there rather than carried over. */ +static uint64_t flan_map_seed(const flan_map *m) { + return flan_mix64((uint64_t)(uintptr_t)m->data + 0x9e3779b97f4a7c15ULL); +} + +static int64_t flan_map_cap(const flan_map *m) { + return m->data ? ((int64_t)1 << m->log2cap) : 0; +} + +/* 75% of capacity, as fixed-point integer arithmetic. Robin Hood wants a + * maximum load factor under 100% and 75% is where Odin sets it. */ +static int64_t flan_map_threshold(const flan_map *m) { + return (flan_map_cap(m) * FLAN_MAP_LOAD_FACTOR) / 100; +} + +/* How far this element is from the slot its hash wanted. Odin's identity: + * (slot - hash) & mask is the same number as (slot + cap - desired) & mask, + * with fewer operations, because desired is hash & mask. */ +static int64_t flan_map_distance(uint64_t hash, int64_t slot, int64_t mask) { + return (int64_t)(((uint64_t)slot - hash) & (uint64_t)mask); +} + +/* Place one element, already hashed, into a map known to have room. This is + * Odin's swap_loop and nothing else: with no tombstones there is no second + * loop, and the load factor guarantees an empty slot is reached. */ +static void flan_map_place(flan_map *m, uint64_t h, const void *ikey, + const void *ival, int64_t ksize, int64_t vsize) { + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + int64_t cap = flan_map_cap(m), mask = cap - 1; + int64_t kepc = flan_cell_epc(ksize), kcell = flan_cell_size(ksize); + int64_t vepc = flan_cell_epc(vsize), vcell = flan_cell_size(vsize); + int64_t pos = (int64_t)(h & (uint64_t)mask), dist = 0; + uint8_t *k, *v, *tk, *tv; + + flan_map_blocks(m, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + /* The element in flight lives in scratch slot 0; slot 1 is the swap + * temporary. Both are inside the block, so nothing here touches the stack + * with a size only known at run time. */ + k = flan_cell_at(sk, ksize, kepc, kcell, 0); + v = flan_cell_at(sv, vsize, vepc, vcell, 0); + tk = flan_cell_at(sk, ksize, kepc, kcell, 1); + tv = flan_cell_at(sv, vsize, vepc, vcell, 1); + memcpy(k, ikey, (size_t)ksize); + if (vsize > 0) memcpy(v, ival, (size_t)vsize); + + for (;;) { + uint64_t eh = hs[pos]; + if (eh == 0) { + memcpy(flan_cell_at(ks, ksize, kepc, kcell, pos), k, (size_t)ksize); + if (vsize > 0) + memcpy(flan_cell_at(vs, vsize, vepc, vcell, pos), v, (size_t)vsize); + hs[pos] = h; + return; + } + /* The Robin Hood swap: the occupant is richer — closer to home — than the + * element in flight, so the poorer one takes the slot and the richer one + * carries on. This is what keeps the variance down. */ + if (dist > flan_map_distance(eh, pos, mask)) { + uint8_t *kp = flan_cell_at(ks, ksize, kepc, kcell, pos); + uint8_t *vp = flan_cell_at(vs, vsize, vepc, vcell, pos); + uint64_t th; + memcpy(tk, k, (size_t)ksize); + memcpy(k, kp, (size_t)ksize); + memcpy(kp, tk, (size_t)ksize); + if (vsize > 0) { + memcpy(tv, v, (size_t)vsize); + memcpy(v, vp, (size_t)vsize); + memcpy(vp, tv, (size_t)vsize); + } + th = h; h = hs[pos]; hs[pos] = th; + dist = flan_map_distance(h, pos, mask); + } + pos = (pos + 1) & mask; + dist++; + } +} + +/* The slot holding [key], or -1. The middle test is the Robin Hood early exit: + * this probe is further from home than the occupant is, and Robin Hood + * maintains that no element is ever further from home than one it passed, so + * the key cannot be further along. A miss therefore costs about what a hit + * does, which is the property the ordering buys. */ +static int64_t flan_map_find(flan_map *m, const void *key, int64_t ksize, + int64_t vsize, flan_hash_fn hash, flan_eq_fn eq) { + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + int64_t cap, mask, kepc, kcell, pos, dist = 0; + uint64_t h; + void *xfer = NULL; + if (!m->data || m->len == 0) return -1; + cap = flan_map_cap(m); + mask = cap - 1; + flan_map_blocks(m, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + kepc = flan_cell_epc(ksize); + kcell = flan_cell_size(ksize); + h = hash(key, flan_map_seed(m), ksize, &xfer) | FLAN_MAP_OCCUPIED; + pos = (int64_t)(h & (uint64_t)mask); + for (;;) { + uint64_t eh = hs[pos]; + if (eh == 0) return -1; + if (dist > flan_map_distance(eh, pos, mask)) return -1; + if (eh == h + && eq(key, flan_cell_at(ks, ksize, kepc, kcell, pos), ksize, &xfer)) + return pos; + pos = (pos + 1) & mask; + dist++; + } +} + +/* Allocate a block for 2^log2cap slots and zero the hashes. Only the hash run + * needs zeroing — a key or value slot is never read without its hash saying it + * is live — so the keys and values are left as the allocator returned them. */ +static int8_t flan_map_alloc(flan_map *m, flan_allocator *a, int64_t log2cap, + int64_t ksize, int64_t vsize) { + int64_t cap = (int64_t)1 << log2cap; + int64_t bytes = flan_map_block_size(ksize, vsize, cap); + void *p; + flan_fail_bytes = bytes; + flan_fail_align = FLAN_MAP_CACHE_LINE; + flan_fail_id = (int64_t)(intptr_t)a; + p = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, bytes, FLAN_MAP_CACHE_LINE); + if (!p) return 0; + m->data = p; + m->log2cap = log2cap; + m->len = 0; + { + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + flan_map_blocks(m, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + memset(hs, 0, (size_t)flan_cells_bytes((int64_t)sizeof(flan_map_hash), cap)); + } + return 1; +} + +/* Double the capacity and reinsert. The seed moves with the block, so every + * hash is recomputed rather than carried over — which is what a fresh seed per + * block is for. The old block is released only after the last read of it. */ +static int8_t flan_map_grow(flan_map *m, int64_t want, int64_t ksize, + int64_t vsize, flan_hash_fn hash) { + flan_allocator *a = flan_map_adopt(m); + flan_map fresh; + int64_t log2cap = FLAN_MAP_MIN_LOG2, old_cap = flan_map_cap(m); + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + int64_t kepc, kcell, vepc, vcell, i, moved; + void *xfer = NULL; + + /* Smallest power of two whose 75% threshold still holds [want]. */ + while ((((int64_t)1 << log2cap) * FLAN_MAP_LOAD_FACTOR) / 100 < want) { + if (log2cap >= 40) return 0; + log2cap++; + } + if (log2cap <= m->log2cap && m->data) return 1; + + fresh.data = NULL; fresh.len = 0; fresh.log2cap = 0; + fresh.alloc = a; fresh.gen = 0; fresh.epoch = (int64_t)a->epoch; + if (!flan_map_alloc(&fresh, a, log2cap, ksize, vsize)) return 0; + + if (m->data) { + flan_map_blocks(m, ksize, vsize, old_cap, &ks, &vs, &hs, &sk, &sv); + kepc = flan_cell_epc(ksize); kcell = flan_cell_size(ksize); + vepc = flan_cell_epc(vsize); vcell = flan_cell_size(vsize); + moved = m->len; + for (i = 0; i < old_cap && moved > 0; i++) { + uint64_t h; + if (hs[i] == 0) continue; + h = hash(flan_cell_at(ks, ksize, kepc, kcell, i), + flan_map_seed(&fresh), ksize, &xfer) | FLAN_MAP_OCCUPIED; + flan_map_place(&fresh, h, + flan_cell_at(ks, ksize, kepc, kcell, i), + flan_cell_at(vs, vsize, vepc, vcell, i), ksize, vsize); + fresh.len++; + moved--; + } + if (a->caps & FLAN_CAN_FREE) + a->proc(a, FLAN_ALLOC_FREE, m->data, + flan_map_block_size(ksize, vsize, old_cap), 0, + FLAN_MAP_CACHE_LINE); + } + m->data = fresh.data; + m->log2cap = fresh.log2cap; + m->len = fresh.len; + /* Every key and value moved, so any pointer into the old block is stale — + * the same word, bumped for the same reason, as a Vec's reallocation. */ + m->gen++; + return 1; +} + +int8_t flan_map_init(flan_map *m, flan_allocator *a, int64_t ksize, + int64_t vsize, const uint8_t *loc, int64_t loclen) { + if (!a) flan_null_alloc_fail(loc, loclen); + (void)ksize; (void)vsize; + m->data = NULL; + m->len = 0; + m->log2cap = 0; + m->gen = 0; + m->alloc = a; + m->epoch = (int64_t)a->epoch; + /* No block until something is put in it: an empty map that is never written + * costs nothing, which is what makes a (defvar m (Map string i32)) free. */ + return 1; +} + +/* The upsert. spec-memory.md: it either inserts or replaces, and returns Unit + * — there is no Result and no ignorable error code, because a put that put + * nothing and said nothing is the outcome the StorageExhausted rule exists to + * make impossible. */ +int8_t flan_map_put(flan_map *m, const void *key, const void *val, + int64_t ksize, int64_t vsize, flan_hash_fn hash, + flan_eq_fn eq, const uint8_t *loc, int64_t loclen) { + int64_t at, cap; + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + uint64_t h; + flan_map_check(m, loc, loclen); + + at = flan_map_find(m, key, ksize, vsize, hash, eq); + if (at >= 0) { + /* Replace. The key already in the block compares equal to the one handed + * in, so it is left alone: overwriting it would be a no-op for every + * bytewise key and a question nobody has asked for the others. */ + if (vsize > 0) { + cap = flan_map_cap(m); + flan_map_blocks(m, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + memcpy(flan_cell_at(vs, vsize, flan_cell_epc(vsize), + flan_cell_size(vsize), at), + val, (size_t)vsize); + } + return 1; + } + + if (!m->data || m->len + 1 > flan_map_threshold(m)) + if (!flan_map_grow(m, m->len + 1, ksize, vsize, hash)) return 0; + + { + void *xfer = NULL; + h = hash(key, flan_map_seed(m), ksize, &xfer) | FLAN_MAP_OCCUPIED; + } + flan_map_place(m, h, key, val, ksize, vsize); + m->len++; + return 1; +} + +/* Lookup. The value is copied out into [out] — the first Map implementation + * admits copyable keys and values only, so get returns a copy — and the answer + * is 1/0 for found, which the compiler turns into Some/None. */ +int8_t flan_map_get(flan_map *m, const void *key, void *out, int64_t ksize, + int64_t vsize, flan_hash_fn hash, flan_eq_fn eq, + const uint8_t *loc, int64_t loclen) { + int64_t at, cap; + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + flan_map_check(m, loc, loclen); + at = flan_map_find(m, key, ksize, vsize, hash, eq); + if (at < 0) return 0; + if (vsize > 0) { + cap = flan_map_cap(m); + flan_map_blocks(m, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + memcpy(out, + flan_cell_at(vs, vsize, flan_cell_epc(vsize), flan_cell_size(vsize), + at), + (size_t)vsize); + } + return 1; +} + +int8_t flan_map_has(flan_map *m, const void *key, int64_t ksize, int64_t vsize, + flan_hash_fn hash, flan_eq_fn eq, const uint8_t *loc, + int64_t loclen) { + flan_map_check(m, loc, loclen); + return (int8_t)(flan_map_find(m, key, ksize, vsize, hash, eq) >= 0); +} + +int64_t flan_map_len(flan_map *m, const uint8_t *loc, int64_t loclen) { + flan_map_check(m, loc, loclen); + return m->len; +} + +/* Room for [n] entries without reallocating, which means a block whose 75% + * threshold is at least n. */ +int8_t flan_map_reserve(flan_map *m, int64_t n, int64_t ksize, int64_t vsize, + flan_hash_fn hash, const uint8_t *loc, int64_t loclen) { + flan_map_check(m, loc, loclen); + if (n <= 0) return 1; + if (m->data && n <= flan_map_threshold(m)) return 1; + return flan_map_grow(m, n, ksize, vsize, hash); +} + +/* spec-memory.md's first release point, and the same rules the Vec's free + * follows: left zeroed rather than dangling, and an allocator without can-free + * keeps the block because releasing it is free-all's job. */ +void flan_map_free(flan_map *m, int64_t ksize, int64_t vsize, + const uint8_t *loc, int64_t loclen) { + flan_map_check(m, loc, loclen); + if (m->data && m->alloc && (m->alloc->caps & FLAN_CAN_FREE)) + m->alloc->proc(m->alloc, FLAN_ALLOC_FREE, m->data, + flan_map_block_size(ksize, vsize, flan_map_cap(m)), 0, + FLAN_MAP_CACHE_LINE); + m->data = NULL; + m->len = 0; + m->log2cap = 0; + m->alloc = NULL; + m->gen++; + m->epoch = 0; +} + +/* A deep, independent copy. It reinserts rather than copying the block: the + * seed is derived from the block address, so a bytewise copy would be a map + * whose stored hashes disagree with its own seed and whose every lookup + * missed. Reinserting is also what makes the copy's layout independent of the + * original's insertion history. */ +int8_t flan_map_clone(flan_map *dst, flan_map *src, flan_allocator *a, + int64_t ksize, int64_t vsize, flan_hash_fn hash, + const uint8_t *loc, int64_t loclen) { + uint8_t *ks, *vs, *sk, *sv; + flan_map_hash *hs; + int64_t cap, kepc, kcell, vepc, vcell, i, moved; + void *xfer = NULL; + flan_map_check(src, loc, loclen); + if (!flan_map_init(dst, a, ksize, vsize, loc, loclen)) return 0; + if (!src->data || src->len == 0) return 1; + if (!flan_map_grow(dst, src->len, ksize, vsize, hash)) return 0; + + cap = flan_map_cap(src); + flan_map_blocks(src, ksize, vsize, cap, &ks, &vs, &hs, &sk, &sv); + kepc = flan_cell_epc(ksize); kcell = flan_cell_size(ksize); + vepc = flan_cell_epc(vsize); vcell = flan_cell_size(vsize); + moved = src->len; + for (i = 0; i < cap && moved > 0; i++) { + uint64_t h; + if (hs[i] == 0) continue; + h = hash(flan_cell_at(ks, ksize, kepc, kcell, i), flan_map_seed(dst), + ksize, &xfer) | FLAN_MAP_OCCUPIED; + flan_map_place(dst, h, flan_cell_at(ks, ksize, kepc, kcell, i), + flan_cell_at(vs, vsize, vepc, vcell, i), ksize, vsize); + dst->len++; + moved--; + } + return 1; +} + /* ── The filesystem, and the whole of what it adds to the host ABI ─── * * plan.org names the filesystem as the #1 portability risk — "pack assets, one