The first is CLHS 9.1.4.1: a clause that signalled the condition it handles used to re-enter itself until the stack ran out. The stale list now names a lifted clause by the function it is written in, and BUILT.md's cost line is measured against master.
Removal marks a deleted slot unless no probe can have passed it, and a rebuild at the same capacity sweeps them. The header, the entry points and the iteration contract are unchanged. A Map has no bounds check, so the entry asking to convert one is closed with nothing to convert.
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.
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.
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.
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.
[flan_dev_reg_at] still went straight round its walk-level retries — a failed
scan_open and a failed scan_ok both back to the top. It is the one site where
the argument for leaving it holds water, since the agent gates the verb behind
a stopped program and there is no writer to lose to. It loses to the other
argument: it is the same mistake the slot read was making, three lines under
the note explaining why it is a mistake, and a bare retry left next to that
note teaches that the rule has exceptions it does not have.
And the cost of a refusal was written as a bound when it is a typical. "One
writer, so at most one slot of a walk is odd" is true at any instant and false
across a walk — flan_reg_compact writes every slot under its own counter, so a
repeatedly preempted writer can charge the full patience against several slots
of one walk, 4096 of them in the arithmetic worst case. Both docs now say
typical and give the worst, and record what the larger figure means for
request_lock, which is held across handle_line: a refusing listing delays an
abort by that much. Nothing depended on the old number.
Also written down, because it is the clearest statement of why this was not a
timeout: sixty-four bare re-reads of one word finish in about two microseconds,
so against a writer a millisecond from running the old budget was not short, it
was zero wall-clock.
test_reload's registry-under-a-writer case failed about one run in five on a
loaded machine and never on an idle one. The refusals were always unread=1:
one slot, on the best of eight walks, that would not copy. In that mode the
table cannot even move — re-noting live blocks kills nothing, the dead count
stays at zero, and the compaction trigger never fires — so the reader was
refusing a table that sat perfectly still for it.
flan_reg_snap re-read a slot's counter sixty-four times with nothing between
the tries. That is right for the writer it was written for, which holds the
counter odd for seven stores. It is hopeless against a writer the scheduler
took the core from mid-write, which holds it odd for a quantum: the reader
burns all sixty-four looks inside a fraction of that, and the looking is what
keeps the core the writer needs. Hence load-only.
The walk-level retry already had the answer written up at length — a spin takes
a core from the thread being waited on. The per-slot retry never got it. One
pause helper now, shared by both: eight bare looks for the running writer, then
the same 250us step. A refusal means the table would not hold still.
480 contended runs of regfull after, no refusals; 46 of 240 before.
as-slice was a warning, not an operation. The input type already decides
which of the two things happens — a Vec can only be borrowed, an array or a
string can only be viewed, and no call site picks between them — so the second
name expressed no choice a reader could make. And it warned at the moment the
view is taken, which is the one moment nothing is wrong; the danger arrives
later, at the push. slice now takes a Vec at all three arities and as-slice
is gone.
(slice v lo) was free, and is the arity the Vec never had: the runtime already
reads a hi of -1 as "to the end", so the tail form passes the caller's lo and
the same -1 — no slot, no length read, no second evaluation. The merge is
entirely in the checker; the Vec path builds the flan_vec_as_slice call it
always built and neither backend has a line about any of it.
A Vec a call returned is refused at every arity, and not for the array's
reason. (slice (mk)) over an array dangles. (slice (make-vec)) does not — the
storage outlives the expression — but the header is a temporary, so nothing
can ever free the block. The refusal says that and names the let.
The name's own refusal sits in ordinary_call after every table, so a program
that defines an as-slice still reaches its own. It reads for somebody who has
never heard of the old name and writes the call back out, spelling each
argument that is a name or a number.
The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.