18 Commits

Author SHA1 Message Date
ce59f90707 An allocator, an arena, and a Vec that signals when storage runs out 2026-09-12 11:22:57 +07:00
92ca8c9774 Strike steps 1 to 3, and say in BUILT.md why the built thing is that shape
NEXT.md is what is left, so the three steps go, the four questions that were
unsettled under them are answered or replaced, and the reasoning that outlives
the change moves to BUILT.md: why Allocator being a builtin opaque type made
milestone 5 unnecessary, why with-allocator is its own IR node, why the move
rule unions at a join, what ownership is not transitive through yet and what
each refusal waits on, and why StorageExhausted had to land with Vec.

Four things replace the old open list. The use-after-release question is still
open but now has evidence available for the first time, because there is a Vec
to write arena programs with. The release layout is blocked on the reload path
carrying build flags. The generation word has no reader and will not have one
until a slice can carry a Vec's identity. And the budget is an addition to the
spec that wants folding in or replacing with a growable arena.
2026-09-12 11:15:22 +07:00
e2bafec373 Four runtime defects, and the two buffers that now have evidence 2026-09-12 10:55:15 +07:00
e691512af3 A render thunk that signals never reaches result_end
The thunk calls flan_dev_result_begin before it evaluates anything, so an
expression that signals is stopped inside the seqlock's window — and a restart
taken from that break transfers past the thunk, so the matching end never runs.
An unpaired begin cost nothing while the counter only moved at the end. It
costs everything now: incrementing would leave the count odd for the life of
the process, every later read reporting a write in progress, and C-x C-e dead
until the program restarts.

So begin sets the low bit rather than incrementing, and end clears it by
setting rather than adding. The ordinary sequence is unchanged — 2k, 2k+1,
2k+2 — and an abandoned write is over as soon as the next evaluation starts.

What that does not fix, because one buffer cannot: an evaluation running while
another is stopped mid-render shares the buffer, so the inner value is the one
that survives. That was true before the counter was a seqlock and is not a
regression.

Also noted in NEXT.md: rt_die in flan_rt.c has the same exit-with-the-loader-
lock-held shape the break loop just lost. Not fixed with it, because rt_die is
the non-dev path too, where there is no listener to deadlock against — whether
it should be _exit always or only under --dev is a decision.

And the 4K-cap assertions clamp their own String.sub, so a short body prints a
failure instead of raising out of the test.
2026-09-12 10:53:09 +07:00
33b6a65610 A restart takes arguments, checked against the types the clause declared 2026-09-12 10:51:32 +07:00
7c1fcbff19 A name finds one frame; it does not search for one that fits
§4 meets §3, and the answer a reader will assume is the other one. An
inner (use-value [s string] ...) shadows an outer (use-value [v i32] ...),
so an i32 is refused there and the outer clause that would have taken it
is never consulted. Searching outward for a frame whose signature fits
would make which restart runs depend on the arguments, which is overload
resolution on a dynamic stack.

Also: neither of the new guards is a bounds check, so --no-bounds-checks
does not remove them. A wrong index is a wrong answer; a transfer into a
clause whose parameters were written to a different layout is not.
2026-09-12 10:51:00 +07:00
66dcf30c8a Strike four fixed defects, and say why the dlclose rule has two exceptions
NEXT.md: the ring, the seqlock, the break loop's exit and the leaked handle are
struck with what each was fixed to rather than only that it was. The snapshot
generation stays open — it wants a hook a test can drive, which is a design
decision and not a fix. The four-buffer paragraph is now two and two.

BUILT.md carries the reasoning that outlives the change. "Nothing is ever
dlclosed" is restated as "nothing that published anything is ever dlclosed",
because that is what the rule was always about — being pointed into — and the
two modules that are closed are the ones nothing can point into. Stating it the
weaker way is what made a dropped handle look like obedience.

The agent section gains why a full ring refuses rather than drops or blocks,
and why the break loop leaves with _exit. The renderer section gains why the
result counter had to become a real seqlock and why marking it odd needs a
release fence rather than a release store — a release store orders what
precedes it, so the buffer writes could still be hoisted over it, which is the
original bug with more ceremony.

One correction: the release-build story named flan_dev_result_get as the symbol
that came up undefined. That symbol no longer exists.

dune test green; dune build @sanitize clean.
2026-09-12 10:49:05 +07:00
5184d732c9 Say what a typed restart still cannot do, and who has to do it
The language half of §3's parameters is in; the half that makes it worth
having is not. A break loop chooses by position and has nothing to fill a
clause's parameters in with, and that is now the top item in NEXT.md,
spelled out end to end — the accessors the frame can already answer, the
signature on the wire, and the one store that has to happen before the
channel is aimed.

§3 asks for a clause's report string to be settled before parameters and
it was not. The field is cheap and so is the accessor; the only thing
that would read either is the break loop's listing, which lives in the
agent and the daemon, so it would have shipped as a field nothing read.
It belongs with the editor half, which is changing that listing anyway.
2026-09-12 10:48:21 +07:00
1ea9456e2c A web target, built by emcc, that a raylib example reaches unedited 2026-09-12 10:47:27 +07:00
3e15328acd Where the web target stops, including the one that is a missing include
BUILT.md gains the section on the third target and corrects the claim it
already carried: emscripten_set_main_loop had the browser fact right and drew
the wrong conclusion, because asyncify answers the same fact without cutting
main in half.

NEXT.md gets the four holes. sand.flan has no web build, and the proximate
cause is that vendor/agent/flan_agent.c:426 uses struct timeval without
pulling in sys/time.h, which glibc gives it transitively and emscripten does
not; sand's main calls agent/start unconditionally so Reach cannot prune it.
Beneath the include is the decision worth making rather than patching around:
the agent is a socket server and the browser has no sockets, so the honest fix
is to refuse vendor:agent on a web target the way --dev is refused.

Assets are two questions and only the easy one is about emscripten.
--embed-file is a linker argument and so already expressible as an @web line.
The hard one is that the file doing (rl/load-texture "brush.png") is
structurally the one file that cannot say so: Load hands out lflags only for a
directory package, and main is not exported, so a program can never be one. No
flag was invented for it.

And nothing has been opened in a browser, asyncify's cost is quoted rather
than measured, and audio and threads on web are untried.
2026-09-12 10:45:42 +07:00
41025fc0ac A union is not a missing struct either
layout searched only Tast.structs, so a declared union came back as "no struct
is named X" — which reads as "that type does not exist" about a type the
checker knows. Refused by kind beside the enum, and both refusals now have a
test: a new enum and a new union, evaluated into the session.
2026-09-12 10:42:50 +07:00
a8f08eda6d A struct's fields, answered out of the build, keyed by the name that is an identity
(:op "layout" :type T) needs no running program: the daemon owns the build, so
Tast.structs is already in the session it compiled the process from. The open
question was what T is, and it needs no new machinery — Load qualifies every
declaration at import, so two packages' Missing are a/Missing and b/Missing and
the name is the type id. Emit already writes that same qualified name into
flan_error, so the string break reports as :condition resolves as :type by
construction, which is the round trip the test makes.

A bare name is refused with the candidates rather than resolved to a unique
suffix: resolving it would put back the ambiguity the rule exists to remove.
2026-09-12 10:39:16 +07:00
50ed2cbef0 Merge branch 'sanitize' into dev-loop
ASan was instrumenting none of the Flan half: it is an LLVM pass that
only touches functions carrying sanitize_address, which clang's C
frontend adds and hand-written IR does not. Globals get redzones either
way, which is why it looked right. emit.ml puts the attribute on every
define now, and a control asserts the report.

UBSan reaches no Flan code and no flag changes that -- its checks are
frontend-emitted branches, not a pass -- so shift UB and the NaN cast are
not answerable this way. Left as a compiler question, pinned by a control
that must not report.
2026-09-12 09:38:11 +07:00
3c7fdfc076 The left-redzone claim was inferred, not measured, and was wrong
A negative index into a global is silent in bounds.flan, which is
measured. "Because a global has no left redzone" was the explanation
put on it, and it does not survive the obvious test: declare another
defvar in front of arr and arr[-1] is caught, landing in that global's
right redzone. Underflow detection is a question about what the linker
put in front of the object, not about the access. Corrected in
test_sanitize, BUILT.md and NEXT.md.

NEXT.md's entry also goes back to its stated size. It had grown to 78
lines saying what BUILT.md says in the same commit range -- the
attribute, the -O0 decision, the bounds.flan table -- which is the
half-build-log the file's own header warns about. What stays here is
what is next: the UBSan gap as an undecided compiler question, the four
daemon-path buffers the corpus never reaches, and Valgrind.
2026-09-12 09:34:05 +07:00
c806125742 What the sanitizer sweep found, and what a clean run does not prove
NEXT.md's queued section becomes a landed one. The headline is not the
flag: ASan reaches Flan code only because Emit now attributes every
define, and UBSan reaches none of it and has no lever that would, so the
shift-UB and float-cast items that section listed are still open and are
a compiler feature rather than a flag.

The clean result is written with its reach. println.flan pushes a
1100-character string through escaped[1024] on purpose, so that buffer
is genuinely covered; scratch[64] never sees more than 20 characters;
and the 4K result cap, the dev registry guard, SNAP_MAX/SNAP_NAMES and
condition_name[128] are on the daemon path and not in the corpus at all
-- read, not tested. Two defects fixed, both found by reading. Three of
bounds.flan's six out-of-bounds cases caught with the checks off, with
the other three tabulated and explained, and the caveat that ASan sees
out-of-object and not out-of-subobject access, so three of six is a
ceiling and not a measurement.

BUILT.md gets the durable half: the attribute, the absent UBSan lever,
why --sanitize does not force -O0 when --debug does, and the -O0/-O2
divergence that earned it.
2026-09-12 09:31:32 +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
3e3d3b28f0 Merge branch 'print-sweep' into dev-loop
print and println are the whole printing surface now. About 500 call
sites across 47 files, and the site documents either of them for the
first time.

Two pinned outputs moved and both are corrections. sand-headless hashes
to 15595743031174623232 rather than -2851001042534928384 -- the same 64
bits, printed unsigned now that hash-grid's u64 no longer goes through an
(i64 ...) cast, which is the bug the family's explicit widening invited.
And a trap column shifted because the call it names got shorter.
2026-09-12 05:42:57 +07:00
d1da17ff69 Split the log out of the plan
NEXT.md was 1,738 lines and about half of it narrated work that was
finished. BUILT.md takes that half — the reload primitive, cells, the
agent, the session, the daemon, the Emacs client, conditions, the FFI
shim, the layout, and the order it was all built in. Not deleted,
because the reasons in it are load-bearing and would have to be derived
again.

NEXT.md keeps what is left, and says at the top what the two files are
for, so it does not become a log a second time. The struck-through
milestone checklist goes with the log; its one live item, the milestone
4 loose ends, stays. Start here was stale in three places and is
rewritten: conditions are three steps of four, a restart is taken by
position, and find-restart is blocked on a type rather than on effort.

The raylib gap list and the reasons break was declined are written down
where the next session will look for them, rather than living in a
commit message.
2026-09-12 05:25:52 +07:00