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:

stageevidence
mcode, post-streamline15: ["stone",0,2] 16: ["return",0]
linked unitsurvives link_result + materialize_unit
pressed mach pool0014 stone A=0 B=2 / 0015 return A=0, from pit compile disasm on the shop’s own artifact
loweringno 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:170helper_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