Live disassembly, transient overlays, and a list of what is left

C-c C-a disassembles a named function, C-u C-c C-a shows its LLVM IR, and the
daemon keeps a name-to-origin table filled only when delivery answers ok, so it
knows which module owns a name after N reloads.

The honest part is the basis line. It cannot claim "installed now": the agent has
no verb that reports an address, the cell lives in the program's address space,
and eval-expr renders a pointer as <ptr>. So the reply says which of three things
is true - nothing delivered, delivered and queued, or delivered while stopped -
and prints it above the first instruction. The stopped case first read "not
installed yet", which was wrong: the commonest way to stop is to install a body
and have it error.

Overlays clear on the next command in that buffer, through a buffer-local
pre-command-hook installed with the overlay and removed with it. Not
post-command-hook, which fires at the end of the failing command and would clear
the overlay before redisplay.

NEXT.md gains a "Blocked and unfinished" section, which is the point of this
commit. Everything in it was found, decided or half-built this session and then
stopped, and each entry says what blocks it: the four memory questions the Odin
and Carp studies converged on, typed restarts, handler-case, six bugs with
repros, the mutation pass's remaining blind spots, four things that are one line
away, and three places the normative documents contradict the code.

Three drifted claims fixed while there. The file said "There is still no REPL.
Nothing does redefinition, dlopen, or nREPL" in a document that spends fifteen
sections describing exactly those; the raylib inventory said 29 calls against
164; the commit count said 34 against 154.
This commit is contained in:
Joseph Ferano 2026-09-12 04:13:01 +07:00
parent d336da65e5
commit 41a404c230

116
NEXT.md
View File

@ -2,7 +2,7 @@
## Start here — next session
**Branch `dev-loop`, 34 commits, working tree clean, `dune test` green.**
**Branch `dev-loop`, 154 commits, working tree clean, `dune test` green.**
The dev loop works end to end: `flan dev program.flan`, then `C-c C-c`, `C-x C-e` and `C-c C-r` in Emacs against the
running process. Conditions are two steps in of four.
@ -251,14 +251,7 @@ that calls an existing binding is unaffected.
34 51 68`, four separate bytes — a `Color` is *not* the little-endian reading of the packed integer, so an identity
would have passed a weaker test. That case is in the acceptance table, skipped if `libraylib` is not installed.
The bindings are 29 calls: window (`init-window`, `close-window`, `window-should-close?`, `set-target-fps`,
`set-trace-log-level`), keyboard (`key-pressed?`/`down?`/`released?`), mouse (`mouse-button-pressed?`/`down?`/
`released?`, `get-mouse-position`), `get-color`, drawing (`begin-drawing`, `end-drawing`, `draw-fps`,
`clear-background`, `draw-rectangle`), textures (`load-texture`, `texture-valid?`, `unload-texture`, `draw-texture`,
`draw-texture-v`, `draw-texture-ex`, `draw-texture-rec`), and the shapes texture and rectangle intersection
(`set-shapes-texture`, `get-shapes-texture`, `get-shapes-texture-rectangle`, `get-collision-rec`), plus the `Key`,
`MouseButton` and `TraceLogLevel` enums and the `Vector2`, `Color`, `Texture2D` and `Rectangle` structs. Adding one was
three lines — a `declare`, an `extern` prototype and a one-line wrapper — and is now one `declare-c`.
The bindings are 164 calls across thirteen structs: window, keyboard and mouse; drawing (rectangles, circles, lines, triangles, rings, ellipses, text); the eleven `collision-*` predicates; textures; the Image family; `Camera2D`; `RenderTexture2D`; the whole audio surface (device, `Wave`, `Sound`, `Music`); fonts and glyphs; and gamepads, touch and gestures — plus the `Key`, `MouseButton`, `TraceLogLevel`, `GamepadButton`, `GamepadAxis` and `Gesture` enums. Adding one is a single `declare-c` line; there is no C to write.
The texture calls are the first ones with no headless test, because loading one needs a GL context. What the acceptance
case does instead is pin the two new struct layouts using the only things raylib computes from those fields without a
@ -444,6 +437,110 @@ addressed. `test/programs/values.flan` pins this down.
Non-local exit is lowered explicitly: `return`, `some` and a failed bounds check are branches, never platform unwinding,
so wasm32 needs no exception proposal.
## Blocked and unfinished
Everything below was found, decided or half-built and then stopped. Each says what blocks it. Nothing here is a
vague intention — if it is listed, someone has already established it is real.
### Unblocked now, and ranked
1. **Allocators, then `Vec` and `Map`.** The critical path, and the only thing standing between this and writing a
game. **`Vec` does not need generics** — that was wrong and is worth un-learning: Odin's containers are compiler
builtins over a *type-erased* runtime (`base/runtime/dynamic_array_internal.odin`), where `$T` appears only in thin
wrappers producing `size_of`/`align_of` at the call site, and per-key hash and equality are compiler-emitted
procedures passed as a runtime argument (`src/llvm_backend.cpp`, `Map_Info`). That is exactly what
`spec-memory.md` already specifies. Before writing any of it, settle the four things the Odin and Carp studies
converged on, because all four are cheap now and expensive after:
- **When is storage released?** `spec-memory.md` never says. Odin's answer is `defer delete`, which Flan cannot
express — `defer` is function-scoped and refused in a `let`, a loop or a branch. Carp's answer is scope-end
frees, which it then could not reconcile with arenas and so has no allocator at all.
- **A `drop` hook** for a struct owning something that is not memory — a `Texture2D`, a socket, a file handle. Carp
shipped `delete` and then had to add a separate `drop` interface (`docs/Drop.md`). The hard part is ours alone:
what runs `drop` when the arena resets underneath the value.
- **`alignment`** appears nowhere in the design. Every Odin allocation carries it, and `#soa` and component-wise
fixed arrays want 16-byte alignment.
- **Allocation failure.** Unspecified. Odin returns an ignorable error, so a failed `append` silently appends
nothing. Flan has a better answer available for free: a `StorageExhausted` condition with a `retry` restart.
Decide which, because `push`/`put`/`clone`'s signatures depend on it.
2. **Typed restarts — `(use-value [v T] v)`.** The author's third TODO, and the most-wanted thing across every
comparative study. SBCL's report: restarts without parameters lose "the entire supply-a-value half of the standard
vocabulary", because `use-value` and `store-value` are the only two whose answer comes from outside the program.
Needs argument marshalling in `emit.ml` and §3's arity check in `check.ml`; both files are free now. The leverage
SBCL lacks: `eval` already compiles and runs an expression inside the live program, and the daemon already holds
the struct layouts, so "ask the human, type-check the answer, hand it over" is a short hop.
3. **`handler-case`.** Not a convenience — it is the fix for the loudest gotcha in `conditions.org`. A handler closes
over nothing *only because* a `handler-bind` clause runs at the signal point; a `handler-case` clause runs in the
establishing frame, which is ordinary in-frame code exactly like a `restart-case` clause. SBCL's is `handler-bind`
plus a transfer and nothing more (`src/code/error.lisp:196-268`). Every piece exists.
### Bugs found and not yet fixed
- **A restart chosen at a break inside a thunk is accepted, announced, and silently not taken.** Demonstrated.
`flan_reload_call` allocates its own `xfer` and discards it on return, so a transfer aimed at a frame below the
`flan_agent_poll` C frame unwinds only as far as the thunk. Three statements that the program will resume, none
true. Fix: record the restart-stack depth on entering `break_loop` and refuse any frame below it.
- **Restart names are served from a stack that is being mutated.** The stopped thread is not holding still — the break
loop runs `flan_agent_poll`, which runs arbitrary Flan, and every `restart-case` it enters pushes and pops the same
global list. `flan_restart_name` can return a pointer into a popped frame, which `send` then reads out of bounds.
Fix: publish an immutable snapshot when the break loop is entered.
- **The job ring has no fullness check**, and the comment describing its overflow is wrong. `publish` never consults
`tail`; past `QUEUE` entries it overwrites the slot the consumer is reading, and `job` is 24 non-atomic bytes.
Reachable from a program that goes a long time between `agent/poll` calls.
- **`flan_dev_result_get` is not the seqlock its comment claims** — it reads the generation first, then a non-atomic
length, then returns a bare pointer the caller sends later.
- Smaller: `exit(134)` from the break loop with the listener inside `dlopen`; a `dlopen` handle leaked when a module
has no installer.
- **`(A {:x 1})` on a union variant says "unknown struct A"** rather than the union refusal `check_struct` plainly
intends — `env` has no table of variant names. A diagnostics bug, not a backend death.
### Test blind spots, from a mutation pass
Sixty mutations, nineteen left the whole suite green. The severe cluster is closed (`cleanup.flan`,
`signedness.flan`); these are not:
- `Reach`'s walk of index expressions, `addr` places and `restart-case` clause bodies — each confirmed to prune a
function a valid program calls, so the build fails to link.
- `flan_dev_global`'s size-change guard — the layout-drift check, with no test that retypes a global across a reload.
- A local shadowing an imported name is qualified anyway.
- The 4K result cap and the registry overflow guard have **no coverage at all**, rather than a missing assertion.
- The reader accepts an unknown string escape; `+5` stops being a number.
- And a warning: a reader mutation makes the suite **hang** rather than fail. A green run is not the only outcome to
plan for in CI.
### One line away
- **`match` over enums.** Fully desugarable, wanted, and blocked only by `Ast.pattern` needing a keyword case, which
`load.ml` matches exhaustively.
- **DWARF for a redefinition module.** `Emit.redefinition` takes `~debug` and is tested; `Session.eval` does not pass
it. That also unblocks source interleaving in the disassembly buffer.
- **Let-bound locals print as `s0`, `s2`** under lldb. Parameters get their real names; `Tast` refers to the rest by
slot index and `Check` drops the names.
- **`Build.executable` returns only `out`**, so the daemon recovers the host `.ll` by recomputing `Build.workdir ()`.
### Deferred with a reason
- **Writing through a string literal** — see Sharp edges. Needs provenance, which is open decision #3.
- **`cstring` as a type.** Odin has no `string → cstring` conversion at all; it pays the same copy our shim already
makes. The one thing it buys is the *return* direction, and nothing in `vendor/raylib` returns a string.
- **`rune`.** Odin's is a 4-byte integer distinguished by a flag, so `i32` is the same thing. Non-ASCII text is
blocked on font loading, not on the string layer — and fonts are now bound.
- **Macro expansion.** The reader and the declaration are in. Running a macro means compiling it and `dlopen`ing it
into the compiler, which is the reload primitive pointed at ourselves — but a macro is `[Form] -> Form`, so `Form`
has to be a Flan union whose layout the compiler and the loaded macro agree on, and union *values* are milestone 6.
### Documents that contradict the code
- **`plan.org`'s jank #947 citation is wrong in its mechanism.** jank does not relink (it calls through vars, which
are already indirection cells) and never unloads (`remove_symbol` has no callers). The real cause was a
process-teardown race. We are safe from the repro — because we compile out of process, not because of cells. A
normative document citing the wrong mechanism protects the wrong invariant.
- **`plan.org` still lists open decision #7 as open** and the interpreter as a backend. It was settled the other way;
`NEXT.md` records the consequences as "already applied" to `plan.org`, and they never were.
- **nREPL's `eval` does carry `file`, `line` and `column`** — jank reads all three. The choice of s-expressions still
stands on its other grounds; the stated reason does not.
## Sharp edges
- **Writing through a string literal is undefined, and the two build modes
@ -1295,7 +1392,6 @@ is a subset of the dev path's machinery. Check `llc`'s major version against cla
IR text is currently absorbed by the driver behind `-Wno-override-module`, and a version mismatch surfaces as IR parse
errors.
**There is still no REPL.** Nothing does redefinition, `dlopen`, or nREPL. `build` is the only way to run code.
## Next — the REPL is the priority