Joseph Ferano 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
..

What is in here, and what is still true

These are the documents that are written once and read occasionally: the reasons behind the code, the reports from finished investigations, and the record of individual work sessions. The documents that are edited every day stay at the repository root, because source comments cite them by bare filename from dozens of places and a path that moves is a path that rots. So plan.org, NEXT.md, spec-memory.md and spec-conditions.md are one directory up, and everything here points back at them.

A note on how to read the citations below: a document in this directory that says plan.org means the one at the repository root. Nothing in docs/ is named for a file at the root, so there is no ambiguity, and rewriting a hundred prose mentions into ../plan.org would have cost more in readability than it bought in precision.

If you have thirty seconds

Read BUILT.md. It is by far the largest document here and it is the one that pays: it holds the reason behind every part of the compiler that exists, from why nothing is ever dlclosed to why the printer is a compile-time walk over a type rather than a runtime function. It is current, it is maintained, and deleting it would mean deriving all of it again. If you want to know why a thing is the shape it is, the answer is in here.

After that, ../NEXT.md at the root for what is actually in flight, and REFERENCES.md here for where the evidence comes from — the reference clones on this machine, what each one answers, and the standing rule that a claim in these notes was read out of a clone rather than recalled.

The current documents

File What it is
BUILT.md Why the parts that exist are shaped the way they are. The longest and the most load-bearing document in the repository. Current.
DISCUSS.md Open questions, raised and deliberately not answered. Nothing in it is a decision or a task; entries that have since been answered say so and point at where the answer landed. Current, though it is half archive by now.
PORTING.md What the author's game, siam-farmer, needs from Flan that Flan does not have yet, measured against the code that exists rather than against a plan. It is the requirements document the language is actually steering by. Current.
REFERENCES.md The reference clones under ~/Repositories, what each one is consulted for, and the specific files and lines already cited from them. Current.

The reports

File What it is
SPIKE-GENERICS.md Milestone 5's parametric polymorphism, run early and out of order as a spike. The spike succeeded and generics landed, so this is the report of finished work rather than a live plan — but its findings about where the cost falls are still the reason the implementation looks the way it does. Historical, findings still stand.
overview.md The first brainstorm, from before the language had S-expressions. It says in its own first line that it is superseded and no longer accurate. Kept for history only; its table of what changed is the only part worth reading. Superseded.

handoffs/

One report per work session, each written by whoever held the branch at the time. They are the operational record: what was attempted, what was measured, what was decided without being able to ask, and what was left open for the next lane. They are all historical the moment they are written — a handoff describes a session that has ended — but they are cited by name from source comments and from NEXT.md, because the reasoning behind a guard or a calling convention is often only written down once and this is where it was written.

Six of them are the hand-written x86-64 backend, read in the order the work happened: handoffs/HANDOFF-x86-rt.md set the remaining-items list that the four after it close, handoffs/HANDOFF-x86-redef.md built the redefinition emitter, handoffs/HANDOFF-x86-aggregates.md took that across the struct boundary, handoffs/HANDOFF-x86-guards.md settled the two guards nothing reaches, handoffs/HANDOFF-x86-debug.md added debug information, and handoffs/HANDOFF-x86-cost.md measured what the backend costs and set the survey running on its own so a refusal cannot sit unnoticed again.

The other six sessions are unrelated to each other. handoffs/HANDOFF-arith.md is why a divide by zero is a condition rather than a SIGFPE. handoffs/HANDOFF-raylib-ports.md is the running record of the last two raylib example ports, whose lasting findings were folded into PORTING.md, and handoffs/HANDOFF-cimport-ptr.md is the arm cimport.ml's header check had been promising in a comment and not implementing — which is what those two ports found, worked around and wrote up. handoffs/HANDOFF-devtest-noise.md is the linker error dune test used to print on every run. handoffs/HANDOFF-emacs-flake.md is the test_emacs flake that turned out to be SIGPIPE killing the daemon mid-reply, and it is worth reading for the two mechanisms it rules out as much as for the one it found. handoffs/HANDOFF-tidy.md is this reorganisation.