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.
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.
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.
(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.
The louder failure had the quieter answer. A generated reader accumulates
errors on the cursor — which is what lets it be a straight line of
assignments — and the cursor is made and dropped inside the entry point, so a
stray brace in a file read at run time handed the program a zeroed struct and
said nothing at all. That is the one thing the rest of vendor:edn refuses to
do: read-file answers an Option precisely so a malformed document is
distinguishable from one that is literally nil, and the hand-written reader in
test/programs/edn.flan tests ok? and prints the reason. ReadFailed is the
derived reader being as honest, in both packages, and it sits beside
SchemaDrift because both are "the file is not what this program was built for".
Then the writing-down. docs/BUILT.md gets the section: the four things the
macro system did not have and now does, each general and none of them
mentioning EDN — a macro reading a file at the call site's path, a package
macro calling its package, one call answering several declarations, and
compile-error, which is the one piece that had to go in the compiler and the
reason it had to. The set rule the real game file decided is there too, and
what defjson shares, which is the design and not the code.
NEXT.md item 9 and PORTING.md §3.9 both close. Not as (read-edn T bytes): the
struct comes from the *file* rather than from a type declared by hand, so the
~80 lines PORTING prices for two schemas are not written at all. The competing
answer PORTING names — compile-time embedding — turned out to be the other half
rather than a competitor: the shape comes from the file at compile time and the
bytes may come from an embed beside it, which is exactly what
test/programs/edn-provide.flan does.
edn-provide.flan reads assets/edn/tileset.edn through a struct derived from it,
and its first five lines are edn-read.flan's first five character for character.
Two readers over one file agreeing is what says the derived one is right; either
alone could be self-consistently wrong. The pair memberships are the derivation
deciding in public: the set became a (Map [2 i64] bool), so [3 4] is a key and
[9 9] is not, where a version that made it 108 loose integers would have
compiled and answered differently on all four.
A tuning file beside it covers the rest of the matrix — a string, an integer, a
float, a boolean, a vector summed rather than counted, and a map inside a map
read two field loads deep — and then drift: the struct was derived from a file
with :speed and without :level, and the bytes read carry the opposite. Both are
named, and the read carries on.
The refusals write their own data file, because the data file is the test. Each
is asserted on the position it names, not on the fact of failing, and one of
them checks a line and column into a file the compiler is not reading — which
is the whole of what compile-error was added for.
Two things the tests caught. Load extends the ambient macro set rather than
replacing it, so a package reached twice handed its declarations over twice and
the module refused them as a redefinition; Macro.compile dedupes by name, which
is the rule macro_union already applies a level up. And `where` held a line and
a column at once, which the prelude's note over append-i64 says cannot be done:
i64->bytes renders into one shared static buffer, and both numbers read as the
second one.
(edn/defedn Tileset "assets/tileset.edn") reads the file while the program is
being compiled, derives the struct its shape implies, and emits that struct
with a reader over the tokenizer next door. From there (.texture-path data) is
a field load: no Value, no match, no runtime tag, nothing looked up by name.
The real game file is the case it was built against, and its set of [x y] pairs
is why a vector inside a set becomes a fixed array rather than a Vec — a set is
this repo's (Map T bool), so its elements are map keys, and [2 i64] is one
where (Vec i64) is not.
Two things found while writing it. A quasiquote inside a package's ordinary
function was not qualified — only a defmacro's body goes through
qualify_macro — so a (defn ... [c (Ptr Cursor)] ...) emitted from a derivation
helper reached the importer naming a type it had never heard of. The name
survives into a string, which is the property the expander depends on and
exactly what puts it out of a rename's reach; load.ml now qualifies a literal
(Form.Sym {.s "..."}) naming something the package owns. Until a package's
macros could call the package's functions there was no helper that built code,
so this could not have shown before.
And (vec-new) and (map-new) have to be told what they build by naming a type,
which (Vec i64) and [2 i64] have no way to be. Both fall back to what the
context wants and a signature is a type position, so each collection gets a
one-line constructor stating its type. The reader reads better for it.