Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.

LANDED on arc/one-pool. What follows is D2’s report, kept because every claim in it held. The one thing it did not predict is at the bottom, under “What landing it actually cost”.

R6 one-pool: exactly what is left, measured from D2

Written while landing D2 (pit product). This is the “if full one-pool proves too large, REPORT precisely what it needs” half of that brief, and it is a report rather than a plan: every claim below is a file and a line in the tree as of arc/product.

The state D2 leaves

The press is still one pool per unit, on the STATIC link lane. Nothing in this landing changed that, and changing it halfway would have been worse than not changing it — see §3. boot_cart.pool_executable still calls mach_lower.press_unit once per module, and press_unit still emits a one-row pool (pit-shop/mach_lower.cm:292). The census the press logs on every executable is unchanged and still honest:

press: linked 50 unit(s), 0 finalized site(s), 656556->651324 instructions,
       75828->75031 guards, 4214->1639 literal rows, 1469 names + 6 log sites
       stripped

0 finalized site(s) is plans/archive/night-2026-08-04/press-switch.md §4 restated: the units carry no import op for the linker to finalize, because the press compiles them on the static lane where a use() lowers to a context load.

What D2 did add to that line is the last two figures: stripping is live on the product lane (1,469 names and 6 log sites removed from the mcode product’s 50-unit program), and inert everywhere else.

What one-pool needs, in dependency order

1. The press must compile on the pool lane — and must not do so alone

shop_build.cm sets link_mode from msg.link == "pool" ? "pool" : null (:1788), and the realize lane threads it through shop_realize.cm:441, :1068. Passing link: "pool" from realize_fleet (shop_tools/cement.ce) is a one-line change and is the wrong thing to land on its own: on the pool lane the linker finalizes cross-unit edges into ["function_ref", d, provider_unit_id, fn_id, member] (pit-linker/mcode_link.cm:1242), which source/mach_pool.c:4590 lowers into

EM_ABX(MACH_LOAD_MODULE_RESULT, s.module_scratch, provider_unit)
EM_ABX(MACH_LOADK,              s.key_scratch,    cpool(member))
EM_ABC(MACH_LOAD,               ref_dest, module_scratch, key_scratch)

and MACH_LOAD_MODULE_RESULT indexes state.base + bx into the actor’s ONE results array (source/mach_vm.c:5433). With one unit per pool the provider is in a different cart section and that index names nothing. Flipping the lane without §2 and §3 produces a cart that presses cleanly and dies at run time.

2. The import covering — the shop already computes it

mach_pool_emit.emit takes, per row, imports: [...] indexed by dense import id, where {unit: <executable-local id>} means “the provider is in this same image” (shoplib/mach_pool_emit.cm:16-25, finalize_imports at :352). The press currently hands it imports: [] (boot_cart.cm’s link_rows_for), which is the honest covering for a statically compiled unit set.

The facts to fill it with already exist and are already dense: record_manifest_unit (pit-shop/shop_build.cm:1199-1218) builds, per unit, imports: [{request, provider_locator}] in exactly the order compiler.portable_imports gives the dense ids — the comment at :1209 states that invariant. executable_manifest.build consumes the same array to compute initialization order (pit-shop/executable_manifest.cm:240). So the covering is index_of[provider_locator] over the press’s own row order, and the only work is carrying that array out to pool_executable, which today receives module rows carrying locator/mcode/mach and not imports (shop_build.executable_module_refs, :362).

3. One pool, N units, and the plan row that says which unit

This is the real work, and it is smaller than press-switch.md assumed because the C side is already parameterized.

The emitter is ready. mach_lower.link_fragments(rows, options) is an N-row fold (pit-shop/mach_lower.cm:160) and mach_pool_emit writes one unit_instance per pool entry (shoplib/mach_pool_emit.cm:769). Only the two convenience wrappers (lower_unit, press_unit) hard-code a single row.

The cart format is ready. cement.make_module_table dedups module rows by mach content hash, so N modules sharing one pool collapse to ONE section (pit-shop/cement.cm:593-617), and plans_for/unit_row give every plan UNIT entry that section’s placed coordinates (:761-782). Nothing there needs a new concept.

C is ready, with one gap. pit_private_image_provider_resident_function already takes an entry_index (source/image_provider.c:1014) and selects that pool entry (Pit_MachPoolEntry(binding->binding.image, entry_index, &entry), :1001). The gap is one caller: plan_run_pool passes a hard-coded 0 (source/start_plan.c:295), and plan_unit_range reads only off/len out of the plan row’s unit record (:366-390).

So the missing piece is a number in the plan row. Each ENTRY_UNIT row needs an entry field beside off/len/hash, naming which unit of the shared pool this plan entry runs. Concretely:

wherechange
pit-shop/boot_cart.cmpress once per executable; give each module row the shared pool hash plus its pool entry index
pit-shop/cement.cm plan_record (:552)copy the placed row and add entry
pit-shop/cart2.cmencode the field (plan rows are nota records, so this is additive)
source/start_plan.cread entry in plan_unit_range, thread it to plan_run_pool’s entry_index
engine/boot_walk.cmnothing — it passes row.unit through whole (:168)

Absent field = 0 = today’s behaviour, so every existing cart keeps booting and the dev fixpoint is unaffected until the press actually emits a shared pool.

4. What it is worth, and what it costs

The payoff is mach_pool.c:4590 becoming reachable: a cross-module call stops going through MACH_LOAD_BOUND_MODULE and becomes a direct read of the provider’s already-initialized stone export record. That instruction is currently dead code that starts working — the deletion inventory §5.4 says so and this landing confirms it from the other side.

The cost is a genuine format move: the cart’s module→section relation stops being 1:1, pit cart info’s per-section attribution has to name several locators per pool, and every hash in the image changes. It is also the point at which plans/proposal-notes/deletion-inventory.md row 13 (the static link lane, ~550-600 LOC + ~210 test LOC) becomes deletable, together with E1.

The one thing not to repeat

Do not measure “does one-pool pay” against the DEV cart. A dev cart is small and is meant to be; the numbers that matter are a product’s. For scale, the mcode product pressed by this landing is 3,629,072 bytes across 52 pool sections against the development cart’s 9,674,768 bytes across 131 — and 52 of those 52 sections are exactly the 50 modules of one executable plus the engine and the walker. Under R6 that is one section.


What landing it actually cost (2026-08-06, arc/one-pool)

§3’s table was right and complete: the entry field, plan_record, cart2 (nothing — plan rows are nota records and the field rode for free), start_plan.c, and boot_walk.cm untouched. Three things it did not name.

1. Unit ids had to become PLAN SLOTS. §3 said the covering’s {unit: <executable-local id>} becomes LOAD_MODULE_RESULT bx. What it did not follow through is what the VM does with bx: state.base + bx indexes the running actor’s ONE result array, and boot_walk.cm fills that array BY PLAN ENTRY INDEX (values[i], one slot per plan row, natives and value rows included). The linker’s dense 0..N-1 ids are not those indices. So cement.plan_slots now exports the map build_exec_plan already builds, and link_rows_for uses it. Costs nothing on the static lane (no unit indexes results there) and is the difference between the pool lane working and the cart §1 warns about.

2. The early refusal probe calls plan_record with section BLOBS. cement.build’s probe_plans = plans_for(sections) hands unit_row the raw blob array so a plan that cannot be built at all refuses with a name. Copying the placed row to add entry panicked there (“not an object”). Ask whether it is a record; the probe’s answer is discarded anyway.

3. A resident window was RE-DERIVED once per unit. This is the one that would have made one-pool look like a bad idea. provider_function_from_bytes blake2s the whole window and re-opens its header before provider_adopt_mapping discovers it already holds the placement. With one pool per program the walk calls it once per unit against the SAME window: a 101-unit program hashed the same five megabytes a hundred times. Measured on the dev cart: 18.6 s to breed the root shop actor. provider_reopen_resident keys on (placement_id, base, length) and skips both — 0.24 s after, against 0.066 s for the old 131-section cart.

The numbers

artifactbeforeafter
dev root cart9,682,960 B / 131 sections15,048,720 B / 15 (13 programs + engine + walker)
mcode product3,629,072 B / 52 sections3,297,296 B / 3 (−9.1%)
--app pit-shop/logger1,069,072 B / 26 sections938,000 B / 3 (−12.3%)

--pool-units unit reproduces the logger cart at 1,069,072 B / 26 sections, byte for byte — the ROM-window shape is a lever, not a memory.

The dev cart is the one that grew, and the reason is exact: its 131 sections were shared across 14 executables (lang/messaging ×13, lang/logging ×13, pit-compiler/* ×2…), and one pool per executable replaces cross-executable dedup with within-executable interning. The trade is a win per program and a loss per fleet. R6’s literal reading — a cart is ONE pool, not one per program — would recover it and then some: 336 plan rows over 13 pools would become 336 rows over 1, and a module two executables share would be one entry instead of two. That needs pool_executable to stop being the press boundary (it is called per executable by both cement_root_cart and cement.ce’s press_fleet), and it needs a rule for what unit_instance is for an entry two executables share with different plan slots — finding 1 says those are different numbers. Not attempted here.

The press still compiles on the static lane, so finalized_sites is still 0 and mach_pool.c:4590 is still dead. What one-pool removed is the structural blocker (§1): a finalized provider is now an entry of the same pool, and its executable-local id is now the number the result array is indexed by. What remains is the compile lane itself — realize_fleet passing link: "pool" — which is a second realization of the boot closure under a different key, and which the dev lane must NOT take (per-unit pools cannot resolve a cross-unit edge).

Source: plans/proposal-notes/one-pool.md