From bb2471f7a1bb0a529978760539bee24e92cfc9fb Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 18 Sep 2026 12:23:39 +0700 Subject: [PATCH] The gen word leaves both headers: a check nothing runs is weight, and Odin carries none of it --- lib/check.ml | 6 +++--- lib/emit.ml | 19 +++++++++-------- runtime/flan_rt.c | 45 +++++++++++++++-------------------------- test/test_acceptance.ml | 2 +- 4 files changed, 29 insertions(+), 43 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index 99c1675..b8caf6f 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3831,9 +3831,9 @@ and named_call ctx ~want loc name args = (* spec-memory.md's first release point. Since the repeal, what it consumes it consumes at run time only: nothing marks the binding dead, so a second [free] or a read after this one type-checks and misbehaves at run time — - the allocator aborts on a double free it can see, and the dev build's - generation word traps a stale read. That is the Odin contract: free is a - thing you write, and writing it twice is yours to not do. *) + the allocator aborts on a double free it can see, and the epoch word + traps a read through a released region. That is the Odin contract: free + is a thing you write, and writing it twice is yours to not do. *) | "free" -> arity loc name 1 args; let target = check ctx (List.hd args) in diff --git a/lib/emit.ml b/lib/emit.ml index 86d2f81..083da71 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -287,7 +287,7 @@ let rec lay m (t : Types.t) : int * int = | Types.Ptr _ -> 8, 8 | Types.Alloc -> 8, 8 | Types.Fn _ -> 8, 8 - | Types.Vec _ | Types.Map _ -> 48, 8 + | Types.Vec _ | Types.Map _ -> 40, 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 @@ -512,15 +512,15 @@ let rec dty m d (t : Types.t) : int = | Types.Alloc -> dnode d "!DIDerivedType(tag: DW_TAG_pointer_type, name: \"Allocator\", baseType: null, size: 64)" - (* Shown as what it is. The two dev words are in the layout and so they - are here too: a debugger that showed four fields of a six-field struct - would put the reader's offsets out by two. *) + (* Shown as what it is. The epoch word is in the layout and so it is + here too: a debugger that showed four fields of a five-field struct + would put the reader's offsets out by one. *) | Types.Vec e -> composite (Types.to_string t) [ ("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) ] - (* Six fields again, and shown as six for the same reason: a debugger + ("epoch", Types.Int Types.I64) ] + (* Five fields again, and shown as five 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 @@ -529,8 +529,7 @@ let rec dty m d (t : Types.t) : int = 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) ] + ("allocator", Types.Alloc); ("epoch", Types.Int Types.I64) ] |> fun n -> ignore k; ignore v; n (* A pointer to code, and lldb is told exactly that and no more. DWARF has DW_TAG_subroutine_type for the signature behind it, and spelling @@ -2668,11 +2667,11 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher %slice = type { ptr, i64 } ; (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 } +%vec = type { ptr, i64, i64, ptr, 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 } +%map = type { ptr, i64, i64, ptr, 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 } diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 492dc02..4267dbb 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -906,8 +906,7 @@ struct flan_allocator { void *data; uint32_t caps; /* Bumped on every free-all. A container records it and traps if it moved: - * spec-memory.md, "Dev builds detect a released region". Separate from the - * per-Vec generation word, which answers a different question. */ + * spec-memory.md, "Dev builds detect a released region". */ uint64_t epoch; /* Dev accounting for the general-purpose tier: "did you forget to free" is * an allocator-tier question and this is the allocator's answer. */ @@ -1392,30 +1391,26 @@ _Noreturn void flan_region_only_fail(const uint8_t *loc, int64_t loclen) { * produce the numbers, and it passes them in. There are no generics here and * none are needed. * - * Header, and it is six words rather than the spec's four: + * Header, and it is five words rather than the spec's four: * * ptr len cap allocator the release layout spec-memory.md fixes - * gen bumped on every reallocation — the stale-slice - * word spec-memory.md asks for. It has no reader - * and cannot have one as things stand, which is - * the part "not yet" used to hide: a slice is - * ptr+len, so it carries neither the Vec it came - * from nor the generation it was taken at, and - * the check has nothing to compare. Giving it a - * reader is a third word on every slice in the - * language, not a change to this file. Nothing - * here or anywhere else reads it; do not write - * code that trusts it. See docs/BUILT.md. * epoch the allocator's epoch when this Vec last * touched it. Any operation on a container whose * recorded epoch has moved traps. * - * The two dev words are present in every build, not only a dev one, and that + * There used to be a sixth word, gen, the stale-slice generation + * spec-memory.md once asked for. It was bumped on every reallocation and + * consulted by nothing — a slice is ptr+len and carries neither the Vec it + * came from nor the generation it was taken at, so the check it promised had + * nothing to compare — and Odin's header (data, len, cap, allocator, and + * nothing else) is the model this one follows. Deleted 2026-09-18 with the + * ownership repeal; see docs/BUILT.md. + * + * The epoch word is present in every build, not only a dev one, and that * is not laziness: a redefinition module is built by llc and ld against a host * that was built separately, and nothing makes the two agree on a struct size. * A layout that changes with a build flag is a layout that can disagree across - * that boundary silently. Dropping them in release is deferred and docs/BUILT.md - * says what it is blocked on. + * that boundary silently. * * Every entry point returns int8_t 1/0 for "did it fit", and never reports * failure any other way: the condition, the restart and the message are the @@ -1426,7 +1421,6 @@ typedef struct flan_vec { int64_t len; int64_t cap; flan_allocator *alloc; - int64_t gen; int64_t epoch; } flan_vec; @@ -1546,7 +1540,6 @@ static int8_t flan_vec_grow(flan_vec *v, int64_t want, int64_t size, v->cap = cap; /* Any slice taken before this points at storage that may have moved. The * word is bumped here and read nowhere yet; see docs/BUILT.md. */ - v->gen++; return 1; } @@ -1565,7 +1558,6 @@ int8_t flan_vec_init(flan_vec *v, flan_allocator *a, int64_t cap, int64_t size, v->ptr = NULL; v->len = 0; v->cap = 0; - v->gen = 0; v->alloc = a; v->epoch = (int64_t)v->alloc->epoch; if (cap <= 0) return 1; @@ -1649,7 +1641,6 @@ void flan_vec_free(flan_vec *v, int64_t size, int64_t align, v->len = 0; v->cap = 0; v->alloc = NULL; - v->gen++; v->epoch = 0; } @@ -1701,11 +1692,11 @@ int8_t flan_vec_clone(flan_vec *dst, flan_vec *src, flan_allocator *a, * 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 + * allocator 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 + * Raw_Map must be three words. This header already carries an allocator 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 @@ -1757,7 +1748,6 @@ typedef struct flan_map { int64_t len; int64_t log2cap; flan_allocator *alloc; - int64_t gen; int64_t epoch; } flan_map; @@ -2274,7 +2264,7 @@ static int8_t flan_map_grow(flan_map *m, int64_t want, int64_t ksize, 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; + fresh.alloc = a; fresh.epoch = (int64_t)a->epoch; if (!flan_map_alloc(&fresh, a, log2cap, ksize, vsize)) return 0; if (m->data) { @@ -2302,7 +2292,6 @@ static int8_t flan_map_grow(flan_map *m, int64_t want, int64_t ksize, 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; } @@ -2313,7 +2302,6 @@ int8_t flan_map_init(flan_map *m, flan_allocator *a, int64_t ksize, 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 @@ -2529,7 +2517,6 @@ void flan_map_free(flan_map *m, int64_t ksize, int64_t vsize, m->len = 0; m->log2cap = 0; m->alloc = NULL; - m->gen++; m->epoch = 0; } diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 8d7fe44..46baa50 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -762,7 +762,7 @@ let () = let dbg = Emit.program ~debug:true (Check.program (Parse.program (Reader.read_file "programs/vec.flan"))) in if not (contains dbg "name: \"Allocator\"") - || not (contains dbg "name: \"(Vec i32)\", size: 384") + || not (contains dbg "name: \"(Vec i32)\", size: 320") then begin incr failures; print_endline "FAIL debug info for Allocator and (Vec T)"