Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

The compiler lane — throughput and output quality

Split out of plans/one-binary.md P4/P6 by John’s ruling, 2026-08-03. The original justification for carrying this work inside the one-binary arc was that whole-program compilation (P6) would multiply streamline’s ~77% share of compile time, so the IR rework looked like a prerequisite of the linker. The E-series findings dissolved that premise: the 8m20s worst case is dominated by eliminable work (provenance encode, defeated early cutoff, JSON manufacture — see plans/derivations.md “shop-perf rows (E)” and the D3 residue), not by honest compile throughput. Compiler speed and output quality still matter; they are their own lane, tangential to the cart work, and nothing in one-binary waits on them.

What stays in one-binary: P3 (forge fusion), P5 (Pit lowering), P6’s SIZE half (import-edge finalization, name/log stripping, program-level literal pool, link plans, the dedup census machinery feeding P7), P7, P8. Those serve the primary goals: the fused cart and smaller carts.

The unruled opportunity map remains plans/compiler-vision.md; this file is the ruled, active subset.

Rev 2 (2026-08-04). Added CP4–CP8 and the parallel dispatch map, from the two-prong direction (faster programs, faster compiler): the honest-site codegen shrink (CP4), the per-unit streamline diet that CP2’s link streamline makes possible (CP5), the inspection/oracle tooling for judging mcode and bytecode quality (CP6), size levers as link-plan profiles (CP7), and the PIC chain written down and parked (CP8). CP1–CP3 carry forward; CP2b gains its legality conditions. Items marked NEEDS RULING are open.

CP1 — the interned streamline IR — LANDED

Confirmed landed on dev (John, 2026-08-04): pit-compiler/stream_ir.cm is in-tree and streamline runs on the interned API (row/tombstone/ replace). The pre-merge fix list is retained for the record; any item not verifiably done at merge — the MEASUREMENT (item 1) especially — folds into CP6.2’s baseline capture rather than reopening the branch.

Originally codex/one-binary-p4 (04a3bb24c), reviewed 2026-08-03: mergeable-with-fixes, no confirmed miscompile, inline.cm verified a pure extraction. The fix list as reviewed:

  1. Take the measurement — log.passes[].ms before/after on the dev cart. The 77% figure is the budget; the review flagged a suspected regression at the decode edge (decode() rebuilds every row; the zero-alloc passthrough became dead code), so the number decides, not the premise.
  2. Artifact-identity check: compile the corpus at both revs, diff nr_slots and final mcode. This empirically settles the register-allocator rewrite (H4) and the reconcile short-circuit (H3).
  3. Record the five semantic changes (H1–H5) — the guardcse predecessor fix (H2) explicitly as a SOUNDNESS FIX, not a refactor. Fixes inside refactors are kept, never reverted — the rule is they land NAMED, so bisect/blame and the gates see them.
  4. H1 (int_ranges narrowing swap: Jacobi → Gauss-Seidel with discard-on-cap) is the one with residual miscompile-shaped risk — its facts delete type guards in the default Mach pipeline. The artifact-identity diff covers the corpus; the fuzzer covers observable behavior; if either shows drift, the narrowing change is separated and judged on its own.
  5. M1: encode failure must refuse by name instead of degrading to silent pass no-ops; add a load-time assertion that every classification-table key resolves to an opcode id (the silent-drop hazard for future opcodes).
  6. Promote tests/stream_ir.cm into the baseline gate; add cases for row() object sharing and cache invalidation.
  7. Guard comments: the drop_dead_gets argument contract (M2) and the prepare_parents-snapshot ↔ no-closure-inline dependency (M3).

Docs drift (docs/spec/streamline.md:8, :464 describe the old in-place array) is recorded and deferred per the 2026-08-03 docs ruling.

With CP1 landed, the streamline-pass rows (CP2b level 1, CP4.1, CP4.4, CP5) are unblocked and dispatchable.

Source-unit probe, 2026-08-04

The permanent source-unit command is pit compile profile <locator>. Its authoritative clock is the exact mcode_unit_result request used by a builder; phase census and pass clocks run separately so their bookkeeping is not billed to that number. --stage also measures canonical encoding, content hashing, and write-once object staging without publishing a catalog edge. Every registered streamline switch can be disabled individually or all at once.

One runtime/tests/vm_suite.ce --stage sample at 544bbe050 measured 8,152 physical lines, 247 KB, 82,881 lexer tokens (52,803 significant), and 134,728 raw -> 93,194 final instructions across 1,593 functions:

phasewallshare of exact source -> portable mcode
tokenize220.00 ms1.5%
parse113.26 ms0.8%
requirements393.36 ms2.7%
fold236.92 ms1.6%
mcode emission260.82 ms1.8%
streamline12,788.39 ms87.6%
cross-sample residual588.60 ms4.0%
exact source -> portable mcode14,601.35 ms100%
canonical/hash/object-store tail700.40 msseparate
of which object writes3.85 msseparate

The tail is not the compile regression. Named inner optimize-function pass bodies account for only about 2.04 s of the diagnostic streamline run, leaving about 10.7 s in streamline’s unclocked outer machinery: IR encode/decode, inlining rounds, TCO/LICM/switch/call resolution, range reconciliation, slot compression, and record-shape facts. Coarse clocks around those outer phases are therefore the next profiling move; another shop/store optimization would be aimed at the wrong budget.

History supplies a stronger same-file comparison than the old aggregate lines/second claim. (The instrument these three readings came from, tools/compiler_budget.ce, was retired 2026-08-06 — the readings stand as history, but a fourth point in this series has to come from make bench.) The checked-in compiler budget for pit-compiler/streamline.cm moved from 1,014.3 ms streamline at 3ba5c1913 (2026-07-09, 120,262 raw instructions), to 5,363.9 ms at 05d91a9fc (2026-07-26, 141,702 raw), then 5,825.3 ms at e0c42726b (2026-08-01, 141,789 raw). Most of the roughly 5.3x jump happened during the July 10-14 optimizer campaign, while raw IR grew only about 18%. CP1’s already-recorded decode-edge suspicion remains live, but it cannot by itself explain the earlier jump. Do not guess at a pass or remove an optimization yet: clock the outer phases, then use the permanent switches to measure the responsible phase/pass against output quality and byte identity.

Current-dev streamline outer-phase follow-up, 2026-08-04

At fc85ec8a1, a disposable phase-only log over tests/vm_suite.ce reproduced the uninstrumented streamline wall (about 10.1 s) without enabling run_pass’s before/after IR censuses. No pass switches were changed. The coarse sample was:

streamline phasewallshare
first inline round5,508.81 ms53.9%
initial optimize + mandatory lowering2,055.95 ms20.1%
range reconciliation524.54 ms5.1%
second inline round + re-optimize478.25 ms4.7%
slot compression258.36 ms2.5%
final nop compaction208.43 ms2.0%
final call resolution/annotation201.81 ms2.0%
record-shape facts145.96 ms1.4%
checked-store facts135.91 ms1.3%
TCO134.04 ms1.3%
IR decode124.57 ms1.2%
all smaller phases446.71 ms4.4%
profiled streamline region10,223.36 ms100%

The first-round label was then split. prepare_parents itself cost only 16.16 ms. Across both inline rounds, the shared callresolve.resolve_all path cost 5,650.38 ms, of which 5,500.17 ms rebuilt immediate-parent fn_slot_fids maps. It rebuilt 1,473 parent maps for the 1,473 calling functions and rescanned 16,647,382 parent instructions: about 11,302 instructions and 3.73 ms per calling function. By comparison, all self maps cost 59.23 ms, jump-target maps 42.46 ms, call-site scans 27.50 ms, and the inliner’s separate flow-sensitive resolver 173.01 ms.

This is the concrete algorithmic regression seam: prepare_parents caches ownership, but make_ctx deliberately rebuilds the parent’s function-slot map for every child because inlining can mutate parent streams. On vm_suite that correctness precaution degenerates into repeated scans of the same large parent. The likely shape is a per-inline-round parent-map cache with precise invalidation when that parent host is spliced (or an ordering guarantee that only caches finalized parents). Preserve the existing nested-function and mutable-parent guards; the measurement argues for eliminating repeated map construction, not weakening resolution.

Moved here with its gates intact:

  • Gate, absolute: before anything inlines across units, the differential fuzzer grows the linked-vs-unlinked dimension — one source with two legitimate compiled forms must agree. This is the hpack miscompile’s bug class and the fuzzer is currently blind to it. (The harness itself is CP6.3 and can be built now, ahead of any linking.)
  • Inlining is un-capped by P1 (landed); the slot fix was its prerequisite.
  • Whole-program streamline runs over the linked mcode program that one-binary’s P6-size linker produces; CP1’s interned IR is what makes that affordable. CP2 therefore follows CP1 and P6-size, in that order.
  • Cross-unit facts consumption (“consume exact types after inline splices”) survives as a portable patch in the 2026-08-01 cleanup archive, awaiting a gate — pick it up here, not before. The gate is perf/ir_census.py gate plus make bench; make budget’s compiler half was retired 2026-08-06.
  • Purity facts (ruled into scope 2026-08-04): a pure fact computed bottom-up per function and finalized at link time, when the whole program is visible. Two customers, one analysis: it completes the stripped-log enforcement (tier 2 of the one-binary ruling — calls in log arguments become checkable, closing the named limitation) and it is optimization fuel CP2 wants regardless (wider inlining legality, motion past calls, dead pure-call elimination). Per-unit approximation via traveled facts is possible earlier, but the complete lattice belongs here.
  • Whole-program motion for common calls: once the link sees everything, the log-binding lookup and other repeated common-call prologues become hoistable program-wide (one binding per actor instantiation, GETUP at sites — the per-unit form is CP4.2; the link generalizes it). Same for the shared panic tails: CP4.4’s per-unit helpers graduate to whole-program helpers here, using CP2b’s member-granular references.

CP2b — call directness (ruled into scope 2026-08-04)

John’s ruling: imports are records of functions, and today blob.write_* pays a record field lookup PER CALL plus the generic calling convention — measured pain in exactly the hot paths (the nota canonical encoder). Three levels, each with an existing home:

  1. Per-unit (can land before CP2): a def-bound STONE record’s fields are compile-time constants — streamline folds my_rec.f(...) to a direct call to the field’s value. The general form of compiler-vision’s shape-aware record ops; no link-time view needed.
  2. Link-time (P6-size + CP2): import-edge finalization extends to FIELD granularity — the linker resolves blob.write_blob through the stone export record to the function reference itself, using R7’s already-named mcode extension (the direct function-reference form). The per-call lookup dies program-wide.
  3. Native directness (P8, verbatim): finalized edges to native modules lower to direct symbol-index calls stamped with binary identity. Whether mach adds a direct-native-call form or fuses one is a lowering concern (R7); the AOT lane, when resumed, inherits the same finalized edges as direct C calls. Mcode requirement (completing R7’s extension list, ruled 2026-08-04): beside the direct function-reference form, imports gain MEMBER-GRANULAR references (module::member), so a finalized call site names a native function portably — by locator and name, never a symbol index; the lowering resolves the member reference against the binary’s natives table at press, stamped and mount-validated like every external-pool match.

Interim note for hot pit code today: hoisting def write = blob.write_blob out of a loop is the legitimate hand form of level 1 and is consistent with, not papered over by, the compiler doing it later.

Legality conditions (added rev 2):

  • The fold applies only when the resolved value is a literal stone record. A module may legitimately return a proxy or a function; the link KNOWS which it got, so a proxy/function return keeps the guarded generic path and only literal-record returns fold. This is why the fold is a link/CP2b fact and not a blanket assumption: “it could be a proxy” stops being a per-call runtime question and becomes a per-edge link-time fact.
  • What needs new mcode, precisely: folding a member that is a text or number constant needs NOTHING new — it becomes an ordinary access operand. Folding a member that is a function needs the direct function-reference form (a closure is an identity, not a literal — it cannot be pasted as a constant). Inlining that function’s BODY is then ordinary inline.cm work at link scope over the direct reference. The two ruled extensions (direct function reference, member-granular import reference) plus P6’s program-level literal pool close the list; no third extension is expected.
  • Guard cascade accounting: the fold deletes, per finalized call site, the member load, the is_record arm on the module binding, and the is_func arm on the callee, and turns the indirect call into a direct reference. Whatever the link can additionally prove about the folded value’s type feeds the existing guard passes downstream. A member proven non-function at a call site is a press-time compile error — ahead of time, as ruled.
  • Contradictory-use diagnosis (RULED, John, 2026-08-04: per-unit error). def fa = use(...) then var n = fa + 3 and later fa.rec(...) — the uses are mutually exclusive for any value fa can actually be. Per-unit usage-contradiction inference makes this a compile error in the unit itself, no link needed — it catches the bug even in dev, accepting that some today-legal (absurd) programs become errors. The link’s concrete import fact still catches the residue that per-unit inference cannot see.

CP3 — PGO-driven output (queued, unruled details in compiler-vision)

Site-ID-keyed profiles driving inline decisions, branch layout, guard ordering; hot/cold placement groups via the link plan. Consumes the origin join on demand (E1’s lazy join must serve PGO normalization as well as symbolication — requirement recorded in board.md). Nothing here starts before CP2’s gates exist. Prerequisite reality check recorded in CP8: PGO runtime collection is dead on both lanes until the L2b drain rebuild lands.

CP4 — honest-site codegen quality (the log/panic fat, generalized)

Measured 2026-08-04 (session record): a single compiler-emitted panic guard site is 10 mcode instructions lowering to 11 mach words / ~9 dispatches; pit-compiler/streamline.cm carries 2,986 such sites ≈ 30% of its 107k instructions, shoplib/canonical.cm ≈ 20%, from a vocabulary of 7 distinct messages. This is the shape the dedup census’s “35.4% duplicate 8-word runs” was seeing. The lowering of log.<channel>(...) itself is already ruled and minimal (mcode.cm:1813 — one call, the claim is the gate); the fat is around it, and every piece has an identifiable cause and an independently shippable fix. Ranked; each helps far more than log:

  1. Stone facts for pool constants. access-loaded literals are stone by construction; streamline never learned the fact, so the emitter’s defensive escape-stone survives at every call passing constant text. Teach the type lattice “this slot holds a pool constant, already stone” and elide. One word + dispatch at every such call site program-wide. Touches streamline passes (unblocked — CP1 landed).
  2. Unit-level intrinsic preload + GETUP. The intrinsic preload only exists at module level: parse.cm collects intrinsics onto the root AST node, so load_intrinsics finds nothing on nested function nodes, and every unbound global (log, format, …) inside any nested function pays the 3-instruction context/access/load triple at EVERY site. Fix: bind once per unit instantiation; sites reach it via MACH_GETUP/MACH_GETUP_CACHED (both exist). The binding stays the capability per the logging ruling — it is per-actor state and cannot be a pool constant, but it is immutable per actor, so one lookup per instantiation is honest and one word per site after. Touches parse.cm/mcode.cm emission — parallel-safe against CP1.
  3. Array-literal fill. MACH_NEWARRAY’s doc comment (“B = element count in R(A+1)..R(A+B)”) is stale: the handler treats B as a capacity hint and every array literal pays NEWARRAY + PUSH×N. Fix generally: allocate elements in consecutive slots and add a real fill-from-registers lowering, or an open-time NEWARRAY+PUSH fusion (the pair-fusion machinery exists). Helps every array literal in every program. RULED (John, 2026-08-04): varargs rejected. log is an ordinary bound function expecting (text, array), and functions take at most four arguments — the language has no varargs, so a mach-level argc trick cannot change the callee’s shape. Array-fill fusion is the path for log sites, and CP4.4 absorbs the rest: in an outlined tail the array is built once inside the shared helper, not at each site.
  4. Outline the panic tail, per unit. Seven helper functions per unit (one per message), each holding today’s tail once; a guard site becomes check + branch + call (~2–3 words, was 11). Reached by CALL, the site’s pc is in the frame and the existing pit.mach.debug pc→line tables report the exact guard line — no per-site line operands, no shared-line regression. This is the shape C (__assert_fail), Rust (cold panic shims), and Go (runtime.panicIndex) all converged on. Fully inside “Pit emits / C reads”; no VM change, no touch to the claims design. Graduates to whole-program helpers under CP2 once member-granular references land. Touches streamline/emission (unblocked — CP1 landed); sequenced as lane 1 phase 2, after CP4.1–.3 are measured.
  5. RULED OUT (John, 2026-08-04): no dedicated panic/log opcode. After 1–4 it buys ~1 word per site and is the only option that brushes the logging ruling. Revisit only if, after CP4.1–4 land and are measured, the residue still matters.

Arithmetic: 1+2+3 take an inline panic site from 11 words / 9 dispatches to ~6 / 6; adding 4 takes it to 2–3 words. streamline.cm’s guard overhead goes ~30% → ~6–8% inline → ~3% outlined. A user log.warn("x") lands at ~5 words. CP4 is also the existence proof for CP7’s general outliner: the panic tail is the one shape the census already convicted.

CP5 — the per-unit streamline diet

CP2 gives the program a second, link-time streamline that runs only at explicit pressings (R4/R6 — never in the dev loop). That changes what the per-unit streamline is FOR: it stops being the only optimizer and becomes the producer of honest, compact, cacheable unit artifacts. Work should move to the stage that runs least often.

Premise (RULED, John, 2026-08-04): dev-mode builds may be less optimized — but the mechanism is not a hard-coded division of passes between stages. Every optimization pass gets an explicit SWITCH, toggleable independently at BOTH the per-unit (mcode) stage and the link stage (passflags.cm is the existing seam). All switches up = the most optimized build the compiler can make; size-targeted profiles set them differently. The compiler carries these switches even before the shop grows a mechanism to toggle them. A pressed executable runs both stages anyway, so assigning a pass to link costs pressed builds nothing; what the dev profile chooses to skip per-unit is exactly what dev gives up. The MOVE/KEEP guidance below is therefore the DEFAULT SWITCH ASSIGNMENT for the dev and ship profiles, not a structural claim about where a pass is allowed to run.

Guidance, pass by pass (the driver today runs the 8-pass cycle up to 6× per function: 2× per optimize_function, which itself runs at three phases bracketing two inline rounds):

  • MOVE to link: inlining, both rounds. inline.cm is already a pure extraction (CP1 verified); at link scope it is the same operation with the whole program visible, and CP2 runs it there anyway. Per-unit inlining bloats the cached artifact, duplicates work (inlined copies get re-streamlined per consumer), and — structurally — the two inline rounds are WHY the driver re-runs optimize_function at phases 3 and 4. Removing them collapses the driver to one convergent cycle plus lowerings. This is the single biggest per-unit compile-time lever and most of the “wrong time” work: inlining decisions belong where the call graph is, and the call graph lives at the link.
  • MOVE to link (measure each): the heavy fact passes — licm, int_ranges, sroa, forward, switch. Candidates, not a verdict: each moves only if the per-unit time saved is real and the dev-mode regression is acceptable under the premise ruling. They re-run at link over the whole program regardless, so keeping them per-unit is duplicated work.
  • KEEP per-unit: the semantic lowerings. insert_stone (the pretext uniqueness invariant is VM safety, not optimization), lower_concat / lower_concat_space (VM contract — MACH_CONCAT surviving to lowering is an error). These define what valid unit mcode IS.
  • KEEP per-unit: cheap guard elimination — eliminate_type_checks, guardcse, notstone. They shrink the cached artifact (smaller units, less JSON/interned bytes, faster lowering) and carry most of dev-mode’s performance for their cost. They also re-run at link where cross-unit facts widen them; that duplication is cheap and worth it.
  • KEEP per-unit: compress_slots. Its original motivation (the 255 operand cliff) died with P1/mach-v3, but it has since become the linear-scan register allocator — smaller frames pay on every lane, and the lowering consumes its output. Do not drop; CP1’s H4 governs its correctness story.
  • REPLACE: the fixed cycle counts. run_cycle("") + run_cycle("_2") and the 3× optimize_function schedule become convergence-driven — a per-pass dirty flag, stop when a full cycle changes nothing. Most functions converge in one cycle. This is safe under any premise ruling and is independent of the diet itself.
  • DO NOT do separately (subsumed by CP1): the string-nop allocations ("_nop_tc_17" heap texts thrown away by compact_final_nops), the per-label full slot-type array copies, the text(slot) record keys in eliminate_moves. All of these dissolve in the interned IR. Land CP1; don’t micro-fix the representation it deletes.

Gate: before/after per-unit streamline ms over the corpus, dev-workload perf delta, pressed-output identity-or-better via CP6 oracles. Sequencing: dispatchable now (CP1 landed); the move-to-link items obviously require CP2’s link streamline to exist — until then the switches simply default them off in the dev profile and on at ship, which is the ruled first form anyway.

CP6 — inspection and oracles (“is the mcode good, is the bytecode good”)

Two questions, tooled so they can be answered on any random snippet and enforced on the corpus. Exists today: compile.ce actions (tokenize, parse, fold, mcode, streamline w/ check+stats modes, compile, dump_ir), ir_stats category census, the differential fuzzer, pit bundle info, the pool validator, pit.mach.debug pc→line tables. Gaps, in order:

  1. A mach disassembler action. compile.ce disasm (or pit bundle disasm): per-function lowered words with fusion applied, cpool entries, shape ordinals, jump targets resolved. This is the tool for “let me look at what the bytecode ends up like” — today that requires reading validator error paths. Read-only; build now.
  2. The guard/quality census as a gate. Per-unit instruction-category budget: guard %, words-per-line, panic-site count, binding-triple count. Run over the corpus and recorded next to the test baseline so codegen quality cannot silently regress — CP4 and CP5 are measured against exactly these numbers. ir_stats already computes the categories; this is wiring plus a recorded baseline.
  3. The linked-vs-unlinked differential dimension. CP2’s absolute gate, buildable NOW against the trivial “link” (concatenation, no optimization): one source, compiled per-module and fully-linked, must agree observationally. Standing it up early means the harness is debugged before anything interesting flows through it, and it immediately covers the hpack bug class for today’s pipeline too.
  4. Mcode→bytecode invariants. The full differential oracle (C lowering vs Pit lowering) is one-binary R2/P5 property, not this lane’s. What this lane adds now: a strict post-lowering checker — operand bounds, cpool reference validity, jump-target alignment, fusion legality — runnable on any pool, extending the existing open-time validator into a development assertion mode.
  5. “Why did this guard survive.” streamline’s check mode annotates surviving is_* sites with the reason (slot type unknown-at-join, param polymorphic, fact killed by call, …). Cheap in check mode, and it turns “I want to inspect random mcode snippets to make sure guards are falling” from an archaeology session into a one-command answer.

Small pools are a first-class target. Per the CP5 ruling, every pass has a compiler-level switch at both stages; PROFILES (dev, ship, small) are named switch settings, surfaced through the link plan (R6’s granularity lever) once the shop grows the toggle mechanism — the switches exist in the compiler first. Knobs, all existing or already ruled homes:

  • Inline budget. inline.cm’s cap becomes a plan parameter: 0 (never, smallest), default, aggressive. Inlining togglable = this knob; CP2’s link-time inlining reads the same parameter.
  • Outlining (“inverse inlining”). Precedent is real: LLVM’s MachineOutliner (on at -Oz) extracts repeated instruction runs into functions; GCC/gold ICF folds identical function bodies. Per R7, outlining is a lowering/link-plan concern (a target decision), so it lives with the link plan, never in mcode. Ordering: CP4.4’s panic-tail outlining first (the one shape the census convicted — known payoff); then P7’s honest post-P2 census decides whether a GENERAL outliner exists at all (“if the honest number is small, this phase deletes itself” — that ruling stands; the general mechanism is gated on the census, not on enthusiasm).
  • Stripping. Name stripping and per-channel log stripping are P6-size (one-binary), surfaced as plan flags; recorded here only so the profile list is complete in one place.
  • Fusion toggle already exists (PIT_MACH_NO_FUSE) and joins the profile surface rather than remaining an env var.

CP8 — the PIC chain (documented, parked)

Written down so it stops being re-derived; parked because CP2/CP2b delete the monomorphic majority of record-access overhead first, and the PIC covers only the residue that stays polymorphic at press. Evidence base: the load-PIC hit path (type check + I-bit + shape-pointer compare + dense indexed load) measured ~15% Mach / ~24% native on the record microbench when receivers were shaped; store-PICs measured negative and were deleted; the shaped MISS path (rec_shape_find, a linear scan) measured slower than the generic probe with its precomputed hash — the fallback is the hazard, the hit is not. Coverage, not mechanism, is the whole problem: the selector shapes only hot/self-recursive construction sites, so nbody’s real-PGO run saw 46 hot load sites, 10.5M executions, zero shape targets; forcing cold shaping regressed 6.9%/12.5% and is REJECTED on measurement.

The chain, in dependency order:

  1. L2b PGO drain rebuild (one-binary P-0 tail) — runtime collection is dead on BOTH lanes; the emitter is stashed; D5 deleted the collector. Nothing downstream is real until a program run produces a non-zero count end-to-end. DEFERRABLE for this lane’s purposes: with CP8 parked and CP3 last, nothing in CP needs it until those start. It remains a one-binary P-0 close-out obligation on its own merits — schedule it there, not here; the only hard requirement now is that the stash stays alive.
  2. Record-literal provenance fix["record", …] bypasses position stamping, so construction sites are invisible to the origin map. Small standalone defect fix; ALSO serves origin integrity generally — pull it forward regardless of the chain (parallel-map lane 2).
  3. Profile-driven constructor shaping by canonical-hash matching. Shape identity is the canonical key-set hash — global by construction. The profile names the key-sets hot load sites actually see; the selector already computes each construction site’s would-be key set before rejecting it for coldness. Match by hash and shape those constructors on evidence instead of the hotness heuristic. No interprocedural dataflow needed: a wrong match just falls to the generic path it would have taken anyway. (This also needs the register-lane shape gap closed or all experiments pressed to pools: PitCodeRuntimeMeta.record_shapes is populated nowhere since the register loader’s deletion — shaped construction currently works only from mapped pools.)
  4. PIC arming via the existing promotion machinery (≥1024 executions, ≥90% top-four coverage) in a --pgo link plan.

Open question to answer WITH the rebuilt collector, before defaulting any of this on: the 15% was a record-access microbench; the macro-level share of record polymorphism in real programs after CP2b lands is unknown, and it decides whether this chain is worth its complexity. Revisit after CP2.

Lane integration (2026-08-04, per the board’s lane-branches ruling)

The first four topic merges (2026-08-04 morning) went straight to dev and collided with the P3 landing — the lesson is on the board. From now on:

  • cp topic branches root at and merge into lane/cp, never dev.
  • One reseed per batch happens ON lane/cp, so its tip is always a coherent (source + matched artifacts) generation.
  • lane/cp → dev is a single whole-set merge performed by the integrator (one session), gates run in the lane worktree beforehand; dev’s checkout is a viewer — no builds, daemons, or seeds ever run in it. After any lane/p → dev landing, dev back-merges into lane/cp promptly.
  • The two ruled mcode extensions land through whichever lane needs them first; the other side picks them up on its next back-merge.
  • CURRENT BLOCKER: dev f2614a81e is the trapped state (P3 source merged, artifacts reverted) — a fresh worktree cannot complete make (root shop: “invalid qop archive”). All fresh-worktree gating waits on lane/p’s cold-recovery must-fix landing and back-merging. Source-only commits on lane/cp are fine meanwhile.

Ordering

  • Now, four lanes in parallel (CP1 landed):
    • Lane 1 (cp/emission): CP4.2 preload/GETUP + CP4.3 array fill + the record-literal provenance fix. Phase 2, own merge point: CP4.4 panic-tail outlining.
    • Lane 2 (cp/tooling): CP6 — with the CP6.2 BASELINE CAPTURE as its first deliverable, merged before any other lane merges (see Measurement protocol).
    • Lane 3 (cp/facts): CP4.1 stone facts + CP2b level 1 fold + the ruled contradictory-use error.
    • Lane 4 (cp/switches): CP5 pass switches at both stages + the convergence-driven driver. Lanes 3 and 4 both edit streamline.cm: merge 3 first, 4 rebases over it. Lane D (L2b drain rebuild) stays DEFERRABLE — see CP8.1. One-binary P3/P6-size proceed in their own lane and coordinate only on the two ruled mcode extensions.
  • After lane 2’s baseline + P6-size + the fuzzer dimension (CP6.3): CP2, absorbing CP5’s link-stage switch defaults and CP2b level 2.
  • After CP2: CP3, and the CP8 revisit with real profiles.
  • Status (John, 2026-08-04): one-binary P1/P2 landed; the C, D, and F rows are drained; E is drained except E9/E10; P3 is running now, with the rest of the P lane (cartridge fusing) to follow. The “re-measure after E4” gate is therefore satisfied — CP1’s measurement (item 1) runs against the post-E baseline, not the July numbers.

Parallel dispatch map (what does not touch what)

LaneBranchRowsFiles ownedConflicts
1cp/emissionCP4.2 preload/GETUP, CP4.3 array fill, provenance fix (CP8.2); phase 2: CP4.4 outliningpit-compiler/parse.cm, pit-compiler/mcode.cm, source/mach_pool.c, source/mach_vm.c (fusion)none
2cp/toolingCP6.2 baseline capture FIRST, CP6.1 disasm, CP6.3 linked-vs-unlinked harness, CP6.4 invariantsinternal/compile.ce actions, ir_stats.cm, new tools, fuzzer harnessnone — read-only over the formats; merges first
3cp/factsCP4.1 stone facts, CP2b level 1 fold, contradictory-use errorstreamline pass files, resolve/parse diagnosticsshares streamline.cm with lane 4 — merges before it
4cp/switchesCP5 switches (both stages), convergence driverpit-compiler/streamline.cm driver, passflags.cmrebases over lane 3
DL2b PGO drain rebuild (CP8.1) — DEFERREDPGO collectors, pit-compiler/pgo.cm, the stashstart only for CP3/CP8 or one-binary close-out
Eone-binary P3, P6-sizeforge, linker (shoplib/mach_pool_emit.cm), shopcoordinates with lanes 1/3 only on the two mcode extensions

Merge order: lane 2’s baseline first, then lanes 1 and 3 in any order, then lane 4. Lane 1’s phase 2 (outlining) is its own later merge point.

Measurement protocol (baseline before depth)

One script, run at every lane merge, diffed against a committed baseline. The tools exist from the July perf run — this is assembly, not building. It is CP6.2’s first deliverable and nothing else merges before the baseline is captured. Three axes:

  • Compiler time: per-unit pipeline ms over the corpus and per-pass ms via run_pass’s existing log.passes instrumentation (the $receiver-based pass-profiler recipe from the July run — long-running one-shot tools must be $receiver-based or the session probe window kills them). Plus the coarse wall numbers: clean make, seed, hot/cold ps.
  • Output quality: ir_stats category census per unit — final instr count, guard %, panic-site count, binding-triple count — plus lowered pool bytes per unit. These are the numbers CP4/CP5 claim against.
  • Runtime and memory: the benchmark suite, with the flag-gated dispatched-instruction and per-opcode counters (actor.op_counts()), alloc/GC stats (actor.vm_stats()), and the service benches (record_lookup, record_grow, text_search, gc_churn). Counters stay gated — always-on costs 15–35%. Store steady-state MB.

The baseline is captured once on dev before lane merges begin and committed; each change lands a short note against it (the July perf/changes/ note convention). Every merge re-runs the script; regressions on any axis are named in the merge note or reverted. Pressed-output artifact identity with all switches up is the additional gate for lane 4.

Rulings record (all four resolved, John, 2026-08-04)

  1. CP5 premise — RULED yes, with the mechanism corrected: dev builds may be less optimized; the lever is per-pass switches at both stages (see CP5), not a hard-coded stage division. Compiler-side switches land before any shop toggle mechanism exists.
  2. CP4.3 — RULED: varargs log rejected. No varargs in the language; log’s shape stays (text, array). Array-fill fusion plus outlined tails are the path.
  3. CP2b contradictory-use — RULED: per-unit compile error.
  4. CP4.5 — RULED: no dedicated panic/log opcode. Revisit only on measured residue after CP4.1–4.
  5. Priority (John, 2026-08-04): program speed and size outrank compiler speed. An expensive optimization is acceptable behind a toggle — roughly 5% program gain for 2x compile time is a good trade. The profile/switch machinery is the home: such passes default on in ship (when measurement justifies), off in dev. First customer: the join-aware type-facts pass (CP9.2).
  6. P6-size pulled ahead of P5 (John’s direction, 2026-08-04). The mcode linker’s size half is the single missing prerequisite for CP2b level 2 and CP2; it does not depend on P5 or the L2b drain (“an ordering, not a contract”). L2b stays deferred; the stash must survive. P6 lands through lane/p as its own landing.

CP9 — active rows from the 2026-08-04 measurement round

The pit compile profile instrument (merged) reset the compiler-speed map: named pass bodies are ~2s of a ~13s unit compile; the cost is the outer machinery, dominated by one waste loop.

  1. Inline parent-map cache (dispatched: cp/inline-cache). callresolve rebuilds immediate-parent fn_slot_fids once per call-bearing function: 1,478 rebuilds, 16.7M instructions rescanned, 5,767 ms = ~44% of the 13.1 s vm_suite unit compile (reference table in the outer-phase section above). Per-round cache with precise splice invalidation; output-invariance proven by corpus byte-identity A/B; all resolution guards stay.
  2. Join-aware type facts (dispatched: cp/join-types). The guard- reason census’s top class is “unknown at join” (1,281 of 3,021 decomposed). Meet-over-predecessors merging to fixpoint, conservative on backedges, as a switchable pass under ruling 5. Soundness bar: the int_ranges precedent — differential fuzz on/off must agree.
  3. Outliner shared log binding (dispatched: cp/outline-binding). Census: log-binding triples 49→358, Δtriples == Δfunctions exactly — each outlined helper rebinds log. Route helpers through the unit preload/GETUP; triples return to ~one per log-using unit.
  4. Startup residuals — COLD HALF FIXED, hot half open, and the baseline was not like-for-like. Diagnosed 2026-08-04. Neither residual was caused by the batch: clerk.ce, shop_actor.ce and shop_source.cm are byte-identical at postp3 (fc85ec8a1) and at the batch tip, and pit shop mounts printed mounts: [] at BOTH revs. What the batch changed was which path the clerk takes. The clerk enters source mode only when its own source resolves, and a package resolves only through the snapshotted .pit/sources/<pkg> (a link is an upstream, never a read path — direct_package_root). When the postp3 baseline was captured, .pit/sources/pit-shop had not been snapshotted, so the clerk booted SOURCE-LESS and returned before the freshness check; the batch’s mandatory re-press (make seed, f033dd1e0) snapshotted the workspace and flipped it to SOURCE mode. The 0.42s/0.023s baseline therefore measures a different code path. Like-for-like, both in source mode, the batch tip is faster than postp3: cold 11.9s vs 27.2s, hot 0.45s vs 0.77s.
    • COLD (fixed). start_boot_image_services ran before open_content assigned content_path, so every fused resident started with seed_path = null — no seed mount at all, and shop_source memoizes the mount table, so await_seed_catalog re-read one cached null 200 times and spent its whole 10s ceiling. The fix settles content_path above start_boot_image_services; the bound is untouched and now hits on the first try. Cold ps 11.9s → 0.71s. Regressed by tests/cli/seed_path.ce (the clerk must observe the seed mount); re-pressed, fixpoint held.
    • HOT — the linked-upstream read is FIXED; the attribution that named it the dominant cost of ps was WRONG, and the residual is still open. Ruled and landed on cp/linked-stat, 2026-08-04.
      • What was fixed. source_dep_valid no longer content-reads and re-hashes every dependency of a LINKED package on every realize hit. Each dependency row now records the link target’s stat identity (size, ns mtime, inode) beside its content hash, and a hit whose identity still matches skips the read. Everything else — no recorded identity, none available, a differing one, and the FIRST check of each linked package per shop session — falls back to today’s content compare, which bounds any staleness the stat contract can admit to one session. The contract is written down in docs/shop/shop.md (“Development links are mutable upstreams”) and regressed by tests/store_freshness.ce with counters, not timings.
      • What it bought, measured on cp/linked-stat (load 3–5, source mode, pit-shop/terminal.ce, 30 deps of which 21 are linked): validate_realize_entry 11 ms → 0 ms, and the 21 content reads per hit collapse to 21 stat skips. Steady state is one content pass per linked package per daemon, then none.
      • What it did NOT buy, and this is the correction. Hot ps in that shop is ~0.98 s before AND after. The freshness check was ~1% of it, not the ~0.54 s claimed above. That 0.54 s was inferred from an un-logged window, never instrumented; the instrumented number says the window is ~0.3 s and the freshness check is 11 ms of it, with the remaining ~0.67 s falling AFTER realize hit pit-shop/terminal.ce (hit → mounted ps → terminal halt). The hot residual is therefore a NEW, unattributed row: instrument the post-hit half (executable hydration, mount, actor start) before proposing a fix for it.
      • Bench gotcha, for whoever takes the residual. A make local worktree boots the clerk SOURCE-LESS (.pit/sources/pit-shop carries the .cm files but not clerk.ce), so hot ps is ~0.02 s there and NO shop-source edit reaches the daemon at all — the clerk runs boot-cartridge code and a measurement taken that way measures the previous revision. Pull pit-shop/clerk.ce (and the other resident .ce files) through shop_build.file with upstream_current, then restart: the successor boots in source mode and the numbers above are reproducible without make seed.
  5. July 10–14 streamline regression hunt (queued). Same-file compile went 1.0s→5.4s while IR grew 18%; A/B with the merged –disable-passes switches. Expect row 1 to eat most of it; hunt what remains.
  6. OPEN DEFECT — stale cached units wedge the daemon under P6’s reader (found at the P6 landing, 2026-08-04). P6’s independent reader validation (pit-shop/mcode_lower.cm) rejects pre-P6 cached mcode units as malformed FATALLY at service start; a warm store from before the landing wedges the boot (“malformed mcode unit for …”). Remedy today: wipe the shop store (rm -rf .pit; make). Real fix, priority: a reader rejection must behave as a cache MISS (recompile), or the unit cache key must fold the format version the reader enforces so old units never hit. Every existing dev shop hits this once on first boot after the landing.

Source: plans/archive/compiler-perf.md