spec-memory.md defines an allocator as a procedure plus an opaque data
pointer, which reads as a function value, which check.ml refuses four ways.
None of the four is anywhere near this: `Allocator` is a `Types.t` case with
no user-writable constructor, the way `string` is a builtin ptr+len, its
procedure is a C symbol the emitter names, and every operation is an ordinary
named call that `check_call` already routes through `named_call`. The one
thing that really does need milestone 5 is a *user-written* allocator — it
wants a defn's name in value position — and that is refused by name with that
reason rather than left to come back as an unknown function.
An `Allocator` value is a pointer to the runtime's struct and never a copy of
one. That is forced, not chosen: the capability set has to be readable from
wherever a container landed, and `free-all` bumps an epoch every container
made from the allocator has to observe. A copy would give each its own epoch
and the dev trap would never fire.
Two decisions the spec left to be made here, both announced in BUILT.md:
`free-all` is retain-capacity — offset = 0, the pages stay — and handing the
pages back is `arena-destroy`, a separate operation. Zig's reset takes a mode;
Odin's arena_free_all is already retain-capacity in effect. Taking the mode
would have grown the operation table the spec froze at four. The epoch is
bumped either way, because the pages being the same does not make a container
made before the reset valid.
`context/allocator` and `context/temp` are dynamic variables with save and
restore, not extra parameters. The spec calls the allocator part of the
calling convention; the literal reading touches every signature, the FFI shim,
the dev trampolines and the reload ABI for the same observable behaviour.
`with-allocator` is its own IR node rather than a let and two calls, because
the restore has to happen on the transfer path too. A body that errors leaves
through the landing pad, and a context allocator left pointing into a region
nobody outside the body has heard of would be wrong in the break loop, which
is exactly where something is about to allocate to render a condition. The
acceptance program asserts that path by taking a restart out of a body.
The backend grew one prim, `Rt of string`: a call into the runtime's C named
by symbol, with argument and result types read off the expression nodes. The
container runtime is type-erased and therefore *is* a list of C entry points,
so one arm covers all of them rather than one arm each.
vendor/raylib has no C in it any more: shim.c is deleted and its 84 wrappers
are emitted from declare-c, which names the library's function in the library's
own signature. The reason the shim exists is unchanged - a small struct's
calling convention is a per-target classification and clang reproduces it for
free - but writing it by hand has stopped.
declare-c is a second form rather than a change to declare, because the two make
opposite claims about the same shape: (declare start-raw [path string] ...) says
the symbol takes ptr+len, and (declare-c init-window [... title string] ...)
says it takes a NUL-terminated char*. No structural rule separates them, so the
author says which.
The merge needed two fixes that neither lane could have found alone.
Load's uses-walker matches decl_kind exhaustively and did not know DeclareC, so
the reachability work and the generator did not compile together.
And the generated C is now emitted in parts keyed by the wrapper's own C symbol,
not as one translation unit. Reach.link drops the bindings nothing reachable
calls; a single TU holding every wrapper referenced every raylib symbol, so
sand-headless - which deliberately links no libraylib, and is the reason Reach
exists - failed at the link with undefined references to GetTime and its
neighbours. The first attempt keyed the parts by Flan name and broke the other
way, dropping a wrapper that was called: the flattened declaration is named
foo-c when a Flan wrapper is generated over it and foo when none is needed, so
the Flan name is not one thing. The wrapper's C symbol is what the declaration
binds in both branches.
Worth recording how close that came to passing: the acceptance suite died with
an exception rather than printing FAIL, so a grep for failures counted zero and
the suite looked green. Only the count of reporting suites - ten where there had
been eleven - showed it.
A package handed over its .c files and its `link` arguments the moment it was
imported, whatever the importing program did with it. That is what made sand's
two halves two files: anything naming vendor:raylib linked libraylib on every
target, and on wasm32 that link cannot succeed, so the headless run could not
so much as mention the package the interactive one needs.
Reach.link answers it from the checked program instead. Start at main and at
the globals that run before it, follow every call — including the Handled
frames, where a lifted handler clause is reached by address and by nothing
else — and keep what is reached. A package none of whose externs survive
contributes no C and no linker argument.
Dropping the flags alone would only move the failure: the bodies that called
into raylib would still be emitted, and wasm-ld would fail on the symbols
rather than on the argument. So the same walk prunes the functions and externs
too. Only those — globals, structs and unions stay, because an unreferenced
global is bytes in BSS and a dropped one is a silently different program.
Dev builds keep everything. What a REPL may redefine next is not a function of
what has been called so far.