flan/TODO.org
Joseph Ferano 57fe91f303 Five records become one, and every citation lands somewhere
FIX.org, NEXT.md, DISCUSS.org, docs/DISCUSS.md and the session handoff at the
root are one TODO.org now: 293 entries under seven subsystem headings, each
carrying an org keyword that says where it stands. A DONE entry is a few lines
saying what was decided and what that rules out; the reasoning that would not
compress — the embedding spike and the four reports the hand-written x86
backend was built from — moved into docs/BUILT.md instead, and its entries
point there in one line.

Every entry was checked against the tree before it got a keyword, and the
prose was wrong in both directions. Things the deleted files called open were
built: the first-evaluation stall, main being redefinable, macro parameter
lists, the type-limit constants, the array constructors, the byte fills,
inc/dec, the discard's fontification, the Emacs buffers, rt_die's _exit, the
backtrace surface, and the acceptance failure that could print and still exit
zero. Things they called done were not: the backend reports' no-plan buckets
had gone stale in the other direction, the value-dependent defvar was
superseded rather than built, and macro-expansion source locations are on an
unmerged lane, so that entry is NEXT and names the branch.

Every comment that cited one of the five by name now cites a heading that
exists, in TODO.org or in docs/BUILT.md. The session reports under
docs/handoffs/ keep naming the files they worked on, because rewriting them
would falsify what those sessions did; each carries a note saying where the
content went.
2026-09-21 21:05:48 +07:00

97 KiB
Raw Blame History

Flan

Every decision, open question and known gap, one ** heading each under the subsystem it belongs to. A DONE entry says what was decided and what that rules out, in a few lines — never the argument and never the measurements. Where the reasoning will not fit, it is in docs/BUILT.md and the entry is one line pointing at it. A CANCELLED entry carries the one-line reason, because an idea rejected without a record is an idea that gets re-proposed.

Language surface

DONE Dynamic-first, and the dyn half of the language

CLOSED: [2026-09-20] An unannotated parameter or return is dyn: a NaN-boxed value over a mark-sweep heap, with --no-gc refusing residual dyn by location. The typed language is unchanged underneath it.

DONE The dynamic paths mimic Clojure, the static paths mimic Odin

CLOSED: [2026-09-19] A tiebreaker for features, not a licence for meanings. A question the dyn side has that Clojure has answered takes Clojure's answer; on the static side Odin's; for conditions, Common Lisp. Rules out using it to settle anything about what an operation computes.

DONE Arithmetic semantics do not fork across the two spaces

CLOSED: [2026-09-20] One operator, one meaning, both sides. / truncates toward zero and % follows the dividend everywhere. Rules out Clojure's flooring mod as a dyn-side-only behaviour of %; a flooring mod would be a second, separately named operation available to both.

DONE The return slot stays mandatory

CLOSED: [2026-09-19] A defn writes its return type, and unit is (). The parse ambiguity a missing slot would open is real, and () does not collapse into dyn. Rules out the optional return slot.

DONE def, defonce and defconst are the three forms

CLOSED: [2026-09-20] def is Common Lisp's defparameter and re-initialises on every run; defonce is CL's defvar under Clojure's name and keeps its value; defconst is folded. A def's initialiser is always lifted, zero and literal included, so editing the form and pressing C-c C-c reaches the same storage. Rules out the value-dependent defvar that was sketched — "re-initialise only if it would come out different" was never needed: a dev build emits a defconst as a mutable global, so a folded constant is tunable live without any dependency tracking.

DONE The third element of a defvar decides

CLOSED: [2026-09-20] A type there is the zeroed static global; a value there is a dyn global initialised from it. The same dispatch the parameter vector already makes. A symbol that is neither gets a refusal naming both readings.

DONE A macro has a parameter list, and [args] is the first argument

CLOSED: [2026-09-20] A defmacro takes real parameters with destructuring and & variadics, checked at the call before expansion; the whole argument list is spelled [& args]. Rules out a legacy mode where one bare parameter keeps the old meaning, which would make [a] and [a b] mean unrelated things.

CANCELLED Map destructuring in a macro's parameter list

CLOSED: [2026-09-20] A macro's argument is a Form, whose map case is a flat run of alternating forms with no field names, so the pattern cannot be translated without being given a new meaning and none of the candidates is obviously the one anyone wants. Vectors and & are the 95% case and are built.

DONE A macro fails at its call site in its own words

compile-error is a builtin reachable from a macro body, so a macro reports what is wrong where it was written rather than aborting the compile with no location. The prelude's own unless has not been converted and still answers a bare undefined name.

TODO gensym's counter restarts in a second module

The counter lives in the loaded module and a module is dlopened once per compiler process, so it is process-wide in practice — but the rounds already build more than one module for a program whose macros call macros. Seed it from the module's index.

TODO A quasiquote inside a quasiquote is refused

Nothing counts nesting levels — not the reader, deliberately, and not the desugaring. Only a macro that writes a macro wants one.

TODO until, cond, when and dotimes are still special forms in parse.ml

until and cond are free to move to the prelude whenever somebody wants them. when and dotimes are not: the prelude uses them 29 and 12 times, so moving either makes the prelude depend on the macro the macro module has to compile the prelude to get. cond also has a (cond a) refusal a macro cannot produce.

DONE Macros are imported from a package

The old refusal claimed collecting a package's macros needed a second import resolver at the Form level. It did not: the file being compiled is parsed before Load runs too, so Load.program takes forms and uses the one resolver that always existed.

DONE A prelude function may call a prelude macro

It was a cycle, not an ordering — the arity error came from the Check.program inside Macro.compile, where expansion is off. "A prelude macro may not call a macro" still stands and names itself when violated.

DONE The expander design: running a macro means dlopening it

There is no interpreter, so the compiler builds a shared object and dlopens it into itself. A call inside a quasiquote is output, not a compile-order dependency, and quasiquote is desugared before the walk, which is load-bearing.

DONE (comment …) is a prelude macro

CLOSED: [2026-09-20] A macro's arguments are raw Form and are never checked as expressions, so what is inside never has to be a program — it obeys the reader's rules only. #_ is the other spelling and they are not rivals: #_ discards one form and works in argument position.

DONE inc, dec, ++ and –

CLOSED: [2026-09-20] Four prelude macros, generic for free. A word for the pure pair and C's punctuation for the mutating pair. (++ PLACE) expands to (set PLACE (+ PLACE 1)), so a place with a side effect in it is evaluated twice — not fixable without a reference type the language does not have.

DONE Type-limit constants

CLOSED: [2026-09-20] i8..i64 and u8..u64 max and min, f32=/=f64 max, min-positive and epsilon, each carrying its type. Kebab and the type's own name — i32-max, not INT_MAX. There is no f32-min, because what a caller wants is the smallest positive value and the name has to say which.

DONE The two byte fills: (filled BYTE) and (dead-beef)

CLOSED: [2026-09-20] Two builtins rather than one — "why not both?" — spelled the way (zeroed) is, taking the type expected of them, so a place is filled with set and there is no place-taking form to learn beside it. A pattern's ascending bytes are its big-endian bytes, which is how the hex literal reads. Two and not one with a wider operand because the intrinsic takes a single repeated byte: the byte fill is one instruction and the four-byte pattern is a loop on both backends.

TODO There is no literal for an infinity or a NaN

lib/reader.ml has no literal for either, and float_repr prints inf and nan as words the reader will not read back. (/ 1.0 0.0) is the only route to an infinity, and the constant folder is integers only, so it cannot be a defconst. Closing it needs a reader literal or a float-capable folding pass.

TODO A u64 constant above 2^63 cannot be written in decimal

The reader reads a decimal integer literal as a signed 64-bit number; hex is read as a bit pattern and works. The same limit has a second face: a cast's argument is checked against the default type, so (u64 2935910691) is refused for not fitting in an i32.

DONE {.row .col} binds same-named locals

CLOSED: [2026-09-20] A new arm at the top of dmap in lib/parse.ml, before the pair arm. Works in let and nowhere else. The note claiming there was no grammar collision was wrong — a bare dotted symbol in head position already parsed as something else and failed later at the use.

DONE An empty body where a body is optional

CLOSED: [2026-09-20] (when test), (fn []) and a defn returning () with no body all parse. Relaxing fn opened a hole the checker had to close — an empty fn body at a non-unit want is refused, because without that the call read a return value nothing wrote.

CANCELLED () as a unit value in expression position

CLOSED: [2026-09-20] () stays the type-position spelling of Unit and has no value-position meaning. Empty forms doing the right thing covers the need that made it look attractive, and () stays unspoken-for in case the language grows lists.

CANCELLED // for forced truncating division

/ on two integers already truncates toward zero, and casting a float division truncates too, so (i32 (/ a b)) is the spelling. Python's // is floor division, which is a different operation from the one that was wanted.

DONE builtin/ is a reserved qualifier

CLOSED: [2026-09-20] builtin/length is the builtin whatever else the file has decided length means, and it is legal whether or not anything is shadowed. It wears the package qualifier's spelling deliberately, because builtin is reserved rather than resolved — every other qualifier in a finished program comes from an import alias.

DONE println is variadic

CLOSED: [2026-09-20] Clojure's semantics — every argument in order, one space between each pair, println ends the line, (println) is the newline alone. Typed and dyn arguments mix in one call because each gets its own printer and both share stdio's buffer.

DONE The six comparisons take two operands or more

CLOSED: [2026-09-21] < <= > >= and = chain adjacently; != asks about every pair, Common Lisp's /. Asking whether a sequence increases is a question about neighbours; asking whether values are all different is a question about the set. (< x) stays refused, and % and the shifts stay two operands.

DONE dotimes counts from where you say

CLOSED: [2026-09-21] Three arities, with stop exclusive in every one, so (dotimes [i 0 n]) is (dotimes [i n]) — one rule, not two. A negative step counts down; a literal step of zero is refused at compile time and a computed zero runs no times at all, which is terminating and deterministic.

DONE slice takes three arities, and at/slice reach a string

CLOSED: [2026-09-20] (slice a) and (slice a n) are written out into the three-argument form. A string slice answers a string rather than a [u8], because a byte slice is writable-looking and these bytes are not the program's to write.

DONE One slice; as-slice is gone

CLOSED: [2026-09-21] The input type already determines the semantics completely, so the second name expressed no choice, and it warned at the moment nothing is wrong — the danger arrives at the push. A Vec a call returned is accepted where an array a call returned is refused: one dangles and one only leaks, and leaking is defined behaviour here.

TODO (clone slice) as the general spelling of what (bytes s) does

Not built because of the who-frees question bytes answers by leaning on free-all and arena-destroy. flan_bytes_dup is already the lowering, so if slices grow a clone the two should share it.

DONE The count is length, and len is a name a program can have

CLOSED: [2026-09-21] One arm in the checker and one row in the builtin table. A call to an undefined len is refused after every table and after the shadowing guard, so a program with its own len never sees the refusal.

CANCELLED Counting abandonments

CLOSED: [2026-09-21] An abandoned verb was drafted and dropped. The wait already ends when the thunk breaks and the restart's own reply says what taking it did; a third telling read by nobody is how a wire grows a verb whose answer drifts from what happened.

DONE The randomness surface

CLOSED: [2026-09-21] (rand-int), (rand), (rand-bool), (rand-int-range lo hi), (rand-float-range lo hi). The generator is PCG-RXS-M-XS 64, so every call costs one draw and a seeded run is reproducible — but the permutation is a bijection of the state, so someone holding one result can predict the rest. Fine for a grid, not for a key.

DONE A value-producing array constructor

CLOSED: [2026-09-20] (array-fill [n ...] v) and (array-gen [n ...] f) build the whole array as a value, so one can be a def's initialiser. Ranks are spelled flat; there is no nested bracket syntax. Row-major order is a promise, and the fill value and the generator are each evaluated once, before any loop runs.

DONE (array 4 T) is the zeroed array constructor

A let binding takes no type, so (let [pts [4 rl/Vector2]] ...) reads the bracket as a two-element array literal. (zeroed [4 rl/Vector2]) was proposed first and rejected on how it reads — unambiguous to the parser, still looks like a two-element vector to a person.

DONE A defining form owns what happens on a re-run

CLOSED: [2026-09-20] Each computed global's initialiser guards itself with its own flag, per global rather than per startup function, and the daemon has no policy about globals at all. Dev builds only; a release build's output is byte-identical.

DONE defenum autoincrement

CLOSED: [2026-09-17] C's rule: a member with no value is the previous plus one, and the first is zero. An explicitly written duplicate is an intended alias and allowed; one produced by autoincrement walking into another member's value is refused, naming both.

DONE Every mapped raylib enum carries a Flan-side prefix

CLOSED: [2026-09-21] An optional third column on the enum line in vendor/raylib/bindings, stripped before the C prefix is applied. A reading choice and not a collision fix — a keyword resolves against the expected type and against nothing else, so two enums could always share a member spelling. What the prefix buys is the call site read on its own.

TODO match over enums

Fully desugarable and wanted, blocked only on Ast.pattern needing a keyword case.

DONE defdata is the tagged sum, defunion is C's untagged one

CLOSED: [2026-09-17] The old tagged defunion is renamed defdata; defunion becomes the C-style untagged one, serving the FFI and type punning, and is verified against the header where one exists.

DONE A defdata carries its tag, and case order is part of the contract

Tags are declaration order from zero, so a zeroed value is the first declared case. Option is a two-case sum wearing a special coat, so the existing arms grew a second subject rather than a second path. A non-exhaustive match is refused, never defaulted.

DONE An imported defdata keeps working across a package boundary

Both the tagged and the untagged form import now. It was never a blocker for Form, because the prelude is parsed and prepended into the same flat namespace before collection runs.

TODO (Result T E) and try

Another sum type, queued after sum values landed, and still refused by name. docs/PORTING.md records that it has no customer in the game's code.

CANCELLED (Handle T) and the pool

CLOSED: [2026-09-18] Built, then removed. Two containers are enough — a generational slab is a library over a Vec — and everything copies as its header once move-only is gone. The (Ptr T)-from-resolve hazard and the pool-as-enumeration argument for managed classes go with it.

CANCELLED drop, and unwind-protect with it

CLOSED: [2026-09-17] drop runs code somewhere the reader is not looking, which is the C++ behaviour the author does not want; it would not cover the motivating case, since Image and Texture2D are raylib's types; and its one real advantage, cascading through a container, was the case Handle made rare. with-cleanup=/=unwind-protect was put and rejected as awkward with several resources. defer is the answer, and the raylib begin/end pairs that seemed to motivate it are a macro problem.

DONE defer may be written in a let

A let at the top level of a function body has exactly the function's extent, so a defer in one always registers. A loop body and a branch are still refused by name: defer is a compile-time construct with the cleanup copied into every exit path, so "maybe registered" is not expressible.

DONE edn reads into a struct and answers a dynamic value

CLOSED: [2026-09-17] Two projects, not one, and the dynamic half goes through an allocator rather than through a teardown operation. (edn/read bytes) answers a dyn — the separate Value union it was first written against is gone. Rules out destructors and finalizers: free-all takes the region.

DONE A data file's struct is derived while the program is compiled

The typed half arrived as a provider macro, (edn/defedn Name "path.edn") and defjson beside it, rather than as the (read-edn T bytes) spelling it was asked for. A macro may read a file at the path the call site is written at, and there is no run-time type information to do it any other way.

DONE Assets are embedded at compile time

CLOSED: [2026-09-12] Odin's answer. (embed "p") is a compiler feature, so it needs no build flags, no linker arguments and no per-target packaging, and it works identically on desktop and web. Rules out a linker-flag or per-target packaging answer, which a single file could not have used anyway — Load gives link flags only to directory packages.

DONE slurp and barf are the file surface

CLOSED: [2026-09-12] (slurp path) reads a whole file into a (Vec u8); it had to wait for an allocator, because the length is not known until the file is read.

DONE Writing a file is desktop-only and signals on web

CLOSED: [2026-09-12] Flan has no conditional compilation, so "isolate this to desktop" is not expressible and a build-time refusal would be unusable. barf on web signals FileError under a restart and the program decides. Rules out both the silent no-op — which is how a save file disappears with nothing said — and the build-time refusal.

TODO Conditions get a parent link, not class inheritance

A condition type may name a parent where it is declared, and handler matching walks that static chain. It buys the hierarchy conditions most lack — a catch-all "any file error" handler — at compile-time cost only. Rules out the class answer: a class condition allocates at the signal site, inverts the lifetime rule, and lets a layout change under a standing handler frame. Not built.

TODO Can a condition be a class?

Unanswered, and the shape that probably wins is both — a struct condition stays what it is, a class condition allocates and matches by walking its class chain. Three costs, one serious: signalling would allocate on the failure path.

DONE handler-case

CLOSED: [2026-09-19] (handler-case BODY [(T [c] ...)]) — body first, clauses after. It is a handler-bind whose clause invokes a restart the form established around itself, so no backend work was needed. A clause runs at the form, which is why it sees the establishing function's locals where a handler-bind clause cannot.

DONE A restart that abandons the evaluation

CLOSED: [2026-09-21] An expression that signalled used to offer an empty restart list, leaving abort, which takes the compiler, the session and the game. Abandoning drops the expression; it does not undo it, and every surface says so. At a trap there is no transfer channel, so nothing can be abandoned, and that is correct.

TODO A restart-case clause has no report string

The field is cheap and the accessor is cheap, but the only consumer is the break loop's listing, so it would ship as a field nothing read. It belongs with the listing work.

WAIT find-restart and compute-restarts

Blocked on a type, not on effort: the spec gives them (Option Restart) and a list, and there is no Restart type and no list type to return one in. The minibuffer prompt never needed them — it reads the snapshot over the agent's socket.

DONE into fuses at compile time because it is a macro

Not transducers and not Rust's iterators, both of which compose at runtime and need function values and allocation. A macro writes the call straight into the loop body, so the function name is syntax and never a value. What it gives up is building a transformation at runtime, which is close to useless in a game. Reductions deliberately do not share the form.

DONE loop and recur

Tail position is a permission that is withdrawn rather than a pre-pass. loop is itself a barrier for break and continue, because a loop answers with the value of its body so a jump out has no value to give — which is also why it takes no label. Neither backend learned anything: a loop is a let, a While on true, and two jumps. Mutual recursion still needs real tail calls and is out of scope.

DONE break and continue, with loop labels

Labels are Odin's, in the head position, where a keyword is unambiguous because a loop condition is never one. It is not a goto — control can only leave a loop it is already inside.

DONE The second tier of the standard library

The prelude had 44 allocation-free functions because there was nothing to allocate from. append, concat, join, split, format-f64, atan2, pow and clamp are the tier that returns new storage.

DONE The Clojure patterns deliberately not copied

Four rules. The thing being operated on comes first, everywhere, so there is no second threading macro. A membership test says which thing it tests. A ? name answers yes or no and a finder answers the thing or nothing. Composition reads left to right. Rules out inheriting Clojure's contains?, some and comp shapes.

DONE A pointer from C needs a length before it can be indexed

CLOSED: [2026-09-13] (slice-from-ptr p n): the caller states the length and owns being right about it. The alternative weighed — a per-binding declaration naming which argument carries the count — cannot reach a count that is a sibling field. No marker on the name; ptr is the marker, it owns nothing, and free refuses it.

TODO A string cannot be returned from C

A string crosses as a parameter only — a C function that returns one returns something Flan has no owner for. It is what makes GetGamepadName unbindable, and the same rule refuses TextFormat, which is variadic and so has no honest signature either.

TODO Model, Mesh and FilePathList want a defstruct, and a callback wants the other direction

Ray and BoundingBox have their defstructs now. FilePathList is a char** and blocks the drop-files example; Model and Mesh are ordinary widening. Function-pointer parameters — SetTraceLogCallback, the audio stream processors — are the callback direction of the FFI and nothing has needed it yet.

DONE raymath is written in Flan, because static inline has no symbol

CLOSED: [2026-09-13] Clamp, Vector2Add and the rest exist only in the header, so declare-c has nothing to name. raymath's semantics exactly, including normalize's zero-length guard. The C-shim alternative was rejected: it buys identical arithmetic for a compilation unit in the build and a second place raylib's semantics are written down.

TODO rlgl's matrix stack is unbound

core_2d_camera_mouse_zoom is skipped for want of it — a different reason from the raymath one.

CANCELLED cstring as a type

Odin has no string-to-cstring conversion at all; it pays the same copy the shim already makes. The one thing it buys is the return direction, and nothing in vendor/raylib returns a string.

CANCELLED rune as a type

Odin's is a 4-byte integer distinguished by a flag, so i32 is the same thing. Non-ASCII text was blocked on font loading, not on the string layer, and fonts are bound.

DONE Two function types: Fn captures, CFn cannot

CLOSED: [2026-09-21] (Fn [T ...] R) is {code, env}, two words; (CFn [T ...] R) is the bare address, one. Capture is by value into a stack environment, non-escaping only, and an ordinary defn declares no environment and is byte-for-byte what it was. The static side does not pay for the dynamic side. Rejected names: Closure, Proc, Fun, Func, Fnptr.

NEXT Escaping closures, allocated on the GC side

The second half of "do both". What changes is where the environment points — a frame slot today, a collector allocation then — and the escape check goes away with it, along with the refusals on returning, storing, pointing at and pushing a capturing value. Two things for it to know: a widening thunk's environment holds a code pointer rather than a GC object, and capturing a dyn stays refused until a synthesised environment has a descriptor.

TODO CFn and C's calling convention

A Flan function's signature ends with the transfer channel and a C caller knows nothing about one, so a CFn is not a C callback today. Under a future --no-conditions flag a CFn signature could drop the channel and reach C's exact convention, which is the direction the author is interested in.

DONE defclass is a named dyn map with a shape tag

CLOSED: [2026-09-20] An instance is a dyn map with its class in the object header, so get, put, has-key? and length need no new operation. CLOS dispatch and Clojure's arbitrary dispatch are one mechanism: a class dispatcher is the shape tag of the first argument used as the dispatch function. Method bodies are inlined into one dispatcher function, so a generic is one top-level name and one cell — adding a method to a running program is an ordinary redefinition.

CANCELLED Class features deferred, each with its reason

CLOSED: [2026-09-20] Inheritance, multi-argument dispatch, :before=/:after=/=:around= and call-next-method, named-slot construction, unknown-slot checking, computed dispatch values. With single dispatch on literal values there is no specificity question, and inheritance or multiple dispatch would create one. Unknown-slot checking needs class-typed tracking the dyn side deliberately does not have.

TODO update-instance-for-redefined-class, the user hook

Left out of v1 because name matching is the half that makes redefinition usable and the hook is what makes it expressive. The obvious spelling is a generic riding the dispatch that exists, and the migration already computes both the added and the discarded lists. Rolling a failed migration back becomes a real question the day this lands.

DONE The module system stays directory-as-package

Several files in one directory are one module; a loose file is a module of one, and no package line is required or accepted. Confirmed against Odin, which requires the declaration despite the same rule, so the line buys only the ability to disagree with the directory name. Acyclic imports are kept deliberately — a definite package order is what the macro expander needs.

DONE A name imported through a package keeps the inner alias

If area/ imports shape, the type is shape/Box in the finished program and never area/shape/Box. Forced rather than chosen: a directory reached along two routes must arrive under one set of names or the checker sees every declaration twice. The price is that the same directory under two aliases is refused, naming both.

TODO A package cannot mark a name private

rl/get-color-raw is callable from outside its package. The refusal machinery takes a second rule in one line; the blocker is that there is no way for a package to say a name is private, and adding one is a parser change the author has to choose a spelling for. Every lane has skipped it for that reason.

CANCELLED A struct version word, so a redefined layout keeps working

CLOSED: [2026-09-20] Dropped rather than deferred. A shape still being discovered lives on the dyn side as a defclass; a typed defstruct is a commitment to a layout, and changing a commitment restarts the process. SBCL's push-through-and-invalidate is cheap only because its instances carry headers. docs/SBCL-REDEFINITION-NOTES.md is the reading behind it.

WAIT An F#-ish indentation surface beside s-expressions

Deferred 2026-09-19 with no spike queued, blocked on evidence: the author will write imperative Flan as it stands and see whether the parens still grate. If it is ever built, it is one AST with the existing forms unchanged and a second reader in front. Rules out Parinfer, wisp and sweet-expressions, and a simplified in-paren syntax — all thin the parens without removing them.

TODO The shims in sand.flan can go

sand.flan defines dyn->f64 and dyn->u32, one-line functions whose only job is that their parameter slot unboxes. Every call site can write (f64 d) and (u32 d) now. The author's file, not a lane's to edit.

DONE sand.flan's game-data.edn initialiser no longer aborts the headless import

A global initialiser that read a file at startup failed wherever the working directory was not the author's. It is wrapped in a handler-case with a FileError clause; away from that directory the data reads as nil.

TODO A declared name may carry the $ sigil

(defn $foo [x i32] i32 ...) is accepted and ($foo 3) calls it; so is (defstruct $S [a i32]), whose type can then be written nowhere. The character is reserved in every type position and in no name. Refusing it in a declared name would close it properly, and that is a decision about the spelling.

Checker

DONE The ownership flow analysis is repealed

CLOSED: [2026-09-18] Static use-after-move and double-free checking is gone; types, allocators and the dev build's generation checks are the net instead. An unsound checker is worse than none, because it is believed. Rules out a borrow checker returning as anything but an additive pass. docs/BUILT.md carries the shape of what replaced it.

DONE Move-only is gone with it

CLOSED: [2026-09-18] Everything copies as its header, the copyable? predicate is off the list, and the owning-field refusals on structs and sums are lifted. The region rule stands. The earlier ruling that a move-only global's lifetime is the process's — so reading one is always a borrow — has no subject left.

DONE Implicit numeric widening is legal; narrowing stays a hard error

CLOSED: [2026-09-20] A conversion is admitted exactly when no value of the source can come out as a different number. No second type-checking mode and no flag, which was the objection to the -Wconversion middle ground that was asked for. Integer into float is exact-only, containers are invariant, and dyn is not in the lattice.

DONE abs is one generic, and a bound joins to the wider type

CLOSED: [2026-09-20] Numeric scalars bound to one type variable resolve to the join of them all, which walks back the same day's "widening does not cross a generic binding". A joinless pair is deferred and re-asked against the final binding, which is what makes acceptance order-independent. integer? exists because numeric? admits floats, where the branch spelling of abs hands back a negative zero.

DONE A conversion is legal at a bounded variable when it is legal at every type the bound admits

CLOSED: [2026-09-21] A machine-type target needs numeric?; an enum target needs integer?; ordered?, equal? and hashable? admit nothing. A predicate gates an operation by what it claims, not by the set it happens to denote this week — which is why ordered? is refused even though every type it admits today converts.

TODO There is now no generic enum to integer conversion

Recorded as a loss. The one spelling that worked did so by not asking about the operand at all, so removing it was still right. enum? is the eventual answer — it would entail ordered? and equal? and not numeric?, so the cast rule becomes a disjunction and the refusal has to name whichever the reader meant. Each part of that is a decision and the author has not been asked.

TODO The Ptr and union arms of the fill boundary are relaxable

What may be byte-filled is numbers, and structs and fixed arrays of numbers. A Ptr is refused so the rule stays one sentence, and an untagged union because the walk goes over a struct's fields rather than a union's members. Both are named in the decision as the arms to relax first if it is reopened, and a poisoned pointer is arguably the useful case.

TODO A compound constant expression at a bounded type variable

(+ x (+ 1 2)) at a bounded variable is refused where (+ x 3) works — the literal arm admits a bare constant and nothing folds the compound first. Walk-backable, so it waits until a body actually wants it.

DONE Generics by monomorphisation, checked abstractly, with where predicates

CLOSED: [2026-09-13] A where clause tells the abstract pass what it may assume, so the body checks at the definition and the call stays (sort xs). Not a type class — a predicate carries nothing and gates a builtin the compiler already has. The fork that said an unconstrained + over a type variable must be rejected turned out to be false, and it is not what Odin does. The five predicates are ordered?, equal?, hashable?, numeric? and integer?.

DONE A type variable takes a $ sigil

CLOSED: [2026-09-13] $t binds in a parameter vector and bare t reads it. Three reasons, against plan.org's "lowercase names are variables, capitalised are concrete": there is no binding site without a sigil; introducing one is invisible, so a mistyped type made a function more permissive; and [n t] gave absence opposite meanings on the two sides of the bracket.

DONE A type variable may be written with its sigil at a use

CLOSED: [2026-09-21] (vec-new $t), (map-new $k $v) and ($t x) work; every type position already did. The feature was specified correctly and three membership tests asked about the name as written, where the tables are keyed on the bare name.

DONE Milestone 5 was mostly already there

CLOSED: [2026-09-20] What the lane added was a written integer zero standing where a numeric?-bounded variable stands — legal because every type numeric? admits is an integer or a float — the widening boundary, and a refusal for instantiating a type variable at dyn, which names defgeneric=/=defmethod as the other spelling. A float literal is still refused at a numeric? variable, since the predicate covers both halves.

DONE hashable? gates the type and not the operations

CLOSED: [2026-09-13] put, get, has-key?, reserve, clone and map-remove are deferred to the instantiation, joining print and println. The membership rule is not a headcount: either the operation cannot fail after substituting, or a declared predicate gives its failure somewhere to land. A generic that does not declare the predicate gets no deferral.

DONE The runaway instantiation cap names the chain

CLOSED: [2026-09-21] A structural occurs-check stops an instantiation that asks for a copy of itself, and the refusal prints the chain of instantiations that got there rather than the depth it gave up at. The bare depth number is a backstop that also prints the chain. Before any of it, the compiler hung rather than failed, which wedges C-c C-c with nothing to show.

TODO Generic types

(defstruct Pair [a $t b $t]) cannot be spelled, and neither can a length parameter. Types.Named is a bare string with no room for parameters; giving it some changes the type, the layout calculator, both backends, the renderer and the DWARF path. Same price for one as for both. Decided and unblocked, deliberately not started — it is a language feature under a freeze, and it was stopped once already for that reason. The motivating case is Odin's Small_Array: a fixed-capacity array with a count and no allocation.

TODO A value predicate over a length parameter

Odin's where N > 0= is a predicate over a value, not a type, and a where clause here admits nothing but type predicates. Whether it should take value predicates over a length parameter deserves answering deliberately rather than falling out of the implementation.

TODO "In instantiation of" notes

A refusal inside a copy points at the generic's source with no note naming the call site that asked for that type. The data is there — instantiation_origin exists and the session already uses it — and wiring it into every failure under an instantiation is a lane of its own.

TODO Generics across a real compilation-unit boundary

It works today because Load flattens imports before checking. A package boundary that ever becomes a real unit boundary needs the generic's body to cross it, which separate compilation cannot do — which is why C++ puts templates in headers.

DONE Collapsing the prelude buys 27 to 15, not 27 to 6

CLOSED: [2026-09-13] The honest number. sum-* would widen into a type-level function, which is a constraint system or an associated type; append-i64 and append-f64 are two different primitives and choosing between them per instantiation is compile-time overloading. Five do not collapse and should not.

DONE A bare {.field v} takes its type from the position it stands in

CLOSED: [2026-09-20] The refusal moved out of the parser, where it could not see the enclosing defn's return type, into the checker, which reads the type name off the expectation. Refused with no want, at a dyn want and at a non-struct want. A dyn want keeps the dyn map literal — a .field-keyed brace is not being given a second meaning.

DONE (Cell 1 2) is positional, and its arity is exact

CLOSED: [2026-09-20] Positional construction gives every field or it is refused. Not a retreat from zero-is-initialisation: a positional list cannot say which field it left out, and which field a short list omits depends on a declaration order the author is free to change. The field-reorder hazard is accepted as the price, with refactoring tooling named as the eventual answer.

DONE Shadowing a builtin is legal, and the definition wins

CLOSED: [2026-09-20] Clojure's model: allow shadowing, but warn. Builtin-wins had never been a rule — it was the implementation trying the builtin arms first. The shadow reaches exactly the file the definition was written in, decided by the file and not by the enclosing function's name, because a global initialiser has no enclosing name but does have a file.

DONE int and float are builtin aliases

CLOSED: [2026-09-20] Exactly those two, spelled as machine types rather than prelude aliases because the cast check does not look in the alias table and (int x) had to have a reading. Every message still says i32. integer, long, double, uint and str keep the teaching refusal, and any other unrecognised lowercase name is still a type variable.

DONE A defconst is a compiler const

CLOSED: [2026-09-20] A defconst's initialiser has to be a compile-time constant, and the refusal is the checker's so both backends refuse the same program. Integer arithmetic is folded before the check sees it and the folder is integers only, so (defconst half f64 (/ 1.0 2.0)) is refused.

DONE Container globals start zeroed, and a defconst container is refused

CLOSED: [2026-09-18] A global initialiser is a compile-time constant, a container's only constant is the empty one, and a constant is not an assignable place.

DONE A numeric cast opens a dyn box

CLOSED: [2026-09-20] Every numeric cast takes a dyn operand. The same kind unboxes; a cross kind coerces with a once-per-site warning; a non-number traps. The cross-kind case coerces rather than trapping, which overrides the tempting rule of matching the parameter boundary. It is lowered as a branch into two ordinary casts so nothing has a second opinion about range and NaN, which is what keeps the two backends in step.

DONE nil crosses at (Option T) and nowhere else

CLOSED: [2026-09-20] nil and None are the same value at an (Option T) boundary. At a bare T it is a compile-time refusal where the checker can see it and a run-time trap where it cannot. (Some nil) and (Option (Option T)) are unconstructible.

DONE Typed = and != reach strings

CLOSED: [2026-09-20] Bytewise, with length and same-pointer fast paths on both backends. Equality only — ordering a string needs a collation nobody has chosen.

DONE dyn truthiness in if, when, cond, and, or, not and while

CLOSED: [2026-09-20] nil and false are false and everything else is true, on the dyn side only; typed conditions stay strict bool. and and or hand back the operand that decided them, Clojure's rule, through a desugaring that evaluates each test once.

TODO A bool arm and a dyn arm joining as dyn

With both arms of a desugared and=/=or holding real values, a non-bool dyn on the losing side meets the strict bool boundary and traps — (or false (box "s")) is the case. Whether a bool arm and a dyn arm should join as dyn is the author's call and is not settled.

TODO A truthiness failure re-runs the whole failing subtree

The retry exists to keep a refused literal's message unchanged and re-runs the subtree rather than the leaf, which is exponential in nested not depth on a program that does not type-check. Moot for anything that compiles; only the daemon's half-typed recompiles could feel it. A cheaper retry was tried and shelved because it changes which literal gets the nicer message.

TODO and's last operand gets a misdirected caret

(println (and true true (vec-new i32))) puts the caret on the second true. The last operand of an and is the then arm and the then arm is typed first, so the mismatch is blamed on the else arm, which carries the previous operand's location. The fix is preferring the arm that is not a compiler temp when deciding whom to blame. Three others were considered and rejected: relabelling the else arm reads backwards, a bool sentinel reverts the or fix, and inverting the condition costs a not per operand.

TODO Signature pairing's cold-rebuild edge

Whether a parameter vector reads as one annotated parameter or two dyn ones depends on what type names exist, so adding a type can silently re-pair an existing signature between compiles. A changed-pairing warning was proposed and not queued.

DONE A typed container crosses into dyn as a view, and only from permanent storage

CLOSED: [2026-09-20] The descriptor is pointer, length and element type — a slice plus the piece a slice is missing. A Vec view holds the address of the Vec's own header and reads pointer and length live, so a reallocating push cannot go stale. Elements are i64, f64 and bool only. Rules out a heap-held header and anything behind a (Ptr T).

DONE A view of a Vec goes stale at the push, and the warning is at the push

CLOSED: [2026-09-21] What was built is the sentence, not a diagnostic. docs/BUILT.md beside the Vec surface table, and spec-memory.md under Borrowing, carry it.

CANCELLED A live view at the push, detected cheaply

CLOSED: [2026-09-21] Investigated and not built. (reserve v 100) followed by a view and a push is correct code, so any per-push flag is a false positive by the language's own semantics; the refined version needs liveness across control flow, which is the flow tracking that was repealed.

TODO Catching a use-after-release statically

Open, and for the first time with evidence available: the epoch trap is built, and there is a Vec to write real arena programs with, so whether the escapes that actually occur are lexical can now be answered. The next thing to look at, not the next thing to build.

TODO A fixed array of structs or of strings is not a map key

Refused by name, narrower than the spec's key set; a struct holding the array works. It needs the per-element walk a struct key gets, driven by a loop rather than a field list.

TODO map-keys and map-values cannot be prelude functions

Iteration is built; the remaining refusal is generics. A defn has to name its types and (defn map-keys [m (Map K V)] (Vec K)) has no K. The loop is three lines at the call site, where K is known.

TODO (vec-new [u8]) is refused

The element type must be a bare symbol naming a type, so a (Vec [u8]) can only be made where the context names it. The fix is letting it take a type expression — the same parser that already reads [u8] in a parameter list.

TODO An array literal cannot say it is [f32]

A float literal defaults to f64, an array literal has no context, and a let has no annotation. Same shape as (vec-new [u8]) and probably the same fix.

TODO A let binding takes no type annotation

Everything under the surface is there — the binding carries a type slot and the checker consumes it as the want — and only the way it is written is open, because let is a flat list of pairs and cannot disambiguate by count. No longer the blocker it was, since (array 4 T) answers the case that raised it. plan.org's rule is "annotate function signatures, infer locals", so a general annotation is a deliberate absence.

TODO A read-only slice type

bytes-view is read-only by convention only — the type system cannot say a [u8] may not be stored through, so a trap on read-only memory is the enforcement. A read-only slice type, or provenance, is what would move that refusal to compile time.

TODO Writing through a string literal

(let [s (bytes-view "Hi")] (set (at s 0) \h)) stores into read-only memory at -O0 and is deleted as undefined at -O2 — same source, and which way it fails depends on a flag. Narrowed when (bytes s) started copying, so the common spelling no longer reaches the edge. Emitting literals as mutable globals is not a fix: it moves which flag misbehaves and costs their read-only placement.

TODO (slice d 1) over a dyn string is refused where (at d i) works

The typed and dyn spaces disagree about a spelling, which the standing rule forbids. A dyn slice should exist.

TODO (slice "abc" 0 99) is not refused at compile time

A string type carries no length, so there is nothing to compare the bound against — consistent with a slice of a slice. A missed nicety rather than a hole; the runtime check still catches it.

TODO An owning temporary as into's source leaks

The macro binds a non-name source to a name the caller cannot reach and cannot know whether the type owns anything. A call in that position should borrow, and drop is what would close it.

TODO Notes on the type-mismatch errors

The most common error class, and it has no second place to point at, because the function table records parameter types and a return type and no locations. A small change to what collection records.

DONE An error is a value, and there is more than one of them

The span went into the location type itself as an exclusive end defaulting to the start, so every refusal site kept its meaning and a location nobody widened is a zero-width span at a point. Macro provenance went the same way. Deliberately not collecting: the reader (there is no resynchronising a paren stream), the first checker pass, Load and the shim.

DONE An index converts from a narrower integer and never from a wider one

An i64 index is refused because 2^32+5 truncates to 5 and would read the wrong element with no trap at all. A u32 index works; anything above 2^31 truncates to a negative i32 and the unsigned bounds check rejects it.

DONE There is one top-level namespace

One pass rejects a second declaration of a name whatever kind either one is. The environment's tables are per-kind, so only a function was ever checked for a duplicate — a defn beside a defonce of the same name type checked and then died in the backend as a redefinition of a symbol, a message with no source location.

DONE A u64 literal is its 64-bit pattern

The cost of accepting the pattern is that a negative decimal literal is accepted as a u64, because the reader records the value and not how it was written. Narrower unsigned types keep the strict check, which is where a typo like 300 for a u8 shows up.

DONE A folded constant does not skip the range check

The folding pass makes its own call to the range test, because a global's initialiser has to be a compile-time constant and only that pass knows this one is.

DONE A defn must state its return type, and an unknown one says so

(defn f [] f65 0.0) says unknown type f65 — did you mean f64?. The unconditional return slot removed the guess, and the pre-pass that collected a file's type names went with it.

DONE Anything that binds a name or alters control flow is recognised explicitly

The house rule that caught two misparse bugs: anything that binds a name, alters control flow, or is not yet implemented must be recognised by name and rejected if unsupported. Rules out silently falling through to a generic arm.

DONE The escape was real: a value the compiler builds trips no function-value refusal

All four function-value refusals were about surface syntax, so Allocator could be a builtin opaque type with no user-writable constructor and the containers needed nothing from generics. The compiler already did exactly this twice — the lifted handler clause and the dev build's indirect call.

DONE Function values, with no capture

map, filter, reduce and a comparator-taking sort arrived with no generics at all, which is what the diagnosis predicted. A map that changes the element type is the one shape that did not come with them: one copy per ordered pair of types rather than per type.

TODO CFn in a struct or a fixed array

A zeroed function value is a null pointer, so a function value is refused in any position zero-initialisation would conjure one — CFn included. An (Option (CFn ...)) field is already legal. A table of function pointers is exactly what CFn is for, and the objection is about zero-initialisation rather than about capture.

DONE Structural compatibility is identical layout

Same fields, same types, same order, so structural compatibility is "the same memory" — no copy, no reordering, no adaptor. Writability is the question that decided it: read-only structural access could gather fields into a temporary and ignore order, writable access has to alias the real storage. Flexible field order waits for classes deliberately, because a class owns its layout and a Vector2 should not pay for identity and metadata. Not implemented.

Backends

DONE The x86 backend tracks LLVM at -O0

CLOSED: [2026-09-20] A construct LLVM compiles, this backend compiles, and the two agree on what the program observably does. "LLVM takes this and x86 does not" is by itself a defect report, not a discussion. The ruling was made by a typed float %, which compiled under LLVM and died at build time on x86; the fix calls the same function LLVM's code generator calls, so agreement is by construction.

DONE The x86 backend is the dev daemon's default

CLOSED: [2026-09-14] flan dev picks it and --llvm leaves it; every other command is LLVM by default. That split is what keeps the calling convention licensed — a dev build compiled entirely by one backend, a release build entirely by the other. --debug takes LLVM's side on its own, because an x86 redefinition module carries no line table.

DONE A crossed reload is refused by an ABI marker

CLOSED: [2026-09-14] Each backend defines a marker symbol its own modules reference, and the pair is refused at dlopen naming both. Before it, an x86 host given an LLVM module died at the first redefined function taking a struct and nothing said why.

DONE The x86 redefinition emitter

CLOSED: [2026-09-14] The counterpart to the LLVM one, which is what the whole backend exercise was for: until it existed the backend built whole programs and could not serve a single C-c C-c. It emits no line table, which is why --debug goes to LLVM.

DONE Conditions, bounds checks and the cell on the x86 backend

The transfer guard, the landing pads, the per-function transfer exit, the restart machinery and the two bounds checks are written from the specification rather than ported, and one cell per function makes every call site redefinable. Every program in the corpus that compiles, has a main and terminates agrees with the LLVM build down to stderr. docs/BUILT.md, "The hand-written x86 backend, and the four measurements behind it", is the account.

TODO Nothing pins the LLVM side at -O0 when the two backends are compared

The survey builds both sides at the default -O2, so a construct LLVM folds is compared as a constant rather than as a lowering. That is how the float % gap survived. Two things would close it: an -O0 pass of the sweep, and something that walks the two backends' primitive match arms mechanically. Neither is queued.

TODO (uninit) and unreachable differ between the backends, and the language has not said what they mean

(uninit) is stable garbage — whatever the stack slot held — rather than poison, and an exhausted match is ud2, a defined SIGILL, rather than undefined behaviour. Both are deliberate and both are now written down, but the language still has not defined what reading an uninitialised value means, which is the item.

DONE f64 to i64 out of range, and INT64_MIN / -1

CLOSED: [2026-09-14] Carried through three backend reports as "a language decision, not backend work", then taken: ArithError replaced SIGFPE for a divide by zero, for INT64_MIN / -1 and for an out-of-range float-to-int cast, and both backends agree on every case. Float division is deliberately unguarded, because IEEE already answers it.

DONE Rt with an aggregate return was never a gap

CLOSED: [2026-09-13] Every aggregate-valued runtime result crosses through an out-pointer, so the refusal is unreachable and building a hidden-pointer convention behind it would have been wrong — that path is the C boundary, where a 16-byte slice comes back in two registers. Rules out writing a SysV classifier for it.

DONE An assignment is whole or it never happened, on x86 too

CLOSED: [2026-09-21] Two bugs. An aggregate was built in its destination, so a signal part-way through left it part-written — worst for a sum case, whose destination is zeroed first; it is built into a frame temporary and copied over now, unless lowering is bound to reach the end. Separately, the transfer exit zeroed an aggregate return value, which for an aggregate is the caller's storage; it zeroes scalars only.

TODO A global initialiser still builds an aggregate straight into its destination on x86

The statement-level assignment goes through a frame temporary now, so the half-write is closed where a program can observe it. The release build's globals-init path was not converted and still writes in place.

TODO An aggregate built in place can read its own destination

(set p (P {.a (.b p) .b (.a p)})) answers 2 1 under LLVM and 2 2 under --x86: lowering asks whether it can transfer and not whether it can alias. The fix has a shape already — the same temporary, chosen by an aliasing question. Whether the two become one predicate or two is the lane to decide.

TODO The aggregate temporary is an unrooted buffer while it is filled

No root table names it. Harmless only while a dyn field in a struct was refused, and per-type descriptors have since lifted that. Whoever relies on a dyn field reaching this path has to root the buffer, or a collection running inside the construction will not see what has been built so far.

TODO Marking through a descriptor an x86 reload module emitted

The module links and runs. What is not proved is a collection running while a live instance of a dyn-holding struct sits in a frame of a body that module delivered. For the next sweep rather than for a lane.

TODO A sliced string loses the trailing NUL

The x86 backend emits a NUL after every string constant and the LLVM one does not, so a declare-c wrapper leaning on the courtesy is already backend-dependent as well as slice-dependent. The contract is pointer and length, and nothing promised otherwise.

TODO Frame descriptions are gated on –debug

They are correct in every build and free at runtime, and a release build is where a crash would most want them. One if in three places.

TODO A !DILexicalBlock per Let

Inside nested =let=s that bind the same name, a debugger still answers with the outer one. The disambiguating suffix makes both visible, which is not the same as making the answer right. It needs block structure the typed IR does not carry, and the variable declarations moved out of the entry block.

TODO UBSan sees no Flan code, and no flag changes that

UBSan's checks are branches clang's C frontend emits inline, not a pass, so shift undefined behaviour, alignment and a NaN float-to-int cast are unreached. Either the emitter grows those checks behind the flag — a compiler feature of the same shape as the bounds checks — or they belong to the checker. Not decided.

DONE A JS backend is a dialect, not a second machine

CLOSED: [2026-09-17] Object mapping, not linear memory: a Flan struct becomes a plain JS object. That means garbage collection, so no pointers, no manual free, no arena and no allocator — some Flan programs will not compile to JS, and that is named up front rather than discovered. Reader conditionals are Clojure's inline ones, not file-level target naming. Rules out asm.js-style linear memory, which is what wasm exists to replace.

WAIT The JS backend is held behind the dev loop

Held 2026-09-17. wasm32 already reaches the browser, and a third backend beside the two that exist is the largest item on the list. In the meantime lib/js.ml compares string views by identity and answers string equality wrongly; the option on the table when it is picked up is a loud refusal in that arm rather than an implementation, so the dialect says it cannot do this instead of saying something false.

TODO The web target does not reach four things

Nothing has been opened in a browser — node sand.js gets as far as glfwInit before dying on window is not defined, which proves the module is live and proves nothing about the canvas. Asyncify's cost is quoted rather than measured, no frame time on web has been taken, raylib's audio and threads on web are untried, and a wasi build reaching raylib fails on undefined symbols because the raylib link line is tagged native.

DONE A package's .c files can be addressed to a target

A tag in the name before the extension — flan_agent.web.c replaces flan_agent.c on a web build. Refusing the agent package on web was the honest-looking option and is ruled out by arithmetic: there is no conditional compilation, the flagship program calls into the agent unconditionally, and Reach cannot prune a package something reachable calls into. A refusal is only honest when the caller has a way to not ask.

TODO The IR and the disassembly are not annotated with the source

The emitter writes .ll as text and every typed IR node carries a location, so an IR comment costs nothing and cannot break anything. The disassembly half is objdump, which the daemon already shells out to without -S or -l. Settled in conversation: -O0 gets the full annotation and -O2 gets nothing or whatever best-effort mapping falls out, so --debug forcing -O0 is fine. Writing a disassembler stays off the table; richer annotation of objdump's output needs only what the daemon already holds, which is where SBCL's advantage actually comes from.

Runtime

DONE An index out of range is a condition

CLOSED: [2026-09-13] A failed bounds check signals BoundsError; a handler can answer it, a restart-case catches the transfer, and an unanswered one dies with the location and the index. Vec's checks are plumbed the same way, since indexing an array and indexing a Vec are one form. Rules out exiting on the spot.

DONE No restart is established at the failing index

CLOSED: [2026-09-13] retry exists for allocation and for files because those attempts are repeatable; nothing a handler can do makes index 51 valid for a length-50 array. use-value for the index would cost every indexing operation a restart frame. What answers a bad index is the program's own continue.

DONE ArithError signals and dies, with no restart offered

The runtime cannot push a restart frame on a program's behalf — a restart frame is allocated in the restart-case that offers it. Rules out use-value at the failing operation. Only a saturate restart on the cast arm alone was left as a question, and it is not written anywhere.

DONE slice-from-ptr's runtime refusal names the promise

CLOSED: [2026-09-14] It used to reuse the slice error and report a range and a length the caller never wrote. slice-from-ptr is the one form where the compiler cannot check the thing that matters, so its refusal is where the promise is spelled out. The check is signed on purpose: a negative length sign-extended is a huge unsigned value an unsigned compare waves through.

DONE A dying program uses _exit

exit runs the atexit chain and the ELF destructors, which want the loader lock the agent's listener thread may hold inside dlopen, so a program that should die could hang. Unconditionally rather than only under --dev.

TODO A Map's bounds check and the stale-container failure still die

Deliberate for the stale case — the region the container lived in was released and there is no frame to go back to that would not read freed memory. The Map path was left alone rather than converted half-way.

DONE Six trap paths park instead of killing the session

CLOSED: [2026-09-18] A trap in a dev session stops for inspection rather than taking the daemon with it. Two of the six refuse the resume, because there is nothing to resume into. A standalone build dies as before.

DONE bytes copies, string constants trap, and a segfault parks

CLOSED: [2026-09-20] An in-place sort over (bytes "INSERTIONSORT") wrote into a string constant and took the whole session down. (bytes s) allocates a writable copy through the allocator surface — never a hidden malloc — and (bytes-view s) is the old free reinterpret. A dev build installs a SIGSEGV handler that parks in the break loop; it needs SA_NODEFER, because the handler is the park and never returns.

DONE Allocators, (Vec T) and StorageExhausted

Three amendments to a frozen spec: free-all is retain-capacity with arena-destroy beside it; the allocator context is a dynamic variable rather than a calling-convention parameter; and the Vec header is the same size in every build, because a layout that changes with a build flag can disagree silently across the reload boundary. One addition: a budget, because retry needs a handler that can make the same request succeed.

TODO The Vec generation word has no reader

It is bumped on reallocation and read by nothing. The stale-slice trap it exists for needs a slice that can carry the Vec's identity, and a slice is pointer and length — so either slices grow a word in a dev build or the trap does not exist. Today it does not.

TODO The allocator's budget is not in the spec

alloc-budget and set-alloc-budget exist and the spec does not mention them. Worth folding in or replacing with a growable arena.

TODO The Vec header is not the size the spec fixes

Five words in every build rather than the spec's four, and for a stated reason: a redefinition module is built separately from its host and nothing makes the two agree on a struct size. Give the reload path a way to carry the build flags and this falls out.

TODO arena-destroy under a live view reads freed memory

The epoch check that makes free-all safe under a live view does not survive arena-destroy, which frees the block holding the epoch. It happens to trap in practice because the freed block still holds the bumped value. A question about arena-destroy's ordering, not about views, and the typed side has the same shape.

DONE Map removal costs a backward-shift loop

Removal landed, with the loop the spec predicted as its cost. Deferring it was what had kept the implementation free of tombstones and of Odin's backward-shift loop; taking it is taking the loop.

TODO The Map is slower than CPython's dict at a million entries

Keys, values and hashes are three separate runs, so a lookup that misses everything costs three cache misses where a compact dict costs two. One byte of metadata a slot — the Swiss-table arrangement — is the known answer and is not built. The crossover is somewhere between ten thousand and a million and nobody has found it.

DONE dyn maps and interned keywords

CLOSED: [2026-09-20] Keywords are interned and immortal, so equality is pointer equality and there is no collector object to attribute to one.

DONE Per-type descriptors make a dyn field in a struct markable

CLOSED: [2026-09-20] A struct or a condition holding a dyn carries a descriptor naming the byte offsets the collector must follow, emitted by both backends and by both redefinition emitters. The stopgap refusal on a dyn field in a struct is lifted.

DONE A redefined defclass migrates its instances lazily

CLOSED: [2026-09-20] CLHS 4.3.6 minus the user hook. Nothing is enumerated and no heap is walked — the redefinition is constant time and each instance pays once, at its next touch. Neither printer migrates, so a stale instance shows its old slots to the editor until something touches it. The registry is advisory: a key the class never declared is dropped by the next migration, which is data loss with no enforcement behind it.

TODO A class registry keeps one slot list per class, not one per layout version

"A redefined class's old instances stay resolvable" needs every version's metadata retained for as long as any instance holds it, the way nothing is ever =dlclose=d. What exists is one current slot list and one generation per class, and migration is lazy and additive.

DONE A spin is not patience: the registry's slot read

CLOSED: [2026-09-21] Sixty-four bare re-reads of one word finish in about two microseconds, so against a descheduled writer the old budget was not small — it was zero wall clock. There was no timeout to widen; what was added is the first wall-clock patience the slot read ever had. A refusal now means the table genuinely would not hold still.

DONE The 4K result cap is not a transport buffer and stays

The bound is the buffer the game thread writes into, so a growable one means the frame thread calling realloc, and that breaks the seqlock, which 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. Rules out deleting it as transport machinery.

DONE The snapshot copying and generation stamping stay

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 generation stamp keeps a nested break from claiming a choice made against the outer one. Rules out deleting them with the transport.

TODO The seqlock's losing race has no test

The result read copies into the caller's buffer and checks the counter either side; nothing drives the case where the counter moves. The one threaded test races the registry table instead.

TODO The snapshot generation's racing stale claim has no test

The nested-break case is tested deterministically now. What is still absent is the race: landing a request inside a two-millisecond poll from outside the process. It wants a hook the test can drive, not a sleep.

TODO SNAP_MAX and SNAP_NAMES are read rather than tested

Three of the four named buffers have evidence. Only this pair is still read rather than driven, and sixty-five nested =restart-case=s are a lot of program for a clamp.

CANCELLED Probing for an interior overrun under memcheck

CLOSED: [2026-09-12] An arena is one malloc and a Map's four runs are one allocation, so an interior overrun is not observable, not merely unreported. No client request fixes it. Recorded so it is not re-proposed as a gap in the sweep.

DONE ASan does not see an uninitialised read; valgrind does

CLOSED: [2026-09-12] memcheck needs no instrumentation — it works on the binary, so hand-written IR arrives on the same footing as clang's C, which is why it was reachable where MSan was not. Rules out treating a clean sanitizer run as evidence about stack lifetime.

DONE The memcheck half of the allocation registry

CLOSED: [2026-09-14] The registry knows a free-all killed everything in a region, so a later read through a pointer into it is answerable; memcheck is told the same fact, so the same read is reported. The two stay two claims — different tools reaching different people.

TODO The leak question across the corpus

Both sweeps run with leak checking off, because allocate-once-never-free is this runtime's design and a leak check produces a suppression list. A green sweep therefore says nothing about who frees the newly allocating (bytes s). Worth asking on purpose one day, across the whole corpus and not one program.

TODO An unhandled condition has no location

The error entry point takes five integer arguments, which fills the argument registers; a location pair makes seven, so the x86 backend would need stack argument passing at a call site whose register file is exactly full. The dev-side half is different work: the trap hook hands control to a session in-process with the compiler, which can read the source.

TODO trap_oom has no site

It is reached from the allocator, which has no site to be given. The range trap already carries the location pair and every caller passes null, so giving at, set-at and push a site is a call-site change rather than another round of signature churn.

TODO A restart has no location

The restart frame is mirrored across both backends and the runtime, so giving continue a file, line and column means two fields, stores in both backends, an accessor, the snapshot copying it and the buffer printing it. A cross-backend ABI change; do it as one lane, not as a rider. A site for user error calls is the same lane if the frame is being touched anyway.

TODO handler-case's own restart is listed in a break loop under it

The restart the form makes up for itself is on the restart stack like any other. Hiding it means a new field in the frame layout written out in both backends and the runtime. Choosing it is refused loudly rather than answered wrongly, so this is cosmetic.

TODO A formatted number does not outlive its frame

The conversion buffer is one frame slot per call site, so returning a string built from it returns a view of storage the return has just released, and pushing one pushes an element aliasing that slot. Neither shape is refused. Copy the bytes for anything that outlives the expression that made them, which is what append-i64 and append-f64 do.

DONE A shift count is bounded two different ways

A literal count out of range is rejected by the checker; a computed one is masked to the operand's width minus one. A shift by the operand's own width is poison in LLVM rather than a wrong number — (<< 1 32) compiled to a bare return. The mask is what the hardware does anyway and is folded away when the count is constant.

DONE The linked-list frame beat an array with a stack pointer

The opposite of what the escaping-alloca argument predicts, and the measurement that first said otherwise was comparing a 40-frame binary with a 600-frame one. That is why every number in docs/BUILT.md is a minimum of nine runs.

TODO runtime/flan_dyn_stub.c is dead

No dune rule mentions it, no module refers to it, no test links it, and it does not compile — two conflicting-type errors against its own header. It is maintained by accident: one lane added a function to it, which is duplicity on the same side of the same capability. The recommendation is delete, and the author added the file, so it is his call.

Dev loop

DONE The dev loop, step 1: the reload primitive

A list of top-level forms is recompiled and installed into a running process, and call sites compiled before those forms existed follow them through an indirection cell.

DONE The dev loop, step 2: a name the process was never built with

A defn or a defonce the process was not built with can be added and then redefined again, through a by-name cell in the dev registry.

DONE The dev loop, step 3: the agent installs at a frame boundary

The program takes a redefinition over the agent socket and installs it between frames.

DONE The compiler is a thread inside the program

CLOSED: [2026-09-14] One binary that is the compiled program and the whole OCaml compiler. The program keeps main() and the compiler comes up on a side thread beside the agent's listener, rather than the program being loaded into the daemon — macOS needs a window on the main thread, and the agent was already a server inside the program. Crash isolation is given up knowingly. docs/BUILT.md, "One process", is the account, and the embedding spike that made it safe to commit to is under it.

DONE Transport and code generation were independent, and transport was the larger prize

CLOSED: [2026-09-14] Merging the processes removed the transport; a new backend removes code generation. Neither implied the other, and the two were repeatedly conflated. The order held: spike the embedding, merge while keeping the existing build path exactly as it was, measure what was left, and only then choose a backend.

DONE Re-runnable main after the window closes

CLOSED: [2026-09-17] A finished program parks instead of dying, and a daemon op wakes it and re-enters main on the same thread, because a window belongs to the thread that opened it. Globals are not reset between runs — the process never died. Rules out a fresh process per run.

TODO Re-run does not work under –two-process

A finished child process is genuinely gone, so there is nothing to wake. Re-run is merged-build only, and since the default backend runs merged it is no longer the blocked case.

DONE An accepted re-run reads as running

CLOSED: [2026-09-21] A caller that asked for a re-run and then waited for the program to park was answered by the park it had just ended. The state is set under the lock that accepted the request, so the state is the decision rather than a report of it. Rules out teaching the tests to wait on something else — the runtime refuses a second re-run precisely because the first is committed, so the two answers disagreed about the same fact.

DONE C-x C-e installs a top-level form

CLOSED: [2026-09-17] Context-aware: a top-level form compiles and installs, anything else evaluates as an expression. C-c C-c stays the explicit alias.

DONE C-x C-e answers against a parked program

CLOSED: [2026-09-17] An expression evaluates against a parked program by draining the agent's ring from the park, rather than by loosening what a frame boundary is. Safe because a parked program has no concurrency at all.

DONE Evaluating a def assigns

CLOSED: [2026-09-21] def is Common Lisp's defparameter, and evaluating a defparameter assigns. The previous reading — a promise about the next re-run — was wrong for the reason the form is named after. Both events happen: an evaluation that assigns and a load that re-initialises. defonce and defconst are unchanged.

DONE An evaluated expression that signals says so at once

CLOSED: [2026-09-20] The wait recognised only a pause, so every other stop fell through to a timeout arm whose sentences were about something else. "A stop entered after the module was delivered" is a generation number rather than a name, so evaluating from inside a break into a thunk that stops on the same condition class is settled by comparing two integers.

TODO Whose break it is, which no counter answers

A game loop that signals during the build or the wait bumps the generation exactly as a thunk would. The machine-readable fields stay right; what is wrong is the sentence. The per-frame program-or-eval label is computed by the daemon from ownership, not from anything in the frame, so this is not the shadow-stack gap it was once written down as. Not queued — the window is narrow.

DONE The first evaluation no longer stalls behind the agent socket

The accept loop used to sit behind a ten-second wait for the agent socket, so a program that binds its socket late — or not at all — looked ready and answered nothing. The wait is no longer in front of the accept loop.

DONE The parked-program note is said once per park

A finished program is always parked, so re-evaluating main after every run printed the whole note again. The daemon remembers whether it has said it for this park.

DONE A session ends when no editor has held the socket for a grace period

CLOSED: [2026-09-18] Two graces — five minutes parked, thirty seconds live — because a parked program is invisible and a live one may be a window somebody is watching. Armed only after a first client has connected, so a headless daemon is untouched.

DONE Parked orphans exit with their daemon

CLOSED: [2026-09-18] A child outliving its daemon is killed by the kernel. Found in passing that half the observed orphans were merged daemons whose editor had vanished, which the grace above answers.

DONE The daemon survives a client that closes mid-reply

CLOSED: [2026-09-14] A reply written into a socket whose reader had gone used to kill the process, and in a merged build that process is the program, the compiler and the listener at once. It reported as a connection refusal on a path that plainly existed, which misdirected two investigations.

TODO The daemon leaves its temp directory behind

About seven megabytes a session, and nothing removes it. Two deliberate non-goals when it is fixed: not on a crash, because the directory is the post-mortem, and never another session's directory, because a stale pid is not proof of anything. The agent socket goes with it.

DONE (agent/start) takes no argument, and binds before main

CLOSED: [2026-09-20] The zero-argument form takes the daemon's socket where there is one and an announced path where there is not, bound by a constructor before main. Reach prunes a package nothing calls into, so the constructor reaches only a program that polls or waits. Full invisibility — a dev build linking the agent whether or not the source says so — needs the package force-linked and is a decision about what --dev means.

TODO FLAN_AGENT_SOCKET in a shell's environment steals the socket

Binding unlinks the path first, and before the constructor that unlink was reached only by an explicit call. A sentence about the shape of the gate rather than an observed problem: only the daemon sets the variable and it never runs release builds. The fix, if it is ever felt, is a narrower gate.

TODO The daemon's "has not called (agent/start …)" note is unreachable

Unreachable, not merely unexercised: the one state it was true of is closed by the constructor. Retiring it is the author's call over a lane that merged days ago, so it is left in place saying a true thing about a state nothing can be in.

DONE The allocation registry

CLOSED: [2026-09-13] One insert per allocation, always on in a dev build, no opt-out. Nothing per-region, no range recording, no per-allocator opt-out. It does not cover stack locals and globals, which the shadow stack and the static type table answer by name; a stack address is deliberately not in the table.

DONE The shadow stack

CLOSED: [2026-09-12] The route to a backtrace and to locals together, dev-only so a shipped game pays nothing. Chosen over DWARF deliberately: DWARF owes a lexical block per let before shadowed locals are even honest, and that buys locals in lldb rather than in the break loop.

DONE The x86 backend pushes shadow-stack frames

It does, in every dev build. The prose in the daemon that rewrites the agent's reply to say otherwise is now false and should go with whoever next touches it.

DONE A restart is not a transaction

If a frame mutates a global and then signals, taking a retry re-runs the mutation. Nothing rolls back, and Common Lisp offers no help either. The discipline is that the author chooses where the retry boundary is, and it matters more here because the intended use is a game loop.

DONE The break loop's display pass

CLOSED: [2026-09-20] Off a dogfooding session that hit a bounds error with no line number and unreadable field names. The headline reads the condition's fields inline, the buffer draws the source line with a caret, compiler temps are hidden from the locals listing, and a shadowed restart is takeable by index.

DONE The break buffer opens by itself when the program stops

The client already knew the moment it happened. Three questions settled with it: it displays rather than takes focus, (pause) is not special, and the behaviour is a defcustom.

DONE The condition itself is on the wire

The agent keeps the condition pointer beside its name and a verb hands it back, so the editor can render the condition's own fields rather than only its class.

TODO A restart's source location and arity are not on the wire

The restart frame is prev, a name id, a name and a length. A backtrace and locals landed out of the shadow stack and needed no debug information; these did not come with them.

TODO The editor half of a typed restart

The language half is in — a restart clause takes parameters and invoke-restart passes them. What is missing is the half only an editor can do: arity and signature on the frame, the restart listing carrying the signature, and the daemon compiling each argument against the declared type and writing the values into the frame's buffer before aiming the channel.

TODO The type identity of a local is not qualified

Settled for conditions and for structs, because Load qualifies every declaration at import. Still open for locals, where the debug information gives a bare name and nothing qualifies it.

TODO The render-thunk-per-inspection design

An inspection still compiles a thunk per request. A redesign rather than a deletion, and its own lane: it is what unblocks the inspector retaining a value.

TODO The watch design is reopened

Push was chosen partly because polling costs a compile, and that premise weakened when the processes merged. The table-and-read design stands; whether it should stay pushed is open.

TODO A watch over a struct or a slice

Scalars work today through four runtime entry points and need no compiler change. A struct or a slice needs a compile-time walk over its type — one arm beside print.

DONE Two ways to root a walk

The inspector takes a frame and a slot index as well as an expression. An index is the thing a listing can hand back where an address is not something an editor should hold. The one prediction that did not survive contact: walking between the two modes was listed as a cost and is not one, because a stack entry carries its own root.

DONE C-c C-c on a generic installs its instantiations

Only instantiations reach the function list, so a generic name used to report that it had installed nothing at all. The session expands to instantiations before reporting.

TODO Signature generations and stale-caller warnings

The biggest hole in "you never restart the program". A changed signature is refused outright today and that is a placeholder, not the design. It needs function versions, a trampoline per version, and caller tracking good enough to name the sites; the cell gives the indirection, and what is missing is that a cell holds one bare pointer with no signature, so there is nowhere to put a second version.

DONE A module carrying a string literal is never unloaded

The transient rule is that a module retaining nothing may go, and a string literal counts as something retained — which silently stopped every module carrying one from ever being unloaded. That is why frame descriptors got their own counter.

DONE A redefinition delivered while parked installs on the next re-run

The park used to drain the agent ring only when something had asked it to poll, and a plain redefine does not, so every generation ran one re-run later than whoever pressed the key expected. The drain happens in front of the exit now.

TODO A dyn value from eval-expr never reaches the reply's value field

It renders to the program's own stdout and arrives on a later reply's output instead. Where a dyn expression's value should surface is a question about the editor protocol.

DONE Memory diagnostics on demand

CLOSED: [2026-09-20] Two classes of allocation, surfaced on demand and never changing what compiles. Precision over completeness: a site that does not allocate is never marked, and vec-new, map-new, dyn arithmetic, keywords and dyn push and put are deliberately silent, each for its own reason.

TODO A debug tracking allocator over the raylib boundary

ASan's leak detection covers memory instrumented code allocated — the Flan allocator, already clean. It does not cover a leaked texture, because that memory belongs to uninstrumented raylib. Every raylib call goes through a generated wrapper, so a dev build can count acquisitions against releases there and report what is still held at exit, by name.

DONE –dev –sanitize was unbuildable, and nothing built it

CLOSED: [2026-09-21] clang's sanitizer pass faulted on a constructor table naming a function the module only declares; the table names a local definition that calls the two now. A configuration nothing builds can be broken for a month, and this one was, so the sweep builds dev programs twice under the existing alias.

DONE test_dev's abort rows raced and killed the binary

CLOSED: [2026-09-20] What says an abort worked is the waitpid underneath it, not the reply — the program exits from its own thread while the reply is composed on another, and nothing orders the two. The read raised on a closed socket and the test binary exited 1 with no failure line, which is the worst shape a failure can have when a lane is judged on the exit status.

TODO A program driven by a real flan dev daemon under a sanitizer

The daemon builds its host through its own path and the CLI has no way to pass a sanitizer flag to it. Named as the check worth adding next; a day rather than an hour. The x86 backend is not a gap here — that pair is refused by name, because there is no sanitizer pass over hand-written assembly.

TODO A transient signal 11 on a globals daemon

Seen once, never reproduced, on a daemon whose fixture had just gained a host global Vec and a run-time-new one. A reproduction under load would settle it.

TODO test_dev daemons fail to bind under load

Daemons exiting with status 1 or 2 before binding their socket, across most of the file at once, while the load average is high. No stale socket or leftover daemon afterwards; green on a quiet machine. Distinct from the registry race and the stale-park re-run flake, both of which are fixed. A daemon that dies before it binds died on the compiler's side, before any program it built has run a line.

DONE A test binary that hangs is killed by its own alarm

A reader branch that forgets to advance loops for ever and the suite waits as long as it is left to; in CI that is a job the runner kills with nothing named. The alarm is generous on purpose, because an alarm that fires on a slow machine is a flake and a flake is how a watchdog gets deleted.

DONE A forked acceptance failure reaches the exit status

The fork pool is drained before anything reads the failure count, and a nonzero count is an exit status. A red row used to be able to print and pass.

Editor

DONE The syntax table and the font-lock lists are read off the parser

CLOSED: [2026-09-21] The special-form list is the heads the parser dispatches on, the builtin list is the checker's, and the constants are the bare symbols; defmacro is a definer. Three kinds of name had been mixed into one list, so push was drawn like let.

DONE A user-defined macro is highlighted from the live session

CLOSED: [2026-09-21] The definitions op gained a macro kind and the mode consumes the live cache as a font-lock source through a matcher function over a hash table. Macro-ness had been erased by check time, so the op was extended rather than the editor made to guess. CIDER over nREPL is the precedent. The rules carry no override flag, so the static table wins by mechanism and a program defining its own length cannot repaint the builtin.

DONE A discarded form is drawn as a comment

A syntax-propertize function gives a #_ span the comment class, clojure-mode's approach, including chained discards. The compiler side was never the gap.

DONE A binding vector indents name-under-name

CLOSED: [2026-09-21] The indenter is ported from clojure-mode's source rather than derived from it, so brackets-mean-binding and pairs-align come with it. defn parameter lists and restart-case clause parameters were the same shape and were fixed in the same pass. Deriving from clojure-mode at runtime stays rejected: it would add an external dependency to a mode that ships in this repository and needs nothing beyond stock Emacs, and that community is mid-transition to a tree-sitter mode.

TODO 373 lines in 20 files still reindent differently

Concentrated in four files, untouched by the Emacs pass and untouched before it. The indenter and the hand-formatting there disagree about shapes nothing has looked at.

DONE C-c C-i inspects the expression at point

No prompt, because the expression is already written in the buffer. C-u opens the minibuffer instead, pre-filled, and so does having nothing at point to take.

DONE An evaluation's value is shown at the end of the line and echoed

Both, not one or the other. The overlay used to suppress the echo on the argument that saying one number twice teaches a reader to skip both; in practice the echo is the half still there after the next keystroke takes the overlay down.

DONE Two streams and one tool list

CLOSED: [2026-09-20] The daemon's log is one buffer under compilation-minor-mode, the REPL is the working stream, and a third buffer holds the errors and the memory sites. The separate output buffer is gone — the program's output already rides every reply, so both destinations were editor-side routing all along.

DONE A breakpoint is marked from the editor, without editing the buffer

C-u before an evaluation marks a form so the program stops when it runs, for three targets: the top-level form, the last expression, and the form point is inside. The mark is a separate field the daemon splices after parsing, where locations are already attached, rather than text spliced into the source — which would shift every line and column after it. It sticks, like Clojure's, until the form is evaluated again plainly.

DONE A backtrace and a frame's locals have an editor surface

Both were daemon ops with nothing calling them. One command shows the backtrace with the selected frame's locals.

TODO Hex, binary and an address on a primitive in the inspector

The last item of the Emacs batch besides the break buffer, and independent of it.

TODO A defclass is not on the definitions list as a type, and a sum's cases are not drawn

A defclass is expanded away before the checker — a dyn map and a shape tag by then — so there is no class table to read. A sum's cases are one symbol each and the daemon answers with the type's name only. CFn also wants adding to the type rule.

CANCELLED A flycheck checker, and a structured JSON report

CLOSED: [2026-09-20] The workflow is compile-at-the-end, not live linting. Not built per the specification's own branch — the flag is the command and the printed shape is the error pattern, so anyone who wants one has the four lines, and the manual carries them.

TODO compilation-mode steps over the notes

Nothing sets the skip threshold, so next-error walks the errors and steps over the notes, which are still parsed, coloured and clickable. Labelling them as warnings would make them navigable and is refused: a note is not a warning.

Docs and the repository

DONE dune test stays fast and the slow checks stay opt-in

CLOSED: [2026-09-14] The suite is run constantly, including by every lane, so a second added to the default run is paid hundreds of times. Rules out attaching the sanitizer, valgrind or backend sweeps to the default run.

DONE A lane runs dune test and nothing more

CLOSED: [2026-09-19] The corpus sweeps run once after several lanes have landed, and their fixes are dispatched as one batch. A survey walks the whole corpus, so a lane touching a handful of programs was paying the full cost to learn nothing about the rest. The consequence accepted is that a lane is reviewed on its code.

DONE A check nothing runs rots, so every check has an alias and CI runs them

The page's example checker and the backend survey each rotted for days or weeks because neither ran unless somebody remembered. Both are aliases now, inside the umbrella alias CI runs on every push. Both are too slow for the default suite, which is the tension that caused it.

DONE Compiler messages are written for a first-timer

CLOSED: [2026-09-21] Two rules, not one. A message says what is wrong and what to write, and stops. And it says it to someone holding this compiler and nothing else — no prior spelling, no milestone number, no rename framed as a rename. Every suggestion a message prints must compile, and an assertion about the compiler's own invariants is prefixed and says it is a compiler bug.

DONE The diagnostics pass

CLOSED: [2026-09-20] Graded against the contract: show the code with the caret, say what was understood, say what conflicts, name the fix. Two behaviour changes came with it — an unannotated two-name parameter vector compiles as two dyn parameters, and a defn named after a builtin stopped being unreachable.

TODO Three structural diagnostics questions are still the author's

Printing the stable kind at the end of the first line, the understood-then- conflicted clause order as a writing rule, and non-cascading multiple errors. Each needs a decision rather than work.

TODO docs/BUILT.md still describes (Handle T) and the pool as built

It reads as shipped fact for a type the checker has no constructor for and the runtime has no code for, including an enumeration primitive in the present tense. The pool was removed on 2026-09-18.

TODO The daemon still tells an editor the x86 backend pushes no frames

It does push them, in every dev build. The reply is rewritten from a claim that stopped being true.

TODO web/index.html still claims there is no implicit widening

Two places. Left alone deliberately — the website has its own rewrite lane.

TODO plan.org's Types section lists a predicate that no longer exists

It still describes copyable? and "a type variable is move-only by default", both of which the ownership repeal removed. Left alone deliberately by the generics lane as the repeal lane's sentence to retire.

TODO plan.org cites the wrong mechanism for jank's relinking bug

The real cause was a process-teardown race; jank calls through vars, which are already indirection cells. 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.

TODO docs/SPIKE-GENERICS.md lists landed work as remaining

Three items are under "Mechanical" as remaining work and have landed. A dated report going stale at a live claim.

TODO Two citations in spec-memory.md do not land where they say

One is off by a line in a reference clone; the other names an arm that is not the one the claim is about. The claim behind the second is true and cited in the wrong place, and a third companion citation in the same section is stale too.

TODO The sand hash is quoted as prose in five places besides its assertion

The assertion carries the current number; the prose copies do not all. Whoever re-takes the number has that list.

DONE sand.flan is two programs

CLOSED: [2026-09-13] The windowed one is never executed here; the headless one is the acceptance case and runs at both optimisation levels, as a dev build, and on wasm32. The physics is untouched on purpose — the three reference implementations disagree there, so parity does not name a target.

DONE The brush is embedded, not loaded from a path

Deliberate, because a path-based load is the one shape the browser cannot have. Said so it is not later read as an accident. It holds for the shipped programs rather than for the repository — a test fixture still loads an image from a path.

TODO A package under test/programs needs a glob line in four places

Four sweeps each walk the programs directory and the glob does not descend. Without the line the corpus row fails with "no package at …" and prints no failure line, so a grep for failures reads green over it.

TODO The macro programs are not in the sanitizer sweep

That sweep runs an explicit list, not a glob, so landing the macro programs did not add them. A one-line edit.

TODO The mutation pass has not been re-run

Sixty mutations, nineteen of which left the whole suite green; all nineteen are closed, each re-planted and watched fail against the new test. What is open is that the pass has not been run again, so nineteen is the old number.

TODO bin/main.ml spells the compile pipeline out by hand

Two places each do a load, a check and a link feeding the builder, where the tests go through one helper. The CLI loads through its own path and checks with a different entry point, so this is not a matter of calling the test module from the binary — closing it means the pipeline moving into the library.

TODO Build.executable returns only its output path

The daemon recovers the host's IR file by recomputing the working directory. One line away: return the path rather than recomputing it.

TODO The 2MB OFL font is not vendored

One example wants a font that is OFL and redistributable; it says on screen when it is missing and runs either way. A call about the repository, not about the port.

DONE old-ocaml/ and the built executables are untracked on purpose

The executables are what a build drops beside their sources. old-ocaml/ is the pre-rewrite frontend, kept as reference and excluded from the build; its contents are also in git history.

DONE A lane that stops mid-repair says which pieces it ran

A handoff once said one of five hand-offs was dropping a value; four of the five were never written at all, and the first step it recommended could not have worked because there was no comparison to print.

CANCELLED Examining branch emacs-batch-a63bd

The branch no longer exists, so there is nothing left to look at. git log is the record.