434 Commits

Author SHA1 Message Date
88584f76bc The records say what the tree does, and an arena grown in place answers to its budget 2026-09-25 07:39:11 +07:00
cbd910c117 An arena's budget holds when a block grows in place, and a shrink is never over budget 2026-09-25 07:36:52 +07:00
37ccca56a3 Every sweep reads the corpus through one alias, and a build that raises is a FAIL line in each of them
The sanitizer sweep also lists the five macro programs that run.
2026-09-25 07:23:12 +07:00
85b9f35a66 (watch "name" v) renders any value into the watch table the way print renders it 2026-09-25 07:22:59 +07:00
2250548b07 A test that sees a process die on a signal names the signal, not OCaml's number for it
OCaml numbers the signals negatively and in its own order: SIGTERM is -11. The globals daemon's "transient signal 11" was a SIGTERM, not a segfault.
2026-09-25 07:21:44 +07:00
f142cfaadf A u64 constant above 2^63 can be written in decimal, and a cast's integer literal too wide for i32 is read at the cast's type 2026-09-25 07:16:42 +07:00
aa629da2e5 vec-new and map-new take a type expression where they take a type, so (vec-new [u8]) makes a Vec of byte slices 2026-09-25 07:10:48 +07:00
b4e19bebf8 A loop's initialisers are checked in order and each sees the names bound before it, as a let's do 2026-09-25 07:06:17 +07:00
9194f918fc An aggregate that reads its own destination sees the old value, on x86 too
x86 built a settling right-hand side straight into its destination, so (set p (P {.a (.b p) .b (.a p)})) wrote .a and read it back for .b. assign now also asks whether the value reads storage the destination lies in, and goes through the temporary when it does. The global-initialiser entry was already closed by startup_plan's Set; the unrooted-temporary entry turns out to be a both-backends rooting gap and stays open for a decision.
2026-09-25 07:05:18 +07:00
3a3674efb7 A gensym is never the same name twice in one compiler process, however many macro modules it loads 2026-09-25 07:04:10 +07:00
6b9d1fa644 A cast of NaN or an infinity to an integer says so by name, on both backends 2026-09-25 06:58:06 +07:00
45cc4d20e8 One file holds what five did
# Conflicts:
#	FIX.org
2026-09-21 21:07:40 +07:00
57fe91f303 Five records become one, and every citation lands somewhere
FIX.org, NEXT.md, DISCUSS.org, docs/DISCUSS.md and the session handoff at the
root are one TODO.org now: 293 entries under seven subsystem headings, each
carrying an org keyword that says where it stands. A DONE entry is a few lines
saying what was decided and what that rules out; the reasoning that would not
compress — the embedding spike and the four reports the hand-written x86
backend was built from — moved into docs/BUILT.md instead, and its entries
point there in one line.

Every entry was checked against the tree before it got a keyword, and the
prose was wrong in both directions. Things the deleted files called open were
built: the first-evaluation stall, main being redefinable, macro parameter
lists, the type-limit constants, the array constructors, the byte fills,
inc/dec, the discard's fontification, the Emacs buffers, rt_die's _exit, the
backtrace surface, and the acceptance failure that could print and still exit
zero. Things they called done were not: the backend reports' no-plan buckets
had gone stale in the other direction, the value-dependent defvar was
superseded rather than built, and macro-expansion source locations are on an
unmerged lane, so that entry is NEXT and names the branch.

Every comment that cited one of the five by name now cites a heading that
exists, in TODO.org or in docs/BUILT.md. The session reports under
docs/handoffs/ keep naming the files they worked on, because rewriting them
would falsify what those sessions did; each carries a note saying where the
content went.
2026-09-21 21:05:48 +07:00
Joseph Ferano
a14a872a44 A form a macro splices through is reported on the line it was written on 2026-09-21 19:16:45 +07:00
0c711942f7 A thunk named for the order it was minted in is a different signature after a reorder
Three from a review of the commit before this one, and the first is a dev
loop defect.  Naming the widening thunk thick/<count so far> put traversal
order into a symbol, and Session.compatible compares a reload's functions
against the running program's by name -- so swapping two calls in a body
renamed nothing the programmer can see and reported "thick/0 changes
signature, from [i32] i32 to [i64] i64.  Restart to change it."  The name is
the signature again, written by thick_enc: every type self-delimiting, an
atom as its length and then its spelling, so the boundaries mangle_ty loses
are in the string rather than inferred from the separators.  mangle_ty is
untouched -- it is load-bearing for the instantiation names a backtrace, a
Reach edge and a dev cell show -- and the memo stays keyed on the types.
fn-thunk-reload.flan is the pair of widenings and test_session reorders them.

The second was a silent miscompile.  bind_ty's Fn-pattern-against-CFn-argument
arm was structural, so it matched nested function positions too, and the
catch-up pass that builds the thunk compares the whole substituted parameter
list -- false when the mismatch is inside one, and the fallthrough handed the
argument over unchanged: one word where the instance declares two, 255 on
both backends with nothing said anywhere.  The widening is a value the caller
builds around the whole argument and there is nowhere inside one to build it,
so the arm is admitted at the top of an argument's type and nowhere else, and
the catch-up arm is total over the pair rather than guarded -- bind_ty's
fallthrough is Types.fits, which admits Never, so a binding can still succeed
over a pair the two words cannot bridge.  fn-generic-nested.flan is the
parameter position and fn-generic-nested-return.flan the return one.

And the third closes the hole the escaping inversion was for.  A match arm
binds the case's fields to slots and the store that fills them is inside the
branch, not in any form escape_check reads as a binding, so (match o (Some f)
f ...) handed back through a name what a case-field read cannot hand back at
all -- sound only because Some_ denies suspects.  An arm's slots of function
type are suspect now, for the same reason the read itself is.
2026-09-21 15:44:28 +07:00
a28aeda633 The thunk memo was keyed on a name that two signatures can share
Review found it, and it is a silent miscompile on both backends rather
than a refusal anywhere.  mangle_ty flattens a whole signature into one
hyphen-joined string, so (CFn [(Ptr i32)] i32) and (CFn [ptr i32] i32) --
the second over a struct someone called ptr -- flatten alike; keyed on
that, the second widening reused the first's thunk at the wrong arity.
The key is the types now, compared with Types.equal, and the symbol is a
counter over the thunks already minted, so nothing is derived from a
spelling.  fn-thunk-share.flan is the pair, and it prints 5 and 17.

And a regression beside it: a generic whose function parameter binds the
type variable.  (defn apply2 [f (Fn [$t] $t) x $t] ...) called as
(apply2 bump 1) compiled before this lane and stopped, because bind_ty
had no arm admitting a CFn argument at an Fn pattern -- and once it had
one, the call still handed one word to an instance declaring two, because
a parameter that still mentions a variable is checked with no expectation
and expect never sees the pair.  Both halves: the arm, and the widening
in generic_call's catch-up pass beside the numeric one.  The same gap hid
new functionality -- a CFn argument at a (CFn [$t] $t) parameter had no
arm either -- and mentions had no CFn case, so bound_exactly answered
wrong for a variable living only inside one.

The corpus missed all of it because the prelude binds $t from an earlier
argument, so the parameter is concrete before bind_ty sees it.

While here: escaping's enumeration is the *clean* set now rather than the
suspect set.  It had a hole where a list like that cannot -- an (at s 0)
over a slice of Fn read as clean while the Vec, struct and pointer
spellings were refused.  Unreachable today, and the header claims the
list is closed.  The same inversion fixes which refusal message an index
read gets.

Three minors: Anull was defined and never constructed; the capture-dyn
message substituted a descriptor into a noun slot; and session.ml
rendered a defn's changed signature as (Fn [...] ...), which is now a
real type and not the same as (CFn [...] ...) -- it writes the parameters
and the return the way a defn writes them.

And one rounding corrected in the docs: handler-bind is not free for a
program that captures nothing.  %handler grew from 24 bytes to 32, every
push writes a null into the new field, every clause gains ptr %env with
an alloca and a store, and flan_signal passes one more argument per
dispatch -- twenty changed x86 lines on loops.flan.  Small, real, and
paid by every conditions program.
2026-09-21 13:41:44 +07:00
2572f0a537 An fn sees the locals it was written among, and Fn says so in its type
spec-memory.md's case 2, capture by value into a stack environment, and
the calling convention the author's rulings asked for.

    (Fn  [i32] i32)   captures; {code, env}; the common case
    (CFn [i32] i32)   the bare address; one word; cannot capture

A local of the enclosing function that an fn names is copied into a
struct the checker synthesises, held in a slot of that function's frame,
and the value carries its address; the lifted body reads the copies back
into named slots of its own, once, at entry.  So the name in the body
means what the local held at the instant the value was made --
fn-capture.flan changes the local through a pointer after the value
exists and the fn still answers with the old one.

Two types rather than a uniform environment parameter: "while it's dyn
first, static side should never have to pay the price for the existence
of the dyn side... if you fully opt out, for instance, using --no-gc
flag, then we should be operating under Odin/C semantics and never paying
any runtime costs."  The environment is declared by exactly the bodies an
(Fn ...) value can reach -- a lifted literal in an Fn position, every
handler clause, and the widening thunks -- and by nothing else.  An
ordinary defn emits the signature it always did; calc-me and fourteen
corpus programs were diffed to say so.

CFn, because the C carries information: a value with no environment is
the only kind that could ever cross to C, and under the --no-conditions
direction FIX.org records it becomes literally a C function pointer.  It
is not that today -- a declare cannot take a function type at all -- and
crossable's refusal says so where a reader would otherwise be misled.
Nobody needs CFn: Fn accepts everything, and the commonest reason to
reach for the narrow one is that a *named* function handed to an Fn pays
a hop through the widening thunk where a CFn is a direct call.

That thunk is one small function per distinct signature widened, which
reads the bare address back out of the environment and calls it.  The
cheaper trick -- the environment last, ignored by a body that never
declared it -- is legal under SysV and is a trap under wasm32's
call_indirect, which compares the signature at the call.  Every indirect
call is exactly typed now.

A handler clause captures the same way and is sound with nothing left
over: its frame is popped by the body that pushed it.  What is refused
there is a *store* into a captured name -- it is a copy, and writing to
it would leave the local as it was.

And the other half, which is what "non-escaping" means: a value carrying
an environment may be called, passed down and let-bound, and may not be
returned, stored, pointed at or pushed into a container.  A parameter of
type Fn is treated as one, which answers "passed to something that stores
it" with no interprocedural analysis -- the store is refused inside the
callee.  Everything of type CFn is clean for free, which is the second
thing having two types buys.  Every refusal names case 3, the environment
the collector owns.

Two pre-existing bugs fell out on the way.  A lifted fn asked for Fnval,
so `flan reload' on any function containing an fn literal died at llc
with an undefined cell; it takes Flanfn now, which is the choice a
handler clause always made.  And a redefinition module now carries its
own hidden copy of every thunk it names, which is the same bug shape
caught before it shipped.
2026-09-21 13:41:44 +07:00
290d322b44 An assignment is whole or it never happened, on x86 too
Two bugs, both in lib/x86.ml, both reached by signalling a condition from
inside the value being assigned.

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

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

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

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

The def-edit paragraph in docs/BUILT.md described this as open and is now the
claim it was waiting for. FIX.org records the two lanes the review of this one
turned up and this one does not take: an aggregate built in place can read its
own destination, which is an aliasing question rather than a transfer one; and
the temporary is a frame buffer no root table names, which matters the day a
struct may hold a dyn field.
2026-09-21 12:37:08 +07:00
d6fc15474b The count is length, so len is a name a program can have
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.

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

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

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

sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
2026-09-21 11:58:56 +07:00
3672da28be A diagnostic is for someone who has only this compiler, and says what to write 2026-09-21 11:44:59 +07:00
b98108791d Merge branch 'worktree-agent-a1a1e7420cccc5fa6' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 11:05:28 +07:00
6df50e9ce1 The sand grid hashes the new generator
The draw is 64 bits now, so every grain lands somewhere else and the old
number could not be right again. Re-taken from a run, which is the one
manual step this case has always had.

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

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

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

At two operands both readings are one pair and neither goes through the n-ary
lowering, so every comparison there is emits what it always did. (< x) joins
(+) and (- x) as a refusal — it would be true whatever it was handed.
2026-09-21 11:03:39 +07:00
a0278378ab Five randomness functions, and a draw wide enough to answer them
rand-int, rand, rand-bool, rand-int-range and rand-float-range, at the widths
the author ruled: a u64 draw and an f64 in [0, 1). rand-seed and rand-state
keep their names. The four old names are not names, and each is refused by the
one that is, with a call that compiles — in both the call and the bare-name
position, because a Lisp-1 makes the second a real thing to write.

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

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

sand.flan is not touched. It calls rand-f32, so the cases that compile or
re-evaluate it skip themselves on the fixture rather than on a comment: fix
its two calls and every one of them runs again. Its hash is the old
generator's grid and gets re-taken then.
2026-09-21 10:57:18 +07:00
bb274432a7 Merge branch 'worktree-agent-adff9fa1b537fecf8' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 10:29:07 +07:00
9573884d97 Merge branch 'worktree-agent-a9a425fd59a33124b' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 10:05:42 +07:00
5ad16d6815 A conversion asks the bound, not a type the variable does not have
(i32 (at xs i)) inside a body bounded integer? was refused with "i32
converts a number, found t". Arithmetic, comparison, min/max, the
bitwise fold and the shifts all ask the where clause; the conversions
were the family nobody had gone back to, and the cast block held three
arms of it.

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

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

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

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

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

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

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

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

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

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

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

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

The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.
2026-09-21 09:51:35 +07:00
10b1abbf09 The last enum member takes its prefix, and the alias goes
TraceLogLevel carried `warning` beside `log-warning` so that sand.flan's
one call kept working while the rename landed. The review found it was not
inert: render.ml folds members last-wins, so a 4 read back as :warning in
the break loop, the did-you-mean offered it, and every TraceLogLevel error
listed it. A spelling nobody should write was the one the compiler taught.

So the call is respelled and the alias is gone. Every member of every mapped
enum carries its enum's prefix now, with no exception to explain.
2026-09-21 09:32:37 +07:00
2d8d9cd5a8 dotimes counts from where you say, and can count down
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set. Now it is (dotimes [i 9 -1 -1]).

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

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

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

Still a special form, still a Let and a While with the step in the latch, so
neither backend learned anything — the new program prints the same thing
under --x86 and at -O0. load.ml's Form-level walk had to learn more than one
bound for the same reason parse.ml did; it is part of this feature and not a
bug that was sitting there, because before this a three-bound dotimes was a
parse error long before that walk could reach it.
2026-09-21 08:09:53 +07:00
a4c6b996ff def re-runs its initialiser, and defvar is renamed defonce
The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.

One parse arm reads both forms; the difference is Ast.reinit, carried to
Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and
Check.check_global lifts every def initialiser — zero and literal
included — into global/<n>, so the host's startup reaches it through the
function cell and a re-evaluated def swaps it (Session's def_inits;
Emit.redefinition declares the cell for a non-sibling target). The old
defvar spelling is refused with the rename and both compiling spellings,
and every program, test, doc and editor list is swept — except sand.flan,
the author's live WIP, whose seven defvar lines are flagged in FIX.org
and keep its three dependent tests red on this branch.
2026-09-21 07:12:04 +07:00
450728c9f2 Merge branch 'worktree-agent-ab376a9e12b4af136' into dev-loop
# Conflicts:
#	FIX.org
2026-09-21 07:05:41 +07:00
700e762b27 slice takes one, two or three arguments, and reaches a string
A fixed array does not decay to a slice at a call, so passing one to a
function over [$t] meant writing (slice a 0 (len a)) at every call site.
(slice a) is the whole of it now and (slice a n) is the tail from n, filled
in by check.ml into the three-argument form: same node, same static bound
checks, same runtime trap, and on a fixed array the implicit length is the
constant (len a) already folds to. Neither backend grew an arity case. A
target that is not already a name goes through a slot first, so (slice (f x))
calls f once.

at and slice also reach a string, because (bytes s) was the only route to a
byte and it is about to start copying. (at s i) is the byte, bounds-checked;
(slice s ...) at all three arities answers a string viewing the same bytes,
not a [u8], which would be a writable-looking view of storage the program
does not own.

Neither is a place, and the refusal lives in [indexed] rather than in
check_place, which is the part that matters. There are three routes to a
Pindex and they share no code: check_place, the single-index set arm that
checks its own target, and addr. Asked in check_place, the question is
answered for two of them and missed for the one a person writes — a store
into a string literal compiled, and the backends disagreed about it. So
[indexed] takes a ~place location and asks at every dimension, because
(at g 0 0) over a [[2 string]] reaches the string only at the last step.
One message, and addr gets it too, so it reads as value-versus-place rather
than as a rule about assignment.

Slicing an array a call returned is refused at every arity. The view
outlives the temporary, both backends print whatever the frame reused, and
nothing traps — which was already true of (slice (mk) 0 3) and only
survivable while nobody wrote it. (slice (mk)) is short enough to become a
habit. An array literal is not this case and stays legal.

Two backend cases. emit.ml's element_addr grew the String arm beside the
Slice one. x86.ml's index_len had answered None for a string — correct while
nothing could index one, and a skipped bounds check the moment something
could — and now reads the length word, so both check the same thing.
2026-09-20 23:36:40 +07:00
d1443808c6 Review follow-ups: the park had the original bug inside it
- SA_NODEFER. sigaction without it blocks the handler's own signal for the
  whole handler, and here the handler is the park — it never returns. A
  hardware SIGSEGV delivered while SIGSEGV is blocked is not handled: the
  kernel forces the default action. Fault, park, eval something at the
  break loop that faults, daemon gone, exactly the author's session one
  level in. Measured both ways; flan_crash_entered is cleared before the
  hook so each break-loop fault still gets its line, and the case is pinned
  (trap_park ~refault:true), confirmed to fail without the flag.
- Scope the handler to the thread it was armed on. A disposition is per
  process and a merged dev session is one process, so this was shadowing
  OCaml's SIGSEGV handler — and Stack_overflow — for the daemon's whole
  life. Other threads chain to what was installed before. Arming per run
  would leave the parked prompt's evaluations unprotected, since those are
  program code too; the comment says so. Also makes the per-thread
  sigaltstack honest.
- Sweep dyn-view.flan and string-eq.flan, which dev-loop added after the
  first sweep. string-eq:46 wanted the aliasing outright: its comment is
  about two slices sharing a base pointer.
- A StorageExhausted row for bytes, asserting the retry copies once and
  whole rather than re-evaluating its argument.
- Gate the flan_dev_crash_enable declare to dev builds, so this lane adds
  no dev-only text to a release module. flan_bytes_dup stays ungated: a
  release build really calls it.
- Guard the section for wasm32, which compiles this file and has no
  signals.
2026-09-20 23:35:03 +07:00
2e203f64b8 bytes copies, bytes-view aliases, and a dev-session segfault parks
The INSERTIONSORT crash, all three rulings (FIX.org 2026-09-20):

- (bytes s) allocates a writable copy through the allocator surface —
  context or (bytes s a), StorageExhausted with retry, a registry note in
  dev builds (flan_bytes_dup, lowered like vec-new). (bytes-view s) is the
  old zero-cost reinterpret, renamed, read-only by convention; every
  in-repo reader swept over to it. (string b) unchanged.
- String constants were already read-only on both backends at -O0; now
  pinned — bytes-copy.flan rows on LLVM/-O0/--x86, and dies_segv rows
  asserting the write-through-view trap on both backends.
- A dev build installs a SIGSEGV/SIGBUS handler by the same dev-only
  constructor slot that arms the registry: one line naming the address and
  the innermost frame, then the trap-hook park — stopped, not dead, the
  daemon serving. No agent: message and re-raise. Release builds untouched.
  Pinned by trap_park over dev-segv.flan.
2026-09-20 23:12:42 +07:00
831cab9fb2 Review follow-ups: x86 parity, ArithError, and three refusal bugs
Five fixes off the independent review, plus the author's u8 ruling.

x86 parity: the bad-index block always ran x86 (it is flan dev's
default) and now says so with an explicit --x86; the condition render
gets an assertion under the x86 backend too, beside the LLVM one, and
a user error is pinned as carrying no site on both.

ArithError's layout is now pinned: {i32 op; i64 lhs, rhs} in C against
the prelude's defstruct, read field by field through the break loop's
render, driven from the editor through a divide under a restart-case.
That also covers condition and site on LLVM.

Three refusals that were wrong: trap_site tested the prefix "err"
and so ate any site whose path began with those letters; source_line
let Sys_error from input_line escape and take the whole break reply
with it, leaking the handle; and a condition with no fields was
reported as a name no struct has. The daemon now sends its own field
count and the buffer tells the two empties apart.

Nits taken: an over-long site is dropped rather than silently
truncated into a plausible one; the caret pads with the source line's
own tabs; the headline says when it has cut the field list;
flan-cnr-layout is live again as the single spelling of that request
rather than dead beside an inlined copy.

And the ruling: a u8 renders as 97 (\a) where a person is inspecting
and stays 97 where the program is printing.
2026-09-20 22:55:11 +07:00
aa989931dc Merge branch 'worktree-agent-a310fd64a7205d17f' into dev-loop
# Conflicts:
#	FIX.org
#	lib/check.ml
2026-09-20 22:17:58 +07:00
2b91fd2146 print and println go variadic, with Clojure's spacing
Arguments print in order with a single space between each pair, println
ending the line; (println) is the newline alone and (print) is nothing.
The checker's arm renders each argument exactly as it did alone, so typed
and dyn values mix in one call, one-argument sites are byte-identical, and
an unprintable argument is still refused at its own span.
2026-09-20 22:09:06 +07:00
5b76237af2 Merge branch 'worktree-agent-aa39c3fbfc562d62a' into dev-loop
# Conflicts:
#	FIX.org
2026-09-20 22:01:15 +07:00
cdda829835 The direction a container-fixed binding admits, pinned
A narrower scalar at a $t a slice already fixed widens into the fixed
type — the same cast a monomorphic parameter applies — where the old
rule refused both directions. One accepts pin, one runtime line in
int-generic.flan, and the web page's predicate table catches up: five
predicates, integer? at the head, and the entailment chain grown one
link.
2026-09-20 21:48:39 +07:00
f71cc40bb5 integer? bounds the integer-only bodies, and abs is one generic
The fifth predicate: integer? admits every integer kind and no float,
entails numeric? (and through it ordered? and equal?), and gates what
only integers support — the bitwise fold asks for it, the shifts admit
a bounded variable under it, and the float literal in an integer? body
is refused in the bound's own words. The literal arm needed nothing:
the entailment admits an integer constant under either bound.

abs-i32 and abs-i64 collapse into one integer?-bounded generic whose
i32/i64 copies even keep the old symbols; abs-f32/abs-f64 stay as the
float spellings because the right float abs is a sign-bit clear no
integer body spells, and (abs 1.5) now refuses naming the bound — the
where clause is checked before the name-collision check, which used to
answer that call with 'abs-f64 is already defined'.

Mixed widths at one $t join at the wider type now, in either argument
order — the author reversed the refuse-both rule on 2026-09-20. A
joinless pair is deferred and re-asked against the final binding, so a
later wider argument settles u32-vs-i32; u64-vs-i64 still refuses, and
a container-bound variable still binds exactly. The out-widened
arguments catch up through the ordinary Cast.

Two review follow-ups folded in: a struct field's unknown-lowercase
message stops suggesting a parameter vector it does not have, and the
tyvar-at-dyn message says defgeneric/defmethod in words instead of a
schematic that does not compile.
2026-09-20 21:42:50 +07:00
86091541d4 Merge branch 'lane-array-fill' into dev-loop
# Conflicts:
#	FIX.org
2026-09-20 21:39:21 +07:00
47b3ceb878 The inline generator was owed the want the form already knew
(array-gen [3 4] (fn [i j] ...)) — the canonical form — was refused:
check_fn saw no (Fn ...) want and no position to take types from. But the
form knows them: one i32 index per dimension is the rank's own promise.
check_array_gen now hands an inline fn its parameter types directly, with
the annotated element type as the return want where the annotation reaches
that deep, and the return left for the body to say where it does not — so
a bare inline fn infers its element type the way a fill value does, and a
body that disagrees with an annotated element is reported at the
generator's answer, per element. Named defn generators check as before.

check_fn grows a ?gen way in for exactly this: parameter types without a
Fn want, return optional. An inferred-return body sees Unit as ctx.ret, a
rough edge left rough on purpose.

Pins: inline at rank 1 and 2, inferred element, annotated defvar, the
per-element mismatch, inline arity. The acceptance program gains the
inline form, a struct-valued fill (the per-element store is a struct
copy), and evaluated-once (a counting fill value called one time for four
elements) — riding the three existing rows, no new ones. And the FIX.org
entry the pass never wrote: dims by the [n T] rule, one index per
dimension, the Zero+While/Set/Pindex lowering with no backend edits,
composition by nesting the forms, and this fix.
2026-09-20 21:34:14 +07:00
c20a4b90dc Merge branch 'lane-m5-generics' into dev-loop
# Conflicts:
#	FIX.org
2026-09-20 21:09:30 +07:00
Joseph Ferano
b64770feb7 A generic crosses a package boundary, and a trial leaves one copy
The two compositions the milestone owed, pinned, and the record of the
whole lane.

A package whose exports are generic: pkgs/gen, imported by
pkg-generic.flan at three shapes. One generic at two element types.
One that calls another in its own package at its own variable, so the
transitive copy is generated from a call site two files away. And a
generic written in the program calling one written in the package at
its own $t, which only resolves once Load has flattened both bodies
into one namespace -- the thing that has to change the day a package
becomes a real compilation unit, because a copy is made from a body
and a body that did not cross cannot be copied. Plus the call-site
half of a bound written in another file, quoted here rather than
pointed at in a file the caller cannot change.

And the composition with the widening trial. A binary operator
re-checks its right operand at its left one's type inside a trial, so
a generic call written there is checked twice and once thrown away.
The discarded pass's instantiation does not go back out: instantiate
rewinds a copy whose *body* refused, which is a different event. It
does not have to, and the reason is this lane's own rule rather than
luck -- a generic call's instantiation is read off its arguments and
never off the ambient want, so both passes ask for the same types and
the second ask is a cache hit. Pinned by counting the copies in the
checked program.

The widening lane's note said that cache already rewinds itself. It
does not. Corrected in the comment and in FIX.org, in place.
2026-09-20 21:00:22 +07:00
Joseph Ferano
d5fed12d48 Three messages about milestone 5, from a milestone that arrived
The refusals generics obsoleted, swept. Every message that sent
somebody to a schedule now says what is actually true of the thing in
front of them.

An unknown lowercase type name used to be reported as unimplemented
generic code over a type variable. Generics are implemented, and
resolve_name consults env.tyvars and env.subst long before anything
reaches that arm -- so a lowercase name arriving there is a typo too
far from any type to guess at, or a type variable nobody introduced.
The sentence names the sigil that would introduce it.

A capitalised name given type arguments is the other half, and it is
still genuinely unbuilt: Types.Named is a bare string with no room for
parameters, and giving it some is a change to Types.t and therefore to
the layout calculator, both backends, Render and DWARF. Both sites
that reported it -- the type resolver and the value-position fork --
now say a generic *type* is not there yet and point at the generic
function that is.

Plus the prelude's side of it. pos?, neg? and zero? are three
questions about a number's sign, one body each, answering at every
numeric type -- the family the whole feature was asked for, and the
one thing the landed generics could not write until a literal was
allowed to stand at a bounded type variable.

Two collapses examined and declined, with the real reason written
where the old one was. abs stays per width because numeric? is the
only bound that admits a written 0 and it admits floats too, and the
integer body is the wrong abs for a float: it hands back a negative
zero. It waits on an integer? predicate, which is language surface.
min and max stay builtins because they are variadic and slot each
operand so it is evaluated once; a binary prelude generic would put
the double evaluation back at the call site. Their generic half was
never missing -- ordered? already admits them in any body that
declares it.
2026-09-20 20:46:35 +07:00
8d49d0dddc builtin/ always reaches the compiler's own name
# Conflicts:
#	FIX.org
2026-09-20 20:27:35 +07:00
Joseph Ferano
879a439951 A written zero stands where a numeric type variable stands
pos? over every numeric type from one definition was the motivating
example for milestone 5 and was the one thing the landed generics could
not write: (> x 0) refused with "expected t, found the integer literal
0", because int_literal had no arm for a want that is a type variable.

It has one now, and the bound is what makes it sound rather than
optimistic. Every type numeric? admits is an integer or a float, and an
untyped integer constant is usable at all of them, so there is no
instantiation of a numeric? variable at which the literal has no
meaning. Under a weaker bound there is -- ordered? admits an enum -- so
numeric? is what is asked for and the refusal names it.

The float literal is refused at a type variable even under numeric?,
and that asymmetry is the concrete arms' own: an integer constant is
usable where a float is wanted and a float literal is never usable
where an integer is wanted, so a body written with 0.5 has no meaning
at the integer half of its own bound. Refusing at the definition is
what the abstract pass is for; the alternative is a surprise at
whichever call site first asks for i32.

The node the abstract pass builds is never emitted. Each copy
re-checks the same form with the variable substituted, and that is
where the literal is built at the concrete width and range-checked --
so (+ x 300) is fine at i32 and a refusal at u8, and u8 is where it is
refused.
2026-09-20 20:21:41 +07:00