From a13c2f9ec3d9fd7a601e10b2ba645fb3f8d15b7a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 15:52:57 +0700 Subject: [PATCH] The Map header mirror the collector reads is checked against flan_rt.c's, array key pairs cannot share a name, and a program holding only a Map of dyn sets up the collector --- TODO.org | 2 +- docs/BUILT.md | 4 ++-- lib/check.ml | 5 +++++ lib/emit.ml | 2 +- runtime/flan_dyn.c | 15 +++++++++++++++ runtime/flan_dyn.h | 4 ++++ runtime/flan_rt.c | 16 ++++++++++++++++ test/dyn_ops.c | 18 ++++++++++++++++++ 8 files changed, 62 insertions(+), 4 deletions(-) diff --git a/TODO.org b/TODO.org index 7e22ee01..d2c3d726 100644 --- a/TODO.org +++ b/TODO.org @@ -522,7 +522,7 @@ Func, Fnptr. CLOSED: [2026-09-25] Only a capturing =fn= that may outlive its frame gets a collector environment; one only called or passed down keeps its stack environment, as every handler does. -Capture stays by value, and a =Map= of function values is refused. Rules out a +Capture stays by value; a =Map= walks its values as a =Vec= does. Rules out a tag bit on the environment word and a heap environment for every closure. ** WAIT CFn and C's calling convention diff --git a/docs/BUILT.md b/docs/BUILT.md index 8aed28bc..3ded16d5 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -1265,8 +1265,8 @@ looked wrong. ### Proved by comparing output, never by reading bytes -`test/survey-x86.sh` builds each program in `test/programs`, the `x86-p*` probes among them, twice — once default, once -`--x86`, **with the same bounds-check setting on both sides** — runs both, and compares stdout, stderr and the exit +`test/survey-x86.sh` builds each program in `test/programs`, the `x86-p*` probes among them, twice — once through LLVM +at `-O0`, once `--x86`, **with the same bounds-check setting on both sides** — runs both, and compares stdout, stderr and the exit status. stderr is not a detail: every message the condition machinery produces goes there, each carrying a location this backend emits by hand as a `.rodata` label and a length in a register, and an exit status of 134 with the wrong text beside it is exactly the failure that reads as a match. diff --git a/lib/check.ml b/lib/check.ml index d20806f1..dcaa810d 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -3815,6 +3815,11 @@ and array_key_pair env loc n e = | _ -> '_') (Types.to_string aty) in + (* The mangle is many-to-one — [a+b] and [a_b] come out alike — so a digest + of the printed type, which is an identity, keeps two such keys apart. *) + let tag = + tag ^ "/" ^ String.sub (Digest.to_hex (Digest.string (Types.to_string aty))) 0 12 + in let hname = "map/hash/array/" ^ tag and ename = "map/eq/array/" ^ tag in let known name = List.exists (fun (f : Tast.fn) -> f.Tast.name = name) env.lifted diff --git a/lib/emit.ml b/lib/emit.ml index d5bf8184..edd3773a 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -5130,7 +5130,7 @@ let uses_dyn (p : Tast.program) = let rec carries seen (t : Types.t) = match t with | Types.Dyn -> true - | Types.Array (_, e) -> carries seen e + | Types.Array (_, e) | Types.Map (_, e) -> carries seen e | Types.Named n when not (List.mem n seen) -> (match Hashtbl.find_opt structs n with | Some st -> diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index 10f25c9d..ea488a33 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -269,6 +269,21 @@ typedef struct flan_dyn_map_hdr { #define DYN_MAP_ALIGN 64 #define DYN_MAP_FULL 0x80 +/* This mirror's numbers, compared against flan_rt.c's [flan_map_layout] by + * test/dyn_ops.c's "layout" mode. */ +void flan_dyn_map_hdr_layout(int64_t out[10]) { + out[0] = (int64_t)sizeof(flan_dyn_map_hdr); + out[1] = (int64_t)offsetof(flan_dyn_map_hdr, data); + out[2] = (int64_t)offsetof(flan_dyn_map_hdr, len); + out[3] = (int64_t)offsetof(flan_dyn_map_hdr, log2cap); + out[4] = (int64_t)offsetof(flan_dyn_map_hdr, alloc); + out[5] = (int64_t)offsetof(flan_dyn_map_hdr, epoch); + out[6] = DYN_MAP_HEAD; + out[7] = DYN_MAP_GROUP; + out[8] = DYN_MAP_ALIGN; + out[9] = DYN_MAP_FULL; +} + /* flan_allocator's prefix, far enough to read the one word a stale-container * check needs. The struct has more fields after [epoch]; this file never * touches them; and the alignment of a leading same-typed prefix is the same diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 38b110f1..7667650b 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -508,6 +508,10 @@ void flan_gc_set_floor(int64_t bytes); * otherwise does. */ void flan_dyn_vec_hdr_layout(int64_t out[6]); +/* flan_dyn.c's mirror of flan_map and of the block geometry the marker reads, + * compared against flan_rt.c's [flan_map_layout] the same way. */ +void flan_dyn_map_hdr_layout(int64_t out[10]); + #ifdef __cplusplus } #endif diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index b18f2455..2b52b9be 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -2825,6 +2825,22 @@ typedef struct flan_map { int64_t epoch; } flan_map; +/* The header's layout and the block geometry's constants, for the same check + * [flan_vec_layout] exists for: flan_dyn.c restates both to walk a map's full + * slots, and test/dyn_ops.c's "layout" mode compares the two. */ +void flan_map_layout(int64_t out[10]) { + out[0] = (int64_t)sizeof(flan_map); + out[1] = (int64_t)offsetof(flan_map, data); + out[2] = (int64_t)offsetof(flan_map, len); + out[3] = (int64_t)offsetof(flan_map, log2cap); + out[4] = (int64_t)offsetof(flan_map, alloc); + out[5] = (int64_t)offsetof(flan_map, epoch); + out[6] = FLAN_MAP_HEAD; + out[7] = FLAN_MAP_GROUP; + out[8] = FLAN_MAP_ALIGN; + out[9] = FLAN_CTRL_FULL; +} + /* ── Hashing ────────────────────────────────────────────────────────── * * FNV-1a over the bytes, then a final avalanche. FNV alone leaves the low bits diff --git a/test/dyn_ops.c b/test/dyn_ops.c index 12114fc6..f6c6fe23 100644 --- a/test/dyn_ops.c +++ b/test/dyn_ops.c @@ -61,6 +61,7 @@ void flan_rt_init(int32_t argc, char **argv); void flan_vec_free(void *v, int64_t size, int64_t align, const uint8_t *loc, int64_t loclen); void flan_vec_layout(int64_t out[6]); +void flan_map_layout(int64_t out[10]); static int failures; @@ -597,6 +598,23 @@ static void layout(void) { fail(msg); } } + { + /* The Map header and its block geometry, restated in flan_dyn.c so the + collector can walk a map's slots. Two copies, compared directly. */ + int64_t mrt[10], mdyn[10]; + static const char *const mnames[10] = + { "sizeof", "offset of data", "offset of len", "offset of log2cap", + "offset of alloc", "offset of epoch", "head bytes", "group", + "alignment", "full bit" }; + flan_map_layout(mrt); + flan_dyn_map_hdr_layout(mdyn); + for (i = 0; i < 10; i++) + if (mrt[i] != mdyn[i]) { + snprintf(msg, sizeof msg, "flan_map vs. flan_dyn_map_hdr's %s: %lld vs. %lld", + mnames[i], (long long)mrt[i], (long long)mdyn[i]); + fail(msg); + } + } printf(failures == 0 ? "layout ok\n" : "layout failed\n"); }