1039 Commits

Author SHA1 Message Date
66f925c36f A redefined defclass migrates its live instances on their next touch
# Conflicts:
#	FIX.org
2026-09-20 19:58:42 +07:00
5b53043fd5 A defn outshadows a builtin and says so once 2026-09-20 19:56:42 +07:00
d9dd1435a6 What was run, and the one thing a green build does not mean
The lane's record in FIX.org: dune test green before and after the rebase,
test_dev.exe run directly because a cached run swallows its label, and
@sanitize clean on the committed source, which is where the
two-thousand-instance migration under collection is actually looked at.

The line worth keeping is about the rebase. dune build does not compile
flan_dyn.c — it is a string the compiler carries and hands to clang at
flan run — so a green build is no evidence about that file. A trap1 call
merged clean into a tree where trap1 had grown a leading location pair,
and nothing said so until a program was compiled.

Also corrects this entry's own description of the session pins, which
still described the needles as they were before they were made to
discriminate.
2026-09-20 19:56:24 +07:00
a983a46ea5 The review's follow-ups: a file decides the shadow, and a set answers the membership
The leak review found: an importer's (defn len ...) reached inside an
imported package's (defvar sz i32 (len "abcd")) and made it 999. A global
initialiser is checked with no enclosing function, so the qualified name the
first cut asked about was not there to ask. The file the definition was
written in is what the shadow follows now, which is what FIX.org had already
named as the fix if it ever mattered. It mattered.

builtin_set beside builtin_names: the guard is the first arm of the dispatch
and ran a linear walk of eighty-odd strings at every named call. The list
stays for the did-you-mean, whose order is its order.

Pinned: a shadowed operator warns and lowers to a Call, and a call carrying
another file's name reaches the builtin. The corpus program grew both cases
and the package grew the initialiser that demonstrated the leak.

And the int/float section's sentence about "the arity precedent, where the
builtin wins" now says that the precedent was deleted the same day, since
this lane is what deleted it.
2026-09-20 19:56:17 +07:00
5cfefa586b The rebase's four repairs, and two pins that were not pinning anything
Rebased onto dev-loop. Three conflicts were additive and both sides are
kept: FIX.org's two appended sections, want_map's diagnostics argument
against the class_sync inserted beside it, and test_dev.ml's agent-socket
block against this lane's migration block, whose comment no longer says
"the block above" now that something sits between.

The fourth is the one the auto-merge hid. flan_dyn_class_def's argument
check was written against the pre-diagnostics trap1 and merged clean into
a tree where trap1 takes a location first, so the class name would have
been read as a length. dune build does not compile flan_dyn.c, so the
green build said nothing; caught by compiling a program.

Two pins in test_session.ml asserted "flan_dyn_class_def" against the IR
text, which every module contains because emit.ml declares every runtime
entry point in all of them. Both now assert the call and the packed slot
list. Checked by mutation: with the thunk suppressed the old needles pass
and the new ones fail, along with the daemon's slot count.

Also disclosed: say_render is the second raw reader beside render, and
neither syncs, so a stale instance shows its old slots in the inspector
until something touches it. That is the editor-facing consequence of
keeping the printers printers, and it is now in the runtime comment and in
FIX.org rather than left to be met. And the stale-caller walk says in as
many words that it is a tripwire, unreachable on purpose, not a filter to
be tidied away.
2026-09-20 19:54:53 +07:00
63fb5c629c The other backend runs the class thunk too, and now something says so
The migration transcript runs on x86, because that is what flan dev takes
unasked. What is backend-specific about any of this is one thing — whether
the registration thunk reaches the runtime at all — and the evidence for
LLVM was that the IR contained the call, which is emission and not
execution. x86.ml's own header claimed for some time that it did not emit
flan_reload_call, which is exactly the kind of sentence not to trust twice.

So: the same program under flan dev --llvm, one instance, one slot added,
and the four answers that say the migration happened. Short on purpose —
everything past the thunk is flan_dyn.c's, and flan_dyn.c does not know
who called it.
2026-09-20 19:46:57 +07:00
7f92136401 Old instances of a redefined class now follow the class
CLHS 4.3.6's update protocol, minus the user hook, on the dyn side's
defclass. Redefining a class used to be silent: a class is sugar for a
constructor defn, so the edit replaced a body and the instances already in
the program kept their old keys for ever.

Three pieces. A registry in flan_dyn.c holding each class's current slot
list and a generation, made only of interned kw_entry pointers so the
collector has nothing to trace in it and no root to push for it. A uint32
generation on the instance, fitted into the padding kind and mark leave in
front of len's alignment — sizeof(flan_obj) is 48 with it and was 48
without, and flan_dyn_obj_size is there so a later field that moves it
fails a test. And a registration thunk per reload, run by the agent
through flan_reload_call after the module's bodies are published: it has
to be a thunk, because the case this exists for is a class redefined and
not constructed.

Migration is lazy, at want_map, len's map arm and dyn_equal's. Slots kept
by name, gained slots nil, dropped slots gone, identity preserved, entries
rebuilt in the class's order so a migrated instance is indistinguishable
from a fresh one. Equality migrates both operands first, so it is over the
class as it is now.

The session had to stop refusing the constructor's signature change, and
does so only for a defclass and only when no compiled caller is left
behind. The checker gets there first in practice; the walk in eval holds
the reason locally rather than inheriting it.

The registry is advisory: a class instance is an open map, so a key a raw
put wrote that the class never declared is dropped by the next migration.
FIX.org says that plainly rather than pretending enforcement.
2026-09-20 19:46:57 +07:00
e03028819c A defn named after a builtin now wins, and says so once
The author's rule: "allow shadowing but warn". A user (defn get ...) is
legal, the user's definition wins at every call site in the file that wrote
it, and the compiler warns once at the definition.

Builtin-wins was never a rule anybody wrote: named_call is one match on the
name, the builtin arms are string literals, and the three arms that look a
name up are the last three in it. So a guard goes first, the trailing three
are factored into ordinary_call, and both routes into it resolve a name the
same way.

The shadow stops at the file that declared it. An imported package's names
were qualified at the import, so a get written inside one is the builtin's
and stays the builtin's; the prelude is excluded by its file for the same
reason. programs/shadow-builtin.flan is both halves at once.

The warning prints from build_program, which is what every command and the
dev daemon's reload go through, in the shape --warn-memory established:
file:line:col, the squiggle, and an exit status that does not move.

And the message that described the old world is gone — the builtin-arity
note said a defn does not replace a builtin, which is no longer true and is
no longer reachable.
2026-09-20 19:46:43 +07:00
b87ae11fa8 The editor protocol never waits for the agent, and a parked delivery installs first 2026-09-20 19:27:12 +07:00
bbacbcb666 dead-beef's byte arm learns arity's new ctx argument 2026-09-20 19:27:06 +07:00
0a4c52d5ee filled and dead-beef, the two byte fills
# Conflicts:
#	DISCUSS.org
#	FIX.org
#	test/test_acceptance.ml
2026-09-20 19:25:08 +07:00
0cc229dc6c trap_park stops dying on the abort race, and Closed means what it means
# Conflicts:
#	FIX.org
2026-09-20 19:24:47 +07:00
f6416858bb defmacro takes a real parameter list, and [args] means the first argument
# Conflicts:
#	FIX.org
#	lib/prelude.ml
#	vendor/raylib/modes.flan
2026-09-20 19:23:01 +07:00
d225aa4172 int and float are the two spellings i32 and f32 answer to 2026-09-20 19:19:42 +07:00
26242388b9 int and float are the machine types, not a second name for them
The author's exception to the foreign-spelling list: int is i32 and float
is f32, and nothing else on that list moves.

Spelled in Types.ikind_of_name and Types.fkind_of_name rather than as two
prelude defaliases, because Check.is_cast asks those two functions and never
the alias table — a prelude alias would have left (int x) with no reading
while (i32 x) had one. Both names join primitive_names for the same reason
one layer down: that list is what decides (vec-new int) and the three-element
(defvar x int).

Nothing reverses: ikind_name still says i32, so every message, signature,
inspector line and DWARF name shows the machine type whichever spelling was
written.

A defalias restating the builtin is the no-op it says it is; one pointing the
name anywhere else is refused, since the alias table is never consulted and
the declaration would otherwise mean i32 in silence.
2026-09-20 19:09:26 +07:00
2afa554cce The flake note said noise; the full-suite numbers say otherwise
Isolated test_dev is 4-in-6 here against 2-in-6 at the base, which is noise.
The full suite is 5-in-5 here against 0-in-5 at the base, which is not — and
with this lane's three acceptance rows disabled it drops to 1-in-3. The rows
add compile jobs to the pool test_dev runs alongside, and a busier machine
loses the trap_park poll race more often.

Still not a new defect, and none of this lane's compiler code is implicated.
But the earlier note's suggested fix is now worth doing rather than noting,
and saying 'noise' would have sent the next reader the wrong way.
2026-09-20 19:02:53 +07:00
2240100ee9 Review follow-ups: the default's payload, four wrong reasons, a doubled prefix
1. The bare (dead-beef) built its default with Int64.of_int32, which
   sign-extends 0xDEADBEEF to -559038737 on a node tagged u32 — where the
   spelled-out literal arrives as 3735928559, because in_range admits it as
   the unsigned value it is. Masked to 32 bits, so the two spellings really
   do carry one payload; verified by diffing the emitted bodies of (dead-beef)
   and (dead-beef 0xDEADBEEF), which are now identical instruction for
   instruction.

2. The refusal's catch-all told a union and a function value that they
   'carry a tag that names a case'. Neither does: env.unions is the untagged
   unions, and an Fn is a code address. Split into one arm per reason —
   union, Fn, enum, Option, data type — and each is now pinned, so they
   cannot quietly re-merge. Same correction in FIX.org's bullet.

3. js.ml prefixed its own message with 'js: ', which bin/main.ml prepends
   too, giving 'js: js: ...'. Dropped, and the message now names the builtin
   it refuses, which its comment already claimed it did.

4. FIX.org said x86.ml reads both pattern helpers out of Emit. It reads only
   word_of_pattern; the tail walks rax with shr.
2026-09-20 18:58:32 +07:00
5ea6884d2c The diagnostics pass: every message shows, explains, and names the fix
# Conflicts:
#	FIX.org
2026-09-20 18:49:12 +07:00
3c39354833 Measure the Wire.Closed flake rather than assume it
It exits 1 with no FAIL line, which is the shape an earlier lane wrote up.
Two-of-two early failures looked like they might be this lane's, so: 4 in 6
here against 2 in 6 on a detached worktree at this branch's own base commit,
running test_dev alone. Noise at that sample size, same exception, same
mechanism.

Adds one detail to the earlier note, which had only ever seen the flake on
dev-trap-null-alloc: one of my six landed on dev-trap-free-all instead, so
what is racy is trap_park and every row that calls it.
2026-09-20 18:46:58 +07:00
78d9a0f051 The review's fixes: a suggestion that does not compile, and a confident wrong guess
F1 was the blocker and it was the worst kind of fault this pass can have: the
condition message told the reader to write (not= x 0), and not= does not
exist — the operator is !=. Applying the compiler's own advice got 'unknown
function not= — did you mean not?'. Both branches say != now, and all three
— the named form, the float zero, and the unnamed one — were checked by
compiling the sentence the compiler prints.

F5: a typo of a declared capitalised name got the generics lecture. (Piont 1
2) with Point declared was told that a capitalised name given type arguments
is milestone 5 work, which is a confident answer about a feature nobody was
reaching for. The did-you-mean runs first and, for a capitalised head only,
asks the type tables as well; the generics sentence is left for a head that
resembles nothing.

F2: flan_dyn_cast_kind had the site live and passed NULL on the trapping
path — the one entry point on this side that had a location and threw it
away. The acceptance row now pins the prefix it prints.

F3: the case-typo row used (data ...), which is not a top-level form, so it
refused as an unknown top-level form and the needle 'unknown' matched that
rather than the rule. Rewritten with defdata, and as a pair: a capitalised
head gets no accessor advice, a lowercase one does. Both halves were checked
to fail when perturbed.

F4: an end-to-end pin for the headline. programs/dyn-trap-site.flan is
compiled, run, and its stderr read for the file:line:col in front of the
sentence, on both backends and at -O0. Proven live: three failures when the
expected line is wrong.

F8: usize and size_t stay off the foreign-spelling list, and the comment now
says why — the honest answer is pointer-width, which is u64 here and u32 on
wasm32, and a tree that builds both cannot name one of them.

F10 pins the fourth dot shape. F6 moves the not-reached reasons out of the
commit bodies and into FIX.org, where they can be read without git.
2026-09-20 18:46:48 +07:00
e807986622 The dogfood batch: empty forms, comment, inc and dec, guards, limits, shorthand
# Conflicts:
#	FIX.org
2026-09-20 18:34:13 +07:00
7bd2c99353 sentinel-filled is now dead-beef, and takes the pattern
The author's revision. The name says what it writes, and the pattern is the
program's to choose: (dead-beef) is DEADBEEF, (dead-beef 0xBAADF00D) is
BA AD F0 0D. One byte-order rule covers both — a pattern's ascending bytes
are its big-endian bytes, which is how the hex literal reads left to right —
so every candidate DISCUSS.org listed is now spellable without the compiler
naming any of them.

The bare form is not a case a backend knows about: the checker writes
Tast.dead_beef_default in where the argument would have been, so
(dead-beef) and (dead-beef 0xDEADBEEF) are the same node and an acceptance
row prints both to say so.

The operand is an ordinary u32 expression, which is what the byte arm
already accepts for its byte. A literal is byte-reversed at compile time and
still reaches the loop as an immediate; a computed one is reversed at run
time, by llvm.bswap.i32 on one backend and bswap on the other, after which
the tail shifts its bytes out of the word rather than folding them. The
program runs a computed pattern over lengths 6 and 7 deliberately: that is
the case a constant-only implementation would pass by accident.

filled is untouched, and so is the fill boundary.
2026-09-20 18:33:05 +07:00
097161fd41 The park left for a re-run without draining its ring
[flan_merged_park] drained the agent's ring on one of the two flags that
wake it. [program_poll] — which an expression sets, by way of [Program.wake]
— polled and went back to sleep; [program_asked] broke out of the loop and
re-entered [flan_program_main] with the queue untouched. A plain
redefinition sets neither, so a body delivered to a parked program was still
in the ring when the run it was delivered for started, and installed at that
run's first frame boundary instead: everything main did before its first
(agent/poll) ran the body the person had already replaced, and the change
showed up one run late. A redefined main is the whole of a run, so it would
have had to be asked for twice.

Both flags drain now, and the exit drains before it leaves. The re-run is
still tested first and cannot be starved: the flag is latched at the top of
the round and nothing in the round can clear it.

The transcript row in test_dev.ml asserted the old ordering by name — two
lines out of the second run, the first of them the stale body — so it is a
line shorter now, and the absence of that line is the claim. The park-note
fixture grew a print of the redefinable body before its first poll, which is
what makes the new row able to see which body the re-run started with.

This is what the note the delivery is answered with has been promising: a
module queued against a park installs no later than the program's next run.
It now installs before that run's first frame rather than during it.
2026-09-20 18:32:01 +07:00
9fd1eb113f No sentence for a & that cannot arrive 2026-09-20 18:27:02 +07:00
92bf091361 A struct writes itself where its type is already known, and positionally 2026-09-20 18:23:42 +07:00
b2d1df300b macros.flan says which of the two spellings it is 2026-09-20 18:23:22 +07:00
eeac54a8e3 The parameter list, its tests, and the note about what it breaks 2026-09-20 18:21:54 +07:00
de3a3ed3c8 The writeup said eleven checker rows; there are fourteen
Two accepting and twelve refusing. Also records that the three acceptance
rows were confirmed to run rather than inferred from a green exit: the
expectation was broken on purpose once and all three reported.
2026-09-20 18:19:45 +07:00
69646e534e A macro's parameter list, and one grammar for it
(defmacro do-grid [[r rows c cols] & body] ...) — positional names, a [ ]
pattern wherever an argument is a vector, and & for the tail. The reading of
the list lives in Expand, below both sides that need it: Parse turns it into
the bindings a macro body opens with, and Macro checks a call against the same
reading before expanding it, so arity and shape are refused with the call's own
location rather than with the Loc.from_macro stamp every node of an expansion
carries.

The breaking half: [args] used to bind the whole argument list and now binds
the first argument. The whole list is [& args], and every defmacro in the tree
— prelude, vendor, tests, the elisp fixtures — was migrated to it. One grammar,
not a legacy mode.
2026-09-20 18:18:24 +07:00
1702a62308 Pin the () body guards, the match-arm rule, and correct two comments 2026-09-20 18:18:20 +07:00
99f519ba6f Two byte fills: (filled BYTE) and (sentinel-filled)
DISCUSS.org's sentinel-fill idea, built as two builtins because the author
asked for both: a memset with a byte the program picks, and the fixed
DE AD BE EF pattern a hex dump reads as DEADBEEF.

Both are spelled the way (zeroed) is — the value of whatever type is
expected of them — so (set grid (filled 0xFF)) fills a place and there is
no second, place-taking form beside set.

What may be filled is numbers, and structs and fixed arrays built out of
them. Everything else is refused by name: a filled dyn is a collector root
pointing at nothing, a filled Vec header frees a wild address, a filled
slice length is a bounds check that passes, and a filled bool is an i1 to
LLVM and a whole byte to x86, which is the one divergence this feature
cannot have.

The byte fill is llvm.memset / rep stosb. The four-byte pattern cannot be
a memset on either side — the intrinsic takes one repeated i8 — so it is a
counted dword loop in emit.ml and rep stosd in x86.ml, with the pattern
bytes and their little-endian word living once, in Emit. A size that is
not a multiple of four ends on DE, DE AD, or DE AD BE.
2026-09-20 18:15:19 +07:00
16f90a7ef5 The dot advice says assign where the form is an assignment
(set p.x 1) was being told that a field is read with an accessor, which is a
sentence that does not apply to the form it is printed under. The place
spelling is (set (.x p) 1), checked to be a real form.
2026-09-20 18:14:58 +07:00
c1b18cb181 Two struct spellings the parser was never able to decide
A bare {.field v} had its refusal in Parse.expr, before any checking, so a
defn whose return type was the only place the struct's name appeared could
not build one. The refusal moves to Check: Parse builds an Ast.Bare out of
the same struct_fields the named form uses, and check_bare reads the type
name off the expectation and hands that very list to check_struct. ZII, the
unknown-field refusal and the duplicate-field refusal are therefore not
copies of the named form's rules but the named form's rules.

Braces at a dyn want are the dyn map literal and stay exactly that. A
.field-keyed brace was never part of that spelling, and at a dyn want it is
refused by name rather than given a second meaning.

(Cell 1 2) is the other half, and it is character-for-character an ordinary
call, so only the symbol table separates them. It is decided on the last arm
of named_call, after a local of function type, a generic and the function
table -- so a defclass constructor, which is a real defn, resolves above it
and is untouched. Arity is exact: ZII is what the braces do, and a positional
list cannot say which field it left out, so it is not allowed to leave one
out. The refusal names the first field it did not reach and points at the
spelling that does mean "zero the rest".

Both are gone before any backend sees them -- Tast.Make either way -- and the
three acceptance rows print the same lines to say so.
2026-09-20 18:12:45 +07:00
89481cf8ec The wait for the agent socket was in front of the accept loop
[merged_serve] waited up to ten seconds for the program to bind agent.sock
before it started [accept_loop]. The listening socket was already up, so an
editor connected fine and then heard nothing: every first request of every
session cost the whole wait when the program calls (agent/start ...) late —
sand.flan starts it after rl/init-window returns — or never.

Nothing the editor asks needs that socket. In one process a delivery is a
call into flan_agent.c, not a connect, and the two-process daemon has already
waited for the bind in [two_process] and fails if it never comes. What the
wait was for is the sentence a program with no agent deserves, and a sentence
does not have to be in front of the loop to be said. So it is a deadline the
session passes ([agent_check]) rather than a wait it does: read from the
accept loop between connections and from [serve] before each request, because
an editor holds one connection for a whole session and the loop is not
cycling while it is attached. No thread, for the reason lib/dune gives about
what the merged link does to this library's dependencies.

Delivery stays honest either way. A program that links no agent at all
refuses through [over_socket]'s ENOENT, as before. One that has the agent but
has not started it takes the module into the ring and is answered with a note
that promises the poll and not a frame: a program with no (agent/poll) in it
never installs this, and "at its next frame boundary" would be the reply that
makes a redefinition look applied when it is not. C-x C-e's five-second
timeout gets the same distinction instead of asking whether a program that
has not got to its loop yet is calling the poll in it.

And the note a parked program's delivery carries is now said once per park.
A finished program is parked, so re-evaluating while a run's output is on the
screen repeated a paragraph on every C-c C-c. The first delivery of each park
explains itself; the rest say the one line that is the claim. [rerun] clears
the flag as well as [eval] does, so a new park is a new reader.
2026-09-20 18:11:32 +07:00
fa2b56ba5a Empty fn bodies, the pins for all six items, and the FIX.org entry 2026-09-20 18:11:20 +07:00
859e3aa7f4 The shadowed builtin, and's misdirected caret, and the expansion count
Ranks 7, 10 and 19.

A defn whose name is a builtin's is silently unreachable — the dispatch
reaches every builtin arm before it looks in the function table — and the
arity refusal that followed measured the call against the builtin while
pointing at a call the reader had written for their own. The count stays the
builtin's, because the builtin is what runs; the message says so and notes
the definition that is not being reached. The shadowing itself is not
refused: that is a language decision and not a fix pass's to make.

FIX.org recorded and's misdirected caret with three rejected fixes and one
accepted — check_if preferring the arm that is not a compiler temp — and said
it was a check.ml change nobody owned. and's last operand is its then arm and
the sentinel carrying the previous operand's location is its else arm, so the
mismatch landed one operand early. Only and needs it: in an or the chain is
already in the else arm, and with an expectation in hand neither arm is
checked against the other.

'expanding this declaration produced 2 of them' had no antecedent once read
cold. The head of the expanded form is the macro's name and says what
expanded, and the expression form names (do ...).
2026-09-20 18:11:16 +07:00
ea84394dd0 Three more: the return slot, the case payload, the missing collection
Ranks 13, 14 and 9.

A body form in the return slot was blamed at whatever leaf the type parser
gave up on — the [1] in [(defn f [x i32] (+ x 1))], three forms deep — where
the mistake is that the whole form is in the slot. The slot is blamed now and
the parser's own reason keeps its span as a note. Where the parser gave up on
the slot form itself the old shape stands, because a message like 'unit is
written (), not Unit' already names the right thing and leading with it is
better than restating it. The literal -- is an em dash now, like everything
else in the tree.

A one-field case binds the payload itself, so [(match s (Circle c) (.r c))]
reached for a field of an f64 and got a type fact. The binding carries what
the pattern made it, in words, derived at the arm from the case and the
subject; the refusal says the value is already in hand. Set by that path and
nowhere else, so every other binding's refusal says exactly what it said.

The missing collection asserted a thing and contradicted it in the same
sentence — 'is a directory named nosuch somewhere above X, and there is
none'. It states the rule and the two ends of the search instead. The bare
/. it printed was Filename.concat of a directory and a dot, and is cleaned
where the path is made absolute.
2026-09-20 18:06:13 +07:00
6c017c2cf8 Six dogfooding items: empty bodies, comment, inc/dec, () bodies, type limits, {.field} 2026-09-20 18:02:59 +07:00
8cba440aca Four more from the worst-20: the constant, the let annotation, the quote, the operand
Ranks 15, 6, 11 and 18, all of them the same fault in different words — the
message states a fact the reader already had and leaves out the half only the
compiler can see.

'k is a constant' was four words. It says what a constant is, names defvar,
and notes the defconst — [no_container_defconst] is the house's shape for
this and [declared_note]'s is the note's.

(let [x i32 5] ...) is what everyone arriving from a typed language writes,
and let has no annotation slot, so the i32 became x's value and the 5 was
left over: 'binding 5 has no value', which reads as if they had miscounted.
The annotation is blamed now, at its own span. Checked only when the vector
was about to be refused anyway, and the test for 'this names a type' is
syntactic because nothing resolves at parse time.

The unterminated string had one column on the opening quote and no note,
alone among this reader's three two-place errors. It has the same note its
neighbours have.

'+ takes numbers, found string' pointed at the whole form when the operand
was right there — the same whole-form-vs-operand fault the condition work
fixed once already. Text gets the extra clause it was reaching for, naming
concat and join without a call shape: the spelling that builds a slice of
byte slices out of string literals is not a clause in a sentence, and a
message that guessed at one would be wrong.
2026-09-20 18:01:08 +07:00
5afa707d76 The notes live in DISCUSS.org; the empty stub goes 2026-09-20 17:59:21 +07:00
1526b6fe3f Typed structs do not version; an evolving shape is a defclass 2026-09-20 17:58:26 +07:00
8add0093ba The defvar follow-ups, the spellings other languages use, and two register warts
The four the defvar review left behind, plus the two the author's dogfooding
notes name.

A three-element defvar that is neither a type nor a value gets a paragraph
about the fork it stands at, and the paragraph is right for the name that
genuinely could have been either. Three names cannot: a data case, which is
a third thing with its own spelling; a name another language uses for a type
this one has; and a plain type typo, where a confident one-edit suggestion
was turning a line into four. Each answers first now.

A bracket form never reaches that fork at all — the parser gives it the type
reading outright — so a value name inside one landed in [resolve_name] and
came back as a lecture about generic code. Both readings at the element that
decided it, and the dyn spelling it offers is checked to be a real form.

[int] is two edits from [i32] and so outside the one-edit net, correctly:
two edits is a guess. But the name is not a guess, it is what four other
languages call the default integer, so a short list answers it by name.
Nothing goes on that list without one honest answer — [char] and [void] are
off it, and the comment says why.

A parameter called [i] is not a mistyped [i8]. The machine types size
themselves in the name, so a typo keeps the digits and a parameter name has
none; that is the rule that stopped (defn idx [v i] dyn ...) being refused.

A type written in a two-element defconst was reported as an unknown name,
because that form has no type slot and the brackets read as an array
literal. A type name inside one is unambiguous — a type and a value cannot
share a name here — so it says what happened and names defvar.

Two register warts alongside: no message cites a repo filename at the reader
any more (plan.org in three, spec-memory.md in one).
2026-09-20 17:55:59 +07:00
db2f6c69b9 The call argument names the parameter, and the condition states the rule
Two of the audit's top five, and both are the same complaint: the message
states a type fact and stops where the reader needed the other half.

A call argument's refusal now says which argument of which function it is and
notes the parameter's own declaration, which is [declared_note]'s shape moved
to the place the audit calls the most-hit message in the compiler. [fns]
threw the parameter names and locations away when it resolved the types, so
[fparams] keeps the vector as written; a foreign declare and a generic copy
have no entry and degrade to the message alone rather than a wrong pointer.
The enrichment is conditional on the refusal being raised against the
argument's own span, so a mismatch deeper inside it is not misattributed, and
the rekind stops a nested call being named twice.

A condition that is neither bool nor dyn now states the rule instead of the
fact, with the comparison spelled out using the condition's own name where it
has one. Two things it does not do: it does not offer a comparison for a type
that has no zero, and it does not say anything about the dyn side, where
Clojure's truthiness means 0 is true. The re-check at bool still runs first
and its answer is kept wherever it knows more — a literal names itself, and
[None] names itself, and both beat a type name.
2026-09-20 17:44:11 +07:00
2b5d793d7a The dot habit, and a did-you-mean over values
Two of the audit's four cheapest structural wins, and they share a raise
point. [p.x] is how C, Go and Odin spell field access, and it arrived here as
the symbol [p.x] and left as 'unknown name p.x' — true, and no use to anyone.
The head is looked up now, so the refusal can say what [p] actually is, and
the struct's declaration comes along as a note. A capitalised head is left
alone: [Shape.Circle] is a real spelling and a typo in one is a mistyped case.

[near_miss] was written, tested and wired to the type tables alone, so a
mistyped value name got the bare refusal. [one_edit] is hoisted out of it so
the value side matches on the same rule rather than a second one that would
drift, and the candidate list at a value position is the scope, the globals
and the functions — plus, at a call, the builtin names, which live in no
table the checker keeps and reach the raise through a forward reference.

Two repairs alongside: the data-type message's format string carried eleven
stray spaces from a wrapped line, and (Pair i32) in a defvar had lost the
type fork's 'generics are milestone 5' answer when it started falling down
the value fork.
2026-09-20 17:40:45 +07:00
6d82221978 A dyn trap that says where: the nine trapping operators take a site
The dyn arithmetic and ordering entry points printed their sentence with no
file, no line and no column, which in a dynamic-first language is the type
error arriving from nowhere. flan_rt.c's bounds and arithmetic traps have
taken an emitter-threaded (loc, loclen) pair since they were written, and
[flan_dyn_cast_kind] is the fresh precedent on the dyn side; this is the same
pair, threaded through [arith], [want_nums] and [order] to the five
arithmetic and four ordering entry points. [eq] never traps and takes none.

The three trap printers take the pair and print nothing for a NULL loc, so
every other call site in the file — and test/dyn_ops.c, which calls the
runtime directly and has no source position — keeps its sentence byte for
byte. [trap_oom] is left alone: it is reached from [gc_alloc], which has no
site to be given and would have had to grow one on every allocation path in
the file for no reader's benefit.
2026-09-20 17:36:10 +07:00
ac2af7c537 The diagnostics worklist, ranked, so it stops floating 2026-09-20 17:24:35 +07:00
Joseph Ferano
36624d6e33 An abort's reply races the process it ends
test_dev.ml's abort asks were a bare Wire.send/Wire.recv pair against a
daemon that the abort itself is killing. In a merged flan dev the daemon
is the program: flan_agent.c's listener writes ok and sets aborting, and
the break loop's next pass _exit(134)s from the program thread while the
editor's reply is still being composed on the serve thread. Nothing
orders the two, so the reply arrives or the socket closes, at random.

When it closed, Wire.recv raised Closed, nothing caught it, and the test
binary died with no FAIL line and every case after it unrun. Four runs
out of four at the null-allocator trap.

Both endings mean the same thing and neither is the assertion: the
waitpid wait underneath each row is what says the program went. aborted
answers None for the end that arrived as an exit; the three sites that
abort a stopped or trapped program go through it. The fourth abort is
refused by a running program and ends nothing, so it is left alone.

trap_park's describe poll is guarded with the other answer: these traps
park because flan_trap_hook is installed, so a socket closing there is
the trap having ended the program instead of stopping it, which is the
failure that row already names. And SIGPIPE is ignored for the
watchdog's reason, so a send into the socket a dead daemon left behind
cannot kill the binary silently from the write side.
2026-09-20 17:07:15 +07:00
dc39631db7 M2 item 6: defclass, generic functions, and multimethods on the dyn side 2026-09-20 15:49:15 +07:00
0b4bf850ec The random exit 1 is test_dev's trap rows, and it predates this lane
dune test exits 1 roughly two runs in five with no FAIL line anywhere:
test_dev's trap_park polls a daemon that has just aborted at the break
loop, using a bare Wire.send/Wire.recv pair, and a daemon that exits
between the two raises Wire.Closed with nothing to catch it. The binary
dies and every row after it is skipped.

Measured on a detached worktree at dev-loop's tip with none of this lane
in it: 2 of 5 runs, same exception, same row. Same rate as this branch,
because it is the same code. Written down rather than patched -- the fix
is a claim about what those rows mean when the program ends under them,
which belongs to whoever owns them.
2026-09-20 15:47:06 +07:00
bd29eee3e4 The rebase is what was run, so it is what the note says
The what-was-run paragraph counted three commits and no rebase. It is
five commits and a rebase onto the defvar-dyn lane now, whose load.ml
and ast.ml arms sit beside the four this lane added and were read
against them by hand rather than left to the auto-merge.
2026-09-20 15:38:19 +07:00