96 Commits

Author SHA1 Message Date
9d6694fd92 Merge master: an evaluated expression's lifted bodies are placed with its thunk 2026-09-25 13:18:40 +07:00
d864818956 A handler for a parent reads the condition's own name and message or the condition printed with its values, kept in the handler-case's own frame across the unwind, and a frame re-entered through a signal or a C call names where it is 2026-09-25 13:16:35 +07:00
9ba99d7d5b A dev build puts every closure handed to a named call on the collector's heap, since a redefinition can make the callee keep it 2026-09-25 12:55:56 +07:00
bfd176cce9 Merge master: a handler clause runs without its own handler in force, over the condition descriptor, and StaleCall is an Error 2026-09-25 12:50:01 +07:00
23b144178a A dev build's frame records the call it is in, so a backtrace names the call each caller is making and the expression the innermost one stopped at 2026-09-25 12:12:40 +07:00
c6cb018b9f Only a closure that outlives its frame takes a collector environment, and the collector reads a Vec's elements only through a block it knows is live 2026-09-25 12:12:25 +07:00
73dfaacbc8 A condition type may name a parent and a handler for Error catches every built-in error, a signal passes one descriptor carrying its chain, name, sentence and site, and the break loop shows the sentence the runtime wrote 2026-09-25 12:05:13 +07:00
3892f1ed34 Merge master 2026-09-25 11:50:29 +07:00
680c12e686 A restart frame carries where its clause is written, its :report sentence and whether a handler-case made it up, and the break loop shows the first two and hides the third 2026-09-25 11:35:12 +07:00
97a2795cb2 A module that makes a closure stays mapped, and a closure held beside a collecting operand is rooted 2026-09-25 11:24:34 +07:00
14da41e1d9 A changed signature installs, its stale callers are named, and a handler is off while its own clause runs 2026-09-25 11:17:48 +07:00
389e93ff09 A capturing fn's environment belongs to the collector, so a closure may outlive the frame that made it 2026-09-25 11:13:57 +07:00
40bd96ec65 A source heading escapes control characters, names its file by the path it was given, and quotes a form cut at a character boundary 2026-09-25 11:08:11 +07:00
0d771494a6 Merge branch 'master' into worktree-agent-a8f5ad274482eeb9c 2026-09-25 11:07:02 +07:00
573076994e Merge branch 'master' into worktree-agent-a0a469774e7454c6c
# Conflicts:
#	lib/loc.ml
#	lib/x86.ml
2026-09-25 10:59:24 +07:00
0beafedd14 Dead code is gone, and the daemon says what to fix when a session cannot start or reach its program 2026-09-25 10:45:17 +07:00
39d35f51db A function whose signature changed installs, and a caller compiled against the old one stops on StaleCall at the call
A dev cell carries its body's signature word beside the body, every call through a cell (and every function value taken from one) compares it with the word the site was compiled for, and the session lists the stale callers by file and line on the reply. Both backends, both installers; release builds have neither the word nor the compare.
2026-09-25 10:40:18 +07:00
066fb18940 Every Flan form heads the code it produced in the emitted IR, the x86 listing and both disassembly buffers, and the objects are the same with or without it 2026-09-25 10:34:07 +07:00
cf04bfc8fd A u64 converts to and from a float on x86 as it does under LLVM, across the top half of its range 2026-09-25 10:21:23 +07:00
e511174d3c No top-level value in the compiler goes unreferenced 2026-09-25 10:16:25 +07:00
96be1193c0 The x86 backend describes every frame it emits for an unwinder, in a release build as in a debug one 2026-09-25 10:07:40 +07:00
795ba945d9 A dyn value held beside a sibling that can allocate is rooted until it is used, on every backend 2026-09-25 09:32:34 +07:00
00f116ea29 Six small refusals and conversions say what the program wrote 2026-09-25 08:41:31 +07:00
6f0f4957f4 The break buffer, the inspector and the watch table show what a stopped program holds 2026-09-25 08:37:15 +07:00
ff744120fc A release build's watch is only its value, next-error lets go of a stack that has resumed, and the frame a stop is in is always shown 2026-09-25 08:36:53 +07:00
0653f341d0 A dyn value held in an operand list is rooted while a sibling operand may allocate 2026-09-25 08:36:34 +07:00
9194f918fc An aggregate that reads its own destination sees the old value, on x86 too
x86 built a settling right-hand side straight into its destination, so (set p (P {.a (.b p) .b (.a p)})) wrote .a and read it back for .b. assign now also asks whether the value reads storage the destination lies in, and goes through the temporary when it does. The global-initialiser entry was already closed by startup_plan's Set; the unrooted-temporary entry turns out to be a both-backends rooting gap and stays open for a decision.
2026-09-25 07:05:18 +07:00
6b9d1fa644 A cast of NaN or an infinity to an integer says so by name, on both backends 2026-09-25 06:58:06 +07:00
57fe91f303 Five records become one, and every citation lands somewhere
FIX.org, NEXT.md, DISCUSS.org, docs/DISCUSS.md and the session handoff at the
root are one TODO.org now: 293 entries under seven subsystem headings, each
carrying an org keyword that says where it stands. A DONE entry is a few lines
saying what was decided and what that rules out; the reasoning that would not
compress — the embedding spike and the four reports the hand-written x86
backend was built from — moved into docs/BUILT.md instead, and its entries
point there in one line.

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

Every comment that cited one of the five by name now cites a heading that
exists, in TODO.org or in docs/BUILT.md. The session reports under
docs/handoffs/ keep naming the files they worked on, because rewriting them
would falsify what those sessions did; each carries a note saying where the
content went.
2026-09-21 21:05:48 +07:00
d9bb882bbe A dev build's main is reached through its cell like every other call
# Conflicts:
#	FIX.org
#	test/test_dev.ml
2026-09-21 18:29:02 +07:00
a28aeda633 The thunk memo was keyed on a name that two signatures can share
Review found it, and it is a silent miscompile on both backends rather
than a refusal anywhere.  mangle_ty flattens a whole signature into one
hyphen-joined string, so (CFn [(Ptr i32)] i32) and (CFn [ptr i32] i32) --
the second over a struct someone called ptr -- flatten alike; keyed on
that, the second widening reused the first's thunk at the wrong arity.
The key is the types now, compared with Types.equal, and the symbol is a
counter over the thunks already minted, so nothing is derived from a
spelling.  fn-thunk-share.flan is the pair, and it prints 5 and 17.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
2026-09-21 11:58:56 +07:00
3672da28be A diagnostic is for someone who has only this compiler, and says what to write 2026-09-21 11:44:59 +07:00
0c03550999 Evaluating a def assigns, because that is what defparameter means
The author edited (def colors [4 u32] [...]) in his running game, pressed
C-c C-c, and the colours did not change — the same complaint def was built
to answer, one step further in.

The reading behind it was that a re-evaluated def is a promise about the
next re-run, so the session republished the lifted global/<n> and stopped;
nothing called it. That is wrong for the reason the form is named after: def
is Common Lisp's defparameter, and evaluating a defparameter assigns. The
difference from defvar is not "one takes effect at restart", it is "one
takes effect, the other does not touch the value at all".

So a def now does both. The storage takes the new value at the next frame
boundary, carried by the thunk a redefinition module already has — one
Set per re-evaluated def, in the same flan_reload_call the class
registrations use, run after the bodies are published and on the game
thread. And the lifted initialiser is still republished, so the next re-run
runs the edited one; dev-rerun.flan pins that half unchanged.

A brand-new def gets its initialiser run too, which needed one thing from
each backend: a lifted global/<n> asked for by name is neither a sibling nor
one of the target's own lifted clauses, so it had no cell, and a dev call
goes through a cell. Both now give an unknown one a slot of the module's
own, filled from the registry by the installer.

An initialiser that signals leaves the old value alone — the value is
computed whole before it is stored — and offers abandon-evaluation like any
other thunk. A retype is refused first, by the pass that names both types.
defonce is untouched, which is its whole contract; defconst was already the
immediate one, through consts.
2026-09-21 11:03:30 +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
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
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
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
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
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
Joseph Ferano
2dfab38d06 A dev build's main is reached through its cell like every other call
The entry point was the one call site a redefinition could not reach. A dev
build gives every Flan function an indirection cell and routes every call
through it, which is what makes C-c C-c land on the sites that already exist;
the emitted C main called the Flan-level main by symbol instead. So
flan_program_main — what M-x flan-rerun re-enters — ran the body main had at
the initial build for the life of the process, and redefining main compiled,
installed, reported ok and changed nothing anyone could see.

Both backends had the same direct call and both get the same split. LLVM loads
the cell and calls the loaded pointer; x86 does what call_flan's `Cell target
does, one load because emit_cells defines the cell in this same object.
Release builds keep the symbol, so emit and emit --x86 are byte-identical to
what they were. The cell is initialised to the body this build compiled, so
the first run is the run it always was.

What is left is an off-by-one that belongs to lib/dev.ml: a body delivered
while parked installs at the next frame boundary, which a re-run reaches
inside main, so a generation runs one re-run later than the key press. FIX.org
has the one-call close and why it is not done here.
2026-09-20 17:57:26 +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
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
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
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
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
7db5ec1885 Float % on x86 is a libcall, and the parity ruling is written down 2026-09-20 11:33:26 +07:00