505 Commits

Author SHA1 Message Date
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
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
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
ac7608f94b A method's parameter names are bound in parallel, not in sequence
A let binds in sequence, so binding a method's names pairwise from the
generic's reads a name it has just bound. A generic [a b] with a method
[b a] -- a swap, which is what renaming parameters most often is -- was
handed its first argument twice and could not reach its second at all;
[b c] is the same bug one step shorter. Every argument is now copied
into a temp in the unspellable ~ namespace first and every method name
bound from a temp, uniformly rather than only for the pairs that
collide, because a rule that fires on the tangled case alone is one
nobody exercises. Both shapes are in dyn-class.flan, where the values
are what is wrong rather than the types, and across all three rows.

With it, two things the descriptor fix left behind. descriptors_asm
wrote the descriptors into .rodata and a descriptor holds the address of
its own offset table, so every one of them was a relocation in a
read-only section -- a DT_TEXTREL, which ld warns about in a PIE and
refuses in a shared object, and which was warning in the new daemon
case's own output. They go in .data.rel.ro now, in both the executable
and the reload module; readelf -d on a reload module from each backend
shows no TEXTREL. And FIX.org: the stale held line for item 6, the
fourth read site of the shape tag (say_render, not just print), the
warning that a class's qualifier is the importer's alias so a
hand-written :a/point is coupled to one import's name, and the gap
flagged for the next sweep -- marking through a descriptor an x86 reload
module emitted is still unexercised.
2026-09-20 15:36:27 +07:00
0511cc1a01 A class's name and its keyword are one dispatch value
(defmethod area point ...) and (defmethod area :point ...) were both
accepted and the second was dead code: a class stands for the keyword
its instances carry -- that is the whole of how the two dispatch styles
share a mechanism -- so the duplicate scan has to compare them as one.
It compared the written forms, which differ. Normalised in the scan and
pinned in test_flan.ml.
2026-09-20 15:36:13 +07:00
52a1ea183a A method added to a running program, proved end to end
The claim classes were built for, made against a real daemon rather than
at the session's report: dev-class.flan is compiled with one method for
one class, a second method is delivered into the live process, and the
call through the generic's cell answers with the new method's body while
the original one goes on answering. That is what a generic being exactly
one top-level name buys, and it is now pinned rather than argued.

Two things came out of writing it, neither of them about classes.

The x86 backend's redefinition module never emitted the per-type dyn
descriptors. Emit.redefinition has always emitted them, by going through
finish; the x86 twin ended at the rodata section and stopped. Nothing
had reached it, because a redefined body had to construct a struct
holding a dyn to need one, and until NoMethod there was no such struct a
compiler-written body could build. What it looks like is not a bad read
at run time but a link failure — desc_of mints a local label, the body
references it, and ld refuses the module with an undefined symbol. One
line, beside the same call in the executable path.

And the thing the test had to be written around: a dyn value answered by
eval-expr does not come back in the reply's :value at all. It renders to
the program's own stdout, which reaches a *later* reply's :output — the
dyn-global rows already read one that way and say so. So every answer
here is compared inside the expression, and what crosses the wire is a
typed 1 or 0. Left as it is; where a dyn expression's value should
surface is a question about the editor protocol, not about this lane.

The session-level test stays: it pins which names an added method
reports for installation, which is the half a daemon test cannot see.
2026-09-20 15:36:13 +07:00
5a62770e52 Classes and generic functions, milestone 2's last item
A defclass is a named dyn map with a shape tag, and a generic function
dispatches on it two ways: CLOS's, where the dispatch value is the class
of the first argument, and Clojure's, where a body computes it. They are
one mechanism and not two — a class dispatcher is (class-of arg0) as the
dispatch function, which is what lets a method written for the class
point and one written for the value :point be the same branch.

    (defclass point [x y])
    (point 3 4)                 ; the constructor, positional
    (class-of p)                ; :point, or nil for anything else
    (defgeneric area [self] dyn)
    (defmethod area point [p] (* (get p :x) (get p :y)))
    (defmulti describe [x] dyn (get x :kind))
    (defmethod describe :square [s] ...)
    (defmethod describe :else [s] ...)

A slot is a key in the instance's own map, so get, put and has-key? are
how one is read and written and no operation was added for any of it.
What the class adds is the tag, and the tag lives in the object's header
rather than in a reserved entry — the queue's note said a reserved key
and this departs from it, because a key would be counted by len, walked
by the renderer and compared by equality, so every instance would answer
a length one larger than its slot count and print a key nobody wrote. A
header field cannot be reached by get or put at all, so no user key can
collide with it. It costs nothing: the map arm of flan_obj's union grows
to the size the view arm already had, and sizeof(flan_obj) is unchanged.
It needs no tracing either — the tag is an interned keyword entry, which
is immortal and is not a collector object.

The tag shows up in exactly three places: class-of answers it, equality
compares it (two instances of one class compare by their slots; an
instance and a plain map with the same entries do not, which is
Clojure's answer for a record beside a map), and both renderers print it
— #point{ :x 1 :y 2}, Clojure's own spelling.

None of the four forms reaches the checker. lib/classes.ml turns the
whole declaration list into ordinary defns at the top of build_program,
the way Shim.expand already turns a declare-c into a declare plus a
defn: a class becomes its constructor, a generic becomes one function
whose body binds the dispatch value and compares it down a chain, and a
method becomes a branch of that chain. It is a pass and not a macro
because a macro sees one form and the generic's body is not decidable
until every method is in hand — a method may be written above its
generic, below it, or arrive at a reload an hour later.

That last case is why the method bodies are inlined rather than lifted.
A generic is exactly one top-level name, so adding a method to a running
program is the ordinary redefinition of one function, through the cell
every call site already goes through. session.ml names the generic
alongside the method's own declaration name for that reason. The cost,
recorded rather than hidden: a method is not separately callable and is
not a frame of its own.

A dispatch that finds no method signals NoMethod, a prelude struct
carrying the generic's name and the dispatch value that missed. A
condition and not a trap, because a miss is something a program can be
written to answer, and handler-case around the call is the shape. Its
value field is dyn, the first condition here with one; the per-type
descriptor an item-2 struct carries is what the collector reaches it by.
No restart is established at the miss, which is BoundsError's decision
taken for BoundsError's reason.

Both backends, identically: the two new runtime entry points are
declared in emit.ml and the x86 backend needs nothing, since a dyn call
is a dyn call there. Deferred and written down in FIX.org: inheritance,
multi-argument dispatch, :before/:after/:around, named-slot
construction, unknown-slot checking, and computed dispatch values.
2026-09-20 15:36:13 +07:00
c4e07256db A three-element defvar reads its third element: a type is zeroed static, anything else is dyn
# Conflicts:
#	FIX.org
2026-09-20 15:14:56 +07:00
Joseph Ferano
9321547822 A defvar's third element decides: a type is the static, anything else is dyn 2026-09-20 15:00:15 +07:00
49ee8774d3 The case inside a struct literal is the same unwritable value as a bare one
const_defconst_init matched MakeCase at the top level only, so a case nested in
a struct literal got the general computed message, whose advice nobody could
follow. Emit.const found it by recursing; this finds it the same way, left to
right and first offender wins, which is the order the emitter spelled fields
in. FIX.org's claim that the messages did not change is rewritten to say what
did change about the general one.
2026-09-20 14:47:19 +07:00
ce93ac7622 A defconst's value is what the linker writes, on both backends
The refusal moves to the checker: Emit.const refused a computed defconst by
name while the x86 backend ran it through the startup function behind an
.init~once. flag, like a defvar, so the two backends disagreed about the same
program. One refusal in Check.const_defconst_init ends that, and it is the only
place that can name the way through.

The accepted set is unchanged: Tast.const_init's, which is Emit.const's and the
x86 data_sym path's, plus the integer arithmetic collect's folding pass has
already turned into an Int before the initialiser is looked at.

Emit.const's two refusals become a failwith no program reaches; emit_global's
gconst || const_init loses its left half; x86 needed no edit, since it never
classified by the form. test_flan's infers probe asks Check.expression now that
a defconst can no longer wrap an arbitrary expression.
2026-09-20 14:47:19 +07:00
e6ac833626 The follow-ups the day's reviews left behind, each re-verified
flan.abi.require was spelled by hand in both backends, which is the one
job Mangle has. Moved; emit and x86 produce byte-identical output on the
reload path either way.

Four comments in the dyn-cast code asserted things that are not true.
The warning's location prefix now reads like every other loc-bearing
runtime diagnostic instead of inventing a shape. widen's contract says
what cast_dyn actually does with it. The thread-safety note names the
torn {ptr,len} overread rather than a duplicated line, and says why no
lock. The site table's borrowed loc pointer names what keeps it valid.

The memory op's note claimed a completeness it does not have: dyn push
and put may allocate and are deliberately silent. Said so, in the note,
in the classifier, and in FIX.org where the decision belongs.

The documented flycheck form only matched warnings, so a real error
made it say the checker returned non-zero and found nothing.

flan-clear-memory cleared one buffer where the toggle clears all.

Two comments claimed test/dyn_ops.c calls every function flan_dyn.h
declares; six are declared and never called there.

flan_dyn_stub.c's deadness is written into FIX.org for the author to
decide on. Not deleted here.
2026-09-20 14:32:17 +07:00
b6ea14f930 A numeric cast opens a dyn box itself 2026-09-20 13:52:27 +07:00
2127d4d070 The transfer out of an initialiser is a restart's, not an unhandled signal's 2026-09-20 13:21:11 +07:00
b0ee9a3ce0 The runtime structs are one table, and the mangles one module 2026-09-20 13:20:47 +07:00
0e9707c9ef The review batch: honest headers, named temps, the seqlock's odd window
# Conflicts:
#	FIX.org
2026-09-20 13:11:36 +07:00
811c37dca4 Two sentences said more than was checked
The defconst divergence is dated from the history rather than asserted, and
the claim that it is the backends' only disagreement is gone — x86.ml has
unsupported paths of its own.
2026-09-20 13:09:04 +07:00
5b39730f07 The init-once flag moves out of the namespace a program can write
The guard flag was named .init-once.<global>, and . and - are ordinary
symbol constituents, so (defvar .init-once.x i64 7) beside a computed x
emitted the same symbol twice: the dev build died at the assembler on both
backends, and the flag's Bool was registered over the user's global in
Emit.globals so the store came out as an i1. It is .init~once.<global> now;
~ terminates a symbol in the reader, the same trick destructure~N uses.
test/programs/dev-rerun.flan carries such a global and no new printed line.

Three places said a defconst is the linker's image on one backend and a
constructor's stores on the other and a re-run reaches neither. Tast.const_init
splits on the initialiser and not on the form, so that holds only for a
constant initialiser; a computed defconst is guarded like a defvar on x86 and
refused outright by emit.ml's const. The sentences now say that, including
the divergence.

emit.ml also claimed Check.no_transfer_in_init made it impossible to leave the
guarded branch between the store and the flag. It is syntactic over the
written initialiser only: a callee can signal unhandled and take the call's
transfer edge out, leaving the flag false — which is what should happen, since
the next run retries. Read off the emitted IR for such a program.

The x86 float-Rem comment says why the dead movabs before fmod is kept, and
its mid-sentence line break is gone; math3.flan's first float-% line prints
six values, not four.
2026-09-20 13:07:09 +07:00
2daf7e9164 A short-circuit temp stops calling itself a destructuring one
fresh_temp takes the purpose, so and/or mint and~N and or~N where they used to
mint destructure~N. The names are shown -- the inspector lists a frame locals
by name -- and nothing pinned the old spelling; destructure~nth is an
unrelated compiler builtin. Confirmed in a --debug build DWARF.

The caret wart on and last operand in a want-free position is documented at
the site and in FIX.org rather than fixed: every candidate fix reads worse
than what is there, and the real fix is check_if choosing which arm to blame.
2026-09-20 13:00:55 +07:00
1829cd43b6 One field list per runtime struct, one spelling per symbol prefix
The %handler, %restart, %fninfo and %flanframe shapes were written twice:
as LLVM type strings in emit.ml and as hand-computed byte offsets in x86.ml,
with the two %fninfo initialisers spelled a third and fourth time. Emit.Rt
now holds one field list per struct and derives all four — the type string
and the getelementptr index for LLVM, the offset and the size for x86, and
the initialiser for both. The derived numbers were checked against every old
constant before the call sites moved.

The flan. prefixes were spelled in four files, including both backends
hand-writing "flan." ^ name for a DWARF linkage name instead of calling
their own helper. Mangle now holds them unquoted; each backend adds its own
sigil. The ABI markers stay apart on purpose: flan.abi.llvm and flan.abi.x86
differing is what makes the loader refuse a crossed pair.

The float-to-integer cast bounds and the division-check elision policy are
Emit.cast_range and Emit.div_checks. The second is a language decision and
had been byte-identical in both files; the first had drifted cosmetically.

emit and emit --x86 output for all 166 test/programs, at -O0 release, --dev,
--debug and --dev --debug, stdout and stderr, is byte-identical to the
pre-change compiler.
2026-09-20 13:00:52 +07:00
f030c5f7f1 Memory diagnostics on demand: gc and native allocation sites, faintly
# Conflicts:
#	FIX.org
2026-09-20 12:57:30 +07:00
26439dc22e The runtime's twin emitters share their bodies, and six dead symbols go 2026-09-20 12:06:48 +07:00
0b4f5e139e defvar keeps its value across re-run; the form is the contract
# Conflicts:
#	FIX.org
#	test/test_dev.ml
2026-09-20 12:01:50 +07:00
Joseph Ferano
5b0062219e Ask which lines allocate, and believe the runtime over the enumeration
[Check.memory_sites] is the pass [Check.no_gc]'s shape: it runs over the
finished program, answers a diagnostic list, and tells nothing downstream
that it ran. Two classes on the diagnostic's kind — the collector's heap
and an allocator the program named — so the CLI, the daemon and the editor
dispatch on one field and none of them reads a message to find the class.

[--warn-memory] on check and build prints them where errors go, in the
shape flycheck parses, without moving the exit status. [(:op "memory")]
answers the same list over a session's last checked program, needing no
program on the far end. [M-x flan-check-memory] paints it two faces fainter
than an error's, cleared by an edit or by asking again.

Two of the spec's own examples turned out not to allocate, and the
precision rule outranks the enumeration: (vec-new T) passes a capacity of
literal zero to flan_vec_init, which returns before the grow, and
flan_map_init takes no block at all and says so in its own comment. The
block arrives at the first push, which is the line that is marked. The
classifier reads the capacity argument rather than the symbol, which is
what lets slurp be marked through the same entry point vec-new is silent
through. FIX.org has the rest of the evidence.
2026-09-20 11:58:47 +07:00
e441d62874 Eight emitters were four renderings, and the escape table was three copies
The dev runtime rendered a value twice over: [flan_dev_emit_{u64,i64,f64,str}]
into the result buffer an evaluation is read back from, and
[flan_dev_watch_emit_*] into the current watch slot, with the same four bodies
either side and the sink as the only difference. [flan_dev_result_end] and
[flan_dev_watch_end] were the same ellipsis-and-generation close, comment for
comment, over two buffers.

So the rendering takes the sink as a parameter and the eight entry points are
eight one-line calls into four statics. The exports stay eight: the compiler
emits four of them by name (Session.externs) and a program reaches the watch
four through declare-c. ABI does not collapse because the bodies did.

The escape table is now [flan_escape_char] in flan_rt.c, once: what one byte
reads as inside a quoted string, into a caller's four bytes. A table and not a
printer, because the framings are genuinely different — [flan_escape_bytes]
builds a capped slice to hand back and the dev pair streams into a buffer it
does not own the end of — and the framing is the part that is each caller's.
The dyn printer keeps its own copy, which is docs/SPIKE-DUPLICITY.md §9's one
defended repeat: it is inside the runtime that owns the storage it walks. Its
comment, and flan_rt.c's, no longer tell a reader to change the other two.

Same for the NaN rule, which was spelled four times: [flan_f64_format] is
flan_rt.c's [flan_f64_to_bytes] without the slice, and the REPL emitter and the
watch table call it rather than restating "%g, and nan unsigned".

Byte-identical, checked two ways. A session driven over the daemon's socket
before and after — every arm of the emit family, the escapes, and a string long
enough to reach the truncating close — diffs empty. And a harness linking both
trees' flan_rt.c + flan_dev.c compares 8173 renderings: every byte 0..255
through both string emitters and through flan_escape_bytes, every length across
both caps and the ellipsis either side of them, both NaN signs, both infinities,
i64 and u64 at their extremes. Identical.

Dead code, each verified by its own grep before removal. These are exported C
symbols, so a program could reach one through declare-c; the evidence is that
nothing in the tree does, including the docs that write the surface down.

  flan_dev_watch_u64 (flan_dev.c) — one occurrence repo-wide, its own
  definition. The i64/f64/str siblings are declare-c'd in
  test/programs/dev-watch.flan and written down in emacs/MANUAL.md; this one
  appears in neither, and in no other file.

  flan_break_resume (flan_rt.c) — the only non-prose reference was a stale
  extern in vendor/agent/flan_agent.c with no call under it. Both gone.
  [flan_name_id] stays: the bounds and arithmetic conditions still hash through
  it. [flan_restart_take]'s comment no longer points at a function that is not
  there.

  flan_dev_watch_enabled (flan_dev.c) — prototyped in flan_agent.c, never
  called. [watch_on] is still read directly by the three sites that gate on it.

  clang_stamp (lib/build.ml) — a [lazy] never forced; one occurrence.

  marshal (lib/expand.ml) — no reference anywhere. [write], which it wrapped,
  is called twice in [call], so the [let rec] group is demoted to keep it.

  is_bytes (lib/js.ml) — dead within js.ml. Nothing else in that file is
  touched: the JS backend is parked, not dead.

Kept on purpose: [Loc.forget_sources], documented in docs/BUILT.md as
deliberately retained, and [flan_dev_watch_num_f64], which emacs/MANUAL.md
declares as public surface.

Two comments in lib/dev.ml argued the orphan grace in terms of elisp symbols
from before the rename — [flan-dev--open], [flan-dev--connection],
[flan-dev-poll-interval], and a file called emacs/flan-dev.el. None of those
exist. Re-spelled as [flan--open], [flan--connection], [flan-poll-interval] and
emacs/flan.el, which is where they are; the reasoning is load-bearing and is
unchanged.

dune test: exit 0. 59 lines of code out, 24 lines net of the prose that says
why.
2026-09-20 11:57:35 +07:00
001f17b8bf Four copies in the checker become four callers
get and map-remove asked the runtime the same question through two
identical bodies; only the symbol differed, so map_lookup takes it and
both entry points keep their own preambles and their own prose.

invented_ctx existed and six sites still wrote the sixteen-field literal
out by hand. All six go through it now — three verbatim, three with the
two or three fields that make them a written body rather than an
invented one — so a field added to the record is one edit.

dyn_descriptors and dyn_sites each walked every place a value can live,
and the two walks had already drifted: an unnamed frame slot was "a
local of f" in one and "a local in f" in the other. One value_sites
walker now, with the callers' filters kept on their side; the surviving
text is "a local in f", because a named slot has always read "n in f"
and neither spelling was pinned by a test.

The duplicate-field scan appeared three times and the ZII fill twice —
check_case's own comment conceded the copy. given_once and zii_fill.
The union keeps its unknown-member pass ahead of the scan rather than
folding it in, because that ordering is what it reports today.
2026-09-20 11:50:19 +07:00
7db5ec1885 Float % on x86 is a libcall, and the parity ruling is written down 2026-09-20 11:33:26 +07:00
da40dc2de2 The rule a re-run follows is written down, and a test fails without it 2026-09-20 11:30:58 +07:00
Joseph Ferano
6896128407 Say what the objdump actually showed, and where it showed nothing
The first version of both notes claimed a call to fmod in any build with a
typed float % in it. A literal pair is folded before any call exists, which
is the same fact two paragraphs further down explaining why the corpus block
uses globals. Both now say the measurement: the calls are in the build whose
operands come through globals.
2026-09-20 11:25:49 +07:00
4f060d07db The park's root reset stops at the globals' watermark
# Conflicts:
#	test/test_dyn.ml
2026-09-20 11:24:46 +07:00
Joseph Ferano
58d5ccda94 Typed float % on x86: the same fmod LLVM calls
There is no SSE remainder instruction, and LLVM does not invent one: at -O0
it lowers frem to fmod or fmodf. The backend now calls those two symbols
rather than refusing the operator, which is agreement by construction rather
than a second hand-written identity that would have to get every rounding,
every signed zero and every infinity right on its own.

Rem was the only gap. emit.ml's float surface is Add, Sub, Mul, Div, Rem and
the six comparisons; x86 had everything but Rem, and its comparisons already
build LLVM's ordered predicates out of ucomis, setcc and setnp.

math3.flan grows the operator spelling beside the fmod-f32/fmod-f64 calls it
already had, through globals so the pair is not folded before either backend
sees an operator. FIX.org records the ruling the fix came from.
2026-09-20 11:21:25 +07:00
eec9efc94c M2 item 3: typed containers cross into dyn as views of permanent storage
# Conflicts:
#	lib/emit.ml
#	runtime/flan_dyn.h
2026-09-20 11:18:18 +07:00
931cf860c3 A defvar's initialiser runs once, so its value survives a re-run 2026-09-20 11:16:37 +07:00
e6af2d3f77 The park kept the frames' roots off and the globals' on
flan_merged_park called flan_dyn_root_reset, which emptied the collector's
root stack. The frames' roots had to go — main is left by longjmp, so they
name stack the next run overwrites — but the dyn globals' roots are on that
same stack, pushed once by the emitted main and never popped, and the park
took them with the frames.

The park is not a quiet state. It services evaluated thunks, a thunk
allocates, and an allocation collects. So a program with (defvar config dyn)
answered (get config :s) with its string before any thunk ran and with nil
after one that allocated past the heap's floor — a read of memory the sweep
had freed, answering nil by luck of what the freed words decoded as.

The emitted main now brackets its global pushes: flan_dyn_root_globals_begin
empties the stack, the pushes go on, flan_dyn_root_globals_end records how
many of them there are, and the park resets to that line instead of to zero.
Nothing between the two allocates, which is what keeps the globals from being
swept in the window where they are unrooted — and [begin] emptying the stack
rather than adding to it is what makes a re-entered main re-root the same
globals rather than push a second copy of each, which also closes the other
half: a re-run used to re-push roots over slots left dangling by the park.

Both emitters, because the dev loop's default backend is x86 and a fix in one
lowering is not a fix. A program with no dyn globals emits neither call and
its root stack still resets to empty, which is what an empty push list should
leave behind.

flan_dyn_root_pop now clamps at the globals rather than at zero. An
over-popping frame eating the globals is the one way that clamp could turn a
miscount into this same use-after-free.

Covered twice. test/dyn_ops.c's park mode is the runtime's half — a run, a
park with a collecting thunk in it, and another run, three times over,
asserting both that the global survives and that the frame's five hundred
objects do not. Under ASan the old reset reports heap-use-after-free in
flan_dyn_tag with the free in gc_sweep; under memcheck it reports 24 errors
and still prints the right answer, which is the shape of the bug. test_dev.ml
drives the whole daemon over its socket on both backends against
programs/dev-dyn-global.flan.

Not touched, and it wants a decision rather than a patch: a re-run re-enters
flan_program_main, which re-runs the lifted startup function, so every global
with a computed initialiser is reset by a re-run. That contradicts dev.ml's
own note and FIX.org item 1. It is independent of this — the roots are right
whether or not the values are re-initialised.

Nor is this the reload path. A defvar added by an evaluation gets its storage
from flan_dev_global (emit.ml's new_globals, x86.ml's counterpart) and there
is no flan_dyn_root_push anywhere on that path in either backend, so a dyn
global added to a live session is unrooted. That is a separate defect with a
separate fix, and nothing here makes it better or worse.
2026-09-20 11:06:14 +07:00