diff --git a/BUILT.md b/BUILT.md index 622629f..09b4d87 100644 --- a/BUILT.md +++ b/BUILT.md @@ -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 diff --git a/NEXT.md b/NEXT.md index c125d38..c5eadea 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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".