1028 Commits

Author SHA1 Message Date
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
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
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
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
90110f525a Classes and generic functions, written down
Item 6 marked LANDED, and the section under it: the four spellings with
an example each, why the shape tag is a header field and not the
reserved key the queue's note assumed, why the method bodies are inlined
rather than lifted, and what was deferred -- inheritance, multi-argument
dispatch, the qualifier methods, named-slot construction, unknown-slot
checking, computed dispatch values -- each with the reason rather than a
list. Plus the two findings that outlive the lane: the x86 redefinition
module's missing descriptors, and eval-expr never answering a dyn in
:value.
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
b2558f04de A defconst is a compiler const, on both backends and dyn is not one 2026-09-20 14:50:01 +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
e19dd5be29 The follow-up batch: honest prose, one prefix, one shared label 2026-09-20 14:39:35 +07:00