1157 Commits

Author SHA1 Message Date
09b77adc38 Merge branch 'worktree-agent-a6b21b9d0ff2daa2e' into dev-loop 2026-09-21 12:42:21 +07:00
290d322b44 An assignment is whole or it never happened, on x86 too
Two bugs, both in lib/x86.ml, both reached by signalling a condition from
inside the value being assigned.

The backend has no register wide enough to hold a struct, so it built every
aggregate in its destination, element by element as they were computed. A
transfer out of the middle left the destination in the middle: a global of
four numbers read part old and part new, and so did a local, a struct field,
an array element, a place behind a pointer. A union case was worse than the
rest, since its destination is zeroed before the fields are written.

The second one only looks like the first. An aggregate result is written
through a hidden pointer the caller supplies, and the transfer exit zeroed
that result on its way out, on the correct reasoning that the caller never
reads it. At (set g (f)) the pointer is g, so it zeroed the caller's
variable. It zeroes scalars only now — and with the first half in place
nothing reaches it, so that one is defence in depth and FIX.org says so
rather than claiming a test it does not have.

Assignment builds into a frame temporary and copies, which is the shape
emit.ml always had. The copy is paid where it buys something: [settles] says
whether lowering an expression is bound to reach the end of it, and a
right-hand side that settles is still built in place. That includes a
conversion, except the one direction that is checked — a float narrowed to an
integer — because an array of this language's literals is written
[(u32 1) (u32 2)] and refusing every cast would have taxed the commonest
aggregate there is. A let pays only inside a loop, because a slot reached
once per frame is recorded as bound after the value lands.

half-write.flan is every destination crossed with every right-hand side, run
on both backends, with a scalar row so a regression in one is not read as the
other. dev-halfwrite.flan asks the break loop the half a running program
cannot ask itself: the local, which is invisible to the program and plain to
an editor reading the frame.

The def-edit paragraph in docs/BUILT.md described this as open and is now the
claim it was waiting for. FIX.org records the two lanes the review of this one
turned up and this one does not take: an aggregate built in place can read its
own destination, which is an aliasing question rather than a transfer one; and
the temporary is a frame buffer no root table names, which matters the day a
struct may hold a dyn field.
2026-09-21 12:37:08 +07:00
54d12811f8 Merge branch 'worktree-agent-afd7ad8d87f71fa16' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 12:19:15 +07:00
9ae134beaa The numbers from the rebased branch, and a second shape of test_dev flake
test_reload.exe 25 for 25 after the rebase and the reg_at change, with the
pre-fix binary refusing 4011 of 73306 runs beside it, so the window is known
racy rather than merely quiet.

test_dev flaked twice over while that ran, in two different shapes. Three runs
in six at load 21 were the stale-park rerun, which has a lane. Two of another
six were something else: most of the daemons in the file exiting before they
bound their socket, with no stale socket or daemon left behind, and six green
on a quiet machine after. Neither touches the registry — a daemon that dies
before binding died before any program it builds ran a line of flan_dev.c —
but the second shape does not seem to have been written down anywhere, so it
is written down here.
2026-09-21 12:16:43 +07:00
ef2ac41172 Merge branch 'worktree-agent-a1b7f33fe11893c24' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 12:12:36 +07:00
ae46c5a878 Three sentences that were true only while the state lagged
Follow-ups to the re-run flip, and all of one kind: each said something
correct about a committed re-run that still read as parked, and the flip made
it false.

flan_merged_park's note that the program stays PROGRAM_PARKED across a poll is
now true of every round but the one it leaves on — which is the round that
drains the ring with a run already taken, and the round the fix's own argument
leans on as the long one.

Program.rerun's refusal told the reader to close a window or let the program
finish. With a thunk stopped in the break loop a re-run is accepted, the state
goes running, and a second is refused by that sentence — advice about a game
loop for somebody whose thread is in a break. The C cannot see a break; this
end can, so Dev.rerun passes what it already asked the agent and the refusal
says resume or abort instead.

The five-second timeout in eval_expr and run_render_thunk reached its
break-loop sentence through the Parked arm, so a program stopped between runs
was asked whether it calls (agent/poll). Both gain the arm that names the
break. run_render_thunk's two sentences become a function first: the new one
asks the agent, and that was the body of a five-millisecond tick.

docs/BUILT.md gains the case an editor author meets — :parked nil :stopped t
with no frames running — and FIX.org records that test_dev.ml:763's refusal
assertion was racy before this and is narrowed by it.
2026-09-21 12:11:23 +07:00
13145cc52a Merge branch 'worktree-agent-aba36ddb6a383320f' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 12:10:23 +07:00
3df49184a7 The combination nothing built is built now, eight cases of it
--dev --sanitize had been unbuildable for as long as a dev build has
armed the registry from a constructor, and nobody knew because no alias
built it: test_sanitize built the corpus twice and neither time --dev,
test_dev builds --dev and never with a sanitizer. A configuration
nothing builds can be broken for a month, and that one was. af71459
fixed it; this is what keeps it fixed.

dev_sweep, in the alias that already exists rather than a new one --
five names to remember was already four too many, and this is the same
question @sanitize is for. Seven programs built --dev twice, plain and
sanitized, run standalone with no daemon: a sanitizer report fails, and
so does any divergence from the unsanitized dev run.

Standalone is what makes it nearly free, and it works because a dev
build is not a dev session. The cells, the marker and the constructor
are in the executable either way, and a program that never calls
agent/start runs to its own end. That is why the list is mostly
ordinary programs built the other way: dev-noagent is the only one of
sixteen dev-*.flan that does not import the agent, and the rest stop in
the break loop waiting for an editor, which is test_dev.ml's business
against a real daemon.

The three dyn programs are not interchangeable and the difference is
the collector. dyn-vec allocates and never collects -- flan_dyn.c has a
one-megabyte floor and dyn-vec does not reach it -- so what it says is
that the registry and the allocator agree. dyn-map and p13-dyn-collect
are the only two programs anywhere past that floor, so they are the
only two under which a mark and a sweep run; without them a collection
had still never happened in a dev build under ASan.

dev_segv is beside the sweep rather than in it, because the program
that faults cannot be compared against an unsanitized run: that build's
handler prints its line and parks in the break loop, so the plain half
would hang, and the two are supposed to differ. ASan is meant to own
the fault -- flan_dev_crash_enable checks a weak __asan_init and stands
down -- so the case asserts ASan's report and the absence of the
handler's line. At -O0, because at -O2 the write through a bytes-view
of a literal does not fault at all and both builds print the string
unchanged. That yield had never run in any build anywhere; it was
behind a link that did not happen.

Checked both ways rather than asserted: eight failures with the
constructor naming declarations again, clean with it fixed.
Twenty-six seconds of the alias's 2m30 warm, and nothing added to dune
test.

Two aliases were also green only because dune test runs first.
@sanitize never listed the package directories pkg-diamond.flan
imports, and @page never listed sand.flan, which quotes.sh reaches
through sand-headless.flan; from a cold tree the first died before its
first sanitized build, and @page sits inside @checks where CI hides the
same thing. Both listed now. @valgrind, @x86, @js and @cells were
checked and are complete.

What it still does not reach is in FIX.org: a program driven by a real
flan dev daemon under ASan, which wants a --sanitize the CLI does not
have and a way through Dev.serve. --x86 --sanitize is refused by Build
by name, so there is no second backend to track here.
2026-09-21 12:08:36 +07:00
1440ae4119 Taking a re-run is what ends the park, so the state says so
flan_merged_rerun accepted a request under the lock and left program_state
as it found it: PROGRAM_PARKED, until the parked thread got round to waking.
describe's :parked reads that same state, so a caller that asks for a re-run
and then waits for the program to park again was liable to be answered by the
park it had just ended — the wait fell through on the old park, the next
request went out before the first run had started, and the pair of them made
one run between them; or the thread woke in between and the second was refused
as "already running". One lagging state, two symptoms, and all four of
test_dev's re-run sites could show either under load.

The window was documented over flan_merged_park as "as wide as a flush". It
stopped being that when the ring drain went in front of the park's exit: the
leaving round loads whatever was queued before it breaks, so the window was as
long as the next thing the program had to do.

The store moves to the acceptance, under the lock that made it. There is no
longer a moment in which a committed re-run reads as parked, and the park's own
store on the way back into main stays as the no-op that says where the thread
has got to. Dev.rerun reads the liveness and the break for its note before it
asks, since afterwards the answer is running by construction.

Measured on that loop driven standalone against programs/dev-rerun.flan: 9
failures in 25 runs under two busy-loop burners before, 0 in 50 under the same
load after. FIX.org, 2026-09-21, has the readers that were checked, the path
that cannot exist, and why the two-process daemon has no such window.
2026-09-21 12:02:20 +07:00
8eaeb992d6 sand.flan asks for length
Two sites, and the second is the subtle one: the let binds a local named
length from (length coll), which reads because a sequential let checks an
initialiser before the name it binds exists. That was the reason for the
rename -- len is a variable now.
2026-09-21 12:01:34 +07:00
970d5ac38f Merge branch 'worktree-agent-a2b2b99f84ca798de' into dev-loop 2026-09-21 11:59:19 +07:00
d6fc15474b The count is length, so len is a name a program can have
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.

Shadowing and builtin/ had already taken most of the sting out: a (defn len
...) was legal and won in its own file, and builtin/len reached past it. What
was left is that len was still a builtin — the defn earned a warning, and a
wrapper had to say builtin/ at every inner call. Now there is nothing under
the short name: len is an ordinary identifier in every position, which is
what (let [len (length xs)] ...) wants.

length takes over as shadowing's worked example rather than the feature
losing one. shadow-builtin.flan, builtin-qualified.flan, pkgs/shadowed and the
builtin/ rows in test_flan move to it and go on testing shadowing.

A call to a len nothing defines is answered where an unknown function is,
after every table and after the shadowing guard, so a program with its own len
never reaches it. The sentence is said rather than guessed at — len and length
are three edits apart and the did-you-mean's net is one — and the call is
written back out through spell_arg, as-slice's spelling lifted out of it and
now shared, so what is printed compiles.

sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
2026-09-21 11:58:56 +07:00
df2f23609b The three the review asked for: the last bare retry, and a bound that was a typical
[flan_dev_reg_at] still went straight round its walk-level retries — a failed
scan_open and a failed scan_ok both back to the top. It is the one site where
the argument for leaving it holds water, since the agent gates the verb behind
a stopped program and there is no writer to lose to. It loses to the other
argument: it is the same mistake the slot read was making, three lines under
the note explaining why it is a mistake, and a bare retry left next to that
note teaches that the rule has exceptions it does not have.

And the cost of a refusal was written as a bound when it is a typical. "One
writer, so at most one slot of a walk is odd" is true at any instant and false
across a walk — flan_reg_compact writes every slot under its own counter, so a
repeatedly preempted writer can charge the full patience against several slots
of one walk, 4096 of them in the arithmetic worst case. Both docs now say
typical and give the worst, and record what the larger figure means for
request_lock, which is held across handle_line: a refusing listing delays an
abort by that much. Nothing depended on the old number.

Also written down, because it is the clearest statement of why this was not a
timeout: sixty-four bare re-reads of one word finish in about two microseconds,
so against a writer a millisecond from running the old budget was not short, it
was zero wall-clock.
2026-09-21 11:52:59 +07:00
a29424a7b6 The proof the first commit's numbers could not give
Green runs of a load-only flake, taken on an idle machine, are worth nothing,
and a count alone cannot tell you which kind you have. So the load for the
55-run test_reload proof was a build of flan_dev.c from the commit before this
fix, churning beside it: a positive control as well as a load. It refused 4151
of the 50478 runs it managed in that window — 8% — while test_reload went 55
for 55. Paired on the direct binary too, eight copies at a time: 13 of 80
before, 0 of 160 after.

Also here: the bound BUILT.md used to quote went from two milliseconds to about
sixty-six, and the doc says so rather than dropping the number; the note that
flan_dev_reg_at shared the spin and could answer "never heard of this address"
under load; and the verdict on the two neighbouring dev flakes, which are three
causes and not one.
2026-09-21 11:49:29 +07:00
8f05ece441 The reader spun where it should have waited, and called a still table unreadable
test_reload's registry-under-a-writer case failed about one run in five on a
loaded machine and never on an idle one. The refusals were always unread=1:
one slot, on the best of eight walks, that would not copy. In that mode the
table cannot even move — re-noting live blocks kills nothing, the dead count
stays at zero, and the compaction trigger never fires — so the reader was
refusing a table that sat perfectly still for it.

flan_reg_snap re-read a slot's counter sixty-four times with nothing between
the tries. That is right for the writer it was written for, which holds the
counter odd for seven stores. It is hopeless against a writer the scheduler
took the core from mid-write, which holds it odd for a quantum: the reader
burns all sixty-four looks inside a fraction of that, and the looking is what
keeps the core the writer needs. Hence load-only.

The walk-level retry already had the answer written up at length — a spin takes
a core from the thread being waited on. The per-slot retry never got it. One
pause helper now, shared by both: eight bare looks for the running writer, then
the same 250us step. A refusal means the table would not hold still.

480 contended runs of regfull after, no refusals; 46 of 240 before.
2026-09-21 11:49:29 +07:00
b8856bef3a Merge branch 'worktree-agent-a39cf777643bbbdcc' into dev-loop 2026-09-21 11:45:38 +07:00
3672da28be A diagnostic is for someone who has only this compiler, and says what to write 2026-09-21 11:44:59 +07:00
af714598b8 Merge branch 'worktree-agent-a93ce7ad4d1f34196' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 11:28:56 +07:00
7b4f99261e Merge branch 'worktree-agent-a6bf2d4580db1ab57' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 11:26:40 +07:00
b98108791d Merge branch 'worktree-agent-a1a1e7420cccc5fa6' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 11:05:28 +07:00
6df50e9ce1 The sand grid hashes the new generator
The draw is 64 bits now, so every grain lands somewhere else and the old
number could not be right again. Re-taken from a run, which is the one
manual step this case has always had.

sand.flan's two calls say (f32 (rand)).
2026-09-21 11:05:03 +07:00
165685490e Comparisons take a run of operands: the orderings chain, != is all-pairs
(< a b c) was an arity error. The folding operators had taken two operands
or more since fold_left_prim went in; the six comparisons had not, and they
are the ones the game hit.

The orderings and = chain: (< a b c) is a below b and b below c, because the
left fold would compare a bool against a number. != does not — the author's
ruling is that (!= 1 2 1) should be false — so it asks about every pair,
Common Lisp's /=. Whether a sequence is increasing is a question about
neighbours; whether a set of values are all different is a question about the
set, and the pair chaining never looks at is the one that decides it.

Every operand is bound to a slot first, in source order, so an operand two
pairs name is evaluated once — the spelling a reader would write, (and (< a b)
(< b c)), evaluates b twice. The conjunction then stops at the first pair that
fails, with nothing observable riding on it: everything has already run.

At two operands both readings are one pair and neither goes through the n-ary
lowering, so every comparison there is emits what it always did. (< x) joins
(+) and (- x) as a refusal — it would be true whatever it was handed.
2026-09-21 11:03:39 +07:00
0c03550999 Evaluating a def assigns, because that is what defparameter means
The author edited (def colors [4 u32] [...]) in his running game, pressed
C-c C-c, and the colours did not change — the same complaint def was built
to answer, one step further in.

The reading behind it was that a re-evaluated def is a promise about the
next re-run, so the session republished the lifted global/<n> and stopped;
nothing called it. That is wrong for the reason the form is named after: def
is Common Lisp's defparameter, and evaluating a defparameter assigns. The
difference from defvar is not "one takes effect at restart", it is "one
takes effect, the other does not touch the value at all".

So a def now does both. The storage takes the new value at the next frame
boundary, carried by the thunk a redefinition module already has — one
Set per re-evaluated def, in the same flan_reload_call the class
registrations use, run after the bodies are published and on the game
thread. And the lifted initialiser is still republished, so the next re-run
runs the edited one; dev-rerun.flan pins that half unchanged.

A brand-new def gets its initialiser run too, which needed one thing from
each backend: a lifted global/<n> asked for by name is neither a sibling nor
one of the target's own lifted clauses, so it had no cell, and a dev call
goes through a cell. Both now give an unknown one a slot of the module's
own, filled from the registry by the installer.

An initialiser that signals leaves the old value alone — the value is
computed whole before it is stored — and offers abandon-evaluation like any
other thunk. A retype is refused first, by the pass that names both types.
defonce is untouched, which is its whole contract; defconst was already the
immediate one, through consts.
2026-09-21 11:03:30 +07:00
efcee7ea07 Merge branch 'worktree-agent-ae090b25442f691f1' into dev-loop 2026-09-21 10:57:56 +07:00
69c1116efc The generic allocation program takes the one slice
The slice lane merged after this program was written and as-slice went
with it. Nine calls, one name.
2026-09-21 10:57:52 +07:00
a0278378ab Five randomness functions, and a draw wide enough to answer them
rand-int, rand, rand-bool, rand-int-range and rand-float-range, at the widths
the author ruled: a u64 draw and an f64 in [0, 1). rand-seed and rand-state
keep their names. The four old names are not names, and each is refused by the
one that is, with a call that compiles — in both the call and the bare-name
position, because a Lisp-1 makes the second a real thing to write.

The generator's step is untouched, so a seed means what it meant. Its output
function is not: PCG-XSH-RR folded the state to 32 bits, and no honest u64 or
53-bit f64 comes out of 32 bits without a second step. PCG-RXS-M-XS 64 answers
64 from the same one, so all five still cost exactly one draw and a seeded run
is reproducible. The price, written where it lives: the permutation is a
bijection of the state, which is what 64 output bits from 64 state bits costs.

The sequence is therefore a different one, and programs/rand.flan pins it —
reproducibility across the five, the single-draw cost of each, the half-open
boundaries, and rand-bool's count over a thousand flips.

sand.flan is not touched. It calls rand-f32, so the cases that compile or
re-evaluate it skip themselves on the fixture rather than on a comment: fix
its two calls and every one of them runs again. Its hash is the old
generator's grid and gets re-taken then.
2026-09-21 10:57:18 +07:00
bb274432a7 Merge branch 'worktree-agent-adff9fa1b537fecf8' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 10:29:07 +07:00
7794f06f00 The same mistake was wearing two faces, and neither named the fix
($u x) was an unknown function in the same body where (vec-new $u) was an
unbound type variable, because the cast arm did not take the sigil clause
type_named took. It takes it now, so one mistake has one story.

And the story was a rule rather than an answer: "only a defn signature can"
is what to say when nothing is in scope to name — a struct field, a global —
but inside a signature that introduces $t, the name that was meant is almost
always t. It names them. Which names those are comes from tyvars abstractly
and from subst inside an instantiation, because a body is checked under both
and reading one would answer the same mistake two ways in a single run.
2026-09-21 10:27:54 +07:00
75177a5a18 Review follow-ups: the constructor took a name a program can write
The wrapper was called flan.dev.ctor, and that is a name Flan can reach:
(defn dev.ctor [] i32 7) mangles onto exactly it. The program compiles
and runs as a release build and fails a dev build with a redefinition
clang refuses -- loud, and only under LLVM with --dev, but mangle.ml's
own comment exists to make it impossible rather than loud. The name is
[Mangle.dev_ctor] now and starts with the dot no reader token can
produce, beside .init-globals and .init-data. The colliding program
builds and prints 7.

Two comments in survey.sh, both of them reasoning rather than
behaviour. The counts argument against a per-name -O0 list was no
argument: excluding moves the counts just as much. What actually
carries it is that dies_segv builds both programs at -O0 on both
backends and asserts more than this sweep would. And dev-segv's park
under --dev is a forever-list reason that happens to land on a program
this list already covers, not a second reason for this list.

FIX.org takes the sweep, and one thing the sweep cannot say: the two
heap cases of bytes-copy.flan leak 24 bytes through flan_bytes_dup,
measured with --leak-check=full, and both corpora are blind to it by
policy -- detect_leaks=0 on one side and --leak-check=no on the other.
The row proves the copy is in bounds and written. It says nothing about
who frees it, and a green sweep should not be read as though it did.
2026-09-21 10:12:55 +07:00
9573884d97 Merge branch 'worktree-agent-a9a425fd59a33124b' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 10:05:42 +07:00
5ad16d6815 A conversion asks the bound, not a type the variable does not have
(i32 (at xs i)) inside a body bounded integer? was refused with "i32
converts a number, found t". Arithmetic, comparison, min/max, the
bitwise fold and the shifts all ask the where clause; the conversions
were the family nobody had gone back to, and the cast block held three
arms of it.

The machine-type target asked Types.is_numeric of its operand and the
enum target asked Types.Int _, so a variable fell through both to the
refusal however it was bounded. The variable target had the opposite
defect: it asked the bound of the target and then took any generic
operand, so a second variable declared only ordered? passed the abstract
pass on the strength of a sentence about a different one. Nothing wrong
was ever emitted through it — ordered? admits numbers and enums and both
convert at the instantiation — which is exactly why it is worth closing:
the hole opens the day ordered? admits a type that does not.

The rule is the repo's own, applied to a set instead of a type: a
conversion is legal at a bounded variable exactly when it is legal at
every type the bound admits. A machine-type target needs numeric?, an
enum target needs integer? because numeric? admits the floats the
concrete arm refuses, and ordered?/equal?/hashable? admit nothing — the
last by what the predicate says rather than by the set it denotes, since
hashable? already admits strings and ordered? may.

Both float targets and the narrowing i32 stay legal: (f64 i64-x) rounds
above 2^53 and (i32 f64-x) truncates where the types are written, and a
generic that refused what its copies accept would be the fork the rule
forbids. FIX.org, 2026-09-21, has the account, and records what this
costs: no predicate now licenses a generic enum to integer conversion,
and enum? is the eventual answer.

The refusal says what the variable is known to be and what to write, in
the clause spelling unconstrained already uses: a body with no clause
gets the clause, a body that has one is told which predicate to add.
2026-09-21 10:01:18 +07:00
482b869835 Three membership tests asked the name as written, and one carries a sigil
(vec-new $t) in a generic body was refused as if it had said nothing about
its element type. The feature was not missing: env.tyvars and env.subst are
keyed on the bare name, and type_named and the cast arm asked them of the
name as written, so the spelling with the sigil fell past the guard into the
no-element-type message. (vec-new t) had always worked.

One helper the three of them share, beside resolve_name, which already
stripped for itself. A sigil on a name nothing binds reaches resolve_name
now too, so it is answered as the unbound variable it is.
2026-09-21 10:00:40 +07:00
4ab1d872b0 Merge branch 'worktree-agent-adecbcf29e2524871' into dev-loop 2026-09-21 10:00:04 +07:00
c3319e6268 Review follow-ups: a leak is the program's, a wrong answer is not
Three things the review found, and the refusal it was right about.

The returned-Vec refusal is gone. It called a leak a dangle: the storage a
returned Vec owns outlives the expression, so the view reads what it says it
reads, and what is lost is the owner. (len (mk)) and (at (mk) 0) lose the same
owner and compile, spec-memory.md already says an overwritten global Vec leaks
its first block, and under a region there is nothing to leak at all. "We're
purposely doing manual memory management for the static side, so whatever."
The array refusal stays exactly as it is — that one is a view into a frame
that is gone and answers bytes the frame has since reused. Wrong answers are
the compiler's business and leaks are the program's, and both docs now draw
that line, because the two forms look alike.

The -1 sentinel was reachable from user syntax: (slice v 0 -1) answered the
whole Vec on both backends while (slice a 0 -1) was refused as a negative
bound. The refusal now runs on the bounds the reader wrote, before the
implicit hi is built — the only order that works, since the sentinel is itself
a -1 and a check on the finished pair would refuse (slice v). The
backwards-pair check moved into the branch where both ends are written.

The bounds seam is closed toward index_expr, and the tiebreaker is not which
half is older. indexed and vec_at both take their index through it, so
(at a c) over a u32 compiled where (slice a c) did not: the fork was between
slice and at as much as between two targets. A bound is a subscript.

Also an x86 row for vec.flan, so the new arity is pinned on both backends in
CI rather than by hand, and the comment columns the sweep shifted left in
slurp, into, format and algorithms.
2026-09-21 09:59:33 +07:00
9ce51ba94e One slice over everything with elements, and the warning at the push
as-slice was a warning, not an operation. The input type already decides
which of the two things happens — a Vec can only be borrowed, an array or a
string can only be viewed, and no call site picks between them — so the second
name expressed no choice a reader could make. And it warned at the moment the
view is taken, which is the one moment nothing is wrong; the danger arrives
later, at the push. slice now takes a Vec at all three arities and as-slice
is gone.

(slice v lo) was free, and is the arity the Vec never had: the runtime already
reads a hi of -1 as "to the end", so the tail form passes the caller's lo and
the same -1 — no slot, no length read, no second evaluation. The merge is
entirely in the checker; the Vec path builds the flan_vec_as_slice call it
always built and neither backend has a line about any of it.

A Vec a call returned is refused at every arity, and not for the array's
reason. (slice (mk)) over an array dangles. (slice (make-vec)) does not — the
storage outlives the expression — but the header is a temporary, so nothing
can ever free the block. The refusal says that and names the let.

The name's own refusal sits in ordinary_call after every table, so a program
that defines an as-slice still reaches its own. It reads for somebody who has
never heard of the old name and writes the call back out, spelling each
argument that is a name or a number.

The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.
2026-09-21 09:51:35 +07:00
892366e86d The program the copy was written for is in both sweeps now
bytes-copy.flan came in with the lane that made (bytes s) allocate, and
it went into test_acceptance.ml and nowhere else. It is the one program
in the corpus that takes a block from an allocator, writes through it
immediately, and then takes another from an arena that is freed and
destroyed under it -- which is the shape both opt-in sweeps exist for,
and neither was running it.

Both, not one, because the two tools answer different halves: a copy one
byte short is a heap overflow ASan names, and a copy whose tail was
never written is an uninitialised read only memcheck can see.

Clean under both.
2026-09-21 09:45:57 +07:00
f75b0ca033 A constructor naming a declaration is what clang could not sanitize
--dev --sanitize did not compile. Any program, at any optimisation
level: clang 20's AddressSanitizer module pass segfaults on the module,
and the message it leaves behind is its own crash backtrace rather than
anything about the program. Reduced here to five lines of IR, and the
trigger is narrow -- @llvm.global_ctors naming a function that the
module only *declares*. Both of ours are declarations, because both are
C in the runtime.

This predates the crash-handler lane and is not its doing: the one-entry
table, with flan_dev_reg_enable alone in it, crashes the same way, and
that entry has been emitted since the registry was armed from a
constructor. What the lane did was add a second declaration to a shape
that was already crashing, where nothing built the combination to
notice.

So the table names a local definition that calls the two, which is the
shape clang emits for its own constructors, and it costs a call once
before main. The ordering it fixes was unspecified before -- two entries
at one priority -- and arming the registry before installing the handler
is the order that was wanted.

What it unblocks is the reason to care. flan_dev_crash_enable yields to
ASan through a weak __asan_init so the two do not both own SIGSEGV, and
that line could not have run, because the build it guards would not
link. It does now: a dev build of dev-segv.flan at -O0 under ASan takes
the fault, ASan reports it with the frame, and the handler stays out of
the way instead of parking. No alias covers the combination, so that
check was made by hand.
2026-09-21 09:45:54 +07:00
10b1abbf09 The last enum member takes its prefix, and the alias goes
TraceLogLevel carried `warning` beside `log-warning` so that sand.flan's
one call kept working while the rename landed. The review found it was not
inert: render.ml folds members last-wins, so a 4 read back as :warning in
the break loop, the did-you-mean offered it, and every TraceLogLevel error
listed it. A spelling nobody should write was the one the compiler taught.

So the call is respelled and the alias is gone. Every member of every mapped
enum carries its enum's prefix now, with no exception to explain.
2026-09-21 09:32:37 +07:00
da987169c8 Merge branch 'worktree-agent-ab561492e212bf034' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 09:27:15 +07:00
0a0517f1d1 Merge branch 'worktree-agent-a864e4baf08dc8a0e' into dev-loop 2026-09-21 09:04:59 +07:00
41334b3a8f Review follow-ups: a trap is not an evaluation, and the way out cannot be truncated away
The break buffer captioned every refused restart "below this evaluation", which
is the wrong sentence at a trap: there is no transfer channel, nothing on the
list can be taken, and there is no evaluation to abandon. The terminal listing
had always said the two apart. The wire now does too — a bare ! line ahead of
the entries, because a trap with no restarts at all still has to be able to say
so, and because :abandon being nil means three different things.

The boundary was also the first entry truncation dropped: snap_push walks
innermost first, and the boundary is the outermost frame of the evaluation. A
slot and a name's worth of bytes are kept back for it.

flan_break_resume goes; nothing has called it since choices became positions,
and NEXT.md already said there was no such function. eval_boundary is cleared
between runs beside the two stacks that already were. And the note on a taken
restart stops costing a second round trip: the agent answers ok abandon for the
boundary, which is the end that knows.

Nested boundaries are tested rather than argued: two evaluations, six restarts,
and abandoning the inner leaves the outer with its own still on offer.
2026-09-21 09:04:35 +07:00
fe6744ae41 An evaluation that signals can be abandoned, and the program keeps running
A C-x C-e that blew up took the session with it. The expression's break offered
either nothing at all — a bad index establishes no restart, and the program's
own are below the thunk boundary where a transfer has nowhere to land — or a
list on which every entry was refused. That left abort, and abort is _exit(134)
over a mistyped index.

So the boundary offers a restart of its own. The agent pushes a real frame
around every evaluation, after the floor is read so that it lands above it;
taking it unwinds to the thunk, flan_reload_call drops the channel it holds,
and the poll returns to the game loop. It abandons and does not undo, which is
said in the agent's line, the daemon's note, the buffer's row and the manual.

The other half was a silence. The break buffer drew every restart as takeable
and ignored the :unreachable the wire already carried, so a digit on one went
out to be refused and nothing came back. Those rows now carry the reason and
are refused where they are read, and :abandon names the position that drops the
evaluation — a position, because a program may establish a restart of that name
itself.

Not the threading, which is what the report suspected. The thunk does run on
the game thread; a thunk on a thread of its own would have had the same empty
list and the same abort.
2026-09-21 09:00:26 +07:00
216d1c0ab0 A program written to fault cannot be compared at -O2
The two new fault-by-design programs came into the corpus with the
bytes/bytes-view lane and the survey has been comparing them at its own
optimisation level, where the store into .rodata is undefined, LLVM
deletes it and exits 0, and this backend -- which has no optimiser to
delete anything with -- executes it and takes 139. That reads as a
lowering disagreement and is not one: at -O0 the two backends agree
exactly, and test_acceptance.ml's dies_segv rows already pin that on
both of them.

So a second exclusion list beside the one for the programs that never
stop, with its own reason written down, rather than building the whole
corpus at -O0 and changing the measurement every baseline was taken
against. dev-segv would belong on it in any case: it calls agent/start,
and under --dev it parks in the break loop rather than dying.

  MATCH 171, DIFFER 0, REFUSED 0, NOX86 0, SKIP 47
2026-09-21 09:00:25 +07:00
190fdadcb8 Merge branch 'worktree-agent-a617552a2c34ddc3c' into dev-loop 2026-09-21 08:11:55 +07:00
2d8d9cd5a8 dotimes counts from where you say, and can count down
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set. Now it is (dotimes [i 9 -1 -1]).

Three arities: [i n], [i start stop], [i start stop step]. The stop is
exclusive in all of them, so [i 0 n] is [i n] — one rule, not two — and a
negative step counts down, testing with > instead of <.

A literal step of 0 is refused where it is written. One that is only a value
cannot be, so the condition asks the sign first and 0 falls out of it as a
loop that runs no times: terminating and deterministic, and free, because a
literal step still emits the single comparison it always did.

Each bound is evaluated once, left to right, before the counter exists: the
start into the counter, the stop into the hidden slot it always had, the
step into one of its own unless it is a literal.

Still a special form, still a Let and a While with the step in the latch, so
neither backend learned anything — the new program prints the same thing
under --x86 and at -O0. load.ml's Form-level walk had to learn more than one
bound for the same reason parse.ml did; it is part of this feature and not a
bug that was sitting there, because before this a three-bound dotimes was a
parse error long before that walk could reach it.
2026-09-21 08:09:53 +07:00
52e9c92b0f The enum prefix goes uniform across all eleven
The author's ruling: "I think the prefix reads better, keep it" — so two
prefixed enums out of eleven was the inconsistency, not the prefix.
TraceLogLevel takes log-, CameraProjection projection-, CameraMode camera-,
GamepadButton button-, GamepadAxis axis-, Gesture gesture-, MouseCursor
cursor-, TextureFilter filter-, PixelFormat pixel-.

Two of those are judgement. CameraProjection and CameraMode share raylib's
CAMERA_ and deliberately do not share a Flan prefix: they are two questions
asked of the same struct, and :projection-perspective beside :camera-orbital
says which is being answered. GamepadButton and GamepadAxis take the short
stems rather than a shared gamepad-, which keeps :button-left-face-up and
:axis-left-trigger readable.

It is a reading choice and not a collision fix, and bindings, raylib.flan and
docs/BUILT.md all say so: a keyword resolves against the expected type and
nothing else, so :point at a TextureFilter site was never ambiguous. What the
prefix buys is the call site read on its own.

The three constant exception lines are keyed on the member's full Flan
spelling and moved with it. flan generate-c vendor/raylib is green against
raylib-5.5.h, and the check was confirmed non-vacuous by breaking it:
filter-trilinearr reported TEXTURE_FILTER_TRILINEARR rather than passing.
test_flan pins one member of each of the eleven to the C name the rule
reaches, read out of the real bindings file.

sand.flan line 121 is (rl/set-trace-log-level :warning) and is the author's
to respell. TraceLogLevel carries a warning alias beside log-warning, mapped
by name in bindings, so the suite stays green until he does; FIX.org has the
three-edit removal recipe.
2026-09-21 07:59:48 +07:00
0a8dc1d82b sand.flan takes the def/defonce split
The colours were the report: edited, re-evaluated, unchanged, because
defvar had said init-once and meant it. They are a def now, and so is
gravity, which is a number to turn while the sand falls, and game-data,
which is a file to edit and see re-read.

The grid, the velocity field and the arena stay defonce: they are what
the running game is, and a re-run that emptied them would be a restart
wearing a re-run's name. current-color is the same argument about a
smaller thing -- a brush you picked is a brush you keep.
2026-09-21 07:34:43 +07:00
9cfe7e7218 Merge branch 'worktree-agent-aff5ef76736f2ab5d' into dev-loop 2026-09-21 07:31:19 +07:00
1cd4e0ec2e The defining form is fixed at build time, all three of them
The first cut of the form-change refusal asked only about def and defonce
and asserted in its comment that defconst was another arm's business. It
was not: defconst to def at the same type fell past every arm, and defonce
to defconst fell past them into the consts republish, which stores the
declared value over live storage at the frame boundary. One refusal over
gconst and grerun together now covers all six directions.

And two coverage gaps closed by running rather than reasoning: reload-v6
carries a (def dial i64 5) the host was never built with, so the x86 image
path executes and its 5 shows in the transcript's arithmetic; dev-rerun's
echo reads counter in its initialiser and follows it 40, 41, 42, 43 across
re-runs, where a captured first answer would print 40 four times.
2026-09-21 07:30:57 +07:00
a64bee6d96 Review follow-ups: a new def's image, the keyword that cannot change, and the sweep
Three defects, all from lifting every def initialiser, none of which the
suite caught:

A def typed fresh into a live session came up zero and stayed zero. The
image flan_dev_global copies on the allocation is the only value a new
global ever gets — the host's .init-globals never calls its initialiser —
and both backends chose that image with Tast.const_init, which a def's
lifted Call fails by construction. Emit.initial_image reads the constant
back out of the lifted body; the x86 twin had the same bug.

Changing a global between def and defonce was silently ineffective: the
guard lives in the startup function compiled into the host, which a reload
cannot republish. Session.compatible refuses both directions and says to
restart; editing the value stays allowed.

And global/<n> no longer leaks into the signature refusal when a def is
retyped — the global loop names the same fact in words a reader can act on.

flan check prints def, defonce or defconst off grerun; (defvar) with no
arguments names the shapes rather than offering (defonce ); the docs,
plan.org, runtime comments and valgrind.supp are swept; BUILT.md states
the release-build cost and the uninit caveat.
2026-09-21 07:19:33 +07:00