From 4ae7beb6ce8571f822811c64034e2bc7d89a6bed Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:01:31 +0700 Subject: [PATCH 01/24] An evaluation sent from a package's file is to resolve names in that package --- TODO.org | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TODO.org b/TODO.org index a76e8535..09b52b01 100644 --- a/TODO.org +++ b/TODO.org @@ -1931,6 +1931,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, 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 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 From 8006f8728f32875a10def9f879a3b356bd6f27dd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:03:19 +0700 Subject: [PATCH 02/24] The daemon buffer is to colour the compiler's lines and set the program's output apart --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 09b52b01..7e7237d2 100644 --- a/TODO.org +++ b/TODO.org @@ -2022,7 +2022,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=, 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=) 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. From 299b3bcb61a26052fbc02780e9e84d5046bd9c27 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:03:46 +0700 Subject: [PATCH 03/24] The OFL font stays out of the repository --- TODO.org | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TODO.org b/TODO.org index 7e7237d2..9dbb99f7 100644 --- a/TODO.org +++ b/TODO.org @@ -2208,10 +2208,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 recomputes =Build.workdir=. -** 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. +** CANCELLED The 2MB OFL font is not vendored +CLOSED: [2026-09-25] +Two megabytes of history for one example that already says on screen when the +font is missing and runs without it. ** 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 From 0fb30ad9453d5ce34feb2d66938723923380d457 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:04:13 +0700 Subject: [PATCH 04/24] The mutation pass is to run again with the next heavy sweep --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 9dbb99f7..4ffc4ba8 100644 --- a/TODO.org +++ b/TODO.org @@ -2188,7 +2188,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. 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 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. From aa1bbe550c0a73eb1da5a255868afe2de58f2509 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:04:50 +0700 Subject: [PATCH 05/24] A use-after-release check is to be studied before anything is built --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 4ffc4ba8..82913188 100644 --- a/TODO.org +++ b/TODO.org @@ -864,7 +864,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 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 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 From cf737076e9b3bf0580a06410b40963b930972df6 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:06:41 +0700 Subject: [PATCH 06/24] A moved Vec's old buffer is to be poisoned in a dev build, and the unread generation word goes --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 82913188..f3bf91a2 100644 --- a/TODO.org +++ b/TODO.org @@ -1242,7 +1242,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 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 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. From 763c8354963420477596b06d58b62152e71b1782 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:07:19 +0700 Subject: [PATCH 07/24] Per-version class layouts wait for a case that needs them --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index f3bf91a2..6ad8021e 100644 --- a/TODO.org +++ b/TODO.org @@ -1311,7 +1311,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 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 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 From 9cc820cb520da7a13c286c33d763bec0269bb2bd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:08:22 +0700 Subject: [PATCH 08/24] A class redefinition is to take a migration hook written from the live session --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 6ad8021e..f0c1f584 100644 --- a/TODO.org +++ b/TODO.org @@ -524,7 +524,8 @@ 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 +** 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 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 From 96b7ffb654fc77355f84e8ef21708f2274eb9fca Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:10:33 +0700 Subject: [PATCH 09/24] A program stays one compilation, which is why a generic's body is always there --- TODO.org | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/TODO.org b/TODO.org index f0c1f584..542a1ca8 100644 --- a/TODO.org +++ b/TODO.org @@ -737,10 +737,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 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 A program is one compilation, so a generic's body is always visible +CLOSED: [2026-09-25] +Odin's and Zig's model: packages are never compiled separately. The cost is build +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 CLOSED: [2026-09-13] From 764c5d4561787796ae90576ab90485bc7e44fdec Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:12:56 +0700 Subject: [PATCH 10/24] Every string literal is to end in a NUL on both backends, and a literal crosses to C uncopied --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 542a1ca8..eaf8fc9e 100644 --- a/TODO.org +++ b/TODO.org @@ -1120,7 +1120,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. 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, 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 From 1e462e06f6549e62c94e4f274ddc70f8cba0f5e8 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:13:30 +0700 Subject: [PATCH 11/24] Lexical blocks in DWARF wait for a debugger that needs them --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index eaf8fc9e..65e2cbb8 100644 --- a/TODO.org +++ b/TODO.org @@ -1132,7 +1132,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 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 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 From dab556ab7ffcb8e04b4dc7922a8c4f5bd8cf9115 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:14:04 +0700 Subject: [PATCH 12/24] Re-run under --two-process is to start a fresh child and say so --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 65e2cbb8..4d2fe09d 100644 --- a/TODO.org +++ b/TODO.org @@ -1476,7 +1476,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 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 merged-build only, and since the default backend runs merged it is no longer the blocked case. From 9dd7dd41376183e935657b8cc3ab8d854c23ad36 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:14:48 +0700 Subject: [PATCH 13/24] The agent socket is to bind only for the daemon that launched the program --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 4d2fe09d..83afb571 100644 --- a/TODO.org +++ b/TODO.org @@ -1570,7 +1570,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 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 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 From c1dcdeb5aff387fe3cce7e3b4cbae161ea03e56d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:15:47 +0700 Subject: [PATCH 14/24] A stop is to say whose it is from the frame that stopped --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 83afb571..cb5b1090 100644 --- a/TODO.org +++ b/TODO.org @@ -1517,7 +1517,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 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 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 From 49a7860cc47c28c7711562a6dfab7df4fa2c336c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:16:45 +0700 Subject: [PATCH 15/24] The truthiness retry is to reuse the first pass and stay linear --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index cb5b1090..e6cb60ad 100644 --- a/TODO.org +++ b/TODO.org @@ -827,7 +827,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 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 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 From ea19dedb90db422cac66f74468808a8bfb66a4f9 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:18:17 +0700 Subject: [PATCH 16/24] The corpus is to be swept once for leaks nothing owns --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index e6cb60ad..ccaa82c0 100644 --- a/TODO.org +++ b/TODO.org @@ -1380,7 +1380,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 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 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 From e85b2d3e98d8e68ccef6f1725e9ed44bc6997b29 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:19:02 +0700 Subject: [PATCH 17/24] A dev session is to run under the sanitizer on request --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index ccaa82c0..73efe62b 100644 --- a/TODO.org +++ b/TODO.org @@ -1730,7 +1730,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 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 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 From ca031d6b3892b9b607ca592e59a52f044c7f64a8 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:19:22 +0700 Subject: [PATCH 18/24] Every tracked Flan file is what the indenter would write, and a with- head indents its body like a definer clojure-mode decided each shape. A with- head, and a def or with- head behind a namespace, indents as a body; a qualified head finds its unqualified spec. The rest was hand formatting: cond and match results under their tests, ordinary call arguments under the first one, and lone ; continuation comments rewritten as ;; lines above the code. --- TODO.org | 13 +- emacs/flan-mode.el | 23 +++- emacs/test-flan-mode.el | 48 +++++++ examples/textures-image-generation.flan | 6 +- syntax-sketch.flan | 4 +- test/programs/arena-region.flan | 142 ++++++++++---------- test/programs/arena-value.flan | 12 +- test/programs/edn.flan | 9 +- test/programs/frame-rollback.flan | 2 +- test/programs/json.flan | 170 ++++++++++++------------ test/programs/pkgs/tree/tree.flan | 8 +- test/programs/utf8.flan | 6 +- vendor/edn/provide.flan | 112 ++++++++-------- vendor/edn/read.flan | 52 ++++---- vendor/json/provide.flan | 82 ++++++------ vendor/raylib/raylib.flan | 24 ++-- 16 files changed, 391 insertions(+), 322 deletions(-) diff --git a/TODO.org b/TODO.org index a76e8535..1ba9ec00 100644 --- a/TODO.org +++ b/TODO.org @@ -1804,10 +1804,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 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 Every tracked .flan file reindents to itself +CLOSED: [2026-09-25] +clojure-mode decided each shape. The indenter was wrong on one: a =with-= head, +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 No prompt, because the expression is already written in the buffer. =C-u= opens diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 3a78cbfa..d73d54f1 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -585,8 +585,22 @@ Anything not named here that begins with `def' is treated as `:defn' by `flan-indent-function'; anything else indents as a function call.") (defun flan--indent-spec (name) - "The indent spec for the form called NAME, or nil." - (and name (cdr (assoc name flan-indent-specs)))) + "The indent spec for the form called NAME, or nil. +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") "Loops that may carry a label, which `break' and `continue' name. @@ -755,8 +769,9 @@ decision to `calculate-lisp-indent'." ;; No spec. Anything else spelled `def…' is a definition and indents ;; like one, which covers `defstruct', `defdata', `defunion', ;; `defenum', `defonce', `defconst' and `defalias' without naming - ;; them. - ((and name (string-match-p "\\`def" name)) + ;; them. A `with-' form is a body too — `rl/with-drawing', + ;; `rl/with-mode-2d camera' — whatever it takes before the body. + ((flan--definer-p name) (+ lisp-body-indent head-column)) ;; A clause: `(name [params] body…)'. `handler-bind', `handler-case' ;; and `restart-case' all write their clauses this way, and the head is diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el index d150748b..be9d1761 100644 --- a/emacs/test-flan-mode.el +++ b/emacs/test-flan-mode.el @@ -326,6 +326,54 @@ y 2.0] (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 diff --git a/examples/textures-image-generation.flan b/examples/textures-image-generation.flan index 78d38739..4c4b8529 100644 --- a/examples/textures-image-generation.flan +++ b/examples/textures-image-generation.flan @@ -65,13 +65,13 @@ ;; to corner. (set (at textures 0) (upload (rl/gen-image-gradient-linear screen-width screen-height 0 - rl/red rl/blue))) + rl/red rl/blue))) (set (at textures 1) (upload (rl/gen-image-gradient-linear screen-width screen-height 90 - rl/red rl/blue))) + rl/red rl/blue))) (set (at textures 2) (upload (rl/gen-image-gradient-linear screen-width screen-height 45 - rl/red rl/blue))) + rl/red rl/blue))) ;; density 0 means the falloff reaches the edge of the image. (set (at textures 3) (upload (rl/gen-image-gradient-radial screen-width screen-height 0.0 diff --git a/syntax-sketch.flan b/syntax-sketch.flan index f52bb6eb..07bfce3b 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -13,8 +13,8 @@ ;; Normative references: spec-memory.md (ownership, containers, places, ;; 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 ───────────────────────────────────────────────────── ;; [4 f32] fixed array — a value, copies on assignment diff --git a/test/programs/arena-region.flan b/test/programs/arena-region.flan index 397b8170..69077139 100644 --- a/test/programs/arena-region.flan +++ b/test/programs/arena-region.flan @@ -28,82 +28,82 @@ (let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)] (cond (= which 1) - ;; The refusal. The context here is the heap, which can free one - ;; block, and a (Vec Value) against it is a free that would release - ;; the slots and strand every inner Vec — so the construction dies - ;; rather than the free three hundred lines later. - (let [bad (vec-new Value)] - (println (length bad))) + ;; The refusal. The context here is the heap, which can free one + ;; block, and a (Vec Value) against it is a free that would release + ;; the slots and strand every inner Vec — so the construction dies + ;; rather than the free three hundred lines later. + (let [bad (vec-new Value)] + (println (length bad))) (= which 2) - ;; Use after free-all, which is a different mechanism and worth - ;; pinning separately: the allocator's epoch moves on every free-all - ;; and every container records the epoch it was made at. The header - ;; below was copied *out* of the arena container into a local before - ;; the release, which is the case the check has to cover and the - ;; reason spec-memory.md makes an Allocator a pointer rather than a - ;; copied value — a copied allocator would carry its own epoch and - ;; the copy would never notice. - (let [outer (vec-new Value frame)] - (let [inner (vec-new Value frame)] - (push inner (Value.Int {.n (i64 7)})) - (push outer (Value.List {.items inner}))) - (match (at outer 0) - (List items) - (do (println (length items)) - (free-all frame) - (println (length items))) - _ (println 0))) + ;; Use after free-all, which is a different mechanism and worth + ;; pinning separately: the allocator's epoch moves on every free-all + ;; and every container records the epoch it was made at. The header + ;; below was copied *out* of the arena container into a local before + ;; the release, which is the case the check has to cover and the + ;; reason spec-memory.md makes an Allocator a pointer rather than a + ;; copied value — a copied allocator would carry its own epoch and + ;; the copy would never notice. + (let [outer (vec-new Value frame)] + (let [inner (vec-new Value frame)] + (push inner (Value.Int {.n (i64 7)})) + (push outer (Value.List {.items inner}))) + (match (at outer 0) + (List items) + (do (println (length items)) + (free-all frame) + (println (length items))) + _ (println 0))) (= which 3) - ;; ZII, which is the hole a guard only at the construction would have - ;; left. The items field is omitted from the literal, so it is a zeroed - ;; Vec with no allocator at all — it never went near (vec-new) — and - ;; the first push is what adopts the context. So the branch is emitted - ;; at every growth too, and there it asks the container, which answers - ;; from the allocator it will adopt when it has none of its own. - (let [v (Value.List {})] - (match v - (List items) - (do (push items (Value.Int {.n (i64 1)})) - (println (length items))) - _ (println 0))) + ;; ZII, which is the hole a guard only at the construction would have + ;; left. The items field is omitted from the literal, so it is a zeroed + ;; Vec with no allocator at all — it never went near (vec-new) — and + ;; the first push is what adopts the context. So the branch is emitted + ;; at every growth too, and there it asks the container, which answers + ;; from the allocator it will adopt when it has none of its own. + (let [v (Value.List {})] + (match v + (List items) + (do (push items (Value.Int {.n (i64 1)})) + (println (length items))) + _ (println 0))) :else - (do - ;; The control, and it is the case the frame tier exists for: a - ;; (Vec (Vec i32)) owns storage at two levels and is perfectly happy - ;; in a region, because free-all releases every block the region - ;; handed out and the inner ones are among them. The rule asks about - ;; the *allocator*, never "does this element own anything", so this - ;; must be built without complaint. - (with-allocator frame - (let [rows (vec-new Row)] - (let [row (vec-new i32)] - (push row 1) - (push row 2) - (push rows row)) - (println (length rows)) - (println (length (at rows 0))))) - (free-all frame) - ;; And the same container against the heap dies — asserted from the - ;; other side in run 1 above; here the point is only that the region - ;; run above got no complaint. - (with-allocator frame - (let [vs (vec-new Value)] - (push vs (Value.Int {.n (i64 41)})) - (println (length vs)))) - (free-all frame) - ;; And the zeroed field of run 3, this time in the region: the growth - ;; guard has to pass here as surely as it has to fail there, or every - ;; ZII container in an arena would be unusable. - (with-allocator frame - (let [v (Value.List {})] - (match v - (List items) - (do (push items (Value.Int {.n (i64 1)})) - (println (length items))) - _ (println 0)))) - (free-all frame)))) + (do + ;; The control, and it is the case the frame tier exists for: a + ;; (Vec (Vec i32)) owns storage at two levels and is perfectly happy + ;; in a region, because free-all releases every block the region + ;; handed out and the inner ones are among them. The rule asks about + ;; the *allocator*, never "does this element own anything", so this + ;; must be built without complaint. + (with-allocator frame + (let [rows (vec-new Row)] + (let [row (vec-new i32)] + (push row 1) + (push row 2) + (push rows row)) + (println (length rows)) + (println (length (at rows 0))))) + (free-all frame) + ;; And the same container against the heap dies — asserted from the + ;; other side in run 1 above; here the point is only that the region + ;; run above got no complaint. + (with-allocator frame + (let [vs (vec-new Value)] + (push vs (Value.Int {.n (i64 41)})) + (println (length vs)))) + (free-all frame) + ;; And the zeroed field of run 3, this time in the region: the growth + ;; guard has to pass here as surely as it has to fail there, or every + ;; ZII container in an arena would be unusable. + (with-allocator frame + (let [v (Value.List {})] + (match v + (List items) + (do (push items (Value.Int {.n (i64 1)})) + (println (length items))) + _ (println 0)))) + (free-all frame)))) (arena-destroy frame) 0) diff --git a/test/programs/arena-value.flan b/test/programs/arena-value.flan index 3c1b846b..8fd9a508 100644 --- a/test/programs/arena-value.flan +++ b/test/programs/arena-value.flan @@ -51,13 +51,13 @@ (match v (Int n) n (List items) - (let [t (i64 0)] - (dotimes [i (length items)] - (set t (+ t (total (at items i))))) - t) + (let [t (i64 0)] + (dotimes [i (length items)] + (set t (+ t (total (at items i))))) + t) (Table entries) - (+ (match (get entries "xs") (Some x) (total x) None (i64 0)) - (match (get entries "ys") (Some y) (total y) None (i64 0))) + (+ (match (get entries "xs") (Some x) (total x) None (i64 0)) + (match (get entries "ys") (Some y) (total y) None (i64 0))) _ (i64 0))) (defn build [] i64 diff --git a/test/programs/edn.flan b/test/programs/edn.flan index f57b7731..9b39d880 100644 --- a/test/programs/edn.flan +++ b/test/programs/edn.flan @@ -213,8 +213,9 @@ ;; ── The refusals, each asserted on its own reason ───────────────── (refusal "\"a\\nb\"") ; an escape inside a string - (refusal "\"a\\\"b\"") ; an escaped quote — the case where a wrong - ; version returns `a\` and leaves `b"` behind + ;; An escaped quote is the case where a wrong version returns `a\` and + ;; leaves `b"` behind. + (refusal "\"a\\\"b\"") ; an escaped quote (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 ;; `#{` that pushed nothing would answer "no error" for both of these. @@ -228,8 +229,8 @@ (refusal "\\a") ; a character literal (refusal "12x") ; starts like a number, is not one (refusal "[1 :]") ; a colon with no name - (refusal "@") ; not the start of any value — and the case a - ; scan-to-delimiter reads as a one-byte symbol + ;; `@` is the case a 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 "[1 2}") ; the wrong closer (refusal "]") ; a closer with nothing open diff --git a/test/programs/frame-rollback.flan b/test/programs/frame-rollback.flan index 2d3ffe66..1fae5248 100644 --- a/test/programs/frame-rollback.flan +++ b/test/programs/frame-rollback.flan @@ -154,7 +154,7 @@ (do (update-frame col) (set frames (+ frames 1))) (continue [] (restore) - (set skipped (+ skipped 1))))) + (set skipped (+ skipped 1))))) (defn main [] i32 (set (.brush world) 1) diff --git a/test/programs/json.flan b/test/programs/json.flan index b5010fc9..73be92bc 100644 --- a/test/programs/json.flan +++ b/test/programs/json.flan @@ -118,41 +118,41 @@ (defn read-value [c (Ptr json/Cursor) t json/Token] Value (cond (= (.kind t) json/tok-bool) - (Value.Bool {.b (match (json/bool-of t) (Some v) v None false)}) + (Value.Bool {.b (match (json/bool-of t) (Some v) v None false)}) (= (.kind t) json/tok-int) - (Value.Int {.n (match (json/int-of t) (Some v) v None (i64 0))}) + (Value.Int {.n (match (json/int-of t) (Some v) v None (i64 0))}) (= (.kind t) json/tok-float) - (Value.Float {.x (match (json/float-of t) (Some v) v None 0.0)}) + (Value.Float {.x (match (json/float-of t) (Some v) v None 0.0)}) (= (.kind t) json/tok-string) - (Value.Text {.s (match (json/string-of t) (Some s) s None "")}) + (Value.Text {.s (match (json/string-of t) (Some s) s None "")}) (= (.kind t) json/tok-array-open) - (let [items (vec-new Value) - u (json/next c) - more (and (json/ok? c) (!= (.kind u) json/tok-array-close))] - (while more - (push items (read-value c u)) - (let [sep (json/next c)] - (cond - (not (json/ok? c)) (set more false) - (= (.kind sep) json/tok-array-close) (set more false) - (!= (.kind sep) json/tok-comma) - (do (json/fail c json/err-unexpected-token (.pos sep)) - (set more false)) - :else - (do (set u (json/next c)) - ;; A comma and then the closer. Named, because a trailing - ;; comma is legal JSON5 and a reader that quietly allowed - ;; it would be reading a different format than it claims. - ;; - ;; The fail is what ends the loop, through the ok? test - ;; below it and not on its own — so these two are in this - ;; order on purpose, and swapping them spins. - (when (= (.kind u) json/tok-array-close) - (json/fail c json/err-trailing-comma (.pos u))) - (when (not (json/ok? c)) - (set more false)))))) - (Value.Array {.items items})) + (let [items (vec-new Value) + u (json/next c) + more (and (json/ok? c) (!= (.kind u) json/tok-array-close))] + (while more + (push items (read-value c u)) + (let [sep (json/next c)] + (cond + (not (json/ok? c)) (set more false) + (= (.kind sep) json/tok-array-close) (set more false) + (!= (.kind sep) json/tok-comma) + (do (json/fail c json/err-unexpected-token (.pos sep)) + (set more false)) + :else + (do (set u (json/next c)) + ;; A comma and then the closer. Named, because a trailing + ;; comma is legal JSON5 and a reader that quietly allowed + ;; it would be reading a different format than it claims. + ;; + ;; The fail is what ends the loop, through the ok? test + ;; below it and not on its own — so these two are in this + ;; order on purpose, and swapping them spins. + (when (= (.kind u) json/tok-array-close) + (json/fail c json/err-trailing-comma (.pos u))) + (when (not (json/ok? c)) + (set more false)))))) + (Value.Array {.items items})) ;; An object key is a quoted string and nothing else — an unquoted one was ;; already refused by the tokenizer as a bare word, so what reaches here is @@ -160,41 +160,41 @@ ;; same string-of the values use, so the map owns its keys and the source ;; buffer is not in the picture. (= (.kind t) json/tok-object-open) - (let [entries (map-new string Value) - k (json/next c) - more (and (json/ok? c) (!= (.kind k) json/tok-object-close))] - (while more - (if (!= (.kind k) json/tok-string) - (do (json/fail c json/err-unexpected-token (.pos k)) - (set more false)) - (let [key (match (json/string-of k) (Some s) s None "")] - (json/expect c json/tok-colon) - (if (not (json/ok? c)) - (set more false) - (let [v (json/next c)] - (if (not (json/ok? c)) - (set more false) - (do - (put entries key (read-value c v)) - (let [sep (json/next c)] - (cond - (not (json/ok? c)) (set more false) - (= (.kind sep) json/tok-object-close) (set more false) - (!= (.kind sep) json/tok-comma) - (do (json/fail c json/err-unexpected-token (.pos sep)) - (set more false)) - :else - ;; Same two steps, same order, same reason as the - ;; array arm: the fail ends the loop through the - ;; ok? test under it, and it has to, because the - ;; next pass would otherwise ask string-of for the - ;; text of a closing brace. - (do (set k (json/next c)) - (when (= (.kind k) json/tok-object-close) - (json/fail c json/err-trailing-comma (.pos k))) - (when (not (json/ok? c)) - (set more false)))))))))))) - (Value.Object {.entries entries})) + (let [entries (map-new string Value) + k (json/next c) + more (and (json/ok? c) (!= (.kind k) json/tok-object-close))] + (while more + (if (!= (.kind k) json/tok-string) + (do (json/fail c json/err-unexpected-token (.pos k)) + (set more false)) + (let [key (match (json/string-of k) (Some s) s None "")] + (json/expect c json/tok-colon) + (if (not (json/ok? c)) + (set more false) + (let [v (json/next c)] + (if (not (json/ok? c)) + (set more false) + (do + (put entries key (read-value c v)) + (let [sep (json/next c)] + (cond + (not (json/ok? c)) (set more false) + (= (.kind sep) json/tok-object-close) (set more false) + (!= (.kind sep) json/tok-comma) + (do (json/fail c json/err-unexpected-token (.pos sep)) + (set more false)) + :else + ;; Same two steps, same order, same reason as the + ;; array arm: the fail ends the loop through the + ;; ok? test under it, and it has to, because the + ;; next pass would otherwise ask string-of for the + ;; text of a closing brace. + (do (set k (json/next c)) + (when (= (.kind k) json/tok-object-close) + (json/fail c json/err-trailing-comma (.pos k))) + (when (not (json/ok? c)) + (set more false)))))))))))) + (Value.Object {.entries entries})) :else Value.Null)) @@ -204,34 +204,34 @@ (defn count-leaves [v Value] i32 (match v (Array items) - (let [n 0] - (dotimes [i (length items)] - (set n (+ n (count-leaves (at items i))))) - n) + (let [n 0] + (dotimes [i (length items)] + (set n (+ n (count-leaves (at items i))))) + n) ;; map-next fills an out-parameter with a copy of the value's bytes, which ;; for a Value holding a container is a second header over the same block. ;; In a region that is an alias and not a second owner, so walking a map is ;; the ordinary iteration and needs no accessor of its own. (Object entries) - (let [n 0 - cur (i64 0) - k "" - e Value.Null] - (while (map-next entries (addr cur) (addr k) (addr e)) - (set n (+ n (count-leaves e)))) - n) + (let [n 0 + cur (i64 0) + k "" + e Value.Null] + (while (map-next entries (addr cur) (addr k) (addr e)) + (set n (+ n (count-leaves e)))) + n) _ 1)) (defn sum-ints [v Value] i64 (match v (Int n) n (Array items) - (let [t (i64 0)] - (dotimes [i (length items)] - (set t (+ t (sum-ints (at items i))))) - t) + (let [t (i64 0)] + (dotimes [i (length items)] + (set t (+ t (sum-ints (at items i))))) + t) (Object entries) - (match (get entries "xs") (Some x) (sum-ints x) None (i64 0)) + (match (get entries "xs") (Some x) (sum-ints x) None (i64 0)) _ (i64 0))) (defn describe [v Value] string @@ -244,9 +244,9 @@ (defn text-at [v Value key string] string (match v (Object entries) - (match (get entries key) - (Some x) (match x (Text s) s _ "") - None "") + (match (get entries key) + (Some x) (match x (Text s) s _ "") + None "") _ "")) (defn read-doc [src [u8]] Value @@ -413,7 +413,7 @@ (println (sum-ints v)) ; [1 2 3] (println (describe (match v (Object e) (match (get e "gravity") (Some g) g None Value.Null) - _ Value.Null))) + _ Value.Null))) ;; The escapes, resolved. The quotes in `name` never existed as bytes ;; in the source; é is two bytes out of six and the emoji is four out ;; of twelve; and the eight one-character escapes are eight bytes. diff --git a/test/programs/pkgs/tree/tree.flan b/test/programs/pkgs/tree/tree.flan index 920cbf6d..7f8c067c 100644 --- a/test/programs/pkgs/tree/tree.flan +++ b/test/programs/pkgs/tree/tree.flan @@ -25,8 +25,8 @@ (match t (Leaf n) n (Branch kids) - (let [s (i64 0)] - (dotimes [i (length kids)] - (set s (+ s (total (at kids i))))) - s) + (let [s (i64 0)] + (dotimes [i (length kids)] + (set s (+ s (total (at kids i))))) + s) Empty (i64 0))) diff --git a/test/programs/utf8.flan b/test/programs/utf8.flan index fb8d27d0..4ecbd325 100644 --- a/test/programs/utf8.flan +++ b/test/programs/utf8.flan @@ -102,9 +102,9 @@ (show-dec (slice lone-cont 0 1)) ; a continuation byte leading (show-dec (slice overlong2 0 2)) ; overlong "/" (show-dec (slice overlong3 0 3)) ; overlong "/" again, three bytes - (show-dec (slice overlong4 0 4)) ; and four. Added after a mutation run: - ; relaxing 0xf0's floor to 0x80 left the - ; whole suite green without this line. + ;; Added after a mutation run: relaxing 0xf0's floor to 0x80 left the + ;; 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 above-max 0 4)) ; U+110000 (show-dec (slice lead-f5 0 4)) ; 0xf5 leads nothing diff --git a/vendor/edn/provide.flan b/vendor/edn/provide.flan index 6196ea6e..f9456dc6 100644 --- a/vendor/edn/provide.flan +++ b/vendor/edn/provide.flan @@ -216,8 +216,8 @@ (let [t (next c)] (when (not (ok? c)) (return (derived-bad - (joined3 "the data file could not be read at " (where src (error-pos c)) - (joined ": " (error-message (.err c))))))) + (joined3 "the data file could not be read at " (where src (error-pos c)) + (joined ": " (error-message (.err c))))))) (cond (= (.kind t) tok-int) (ok-derived `i64 (form-nil) `(need-int c)) (= (.kind t) tok-float) (ok-derived `f64 (form-nil) `(need-float c)) @@ -231,12 +231,12 @@ (= (.kind t) tok-nil) (derived-bad (joined3 "the nil at " (where src (.pos t)) - " has no type to derive — a field that is sometimes absent is not something a struct can hold, so give it a value in the file or take the key out")) + " has no type to derive — a field that is sometimes absent is not something a struct can hold, so give it a value in the file or take the key out")) :else (derived-bad (joined3 "the value at " (where src (.pos t)) - " is not one defedn derives a type from — a map, a vector, a set, an integer, a float, a boolean or a string"))))) + " is not one defedn derives a type from — a map, a vector, a set, an integer, a float, a boolean or a string"))))) ;; A vector, whose elements must all come to the same type. The first element ;; decides; every one after it is compared against that decision and both @@ -245,8 +245,8 @@ (defn- derive-vec [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \]) (return (derived-bad - (joined3 "the empty vector at " (where src at-pos) - " has no element to derive an element type from — defedn reads the shape out of the data, and an empty collection carries none")))) + (joined3 "the empty vector at " (where src at-pos) + " has no element to derive an element type from — defedn reads the shape out of the data, and an empty collection carries none")))) (let [head (derive c (joined name "-item") src)] (when (bad? head) (return head)) @@ -266,12 +266,12 @@ cn (Form.Sym {.s (joined name "-new")})] (ok-derived ty (with-decl (.decls head) `(defn ~cn [a Allocator] ~ty (vec-new a))) - `(let [xs (~cn a)] - (expect c tok-vec-open) - (while (and (ok? c) (not (at-byte? c \]))) - (push xs ~read1)) - (expect c tok-vec-close) - xs)))))) + `(let [xs (~cn a)] + (expect c tok-vec-open) + (while (and (ok? c) (not (at-byte? c \]))) + (push xs ~read1)) + (expect c tok-vec-close) + xs)))))) ;; Why every collection gets a one-line constructor of its own. ;; @@ -294,8 +294,8 @@ (defn- derive-set [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad - (joined3 "the empty set at " (where src at-pos) - " has no element to derive an element type from")))) + (joined3 "the empty set at " (where src at-pos) + " has no element to derive an element type from")))) (let [head (derive-key c (joined name "-key") src)] (when (bad? head) (return head)) @@ -315,12 +315,12 @@ cn (Form.Sym {.s (joined name "-new")})] (ok-derived ty (with-decl (.decls head) `(defn ~cn [a Allocator] ~ty (map-new a))) - `(let [tbl (~cn a)] - (expect c tok-set-open) - (while (and (ok? c) (not (at-byte? c \}))) - (put tbl ~read1 true)) - (expect c tok-map-close) - tbl)))))) + `(let [tbl (~cn a)] + (expect c tok-set-open) + (while (and (ok? c) (not (at-byte? c \}))) + (put tbl ~read1 true)) + (expect c tok-map-close) + tbl)))))) ;; One element of a set. The scalars that are map keys pass; a vector becomes a ;; fixed array, which is one where a Vec is not; anything else is refused here @@ -334,8 +334,8 @@ (return d)) (when (not (key-type? (.ty d))) (return (derived-bad - (joined3 "a set of " (render (.ty d)) - " is not something this builds: a set becomes a (Map T bool), so its elements are map keys. Integers, booleans, strings and vectors of those are")))) + (joined3 "a set of " (render (.ty d)) + " is not something this builds: a set becomes a (Map T bool), so its elements are map keys. Integers, booleans, strings and vectors of those are")))) d)) ;; A vector in key position. Its length is part of its type, so every element @@ -346,8 +346,8 @@ (let [open (next c)] (when (at-byte? c \]) (return (derived-bad - (joined3 "the empty vector at " (where src (.pos open)) - " is inside a set, and an empty fixed array has no element type and no length")))) + (joined3 "the empty vector at " (where src (.pos open)) + " is inside a set, and an empty fixed array has no element type and no length")))) (let [head (derive c (joined name "-item") src)] (when (bad? head) (return head)) @@ -364,30 +364,30 @@ (expect c tok-vec-close) (when (not (key-type? (.ty head))) (return (derived-bad - (joined3 "a set of vectors of " (render (.ty head)) - " is not something this builds: the vector becomes a fixed array, which is a map key only when its elements are compared bytewise")))) + (joined3 "a set of vectors of " (render (.ty head)) + " is not something this builds: the vector becomes a fixed array, which is a map key only when its elements are compared bytewise")))) (let [elem (.ty head) read1 (.reader head) count (Form.Int {.i n})] (ok-derived `[~count ~elem] (.decls head) - `(let [arr (array ~count ~elem) - i 0] - (expect c tok-vec-open) - (while (and (ok? c) (not (at-byte? c \])) (< i ~count)) - (set (at arr i) ~read1) - (set i (+ i 1))) - (expect c tok-vec-close) - arr))))))) + `(let [arr (array ~count ~elem) + i 0] + (expect c tok-vec-open) + (while (and (ok? c) (not (at-byte? c \])) (< i ~count)) + (set (at arr i) ~read1) + (set i (+ i 1))) + (expect c tok-vec-close) + arr))))))) (defn- disagreement [what string src [u8] at-pos i32 n i64 - first Form second Form] string + first Form second Form] string (joined3 (joined3 "the " what " at ") (where src at-pos) (joined3 (joined3 " holds more than one shape: its first element is " (render first) " and element ") (i64->string n) (joined3 " is " (render second) - ". Every element has to be the same shape, because the type this becomes has one element type")))) + ". Every element has to be the same shape, because the type this becomes has one element type")))) ;; ── A map, which is a struct ──────────────────────────────────────── ;; @@ -403,8 +403,8 @@ (defn- derive-map [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad - (joined3 "the empty map at " (where src at-pos) - " has no keys to derive fields from — a struct with no fields is not a shape anything can be read into")))) + (joined3 "the empty map at " (where src at-pos) + " has no keys to derive fields from — a struct with no fields is not a shape anything can be read into")))) (let [fields (vec-new Form) ; the defstruct's [name type ...] vector clauses (vec-new Form) ; the reader's cond: test, body, test, body missing (vec-new Form) ; one per field, checked when the map closes @@ -414,14 +414,14 @@ (let [k (next c)] (when (not (ok? c)) (return (derived-bad - (joined3 "the data file could not be read at " - (where src (error-pos c)) - (joined ": " (error-message (.err c))))))) + (joined3 "the data file could not be read at " + (where src (error-pos c)) + (joined ": " (error-message (.err c))))))) (when (!= (.kind k) tok-keyword) (return (derived-bad - (joined3 (joined3 "the map at " (where src at-pos) " has a key at ") - (where src (.pos k)) - " that is not a keyword. A struct's fields are named, so every key of a map defedn reads has to be one — :name, not \"name\" and not 1")))) + (joined3 (joined3 "the map at " (where src at-pos) " has a key at ") + (where src (.pos k)) + " that is not a keyword. A struct's fields are named, so every key of a map defedn reads has to be one — :name, not \"name\" and not 1")))) (let [fname (copy-text (.text k)) d (derive c (joined3 name "-" fname) src)] (when (bad? d) @@ -441,11 +441,11 @@ ;; bit is decided here, where the field is, so the two cannot fall ;; out of step the way a parallel list of names would. (push missing - `(when (= (bit-and seen ~bit) 0) - (signal (SchemaDrift {.field ~lit - .struct ~(Form.Str {.s name}) - .extra? false - .pos (.pos k)}))))) + `(when (= (bit-and seen ~bit) 0) + (signal (SchemaDrift {.field ~lit + .struct ~(Form.Str {.s name}) + .extra? false + .pos (.pos k)}))))) (set idx (+ idx 1))))) (expect c tok-map-close) ;; An unknown key. The hand-written reader skips one, which is right when a @@ -455,12 +455,12 @@ ;; handle the condition still reads the rest. (push clauses `:else) (push clauses - `(do (signal (SchemaDrift {.field (copy-text (.text k)) - .struct ~(Form.Str {.s name}) - .extra? true - .pos (.pos k)})) - (when (not (skip-value c)) - (return out)))) + `(do (signal (SchemaDrift {.field (copy-text (.text k)) + .struct ~(Form.Str {.s name}) + .extra? true + .pos (.pos k)})) + (when (not (skip-value c)) + (return out)))) (let [sname (Form.Sym {.s name}) rname (Form.Sym {.s (joined "read-" name)}) struct `(defstruct ~sname ~(Form.Vec {.xs (slice fields)})) @@ -539,7 +539,7 @@ (Some src) (provide name path src) None (refuse (joined3 "there is no file at " path - ", read relative to the file this defedn is written in — the same place (embed \"...\") would look"))) + ", read relative to the file this defedn is written in — the same place (embed \"...\") would look"))) _ (refuse "defedn's first argument is the name of the struct to declare, written as a name")) _ (refuse "defedn's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) diff --git a/vendor/edn/read.flan b/vendor/edn/read.flan index cb973ff1..1a8b0a1d 100644 --- a/vendor/edn/read.flan +++ b/vendor/edn/read.flan @@ -93,44 +93,44 @@ (= (.kind t) tok-symbol) (keyword (.text t)) (= (.kind t) tok-vec-open) - (let [items (vec-new dyn) - u (next c)] - (while (and (ok? c) - (!= (.kind u) tok-vec-close) - (!= (.kind u) tok-eof)) - (push items (read-value c u)) - (set u (next c))) - items) + (let [items (vec-new dyn) + u (next c)] + (while (and (ok? c) + (!= (.kind u) tok-vec-close) + (!= (.kind u) tok-eof)) + (push items (read-value c u)) + (set u (next c))) + items) ;; A set ends on tok-map-close, because `}` is the byte that ends it. The ;; dedup is the map's own: put replaces the value of an equal key, so a ;; set with a duplicate in it never exists and #{[0 0] [0 0]} is one ;; element by structure, not by header identity. (= (.kind t) tok-set-open) - (let [s {} - u (next c)] - (while (and (ok? c) - (!= (.kind u) tok-map-close) - (!= (.kind u) tok-eof)) - (put s (read-value c u) true) - (set u (next c))) - s) + (let [s {} + u (next c)] + (while (and (ok? c) + (!= (.kind u) tok-map-close) + (!= (.kind u) tok-eof)) + (put s (read-value c u) true) + (set u (next c))) + s) ;; A map's key is a whole value, read by the same recursion as anything ;; else — :a and "a" are two keys, [0 0] can key a map, and the old ;; (Map string Value) narrowing that collapsed them is gone with the type ;; that forced it. (= (.kind t) tok-map-open) - (let [m {} - k (next c)] - (while (and (ok? c) - (!= (.kind k) tok-map-close) - (!= (.kind k) tok-eof)) - (let [key (read-value c k) - u (next c)] - (put m key (read-value c u))) - (set k (next c))) - m) + (let [m {} + k (next c)] + (while (and (ok? c) + (!= (.kind k) tok-map-close) + (!= (.kind k) tok-eof)) + (let [key (read-value c k) + u (next c)] + (put m key (read-value c u))) + (set k (next c))) + m) :else nil)) diff --git a/vendor/json/provide.flan b/vendor/json/provide.flan index cc5fddef..8d85892b 100644 --- a/vendor/json/provide.flan +++ b/vendor/json/provide.flan @@ -177,8 +177,8 @@ (let [t (next c)] (when (not (ok? c)) (return (derived-bad - (joined3 "the data file could not be read at " (where src (error-pos c)) - (joined ": " (error-message (.err c))))))) + (joined3 "the data file could not be read at " (where src (error-pos c)) + (joined ": " (error-message (.err c))))))) (cond (= (.kind t) tok-int) (ok-derived `i64 (form-nil) `(need-int c)) (= (.kind t) tok-float) (ok-derived `f64 (form-nil) `(need-float c)) @@ -191,12 +191,12 @@ (= (.kind t) tok-null) (derived-bad (joined3 "the null at " (where src (.pos t)) - " has no type to derive — a field that is sometimes absent is not something a struct can hold, so give it a value in the file or take the key out")) + " has no type to derive — a field that is sometimes absent is not something a struct can hold, so give it a value in the file or take the key out")) :else (derived-bad (joined3 "the value at " (where src (.pos t)) - " is not one defjson derives a type from — an object, an array, a number, a boolean or a string"))))) + " is not one defjson derives a type from — an object, an array, a number, a boolean or a string"))))) ;; An array, whose elements must all come to the same type. The first decides; ;; every one after it is compared against that, and both positions are named @@ -205,8 +205,8 @@ (defn- derive-array [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \]) (return (derived-bad - (joined3 "the empty array at " (where src at-pos) - " has no element to derive an element type from — defjson reads the shape out of the data, and an empty collection carries none")))) + (joined3 "the empty array at " (where src at-pos) + " has no element to derive an element type from — defjson reads the shape out of the data, and an empty collection carries none")))) (let [head (derive c (joined name "-item") src)] (when (bad? head) (return head)) @@ -218,12 +218,12 @@ (return item)) (when (not (same-type? (.ty head) (.ty item))) (return (derived-bad - (joined3 (joined3 "the array at " (where src at-pos) - " holds more than one shape: element 0 is ") - (render (.ty head)) - (joined3 (joined3 " and element " (i64->string n) " is ") - (render (.ty item)) - ". Every element of an array has to be the same shape, because the (Vec T) it becomes has one element type"))))) + (joined3 (joined3 "the array at " (where src at-pos) + " holds more than one shape: element 0 is ") + (render (.ty head)) + (joined3 (joined3 " and element " (i64->string n) " is ") + (render (.ty item)) + ". Every element of an array has to be the same shape, because the (Vec T) it becomes has one element type"))))) (comma c) (set n (+ n 1)))) (expect c tok-array-close) @@ -238,21 +238,21 @@ ;; better for it too. (ok-derived ty (with-decl (.decls head) `(defn ~cn [a Allocator] ~ty (vec-new a))) - `(let [xs (~cn a)] - (expect c tok-array-open) - (while (and (ok? c) (not (at-byte? c \]))) - (push xs ~read1) - (comma c)) - (expect c tok-array-close) - xs)))))) + `(let [xs (~cn a)] + (expect c tok-array-open) + (while (and (ok? c) (not (at-byte? c \]))) + (push xs ~read1) + (comma c)) + (expect c tok-array-close) + xs)))))) ;; ── An object, which is a struct ──────────────────────────────────── (defn- derive-object [c (Ptr Cursor) name string at-pos i32 src [u8]] Derived (when (at-byte? c \}) (return (derived-bad - (joined3 "the empty object at " (where src at-pos) - " has no members to derive fields from — a struct with no fields is not a shape anything can be read into")))) + (joined3 "the empty object at " (where src at-pos) + " has no members to derive fields from — a struct with no fields is not a shape anything can be read into")))) (let [fields (vec-new Form) ; the defstruct's [name type ...] vector clauses (vec-new Form) ; the reader's cond: test, body, test, body missing (vec-new Form) ; one per field, checked when the object closes @@ -262,12 +262,12 @@ (let [k (next c)] (when (not (ok? c)) (return (derived-bad - (joined3 "the data file could not be read at " (where src (error-pos c)) - (joined ": " (error-message (.err c))))))) + (joined3 "the data file could not be read at " (where src (error-pos c)) + (joined ": " (error-message (.err c))))))) (when (!= (.kind k) tok-string) (return (derived-bad - (joined3 "the object at " (joined3 (where src at-pos) " has a member at " (where src (.pos k))) - " whose name is not a string, which JSON requires")))) + (joined3 "the object at " (joined3 (where src at-pos) " has a member at " (where src (.pos k))) + " whose name is not a string, which JSON requires")))) ;; The comparison in the generated reader is against the token's RAW ;; text, which costs no allocation per key. That is only the same ;; question as "is this the field" when the name has no escape in it — @@ -277,12 +277,12 @@ (let [raw (.text k)] (when (has-escape? raw) (return (derived-bad - (joined3 "the member name at " (where src (.pos k)) - " has an escape in it. A generated reader compares a key against the bytes as written, which costs nothing per key and is only the same question when the name is written plainly — so this one is refused rather than matched wrongly")))) + (joined3 "the member name at " (where src (.pos k)) + " has an escape in it. A generated reader compares a key against the bytes as written, which costs nothing per key and is only the same question when the name is written plainly — so this one is refused rather than matched wrongly")))) (when (not (name-like? raw)) (return (derived-bad - (joined3 "the member name at " (where src (.pos k)) - " is not a name a program could write, so there is no field it can become. A struct's fields are named; letters, digits, - and ? are what a name is made of")))) + (joined3 "the member name at " (where src (.pos k)) + " is not a name a program could write, so there is no field it can become. A struct's fields are named; letters, digits, - and ? are what a name is made of")))) (expect c tok-colon) (let [fname (copy-of raw) d (derive c (joined3 name "-" fname) src)] @@ -303,11 +303,11 @@ ;; beside the field, so the two cannot fall out of step the way a ;; parallel list of names would. (push missing - `(when (= (bit-and seen ~bit) 0) - (signal (SchemaDrift {.field ~lit - .struct ~(Form.Str {.s name}) - .extra? false - .pos (.pos close)}))))) + `(when (= (bit-and seen ~bit) 0) + (signal (SchemaDrift {.field ~lit + .struct ~(Form.Str {.s name}) + .extra? false + .pos (.pos close)}))))) (comma c) (set idx (+ idx 1)))))) (expect c tok-object-close) @@ -316,12 +316,12 @@ ;; program that declines to handle the condition still reads the rest. (push clauses `:else) (push clauses - `(do (signal (SchemaDrift {.field (match (string-of k) (Some s) s None "") - .struct ~(Form.Str {.s name}) - .extra? true - .pos (.pos k)})) - (when (not (skip-value c)) - (return out)))) + `(do (signal (SchemaDrift {.field (match (string-of k) (Some s) s None "") + .struct ~(Form.Str {.s name}) + .extra? true + .pos (.pos k)})) + (when (not (skip-value c)) + (return out)))) (let [sname (Form.Sym {.s name}) rname (Form.Sym {.s (joined "read-" name)}) struct `(defstruct ~sname ~(Form.Vec {.xs (slice fields)})) @@ -420,7 +420,7 @@ (Some src) (provide name path src) None (refuse (joined3 "there is no file at " path - ", read relative to the file this defjson is written in — the same place (embed \"...\") would look"))) + ", read relative to the file this defjson is written in — the same place (embed \"...\") would look"))) _ (refuse "defjson's first argument is the name of the struct to declare, written as a name")) _ (refuse "defjson's second argument is the path to the data file, written as a string literal — the file is read while this is being compiled, so there is nothing here to compute a path from")))) diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index a6c985e6..1c6a5d29 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -551,7 +551,7 @@ "CheckCollisionCircleRec") (declare-c collision-circle-line? [center Vector2 radius f32 - p1 Vector2 p2 Vector2] bool "CheckCollisionCircleLine") + p1 Vector2 p2 Vector2] bool "CheckCollisionCircleLine") (declare-c collision-point-rec? [point Vector2 rec Rectangle] bool @@ -562,13 +562,13 @@ "CheckCollisionPointCircle") (declare-c collision-point-triangle? [point Vector2 a Vector2 b Vector2 - c Vector2] bool "CheckCollisionPointTriangle") + c Vector2] bool "CheckCollisionPointTriangle") ;; `threshold` is in pixels, and it is not optional in practice: raylib's test ;; is a distance comparison in floats, so a point exactly on the line fails at ;; a threshold of 0. 1 is the useful smallest value. (declare-c collision-point-line? [point Vector2 p1 Vector2 p2 Vector2 - threshold i32] bool "CheckCollisionPointLine") + threshold i32] bool "CheckCollisionPointLine") ;; The one binding whose Flan face is not raylib's, and one of only two in ;; this file with a hand-written wrapper on top. A Flan slice crosses as @@ -685,12 +685,12 @@ "DrawTextureV") (declare-c draw-texture-ex [texture Texture2D position Vector2 rotation f32 - scale f32 tint Color] "DrawTextureEx") + scale f32 tint Color] "DrawTextureEx") ;; A negative source width or height flips the sprite, which is how a sheet is ;; drawn facing the other way without a second image. (declare-c draw-texture-rec [texture Texture2D source Rectangle position Vector2 - tint Color] "DrawTextureRec") + tint Color] "DrawTextureRec") ;; The two above, in one call, and the only one of the four that both takes a ;; source rectangle and scales: `source` picks a cell out of an atlas, `dest` @@ -933,10 +933,10 @@ ;; Angles are degrees, clockwise from the +x axis, and `segments` is how many ;; straight pieces the arc is made of — 0 lets raylib pick from the radius. (declare-c draw-ring [center Vector2 inner f32 outer f32 start f32 end f32 - segments i32 color Color] "DrawRing") + segments i32 color Color] "DrawRing") (declare-c draw-ring-lines [center Vector2 inner f32 outer f32 start f32 end f32 - segments i32 color Color] "DrawRingLines") + segments i32 color Color] "DrawRingLines") ;; Counter-clockwise, and raylib means it: the clockwise winding is culled and ;; draws nothing at all, which looks exactly like a broken binding. @@ -968,15 +968,15 @@ ;; `roundness` is 0 to 1 as a fraction of the shorter side, so 0 is a plain ;; rectangle and 1 is a stadium. (declare-c draw-rectangle-rounded [rec Rectangle roundness f32 segments i32 - color Color] "DrawRectangleRounded") + color Color] "DrawRectangleRounded") ;; No thickness here — see the section note. The `-ex` form below is the one ;; that takes it. (declare-c draw-rectangle-rounded-lines [rec Rectangle roundness f32 segments i32 - color Color] "DrawRectangleRoundedLines") + color Color] "DrawRectangleRoundedLines") (declare-c draw-rectangle-rounded-lines-ex [rec Rectangle roundness f32 - segments i32 thick f32 color Color] "DrawRectangleRoundedLinesEx") + segments i32 thick f32 color Color] "DrawRectangleRoundedLinesEx") ;; ── A slice where raylib wants a pointer and a count ───────────────── ;; @@ -1524,7 +1524,7 @@ ;; so a one-character string is unaffected by it. raylib's own DrawTextEx adds ;; it the same way measure-text-ex counts it, which is why the two agree. (declare-c draw-text-ex [font Font text string position Vector2 - font-size f32 spacing f32 tint Color] "DrawTextEx") + font-size f32 spacing f32 tint Color] "DrawTextEx") ;; Pure arithmetic over the font — no GL, no window — and therefore the one ;; thing in this section the acceptance table can assert. See the note above: @@ -1568,7 +1568,7 @@ "GetGlyphAtlasRec") (declare-c draw-text-codepoint [font Font codepoint i32 position Vector2 - font-size f32 tint Color] "DrawTextCodepoint") + font-size f32 tint Color] "DrawTextCodepoint") ;; DrawTextCodepoints and LoadFontData are not bound. The first is the slice ;; problem again and adds nothing draw-text-ex does not already do from a From 0e7a9ab98f135afc8fd0726b9272141a3f30660f Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:21:28 +0700 Subject: [PATCH 19/24] The x86 survey is to compare against LLVM at -O0 only --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index 73efe62b..f83e2878 100644 --- a/TODO.org +++ b/TODO.org @@ -1056,7 +1056,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 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 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 From e16b5cbceda867010b3f62354f4a3639d42052dd Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:23:18 +0700 Subject: [PATCH 20/24] A local's type is to print qualified by its package --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index f83e2878..a9e037da 100644 --- a/TODO.org +++ b/TODO.org @@ -1639,7 +1639,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 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 at import. Still open for locals, where the debug information gives a bare name and nothing qualifies it. From fc9f3059f9688f6a3812ce07ab1818c088cc49ac Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:23:28 +0700 Subject: [PATCH 21/24] C's calling convention for CFn waits with the callbacks --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index a9e037da..74c103ce 100644 --- a/TODO.org +++ b/TODO.org @@ -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 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 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 From 3a2837c2788036ecbf6ac72b7b5c7197b25fa482 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:31:34 +0700 Subject: [PATCH 22/24] The indent table's docstring says what a with- and a qualified head do --- emacs/flan-mode.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index d73d54f1..f5a45fac 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -581,8 +581,9 @@ For `syntax-propertize-function'." ("declare" . 1) ("declare-c" . 1)) "How each form indents, by name. -Anything not named here that begins with `def' is treated as `:defn' by -`flan-indent-function'; anything else indents as a function call.") +A qualified name falls back to the entry for its unqualified part. Anything +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) "The indent spec for the form called NAME, or nil. From aca3edff41e33d3a4c245caed6d9ea1f32f6064a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:32:05 +0700 Subject: [PATCH 23/24] CLAUDE.md keeps only the rules the code cannot teach --- CLAUDE.md | 143 ++++++++++++++---------------------------------------- 1 file changed, 36 insertions(+), 107 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3a08fcc2..2383974f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,125 +1,54 @@ # Working in this repository -For any agent working here, including a lane in its own worktree. These are -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. +Standing rules for any agent here, including a lane in its own worktree. ## Never -- **Never touch anything under `/home/joe/Development/fnm/`.** That is the - author's own working copy of the falling-sand game, open in an editor with a - live session attached. `fnm/flan/sand.flan` is not the same file as this - repository's `sand.flan` and is never to be read-and-written-back, moved or - edited. The copy in this repository is ordinary tracked source and may be - edited like any other file. -- **Never execute `examples/*.flan`, `sand.flan`, or anything that links - raylib.** They open a window on the author's desktop, which strobes it. - Compile them, emit them, diff them — never run them. -- **Never run `dune clean`.** It walks up out of a worktree and deletes the main - 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.** +- **Touch anything under `/home/joe/Development/fnm/`.** The author's live + working copy, with an editor session attached. This repository's `sand.flan` + is a different file and may be edited normally. +- **Execute `examples/*.flan`, `sand.flan`, or anything linking raylib.** They + open a window that strobes the author's desktop. Compile, emit, diff — never run. +- **Run `dune clean`.** It escapes a worktree and deletes the main checkout's + `_build`. Run `dune` with `--root .` from inside your own worktree. +- **Kill a `flan dev` daemon you did not start.** The author keeps one attached + to their editor. +- **Put `Co-Authored-By`, a `Claude-Session` trailer or any watermark in a commit.** -## Commits and branches +## Commits -`master` is the trunk and tracks `origin/master`. Lanes work in their own git -worktree off it and do not merge or push — merges are resolved by the session -that dispatched the lane. - -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. +`master` is the trunk. Lanes work in their own worktree and never merge or push; +the dispatching session merges. A commit message is one declarative sentence +saying what is now true, not what was done. ## Tests -`dune test --root .` is the suite and takes seconds. Run it constantly; it must -be green before a lane reports. - -`dune build @checks` is `@page`, `@x86` and `@cells` — the reference page's -examples still compile and print what the page says, the hand-written x86 -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`. +`dune test --root .` must be green before a lane reports; grep its output for +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 +the author's permission, never inside a lane. ASan misses uninitialised stack +reads; `@valgrind` catches them. ## Evidence -Read the source, do not recall it. `docs/REFERENCES.md` lists the reference -clones on this machine — Odin, SBCL, Zig, Carp, clojure-mode, CIDER, raylib and -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. +Read the source, do not recall it — this repository and the reference clones +listed in `docs/REFERENCES.md`. Verify a brief's facts before building on them. -The same applies to this repository. Cite a file and a line because you opened -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. +## Records -## Diagnostics +Keep only what the code cannot say. -Elm's shape: the source line, a caret, what the compiler understood, and the fix -named. Beyond that: +- `TODO.org` — one `**` heading per item under a subsystem: `TODO` a gap, `NEXT` + 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 - 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. +## Reviews -## Writing - -User-facing prose — the README, `web/index.html`, the Emacs manual — is plain. -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. +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 +number a lane reports. From 85a136882639feb3195cb913e257bcce7151e8c2 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:41:50 +0700 Subject: [PATCH 24/24] The Vec header keeps its epoch in every build, and the spec is to say five words --- TODO.org | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TODO.org b/TODO.org index b3eb5f76..3a2c14d5 100644 --- a/TODO.org +++ b/TODO.org @@ -1264,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 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 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