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