The snapshot was about two threads, and there are still two

Third on the list to delete, and nothing goes. "The compiler can read the
stopped frame's memory directly, so copying it is ceremony" is the right
instinct and the wrong diagnosis: the snapshot was never about two address
spaces.

A stopped program is not holding still. The break loop polls, flan_agent_poll
runs whatever was delivered, and a C-x C-e thunk is arbitrary Flan that pushes
and pops the one global restart list and the shadow stack while it runs. The
compiler is a thread beside it either way. Restart names are copied because
serving them off the live list hands the reader a pointer into a frame the
loop's own poll may already have popped; a pointer is meaningful to the
compiler now, and the frame it points into is no more alive for that. The
fingerprints have a sharper reason still, already written down: the module a
frame's description lives in can be unloaded before the comparison happens.

The generation stamp is about nested breaks. A thunk this loop runs can error,
push a break of its own, and reach chosen_ready first, claiming an index someone
chose from the outer list. Depth cannot tell those apart because a resumed
outer break and a new one reuse the number. One process changes none of that.

So of the three things the merge was expected to make deletable, one was. The
socket was transport. The result cap and the snapshot are concurrency, and were
only mistaken for transport because the socket was in front of them.

What the merge does unlock here is item 4, deliberately not touched:
flan_agent_frame_slot already hands back an address, and in one process the
compiler could read the value there instead of compiling a thunk to print it.
This commit is contained in:
Joseph Ferano 2026-09-12 22:31:36 +07:00
parent f63974142f
commit a3baa0b25a
2 changed files with 42 additions and 2 deletions

View File

@ -1110,6 +1110,40 @@ So it is a render budget, not a wire size, and it was only ever mistaken for one
Removing the bound is a redesign of the *read* — probe the length, allocate, re-read, validate the generation, retry —
and it belongs with moving the read to a frame boundary, which is the seqlock's own decision and its own lane.
#### The break snapshot is not marshalling either, and all of it stays
Third on the list, and the answer is no, with nothing left over. "The compiler can read the stopped frame's memory
directly, so copying it is now ceremony" is the right instinct and the wrong diagnosis: **the snapshot was never about
two address spaces. It is about two threads, and there are still two.**
A stopped program is not holding still. The break loop polls, `flan_agent_poll` runs whatever the compiler delivered,
and a `C-x C-e` thunk is arbitrary Flan — it pushes and pops the one global restart list and the shadow stack while it
runs. The compiler is a thread beside it either way. So every copy in `snap_push` has the same justification it had
before:
- **Restart names** are copied because serving them off the live list hands the reader a pointer into a frame the
break loop's own poll may already have popped. A pointer is meaningful to the compiler now; the frame it points into
is no more alive for that.
- **Frame names and locations** are copied from the held-still stack for the same reason, and the *fingerprints* have a
sharper one already written down: the module a frame's description lives in can be unloaded once a replacement is
installed, and the comparison happens after that.
- **The generation stamp** (`snap_gen`, `chosen_gen`) is about nested breaks, not about processes. A thunk this loop
runs can error, push a break of its own, and reach `chosen_ready` first — claiming an index someone chose from the
outer list. Depth cannot tell those apart, because an outer break resuming and a new one starting reuse the number. A
generation can. One process changes nothing about that.
The fixed caps go with it: the snapshot is taken **on the game thread**, so it cannot allocate, which is why
`SNAP_MAX`, `FRAME_MAX` and `FRAME_TEXT` are literals and why truncation is reported rather than avoided.
What the merge *does* unlock here is one thing and it is the next item: `flan_agent_frame_slot` already hands back the
address of a slot, and in one process the compiler could read the value at that address instead of compiling a render
thunk to print it. That is the render-thunk-per-inspection redesign — a different mechanism rather than a deletion, and
what makes "the inspector can retain a value" reachable. It is deliberately not done here.
**So of the three things the merge was expected to make deletable, one was.** The socket was transport and is gone; the
result cap and the snapshot are both concurrency, and they were only ever mistaken for transport because the socket was
the thing in front of them.
### The Emacs client
`emacs/flan-mode.el` derives from `prog-mode` with `lisp-mode`'s syntax table, which is most of the work: Flan is

10
NEXT.md
View File

@ -531,6 +531,9 @@ socket did not move. See "One process" in [`BUILT.md`](BUILT.md).
What it reopened is now being deleted one piece at a time, each with its own green run:
**One of the three was transport. The other two were concurrency, and were only mistaken for transport because the
socket was in front of them.**
1. ~~**The internal socket, the line protocol, and `Dev.deliver`/`result`/`ask`.**~~ **Done** — a delivery is a direct
call into the agent's verb table. Measured: the transport was ~50µs of a 23ms redefinition, so the end-to-end
number did not move. Code generation is 19 of the 22 milliseconds, which is the number any backend argument has to
@ -541,8 +544,11 @@ What it reopened is now being deleted one piece at a time, each with its own gre
calling `realloc`, and it would break the seqlock — which is a protocol about torn contents and assumes the address
it copies from does not move. Removing it is a redesign of the read, and belongs with moving the read to a frame
boundary.
3. **`flan_agent.c`'s snapshot copying and generation stamping.** The compiler can read the stopped frame's memory
directly.
3. ~~**`flan_agent.c`'s snapshot copying and generation stamping.**~~ **Refused, in full** — it was never about two
address spaces, it is about two threads, and there are still two. The break loop polls, a thunk it runs is
arbitrary Flan that pushes and pops the live restart list and the shadow stack, and the generation stamp is what
keeps a nested break from claiming a choice made against the outer one. A pointer is meaningful to the compiler
now; the frame it points into is no more alive for that.
4. **The render-thunk-per-inspection design for locals and globals.** A redesign rather than a deletion, and its own
lane: it is what unblocks "the inspector can retain a value".