Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.
The R5 “stone drop” — there was no drop
Written while landing lane “exports” (B2a/B2b/B2c/F2) on arc/module-exports.
The compiler-emitted module freeze works. What looked for most of a session
like a runtime stone drop was an observation error: the probes ran through
the daemon, and the daemon’s realize lane compiles with the SEEDED compiler out
of boot.qop, not the working tree. Keep this file for the gotcha, not for the
bug.
THE GOTCHA — read this first
./pit some_script.ce does not exercise your edited compiler. The daemon’s
realize lane compiles the modules a started actor uses with the seeded compiler
embedded in boot.qop. Edit pit-compiler/, run a script through the daemon,
and you are watching the old compiler’s output until the batch reseed.
What DOES see the working tree: ./pit test run …, ./pit test suite,
./pit fuzz, pit compile … — anything that use()s the compiler modules
directly rather than going through realize.
This is the second lane to lose real time to it (the literals lane hit it too
and wrote it down). It belongs in carried.md, because the failure mode is
maximally deceptive: the toolchain is correct at every stage you can inspect,
and the running program still shows the old behaviour. Everything in the
table below passed while the daemon probe kept failing.
Why it was so convincing here
The control case made it look like a genuine, narrow bug rather than a stale compiler:
- A module written
return stone(r)did disrupt on a cross-module write. - A module relying on the compiler’s freeze did not.
Both lanes agree on the first, because the old compiler compiles a
source-level stone(...) call perfectly well. Only the new emission was
missing, and only on the daemon lane. So the discrepancy pointed at “the
emitted op specifically is being dropped” when it actually meant “the emitted
op is not in the binary you are running”.
Corollary: the 475/475 language-suite “stone sweep” in the first commit was vacuous. The tester realized its modules through the seeded lane, so nothing it ran was compiled with the freeze. The real sweep happens after the exports reseed, at integration.
What was verified, and where
The freeze survives every stage — checked stage by stage before the cause was understood, and all of it still stands:
| stage | evidence |
|---|---|
| mcode, post-streamline | 15: ["stone",0,2] 16: ["return",0] |
| linked unit | survives link_result + materialize_unit |
| pressed mach pool | 0014 stone A=0 B=2 / 0015 return A=0, from pit compile disasm on the shop’s own artifact |
| lowering | no fusion involved — the disassembler names fused opcodes explicitly (fuse_loadf, fuse_is_stone_br); pc 14/15 are plain. mach_pool.c has only a decode switch and the emitter for MACH_STONE |
And then, executably:
tests/mcode_link.cm::test_module_return_is_stone_on_the_pressed_path presses a
module main and runs it through the real lowering and the real VM in-process.
mach_load hands back exactly what the unit main returned; it is stone, and a
store into it disrupts. A sibling test pins the freeze shallow, so a
memoizing module keeps writing into its own nested state.
Both were negative-controlled: with the emission disabled they fail with “the compiler’s module-return freeze did not reach the pressed program” and “the export record was not frozen”.
This is the right home for the assertion anyway — hermetic, no daemon, no seed, and it fails for the right reason.
Two real defects found on the way
Fixed here. pit-compiler/panic_outline.cm:170 — helper_function
declares nr_slots: 5 and read slot 5, one past the end of its own frame.
Every pool containing an outlined compiler panic was therefore invalid:
pool_assert and pool_disasm refuse it (“function N pc 5: B register is
outside the frame”), which is why no module that stores into a record could be
disassembled at all. Fixing it is what made the pool evidence above obtainable.
It is also the store-into-a-stone-record path — the one R5 makes reachable for
the first time.
Not fixed — reported. The static export cache
(pit-compiler/mcode.cm:190-221, 3946-3967) hoists p.member into a hidden
slot at the def p = use(...) site and never invalidates it. Observable today:
m.self.tag = "changed"
m.tag -> "orig" (hoisted at the use() site)
get_tag(m) -> "changed" (same member, opaque receiver)
m.self.tag -> "changed"
This cost an hour: it looks exactly like the export record having been copied
at the module boundary. It has not been — m and m.self are one object. This
is the unsoundness deletion-inventory.md §3.5 says R5 justifies
retroactively; R5 now does, on the pressed path.
F2
deletion-inventory.md §3.0 makes a working compiler-emitted stone the
blocking precondition for deleting the program-side census. That precondition
is met, and the pressed-path test is the standing proof of it.
Source: plans/proposal-notes/r5-stone-drop.md