25 Commits

Author SHA1 Message Date
042b2ce4d0 A restart is not a transaction, and nothing rolls back
If a frame mutates a global and then signals, taking a retry re-runs the
mutation. Control resumes at the restart-case and runs forward; nothing is
undone. Common Lisp has the same property and offers no help either, so this
is written down rather than fixed.

The discipline is that the author chooses where the retry boundary is: a
restart-case above the mutations re-runs them, one below re-runs only what
follows. Put the restart before anything mutates, make the retried section
idempotent, or snapshot what will be re-applied.

It matters more here than in most Lisps because the intended use is a game
loop, and a bad index signalling BoundsError rather than ending the process
made abandoning and retrying a frame an ordinary thing to do.

conditions.org and web/index.html already carried the mechanical half as a
one-line gotcha; those are rewritten in place rather than gaining a second
bullet beside them. spec-conditions.md takes it in section 5, which already
enumerates what a transfer does and does not do. No numbered case changed
meaning.
2026-09-13 09:14:46 +07:00
65fd49f0c6 Where the generated half is written down, in all four places it belongs
web/index.html had no section on the FFI's generated half at all; it has one
now, with the command, the config, and the reason committing the output is what
makes the no-header property honest rather than a caveat.

BUILT.md gets why the 172 stay, which is the part that is easy to get wrong:
136 of them are exactly what the rule produces and the rest are expressible as
overrides, so the superset argument is sound and still leads somewhere bad —
deleting them reduces the signature check to a tautology.

DISCUSS.md 6a and 6b are answered rather than left open, and 6b's own point
about enums turns out to be live in the tree: key-down? keeps its Key
parameter because it is hand-written, and the generated key-up? beside it
takes an i32.
2026-09-13 08:13:32 +07:00
df73f87b2f The return type stops being a guess: the slot is mandatory, unit is ()
The slot after a defn's parameters is unconditionally a type. Parse.decl no
longer takes a set of type names, and is_type_form, qualified_type, types_in,
declared_types and prelude_types are gone with the pre-pass that fed them.

What they were for: (Option f64) and (Some 1) are the same s-expression, so the
parser decided which it had by looking the head up in a set of the file's own
type names. Sound -- one top-level namespace means a name cannot be both a type
and a value -- and brittle, because the set had to be complete. It was wrong
twice in one day, the second time parsing (defn f [] (Rune {.code 65}) (bar))
as a function returning a Rune with a one-form body, silently, in every file in
the language.

Two things fall out. A type the parser could not have known -- a struct
declared further down the file, rl/Vector2 behind an unresolved alias, a
prelude type -- never needed recognising, only placing. And a mistyped type is
a mistyped type: (defn f [] f65 0.0) reaches the resolver's near-miss check and
says did you mean f64, where it used to be read as the first form of the body
and reported as an unknown name.

Unit is written (). The old spelling is refused with a message naming the new
one, the rule the colon-to-dot change followed. Internally it is still
Tname "Unit" and Types.Unit, so the resolver, the shim and the emitter did not
change; Cimport still builds Tname "Unit" for C's void without going through
the parser. Types.to_string prints () though -- that printer prints what a
person would write for every other type it knows, [i32], {K V}, (Ptr T), and
Unit was the odd one out once the source spelling moved.

Dropping prelude_types removes one of the two reasons Macro.reduce may only
drop defns: the memoised set a bootstrap build could have poisoned is gone, so
the remaining reason is the plain one.
2026-09-12 23:18:28 +07:00
26c53e0a19 Every defn in the tree states its return type, and Unit is written ()
The mechanical half, ahead of the parser change that needs it. tools/unit-return.py
fills the empty slot with () and rewrites Unit as () wherever a type is spelled --
(Fn [i32] Unit), (Map i32 Unit), a return type written out.

Deciding whether a defn already had a return type is the whole difficulty, and
the script does it the way parse.ml did: is_type_form is transcribed rather than
improved, because being identical to the parser it replaces is what makes the
sweep meaning-preserving. It is re-runnable, so the lanes that branched before
this can have the same pass at merge:

    python3 tools/unit-return.py .
    python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
        test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
    python3 tools/unit-return.py --raw-ml lib/prelude.ml
    python3 tools/unit-return.py --in-html web/index.html

-v logs every defn it saw and what it decided, which is how a sweep of 440 sites
gets reviewed at all. Embedded modes pool a file's type declarations across all
its fragments, because a snippet split across concatenation -- decls ^ "(defn f
[s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled
names count only in bare-symbol position, for the same reason the prelude's do.
A fragment that cuts off mid-form is skipped rather than guessed at. Five sites
in test_flan.ml still needed a hand, and they are in this commit.

Two things ride along because the sweep needs them: parse.ml reads a lone () as
the return type of a function with no body, which was not a shape the old
optional slot could produce; and the map refusals name () rather than Unit, since
that is now the spelling a caller wrote.
2026-09-12 23:06:40 +07:00
e992491799 The colon belongs to keys; the prose, the page and the sweep agree now
web/index.html's Flan blocks convert and its output blocks do not, which
is the same split render.ml makes: the printed form keeps the colon until
the Emacs inspector that reads it moves too. Same in BUILT.md.

plan.org, spec-conditions.md and spec-memory.md carried struct literals in
the old spelling and now do not.

NEXT.md decision 6 is struck, and batch item 2 with it, naming what to run
at merge. BUILT.md says why the colon belongs to keys -- mostly that a map
literal wants {:key value}, and two literals sharing one syntax would have
left the reader asking the checker which it was looking at.

The sweep was not idempotent and is now: {.k :hi} -- a field already
converted, holding an enum member -- read as a destructuring pair on a
second run and ate the member. A re-run over a lane's files would have
corrupted them silently, which is exactly what the tool exists to do
safely.
2026-09-12 15:00:34 +07:00
9a820d86cd Sweep every field label from the colon spelling to the dot
The script is in tools/ rather than thrown away, because two lanes are
writing Flan in the old spelling right now and their files need the same
pass at merge.

It works on forms, not on text: a keyword becomes a dot only where it sits
in a field-label position inside a brace, so an enum member in value
position, a map key inside an EDN string and a type-position {K V} are all
left alone. :keys keeps its colon -- it names no field.
2026-09-12 14:47:54 +07:00
3afce2aeac Never restarting is the thesis; say where the holes in it are
The manual described the signature-change refusal as though it were the
design. It is not, and session.ml already said so at the refusal: a
signature change should make a new version, leave old callers on the old
one, and warn at the stale sites. plan.org calls it signature generations
and stale-caller warnings, and it is milestone 7's unfinished half.

The struct-layout rejection is the decided one and stays. Conflating them
made a placeholder look like a rule.
2026-09-12 09:38:26 +07:00
1898be6cb0 Repin the break banner, which has numbered its restarts since 4a6a8fa
The .out file and the two places that quote it in prose had the banner
from before restarts were numbered, so check.sh had been red on
breakdemo since that commit. The .out is regenerated from the same
build --dev and timeout run check.sh does, rather than typed: the leading
blank line and the three spaces before each number are part of what is
compared.

The page gets a sentence it was missing. A number in front of a restart
is not decoration -- a restart is taken by position, because an inner one
can shadow an outer one of the same name -- and the banner showed the
numbers without the page ever saying what they were for.
2026-09-12 09:08:35 +07:00
b8019a96d5 Give the page a section on the two printers it never mentioned
print and println had no line anywhere on index.html — a builtin the reader
meets in the first example and is never told about. There is a section for
them now, between arrays and the prelude: what the walk covers, that an enum
comes back as its name and a Ptr does not get followed, that a string is raw
at the top and quoted inside a structure, and that the depth and span caps
are what keep a grid from printing a screenful. printing.flan is beside the
other examples so check.sh has been green on every line of it.

The prelude's table loses its output row, because the prelude has no output
functions left, and the fifty-odd example blocks follow the files they quote.

Two pinned things moved. sand-headless prints 15595743031174623232 where it
printed -2851001042534928384: same bits, read unsigned, because the cast that
made it signed is gone. bounds.flan's trap moved from column 28 to 19, which
is where (at xs i) now starts on that line.

And the indirection-cell illustration is defer.flan rather than hello.flan.
hello.flan cannot show a cell any more: its one call was to a prelude
function, and println is compiler-provided, so the smallest program makes no
Flan-to-Flan call at all. defer.flan's main calls work, and is already on the
page a few sections up.
2026-09-12 05:37:56 +07:00
5ea0bcae84 Remove nth, the alias that was not one
nth and at were documented as the same operation, and as reads they were:
check.ml matched "at" | "nth" in one arm. But a place is recovered in two
other spots -- parse.ml for (set ...) and place_of_expr for (addr ...) --
and both match only Sym "at". So (set (nth a i) x) and (addr (nth a i))
were refused while the at forms worked.

Two names said to be identical that disagree about writing is worse than
one name, and the asymmetry is not worth fixing in three places to keep a
synonym. at is the indexing operation; nth is gone.

The six call sites were all reads, so they rewrite directly. get/put stay
the Map pair: get returns (Option V) and is deliberately not a place.

nth-gone.flan pins the removal -- it has to fail as a name nobody defined,
not quietly resolve to at again.

destructure~nth is compiler-generated and unrelated.
2026-09-12 04:46:41 +07:00
e416f28567 The site's key table, after the keymap moved under it
The prose lane branched before the CIDER buffers landed, so the page still
described C-c C-b as the minibuffer prompt. It is the conditions buffer now, the
prompt moved to C-c C-M-b, and C-c C-i, C-c C-a and C-c C-g are missing
entirely.

Checked against the real keymap rather than against the source: loading
flan-mode in a batch Emacs and asking key-binding what each one resolves to.
That is what caught C-c C-g being unbound - flan-dape.el registers it from its
own file so that flan-mode still works without dape installed - so the row says
so rather than claiming a binding that is not there.
2026-09-12 04:25:58 +07:00
4e89987a83 Stop explaining the significance of the sentence just written
Forty-odd clauses of the shape "— which is what makes X work" and "that is the
point of Y". Each one restates in the abstract what the sentence before it had
just said concretely, and a reader who followed the first does not need the
second. The facts are unchanged; the examples are untouched.
2026-09-12 04:23:35 +07:00
d995094b52 Put the contents beside the text instead of above it
Nineteen entries at the top of a long page are scrolled past once and then
unreachable. A fixed column stays put and scrolls on its own. Below 66rem there
is not room for two columns, so it collapses to a bar with a toggle — a
checkbox, so the page still needs no script to navigate.

The wordmark's dot was a circle at a guessed x, which drifted from the n
whenever the reader's serif was not the one it was measured against. It is a
full stop in the same text run now.
2026-09-12 04:17:27 +07:00
403e598145 Catch up with four facts that moved while this page sat on a branch
The quote checker found three of them on the first run against the new tip: the
sand hash was changed deliberately by the grid lane, break and continue now
refuse by name instead of reading as unknown functions, and the usage text grew
--debug. The prelude also grew a string and UTF-8 family the table did not list.
2026-09-12 04:11:22 +07:00
e0947adb2a An empty needle matched everything, so half the quote checks were decoration
`case $x in *""*)` is always true, so every check whose text came from a grep
went green the moment the line it greps for was renamed — which is exactly the
case those checks exist for, and they guard files other lanes are editing.
Verified by pointing one grep at a string that is not there: ok before, FAIL
after. sqrt-f32 was overstated in the same spirit; it is a declare, not Flan.
2026-09-12 03:58:42 +07:00
bbda5e4cd7 Point the reader at the two scripts, so the page's claim about itself is testable
"Every program below was run" is the kind of assurance nobody can act on. Naming
check.sh and quotes.sh turns it into something a reader can re-run, and says
plainly that a disagreement makes one of them go red.
2026-09-12 03:55:06 +07:00
4d1a0c7807 Build sand for wasm32 and compare the hash, rather than repeat the number
It is the project's headline cross-target claim and the page was asserting it
second-hand. Both targets print 2256461126764447066 on this machine, so the
transcript is now what the page shows.
2026-09-12 03:53:38 +07:00
23601f382d Say that break and continue do not exist, since a loop section implies them
plan.org settled the loop story as "while/for with break/continue and return",
so a reader will reach for them; they are not implemented and, unlike the rest,
not refused by name either — they come back as unknown function.
2026-09-12 03:51:53 +07:00
6c34d4a66e Quote the compiler's own words for the index rule, and pin the Emacs keys
The paraphrase of why a wide index is refused was shorter and said less than
the message; and the keybinding table came from NEXT.md, which is two keys
behind flan-mode.el, so it now reads the keymap instead.
2026-09-12 03:50:48 +07:00
fc47489802 Show the bounds check failing, because "checked" without a message says little
The claim worth making is not that there is a check but that a failure names
the line, and the only way to show that is to trip one.
2026-09-12 03:49:41 +07:00
1d206518cf Colour the primitive type names too, since only the capitalised ones showed
The rule was "capitalised is a type", which leaves i32 and string looking like
ordinary names in the one position — a signature — where the reader is there
to see the types.
2026-09-12 03:48:30 +07:00
a5e0c01224 Check the quoted blocks too, since a paraphrase reads exactly like a quotation
The blocks that are not programs were the ones that had drifted: the usage text
had lost its indentation and the refusal table had trimmed "(see plan.org)" off
every message, so the page was showing wording the compiler does not print.
2026-09-12 03:47:09 +07:00
86ef557433 Run the break loop rather than quote it, since the restart order is a claim
NEXT.md prints the banner with the restarts in source order; the walk is
innermost-first, so it is the other way round. A --dev build under timeout is
enough to settle that, and settles the two place and global snippets with it.
2026-09-12 03:43:24 +07:00
dfd64d89ea The examples are files that run, not prose, so the page cannot drift from them
Copying a snippet into HTML is where a documented language stops being the
real one. Each block on the page is a program here with its recorded output
beside it, and check.sh is what says the page is still true after a change.
2026-09-12 03:40:03 +07:00
2ecfac7561 A page to point someone at, so the language is readable before it is installed
Everything here is checked against the compiler rather than against plan.org:
the design documents describe a language larger than the one that runs, and a
page that documented the plan would mislead the first person to try it.
2026-09-12 03:38:55 +07:00