diff --git a/NEXT.md b/NEXT.md index 0557113..3f583d1 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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