Say what the fingerprint is for, and correct the note that guessed
BUILT.md's locals section said the second whole-frame refusal was a slot count mismatch. It is a fingerprint, and the paragraph now says why a count could not have done the job: the case it exists for is a rename, which changes neither the count nor the types. It also states the bound honestly — a 30-bit hash can collide, and a collision would reproduce exactly the wrong answer this catches, but only between two differing bodies of a function whose name already matched. NEXT.md's item 1 is struck, and the handoff paragraph that diagnosed this is marked wrong rather than deleted. It claimed every piece was written and one of five hand-offs was dropping the number; four were never written. The step it recommended first could not have found that, and a lane stopping mid-repair should say which pieces it ran rather than which it believes it wrote.
This commit is contained in:
parent
10b736f23e
commit
b5d7a6e45f
28
BUILT.md
28
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 `<vec>` and a `(Ptr T)` as `<ptr>`**, 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
|
||||
|
||||
41
NEXT.md
41
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user