571 Commits

Author SHA1 Message Date
2c6459ca8d Merge branch 'worktree-agent-a91f53d60aa4fd40d' into dev-loop
# Conflicts:
#	NEXT.md
2026-09-13 17:56:03 +07:00
d1464ee266 Memcheck is told an arena reset happened, and the agentless session is pinned
Two loose ends.

The arena was invisible to memcheck. free-all is retain-capacity, so from
malloc's point of view nothing died and round two of a reset arena could read
a byte it never wrote, print round one's value, and draw no report.
flan_arena_proc now issues memcheck's MAKE_MEM_UNDEFINED over the whole
capacity beside its registry call. Measured on the same machine: the control
produced ERROR SUMMARY 0 before and 6 errors from 4 contexts after, with
--track-origins naming the client request. It is a control in
test_valgrind.ml now rather than a printed note.

The macro is vendored, not included, and the argument is measurement: the
machine that runs the sweep has valgrind and not valgrind-devel, so a guarded
#include would compile to nothing exactly where it matters and the control
would go quiet with no diagnostic. There is also nowhere to put an -I --
flan_rt.c is cat'd into an OCaml string literal and handed to clang in a
scratch directory. The __x86_64__ guard is load-bearing: the same runtime is
built for wasm32-wasi and emscripten.

Cost outside valgrind: 23 instructions on the free-all path only, about 1ns
per reset over fifty million of them, against a run-to-run spread wider than
the effect. Nothing on alloc, resize or free. valgrind.supp still holds no
suppressions; the corpus stayed clean across the change, which is its own
finding.

merged_serve's warning path deserved a test and has one. The discriminating
fact is not the log line but the policy: two_process kills its child and
fails where merged_serve warns and serves anyway, and nothing held that
second answer in place. dev-noagent.flan plus the last block of test_dev.ml
assert the session still answers describe after the wait runs out. Verified
by reverting the policy: the block reports rather than passing. It costs the
full ten seconds and there is no way to spend less. HANDOFF-f1.md is deleted.
2026-09-13 17:55:00 +07:00
970fe8a278 Queue the slice-from-ptr message, which names a range nobody wrote 2026-09-13 17:51:36 +07:00
fbc9d3d314 Merge branch 'worktree-agent-a638cd779b2075b7d' into dev-loop
# Conflicts:
#	NEXT.md
2026-09-13 17:46:24 +07:00
fd873bb9d7 Mark the queue entry landed, and get the shim's cost right
NEXT.md had the new box as a sibling heading next to "Queued:", so a
reader scanning headings saw the same item twice and still queued.
Folded into one section with a Bound box, the way PORTING.md §1 does
it.

And the example's header claimed the GetCodepoint tail "costs a scan
rather than nothing". The shim's stack buffer is 256 bytes and this
message is 284, so the first thirty glyphs of each pass malloc. Said
so, because that is the kind of number the comments around it are
precise about.
2026-09-13 17:44:59 +07:00
e903e220fb The temporary package the reload case writes is removed again 2026-09-13 17:41:18 +07:00
185d162124 A pointer from C can state its length, and then it is a slice
`indexed` took an Array or a Slice, so a `(Ptr T)` that came back
from C was readable at element 0 through `deref` and nowhere else.
The length is not missing from the world — for `font.recs` it is in
the struct, one field over — it was missing from the language.

`(slice-from-ptr p n)` is the form that says it. No marker on the
name: `!` here means mutates and `?` means asks, and `zeroed`, the
nearest neighbour, carries neither; `ptr` is the marker, because a
`(Ptr T)` only ever arrives from a `declare-c`.

Nothing new in the representation. A slice is already {ptr, i64} in
both backends, so this is two insertvalues; `x86.ml` takes the new
constructor on its existing `unsupported` arm.

It refuses a first argument that is not a pointer, a negative literal
length at check time, and a negative computed one at run time — that
last through `signal_block` and `@flan_slice_error`, reused rather
than growing the runtime a function, and *signed*, because
`check_slice` compares unsigned and a negative i32 sign-extends to a
huge u64 that walks through it. Behind `f.md.checks` like the other
two: on at -O0 and -O2, off only when checks were asked off.

It owns nothing and needed no analysis to say so — a slice is not
move-only and carries no allocator, so `free` refuses it by the rule
that already refuses `(as-slice v)`.

`rl/font-recs` and `rl/font-glyphs` are where the promise is written,
beside raylib's own invariant rather than at every call site, and
they are the shape a count-naming binding directive could never have
covered. `examples/text-rectangle-bounds.flan` is the port that
motivated this and it runs; `test/programs/slice-from-ptr.flan`
covers the form with no raylib and no window.
2026-09-13 17:40:59 +07:00
dd3611f97d An edited package macro reloads as its new body, not its old one
Both unions kept the wrong side. macro_union keeps the left on a name
collision, and both callers had the older set on the left: Load.program
put the ambient set ahead of the packages it had just resolved, and
Session put the copy it had been holding since creation ahead of what
Load handed back. So editing a macro in a package and reloading the file
that imports it went on expanding the old body -- and said nothing,
which in this area is the failure that costs the most to find.

Two tests, because the two unions are reached by different paths: the
reload itself, and the C-c C-c after it, which reads the set the session
kept rather than the one Load just supplied. Each fails on its own if
only the other order is put back.

Also written down, and not fixed: C-x C-e expands no macros at all.
Parse.expr never calls the expander, so (unless ...) as a bare
expression is as much an unknown name as (mac/twice 4) -- the prelude
fails there too, which is what says it is an older gap. Changing it
changes what an expression evaluation means.

And the cost note is cut back to what was measured. Four macro modules
where the file's own macros leave two is what the cache shows; why four
is not settled and no longer claimed.
2026-09-13 17:40:05 +07:00
86174531b7 A package's macros survive the reload, and the suite runs them
The feature was built and never tested. Three things were missing.

The two packages holding a ring of macros and a macro that never settles
were not dependencies of the test stanza, so both non-termination
refusals failed on "no package at ..." rather than on their own reason.
They fire, and now the suite sees them fire.

The positive half of the rule had no acceptance case at all -- only the
refusal that pins the bare name. pkg-macro.flan is asserted at three opt
levels and on the dev path, which is where six package macros and the
program's own coexist in one file.

And the dev loop was broken in exactly the way that matters most here.
Session held the imported macro set but *replaced* it on every
evaluation, and the one form C-c C-c sends carries no import -- so
(mac/twice 4) compiled on the build and came back "unknown function" on
the first reload. It unions now. test_session drives two evaluations,
because one proves nothing: the first could have re-supplied the set.

BUILT.md said the expander collects from the prelude and the file being
compiled. It collects from imported packages too, and the refusal's old
reasoning -- that this needed a second import resolver -- was wrong for a
reason worth keeping written down.

Cold build cost roughly doubles for a program importing a package that
declares macros: a macro module is built per round and the package's
rounds are its own. Warm is unchanged at ~70ms.
2026-09-13 17:35:04 +07:00
1657b87e3d Merge branch 'dev-loop' into worktree-agent-adf9086e00872ff67 2026-09-13 17:24:36 +07:00
888915402e The backend first, then a freeze; any and drop are stashed 2026-09-13 17:23:05 +07:00
3b15501c4f Where this is at the end of the day, and what is not merged 2026-09-13 16:11:57 +07:00
1898a3157d Macros come from a package now, and the refusal's reason was wrong
Load.program takes forms: it reads the import forms, resolves them with the
one resolver it always had, and parses the file with the packages' macros in
front of it. The refusal said this needed a second import resolver at the Form
level. It did not notice that the file being compiled is parsed before Load
runs too, so no shape of the feature could have left import resolution where
it was.

Names arrive qualified, as a defn's do. (mac/twice 4) is a call and (twice 4)
is an unknown name.

Stopped mid-task: dune test was never run and the acceptance wiring is
unfinished. HANDOFF-macros.md has what is left.
2026-09-13 15:40:08 +07:00
861f591bb0 Merge branch 'worktree-agent-ac5ad16091bc3a40e' into dev-loop 2026-09-13 15:33:52 +07:00
a9c0c3e9bf The listing buffers are malloc'd, and a followed pointer says why it stops
Three follow-ups to the registry reader.

The agent's "reg types" handler held its four row buffers as statics. The lock
argument for that was right — request_lock is held across the whole of
handle_line from both entry points — but the BSS was not: 8KB in every build
this package is linked into, including a release build of a game that imports
the agent and never writes a row. That is flan_dev.c's own argument against a
fixed table at a thirty-second of the size. Malloc'd and freed per request,
like the watch handler beside it.

A followed pointer parses as an opaque node, so the inspector refused it with
"no structure for this type" — false and unhelpful at once. It has structure,
it is drawn, and the reason you cannot step in is that the step would start
from the pointee. Said that way instead.

And the three new commands have elisp tests: the address root's wire, that a
path is refused rather than dropped, the listing's rows and totals, the
overflow warning, and a build with no registry refusing rather than showing an
empty table that reads like a program holding nothing.
2026-09-13 15:33:25 +07:00
fe858811cb An address answers with a type, and a killed program is asked rather than hooked
The allocation registry had a recording side and half a reader. This is the
rest of the reader: point at any heap address, a breakdown by type, what is
still held, and the test that stops dev-ptr.flan's header from being read by
hand.

The recorded name, back to a type. The table records a string and has to —
the note is built where the concrete type exists and what crosses into the
runtime is bytes. What closes it is that the string is Types.to_string, which
is the source spelling, so the round trip is the language's own reader,
Parse.texpr and Check.resolve. No table of spellings is written down, so
nothing can fall behind Types.to_string, and a name that is not a type —
"pool slots" — is refused with the name quoted rather than defaulted.

The address root renders a (Ptr T) and not the pointee, which puts it through
render.ml's pointer arm: permission is asked in one place in the compiler, and
an address root and a slot root reach the same two answers by the same code.
Flan has no integer-to-pointer cast, so flan_dev_reg_addr is an extern beside
flan_agent_frame_slot, for the same reason.

One walk and two questions: a leak report is a breakdown with the dead left
out, so flan_dev_reg_by_type is one function and the agent formats it.

"At exit" is not a hook. A program killed by a signal runs no handler, which
is how a game under the editor ends, so (:op "leaks") is the authoritative
reader and can be asked at any moment including the one before the kill. The
atexit hook is for the program that returns from main, is registered from
inside flan_dev_reg_enable rather than by a file-scope destructor so that a
release build does not grow a third not-free place, and is off unless
FLAN_DEV_LEAKS is set because the acceptance table reads stderr.

The memcheck half of item 6 is deliberately not here.
2026-09-13 15:27:31 +07:00
16226c71d0 Merge branch 'worktree-agent-a721d74291e5f212c' into dev-loop 2026-09-13 15:26:43 +07:00
95697d230f Merge branch 'worktree-agent-ac605f8a376484762' into dev-loop
# Conflicts:
#	NEXT.md
2026-09-13 15:24:28 +07:00
8b79cae837 Merge branch 'worktree-agent-a4778b00512de90d3' into dev-loop 2026-09-13 15:24:04 +07:00
762bc988fa The map operations are deferred, and the clause is what pays for it
hashable? gated the type and not the operations: a generic could take and
return a (Map $t V) and could not get or put into one. The hash and the
equality are emitted as concrete symbols chosen from the key type, and
while $t is a variable there is no symbol to name.

The five arms that reach the pair - put, get, has-key?, reserve, clone -
now check their arguments and return a placeholder of the operation's own
type when the key is a type variable: Unit for put and reserve, None for
get so the (Option V) around it still checks, false for has-key?, a zeroed
map for clone. The node is thrown away with the rest of the abstract pass
and the real one is built in the copy, exactly as println's is.

What makes that different from print's free ride is the clause. A map
operation can fail at a concrete type; it is deferred anyway because
{:where (hashable? $t)} is in the signature, so the refusal lands at the
call that asked for the type, against a requirement the author wrote down.
A generic that declares nothing gets no deferral - deferred_key checks
first, and map_type has usually refused the signature already. So the rule
for the allow-list is not a headcount: either the operation cannot fail
after substituting, or a declared predicate gives its failure somewhere to
land. The comment at the print arm says that now instead of "stays two
long".

The instantiation-time refusal names the call site, the type it asked for,
the predicate and the clause, rather than repeating the generic's name
twice.
2026-09-13 15:23:37 +07:00
dec4bcb88b Say why the family argument in bindings does not reach the slice wrappers 2026-09-13 15:18:37 +07:00
5a1c2745b0 An idiomatic layer over the raylib bindings, and raymath in Flan
Three kinds of Flan face over the generated set, which stays honest to C
because that is what makes it checkable against the header.

A slice where C takes a pointer and a count: the eleven vector-array
drawing calls, all eleven rather than the three anybody calls, since a
subset has its hole where the next caller looks. Each guards the empty
slice, which is the part a hand-written call site gets wrong rather than
merely writes out -- raylib takes a count of 0 happily, but taking the
address of element 0 of an empty slice is out of bounds before raylib is
reached.

An Option where C signals with a sentinel: get-key-pressed and
get-char-pressed, raylib's two input queues, both of which say "empty"
with 0. What that buys is in text-input-box.flan, which read the queue in
two places -- once to prime the loop, once at the bottom of the body --
and now reads it in one.

Both of those use the `name` directive in bindings, so the generated
declaration keeps the symbol and gives up the name: nothing about the C
signature is hand-written and the generated half keeps its
agreement-by-construction with the header.

An enum where the header says int: key-up?, key-pressed-repeat?,
mouse-button-up?. These are NOT wrappers -- a C enum parameter has an
int's ABI, so the hand-written declare-c with the Flan type is the whole
fix. They were holes in families whose other halves already took a Key,
so (rl/key-down? :space) compiled and (rl/key-up? :space) did not.

Not built: with-drawing and with-mode-2d. A macro cannot live in a
package -- the expander collects defmacros from the prelude and from the
file being compiled, and one in an imported package is refused by name.
test/programs/pkg-macro.flan is that refusal.

And vendor/raylib/vector.flan, which is raymath written in Flan because
raymath is static inline and has no symbol to bind. A file of its own,
split on declare-c and not on "idiomatic": there is not one declaration
in it, so it is not part of the surface the header check reads, and
raylib.flan is 1300 lines already. clamp and lerp are deliberately absent
-- the prelude has both, and a second lerp would not even be the same
function, since the prelude writes (1-t)a + tb where raymath writes
a + t*(b - a).

Examples: the identical eight-expression box-around helper in
core-3d-picking and models-box-collisions is a half-extent subtracted and
added. shapes-following-eyes keeps its measurement and gives up one line
to v2-sub, which is the honest size of the gap in a file that is nothing
but vector maths.
2026-09-13 15:16:40 +07:00
fc4e51a106 The dev backend, wired — and conditions were never in the way
DISCUSS.md item 16, in item 15's form.

The headline is a measurement that contradicts what everyone expected,
this report included: a program that loops and prints carries no Signal,
no Handled and no RestartCase. Zero of each, measured with hist.ml before
a line was written. What drags conditions in is the bounds check and the
allocator — check_at signals, and signalling needs the channel and the
guard — and neither is a Tast node, which is why a histogram cannot see
either and why the guess was off.

41 of the 111 programs in test/programs build through --x86 and 40 print
exactly what the LLVM build prints. The 41st is bounds.flan and it
diverges on purpose: --x86 is silently a --no-bounds-checks build, and
that is named as a divergence rather than left in a code comment.

The other 40 are refused by name. check_no_transfer is what makes the
missing guard sound instead of hopeful — if nothing reachable can write
the channel, no call can return with it set — so the build stops with the
node and the function rather than compiling forty programs wrongly.

Both bugs found along the way are the shape item 15 predicted: the crash
and the cause in different functions, and correct-reading assembly beside
a wrong answer. The frame model is the thing that did not go wrong.
2026-09-13 15:14:33 +07:00
58b1f49cf2 A discarded value was being stored over the return address
edn.flan crashed by jumping into .rodata, several statements after the
mistake, and the assembly at the jump read correctly. Item 15 said this
is how hand-encoding fails, and it is: the crash and the cause were in
different functions.

The cause is one line of design. A form whose value is thrown away was
handed the sink, and the sink was spelled as an address — rbp+0. That is
the saved rbp, and rbp+8 is the return address, so a non-void form in
statement position stored its value straight over both. A 16-byte slice
did it in one rep movsb.

The sink is now compared by identity and never used as an address:
anything with a value that is handed it gets a frame temporary instead,
reclaimed immediately. The point is not the temporary, it is that the
store has somewhere legal to go.

edn.flan matches the LLVM build now — 60 lines of a hand-written EDN
reader, unions, options, nested collections and all.
2026-09-13 15:08:38 +07:00
fcdaa105af Unions and a two-index (at), which the corpus asked for by name
The sweep over test/programs named its own next two nodes. (at grid r c)
is one node with two indices and not two nodes — an array of arrays is
contiguous, so the second index walks into the element the first landed
on — and machine.flan is the program that says so.

Then unions: MakeCase, CaseField and Match. The payload offset comes from
lay_fields over the same two fields Emit.lay measures a union as, and a
case's field offsets from lay_fields over that case's own fields, so
there is still one layout calculator and this file is still a caller of
it. match reads the tag and compares, an Option reads an i8 at offset 0
and a declared union an i32, and everything past the tag and the binds is
shared — the arrangement emit.ml settled on, for the same reason.

An exhausted match falls through to ud2 rather than to whatever follows.
The checker proved it cannot happen; a defined SIGILL at the instruction
that fell through costs two bytes and is the cheap half of item 15's
question 4.

machine.flan, bytes2.flan, array-ctor.flan and destructure.flan all agree
with the LLVM build now.
2026-09-13 15:06:31 +07:00
ff548996df Merge branch 'worktree-agent-ab63ab2e0656f837e' into dev-loop 2026-09-13 15:04:26 +07:00
786656dfee (at a i) on the left of a set has to reach the array
The corpus sweep found it, and it found it the way item 15 said this work
fails: array-ctor.flan crashed, and the assembly around the crash read
correctly. (set (.x (at pts 0)) 1.5) went through lvalue, lvalue had no
case for At, and the fallback evaluates — so the store landed in a copy of
the element and the array kept its zeros.

emit.ml has this as addr's own At case. One line here, and the program
matches the LLVM build.

Two more programs beside the fizz: one for the internal calling
convention the fizz does not touch at all — a struct argument, a struct
return through the hidden pointer, f32 in the SSE half, eight integer
arguments so two go on the stack, and a slice by pointer — and one for
the rest of the core: a global with an initialiser, recursion, break,
continue, the bitwise family, unsigned shifts and the conversions both
ways. Both agree with LLVM.

al is now zero at every call this backend makes, including the three in
main that were reaching flan_rt_init, flan_argv and flan_exit without it.
Inert on a fixed callee; the point is that there is no exception to the
rule to remember.
2026-09-13 15:03:41 +07:00
75a430dce2 Strike two NEXT sections the day overtook 2026-09-13 15:03:19 +07:00
eebd5d6d2c Merge branch 'dev-loop' into worktree-agent-ab63ab2e0656f837e 2026-09-13 15:02:17 +07:00
b3cb657992 hashable? gates the type and not the operations, and say so where it bites
A map keyed by a type variable cannot be put into inside a generic body:
the hash and the equality are concrete symbols chosen from the concrete
key type, and there is none until the copy exists. The refusal now says
that, and says what hashable? does buy - taking and returning a
(Map $t V) - rather than leaving the reader to infer it.

Closing the hole means adding the map operations to the list of forms
the abstract pass defers to instantiation. That list is print and
println and nothing else, and every member is a place where a refusal
moves from the definition to a call site, which is what the abstract
pass exists to prevent. Two is short enough to hold in your head.

Also written down: four of the prelude's copyable? declarations are
convention rather than checker-enforced. The move analysis tracks
locals, not reads out of a slice, so swap! and friends check without it
- and would still duplicate a header at [(Vec i32)].
2026-09-13 15:01:49 +07:00
8472c50d57 Merge branch 'worktree-agent-ad330000e84e78a2f' into dev-loop 2026-09-13 15:01:35 +07:00
19353648a0 The game's state is fixed arrays with counts, so the verdict stops being conditional 2026-09-13 14:59:32 +07:00
70af1966a2 Braces are no longer a type: (Map K V) is the only spelling
The author's decision, and it removes the one syntax question generics
had. A return type can no longer be written in braces, so a {...} after
the signature is unambiguously the constraint map and there is no
structural rule to explain.

The reasons for the record: the brace's value meaning and its type
meaning do not correspond the way the bracket's do - [1 2 3] is a value
whose type is [3 i32], but {.x 1} is a value whose type is a name, and a
map value is built by map-new with no braces anywhere - and dropping it
reserves {} in type position for anonymous struct types.

Braces in a type are refused with the surviving spelling named rather
than falling through to "expected a type". Types.to_string and
Cimport's source printer both print (Map K V) now, and Shim refuses the
application spelling where it used to refuse only Ast.Tmap.
2026-09-13 14:58:27 +07:00
2155c41465 A whole program goes through the hand-written backend and runs
x86.ml was an encoder and a frame model with nothing calling it. It now
lowers a whole Tast.program to an assembly file, and `flan build --x86`
hands that file to the same clang invocation the LLVM path uses, against
the same runtime objects. The flag is off by default; LLVM stays the
release backend and the default one.

Three programs, built both ways and compared by what they print and what
they exit with rather than by reading bytes: exit 0; a dotimes that
prints; and a fizz over a call, an if, a remainder and two string
literals. All three agree with the LLVM build.

The measurement decided the target. hist.ml over the fizz program shows
no Signal, no Handled, no RestartCase — a loop that prints does not drag
conditions in. What does is the bounds check and the allocator, and
neither is in the reachable set of a program that prints a number.

That is why there is no transfer guard here, and check_no_transfer is
what makes the omission sound rather than hopeful: if nothing reachable
can write the channel, no call can return with it set. It is a
whole-program property, so it is checked once per build and the build
stops with the node's name when it fails.
2026-09-13 14:56:14 +07:00
de93ffc89e A cast to a type variable, and the container builtins over one
(t x) is not a name is_cast knows - t is not a machine type - so it is
its own arm, admitted by numeric? because a cast produces a number.
vec-new, pool-new and map-new all reach the one list of what names a
type, so the spike's line for vec-new had already covered the other two;
zeroed takes its type from the position it is written in. All four are
pinned in programs/generics.flan.
2026-09-13 14:52:17 +07:00
3e6886831d Queue the pointer-length question, with the three examples it blocks 2026-09-13 14:51:31 +07:00
dad725afe4 The prelude's per-type families collapse: 22 functions become 10, 27 become 16
swap!, reverse!, sort!, sort-by!, index-of, min-of, max-of, map!,
reduce and filter, each written once over $t. Every call site in the
corpus moves with them.

min-of and max-of are not min and max because min and max are builtins
over two or more numbers and nothing shadows a builtin. These reduce a
slice, which is a different operation at a different arity.

sort-bytes! did not collapse into sort!, and the reason is the point of
the predicates: a [u8] is not ordered? and cannot be, because < is an
instruction and comparing two slices lexicographically is a loop. It is
sort-by! with bytes<? written in, one line, keeping its name and its
stability note. sum-i32/sum-f32 and append-i64!/append-f64! stay for the
reasons the spike gave.

Not what the notes predicted: none of the ten collapses on a signature
change alone. filter and reduce need copyable? because the checker
demands it - reduce's accumulator at (Vec i32) is a double move - and
the rest declare it because a slice of owning elements would have them
duplicating headers.
2026-09-13 14:49:11 +07:00
10edda6de4 Two comments in vendor/raylib that this batch made false
The cursor block said DisableCursor is 'what nothing here needs';
core-3d-picking toggles it from the right mouse button. Enable/DisableCursor
stay generated — they take no arguments, so there is no Flan face to improve,
which is the same rule that leaves GetMouseX there — but the sentence had to
go.

And bindings named draw-cube-v as one of its three deliberately-generated
variants, which it no longer is. The paragraph now says what happened to it
rather than asserting the opposite of the exclude list below it.
2026-09-13 14:46:00 +07:00
d7ceec448e Ten more raylib examples, and the three small structs 3D needed
Three shapes, two text, three textures, one models and one core, picked for
binding surface rather than for how they look.

shapes-basic-shapes brings in six draw families nothing had called — the
circle and rectangle gradients, the triangles, all three poly draws — and is
the first call in the corpus to pass two Colors or three Vector2s at once.
shapes-collision-area is get-collision-rec, the only binding that takes two
Rectangles and answers a third, on a frame path. shapes-following-eyes is the
raymath gap measured rather than worked around: every line of it is vector
arithmetic written without a vector library, the way the C writes it.

text-input-box drains get-char-pressed's queue, which no example had read,
and needed a MouseCursor defenum for set-mouse-cursor. text-writing-anim
replaces TextSubtext — unbindable, it answers a pointer into a rotating
static buffer — with (string (slice b 0 n)), which is the same operation
without the shared state.

textures-image-generation runs nine Gen* calls and the
gen/upload/unload-image path, all procedural, no file on disk.
textures-fog-of-war needed a TextureFilter defenum: the smooth fog edge is
entirely :bilinear on a 25x15 render texture, and it is also the first
draw-texture-pro with a negative source height. textures-mouse-painting is
the same render texture used as a document rather than as scratch, plus the
round trip back off the GPU — load-image-from-texture, image-flip-vertical,
export-image — which nothing had run.

models-box-collisions is the counterexample to "a models example is a binding
exercise": nothing in it is a Model, and one BoundingBox defstruct un-refuses
four functions. core-3d-picking is the only caller anywhere for Ray and
RayCollision, and picking is the inverse of the get-world-to-screen the
corpus already had.

Added to vendor/raylib: defstructs BoundingBox, Ray and RayCollision;
defenums MouseCursor and TextureFilter with their mapping lines in bindings;
hand-written declare-c for SetMouseCursor, SetTextureFilter, DrawCubeV,
DrawSphere, DrawSphereWires, DrawRay and GetScreenToWorldRay, each excluded
from the generated half on the rule bindings already states. generated.flan
regenerated against raylib 5.5: 272 declarations, 117 refused, every
defstruct, hand-written declare-c and mapped constant agreeing with the
header.
2026-09-13 14:42:49 +07:00
b438a71031 C-c C-c on a generic installs its copies, and a refusal about one says where it came from
A generic defn produces no Tast.fn, so the editor was told nothing had
been installed and nothing had gone wrong. eval now expands a redefined
generic name to its copies, and picks up any copy the running process
was never built with - which is how a redefined caller reaching a
generic at a new element type gets that copy built and loaded.

C-x C-e is the path that could really go stale, and did: it checks
against the live environment, so an expression naming a generic at an
unused type generated a copy that existed in no program and the thunk
called a symbol nothing defined. Marked and spliced.

There was no cache to invalidate. program_with_env builds a fresh env
every evaluation, so the instantiation cache cannot survive one; the
test pins that rather than inventing machinery for it.

A signature change reaches the session as a refusal about put!-i32, a
name the source does not contain. It now says which generic it is a
copy of, at which types, and that every copy changed together.
2026-09-13 14:37:55 +07:00
68625a535e A bindings gap stops a build, and says which file to edit 2026-09-13 14:34:19 +07:00
7f86f32699 where predicates admit operators, and a type variable is move-only until it says otherwise
The spike proved the shape; this makes it the feature. A generic body is
still checked abstractly once, but now it may be told what to assume:
{:where (ordered? $t)} at the head of the body, Clojure's {:pre [...]}
spelling, with five predicates - ordered?, equal?, hashable?, numeric?
and copyable?.

The syntax catch settled structurally: {K V} is still a legal return
type, and a constraint map is told from one by its leading keyword. A
keyword is not a type anywhere in the language, so the slot after the
return type is unambiguous and {K V} did not have to go.

A type variable is move-only by default, with copyable? the opt-out.
Move is the stricter rule, so assuming it can only refuse a valid
program, never admit a bad one. That is Rust's T: Copy and not Odin's
anything - Odin has no move semantics at all.

The runaway refusal no longer names a depth. It names the chain: a
generic already on the instantiation stack, asked for again at a type
built around the one it had before, is growing and will not stop.
2026-09-13 14:33:45 +07:00
2957804ebc The header ships with the package, so the check is not optional any more
vendor/raylib/web/ is gitignored, so the header a build-web.sh tree happens
to have never appears in a git worktree. Every parallel lane that touched
bindings was checking against nothing and was not told. One went looking and
used a copy out of ~/.local/share/Trash, which happened to be byte-identical.
A check that silently does not run is worse than no check.

raylib.h is 128K and now sits beside the package it describes. The argument
for ?${FLAN_RAYLIB_H} was that requiring a header would make a build need
raylib-devel, and that dissolves when the header is committed here. There is
no variable to export any more; to check against a different header, edit
the line or replace the file.

Measured, because always-on has to be paid by every build: a warm build of
raylib-font.flan is 0.10-0.11s against a 0.10s baseline. The header read is
cached and keyed like the object cache, so this is under the noise. Verified
it fires by putting f64 where raylib says int and watching an ordinary build
refuse, naming the tracked header.
2026-09-13 14:32:38 +07:00
d66d717716 Queue generic structs and array lengths, after the core lane and not beside it 2026-09-13 14:29:14 +07:00
70e19753dd Merge branch 'worktree-agent-afcd2406f3660629b' into worktree-agent-ab63ab2e0656f837e 2026-09-13 14:22:33 +07:00
b5d2b5eabd Merge branch 'worktree-agent-a638e5d0f0de7a058' into dev-loop 2026-09-13 14:20:56 +07:00
e4487bb782 Merge branch 'worktree-agent-a596c7c569d5b0ec9' into dev-loop 2026-09-13 14:20:51 +07:00
06810e3906 Merge branch 'worktree-agent-a8eecdace6e85881f' into dev-loop 2026-09-13 14:20:51 +07:00
cebd07c530 Park two decisions from the bindings lane 2026-09-13 14:20:31 +07:00
4ff3e9a922 A finding about the bindings file is not a reason to stop a build
check_constants makes two kinds of finding and they were treated alike.
A value that does not match, or a C name the header does not have, is
the library contradicting the package and stops a build the way a
permuted defstruct does. An enum nobody mapped and a rule that reaches
nothing are about the package's own bindings file -- real, and worth
fixing, but telling a lane that added a defenum to go and edit a config
in a message shaped like "your layout is wrong" is the wrong thing to
fail a build with. Those gate generate-c, where that file is edited.

Also: a const prefix now counts as reaching a name before an explicit
constant line is consulted, so a rule whose every match is also spelled
out by hand is not reported as matching nothing.
2026-09-13 14:16:57 +07:00