The docs described a language that stopped existing today

A consistency sweep, run by checking claims against the compiler rather
than reading for style. Every edit here corrects something that is false
now, or adds something the page had no way to say.

`web/index.html` was the worst of it, and the worst of that was control
flow: the page said there is no `loop`/`recur` and no `break` or
`continue`, and printed the refusal message for `break` as evidence. All
four are built, with loop labels. A live code example called
`index-of-i32`, which no longer exists and would not compile. The prelude
table was the pre-generics per-type families, a paragraph said there is
no `println` two paragraphs after calling `println` the compiler's, and
`sqrt-f32` was "the one `declare` in the file" when there are five. The
"Not implemented yet" table listed `Vec`, `Map`, `Handle`, `Fn`, `fn`,
unions, `defmacro` and quasiquote, all of which check; what is actually
left is `Result`, `try`, a quoted symbol as a value, `errdefer`, `await`,
`handler-case` and the restart-stack readers. Restarts take parameters
(`(invoke-restart 'use-value 21)` answers 42), `defer` in a `let` is
allowed, and there is both an allocator and a `context`.

Generics is a new section, because nobody had documented the syntax. The
brief for it was wrong in one place and the corpus settled it: `$t` goes
in *every* type position including the return type, and bare `t` is the
type-name argument in expression position — `(vec-new t)`, `(t x)`. It
says what a type variable is move-only by default means, since that is
the rule a reader hits first and it is not Odin's.

`FLAN_RAYLIB_H` is gone from every doc that claimed it still decided
something. The passages that say "this used to be opt-in" are kept and
labelled; the ones that said "this is opt-in" are not. `plan.org` had
`{string i32}` in the type list and four predicates where there are five.
`conditions.org` described `errdefer`'s behaviour without saying it is
refused by name. `REFERENCES.md` pointed at the gitignored copy of the
raylib header rather than the committed one, which is the exact trap that
made committing it necessary.

Found and not fixed, because it is not documentation: `vendor/raylib/headers`
still says a build reads it "when the variable happens to be set", which
contradicts the section below it in the same file and is false — moving
the header makes every build fail by name.
This commit is contained in:
Joseph Ferano 2026-09-13 18:04:24 +07:00
parent 888915402e
commit 3e181b52b2
8 changed files with 516 additions and 117 deletions

View File

@ -227,7 +227,7 @@ form, no new `decl_kind`, no reader or parser change. A C symbol the package
already binds by hand is left alone, so `declare-c` stays the escape hatch and
stays the thing that wins.
A leading `?` makes a line optional, and `vendor/raylib` uses it. "No raylib
A leading `?` makes a line optional, and `vendor/raylib` used it. "No raylib
headers are needed" is a real property — a build needs libraylib linkable, not
raylib-devel installed — and requiring a header would take it from everyone in
order to give the check to whoever has one. Unset `FLAN_RAYLIB_H` and the build
@ -235,6 +235,12 @@ is exactly what it was; set it and every signature is checked. A path that is
*set and wrong* is an error naming it, because silently behaving as though
nobody had opted in is the difference between an opt-in and a trap.
*(Superseded. The `?` marker is still what it says here, but `vendor/raylib` no
longer uses it: the header is committed at `vendor/raylib/raylib-5.5.h`, the
line names it directly, and the check runs on every build. See "The header is
committed too" below, which is where the argument in this paragraph is
answered.)*
#### What the diff found
The evidence the whole lane exists for. Against raylib **5.5** — the version
@ -489,6 +495,34 @@ nothing and the read is purely the check. It is a *better* check than before:
425 declarations rather than 172, because `generated.flan` is a package file
like any other and is checked like one.
#### The header is committed too, and the check is no longer opt-in
The paragraph above is superseded: there is no `FLAN_RAYLIB_H`, and the header
read is not conditional on anything. The header is committed at
`vendor/raylib/raylib-5.5.h` and `headers` names that path directly, so the
check runs on **every** build.
**What dissolved the opt-in argument is the commit, not a change of mind about
the property.** "A build needs libraylib linkable and not raylib-devel
installed" is still true and still the reason the opt-in existed — but nobody
needs raylib-devel to have a file that ships with the repository, so requiring
the header costs nobody anything. The trade the `?` was paying for stopped
existing.
**What being optional actually cost was found the hard way**, and the story is in
`vendor/raylib/headers` rather than repeated here. The short of it: the header a
tree happened to have lived under a gitignored directory, so several parallel
lanes were checking against nothing and were not told. **A check that silently
does not run is worse than no check** — the failure mode of an opt-in is not
"the check does not run", it is "the check does not run and the output looks the
same".
There is no environment variable for the path any more. To check against a
different header, edit the line or replace the file. The check still runs over
every declaration in the package, hand-written and generated alike, and only the
hand-written half can actually disagree — for the reason the section above gives,
which is unchanged.
**`bindings`, beside `headers`, is what survives regeneration.** A committed
generated file cannot be hand-corrected — the next run overwrites it and the
edit is destroyed without anybody being told, which is the worst shape an edit
@ -4564,9 +4598,11 @@ and how big, and a 16px tile drawn at 4x is a dest four times the source. `draw-
scale; `draw-texture-ex` has the scale and no source. Neither half draws a tile.
It *was* reachable — with `FLAN_RAYLIB_H` exported the importer brings it in with 250-odd others — and that is the
finding worth keeping. `vendor/raylib/headers` keeps the import opt-in on purpose, so a build needs libraylib linkable
finding worth keeping. `vendor/raylib/headers` kept the import opt-in on purpose, so a build needs libraylib linkable
and not raylib-devel installed. That property is worth keeping and it means **the default build had no draw call for a
grid-based game**. The rule that follows: *a raylib function on a game's per-frame path is hand-written in
grid-based game**. (The opt-in is gone — the header and the generated bindings are both committed now, so the default
build has every declaration. The rule below is unaffected: it is about where a per-frame call is *written*, not about
what the import happens to reach.) The rule that follows: *a raylib function on a game's per-frame path is hand-written in
`raylib.flan` and checked against the header; it is not left to the import.* The import widens the surface; it must
not be load-bearing.

View File

@ -227,7 +227,11 @@ would become an integer at the call site. A migration is therefore not a deletio
kept list of the lines whose face is deliberately nicer than the header's.
**Answered: no, and the blocker is not the one above.** Vendoring stopped being the question once the *output* was
committed rather than the header — no header is needed at any build. The count was also smaller than feared: 136 of
committed rather than the header — no header is needed at any build. (Vendoring then happened anyway, and for the
other reason: `vendor/raylib/raylib-5.5.h` is committed, `headers` names it with no `${...}` in front of it, and the
signature check runs on every build. What that settled was not "can we build without a header" — the committed
bindings had already settled that — but "does the check actually run", and behind an opt-in the answer in several
worktrees was no.) The count was also smaller than feared: 136 of
the 172 are exactly what the rule produces, and the other 36 are expressible as `name` overrides in `bindings`.
What actually decides it is that migration would gut the check. Everything the generator emits agrees with the header

View File

@ -58,10 +58,13 @@ draw, the palette, and the tile riding the cursor are all of the drawing there i
`Rectangle`, `Vector2` and `Color`, all four of which `raylib.flan` already describes, so
it is a `declare-c` beside `draw-texture-rec` and nothing else.
**Why it did not already land.** It is importable — with `FLAN_RAYLIB_H` exported it
comes in with the other 256. But `vendor/raylib/headers` says the header is opt-in on
**Why it did not already land.** It was importable — with `FLAN_RAYLIB_H` exported it
came in with the other 256. But `vendor/raylib/headers` made the header opt-in on
purpose, so that a build needs libraylib linkable and *not* raylib-devel installed. That
property is worth keeping, and it means the default build of this game has no renderer.
property is worth keeping, and it meant the default build of this game had no renderer.
*(Both halves of that changed after this was written: the generated bindings and the
header are committed, so the default build has every declaration and the header check is
not opt-in. The rule below survives it — see the note at the end of §1.)*
The general rule this exposes is worth more than the one line: **a function the game
calls every frame should be hand-written and checked against the header, not left to the
import.** The import widens the surface; it should not be load-bearing.
@ -112,10 +115,20 @@ carve reads out of a 2×1 image and the case goes red.
**And the rule Tier 0 item 4 asked for, written down:** *a raylib function on a game's
per-frame path is hand-written in `raylib.flan` and checked against the header; it is
not left to the opt-in import.* The import widens the surface and is worth having, but a
build that does not have `FLAN_RAYLIB_H` set is the default build, and the default build
build that did not have `FLAN_RAYLIB_H` set was the default build, and the default build
has to be able to draw. The test that goes with the rule is a link check, which is cheap
and is all a GL-context call can have.
**Update — the justification moved, the rule did not.** There is no `FLAN_RAYLIB_H` any
more. `vendor/raylib/generated.flan` holds every declaration the importer produced and is
committed; `vendor/raylib/raylib-5.5.h` is committed beside it and `headers` names it
directly, so the signature check runs on every build rather than on the builds that had a
variable exported. The original argument — "the default build has no header, so it has no
`DrawTexturePro`" — is therefore dead. The rule outlives it for a different reason: a
per-frame call hand-written in `raylib.flan` is one a person transcribed, and the
hand-written lines are the only declarations in the package a header can actually
contradict. Everything generated agrees with the header by construction.
---
## 2. What looks like a gap and is not
@ -254,8 +267,11 @@ project has queued generics for.
`Rectangle`s. Twenty elements, once, at load.
- `game.clj` `init-game`'s `mapv` over four `[keyword path]` pairs to textures. Four
elements, once.
- The prelude is monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
anything over `[r c]` pairs has no helper at all.
- The prelude was monomorphic per element type (`sort-i32!`, `map-f32!`, `reduce-i32`), so
anything over `[r c]` pairs had no helper at all. *(Generics landed after this was
written: those families are one `sort!`, `map!` and `reduce` now, each over a type
variable, so a helper does exist for a slice of pairs. It does not change the count
below — three loops, five lines each, at load time.)*
All three are `dotimes` with a `push`. Five lines each, at load time, run once.
@ -355,7 +371,7 @@ and Flan rejects it at compile time. Part of that rollback machinery is answerin
dynamic-language problem and is not a gap. The out-of-bounds path is the part that
survives the port, and it is the part that matters.
### The watch buffer has no equivalent
### The watch buffer has no equivalent — *closed, Tier 1 item 5*
`watch.clj` is a pull-based watch: the game drops a snapshot of labelled values into an
atom once per frame, and Emacs polls it on its own timer, so the watch rate is decoupled
@ -377,6 +393,15 @@ shows you a number while you drag the mouse.
Whether `flan dev`'s `eval` against a running program could serve it — it does not gate on
`Running` the way `globals_op` does — I did not test, and should not be assumed.
**Closed.** Both halves are built. `M-x flan-watch` is the buffer, fed by a table the
*program* writes and Emacs reads as memory — pushed rather than polled, so the values are
as fresh as the last frame and are still there while the program is stopped.
`watch-num-i64` / `watch-num-f64` are the `spy-num` half, keeping count/min/max/last/mean
per slot with no formatting on the write path. `M-x flan-watch-ghost-mode` shows the same
values inline at the call that wrote each one. The finding above stands as the reason the
inspector was not the answer: it is stopped-only, and this is a different tool. See Tier 1
item 5 and `emacs/MANUAL.md`.
### Offline image tooling
`sprite_atlas.clj` `auto-select-tiles` carves the atlas into 16x16 subimages and keeps the
@ -509,8 +534,10 @@ not compete for the same slot.
4. ~~**Decide the rule the first item exposes.**~~ Decided and written into §1: *a raylib
function on a game's per-frame path is hand-written in `raylib.flan` and checked
against the header, not left to the opt-in import.* The default build has no
`FLAN_RAYLIB_H` and the default build has to be able to draw.
against the header, not left to the opt-in import.* The default build had no
`FLAN_RAYLIB_H` and the default build has to be able to draw. (That justification is
obsolete — the bindings and the header are both committed now — but the rule stands on
the other leg: see the update at the end of §1.)
### Tier 1 — language and tooling, in the order that unblocks the most of this game

View File

@ -17,7 +17,7 @@ its newest release does.
| `zig` | 2026-09-12 | Comptime, build system, error handling, C interop. |
| `jank` | 2026-09-11 | A Clojure with native interop and a REPL into a running process. |
| `janet` | 2025-09-29 | A small embeddable Lisp. `varfn` and its reload story. |
| `raylib` | 2026-07-16 | The C library the FFI binds. **Note:** `vendor/raylib/web/raylib-5.5/` in *this* repo is the vendored 5.5 the bindings are checked against; the clone may be a different version. |
| `raylib` | 2026-07-16 | The C library the FFI binds. **Note:** `vendor/raylib/raylib-5.5.h` in *this* repo is the committed header the bindings are checked against on every build; the clone may be a different version. It used to be whatever `vendor/raylib/web/` happened to hold, which is gitignored and therefore absent from a fresh worktree — the reason the header is tracked now. |
### Odin, specifically
@ -25,7 +25,9 @@ Most of what this repository copies. Places already cited in the notes:
- `base/runtime/core.odin:369``Map_Info`. The type-erased container runtime: `rawptr` keys, cell
info for size and alignment, hash and equality as function pointers. This is why `Vec` and `Map`
here need no generics, and it is the thing **not** to convert when generics lands.
here need no generics, and it is the thing that was **not** converted when generics
landed — `Vec` and `Map` are still type-erased, and a `$t` only reaches them as an
element or key type.
- `base/runtime/dynamic_map_internal.odin` — the map algorithm `(Map K V)` follows.
- `base/runtime/dynamic_array_internal.odin` — the same for `Vec`.
- `src/check_expr.cpp:470-510``find_or_generate_polymorphic_procedure`. The instantiation

View File

@ -45,7 +45,8 @@ stack and neither end can see the other.
#+end_src
~defer~ between the invoke and the target runs, innermost first, before the
clause body. ~errdefer~ does not.
clause body. (~errdefer~ would not, and is in *Not yet* below — it is refused by
name today.)
* The break loop
@ -64,8 +65,8 @@ above it, or ~abort~.
* Not yet
~handler-case~ · ~find-restart~ · ~compute-restarts~ · a clause's report
string. Each refused by name with its reason.
~handler-case~ · ~find-restart~ · ~compute-restarts~ · ~errdefer~ · a clause's
report string. Each refused by name with its reason.
A restart *with parameters* cannot be taken from the break loop: it aims at a
frame by position and has nothing to fill the parameters in with, so the clause
@ -91,6 +92,10 @@ the agent's socket instead.
frame re-runs everything, mutations included; one placed after the mutations
re-runs only what follows. Put the restart before anything mutates, make the
retried section idempotent, or snapshot what will be re-applied.
~test/programs/frame-rollback.flan~ is the worked example of the snapshot,
including the ordering that matters: restore *in the restart clause*, not in a
~defer~ — a defer runs on the ordinary return path too, so that version silently
rolls back the frames that succeeded.
This bites harder here than in most Lisps because the point is a *game loop*
— skip the frame, carry on, don't die. Since a bad index signals

View File

@ -355,6 +355,31 @@ never appeared would send you looking for a bug in the program. A name is
truncated at 31 bytes and a rendered value at 192, with an ellipsis where a
value was clipped.
**A number sampled thousands of times a frame — `watch-num-i64`,
`watch-num-f64`.** The scalar entry points keep one value per name, and from a
hot inner loop that is nearly useless: you see whichever of the 91,200 cells ran
last. These two are the other half. A slot keeps **count, min, max, last and
mean**, and the buffer renders them as one line:
```flan
(declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64")
(declare-c watch-num-f64 [name string x f64] i32 "flan_dev_watch_num_f64")
(watch-num-i64 "cell" (at grid i))
```
The slot renders as `n=… min=… max=… last=… mean=…`, one line per name.
Two entry points rather than one so a program need not cast at the call site.
The write path does **no formatting** — a sample is a load, five compares and
the slot's seqlock — and the listener thread renders once per editor tick, which
is the whole reason this exists rather than a second `watch-i64`. **The window is
since the editor's last tick**, not since the program started: a min and a max
over a whole session reach the session's extremes within seconds and then never
move again, so the two most useful of the five would go dead exactly when you
start playing. A whole number prints as one, because a spy on an array index
reading `66.0000` sends you looking for a rounding bug that is not there.
**Scalars only, so far.** `i64`, `u64`, `f64` and `string` have entry points; a
struct or a slice does not. That is not an oversight in the runtime — a Flan
value carries no header, so rendering one is a walk over its *type* at compile

View File

@ -205,14 +205,21 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
the parser guessed between a return type and a body form from a table of type
names; the guess was silently wrong twice, so the slot is mandatory.
- Every type notation reads as exactly one data item: ~[f32]~, ~[4 f32]~,
~(Vec f32)~, ~{string i32}~, ~(Ptr World)~, ~(Fn [f32] bool)~, ~(Option a)~,
~(Handle a)~.
~(Vec f32)~, ~(Map string i32)~, ~(Ptr World)~, ~(Fn [f32] bool)~,
~(Option $t)~, ~(Handle $t)~. The map spelling was ~{string i32}~ once and is
not any more: braces in type position are refused by name. The ~where~ clause
below is a brace form at the head of a body, so keeping the brace type would
have put ~(defn f [...] {string i32} {:where ...} body)~ in the language — two
braces in a row meaning different things.
- ~i8..i64~, ~u8..u64~, ~f32~, ~f64~ as real machine types; wrapping arithmetic.
- Vector width 128-bit. Fixed arrays with component-wise ops and swizzles.
- Parametric polymorphism by monomorphisation (Odin's model, no type classes, no
HKTs). A type variable is written ~$t~ at its binding site in a signature and
bare ~t~ at a use. This is what makes ~map~/~filter~/~reduce~ and the monomorphic
containers work.
HKTs). A type variable is written ~$t~ wherever a *type* goes — parameter,
return type, or nested as ~[$t]~ or ~(Vec $t)~ — and bare ~t~ wherever a type's
*name* is an argument in expression position: ~(vec-new t)~, ~(map-new t i32)~,
~(pool-new t)~, and the cast ~(t x)~. This is what makes
~map!~/~filter~/~reduce~ and the monomorphic containers work; it collapsed the
prelude's per-type families into one function each.
A generic body is checked *abstractly*, with nothing substituted, so ~=~, ~<~,
~+~ and ~hash~ over an unconstrained variable are rejected at the definition
rather than at whichever call site first instantiates it. **This is not Odin's
@ -220,11 +227,28 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
decisions are independent even though this line once bundled them.
What makes the rejection liveable is a ~where~ clause over compile-time type
predicates, which is Odin's (~core/slice/slice.odin:289~,
~where intrinsics.type_is_ordered(T)~). ~(sort! xs)~ declares ~ordered?~ of its
~where intrinsics.type_is_ordered(T)~). It is written as a Clojure-style map at
the head of the body — ~{:where (ordered? $t)}~, or a vector for more than one,
~{:where [(copyable? $t) (copyable? $u)]}~ — on the precedent of Clojure's
~{:pre ... :post ...}~, and because a bare ~{}~ in expression position is
already refused so nothing else it could be. ~sort!~ declares ~ordered?~ of its
variable, the abstract pass then allows ~<~ in the body, and each instantiation
checks the concrete type satisfies the predicate and refuses the call site if it
does not. Roughly four predicates are wanted here — ~ordered?~, ~equal?~,
~hashable?~, ~numeric?~ — against Odin's forty-one.
does not. There are *five* predicates — ~ordered?~, ~equal?~, ~hashable?~,
~numeric?~, ~copyable?~ — against Odin's forty-one, and they entail one another
in one direction, so one clause usually does: ~numeric?~ gives ~ordered?~,
~ordered?~ gives ~equal?~, and any of the four gives ~copyable?~.
~copyable?~ has no Odin counterpart, because Odin has no move semantics and a
~$T~ there never has to answer the question. *A type variable is move-only by
default* and ~copyable?~ is the opt-out: whether a variable is move-only is not
decidable abstractly — ~i32~ at one instantiation, ~(Vec i32)~ at the next — so
the checker takes the stricter rule, which can only refuse a program that would
have been fine and never admit one that double-frees. The prior art is Rust's
~T: Copy~, differing in that the compiler answers the question rather than a
user implementing a trait.
~hashable?~ is what lets a variable *key a map*: without it the type
~(Map $t i32)~ is refused where it is written, and with it the refusal moves to
the call site that names an unhashable key.
This is not a type class and the difference is worth keeping straight: a type
class carries *implementations* selected per instance and extensible by anyone,
and needs dictionaries and coherence rules. A predicate carries nothing — it
@ -232,11 +256,15 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
a user-defined ~<~; every operation the prelude and the containers need is a
primitive, so it does not bind. Compile-time interfaces, if they are ever
wanted, come after the base checker is stable.
~println~ is a compiler-provided exception and needs an explicit allow-list of
~println~ is a compiler-provided exception and is on an explicit allow-list of
forms the abstract pass defers to instantiation: whether a printer exists for a
variable is only decidable once it is substituted. Keep the list short — every
member moves a refusal from the definition to a call site, which is what the
abstract rule exists to avoid.
variable is only decidable once it is substituted. The ~Map~ operations over a
variable key are the other member, for the same reason — the hash and the
equality are concrete symbols chosen from the concrete key type — and that one
is paid for by ~hashable?~ being written in the signature, so the refusal it
moves still lands against a requirement somebody wrote down. Keep the list
short: every member moves a refusal from the definition to a call site, which
is what the abstract rule exists to avoid.
- Function values split three ways (spec-memory.md): ~(Fn [T1 T2] R)~ is a plain
pointer with no environment — the only kind that crosses FFI or sits in a reload
cell; a *non-escaping* ~fn~ captures enclosing locals by value into a stack
@ -850,8 +878,12 @@ marked.
- Dev backend → interpreter for milestone 2 certainly. Whether it /survives/
milestone 3 is open, not settled — see Compilation.
- ~set~ on places → a fixed list of assignable forms, not ~setf~.
- Loop story → imperative ~while~/~for~ with ~break~/~continue~ and ~return~;
~loop~/~recur~ only if it later earns its place. sand.flan is ported.
- Loop story → imperative ~while~/~until~/~dotimes~ with ~break~/~continue~ and
~return~; ~loop~/~recur~ only if it later earns its place. It did: both are
built. ~break~ and ~continue~ take a label for the loop that is not the
innermost, and ~loop~ is an *expression* whose value is its body's, with
~recur~ rebinding every name at once and jumping rather than calling.
sand.flan is ported.
- Generic parameters → inferred at call sites, no explicit instantiation. No type
classes. An operator over a variable with no ~where~ clause asserting it is
rejected at the definition; a ~where~ predicate is what admits it.

View File

@ -229,6 +229,7 @@ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--ru
<li><a href="#control">Control flow</a></li>
<li><a href="#defer">defer</a></li>
<li><a href="#arrays">Arrays and slices</a></li>
<li><a href="#generics">Generics</a></li>
<li><a href="#printing">Printing</a></li>
<li><a href="#prelude">The prelude</a></li>
<li><a href="#packages">Packages</a></li>
@ -297,8 +298,9 @@ rather than programs.</p>
<p>A minimal Lisp for games. In one line: Odin with a Lisp frontend and a live REPL.
Types are mandatory and inference makes them feel optional; memory is manual; the
frontend is OCaml, the backend writes LLVM IR as text and hands it to
<code>clang</code>.</p>
frontend is OCaml, and the default backend writes LLVM IR as text and hands it to
<code>clang</code>. There is a second one, off by default, that emits x86-64 by hand —
see <a href="#targets">targets and builds</a>.</p>
<p>What it is not:</p>
@ -326,7 +328,9 @@ $ ./_build/default/bin/main.exe run calc-me.flan "1 + 2 * (3 - 0.5) / 2"
<pre><code class="sh">$ flan
usage: flan (read|parse|check|emit|shim) &lt;file.flan&gt;...
flan build &lt;file.flan&gt; [-o out] [--no-bounds-checks] [--dev] [--debug] [--target=wasm32-wasi]
flan import-c &lt;header.h&gt; [package.flan...] [clang flags...]
flan generate-c &lt;package-dir&gt;
flan build &lt;file.flan&gt; [-o out] [--no-bounds-checks] [--dev] [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]
flan run &lt;file.flan&gt; [args...]
flan reload &lt;program.flan&gt; &lt;forms.flan&gt; [-o out.so]
flan dev &lt;program.flan&gt; [-s socket]</code></pre>
@ -444,9 +448,17 @@ notation reads as exactly one data item.</p>
<tr><td><code>string</code></td><td>a byte slice with no NUL</td><td>ptr + len</td></tr>
<tr><td><code>[T]</code></td><td>slice, non-owning</td><td>ptr + len</td></tr>
<tr><td><code>[n T]</code></td><td>fixed array, a value</td><td>n inline items</td></tr>
<tr><td><code>(Vec T)</code></td><td>growable, owning — <em>moves</em> on assignment</td><td>ptr + len + cap + its allocator</td></tr>
<tr><td><code>(Map K V)</code></td><td>open addressing, owning — <em>moves</em>. The only map spelling: braces in type position are not a type</td><td>data + len + log2cap + its allocator</td></tr>
<tr><td><code>(Pool T)</code></td><td>generational slab storage, owning — <em>moves</em></td><td>items + slots + its allocator</td></tr>
<tr><td><code>(Handle T)</code></td><td>a reference into a pool that reports a dead referent</td><td>index and generation packed into an <code>i64</code></td></tr>
<tr><td><code>(Ptr T)</code></td><td>raw pointer</td><td>a pointer</td></tr>
<tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr>
<tr><td><code>(Fn [T ...] R)</code></td><td>a function value</td><td>a pointer</td></tr>
<tr><td><code>Allocator</code></td><td>an opaque builtin: a proc, its data and a capability set</td><td>a pointer to that</td></tr>
<tr><td><code>$t</code></td><td>a type variable — see <a href="#generics">generics</a></td><td>whatever it is instantiated at</td></tr>
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>
<tr><td>a union</td><td><code>defunion</code>, matched by case</td><td>tag + the widest payload</td></tr>
<tr><td>an enum</td><td>its own type in the checker</td><td><code>i32</code></td></tr>
<tr><td><code>()</code></td><td>one value, zero size</td><td>empty</td></tr>
<tr><td><code>Never</code></td><td>fits anywhere; nothing has it</td><td>empty</td></tr>
@ -597,7 +609,8 @@ whose type matters is named at the top level rather than written inline.</p>
<p><code>if</code>, <code>when</code>, <code>unless</code>, <code>cond</code>,
<code>do</code>, <code>and</code>, <code>or</code>, <code>not</code>,
<code>while</code>, <code>until</code>, <code>dotimes</code>, <code>return</code>,
<code>while</code>, <code>until</code>, <code>dotimes</code>, <code>loop</code>/<code>recur</code>,
<code>break</code>, <code>continue</code>, <code>return</code>,
<code>match</code>. <code>and</code> and <code>or</code> short-circuit.
<code>:else</code> is <code>cond</code>'s catch-all.</p>
@ -641,33 +654,64 @@ unless runs when the test is false
a body that changes it cannot change the trip count, and the loop variable is not
assignable.</p>
<p>Loops are imperative, with <code>while</code>, <code>until</code> and
<code>return</code>. There is no <code>loop</code>/<code>recur</code>. There is no
<code>break</code> or <code>continue</code> yet either; both refuse by name:</p>
<p><code>break</code> and <code>continue</code> leave or restart the innermost loop, and
take a <strong>label</strong> when that is not the one meant. <code>loop</code> and
<code>recur</code> are the functional shape beside them: <code>loop</code> is an
<em>expression</em>, its value is the value of its body, and <code>recur</code> rebinds
every name at once and jumps rather than calls — ten million iterations do not grow the
stack.</p>
<pre><code class="sh">break is not implemented yet (see the build sequence in plan.org)</code></pre>
<pre><code>(defn gcd [a i32 b i32] i32
(loop [x a y b] ; recur rebinds every name at once
(if (= y 0) x (recur y (% x y)))))
<p>An early exit out of a loop is <code>return</code>, as <code>first-even</code> does
above.</p>
(defn main [] ()
(println (gcd 84 36))
;; loop is an expression: its value is the value of the body.
(println (loop [i 0 acc 0]
(if (= i 5) acc (recur (+ i 1) (+ acc i)))))
;; continue in a dotimes advances the counter on the skipped iteration too.
(let [sum 0]
(dotimes [k 5]
(when (= k 2) (continue))
(set sum (+ sum k)))
(println sum))
;; A label says which loop. Unlabelled, break leaves the innermost.
(dotimes :outer [a 3]
(dotimes [b 3]
(when (= b 2) (break :outer))
(println b))))</code></pre>
<pre><code class="sh">12
10
8
0
1</code></pre>
<p><code>return</code> is still the way out of a <em>function</em> from inside a loop,
as <code>first-even</code> does above.</p>
<h3>Option, <code>match</code> and <code>some</code></h3>
<p><code>(Option T)</code> is how absence is spelled: a lookup miss, an empty
collection, the end of a stream. <code>match</code> works on an <code>Option</code> and
on nothing else today. <code>some</code> unwraps <code>Some</code> and early-returns
<code>None</code> from the enclosing function.</p>
on a <code>defunion</code>, and on nothing else. <code>some</code> unwraps
<code>Some</code> and early-returns <code>None</code> from the enclosing function.</p>
<pre><code>(defconst nums [4 i32] [4 8 15 16])
;; `some` unwraps Some and early-returns None from *this* function.
(defn doubled-first [s [i32]] (Option i32)
(Some (* 2 (some (index-of-i32 s 15)))))
(Some (* 2 (some (index-of s 15)))))
(defn main [] ()
(match (doubled-first (slice nums 0 4))
(Some i) (do (print i) (println "")) ; 4
None (println "not found"))
(match (index-of-i32 (slice nums 0 4) 99)
(match (index-of (slice nums 0 4) 99)
(Some i) (do (print i) (println ""))
None (println "not found")))</code></pre>
@ -697,10 +741,19 @@ first
second
3</code></pre>
<p><code>defer</code> is function-scoped and is <strong>rejected</strong> inside a
<code>let</code>, a loop or a branch. Block scoping it is not done:</p>
<p><code>defer</code> is function-scoped: it is copied into every exit path of the
function, so it always registers and always runs at function exit. It is therefore
<strong>rejected</strong> inside a loop or a branch, where "always registers" would be a
lie. A <code>let</code> is fine, and that is not an exception — a <code>let</code> is not
a frame here, its bindings are function slots like any other and nothing is released at
scope exit, so a <code>defer</code> written in one has exactly the function's extent.
That is the shape the permission exists for: acquire, defer the release beside it, then
use it.</p>
<pre><code class="sh">defer must be a top-level form in a function body — block-scoped defer is not implemented yet (milestone 4)</code></pre>
<pre><code class="sh">defer is not allowed inside a branch — a defer is copied into every exit path of
the function, so it always registers and always runs at function exit. Write it at
the top level of the function body, or in a let that is (a let has the function's
extent, because nothing is released at scope exit)</code></pre>
<h2 id="arrays">Arrays and slices</h2>
@ -743,6 +796,131 @@ It is a place: <code>(set (at grid r c) v)</code> and <code>(addr (at grid r c))
<p>A reversed range — <code>lo</code> greater than <code>hi</code> — traps, rather than
yielding a huge unsigned length.</p>
<h2 id="generics">Generics</h2>
<p>Parametric polymorphism by monomorphisation: one body is written, and every call
site gets a copy compiled at the types it passed. There are no type classes, no
dictionaries and nothing decided at run time.</p>
<p><strong>A type variable is written <code>$t</code> where a type goes</strong> — a
parameter's type, the return type, or nested inside a type constructor such as
<code>[$t]</code> or <code>(Vec $t)</code>. The sigil is not a binding-site-only
spelling: every type position writes it. <strong>Bare <code>t</code> is the same
variable in <em>expression</em> position</strong>, where a builtin takes the name of a
type as an argument — <code>(vec-new t)</code>, <code>(map-new t i32)</code>,
<code>(pool-new t)</code>, and the cast <code>(t x)</code>.</p>
<pre><code>(defn ident [x $t] $t x) ; needs nothing declared
(defn twice [x $t] $t ; + - * / % need numeric?
{:where (numeric? $t)}
(+ x x))
(defn clamp-to [x $t lo $t hi $t] $t ; &lt; &lt;= &gt; &gt;= min max need ordered?
{:where (ordered? $t)}
(min (max x lo) hi))
(defn first-or [s [$t] d $t] $t ; the variable inside a slice type
{:where (copyable? $t)}
(if (= (len s) 0) d (at s 0)))
(defn one-of [x $t] (Vec $t) ; bare t is the type-name argument
{:where (copyable? $t)}
(let [v (vec-new t)]
(push v x)
v))
(defn main [] ()
(println (ident 3))
(println (ident "text"))
(println (twice 1.5))
(println (clamp-to 12 0 10))
(let [ns [5 3 9 1]
one (one-of 4.5)]
(println (first-or (slice ns 0 4) -1))
(println (at (as-slice one) 0))
(free one)))</code></pre>
<pre><code class="sh">3
text
3
10
5
4.5</code></pre>
<h3>The body is checked once, abstractly</h3>
<p>A generic body is checked <strong>with nothing substituted</strong>, so an operator
the variable is not declared to support is refused at the definition rather than at
whichever call site first reaches a type that happens to work. That is deliberately
<em>not</em> Odin's rule, which checks a polymorphic body per instantiation:</p>
<pre><code class="sh">+ over the type variable t is refused: a type variable supports only what it is
declared to support, and nothing here says t is numeric?. Write {:where (numeric?
$t)} at the head of the body, or take the operation as a parameter — a
(Fn [t t] ...) — and call it here</code></pre>
<p>What makes that liveable is a <code>where</code> clause, written as a Clojure-style
map at the head of the body — <code>{:where (ordered? $t)}</code>, or a vector when
there is more than one: <code>{:where [(copyable? $t) (copyable? $u)]}</code>. There
are five predicates, and each gates builtins the compiler already has:</p>
<div class="scroll">
<table>
<tr><th>Predicate</th><th>What it admits</th></tr>
<tr><td><code>numeric?</code></td><td><code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code>, and a cast <code>(t x)</code></td></tr>
<tr><td><code>ordered?</code></td><td><code>&lt;</code> <code>&lt;=</code> <code>&gt;</code> <code>&gt;=</code> <code>min</code> <code>max</code></td></tr>
<tr><td><code>equal?</code></td><td><code>=</code> and <code>!=</code></td></tr>
<tr><td><code>hashable?</code></td><td>the variable as a <code>Map</code> key — <code>(map-new t V)</code>, <code>get</code>, <code>put</code>, <code>has-key?</code></td></tr>
<tr><td><code>copyable?</code></td><td>reading the value more than once; <code>Pool</code> and <code>Vec</code> element positions</td></tr>
</table>
</div>
<p>They entail each other in one direction, so one clause usually does:
<code>numeric?</code> gives <code>ordered?</code>, <code>ordered?</code> gives
<code>equal?</code>, and any of the four gives <code>copyable?</code>. A
<code>sort!</code> that compares its elements and reads them twice declares
<code>ordered?</code> and nothing else.</p>
<p><strong>A type variable is move-only by default</strong>, and
<code>copyable?</code> is the opt-out. <code>Types.is_move_only</code> of a variable is
not decidable abstractly — the same variable is <code>i32</code> at one instantiation
and <code>(Vec i32)</code> at the next — so the checker assumes the stricter rule,
which can only refuse a program that would have been fine and never admit one that
double-frees. It is Rust's <code>T: Copy</code>, with the difference that the compiler
answers the question rather than a user implementing a trait. So
<code>(defn twice [x $t] $t (+ x x))</code> does not merely want
<code>numeric?</code>; reading <code>x</code> a second time is a use after move:</p>
<pre><code class="sh">x was moved at twice.flan:1:26 and cannot be used again — t is move-only, so
binding, passing or returning one transfers ownership and the source binding is
dead afterwards (spec-memory.md). That rule is what makes a double free
unrepresentable; (clone x) if you wanted a second one</code></pre>
<p>Each instantiation then checks the concrete type against what the signature declared,
and refuses the <em>call site</em> when it does not answer:</p>
<pre><code class="sh">this call instantiates twice at $t = bool, and bool does not answer numeric? —
which twice requires, being written {:where (numeric? $t)}. The requirement is the
signature's, so the refusal is here, at the call that asked for the type: pass one
the predicate admits</code></pre>
<p>Two forms are deferred to the instantiation rather than settled abstractly, because
their legality is only decidable after substituting: <code>println</code> over a
variable, which selects the structural printer per copy, and the <code>Map</code>
operations over a variable key, whose hash and equality are concrete symbols chosen from
the concrete key type. The <code>Map</code> half is what <code>hashable?</code> buys —
without the clause, the type <code>(Map $t i32)</code> is refused where it is
<em>written</em>, and with it the refusal moves to the call site that names an
unhashable key.</p>
<p>This is not a type class and the difference is worth keeping straight. A type class
carries <em>implementations</em>, selected per instance and extensible by anyone, and
needs dictionaries and coherence rules. A predicate carries nothing — it gates a builtin
that already exists. The ceiling is that nobody can supply a <code>&lt;</code> of their
own; every operation the prelude and the containers need is a primitive, so it does not
bind. <code>test/programs/generics.flan</code> exercises the whole of it.</p>
<h2 id="printing">Printing</h2>
<p><code>println</code> prints a value and a newline; <code>print</code> is the same
@ -775,7 +953,11 @@ no newline: true</code></pre>
<p>The walk covers every integer and float type, <code>bool</code>, <code>()</code>,
<code>string</code>, <code>[u8]</code>, enums, <code>Ptr</code>, <code>Option</code>,
structs, fixed arrays and slices. An enum member comes back as its name: the value is
structs, unions, fixed arrays and slices. An owning container has no printer for its
contents and comes back as a marker instead — <code>&lt;vec&gt;</code>,
<code>&lt;pool&gt;</code>, <code>&lt;allocator&gt;</code> — while a <code>Handle</code>
shows its index and generation, and a <code>Map</code> has no printer at all.
An enum member comes back as its name: the value is
an <code>i32</code> by the time the backend sees it, so the name is recovered here from
the checker's table, and a value outside the declared members falls through to the
number, which is what you would want to see. A <code>Ptr</code> prints as
@ -800,7 +982,7 @@ takes the value as it is and prints the number it holds.</p>
<h2 id="prelude">The prelude</h2>
<p>The prelude is written in Flan, all but one line of it, and prepended to every
<p>The prelude is written in Flan, all but five lines of it, and prepended to every
program, so nothing in it needs importing. It holds no printing of its own:
<code>print</code> and <code>println</code> are the compiler's, and
<code>write-stdout</code> — the one output primitive — is what they are written
@ -809,16 +991,27 @@ over.</p>
<div class="scroll">
<table>
<tr><th>Group</th><th>Names</th></tr>
<tr><td>slices of <code>i32</code></td><td><code>swap-i32!</code>, <code>reverse-i32!</code>, <code>sort-i32!</code>, <code>index-of-i32</code>, <code>min-i32</code>, <code>max-i32</code>, <code>sum-i32</code></td></tr>
<tr><td>bytes</td><td><code>bytes=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-byte</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code></td></tr>
<tr><td>slice algorithms, over one type variable</td><td><code>swap!</code>, <code>reverse!</code>, <code>sort!</code>, <code>sort-by!</code>, <code>index-of</code>, <code>min-of</code>, <code>max-of</code>, <code>map!</code>, <code>reduce</code>, <code>filter</code></td></tr>
<tr><td>the per-type layer that stays</td><td><code>sum-i32</code>, <code>sum-f32</code> — the element and the accumulator are different types, which one variable cannot say</td></tr>
<tr><td>bytes</td><td><code>bytes=?</code>, <code>bytes&lt;?</code>, <code>bytes-ci=?</code>, <code>starts-with?</code>, <code>ends-with?</code>, <code>index-of-bytes</code>, <code>trim</code>, <code>digit?</code>, <code>space?</code>, <code>sort-bytes!</code></td></tr>
<tr><td>parsing</td><td><code>parse-i64</code>, <code>parse-f64</code></td></tr>
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>bytes-ci=?</code></td></tr>
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr>
<tr><td>building bytes</td><td><code>append!</code>, <code>append-i64!</code>, <code>append-f64!</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</code></td></tr>
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune!</code></td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and <code>sqrt-f32</code>, which is the one <code>declare</code> in the file</td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and the five <code>declare</code>s: <code>sqrt-f32</code>, <code>sin-f32</code>, <code>cos-f32</code>, <code>atan2-f32</code>, <code>pow-f32</code></td></tr>
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
</table>
</div>
<p><strong>One family, not one per type.</strong> The slice algorithms used to be
<code>sort-i32!</code> beside <code>sort-f32!</code> beside <code>sort-bytes!</code>, and
generics collapsed them: <code>sort!</code> is written once and instantiated at whatever
element type the call passes. <code>sum-i32</code> and <code>sum-f32</code> are what did
<em>not</em> collapse, and they are the honest exception — each widens its element into a
different accumulator, which one variable cannot express.</p>
<p><strong>The RNG is ours, not libc's</strong> — PCG-XSH-RR 32, written in Flan —
because a grid hash is only a regression test if the sequence is byte-identical on
native and on wasm32. <strong>The parsers are ours too</strong>:
@ -826,16 +1019,18 @@ native and on wasm32. <strong>The parsers are ours too</strong>:
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
cannot tell from a real 12.</p>
<p><code>sqrt-f32</code> is the one function in the file that is not Flan:
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code>. Every other number
here is reachable from the four operations and a cast; a square root is not, and the
usual trick of seeding Newton's method from the exponent bits needs a bit-cast between
<code>f32</code> and <code>u32</code> that the language does not have. IEEE-754 makes
<code>sqrt</code> correctly rounded, so libm gives the same bit pattern on both
targets anyway. Every link carries <code>-lm</code>.</p>
<p>There is no <code>println</code>. There is no overloading yet, so each printer names
its type. <code>(println 1)</code> is <code>unknown function println</code>.</p>
<p>Five functions in the file are not Flan, and they are libm's:
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</code>. Every
other number here is reachable from the four operations and a cast; a square root is
not, and the usual trick of seeding Newton's method from the exponent bits needs a
bit-cast between <code>f32</code> and <code>u32</code> that the language does not have.
IEEE-754 makes <code>sqrt</code> correctly rounded, so libm gives the same bit pattern
on both targets anyway. <strong>The other four are not</strong>: IEEE-754 requires
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
<code>powf</code>, and glibc, musl and wasi-libc do differ in the last bit — so the
byte-identical-hash property the RNG exists for does not survive a hash routed through
any of them. Every link carries <code>-lm</code>.</p>
<p>The primitives underneath are few — a primitive is the only thing implemented
twice per backend: <code>argv</code>,
@ -930,9 +1125,9 @@ carries on.</p>
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
(restart-case BODY ; BODY and every clause have the same type = the form's
(name [] CLAUSE) ...)
(name [p T ...] CLAUSE) ...)
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
(invoke-restart 'name arg ...) ; Never. Innermost frame offering the name wins.</code></pre>
<p><code>signal</code> has type <code>()</code>, always. A handler that returns
normally leaves the signaller to carry on — the accumulation case:</p>
@ -991,6 +1186,20 @@ first, before the clause body starts.</p>
-1
2</code></pre>
<p>A clause may take parameters, which is how the answer comes from <em>outside</em> the
frame that offers the restart:</p>
<pre><code>(defn supplied [n i32] i32
(restart-case (middle n)
(use-value [v i32] (* v 2)) ; the handler supplies v
(retry [] 7)))
(defn main [] ()
(handler-bind [(AssetMissing [c] (invoke-restart 'use-value 21))]
(println (supplied 7)))) ; 42</code></pre>
<pre><code class="sh">42</code></pre>
<p>Restart lookup walks the dynamic restart stack from innermost outward and takes the
first frame offering the name, so an inner <code>restart-case</code> shadows an outer
one for the duration of its body. An inner parser's <code>skip-form</code> is found
@ -1032,7 +1241,11 @@ a guard after each call.</p>
<p>So the author chooses where the retry boundary is. A <code>restart-case</code> at the
top of a frame re-runs everything including mutations already applied; one placed after
the mutations re-runs only what follows. Put the restart before anything mutates, make
the retried section idempotent, or snapshot what will be re-applied.</p>
the retried section idempotent, or snapshot what will be re-applied.
<code>test/programs/frame-rollback.flan</code> is the worked example of the snapshot, and
the ordering in it is the part worth reading: restore <em>in the restart clause</em> and
not in a <code>defer</code>, because a defer runs on the ordinary return path too and
that version silently rolls back the frames that succeeded.</p>
<p>This matters more here than in most Lisps because the intended use is a
<em>game loop</em>, where the plan is to skip a frame and carry on rather than die. Now
that a bad index signals <code>BoundsError</code> instead of ending the process,
@ -1047,7 +1260,14 @@ a guard after each call.</p>
<code>restart-case</code> body</strong>, and so is <code>invoke-restart</code> inside a
<code>defer</code>. In each case a bare exit would leave frames on the stack pointing
into a function that has gone.</li>
<li><strong>Restarts take no parameters</strong> in this version.</li>
<li><strong>A restart's parameters are checked at run time</strong>, count then
spelling, because a restart is found by name on a dynamic stack and neither end can see
the other. Lookup is by the name alone and the signature is checked after it, so an
inner <code>(use-value [s string] …)</code> shadows an outer
<code>(use-value [v i32] …)</code> and <code>(invoke-restart 'use-value 21)</code>
stops the program even though the outer clause would have taken it. The clause's
parameters are slots of the function that wrote it, and the invoker fills a buffer that
function owns — by the time a clause runs, the invoking frame has gone.</li>
</ul>
<h2 id="break">The break loop</h2>
@ -1151,8 +1371,9 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
*out = GetMousePosition();
}</code></pre>
<p>No library header is read during a build, deliberately, so a build needs the shared
library to be linkable and not the <code>-devel</code> package to be installed.
<p>A build needs the shared library to be linkable and <em>not</em> the
<code>-devel</code> package to be installed: every declaration a package uses is written
down in the package itself, so nothing has to go looking for a system header.
<strong>Guaranteed:</strong> the C typedef
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
and clang type-checks the wrapper against the generated prototype.
@ -1172,24 +1393,38 @@ directory may carry a <code>headers</code> file naming the library's own C heade
the same <code>declare-c</code> line a person would have written, and writes them to
<code>generated.flan</code> in the package — which is <em>committed</em>.</p>
<pre><code class="sh">$ export FLAN_RAYLIB_H=/path/to/raylib-5.5/src/raylib.h
$ flan generate-c vendor/raylib
wrote vendor/raylib/generated.flan: 253 declarations, 156 refused, of 581 functions.
Every defstruct and every hand-written declare-c agrees with it.</code></pre>
<pre><code class="sh">$ flan generate-c vendor/raylib
wrote vendor/raylib/generated.flan: 269 declarations, 117 refused, of 581 functions
in vendor/raylib/raylib-5.5.h.
Every defstruct, every hand-written declare-c and every mapped
constant agrees with it.</code></pre>
<p>Committing the output rather than generating at build time is what keeps the
no-header property honest: the declarations are in the repository, so every build gets
all of them, they are greppable, and they show up in a diff when the library moves. The
argument is not caching — the clang dump is already cached on disk and in memory.</p>
<p>Committing the output is what makes the declarations a dependency of the repository
rather than of the machine: every build gets all of them, they are greppable, and they
show up in a diff when the library moves. The argument is not caching — the clang dump
is already cached on disk and in memory.</p>
<p><strong>Regeneration is the check.</strong> The cost of committing the output is that
nothing compares the bindings against reality on every build any more, so the one
function that writes the file compares first and <em>refuses to write</em> when the
package and the header disagree: every <code>defstruct</code> against the header's
record, and every hand-written <code>declare-c</code> against the header's signature.
Pointed at a raylib 5.1-dev header while the package is written for 5.5, it reports ten
real differences and writes nothing — which is exactly the silent version skew a
generated file would otherwise bake in and make look reviewed.</p>
<p><strong>The header is committed too</strong>, at
<code>vendor/raylib/raylib-5.5.h</code>, and <code>headers</code> names it by path with
no environment variable in front of it. That line used to be
<code>?${FLAN_RAYLIB_H}</code> — optional, on the argument that requiring a header would
cost everyone the no-<code>-devel</code> property in order to give the check to whoever
had one. Committing the header dissolved that argument, because nobody needs
<code>raylib-devel</code> to have a file that ships with the repository. So the check now
runs on <strong>every</strong> build, and delete the header and the build says so by
name rather than going quiet. What being optional actually cost was found the hard way:
a gitignored web directory meant several working trees were checking against nothing and
were not told, and a check that silently does not run is worse than no check.</p>
<p><strong>Regeneration is the stronger check.</strong> An ordinary build re-reads every
declaration against the header, but only the hand-written ones can disagree — the
generated half came out of that header and agrees with it by construction. The one
function that <em>writes</em> the file therefore compares first and <em>refuses to
write</em> when the package and the header disagree: every <code>defstruct</code>
against the header's record, and every hand-written <code>declare-c</code> against the
header's signature. Pointed at a raylib 5.1-dev header while the package is written for
5.5, it reports ten real differences and writes nothing — which is exactly the silent
version skew a generated file would otherwise bake in and make look reviewed.</p>
<p>This is also why the hand-written bindings are kept rather than replaced by generated
ones. Everything the generator emits agrees with the header by construction, so diffing
@ -1387,8 +1622,12 @@ sim/grid [ [ 0 0 0 0 0 0 0 0 ...] [ 0 ... ] ...]</code></pre>
<p>A pointer is never followed; it renders as <code>&lt;ptr&gt;</code>. Following one
would make the walk cycle, and dereferencing a pointer a REPL was handed is not safe.
The walk is bounded at depth 4 and 8 elements, and the output truncates at 4K.
<code>Map</code>, function values and type variables refuse by name.</p>
The walk is bounded at depth 4 and 8 elements, and the output truncates at 4K. An owning
container renders as a marker rather than its contents — <code>&lt;vec&gt;</code>,
<code>&lt;pool&gt;</code>, <code>&lt;allocator&gt;</code>, <code>&lt;handle 3:1&gt;</code>
— and a function value renders as its signature, because the inspector reaches every local
of a stopped frame and a frame holding one has to render rather than refuse. A
<code>Map</code> is what still refuses by name.</p>
<p>The thunk's module is unloaded afterwards. Nothing points into its text once it has
returned. Sixteen expression evaluations retain zero mappings, where each redefinition
@ -1430,8 +1669,10 @@ something surprising.</p>
<table>
<tr><th>Key</th><th>Does</th></tr>
<tr><td><kbd>C-c C-c</kbd></td><td>the top-level form at point, recompiled and installed</td></tr>
<tr><td><kbd>C-u C-c C-c</kbd></td><td>&hellip;and mark it, so the program stops at the form point is inside (<kbd>C-u C-u</kbd>: on entry)</td></tr>
<tr><td><kbd>C-c C-k</kbd></td><td>the whole buffer, as <em>one</em> module</td></tr>
<tr><td><kbd>C-x C-e</kbd></td><td>the expression before point, evaluated in the running program</td></tr>
<tr><td><kbd>C-u C-x C-e</kbd></td><td>&hellip;and stop <em>at</em> it instead of printing its value</td></tr>
<tr><td><kbd>C-c C-z</kbd> / <kbd>C-c C-q</kbd></td><td>connect (finds <code>.flan-dev.sock</code> upward) / disconnect</td></tr>
<tr><td><kbd>C-c C-o</kbd></td><td>the running program's own output, in <code>*flan-output*</code></td></tr>
<tr><td><kbd>C-c C-r</kbd></td><td>a prompt on the running program (<code>*flan-repl*</code>)</td></tr>
@ -1447,6 +1688,15 @@ something surprising.</p>
</table>
</div>
<p>Some commands have no key. <code>M-x flan-dev</code> starts a program and
<code>M-x flan-dev-quit</code> stops it. <code>M-x flan-watch</code> opens the watch
buffer, which shows values <em>while the program runs</em> rather than while it is
stopped, and <code>M-x flan-watch-ghost-mode</code> shows the same values inline at the
call that wrote each one. <code>M-x flan-inspect-address</code> roots an inspection at a
raw address rather than at an expression. <code>M-x flan-allocations</code> and
<code>M-x flan-leaks</code> read the allocation registry: every block it recorded grouped
by type, and the same walk with the dead left out.</p>
<p><kbd>C-c C-k</kbd> sends one module rather than a form at a time on purpose: a
<code>defvar</code> and the function that uses it have to arrive in the same load, or the
first refers to storage that does not exist yet.</p>
@ -1573,6 +1823,23 @@ module to bind to. Release builds call directly, emit constants as constants, an
the folding back. Dev builds are not pruned by reachability: what a REPL may redefine
next is not a function of what has been called so far.</p>
<p><strong>There is a second backend, and it is off by default.</strong>
<code>flan build --x86</code> lowers the checked program to x86-64 by hand —
<code>lib/x86.ml</code>, writing an assembly file directly — instead of going through
LLVM. It is the <em>dev</em> backend: LLVM stays the default and stays the release path,
and the two never meet in one process, which is what lets the hand-written one pick its
own internal calling convention (every aggregate by pointer, no eightbyte rule, no
classifier) and match SysV only at the C boundary, where the shim has already flattened
every struct.</p>
<p>It covers a subset of the IR and <strong>refuses the rest by name</strong>, so a
build that succeeds is one it really compiled rather than one it half-compiled.
Conditions are the visible gap — anything reaching the transfer channel is refused:</p>
<pre><code class="sh">$ flan build test/programs/algorithms.flan --x86
Fatal error: exception Flan.X86.Unsupported("restart-case needs the transfer
channel, which this backend does not emit a guard for")</code></pre>
<p><code>--debug</code> is a third flag beside <code>--dev</code> and the optimisation
level. <code>--dev</code> asks whether you can redefine the program while it runs;
<code>--debug</code> asks whether you can stop it and read it. It emits DWARF, sets
@ -1604,41 +1871,42 @@ name, with the milestone it belongs to, and the tests assert on the reason.</p>
<div class="scroll">
<table>
<tr><th>You write</th><th>The compiler says</th></tr>
<tr><td><code>(Vec T)</code></td><td>(Vec T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Map K V)</code></td><td>(Map K V) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Result T E)</code></td><td>(Result T E) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Handle T)</code></td><td>(Handle T) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(try …)</code></td><td>try (Result) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td>a union type</td><td>the union type Shape is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(Fn [T] R)</code></td><td>a function type is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td><code>(fn [x i32] …)</code></td><td>calling something other than a named function is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td>a type variable</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td><code>'sym</code></td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td><code>(defmacro …)</code></td><td>parses, but is not expanded: running a macro means compiling it and loading it into the compiler, which is not wired up yet</td></tr>
<tr><td><code>`(a ~b)</code></td><td>is read, but not expanded: macro expansion is not wired up yet</td></tr>
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet</td></tr>
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet</td></tr>
<tr><td><code>'sym</code> as a value</td><td>a quoted symbol (restart names) is not implemented yet — milestone 6 (see plan.org)</td></tr>
<tr><td>a bare lowercase type name</td><td>generic code over the type variable a is not implemented yet — milestone 5 (see plan.org)</td></tr>
<tr><td><code>errdefer</code></td><td>errdefer is not implemented yet (see the build sequence in plan.org)</td></tr>
<tr><td><code>await</code></td><td>await is not implemented yet (see the build sequence in plan.org)</td></tr>
<tr><td><code>handler-case</code></td><td>handler-case is not implemented yet (see the build sequence in plan.org)</td></tr>
<tr><td><code>find-restart</code>, <code>compute-restarts</code></td><td>… is not implemented yet (see the build sequence in plan.org)</td></tr>
<tr><td>a union as a <code>declare</code> parameter</td><td>a parameter of g is U, which cannot cross to C directly — pass (Ptr U) and let the shim read it</td></tr>
<tr><td>a user-written allocator</td><td>a user-written allocator is not implemented yet, and a defn's name in that position …</td></tr>
</table>
</div>
<p>Beyond that list, and just as true: there is no allocator and no
<code>context</code>; there is no <code>println</code> and no overloading; restarts take
no parameters; <code>match</code> works on an <code>Option</code> and nothing else;
<code>defer</code> is function-scoped; there is no package-private marker other than
<code>main</code> not being exported; there are no threads in the language; and the
managed <code>class</code> facility that plan.org describes is a plan and not a feature.</p>
<p>Two of those rows want reading carefully. <strong>A quoted symbol works where a
restart is named</strong><code>(invoke-restart 'use-value 21)</code> is the ordinary
spelling — and is refused only as a value in its own right, because there is no symbol
type to give it. And <strong>the type-variable row is about the sigil</strong>: bare
<code>a</code> in type position is not a type variable and never became one;
<code>$t</code> is, and <a href="#generics">generics</a> is where it is written down.</p>
<p>The vocabulary in <code>spec-memory.md</code> is normative but largely unbuilt:
<code>clone</code>, <code>as-slice</code>, <code>push</code>, <code>get</code>,
<code>put</code>, <code>resolve</code> and allocator-aware operations belong to
<code>Vec</code> and <code>Map</code>, and arrive with them.</p>
<p>Beyond that list, and just as true: there is no overloading; <code>defer</code> is
refused inside a loop or a branch (a <code>let</code> is fine — it has the function's
extent); <code>find-restart</code> and <code>compute-restarts</code> are blocked on a
<code>Restart</code> type rather than on effort; a restart with parameters cannot be
taken from the break loop, which aims at a frame by position and has nothing to fill them
with; there is no package-private marker other than <code>main</code> not being exported;
there are no threads in the language; and the managed <code>class</code> facility that
plan.org describes is a plan and not a feature.</p>
<p>Two of these are settled rather than pending. There is <strong>no interpreter</strong>
and there is not going to be one: the compiled path is the only backend. The instrumentation-based step debugger that wanted one is cut,
and compiled redefinition at ~19ms is perceptually instant for expression evaluation too.
And the <strong>macro expander</strong> is blocked on unions rather than on itself — a
macro is a function from <code>Form</code> to <code>Form</code>, which needs
<code>Form</code> to exist as a Flan union value first.</p>
<p>One of these is settled rather than pending. There is <strong>no interpreter</strong>
and there is not going to be one: compiling is the only way a form is ever run. The
instrumentation-based step debugger that wanted one is cut, and compiled redefinition at
~19ms is perceptually instant for expression evaluation too. That is also how macros run
— a <code>defmacro</code> is compiled into a shared object and dlopened into the
compiler before the file that calls it is expanded, so there is no second evaluator to
disagree with the first.</p>
<h2 id="reading">Further reading</h2>