233 Commits

Author SHA1 Message Date
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
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
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
861f591bb0 Merge branch 'worktree-agent-ac5ad16091bc3a40e' into dev-loop 2026-09-13 15:33:52 +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
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
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
75a430dce2 Strike two NEXT sections the day overtook 2026-09-13 15:03:19 +07:00
3e6886831d Queue the pointer-length question, with the three examples it blocks 2026-09-13 14:51:31 +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
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
4072dd2089 A type variable moves unless it says otherwise, and the clause is a map 2026-09-13 14:14:55 +07:00
9223c9002a An enum is four bytes, and the header check now reads the constants
Two gaps the raylib examples hit.

The layout check compared a Flan enum against the header's `int` and
called it a disagreement. It is not one: Shim.cty lowers a defenum to
int32_t in a struct field exactly as it does in a parameter, which is
what the signature check already knew and the layout check did not. One
predicate now serves both, symmetric, and tolerant of a 32-bit integer
and nothing else -- f64 against the library's float still fails, in the
very struct whose other field is an enum. Camera3D.projection is a
CameraProjection again and rl/camera-projection is gone with it, so
`.projection :perspective` resolves at the construction site.

And generate-c's claim said nothing about a defconst or a defenum
member, so a wrong flag bit was completely silent. `bindings` gained
`enum`, `const` and `constant` lines saying what a Flan constant is
called in C -- the prefix is nowhere in the Flan name, so it is declared
rather than guessed. Nothing goes quiet in either direction: a name the
rule builds and the header lacks is reported, a rule that reaches
nothing is reported, and a defenum with no line is itself a finding,
because otherwise the silence just moves up one level.

clang's dump gives anonymous EnumDecls for every raylib enum and no
value at all for an enumerator written without `= n`, so the constants
are one flat table and the values are counted the way C counts them.
cache_format bumped with the dump type.
2026-09-13 14:11:35 +07:00
0a6199b8df The failure path says which socket it was, checked by running it
FLAN_CLANG=/bin/false against test_repl: 'the daemon exited with status
2 before binding /tmp/flan-repl-dev.sock' in 45ms, where the old wording
would have waited out the whole timer and then blamed the socket.
2026-09-13 14:06:46 +07:00
cdcdd70c4e The object cache outlives the run, and the await says which wait it was
Build.cachedir sat under TMPDIR, which dune makes private per run, so no
test run ever reused an object and every build in the suite was cold. It
moves to $XDG_CACHE_HOME/flan/objcache (FLAN_CACHE_DIR overrides), which
is safe because the keys are total: compile_c digests the source text,
the compiler's stamp and every flag; wasm_resource_dir digests the
builtins archive; compiler_object digests flan.cmxa and flan.a. Writes
were already .tmp-then-rename, so concurrent dune jobs are fine.

Macro.key was the one key that was not total -- prelude text plus the
call's forms, and nothing about the compiler whose codegen produced the
.so it names, which is dlopened straight back into this binary. Under a
per-run TMPDIR that never showed; under a durable cache it is a stale
expander that crashes rather than a compile error. It carries the
compiler's stamp now, handed across start_merged's exec in
FLAN_COMPILER_STAMP because a merged dev binary lives at a per-session
path and keying on that rebuilt a macro module every dev start.

Measured on dev-repl.flan, launch to bound socket: 2.0s cold against
0.48s warm. Whole-program flan build: 1.44s against 0.06s. Full dune
test 25.7s/30.1s before, 24.0s after, user CPU ~50s down to ~34s.

And the await: one timer covered two waits, a build then a bind, so
'the daemon never listened' was a wrong diagnosis of a build that had
not finished. listening now polls the process alongside the socket and
says which -- exited with a status, or still running and therefore still
building. A daemon that dies fails in milliseconds instead of costing
the whole timeout. Thirty seconds, down from a minute, because the build
it waits on is warm now.
2026-09-13 14:04:50 +07:00
b6bb1e502b Generics: abstract checking with where predicates, and plan.org was wrong about Odin twice 2026-09-13 14:04:01 +07:00
635f4d30a6 A stopped program does not sample, so the editor stops asking for a new window
The watch accumulator clears a slot lazily, on its next sample, and the reader
never compares epochs. A previous lane read that as a defect and left
watch_render_num's n=0 arm behind as dead code against the day the reader
became epoch-aware.

It should not. A stopped program takes no samples, so an epoch-aware reader
would report every slot as empty for as long as the program sat in a break
loop — and reading the numbers from the moment you stopped is the whole point
of stopping. The lazy clear is the right answer there.

What was wrong was narrower and lives in the editor: flan-watch--tick sent
`:reset t` five times a second at a program that could not answer it. The read
still goes out every tick; only the reset field drops, guarded on
flan-dev--stopped, which flan-dev.el's background poll already keeps current.

The n=0 arm is deleted rather than commented, since the only way to reach it is
the epoch check just rejected and dead code is an invitation to add one. n is
at least 1 whenever watch_render_num runs, so sum/n cannot divide by zero.

test-flan-watch.el asserts both halves with no daemon: a running tick carries
:reset, a stopped one does not, both still read the table and both leave a
reply in flight. Verified by mutation. What it cannot reach is the daemon
honouring the absent field; test_dev.ml drives a real program for that.
2026-09-13 13:57:52 +07:00
781295e862 Merge branch 'worktree-agent-a30e436605521558c' into dev-loop
# Conflicts:
#	NEXT.md
2026-09-13 13:47:12 +07:00
72d6b8b00e Merge branch 'worktree-agent-a7fa4f6e37607b13b' into dev-loop 2026-09-13 13:46:47 +07:00
122ca45ebd Merge branch 'worktree-agent-a26b9a22ff124dd1a' into dev-loop 2026-09-13 13:46:47 +07:00
6de6c079b0 What the "never listened" flake actually was, written where it was hunted
Two lanes were sent after a contention race on a socket path. There is
none.  The sentence came from three sites and not the two HANDOFF-f1.md
named — test_dev.ml:77 checks the *dev* socket with exactly that wording,
in the same block whose *agent* socket check was fixed — and the cause is
a bound too short for a build: flan dev links the whole program before it
binds, ~600ms warm, measured at 6.5s here and 6.6-6.8s by the lane that
fixed it, against a 5000ms await in test_dev.ml and 8000ms in the other
two.  Build.cachedir sits under dune's per-run TMPDIR, so that build is
cold on every invocation and the failure is not occasional at all.  The
fix landed on another branch; nothing in this commit touches a test.

The agent.sock ordering fix it does confirm: 30 sequential full dune test
runs, 0 failures, no "never bound" in any log — so NEXT.md's "One flaky
test, measured rather than suspected" section, left standing for whoever
confirmed it, goes.

HANDOFF-f1.md's "lengthening any timeout" bullet is qualified rather than
deleted.  It was true of the agent.sock check and read as a rule, and
reading it as a rule is what kept this open for a second lane.
2026-09-13 13:12:17 +07:00
7be57efe18 A type variable is marked, because absence is a bad thing to give meaning to 2026-09-13 13:09:25 +07:00
f2be0a62dd A pause is waited for by name, and a build is not a socket
Two follow-ups to the marking commit.

`Dev.eval_expr`'s new wait matched `Stopped _`, which fires on the first
iteration when the program is already parked on something else — the
break loop allows evaluating, so that is reachable — and answers for a
thunk that has not run yet, on a reply whose own `:condition` names the
other condition. It now waits for `Stopped "Pause"`, which the agent
reports under a nested break because `condition_name` is overwritten on
the way in and restored on the way out. `dev-pause.flan` grows a
`Missing` and a `boom` so the test can park the program on something
else first and tell the two apart.

And the flake NEXT.md had as "seen once and unexplained": `the daemon
never listened` is not a race, it is an llc-and-link of the whole
program before `flan dev` binds — ~600ms idle, measured at 6.6s and 6.8s
with the rest of the suite beside it, against a 5s and 8s await. All
three test binaries now wait a minute; the watchdog is what bounds the
run. Two consecutive full runs green.
2026-09-13 13:07:25 +07:00
97cb77d949 Five gaps the examples hit, written down before they are forgotten 2026-09-13 12:54:42 +07:00
5791faee4e A breakpoint is a function call, and the editor only says where
Finishes DISCUSS.md §9's `pause` marking: the daemon half was already
built, this is the editor half plus the one daemon path it was missing.

`C-u C-c C-c` marks the form point is inside, `C-u C-u C-c C-c` the
top-level form (stop on entry), `C-u C-x C-e` the expression before
point. The buffer is never edited — the position rides beside the code
and the `(pause)` call goes into the tree after parsing, so no source
location moves.

`C-x C-e`'s path needed the daemon: its 5s `wait` answered "the program
did not reach a frame boundary", which is exactly what a thunk parked at
a breakpoint looks like from out here. `wait` is now three-way and asks
`state t = Stopped` only when a pause was requested, so the no-pause
shape `test_dev.ml` pins is unchanged.

The overlay is an annotation and not feedback, so unlike an error marker
it survives `pre-command-hook`; what takes it down is an accepted
evaluation with no `:pause` on it, which is the same thing that takes
the mark itself down.

Tests: a `test_dev.ml` block over the new `dev-pause.flan` that marks,
stops, re-evaluates plainly and then polls half a second confirming it
does not stop again — one sample after `continue` proves nothing, the
resumed frame is still in the old body — and an `emacs/test-flan-dev.el`
block for which form a prefix picks, the byte column, the overlay's
lifetime, and one live round trip.
2026-09-13 12:51:06 +07:00
191dcc1246 An abandoned frame rolls back, and restore goes after the defers
PORTING.md Tier 1 item 6, the last one on that list. Not a language
feature and nothing was added to the language: restart-case, struct
assignment and fixed-arrays-as-values were already built, and what was
missing was the worked example. test/programs/frame-rollback.flan is it
— snapshot at the top of the frame, restore in the `continue` clause,
over one fixed array and one struct, which is engine.clj's grids plus
engine.lisp's shallow copy of the state object. Two `set`s each way,
because both are values; there is no IntGrid walk and no
sb-mop:class-slots walk to write.

The decision in it is the ordering against defers, and both orderings
compile. An answered bounds failure runs the abandoned function's
defers, innermost-first, before the restart clause body starts. Restore
in the clause is therefore the last write on the abandoned path and
needs no agreement with what any defer did on the way out. The rejected
alternative — restore in a defer inside the frame function — also runs
on the ordinary return path, so it rolls back the frames that
succeeded, and nothing reports that as an error.

Pinned with numbers rather than prose: a tick counter inside the
snapshot, written by the frame's defer, reads its pre-frame value,
while a counter outside the snapshot shows the defer ran. And there is
a negative control, the same bad frame with a `continue` that only
counts, because "state equals snapshot" passes trivially on a program
that never wrote anything.

Three acceptance rows beside bounds-condition.flan's, for the same
reasons: -O0, and the dev build where every call goes through a cell.
2026-09-13 12:44:02 +07:00
a016c3f226 Queue what four lanes left, and where each one wrote it down
Three handoffs at the root carry the detail; NEXT.md carries the queue.
F2's Emacs half is seven items with two traps in it, F1 leaves a second
unexplained flake and the sweep that would let its section be deleted,
F3 leaves the two tests its own change did not get. The raylib examples
were never started, and Tier 1 item 6 still has not moved.

Also the worktree problem, because it nearly cost two lanes their work:
at least two were handed out about 485 commits stale, on a tree without
lib/dev.ml. Both noticed. One that did not would look like it worked.
2026-09-13 12:22:17 +07:00
6592cff743 Merge branch 'worktree-agent-aac8d8a2a09e2b151' into dev-loop 2026-09-13 12:19:20 +07:00
aa6087af91 sand.flan is the game the other ports are, and nothing else
765 lines to 201, at parity with lisp/sand.lisp, clojure/src/fnm/sand.clj
and src/fnm/sand.jank. Gone: audio and tone synthesis, the brush textures
and the embedded PNG, the render-texture scene, the HUD font, the camera,
the world cursor, the HUD and the input-state read-out. None of it is in
any reference version, and none of it was the language being exercised.

Inlined the single-use helpers -- empty-at?, move-grain, draw-grid, paint.
settle also carried an unused (rl/Vector2 ...) binding, which put raylib
inside the section whose banner says there is none, and a stray (pause).

The physics is untouched on purpose: the references disagree there, so
parity does not name a target, and settle is what the pinned hash covers.
It still prints 15595743031174623232 at -O2, -O0 and as a dev build.

test_web.ml asserted brush.png's bytes reached the wasm module, proving an
embed survives the web build. web-files.flan is web-built and *run* under
node and asserts the embedded bytes print, which is the same property
checked harder, so the sand assertion goes rather than the embed staying.
2026-09-13 12:19:13 +07:00
fc319474b6 A hot loop keeps five numbers, and the window is the editor's
PORTING.md Tier 1 item 5. The spy half of the watch was already built — the
pushed table, the buffer, the inline ghost text. What was missing is spy-num,
which is the part that item calls least obvious and most valuable, and it is
what this is.

A slot keeps count, min, max, last and mean. Each answers a question you can
ask without building a query: n is the first thing wrong when a loop is wrong,
the range is what one sample can never show you, last is what the scalar watch
would have given you, and the mean is a running sum divided at read time
because a mean accumulated as a mean drifts. A small ring of the last N
samples was the other candidate and loses — N out of 91,200 is a sample of the
tail of the loop rather than of the loop, and past five numbers every richer
answer is a UI for building a query.

The write path does no formatting, which is the feature rather than an
optimisation: a snprintf per sample at thousands a frame is a HUD that costs
more than the game. A sample is a load, five compares and the slot's seqlock;
the listener thread renders once per editor tick.

The window is since the editor's last tick, and that is a deliberate
divergence from watch.clj, where the stats are cumulative until reset-spies!.
Cumulative min and max reach the session's extremes within seconds of play and
then never move again, so the two most useful of the five go dead exactly when
you start interacting with the thing you are debugging — and this tool exists
to show you a number while you drag the mouse. Reset is its own message and
never a side effect of reading, because a destructive read makes looking
change what is there and anything that polls would shorten the window under
the editor that owns it. It bumps one epoch counter and clears no slot; a slot
clears itself on its next sample, so the reader never writes the table.

Ghost text needed one character. The call regexp allowed one hyphenated
segment, so watch-num-i64 backtracked to failure and a numeric watch got no
inline value while appearing normally in the buffer.

dune test is green, run twice. HANDOFF-f3.md carries the reasoning, the two
small gaps left behind it, and what did not work on the way.
2026-09-13 10:35:20 +07:00
d07edef8e5 loop and recur, and into that fuses a chain 2026-09-13 09:35:49 +07:00
cd0fef35eb A restart is not a transaction, and a watched value shows at its call site 2026-09-13 09:35:43 +07:00
c586eed138 Ghost text finds its anchor in the buffer, not in the table
flan-watch-ghost-mode paints each watched value inline, after the line holding
the call that wrote it. An addition to the watch buffer and not a replacement:
both can be on at once, and turning either off leaves the other running.

The earlier note said ghost text was gated on a (watch ...) form in check.ml,
because nothing in the table carries a source location. That is true of the
table and the conclusion did not follow. The call site is in the buffer, and
the name in the table is the string literal in it, so the anchor is searched
for rather than reported. Nothing new is asked of the daemon. The head of the
call is a defcustom regexp, because watch-i64 is a name the program's author
chose in their own declare-c and only the C symbol behind it is fixed.

Both pictures are painted from one reply in flan-watch--absorb, so they cannot
disagree and there is no second watch request in flight. That meant the watch
buffer could no longer be the subscription: arming and the timer now hang off
flan-watch--consumers, and only the last consumer out disarms the table.

Overlays are replaced wholesale on every repaint rather than followed through
edits, which is the whole answer to invalidating one whose line moved. Only
buffers shown in a window are scanned.

Settled and written down: two sites of one name both show it and say so,
because the table has one slot and the last writer wins; a watch in a loop
shows the last value written, as the buffer does, because every better answer
is the query UI this design exists to avoid; a stopped program's values say
"last frame" and change face, since inline they sit in code that looks live;
a site with no row is annotated only when the table reports overflow.

syntax-ppss moves point and clobbers the match data, so calling it inside a
re-search-forward loop and then reading match-string restarts the scan and the
loop never ends. Everything is read out before the check now.

emacs/test-flan-watch.el covers it, loaded from test-flan-cider.el the way
test-flan-mode.el is, so no build change is needed. 203 checks, 0 failures.
2026-09-13 09:33:16 +07:00
0405666b1f The two cases the tests did not cover, and one leak of a permission
A type-changing (map f) is the case into's single shadowed element name
would break if the shadowing were a trick rather than the language's
rule; it is not, because each stage is a fresh slot at its own type, and
into.flan now runs an i32 source into a (Vec f32) to say so.

A move-only accumulator carried round by recur is the shape BUILT.md
pitches the form on and was untested. It works, and recur.flan now
carries a Vec three times round and answers with it.

block's empty-body arm returned before the loop that distributes the
tail, so (do) in a tail position left ctx.tail set for whatever was
checked next. Latent rather than live — every consumer sets it
immediately before use, and the leaking form is always Unit-typed — but
it is one line to close and the invariant is easier to state closed.

Also the PORTING.md line listing loop/recur among the things with no
customer: it was built, and the half of that finding that still stands is
tail calls, which were not.
2026-09-13 09:32:58 +07:00
ca14394e0f into fuses at compile time because it is a macro, not a transducer
(into xs (vec-new i32) (map double) (filter even?)). The function name is
syntax and never a value, so (map double) is (double x) written into the
loop body: no intermediate collection, no closure, no generics, nothing
to inline. Transducers would compose at run time and Rust's iterators
need monomorphisation; a macro needs neither. into.flan counts the pulls,
which is the assertion a unit test cannot make.

The destination is in the form because the destination is the allocation,
and that is what makes spec-memory.md's explicit-allocator rule true by
construction rather than by convention. Which also settles the open
question: reductions do not share the form. A seed is not an allocation,
so (into xs 0 (map cost) (sum)) would be a second form wearing the same
spelling, and the destination would stop being honest about what it is.

A source that is already a name is used as it is, not bound. A (Vec T) is
move-only, so binding it would take the caller's ownership for something
that only reads; a fixed array would be copied once per into. A source
that is anything else is still bound once, which is what a call needs.
The wart is that an owning temporary there leaks, because the binding has
a name the caller cannot reach — a call in that position should borrow,
and drop is what would close it.

All four of the prelude's macro limits bit and none blocked anything. The
three refusals are names nothing defines, which is the only error
facility a macro has. into-wrap is a defn using only special forms, so
Macro.reduce does not drop it, and it is the first thing in the prelude
written as a loop/recur.
2026-09-13 09:27:20 +07:00
c95f11ff18 What the registry answers, and the two places a release build is not free
BUILT.md on the split that is the design — the type is emitted because only the
checker knows it, the death is not because an address needs no type — and on the
part that reads backwards: (Ptr Enemy) already said Enemy, so the registry
supplies permission rather than identification.

The honesty is in the same section rather than a footnote. A release build pays
a load and a not-taken branch per free, because flan_dev.c is in every build and
a second allocator selected by a build flag is worse than a branch. The table is
calloc'd when armed rather than declared, so nothing else is carried. The arena
free-all is answerable now and still invisible to memcheck, and those are two
different claims.

NEXT.md keeps the entry open for what was not built: an op that points at an
arbitrary address, the breakdown by type, leak attribution, the memcheck half,
and the test_dev.ml case that would drive dev-ptr.flan.
2026-09-13 09:25:51 +07:00
fedaec3e18 recur is checked, which is the reason to prefer it over tail calls
There is no TCO here and recur is not a cheaper substitute for one: the
compiler verifies the call is in the loop body's tail position, so the
mistake is a compile error where it was written rather than a stack
overflow somewhere else. A loop is a let, a While whose condition is
true, and two jumps — emit.ml is untouched, and the barrier question
recur asks is the one labelled break already answered.

Tail position is a permission that is withdrawn at the top of check, the
same read-and-withdraw defer_ok does, handed back only by a block's last
form, both arms of an if and a match arm. So nothing enumerates the forms
that are not tails, which a pre-pass over the Ast would have had to, and
would have had to keep doing.

loop is also a barrier for break and continue, which is added rather than
inherited: a loop answers with the value of its body and a jump out has
no value to give. That is also why it takes no label. A while inside a
loop keeps its own break.

Two things the shape forced. A loop binding is a plain name, because
destructuring would make recur's argument count unreadable off the
binding vector. And in_loop's "moves a value bound outside the loop"
rule had to be told about the loop's own names, or (loop [v (vec-new
i32)] ...) would have been refused for doing the ordinary thing.
2026-09-13 09:17:56 +07:00
042b2ce4d0 A restart is not a transaction, and nothing rolls back
If a frame mutates a global and then signals, taking a retry re-runs the
mutation. Control resumes at the restart-case and runs forward; nothing is
undone. Common Lisp has the same property and offers no help either, so this
is written down rather than fixed.

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

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

conditions.org and web/index.html already carried the mechanical half as a
one-line gotcha; those are rewritten in place rather than gaining a second
bullet beside them. spec-conditions.md takes it in section 5, which already
enumerates what a transfer does and does not do. No numbered case changed
meaning.
2026-09-13 09:14:46 +07:00
61c41ed27c Structural typing and tuples are one piece of work 2026-09-13 09:09:29 +07:00
83369196a9 Keep the registry simple; revisit only on evidence 2026-09-13 08:59:27 +07:00
fc6b0f5aba An allocation registry answers what is at an address, without tagging a struct 2026-09-13 08:57:13 +07:00