diff --git a/BUILT.md b/BUILT.md index 5f882f8..eb18060 100644 --- a/BUILT.md +++ b/BUILT.md @@ -1214,8 +1214,32 @@ on what it shows rather than on what it saves. **Four refusals, each by name and with its reason.** Three per slot — invented, not yet bound, and no printer for the type (a map, a function value, a type variable; the arm exists, no program the checker accepts has reached it yet, so it is written and untested) — and two whole frames: one belonging to a `C-x C-e` thunk, whose `Tast` the session does -not keep, and one whose slot count does not match the body this session holds, which is a frame running a body that has -been redefined since and where every slot index would be a guess. +not keep, and one whose *body* is not the body this session holds. + +**The superseded frame, and why a count could not find it.** Installing while stopped is deliberately allowed — it is +the fix-it-and-retry loop — so the frame on the stack and the body the session holds can be two bodies of one function. +A slot count catches a body that gained or lost a binding and nothing else; the case that matters is a **rename**, which +changes neither the count nor the types, and which would otherwise show every *new* name against the *old* body's +storage with nothing saying so. That is the confident wrong answer, in the one place someone is working out what went +wrong. + +So each `%fninfo` carries a **slot fingerprint**: `Emit.slot_fingerprint` over every slot's name together with the +spelling of its type, since either can change on its own. The frame carries the value for the body it was compiled from, +`flan_dev_frame_slotsig` reads it, the agent snapshots it with the rest of the frame and puts it on the backtrace line, +and `Dev.locals` recomputes it from the body it holds and compares. Different means refused by name — *this frame's body +was redefined since it was entered, so its names no longer describe its values* — rather than answered. The count check +stays in front of it because its message is the more specific one. + +Computed in `emit.ml` and read from there by `dev.ml`, so there is one definition of it and the two ends cannot drift. +It stays **off the wire**: `(:op "backtrace")` still answers four fields per frame, because a hash is not something an +editor can act on and the refusal says the fact in words instead. The bound is worth stating: it is a 30-bit hash, so a +collision is possible in principle, and it would reproduce exactly the silent wrong answer this catches — but only +between two *differing* bodies of a function whose qualified name has already matched, since `find_fn` is what gates the +comparison at all. + +The first build of this shipped the fingerprint into `%fninfo` and stopped there — no accessor, no wire field, no +comparison — and was left in the tree with its own test red. Four of five hand-offs missing looks exactly like one +hand-off dropping a number, which is what the note left behind said it was. **A `(Vec T)` shows as `` and a `(Ptr T)` as ``**, because that is what `render.ml` already does for them everywhere else: following a pointer a REPL was handed is not a safe thing to do on someone's behalf, and walking a diff --git a/NEXT.md b/NEXT.md index 5b0d339..13fb82d 100644 --- a/NEXT.md +++ b/NEXT.md @@ -410,11 +410,13 @@ forgotten. Agreed at the end of 2026-09-12. Ordered by priority, not by size. Items 1-3 and 5-6 want the compiler core and should run one lane at a time; item 4 is disjoint and runs alongside any of them. -1. **Fix the one failing test** — `the frame of a superseded body answered with the new body's names`. The slot - fingerprint that should refuse a frame whose body was redefined since it was entered does not fire, so a rename - keeping the same slot count and types shows new names against old values. The suite has been green all session apart - from this, and a red suite stops being a signal quickly. The handoff says to start by printing both sides of the - comparison in `Dev.locals`. +~~1. **Fix the one failing test** — `the frame of a superseded body answered with the new body's names`.~~ **Done**, and + the handoff's diagnosis was wrong. Nothing was dropping the number: the fingerprint was emitted into `%fninfo` and + never read back. `flan_dev.c` called the field `spare`, there was no accessor, the agent never snapshotted it, the + backtrace line never carried it, and `Dev.locals` compared slot counts and nothing else — four of the five + hand-offs were never written, and printing both sides of the comparison could not have found it because there was + no comparison. The mechanism was sound and stayed: it hashes slot *names* as well as types, so it does see a + rename. See BUILT.md, "Locals of a stopped frame". 2. **The colon-to-dot change.** Cheap, mechanical, ~284 sites across 45 files — and it must land **before** `Map`, or map literals and struct literals collide and both have to change instead of one. It gets more expensive every day @@ -1096,26 +1098,17 @@ tests assert on the reason, not just on the failure. ## Handoff: the shadow stack lane, stopped mid-repair Two commits landed and are green: the shadow stack with `(:op "backtrace")`, and `(:op "locals" :frame N)`. See -BUILT.md's two new sections for the design and the measurements. A third commit is **half-built and its own test is -red**, deliberately left that way rather than deleted. +BUILT.md's two new sections for the design and the measurements. A third commit was **half-built and its own test +left red** on purpose; **it is finished now** — see the struck item 1 above — and the rest of this section is kept +because the parts of it that were true are still worth having. -**What is broken, exactly.** `locals` compares the frame on the stack against the body this session holds, and the -comparison is not firing. Installing while stopped is deliberately allowed, so the two can be different bodies of one -function — and a redefinition that renames the locals while keeping their count and types shows every *new* name -against the *old* body's values, with nothing refused. `test_dev.ml`'s "the frame of a superseded body answered with -the new body's names" fails on exactly that, and reproducing it takes one run of that test. - -The fix that is in the tree and does not work yet: `Emit.slot_fingerprint` hashes each slot's name and the spelling of -its type; `emit_fn` stores it in the `flan_fninfo` the frame points at; `flan_dev_frame_slotsig` reads it; the agent -puts it on the `backtrace` line as a fifth field; `Dev.locals` compares it with `Emit.slot_fingerprint fn`. Every piece -is written and the refusal does not happen, so **one of those five hand-offs is dropping the number** — the next person -should print both sides of that comparison first, which is a two-line change in `Dev.locals`, rather than re-deriving -the design. The likeliest suspects in order: `Dev.backtrace`'s line parse silently falling through to `None` for the -new five-field line (it would drop the frame entirely, so probably not); `find_fn` handing back a stale `Tast.fn`; the -hash being computed over a `snames` array that the redefinition path fills in differently. - -Until it is fixed, `locals` is trustworthy for a frame whose body has not been redefined since it was entered — which -is every frame in a program that has not been edited while stopped — and silently wrong for one that has. +**What is broken, exactly — and this paragraph was wrong; kept for what it cost.** It said `locals` compares the +frame on the stack against the body this session holds and the comparison is not firing, that every piece of the +fingerprint was written, and that one of five hand-offs was dropping the number. Four of the five were never written at +all: `Emit.fninfo` stored the fingerprint and nothing else touched it. The first step it recommended — printing both +sides of the comparison in `Dev.locals` — could not have worked, because `Dev.locals` had no comparison to print. The +lesson is the ordinary one: a lane that stops mid-repair should say which pieces it *ran*, not which it believes it +wrote. **Not obvious from the diff.** Two things cost a day between them. The linked-list frame beat an array-with-a-stack- pointer on both benchmarks, which is the opposite of what the escaping-alloca argument predicts, and the measurement