Item 17 left two correctness items and a loose end. The loose end is the
answer to the first item: flan_vec_as_slice's Flan-level return type is Unit,
so is_void answers before is_agg is ever tested, and that is the convention
rather than one symbol's accident. So there was no sret-for-Rt to build, and
building one would have been untestable and wrong.
The second item is built and tested by the only thing that can test it, a
preloaded dlsym, because a dev build with nothing yet redefined prints what a
release build prints whether or not anything reads a cell.
Survey: 93 MATCH / 0 DIFFER / 2 refused before, 97 / 0 / 0 after; 97 / 0 / 0
again with --dev on both sides.
Four raylib fixtures fail, the same four on two consecutive runs:
images and audio, each at -O2 and -O0. They export to a hardcoded
/tmp path and get "Failed to export wave data" because /tmp is full,
and /tmp is full because of a runaway llc in another lane writing a
5.8 GB m4.o out of a 13 KB m4.ll. Nothing here writes to /tmp by a
fixed name and TMPDIR does not reach those fixtures, so item 17 says
that rather than claiming an unqualified green.
The transfer exit returned whatever the return temporary held where
emit.ml returns zero. Meaningless to a caller -- its guard sees the
channel set and never looks -- but main is a caller with no guard, and
what it finds in rax is the process exit status.
The survey compares stderr as well now, which is where every message
the new machinery produces goes: the bounds and slice errors, the
three restart refusals, the transfer failure. Each carries a location
this backend emits by hand as a .rodata label and a length in a
register, and an exit status of 134 with the wrong text beside it is
exactly the failure that reads as a match. It also walks spike/x86's
own probes.
p6-transfer.flan is the two re-propagation branches the corpus does
not reach. Every transfer in restarts.flan stops at a restart-case
inside the handler-bind's extent, so the handler frames never come off
on the transfer path; and in nested and shadowed the inner frame
offers the name, so a restart-case the transfer is not aimed at never
has to put the target back. allocators.flan already covers the third.
89 MATCH 0 DIFFER 0 refused, over test/programs and spike/x86,
comparing stdout, stderr and the exit status.
DISCUSS.md item 17 is the report.
A consistency sweep, run by checking claims against the compiler rather
than reading for style. Every edit here corrects something that is false
now, or adds something the page had no way to say.
`web/index.html` was the worst of it, and the worst of that was control
flow: the page said there is no `loop`/`recur` and no `break` or
`continue`, and printed the refusal message for `break` as evidence. All
four are built, with loop labels. A live code example called
`index-of-i32`, which no longer exists and would not compile. The prelude
table was the pre-generics per-type families, a paragraph said there is
no `println` two paragraphs after calling `println` the compiler's, and
`sqrt-f32` was "the one `declare` in the file" when there are five. The
"Not implemented yet" table listed `Vec`, `Map`, `Handle`, `Fn`, `fn`,
unions, `defmacro` and quasiquote, all of which check; what is actually
left is `Result`, `try`, a quoted symbol as a value, `errdefer`, `await`,
`handler-case` and the restart-stack readers. Restarts take parameters
(`(invoke-restart 'use-value 21)` answers 42), `defer` in a `let` is
allowed, and there is both an allocator and a `context`.
Generics is a new section, because nobody had documented the syntax. The
brief for it was wrong in one place and the corpus settled it: `$t` goes
in *every* type position including the return type, and bare `t` is the
type-name argument in expression position — `(vec-new t)`, `(t x)`. It
says what a type variable is move-only by default means, since that is
the rule a reader hits first and it is not Odin's.
`FLAN_RAYLIB_H` is gone from every doc that claimed it still decided
something. The passages that say "this used to be opt-in" are kept and
labelled; the ones that said "this is opt-in" are not. `plan.org` had
`{string i32}` in the type list and four predicates where there are five.
`conditions.org` described `errdefer`'s behaviour without saying it is
refused by name. `REFERENCES.md` pointed at the gitignored copy of the
raylib header rather than the committed one, which is the exact trap that
made committing it necessary.
Found and not fixed, because it is not documentation: `vendor/raylib/headers`
still says a build reads it "when the variable happens to be set", which
contradicts the section below it in the same file and is false — moving
the header makes every build fail by name.
DISCUSS.md item 16, in item 15's form.
The headline is a measurement that contradicts what everyone expected,
this report included: a program that loops and prints carries no Signal,
no Handled and no RestartCase. Zero of each, measured with hist.ml before
a line was written. What drags conditions in is the bounds check and the
allocator — check_at signals, and signalling needs the channel and the
guard — and neither is a Tast node, which is why a histogram cannot see
either and why the guess was off.
41 of the 111 programs in test/programs build through --x86 and 40 print
exactly what the LLVM build prints. The 41st is bounds.flan and it
diverges on purpose: --x86 is silently a --no-bounds-checks build, and
that is named as a divergence rather than left in a code comment.
The other 40 are refused by name. check_no_transfer is what makes the
missing guard sound instead of hopeful — if nothing reachable can write
the channel, no call can return with it set — so the build stops with the
node and the function rather than compiling forty programs wrongly.
Both bugs found along the way are the shape item 15 predicted: the crash
and the cause in different functions, and correct-reading assembly beside
a wrong answer. The frame model is the thing that did not go wrong.
The same three shapes written in C and as first-class IR aggregates,
compiled by the same clang. { i8, i64 } agrees. { i8, float } does not: C
packs both halves into rax, the IR form answers in al and xmm0. And a
24-byte struct does not agree at all -- C spills through an sret pointer,
the IR form returns it in rax, rdx and rcx, and rcx is a register SysV never
uses for a return value.
That resolves the ret-big anomaly the first pass noted and moved past, and
it makes the finding stronger than it was written: the internal convention
is not the C ABI, not only undocumented in the emitted IR.
spike_call0 deleted with it -- declared, never bound, and the two unused
probes were removed for the same reason.
The verdict, as DISCUSS.md item 15. One function goes from Tast to machine
code and answers correctly, so the question is not whether it can be done.
Three findings decide the shape. Layout is already owned -- emit.ml computes
C struct layout for DWARF and is tested against LLVM's own answer -- so the
silent-drift risk item 10 feared most does not arise. The C boundary is the
easy half, because check.ml already rejects aggregates in a declare and the
shim flattens them. And the hard half was not on anyone's list: Flan calling
Flan passes aggregates by value, and LLVM's lowering of a first-class struct
is per-field rather than the C psABI -- { i8, float } comes back in al and
xmm0 where C would pack it into rax, and a %vec return takes a hidden sret
pointer that does not appear in the define line. The internal convention is
an implementation, not a document.
The audit stands on its own: overflow, shifts and evaluation order are
defined; division by zero, INT64_MIN/-1, the float cast, Uninit and
unreachable are not. Uninit is the one that bites, because poison is where
the two backends are supposed to differ.
Unloading: the shadow stack answers the running half and every dev-build
function is on it -- only the slot table is gated, not the frame. It cannot
answer the pointed-into half, which BUILT.md says is the actual reason
nothing is dlclose'd. Escaped function values need a rule the language does
not have.
web/index.html had no section on the FFI's generated half at all; it has one
now, with the command, the config, and the reason committing the output is what
makes the no-header property honest rather than a caveat.
BUILT.md gets why the 172 stay, which is the part that is easy to get wrong:
136 of them are exactly what the rule produces and the rest are expressible as
overrides, so the superset argument is sound and still leads somewhere bad —
deleting them reduces the signature check to a tautology.
DISCUSS.md 6a and 6b are answered rather than left open, and 6b's own point
about enums turns out to be live in the tree: key-down? keeps its Key
parameter because it is hand-written, and the generated key-up? beside it
takes an i32.
The capability lists were written before the code held the line they claim.
Under an expression root, RET on a field of a union built `(.at s)' and sent
it, and the checker refused it — "a union's fields belong to a case ... they
are reached by (match ...)". A refusal from the far end of a socket is exactly
what this buffer's own comment says not to do: every refusal is by name, here,
with the reason, because RET working on some lines and erroring on others
teaches nothing about the language.
It is a refusal of the *parent* and not of the value at point, which is why it
is not in `flan-inspect-refusal': a struct field that merely holds a union is
an ordinary accessor and has to stay enterable. It is a field of the union
itself that cannot be written. The two cases are one test each.
The slot root steps into it by offset and is unaffected, which is the
difference the manual now claims and the tests now show.
`lib/dev.ml' cited DISCUSS.md item 1 as a hole; item 1 is the answer now, so
it cites BUILT.md instead. And the item 1 stub is two sentences and a pointer
— everything else in it is in BUILT.md verbatim, and DISCUSS.md's own header
says nothing in it is a decision.
The daemon side and the Emacs side both landed with nothing written down. Four
files owed something.
`BUILT.md` gets the whole of it: why rooting at an address alone was rejected
and why that rejection was half wrong, what a path step is and how a union's
case travels with it, why the slot goes by index and not by name, and the two
capability lists side by side — the expression root works on a running program
and cannot name a frame; the slot root names one frame and one slot and reaches
an option's payload and a union case's fields, and needs the program stopped.
Neither contains the other, which is the reason there are two.
`emacs/MANUAL.md` says the same thing in the register that file uses, under the
inspector, because the person pressing `i` is the one who needs to know which
root they got and what it cannot do. The globals section's claim that `i` works
on a global "exactly as it does on a local" was true and is now the interesting
difference, so it says what the difference is.
`NEXT.md`'s decided item is struck with what actually shipped: a frame and a
slot index rather than an address and a type, and `l` crossing between the modes
was predicted as a cost and turned out not to be one.
`DISCUSS.md` item 1 is no longer an open question. The number stays — cimport.ml
and NEXT.md cite these by number — and what stays with it is the one correction
worth keeping: an address is not an expression, but a step does not have to be
one either.
And BUILT.md's last paragraph still said `render.ml` prints `(V {:x 1.5})` and
that the printer would move when its reader did. They moved together some time
ago.
Item 12 asked five questions. All five come back clean, and the answer is
feasible with no obstacle that argues for porting the compiler.
The one that mattered most was signals, and it turned out to be a non-question:
OCaml 5.2 on Linux/amd64 installs no signal handlers at all -- fifteen swept at
four moments, every one SIG_DFL, and a plain ocamlopt executable behaves the
same, so embedding changes nothing. OCaml 5 checks the stack limit explicitly
instead of arming a guard page, so the SIGSEGV the break loop wants was never
taken. The break loop can have it outright and Stack_overflow still works. That
is measured on one platform only, and the entry says so: macOS/arm64 is the one
claim here that must be re-run rather than trusted.
The rest: -output-complete-obj links with dune uninvolved and the existing C
stubs intact, no symbol collides across the four .c files and libasmrun, the
game keeps the main thread while a C-created listener calls into OCaml after
caml_c_thread_register, and a compaction left an 8 MiB arena byte-for-byte
untouched. Startup is 0.6ms and the compiler adds 4.14MB -- which makes the
merged dev build smaller than today's daemon alone, and there is one of it
rather than two.
Written as item 14 with the numbers, the three ways the GC assumption would
break, what the spike does not cover, and the order the real work goes in. The
backend is left exactly where item 13 put it.
BUILT.md gains "The header is read now", directly under the section whose last
paragraph promised that reading a header was what would convert the trusted
half into a checked one and that it was not built. That sentence is replaced by
a pointer to the one below it, in BUILT.md and in shim.ml's docstring both.
It records the things worth not re-deriving: why the dump and not libclang (and
that Zig left libclang too, which strengthens the argument rather than weakening
it), why the import is bounded by the package's own defstructs, why generating
defstructs would make the check circular in exactly the way a _Static_assert
was rejected for, refusal-by-demotion from Zig's failDecl, the naming rule and
what it must actually guarantee, and both const-vs-non-const char * and the
target-varying widths.
The diff and the costs are stated as measurements, with the table: 16 of 16
defstructs and 172 of 172 declare-c agree against 5.5, ten real differences
against 5.1-dev, release +4ms warm, redefinition 31.0 -> 46.5ms.
DISCUSS.md item 6 is rewritten rather than removed. The mechanism question is
settled and is now in BUILT.md; what is left is narrower and is two decisions
that are the author's — whether the header stays a build-time read or becomes a
committed generator, and whether the 172 hand-written lines migrate. Both have
the argument on each side written out, including what migration would lose:
key-pressed? is a better name than is-key-pressed, and an enum parameter
imports as i32 because nothing tells the importer the package calls KeyboardKey
"Key".