Merge branch 'master' into worktree-agent-acca0e216df6ca366

This commit is contained in:
Joseph Ferano 2026-09-25 10:42:50 +07:00
commit 22401a6521
17 changed files with 482 additions and 457 deletions

143
CLAUDE.md
View File

@ -1,125 +1,54 @@
# Working in this repository # Working in this repository
For any agent working here, including a lane in its own worktree. These are Standing rules for any agent here, including a lane in its own worktree.
standing rules, not preferences. Where a rule has a reason, the reason is given
once — the rules without one are the ones that have already cost something.
## Never ## Never
- **Never touch anything under `/home/joe/Development/fnm/`.** That is the - **Touch anything under `/home/joe/Development/fnm/`.** The author's live
author's own working copy of the falling-sand game, open in an editor with a working copy, with an editor session attached. This repository's `sand.flan`
live session attached. `fnm/flan/sand.flan` is not the same file as this is a different file and may be edited normally.
repository's `sand.flan` and is never to be read-and-written-back, moved or - **Execute `examples/*.flan`, `sand.flan`, or anything linking raylib.** They
edited. The copy in this repository is ordinary tracked source and may be open a window that strobes the author's desktop. Compile, emit, diff — never run.
edited like any other file. - **Run `dune clean`.** It escapes a worktree and deletes the main checkout's
- **Never execute `examples/*.flan`, `sand.flan`, or anything that links `_build`. Run `dune` with `--root .` from inside your own worktree.
raylib.** They open a window on the author's desktop, which strobes it. - **Kill a `flan dev` daemon you did not start.** The author keeps one attached
Compile them, emit them, diff them — never run them. to their editor.
- **Never run `dune clean`.** It walks up out of a worktree and deletes the main - **Put `Co-Authored-By`, a `Claude-Session` trailer or any watermark in a commit.**
checkout's `_build`, which takes the author's `flan` binary with it. This has
happened. Run `dune` with `--root .` from inside your own worktree.
- **Never kill a `flan dev` daemon you did not start.** The author keeps a live
one attached to their editor all day. Orphans from finished lanes are fair
game; anything whose cwd is under `fnm/` is not.
- **Never put a `Co-Authored-By`, a `Claude-Session` trailer, or any other
watermark in a commit message.**
## Commits and branches ## Commits
`master` is the trunk and tracks `origin/master`. Lanes work in their own git `master` is the trunk. Lanes work in their own worktree and never merge or push;
worktree off it and do not merge or push — merges are resolved by the session the dispatching session merges. A commit message is one declarative sentence
that dispatched the lane. saying what is now true, not what was done.
A commit message is a single declarative sentence in the repository's voice,
saying what is now true rather than what was done: "A thunk named for the order
it was minted in is a different signature after a reorder", not "fix thunk
naming bug". A body is for the reasoning, when there is any.
## Tests ## Tests
`dune test --root .` is the suite and takes seconds. Run it constantly; it must `dune test --root .` must be green before a lane reports; grep its output for
be green before a lane reports. FAIL, since the exit code alone has lied. `@checks` (`@page`, `@x86`, `@cells`),
`@sanitize` and `@valgrind` are slow and run once between batches of lanes, with
`dune build @checks` is `@page`, `@x86` and `@cells` — the reference page's the author's permission, never inside a lane. ASan misses uninitialised stack
examples still compile and print what the page says, the hand-written x86 reads; `@valgrind` catches them.
backend still agrees with LLVM, and a `--dev` build still calls through its
indirection cells. `@sanitize` and `@valgrind` run the corpus under ASan/UBSan
and memcheck and take tens of minutes.
Those three stay out of a lane's own run. They are swept in a batch after
several lanes have merged, and the fixes are batched with them. Keeping the
default run fast is deliberate: a suite that takes tens of minutes is a suite
nobody runs.
Note that ASan does not see a stack-lifetime bug — uninitialised stack reads
need `@valgrind`.
## Evidence ## Evidence
Read the source, do not recall it. `docs/REFERENCES.md` lists the reference Read the source, do not recall it — this repository and the reference clones
clones on this machine — Odin, SBCL, Zig, Carp, clojure-mode, CIDER, raylib and listed in `docs/REFERENCES.md`. Verify a brief's facts before building on them.
the rest — and what each one answers. A claim in these notes that came from one
of them was read out of the clone; the ones that were recalled instead have been
wrong before.
The same applies to this repository. Cite a file and a line because you opened ## Records
it. When a brief hands you a list of facts, verify them before building on them
— a brief written from a stale tree has sent a lane a full day in the wrong
direction.
## Diagnostics Keep only what the code cannot say.
Elm's shape: the source line, a caret, what the compiler understood, and the fix - `TODO.org` — one `**` heading per item under a subsystem: `TODO` a gap, `NEXT`
named. Beyond that: decided and queued, `WAIT` blocked (say on what), `CANCELLED` rejected with a
one-line reason so it is not re-proposed, `DONE` only when the decision is not
obvious from the code (two lines, what it rules out). On a merge conflict keep
both sides.
- The reason for non-obvious code goes in a comment beside it.
- `docs/BUILT.md` holds only reasoning that spans several files.
- No handoff files.
- A message is written for someone who has never used Flan and does not know its ## Reviews
history. Never "X is now Y", never "X was renamed", never our rationale. If
`defvar` no longer exists, the message says `defvar` does not exist — not that
it became something else.
- The clauses come in one order: what the compiler understood, then what
conflicts with it, then the fix — "expected i32, found string".
- Every suggestion a message prints must compile.
- An assertion about the compiler's own invariants is prefixed `internal:` and
says it is a compiler bug. A user never caused one.
## Writing Every lane gets an independent adversarial review before it merges: build and
run rather than read, measure both sides of a claimed fix, and reproduce any
User-facing prose — the README, `web/index.html`, the Emacs manual — is plain. number a lane reports.
Odin's website is the reference: declarative rather than second-person, the
concept defined before the mechanics, the code example after the prose that
frames it, and rationale given its own subsection rather than mixed in.
No aphorisms, no closing lines built for effect, no rhetorical inversions. If a
sentence's only content is its own style, cut it. This applies to reports and
commit messages as much as to documentation.
## The records
- `TODO.org` — every decision, question and known gap, one `**` heading each
under a subsystem heading, with an org keyword saying where it stands: `TODO`
for a gap, `NEXT` for what is queued, `WAIT` for what is blocked and on what,
`DONE` for what was settled, `CANCELLED` for an idea considered and rejected.
A lane records its decision here, as a few lines saying what was decided and
what it rules out — never the argument and never the measurements. On a merge
conflict in this file, keep both sides: two lanes recording two decisions is
not a conflict.
- `docs/BUILT.md` — why the parts that exist are shaped the way they are. The
largest and most load-bearing document here. Reasoning that will not fit in a
few lines goes here, and `TODO.org` carries one line pointing at it.
- `docs/handoffs/` — one report per finished work session.
- `docs/README.md` indexes all of it.
A `CANCELLED` entry is not housekeeping. An idea rejected without a record is an
idea that gets re-proposed, so the one-line reason is the whole value of it.
## The shape of the work
The author dogfoods the language in a separate project, hits friction, and
reports it. Work is dispatched as parallel lanes in isolated worktrees; every
lane is reviewed by an independent agent before it merges; the dispatching
session resolves the merges.
A review is adversarial. Verify by building and running rather than by reading,
measure both sides of a claimed fix, and say what you actually measured. A lane
that reports a number you did not reproduce is a lane with a number you should
reproduce.

View File

@ -501,7 +501,8 @@ capturing value. Two things for it to know: a widening thunk's environment holds
code pointer rather than a GC object, and capturing a dyn stays refused until a code pointer rather than a GC object, and capturing a dyn stays refused until a
synthesised environment has a descriptor. synthesised environment has a descriptor.
** TODO CFn and C's calling convention ** WAIT CFn and C's calling convention
Decided 2026-09-25: waits with C callbacks, until a program needs one.
A Flan function's signature ends with the transfer channel and a C caller knows 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 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 =--no-conditions= flag a =CFn= signature could drop the channel and reach C's
@ -524,7 +525,8 @@ dispatch values. With single dispatch on literal values there is no specificity
question, and inheritance or multiple dispatch would create one. Unknown-slot question, and inheritance or multiple dispatch would create one. Unknown-slot
checking needs class-typed tracking the dyn side deliberately does not have. checking needs class-typed tracking the dyn side deliberately does not have.
** TODO update-instance-for-redefined-class, the user hook ** NEXT update-instance-for-redefined-class, the user hook
Decided 2026-09-25: build it after typed class slots land, shaped for the REPL — written and installed from a live session as a one-time "here is how to migrate this", without restarting. It receives the instance with the added and discarded slots and their old values, and runs at each instance's lazy migration. A hook that signals parks in the break buffer with a restart that falls back to name-matching migration.
Left out of v1 because name matching is the half that makes redefinition usable 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 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 dispatch that exists, and the migration already computes both the added and
@ -736,10 +738,12 @@ 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 exists and the session already uses it — and wiring it into every failure under an
instantiation is a lane of its own. instantiation is a lane of its own.
** TODO Generics across a real compilation-unit boundary ** DONE A program is one compilation, so a generic's body is always visible
It works today because =Load= flattens imports before checking. A package boundary CLOSED: [2026-09-25]
that ever becomes a real unit boundary needs the generic's body to cross it, which Odin's and Zig's model: packages are never compiled separately. The cost is build
separate compilation cannot do — which is why C++ puts templates in headers. time proportional to the whole program and no binary-only packages. If separate
compilation is ever wanted, Rust's answer is the one to take — a compiled package
carries its generics' checked bodies and the user instantiates them.
** DONE Collapsing the prelude buys 27 to 15, not 27 to 6 ** DONE Collapsing the prelude buys 27 to 15, not 27 to 6
CLOSED: [2026-09-13] CLOSED: [2026-09-13]
@ -824,7 +828,8 @@ 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 =(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. join as =dyn= is the author's call and is not settled.
** TODO A truthiness failure re-runs the whole failing subtree ** NEXT A truthiness failure re-runs the whole failing subtree
Decided 2026-09-25: fix it without changing any message — the retry reuses what the first pass settled for each subtree (memoised by node), so nested =not= is linear. Test with a deep nest that must fail fast and with the existing message tests unchanged.
The retry exists to keep a refused literal's message unchanged and re-runs the 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 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 program that does not type-check. Moot for anything that compiles; only the
@ -864,7 +869,8 @@ 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 semantics; the refined version needs liveness across control flow, which is the
flow tracking that was repealed. flow tracking that was repealed.
** TODO Catching a use-after-release statically ** NEXT Catching a use-after-release statically
Decided 2026-09-25: a study, not a build — how arena memory escapes in real Flan code, and whether a sound lexical check would catch most of it. The result goes in docs/BUILT.md; nothing is built on it without the author.
Open, and for the first time with evidence available: the epoch trap is built, and 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 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 actually occur are lexical can now be answered. The next thing to look at, not the
@ -1051,7 +1057,8 @@ Every program in the corpus that compiles, has a =main= and terminates agrees wi
the LLVM build down to stderr. =docs/BUILT.md=, "The hand-written x86 backend, and the LLVM build down to stderr. =docs/BUILT.md=, "The hand-written x86 backend, and
the four measurements behind it", is the account. the four measurements behind it", is the account.
** TODO Nothing pins the LLVM side at -O0 when the two backends are compared ** NEXT Nothing pins the LLVM side at -O0 when the two backends are compared
Decided 2026-09-25: the x86 parity survey compares against LLVM at -O0 only; -O2 is never its target. The acceptance suite's paired -O2/-O0 rows stay, being the check for undefined behaviour in emitted IR, which is a different question.
The survey builds both sides at the default =-O2=, so a construct LLVM folds is 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 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 survived. Two things would close it: an =-O0= pass of the sweep, and something
@ -1116,7 +1123,8 @@ The module links and runs. What is not proved is a collection running while a li
instance of a dyn-holding struct sits in a frame of a body that module delivered. 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. For the next sweep rather than for a lane.
** TODO A sliced string loses the trailing NUL ** NEXT A sliced string loses the trailing NUL
Decided 2026-09-25: both backends emit a NUL after every string literal, and a declare-c wrapper passes a literal argument to C without the copy it makes for any other string. A string is still pointer and length; no slice is promised a NUL. Rules out a NUL guarantee on every string.
The x86 backend emits a NUL after every string constant and the LLVM one does not, 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 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 well as slice-dependent. The contract is pointer and length, and nothing promised
@ -1127,7 +1135,8 @@ Decided 2026-09-25: emit the x86 backend's =.cfi= directives in every build. No
They are correct in every build and free at runtime, and a release build is where 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. a crash would most want them. One =if= in three places.
** TODO A !DILexicalBlock per Let ** WAIT A !DILexicalBlock per Let
Decided 2026-09-25: waits until Flan is debugged in gdb or lldb; the break buffer, which reads the shadow stack, already answers correctly.
Inside nested =let=s that bind the same name, a debugger still answers with the 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 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 making the answer right. It needs block structure the typed IR does not carry, and
@ -1241,7 +1250,8 @@ 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 across the reload boundary. One addition: a budget, because =retry= needs a
handler that can make the same request succeed. handler that can make the same request succeed.
** TODO The Vec generation word has no reader ** NEXT The Vec generation word has no reader
Decided 2026-09-25: remove the word, and in a dev build fill a Vec's old buffer with the dead-beef pattern when a push moves it, so a stale slice reads visibly wrong values. No slice layout change; a release build is untouched. Rules out a dev-only word on every slice.
It is bumped on reallocation and read by nothing. The stale-slice trap it exists 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 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. length — so either slices grow a word in a dev build or the trap does not exist.
@ -1254,7 +1264,8 @@ live bytes, 0 for none, that a =retry= handler raises. The failure bullet says
"raises the allocator's budget" where it said "grows the arena", since no arena "raises the allocator's budget" where it said "grows the arena", since no arena
grows. A growable arena is not ruled out; nothing here asks for one. grows. A growable arena is not ruled out; nothing here asks for one.
** TODO The Vec header is not the size the spec fixes ** NEXT The Vec header is not the size the spec fixes
Decided 2026-09-25: five words in every build, the epoch included, so a release build still traps on a container whose region was released. The spec changes to match the code; nothing else does.
Five words in every build rather than the spec's four, and for a stated reason: a 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 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 agree on a struct size. Give the reload path a way to carry the build flags and
@ -1321,7 +1332,8 @@ 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 declared is dropped by the next migration, which is data loss with no enforcement
behind it. behind it.
** TODO A class registry keeps one slot list per class, not one per layout version ** WAIT A class registry keeps one slot list per class, not one per layout version
Decided 2026-09-25: waits for a case name-matching migration to the current list gets wrong.
"A redefined class's old instances stay resolvable" needs every version's metadata "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 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 =dlclose=d. What exists is one current slot list and one generation per class, and
@ -1399,7 +1411,8 @@ 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 same read is reported. The two stay two claims — different tools reaching
different people. different people.
** TODO The leak question across the corpus ** NEXT The leak question across the corpus
Decided 2026-09-25: one pass over the whole corpus with LeakSanitizer and memcheck's leak check on. Memory an allocator holds by design is set aside; memory nothing owns is a leak and is fixed. The sweeps' default stays leak-checking off.
Both sweeps run with leak checking off, because allocate-once-never-free is this 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 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 therefore says nothing about who frees the newly allocating =(bytes s)=. Worth
@ -1499,7 +1512,8 @@ A finished program parks instead of dying, and a daemon op wakes it and re-enter
Globals are not reset between runs — the process never died. Rules out a fresh Globals are not reset between runs — the process never died. Rules out a fresh
process per run. process per run.
** TODO Re-run does not work under --two-process ** NEXT Re-run does not work under --two-process
Decided 2026-09-25: re-run under =--two-process= starts a fresh child, installed redefinitions included, and says that globals start over because the process is new.
A finished child process is genuinely gone, so there is nothing to wake. Re-run is 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 merged-build only, and since the default backend runs merged it is no longer the
blocked case. blocked case.
@ -1539,7 +1553,8 @@ 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 inside a break into a thunk that stops on the same condition class is settled by
comparing two integers. comparing two integers.
** TODO Whose break it is, which no counter answers ** NEXT Whose break it is, which no counter answers
Decided 2026-09-25: fix it. A stop records whether the thread that stopped was running the evaluation's thunk or the program's own code, so the sentence is decided by the frame and not by the generation counter.
A game loop that signals during the build or the wait bumps the generation exactly 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 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 sentence. The per-frame program-or-eval label is computed by the daemon from
@ -1592,7 +1607,8 @@ that polls or waits. Full invisibility — a dev build linking the agent whether
not the source says so — needs the package force-linked and is a decision about not the source says so — needs the package force-linked and is a decision about
what =--dev= means. what =--dev= means.
** TODO FLAN_AGENT_SOCKET in a shell's environment steals the socket ** NEXT FLAN_AGENT_SOCKET in a shell's environment steals the socket
Decided 2026-09-25: narrow the gate. The daemon also exports its own pid, and the constructor binds the socket only when that pid is the program's parent (or the program itself, in a merged build).
Binding unlinks the path first, and before the constructor that unlink was reached 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 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 observed problem: only the daemon sets the variable and it never runs release
@ -1656,7 +1672,8 @@ on the frame, the restart listing carrying the signature, and the daemon compili
each argument against the declared type and writing the values into the frame's each argument against the declared type and writing the values into the frame's
buffer before aiming the channel. buffer before aiming the channel.
** TODO The type identity of a local is not qualified ** NEXT The type identity of a local is not qualified
Decided 2026-09-25: a local's type prints package-qualified in the break buffer and the inspector, as a field's and a condition's already do.
Settled for conditions and for structs, because =Load= qualifies every declaration 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 at import. Still open for locals, where the debug information gives a bare name and
nothing qualifies it. nothing qualifies it.
@ -1748,7 +1765,8 @@ 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 exited 1 with no failure line, which is the worst shape a failure can have when
a lane is judged on the exit status. a lane is judged on the exit status.
** TODO A program driven by a real flan dev daemon under a sanitizer ** NEXT A program driven by a real flan dev daemon under a sanitizer
Decided 2026-09-25: =flan dev --sanitize= builds the host under ASan/UBSan on the LLVM backend (refused by name with =--x86=), and the @sanitize alias gains a case driving a real session through reloads and a break.
The daemon builds its host through its own path and the CLI has no way to pass a 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 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 hour. The x86 backend is not a gap here — that pair is refused by name, because
@ -1835,10 +1853,15 @@ 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 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. beyond stock Emacs, and that community is mid-transition to a tree-sitter mode.
** TODO 373 lines in 20 files still reindent differently ** DONE Every tracked .flan file reindents to itself
Concentrated in four files, untouched by the Emacs pass and untouched before it. CLOSED: [2026-09-25]
The indenter and the hand-formatting there disagree about shapes nothing has clojure-mode decided each shape. The indenter was wrong on one: a =with-= head,
looked at. and a qualified =def…= or =with-= head, now indents as a body, and a qualified
name finds its unqualified part's spec. The rest was hand formatting and was
reindented: a =cond= or =match= result on its own line sits under its test, and
an ordinary call's later arguments align under its first. A lone =;= comment
line goes to =comment-column= in every Lisp mode, so continuation comments are
written as =;;= lines above the code instead.
** DONE C-c C-i inspects the expression at point ** DONE C-c C-i inspects the expression at point
No prompt, because the expression is already written in the buffer. =C-u= opens No prompt, because the expression is already written in the buffer. =C-u= opens
@ -1962,6 +1985,12 @@ motion states in that mode; every other key, including what =special-mode-map=
binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly, binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly,
diagnostics, lower) have the same exposure and are not changed. diagnostics, lower) have the same exposure and are not changed.
** NEXT C-x C-e in a package's file resolves names in that package
Decided 2026-09-25: CIDER's rule — an evaluation sent from a file resolves names
as code written in that file would, so =(integrate 1.0)= in =physics/step.flan=
reaches the package's own functions, =defn-= included. Today it answers
"unknown function", resolving as the program's main file.
** NEXT Evil takes the keys in the other Flan buffers ** NEXT Evil takes the keys in the other Flan buffers
The inspect, watch, doc, disassembly, diagnostics and lower buffers get the break The inspect, watch, doc, disassembly, diagnostics and lower buffers get the break
buffer's treatment: the keys each binds itself go to Evil's normal and motion buffer's treatment: the keys each binds itself go to Evil's normal and motion
@ -2047,7 +2076,8 @@ host built with no user program, and a load-file op — the reload machinery
already builds a file as a module. Open: what =flan-rerun= does with no =main=, already builds a file as a module. Open: what =flan-rerun= does with no =main=,
and a half-loaded file. =C-c C-k= is taken by the inspector. and a half-loaded file. =C-c C-k= is taken by the inspector.
** TODO The daemon buffer is navigable but not coloured ** NEXT The daemon buffer is navigable but not coloured
Decided 2026-09-25: errors, warnings and notes take compilation-mode's faces, and the program's own output takes a face of its own so it reads apart from the compiler's.
=*flan*= is all plain text. =compilation-minor-mode= is on (=emacs/flan.el:822=) =*flan*= is all plain text. =compilation-minor-mode= is on (=emacs/flan.el:822=)
so =next-error= works, but a minor mode installs no font-lock. Open: whether the so =next-error= works, but a minor mode installs no font-lock. Open: whether the
program's output should look different from the compiler's. program's output should look different from the compiler's.
@ -2212,7 +2242,8 @@ The five that run — =macros=, =macro-params=, =macro-unless=, =pkg-macro=,
out with the other negative cases. The list stays explicit rather than a glob. out with the other negative cases. The list stays explicit rather than a glob.
Not yet run under the sweep: that waits for the batched =@sanitize=. Not yet run under the sweep: that waits for the batched =@sanitize=.
** TODO The mutation pass has not been re-run ** NEXT The mutation pass has not been re-run
Decided 2026-09-25: re-run it once after the second batch of 2026-09-25 merges, with the heavy sweeps, after asking the author.
Sixty mutations, nineteen of which left the whole suite green; all nineteen are 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 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. that the pass has not been run again, so nineteen is the old number.
@ -2232,10 +2263,10 @@ it kept; without =keep= that file is gone and the second half is =None=. The
two-process daemon moves the host's IR from the path it is given and no longer two-process daemon moves the host's IR from the path it is given and no longer
recomputes =Build.workdir=. recomputes =Build.workdir=.
** TODO The 2MB OFL font is not vendored ** CANCELLED The 2MB OFL font is not vendored
One example wants a font that is OFL and redistributable; it says on screen when CLOSED: [2026-09-25]
it is missing and runs either way. A call about the repository, not about the Two megabytes of history for one example that already says on screen when the
port. font is missing and runs without it.
** DONE old-ocaml/ and the built executables are untracked on purpose ** 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 The executables are what a build drops beside their sources. =old-ocaml/= is the

View File

@ -581,12 +581,27 @@ For `syntax-propertize-function'."
("declare" . 1) ("declare" . 1)
("declare-c" . 1)) ("declare-c" . 1))
"How each form indents, by name. "How each form indents, by name.
Anything not named here that begins with `def' is treated as `:defn' by A qualified name falls back to the entry for its unqualified part. Anything
`flan-indent-function'; anything else indents as a function call.") still unnamed that begins with `def' (but not `default') or `with-' indents as
`:defn' in `flan-indent-function'; anything else indents as a function call.")
(defun flan--indent-spec (name) (defun flan--indent-spec (name)
"The indent spec for the form called NAME, or nil." "The indent spec for the form called NAME, or nil.
(and name (cdr (assoc name flan-indent-specs)))) A qualified name falls back to its unqualified part, so `rl/when' would find
the entry for `when' — `clojure--get-indent-method' does the same."
(and name
(cdr (or (assoc name flan-indent-specs)
(and (string-match "/\\([^/]+\\)\\'" name)
(assoc (match-string 1 name) flan-indent-specs))))))
(defun flan--definer-p (name)
"Non-nil if NAME indents as a definition or a `with-' form.
Either may be qualified: `rl/with-drawing' is a `with-' form. This is
`clojure-indent-function''s fallback for a head with no spec, regexp and all;
`default…' is excluded there because it is not a definer, and here too."
(and name
(string-match "\\`\\(?:\\S +/\\)?\\(def[a-z]*\\|with-\\)" name)
(not (string-match-p "\\`default" (match-string 1 name)))))
(defconst flan--labelled-forms '("dotimes" "while" "until") (defconst flan--labelled-forms '("dotimes" "while" "until")
"Loops that may carry a label, which `break' and `continue' name. "Loops that may carry a label, which `break' and `continue' name.
@ -755,8 +770,9 @@ decision to `calculate-lisp-indent'."
;; No spec. Anything else spelled `def…' is a definition and indents ;; No spec. Anything else spelled `def…' is a definition and indents
;; like one, which covers `defstruct', `defdata', `defunion', ;; like one, which covers `defstruct', `defdata', `defunion',
;; `defenum', `defonce', `defconst' and `defalias' without naming ;; `defenum', `defonce', `defconst' and `defalias' without naming
;; them. ;; them. A `with-' form is a body too — `rl/with-drawing',
((and name (string-match-p "\\`def" name)) ;; `rl/with-mode-2d camera' — whatever it takes before the body.
((flan--definer-p name)
(+ lisp-body-indent head-column)) (+ lisp-body-indent head-column))
;; A clause: `(name [params] body…)'. `handler-bind', `handler-case' ;; A clause: `(name [params] body…)'. `handler-bind', `handler-case'
;; and `restart-case' all write their clauses this way, and the head is ;; and `restart-case' all write their clauses this way, and the head is

View File

@ -326,6 +326,54 @@
y 2.0] y 2.0]
(print y))") (print y))")
;; A `with-' form is a body, qualified or not, whatever it takes on the head's
;; line — `clojure-indent-function''s fallback. From `examples/core-2d-camera.flan'.
(test-flan-mode--check
"a qualified with- form indents its body by two past an argument"
"(rl/with-mode-2d camera
(rl/draw-rectangle-rec player rl/red)
(rl/draw-grid 10 1.0))")
(test-flan-mode--check
"a qualified def form indents its body by two"
"(m/defthing name
(body))")
;; A qualified name finds the spec of its unqualified part.
(test-flan-mode--check
"a qualified when keeps when's spec"
"(rl/when (ready?)
(go))")
;; `default…' begins with `def' and is not a definer.
(test-flan-mode--check
"a default-prefixed call aligns its arguments"
"(default-color a
b)")
;; A cond result on its own line sits under its test, not deeper.
(test-flan-mode--check
"a cond result on its own line aligns with its test"
"(cond
(= k 1)
(one)
:else
(other))")
(test-flan-mode--check
"a match result on its own line aligns with its pattern"
"(match v
(Int n) n
(List items)
(length items))")
;; A trailing argument of an ordinary call aligns under the first argument,
;; even when it is long; only a `with-', `def' or specced head gives a body.
(test-flan-mode--check
"an ordinary call's arguments align under the first one"
"(push missing
`(when (ok) (go)))")
;;; Font lock ;;; Font lock

View File

@ -13,8 +13,8 @@
;; Normative references: spec-memory.md (ownership, containers, places, ;; Normative references: spec-memory.md (ownership, containers, places,
;; generics, function values) and spec-conditions.md (restart semantics). ;; generics, function values) and spec-conditions.md (restart semantics).
(import rl "vendor:raylib") ; directory = package, declaration optional; ;; Imports are always qualified: rl/foo.
; imports are always qualified rl/foo (import rl "vendor:raylib") ; directory = package, declaration optional
;; ── Type notation ───────────────────────────────────────────────────── ;; ── Type notation ─────────────────────────────────────────────────────
;; [4 f32] fixed array — a value, copies on assignment ;; [4 f32] fixed array — a value, copies on assignment

View File

@ -213,8 +213,9 @@
;; ── The refusals, each asserted on its own reason ───────────────── ;; ── The refusals, each asserted on its own reason ─────────────────
(refusal "\"a\\nb\"") ; an escape inside a string (refusal "\"a\\nb\"") ; an escape inside a string
(refusal "\"a\\\"b\"") ; an escaped quote — the case where a wrong ;; An escaped quote is the case where a wrong version returns `a\` and
; version returns `a\` and leaves `b"` behind ;; leaves `b"` behind.
(refusal "\"a\\\"b\"") ; an escaped quote
(refusal "\"unterminated") ; not a refusal, but the other string failure (refusal "\"unterminated") ; not a refusal, but the other string failure
;; A set is read now, so what is left to refuse about one is its balance. A ;; A set is read now, so what is left to refuse about one is its balance. A
;; `#{` that pushed nothing would answer "no error" for both of these. ;; `#{` that pushed nothing would answer "no error" for both of these.
@ -228,8 +229,8 @@
(refusal "\\a") ; a character literal (refusal "\\a") ; a character literal
(refusal "12x") ; starts like a number, is not one (refusal "12x") ; starts like a number, is not one
(refusal "[1 :]") ; a colon with no name (refusal "[1 :]") ; a colon with no name
(refusal "@") ; not the start of any value — and the case a ;; `@` is the case a scan-to-delimiter reads as a one-byte symbol.
; scan-to-delimiter reads as a one-byte symbol (refusal "@") ; not the start of any value
(refusal "`x") ; a Clojure reader macro, not EDN (refusal "`x") ; a Clojure reader macro, not EDN
(refusal "[1 2}") ; the wrong closer (refusal "[1 2}") ; the wrong closer
(refusal "]") ; a closer with nothing open (refusal "]") ; a closer with nothing open

View File

@ -102,9 +102,9 @@
(show-dec (slice lone-cont 0 1)) ; a continuation byte leading (show-dec (slice lone-cont 0 1)) ; a continuation byte leading
(show-dec (slice overlong2 0 2)) ; overlong "/" (show-dec (slice overlong2 0 2)) ; overlong "/"
(show-dec (slice overlong3 0 3)) ; overlong "/" again, three bytes (show-dec (slice overlong3 0 3)) ; overlong "/" again, three bytes
(show-dec (slice overlong4 0 4)) ; and four. Added after a mutation run: ;; Added after a mutation run: relaxing 0xf0's floor to 0x80 left the
; relaxing 0xf0's floor to 0x80 left the ;; whole suite green without this line.
; whole suite green without this line. (show-dec (slice overlong4 0 4)) ; and four
(show-dec (slice surrogate 0 3)) ; U+D800 (show-dec (slice surrogate 0 3)) ; U+D800
(show-dec (slice above-max 0 4)) ; U+110000 (show-dec (slice above-max 0 4)) ; U+110000
(show-dec (slice lead-f5 0 4)) ; 0xf5 leads nothing (show-dec (slice lead-f5 0 4)) ; 0xf5 leads nothing