A defer is in the typed IR twice -- spliced into the body for the normal path,
and again in fdefers for the path a transfer leaves through -- so a dyn
temporary inside one is emitted twice. dyn_roots counted only the body's, and
the second copy went into slots nothing had rooted.
Nothing failed, and that is the whole reason this is worth a commit of its own.
dyn_tmp falls back to a plain slot rather than unbalancing the stack, so the
pushes and the pops still matched, the program ran and printed the right answer,
and the values were simply invisible. Against a stub that never collects there is
no symptom to find -- no leak, no crash, no wrong number. It would have become a
symptom the week the real collector landed, in a defer reached only on a handled
condition, which is close to the worst place to start looking.
What found it was the IR: a rooted slot is spelled %dr and the fallback %dx, and
the assertion is that no dyn program in the corpus emits one of the latter. That
is now a test over all five dyn programs, and it is the only check in the lane
that can see a missing root while there is still nothing to lose one by. When
the collector arrives it is the thing to extend rather than replace.
Also checked, both clean: flan dev --llvm builds and runs a dyn program, which
is the route the x86 refusal sends people to and would have been a link error in
the worst possible place; and the daemon's own refusal already names the flag.
Two decisions in this lane went against the brief and both are written down
where the next reader will meet them: the return slot stayed mandatory, so the
third state ret = None was to grow does not exist and neither does the fallout
listed for load.ml, shim.ml and cimport.ml; and the parameter rule is resolved in
Check rather than in parse.ml, because cimport passes C type names through
verbatim and POSIX's lowercase stat and timespec are writable in parameter
position, which is what makes a syntactic rule unsound rather than merely
awkward.
The open ABI point is in flan_dyn.h beside the root functions rather than only
in the handoff, because the header is what the two sides diff. A rooted slot
holding 0 is not a value: the compiler zeroes every root at entry because the
push happens before the code that fills it and possibly for a branch that never
runs, and 0 is the only pattern it can write without knowing the encoding. If
the real runtime NaN-boxes and integer zero is the zero word then this is wrong
and both sides change together.
Session.compatible needed nothing: it compares with Types.equal over the
parameters and the return, and dyn is equal to itself and to nothing else. Both
directions are pinned anyway, because this is the one place "changes signature"
covers a change the source does not spell out -- a parameter can become dyn, or
stop being dyn, by a type being declared elsewhere in the program.
@x86 128 match 0 differ 0 refused, @sanitize clean, dune test green.
Emit.program ~hidden and ~dev are opposites: a dev build exports its cells,
globals and flan.abi.* so that a redefinition module can bind against them, and
hiding those would give a host that links, runs and silently installs nothing.
Nothing in the types stopped the combination being written, so it fails with a
sentence instead. Build.macro_module is the only caller that asks for hidden and
it never asks for dev.
The handoff's verification is narrowed to what it actually covers: the 540-file
IR diff is about Emit.program's default, and the new path is read off nm -D on
the linked module. It also now says why sand-headless.flan is the fixture that
checks 65d14f4 -- it reaches rl/with-drawing through an import, which is the
package-macro shape the reproduced bug had.
flan dev's merged build is the program and the compiler in one -rdynamic
executable, so it exports every flan.* body it has, and ELF gives it precedence
over anything dlopened afterwards. The compiler expands a macro by dlopening a
module into that same process, and the module is built by Emit.program whatever
backend the session uses -- so under --x86 the caller was LLVM's and the body it
landed in was the dev backend's, which is a crossed pair. It died with SIGSEGV
inside flan.[clamp] during the first expansion, before the program had run a
line, and Dev.start refused the combination rather than do that.
Build.macro_module now asks Emit.program for hidden visibility on the module's
own Flan definitions. There is nothing left for the host to interpose, and the
flan.macro.* thunks stay exported because dlsym is how the compiler reaches
them -- nm -D on the built module lists those three and nothing else of Flan's.
The -Wl,-Bsymbolic that had been binding everything locally since 65d14f4 goes
with it: the module links its own flan_rt.c, and binding that locally aimed its
calls at a runtime flan_rt_init never ran on, with a null flan_exit_hook, so a
trap raised inside an expansion would have exited the process instead of parking
it.
Nothing about the host moved, which is what keeps redefinition modules reaching
its cells, its globals and flan_dev_cell. hidden defaults to false, and the 540
IR files this compiler emits for the test corpus are byte-identical to the ones
before it.
test_dev.ml's assertion that the merged daemon refuses --x86 becomes the session
it was standing in for: dev-macro.flan calls a prelude macro at the top level,
so the daemon coming up at all is the old crash not happening, and one build
then carries C-x C-e, a C-c C-c whose body calls a macro again, the park and the
rerun.
`flan emit --x86` printed a three-line header and then nothing but .byte
blobs. The information was all there and none of it was written down.
Each run of bytes is now headed by the Flan form that produced it, with the
position it was written at, indented by how deeply the form nests. The
headings are queued rather than written, so a form that emits nothing does
not leave its heading on the next form's bytes; atoms queue none at all,
because a literal operand would otherwise steal the heading standing above
the imul that consumes it.
Above each function is a frame map, which is the half no disassembly
recovers: every value in this backend lives in a frame temporary, so
-0x20(%rbp) is the whole vocabulary of the listing and nothing says what it
means. It is read out of what emit_fn already keeps, so it cannot drift.
Beside it, where the arguments arrived and whether there is a hidden sret.
And the bookkeeping is named where it appears -- the transfer guard, the
bounds triple, the arithmetic guards, rep movsb, the dev indirection cell --
with each explained once in a legend at the top rather than at every site.
Always on for `emit --x86`, which exists to be read, and never for a build,
whose .s is a temp file handed to clang. spike/x86/annot.sh is the check that
this costs no byte: emit both ways, assemble both, compare every section.
342 SAME / 0 DIFFER over the corpus in default, --dev and --debug. dump.sh
now shows the annotated listing beside objdump's disassembly -- why beside
what, which is the pairing that answers the mnemonics question.
survey.sh has not been run on this; see the handoff.
The third silent failure was waiting, and it was the September 12th one again:
web/index.html showed the value renderer spelling struct fields with colons, two
days after the sweep that made them dots everywhere else. The check that should
have said so was anchored in NEXT.md, which is a scratch document, so it had been
reporting 'whatever this quotes has moved' into a report nobody could read.
README gains a Checking it section: the four commands, what each one means, and
the plain statement that nothing runs @checks for you. The convention it proposes
is the one this repository already has -- a lane's handoff quotes its counts.