297 Commits

Author SHA1 Message Date
2d8d9cd5a8 dotimes counts from where you say, and can count down
"Is there a way to do dotimes or a loop in reverse?" — the answer was a
hand-written let plus set. Now it is (dotimes [i 9 -1 -1]).

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

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

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

Still a special form, still a Let and a While with the step in the latch, so
neither backend learned anything — the new program prints the same thing
under --x86 and at -O0. load.ml's Form-level walk had to learn more than one
bound for the same reason parse.ml did; it is part of this feature and not a
bug that was sitting there, because before this a three-bound dotimes was a
parse error long before that walk could reach it.
2026-09-21 08:09:53 +07:00
1cd4e0ec2e The defining form is fixed at build time, all three of them
The first cut of the form-change refusal asked only about def and defonce
and asserted in its comment that defconst was another arm's business. It
was not: defconst to def at the same type fell past every arm, and defonce
to defconst fell past them into the consts republish, which stores the
declared value over live storage at the frame boundary. One refusal over
gconst and grerun together now covers all six directions.

And two coverage gaps closed by running rather than reasoning: reload-v6
carries a (def dial i64 5) the host was never built with, so the x86 image
path executes and its 5 shows in the transcript's arithmetic; dev-rerun's
echo reads counter in its initialiser and follows it 40, 41, 42, 43 across
re-runs, where a captured first answer would print 40 four times.
2026-09-21 07:30:57 +07:00
a64bee6d96 Review follow-ups: a new def's image, the keyword that cannot change, and the sweep
Three defects, all from lifting every def initialiser, none of which the
suite caught:

A def typed fresh into a live session came up zero and stayed zero. The
image flan_dev_global copies on the allocation is the only value a new
global ever gets — the host's .init-globals never calls its initialiser —
and both backends chose that image with Tast.const_init, which a def's
lifted Call fails by construction. Emit.initial_image reads the constant
back out of the lifted body; the x86 twin had the same bug.

Changing a global between def and defonce was silently ineffective: the
guard lives in the startup function compiled into the host, which a reload
cannot republish. Session.compatible refuses both directions and says to
restart; editing the value stays allowed.

And global/<n> no longer leaks into the signature refusal when a def is
retyped — the global loop names the same fact in words a reader can act on.

flan check prints def, defonce or defconst off grerun; (defvar) with no
arguments names the shapes rather than offering (defonce ); the docs,
plan.org, runtime comments and valgrind.supp are swept; BUILT.md states
the release-build cost and the uninit caveat.
2026-09-21 07:19:33 +07:00
801c70bd0d The LLVM cell declaration for a lifted def initialiser, pinned in the IR
Session hands global/<n> to the redefinition when a def is re-evaluated,
and that target is not a sibling — its fparent is the global — so its cell
declaration comes from Emit.redefinition's targets pass, a path no test
compiled: the dev-rerun leg runs on x86 merged, which reaches host cells
through the GOT and never needed the declaration. reload.flan carries a
(def paint i64 7) now and test_reload greps the module text for the cell
extern and the hidden body, which is the idiom the file already uses.
2026-09-21 07:12:04 +07:00
a4c6b996ff def re-runs its initialiser, and defvar is renamed defonce
The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.

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

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

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

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

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

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

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

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

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

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

And the ruling: a u8 renders as 97 (\a) where a person is inspecting
and stays 97 where the program is printing.
2026-09-20 22:55:11 +07:00
1cbe8ed386 The break loop keeps its condition, and the daemon renders it
The break loop used to discard the pointer it was handed, so the buffer
could name a BoundsError's fields and never show 648. Now the snapshot
stashes it, flan_agent_condition hands it back on the stopped thread,
and a daemon-built thunk — locals pointed at the condition — renders
each field. Delivered at-stop, so a resume-and-restop cannot get the
old type read over the new pointer.

The trap sites publish their loc around the hook call, the snapshot
copies it, and break answers :site with the line's text as :source —
the frame lines say where each call was; this is the only record of
the indexing itself.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The node the abstract pass builds is never emitted. Each copy
re-checks the same form with the variable substituted, and that is
where the literal is built at the concrete width and range-checked --
so (+ x 300) is fine at i32 and a refusal at u8, and u8 is where it is
refused.
2026-09-20 20:21:41 +07:00
40d62e7643 builtin/name, the spelling a shadow cannot take away
A defn named after a builtin wins for its whole file, and until now that
was the end of it: the builtin had no remaining spelling, so a defn that
meant to wrap one was unbounded recursion. builtin/len is the builtin len
wherever it is written, shadowed or not.

The qualifier is the package one's, and builtin is reserved rather than
resolved: Load refuses it as an import alias, Check refuses it as a
declaration's name, and those two doors are the only ways a qualifier can
be made. named_call and var each strip the prefix and re-enter with a flag
that the shadowing guard consults, so every arm below sees the bare name
and refuses in the builtin's own words.

The shadow warning now names the escape in its second half.
2026-09-20 20:17:17 +07:00
07981cfbff agent/start finds its own socket, and starts itself under the daemon
# Conflicts:
#	FIX.org
2026-09-20 20:11:12 +07:00
38a570bc7a A bind that fails gives the flag back, and three sentences get pinned
The bug review found: [start_on] claimed [started] at the top and every
failure exit left it claimed. Under [flan dev] the constructor is the first
caller and reports to nobody, so a path nothing could bind disarmed the
program's own (agent/start ...) as well — it answered 0 with no socket, no
listener and no hooks, where before this lane the explicit form answered -1.
Success reported for nothing at all is worse than the error it replaced.

So every way out that is not a listening socket unwinds: the fd is closed, a
file the bind managed to make is unlinked, and [started] goes back to 0 so a
later start is a real attempt. Pinned by running the zero-argument fixture
with FLAN_AGENT_SOCKET pointing nowhere — constructor fails silently, main's
own call then fails loudly, "cannot listen" and exit 1.

Two arguments to (agent/start) are refused, which nothing held: the macro's
[& args] cannot say "one at most", so what says it is the expansion splicing
every argument into a function that declares one. The message names
agent/start-at and carries the expanded-from note, and that is what the
acceptance row asserts.

And the reply a delivery gets when there is no agent in the process, which
nothing held either. dev-noagent.flan parks, so it was never this case;
dev-noagent-running.flan keeps running, and the answer is a refusal naming the
socket that could not be reached — not install_note's "queued", which would
promise a poll with nothing to drain. Which leaves that note unreachable in
all three shapes rather than merely unpinned, worked through in FIX.org.
FIX.org also now says what an exported FLAN_AGENT_SOCKET would do: start_on
unlinks before it binds, so an agent-linked program started in that
environment takes the path away from whoever bound it first.
2026-09-20 20:10:39 +07:00
f8dfdaa9a0 Merge branch 'dev-loop' into lane-implicit-widening
# Conflicts:
#	FIX.org
2026-09-20 20:01:54 +07:00
66f925c36f A redefined defclass migrates its live instances on their next touch
# Conflicts:
#	FIX.org
2026-09-20 19:58:42 +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
77d6e43b09 The agent binds before main, so a dev program need not start it at all
A constructor in the agent package binds FLAN_AGENT_SOCKET when it is set,
which is the daemon and nothing else — both shapes set it, before the fork in
--two-process and before the exec in the merged build. So a program under
[flan dev] that calls (agent/poll) and has no (agent/start) in it takes
redefinitions anyway, and one that does call start meets an agent that is
already listening and gets a no-op.

The window this closes was the complaint in DISCUSS.org: a program that opens
a window before starting its agent leaves the daemon waiting on a socket that
does not exist yet. Bound here, the socket exists before main whatever the
program does afterwards — so test_dev.ml's late-agent row asserts the negation
of what it used to. The delivery sent during the sleep no longer carries "the
program has not called (agent/start ...) yet", because that is no longer true
of it; what is still late, and still asserted, is the poll that installs it.

It reaches exactly as far as the linker does. Reach prunes a package nothing
calls into, so a program that mentions the agent nowhere does not link this
file and has no constructor to run: auto-start is for a program that polls and
has dropped its start call, not for one that says nothing about the agent at
all. That limit and the release-build residual are in FIX.org, along with the
daemon branch that can no longer be reached.

agent-nostart.flan is the pin, and its two numbers are the honest ones: 1
before anything could arrive, 1000 after the wait, because a listener bound
before main is still not an install.
2026-09-20 19:52:34 +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
e9c0e96a9b (agent/start) takes no argument, and takes the socket away after itself
The path was ceremony. Under [flan dev] the daemon already decides where it
wants to talk to the program and writes it into FLAN_AGENT_SOCKET, which the
C side has always honoured over whatever the source named — so the argument
was a value nothing read. Outside the daemon any path will do as long as the
program says which one it picked.

So [start] becomes a macro over two functions: no argument picks the
daemon's socket if there is one and otherwise /tmp/flan-agent-<pid>-<clock>.sock,
announced on stderr because a socket nobody can name is a socket nobody can
connect to. The explicit form stays for a program that wants a fixed path.
Extra arguments are spliced into [start-at] rather than dropped, so the arity
refusal is still the checker's, at the call site.

And the socket is removed on the way out. The bind stashes the path it bound
and registers an atexit; the two paths that leave by _exit — the break loop's
[abort] and the orphan handler — unlink it by hand, as the orphan handler
already did for its own copy of the path. Nothing else takes it away: under
the daemon it sits in a temp directory that is still never removed (FIX.org),
and outside there is no daemon at all.

test/programs/dev-loop.flan now names no socket, which puts the whole daemon
block in test_dev.ml behind the zero-argument form; agent-auto.flan is the
standalone half, reached only through the line the program printed, and it
pins the second (agent/start) as a no-op and the socket as gone at exit.
2026-09-20 19:44:34 +07:00
657f640ec7 A reconsidered operand must leave nothing behind, and a literal is never reconsidered 2026-09-20 19:30:04 +07:00
3e4267f57c Every no-implicit-widening comment now says what is true instead 2026-09-20 19:30:04 +07:00
0c50f34916 Widening happens at expect, and the wider operand decides a binary op 2026-09-20 19:30:04 +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
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
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
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
5ea6884d2c The diagnostics pass: every message shows, explains, and names the fix
# Conflicts:
#	FIX.org
2026-09-20 18:49:12 +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
d4def945a9 An array is a value you can write, not a place you have to fill first
DISCUSS.org's "need a value-producing array constructor": the author
wanted grid filled with 255 as part of its declaration and could not
write it. (array n T) produces the zeroed array only, and dotimes is
Unit, so it can mutate a place that already exists but cannot be the
initialiser expression -- which has to produce the whole value in one
go. The grid was declared zeroed and filled in main instead.

Two forms, both expressions, both any rank:

  (array-fill [rows cols] 255)   every element that value
  (array-gen  [rows cols] cell)  every element (cell i j)

Spelled apart rather than one form dispatching on the third element's
type, because an array *of* function values is a thing to want and one
form would have to decide whether (array-fill [4] f) meant four copies
of f or four calls of it.

The dimensions are read in Parse, and that is the whole reason they are
recognised there: handed through as an ordinary call, [rows cols] is an
array literal of two names, and where those names are defconsts it is a
perfectly good two-element array of integers -- the wrong reading, and a
silent one. Read in Parse they are the same len the [n T] type spelling
takes, resolved by the same array_len, with one extra condition of their
own: the fill counts in i32 like every index in the language, so a
dimension no i32 can reach has no loop that could end.

The lowering is a loop over a slot, not an aggregate. Tast.Arr is the
node the backends have and both build it element by element from a list
as long as the array; a fill of [600 [800 u8]] is half a million
elements and there is no list to be had. So these bind the array to a
slot, zero it, run one While per dimension writing through Set of a
Pindex, and answer with the slot -- While, Set and Pindex, which is the
argument check_loop already makes for recur. Nothing new reaches a
backend and all three get the form with no edit. The value stays
value-like: the slot is the form's own, and the Local at the end copies
out the way any array-typed expression does.

Row-major is pinned, not incidental: the first dimension is the
outermost loop, and a generator that counts observes it. The fill value
and the generator value are each bound once before any loop starts, so
(array-fill [n] (next-id)) is one call and n copies of its answer.

What falls out for the defvar the note was written about, and neither
half is a carve-out:

  (defvar grid [rows [cols u8]] (array-fill [rows cols] 255))

is the spelling that works -- a typed global with a computed
initialiser, which is the startup-lifted path with the init-once guard
that defvar already had, so the fill runs once and the value survives a
re-run like any other computed one. The three-element spelling means
what the 2026-09-20 rule says it means: not a type, so a dyn global, and
a typed fixed array crosses into dyn only as a view of storage that
outlives the view. A freshly built array is a temporary, so it is
refused -- by the element rule where the elements are themselves an
array, by the lifetime rule where they are one of the three scalars a
view carries. Both refusals are the ones any other temporary gets.

The type an array-fill builds never goes through resolve, so resolve's
own guard is asked again where it is built: a fixed array of function
values would be zeroed, and a zeroed function value is a null pointer.
2026-09-20 18:34:52 +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
b2d1df300b macros.flan says which of the two spellings it is 2026-09-20 18:23:22 +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