316 Commits

Author SHA1 Message Date
c124df36ca A move-only global is borrowed, never moved 2026-09-17 18:42:37 +07:00
69f5d8a05a A global Vec is borrowed, never moved, and outlives every entry to main
A program that wants to load its data once and keep it could not say so. Every
move-only global was refused where it was declared, on an argument about the
dead set being per function: two functions each freeing the same global would
be a double free nothing could see. The argument was sound and the conclusion
was too strong. It assumed a global has an owner. It does not.

Reading a move-only global is now always a borrow. Nothing may take ownership
of one, so nothing may free one, and with no owner to hand over there is no
double free left to catch. This is not a general ownership model for globals
and is not meant to grow into one: it is sound precisely because the lifetime
question that model would exist to answer has a constant answer here, the
process's. The refusal lands at the read, which is where a move would have been
recorded for a local -- passing the global to something that owns its
parameter, binding it to a local, returning it and freeing it all reach the
same place, and each is told to borrow instead, or to clone if it really wants
something of its own.

Such a global is mutable where it stands. push, put, reserve and set already
take their target through the borrow path, so a global (Vec u8) is filled and
grown in place, and the aliasing that raises is the one every Vec has:
spec-memory.md's explicit Zig/Odin contract, where a push that reallocates
invalidates a slice taken before it and the dev build's generation word traps
on the stale one. Globals get no borrow rule locals do not have, because the
hazard is not new and the trap lives on the Vec rather than on the binding.

What a move-only global may not do is carry a computed initialiser. A global's
initialiser is a link-time constant -- there is no init-at-startup path in the
LLVM backend by design, and the x86 backend that has one deliberately leaves it
out of a reload module, because re-running an initialiser wipes the live state
reloading exists to preserve. So the global starts zeroed, which for a Vec is
an empty Vec and therefore a value rather than a placeholder, and the load is
an ordinary assignment in whichever function loads it. That is also what makes
the data survive: nothing runs between one entry to main and the next, so a
re-entered main finds the global as it left it. A defconst cannot be one at
all, since a constant is not an assignable place and nothing could ever load
it; both refusals name the (defvar g (Vec u8)) that works.

The reload fixture gains a global Vec in the host and another that arrives at
run time, because that is where declaring instead of defining has teeth: a
module that defined the host's Vec would take a zeroed header of its own and
strand the block the process is still using, which a re-zeroed i64 cannot
demonstrate.
2026-09-17 18:42:01 +07:00
de3ad8c052 defenum members may leave their value to the previous one 2026-09-17 18:36:34 +07:00
3771f6820b An enum member's value is optional, and a value nobody chose is refused
Every defenum member had to carry a literal integer, so an enum of twenty keys
was twenty numbers typed by hand and renumbered by hand the first time a member
was inserted in the middle. A value may now be left out, and then it is the one
above it plus one, starting at 0 -- C's rule, because the enums written here are
as often a transcription of a header as they are original.

Autoincrement brings its own silent failure with it. Renumber a member, or slip
one into the middle, and the member below can land on a value some other member
already holds: two names for one number, the program still compiles, and one of
the two is now unreachable through a match on the other, with nothing in the
source saying so. So a duplicate that was *written* is kept -- a Count or a Last
pointing at an existing value is a real idiom and is somebody's decision -- and
a duplicate autoincrement walked into is refused, naming both members and the
number they collide on, and saying that writing the value out is how the alias
is declared to be intended.

The rule lives in the parser rather than beside the duplicate-name check in the
checker because it is a question about the source text. Ast.Defenum holds
resolved numbers and no per-member locations, so by the time the checker has an
enum in hand it can no longer tell which of the values were typed, nor point at
the other member. All members are resolved before any of them is checked: the
value collided with is as often below as above, and (defenum E [A B 0]) has to
refuse A.
2026-09-17 18:34:52 +07:00
e7bb643d9d Merge branch 'worktree-agent-aaf84e55296df121c' into dev-loop 2026-09-14 10:52:13 +07:00
f182fb4728 flan dev --x86: the host and its modules, chosen together
Item 3, and the reason the backend was written. Until now --x86 was read only
by flan build's argument list; the daemon built both halves through LLVM, so
none of this reached the dev loop at all.

The choice is a session setting, not a per-command flag, and it is spelled
exactly as [debug] already is -- one field on Session.t, set once in Dev.start,
carried on every change the session emits. session.ml's comment on [debug]
already gives the reason and it is the same one: the modules have to match the
process they are loaded into. Session.redefinition is the single place that
picks a backend, so the six call sites cannot disagree and the refusal has one
home. Session.change carries the answer beside the text, so the builder and the
text can never come from two different decisions.

There is no fallback and there must not be one. X86.redefinition refusing a form
is reported to the editor; quietly building an LLVM module instead is precisely
the crossed pair flan.abi.x86 exists to refuse at dlopen. A refusal reaches the
editor as a diagnostic like any other -- X86.Unsupported is re-raised as a
Loc.Error at the form it is about, because every caller already handles that and
none handled the other, and a session that died on the first unsupported form
would be worse than one that says so and stays up.

flan reload got the same flag at the same time. A command that could build a
module for a host the other backend compiled is how the crossed pair was
reachable from the CLI at all; the aggregate handoff's two-line reproduction no
longer has a second half.

And the finding: flan dev --x86 refuses the merged daemon. A merged build is the
program and the compiler in one process, and the compiler expands macros by
dlopening a module Build.macro_module made through Emit.program, cached on disk
by the macro source rather than by the backend. The merged host is linked
-rdynamic so a redefinition module can reach its cells, which also exports every
flan.* body it has -- so the macro module's own copy of a prelude function is
interposed by the host's. With an LLVM host nobody notices. With an --x86 host
the caller is LLVM and the body it lands in is this backend's, and the process
dies inside flan.[clamp] during the first macro expansion, before the program
has started. flan.abi.x86 does not catch it and was never meant to: a macro
module deliberately neither defines nor requires a marker. The honest fix is
hidden visibility on a macro module's Flan bodies, which changes the cached
object for both backends and wants a lane of its own. Until then the refusal
names the mechanism and the remedy, and --two-process has no such meeting.

start_merged keeps its --x86 plumbing, unreachable for now, because it is the
half that is right and will be wanted the day the macro module is fixed.

test_dev.ml drives an --x86 daemon through C-c C-c, C-x C-e, a literal, a new
defvar with a value of its own and a new defn, and asserts (twice fresh) is 82 --
which only holds if both registry lookups resolved. The merged refusal is
asserted there too. bin/main.ml learned to print a bare Failure as a sentence
rather than an uncaught exception and its backtrace.
2026-09-14 10:34:49 +07:00
aa82364066 Two aliases for the checks nobody ran, and one word that names them all
@page runs web/examples/check.sh and web/examples/quotes.sh against the compiler
dune just built. @cells runs spike/x86/cells.sh, which was a real pass/fail check
-- four builds, two backends, 22 22 against 42 42 -- that nothing in the tree ran.
@checks is @page, @x86 and @cells together, and its comment argues for where the
boundary sits: everything you can run while making coffee is in, @sanitize and
@valgrind are out because folding tens of minutes in would make the umbrella the
thing nobody has time for, which is the disease rather than the cure.

All three scripts learned to resolve FLAN to an absolute path, which is what
actually stood between them and a dune rule: %{workspace_root} expands relative to
the directory the rule is written in, and every one of these scripts cd's somewhere
before using it. The first run of @page failed with twenty diffs all saying
'../bin/main.exe: No such file or directory', which is at least a failure that says
what is wrong.

docs/BUILT.md carried the same colon-spelled renderer block index.html did, from the
same sweep. Nothing checks BUILT.md, so it is corrected here by hand.
2026-09-14 10:26:48 +07:00
5a67cf458a A name the host was never built with, compiled by the x86 backend
The first of the three things HANDOFF-x86-redef.md left: a function or a
defvar the running process has no symbol for. ELF cannot grow one, so the
address is asked for by string at install time -- flan_dev_cell for a cell,
flan_dev_global for a global's storage -- and parked in a slot this module
defines.

The reference side is one new [loc] case and nothing else. [Lslot] loads the
slot and answers [Reg (scratch, d)], which is exactly what [Lgot] already did
with [Got] where this has [Sym]; every site that reaches a cell already
double-loads, so no call site, no place expression and no [sym_loc] caller had
to learn a third case. [fnctx.slot] is a second predicate rather than a widened
[ext] because they answer different questions -- [ext] says "the host's, reach
it through the GOT", [slot] says "nobody's yet, reach it through a slot I
filled". It defaults to [fun _ -> None], so the whole-program path emits
byte-identical output and the survey goes on being a structural check.

flan_reload_install is now a function with a frame rather than a run of loads
and stores, because it makes calls and a call on an unaligned stack faults
inside glibc's movaps rather than anywhere a reader would look. Its shape is
emit_globals_init's, down to owning the null transfer cell no caller hands it.

A new global's declared value travels with it: flan_dev_global copies the image
onto the allocation the first time the name is interned and ignores it after,
which is where "a reload must not reset the state" lives. emit.ml folds that
value into an LLVM constant and this file has no folder, so the image is a
module-local buffer written by the initialiser lowered as ordinary code -- the
same bargain emit_globals_data already documents.

Republishing a defconst came free once the rest was there: one store of the new
constant into the host's global, which is what emit.ml does.

reload-v6.flan is new. v3's [extra] is declared zero, which calloc also gives,
so a run-time-new global whose initial value never arrived would still pass;
v6's [tuning] is 42 and the host prints 88.

test_reload.ml's x86 section now runs all four modules against the same
transcript the LLVM path is held to, and the refusal it used to assert is gone.
2026-09-14 10:16:42 +07:00
99c5d87125 Both raylib workarounds, and where the second one went instead 2026-09-14 08:37:30 +07:00
37d94ed66f The pointer arm agrees promised, and the comment stops promising it 2026-09-14 08:37:30 +07:00
ff2bd1da12 A socket file must not outlive the process that bound it
The merged daemon now unlinks its socket on the ways out it does not
control as well as the one it does: an atexit for exit(3), which is what a
runtime trap takes, and by hand in die_now and in main's fallback, which
are _exit and skip the chain on purpose.

The client says so too. A refusal on a path that exists is a leftover, not
a daemon declining, and the raw 'Connection refused' has now misdirected
two investigations.

Separately, and it is separate: dev-repl.flan gets dev-robust.flan's
24000-tick budget. Twenty seconds of program under a two-minute test is a
second flake waiting its turn, and it is not the one fixed above -- that
one fails honestly, saying the program exited.
2026-09-14 08:11:35 +07:00
4074eb7b7f A client that leaves before its reply arrives, which used to kill the daemon
Eight hit-and-run connections against the robustness daemon, then a check
that the session is still there and still knows what it installed. Fails
with 'killed by SIGPIPE' against the parent of this commit.
2026-09-14 08:11:35 +07:00
fbafee4957 An editor that leaves mid-reply is a closed connection, not a dead session
SIGPIPE's default action was killing the daemon outright. In the merged
build that is the program, the compiler and the listener at once, and the
socket file is left behind - so the next client got ECONNREFUSED on a path
that exists, which reads like anything but what it is.

Two halves: ignore the signal where the listening socket is bound, and
guard the reply write, because an [exception] case on a [match] covers the
scrutinee and not the branch body, so the EPIPE went past both handlers
below it and out of the accept loop.

test_emacs.ml keeps the daemon's wait status and the program's output on a
failure now, instead of removing the evidence before reading the exit code.
2026-09-14 08:11:35 +07:00
5b54e1b7da Merge the tidy: the crossed-pair case is the marker's, and the citations follow the move 2026-09-14 07:30:14 +07:00
fa83056708 A backend mismatch is refused by the loader, and says so 2026-09-14 07:15:13 +07:00
9d5689ffa2 Every citation of a moved document now resolves from where it is written 2026-09-14 07:12:27 +07:00
682cb745ac Merge branch 'worktree-agent-a4f73ab1c6726fdc2' into dev-loop 2026-09-14 06:55:39 +07:00
4c46a56827 Merge branch 'worktree-agent-a123a91f32f4c9779' into dev-loop 2026-09-14 06:55:39 +07:00
ddaebf2516 Merge branch 'worktree-agent-affdbb9fef702b34d' into dev-loop 2026-09-14 06:55:39 +07:00
2b68262249 Merge branch 'worktree-agent-ab7f6288f142b05df' into dev-loop 2026-09-14 06:55:39 +07:00
70e95119e9 Merge branch 'worktree-agent-a03eb07b90913c874' into dev-loop 2026-09-14 06:55:39 +07:00
9486bc0ac0 The reprint header names a signature, not a count, so it is true on every path 2026-09-13 23:44:42 +07:00
ce72f03a0d The offset the walk never reaches, and a stat that was running sixty times a second 2026-09-13 23:36:03 +07:00
78368811c0 The x86 backend's DWARF is checked by a debugger, not by a transcript
test_acceptance.ml's lldb case is the only part of the suite that says a
person can debug a Flan program: a breakpoint on a Flan name, a backtrace
with .flan files and lines, and locals with their own types. It ran against
the LLVM backend only.

An --x86 arm, with a narrower claim: the breakpoint and the backtrace, and
that the program still prints what it printed. No frame variable, because
x86.ml emits no DW_TAG_variable -- a slot there is a bump-allocated frame
temporary whose lifetime the backend does not model. That gap between the
two backends is now recorded in the place it will be read.

Worth pinning rather than leaving to a handoff, because everything it
exercises is bytes x86.ml wrote by hand -- a line program, a compile unit,
an abbreviation table -- and a wrong byte in any of them is silent.

debug_compile grows an ~x86 flag beside the ~dev one it already had.
2026-09-13 23:32:35 +07:00
4c184e68ce A poem of forty-seven kana, and the one raylib call a Flan string cannot make 2026-09-13 23:28:40 +07:00
3c4ca20fea The robustness fixture says its failure is deliberate, and stops printing a linker's words 2026-09-13 23:24:21 +07:00
fdf7531c37 Nine filters that rewrite the buffer they are handed, and the half of them a headless run can pin 2026-09-13 23:22:37 +07:00
c9394a6bcd The mismatch is reachable from flan reload, and the guard does not close it 2026-09-13 23:18:25 +07:00
ed32915501 The two redefinition builders refuse to be crossed 2026-09-13 23:15:40 +07:00
8ae77db92c A redefined function that takes and returns a struct, on both backends 2026-09-13 23:12:43 +07:00
9ee398e817 The two cases that would have passed while being wrong
An i32 min / -1 and an f32 cast out of range. The first is where the two
backends disagreed silently rather than both dying -- x86 divided in 64 bits
and truncated on the store, answering -2147483648, where LLVM emitted poison --
and it is the only case that exercises the widening on the way into the
condition, so a bug there would have left every other row passing. The second
is the one place the two backends reach the same answer by deliberately
different routes, f32 bounds here and widened doubles there, and the survey is
what says the routes agree.
2026-09-13 23:09:08 +07:00
a431cddd3b A divide by zero names the file and the line, and is answerable
Three arithmetic situations had no defined behaviour and the two backends
disagreed about all three: a divide or remainder by zero, which was a raw
SIGFPE with no message and no location; (/ min -1), whose quotient is one past
the top of the type; and a float to integer cast whose value does not fit,
which LLVM called undefined and would fold to anything.

They now signal ArithError with `error`, exactly as a bad index signals
BoundsError, and die with a sentence naming the file, the line and the operands
only if nothing answered. The guards ride the same --checks flag as the bounds
check and are elided with it.

No restart is established at the failing operation. The sketch this started
from asked for use-value, and the implementation ruled it out: a restart frame
is allocated by the restart-case that offers it, on its own stack, so the
runtime cannot hold one on a program's behalf and use-value here would mean an
alloca and a restart frame at every division in every checked build. That is
the cost already refused for indexing, buying a silently different answer.

The x86 backend is unchanged and is the next commit.
2026-09-13 22:55:26 +07:00
54da06d111 An @x86 alias, and the two scripts that ask what the backend costs 2026-09-13 22:49:38 +07:00
bd69f684ed An --x86 host reloads an --x86 module, checked by running it 2026-09-13 22:40:52 +07:00
9534d5c077 The printed form round-trips, and the macro is named before the walk replaces it
Two things the first pass left on unspecified ground.

Form.to_source is the only field in the protocol carrying arbitrary literal
data -- a macro may build any literal at all -- and Wire.quote escapes only
the quote and the backslash, on the stated ground that both readers take
everything else as itself. test_repl checks that ground now: every escape the
reader knows, both byte-literal spellings to_string would have written raw,
and a whole float, each sent through the printer and the socket and back.

And expand_all read the macro's name out of a tuple beside the walk that
replaces it. OCaml does not promise which half runs first; the failure would
have been the wrong macro named, never an error.
2026-09-13 21:09:50 +07:00
3dd9f61b7d A macro call says what it expands to, and a Form learns to print itself
C-c C-m. One step on the bare key, the fixpoint under C-u: a macro may
quasiquote a call to another macro, and Loc.from_macro is outermost-wins, so
by the time a full expansion settles the intermediate name is gone. One step
is the only thing that can say which macro produced what.

The expansion runs against the macros the *session* holds -- the prelude's,
its imports', and every defmacro evaluated since it started -- and writes
nothing back: a defmacro handed to C-c C-m does not join the session by having
been looked at.

Both non-termination refusals stay refusals, and only where they are needed.
One step makes one call and does not look at the answer, so (s/spin) one-
stepped answers with itself; all the way hits the fuel and names the macro,
inside Dev.serve's guard, so the daemon replies rather than hanging. Macro's
module handling is a Fun.protect now -- a build that raised was a process
about to exit, and the daemon is not that process.

No printer for a Form existed. Form.to_string is an error-message renderer and
is what Macro.key digests, so it is untouched; Form.to_source round-trips
floats, strings and bytes through the reader, and Form.pretty decides where
the line breaks go and leaves the columns to flan-mode.

The answer is a read-only flan-mode buffer shaped like the disassembly one,
with cnr's idea in it: m expands the form at point one more step in place.
Three inherited keys refuse by name -- an expansion is in no file. The text is
sent padded onto its own line and its own column, unlike C-x C-e, so the
refusal lands on the call and not at the start of its line.
2026-09-13 21:06:58 +07:00
9fc04193d9 The refused shape is defmacro at C-x C-e, and the union's boundary is defn's
The head dispatch already names it; test_repl pins it beside the defvar case,
and the editor-typed defmacro moved below the describe assertion that says an
evaluation changes nothing -- that one does, on purpose. BUILT.md gains the
boundary of the shape: the set only grows, a deleted defmacro still expands,
and that is exactly what a deleted defn already does.
2026-09-13 20:37:32 +07:00
b9f5b5c44c A promise the compiler cannot check gets its own refusal, and a session expands its buffer's macros
Two loose ends from NEXT.md.

slice-from-ptr's run-time refusal borrowed @flan_slice_error and reported a
range and a length the caller never wrote. It has flan_slice_promise_error
now: signals BoundsError, walks the handlers, offers the break loop, falls
through to a message and a status like the two beside it. The sentence names
what was promised and what was passed, and a second line says what is not
checked. The condition fields stay (0, n, 0) — the violated condition as a
range, and not (0, n, n), which reads as in bounds.

And a session now holds the buffer's own defmacros: seeded in Session.create
from the same read that produced decls, and added by Session.eval so a
defmacro typed at the editor joins the set the way a defn does. Not a re-read
of the file, which would put unsaved-versus-saved skew inside expansion. The
commit stays below the checker. Macro.program dedupes the ambient set against
the forms being parsed, left-wins, because unqualified names can now collide.
2026-09-13 20:33:47 +07:00
fef6ae04f6 A failed build is a refusal, and a refusal leaves the session standing
The daemon caught Loc.Error at each op and nothing else. That was survivable
while the frontend was the only thing that could refuse a form; it is not now
that expansion is part of evaluating. Both C-c C-c and C-x C-e run a clang
driver through Build.macro_module, which answers with an exit status and a
Failure, and a dlopen that finds no symbol answers with another one. Neither
is a Loc.Error, so neither was answered, and an exception past serve is not a
refused evaluation — it is a dead daemon with the program still on screen and
a closed socket waiting for the editor's next request.

The boundary is now one place, around the whole of a request, rather than a
new arm at each of the dozens of calls. Out_of_memory, Stack_overflow and
Sys.Break go through it: those say the process cannot continue, and answering
"error" to them would claim a session survived something it did not.
Everything else is about the form that was sent, and the message it carries
is the one the user can act on, so a clang exit status reaches :message
instead of being flattened to "internal error".

The session's own state goes with it. Session.eval wrote the imported macro
set above the checker, so a form that did not check left the session holding
a package's macros and none of its declarations; it is held and committed at
the bottom with decls, program and env. Session.eval_expr committed the
generic copies it had instantiated before emitting the module that carries
them, which is the session believing it holds a body nothing was written for;
that assignment moved below Emit.

Both are pinned. test_session drives the two rollbacks in process, and
test_dev drives a real daemon whose macro module cannot be built — the
expression path and the redefinition path, each followed by the same
evaluation succeeding and by the session still knowing the program.
2026-09-13 19:43:18 +07:00
91ec235395 Merge branch 'worktree-agent-a788866b56165817f' into dev-loop 2026-09-13 18:01:35 +07:00
25f56c24a8 A begin/end pair that cannot come apart, and what it still cannot promise
vendor/raylib/modes.flan: five macros over the five pairs the package binds
-- with-drawing, with-mode-2d, with-mode-3d, with-texture-mode,
with-scissor-mode. A second file with no declare-c in it, split out on
vector.flan's reasoning: raylib.flan is the package's statement about C and
nothing here names C, so nothing here can be made wrong by raylib changing.

Each expands to (do (begin-... args) body... (end-...)) -- the calls the
author used to type, in the order they typed them. No let, no gensym: nothing
binds a name, so there is nothing for a caller's name to collide with.

What it removes is the End* that is missing, wrong, or no longer beside its
Begin*. What it cannot remove is a body leaving through the unwind path: a
return or an invoke-restart skips the rest of the do and the End* with it.
defer is the obvious fix and is refused inside a loop body, which is where a
pair always lives -- checked, not assumed. So sand.flan's discipline stays:
keep the restart boundary outside the pair.

35 call sites converted across examples/ and sand.flan. The one left is
core-scissor-test.flan, whose Begin and End sit in two separate `when`s with
the drawing between them -- a conditional pair is a shape a bracketing macro
cannot express.

test/programs/rl-with.flan covers with-scissor-mode, which no example can,
with a frame function unreachable from main so it needs no libraylib;
rl-with-reject.flan is the arity half.
2026-09-13 18:00:35 +07:00
b50f42db15 Merge branch 'worktree-agent-a102fd968fb5e7922' into dev-loop 2026-09-13 17:59:04 +07:00
8ca63a7717 The pause wrap takes the expanded expression's location
C-u C-x C-e was never tried on a macro call. Ast.pause_call takes the
expanded loc, which Loc.from_macro has stamped -- it sets a name and
leaves file, line and column the call site's, so the frame the break
loop reports is the line the reader is looking at. Asserted rather than
argued.

Also: the ring rule stated generally (refused at the parse of whichever
file first has both members in scope, always before a session exists),
and the declaration refusal's sentence made build-neutral, since the arm
fires in an ordinary file parse too.
2026-09-13 17:58:36 +07:00
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
6bc4726ddd C-x C-e expands, and a declaration is not an expression
Parse.expr never ran the expander, so a macro call typed as a bare
expression was an unknown name -- a package's and the prelude's alike,
which is what said the gap was older than importable macros. It is the
wrap Parse.decl already had, applied to the other entry point, with
Parse.with_imported in front of it in Session.eval_expr because the one
expression an editor sends carries no import.

The decision that was waiting: an expression that expands to a
declaration is refused by name, in the head dispatch rather than in a
walk over what the expander answered, so a nested one and a hand-typed
one get the same sentence. A quasiquoted declaration is still a value.

The spin refusal fires on this path; the ring cannot reach it, because a
ring is refused while its own package is parsed. Expansion happens
before the thunk is built, so the 5s three-way wait is untouched.
2026-09-13 17:54:36 +07:00
fbc9d3d314 Merge branch 'worktree-agent-a638cd779b2075b7d' into dev-loop
# Conflicts:
#	NEXT.md
2026-09-13 17:46:24 +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