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

Pit language performance campaign — 2026-07

Restart note: the authoritative branch disposition, later experiments, rejected-arm index, memory ledger, and next-work sequence are in compiler-optimizing-handoff.md. This file remains the detailed audit ledger for the experiments it covers.

This is the audit ledger for the contained performance experiment based at 81685c37. Nothing in this campaign is merged upstream or pushed. A change is only a candidate for later human review after it passes correctness, language semantics, runtime-memory, and repeatable micro/macro benchmark gates.

Objective and priorities

  1. Make AOT native programs as fast as the language contract permits.
  2. Improve Mach where the same compiler proof or a Mach-specific lowering can remove executed work.
  3. Keep runtime memory viable for older consoles. Measure every persistent table, frame, code-size, and heap tradeoff; treat 4 MiB (Nintendo 64 class) as a real design constraint and 288 KiB (Game Boy Advance class) as an extreme profile that may require a deliberately smaller runtime fleet.
  4. Compiler time is observational only during this campaign. Large temporary compiler memory is acceptable for experiments because compilation can be ahead of time, but it must be recorded and must not leak into generated artifacts or runtime state accidentally.
  5. Only language execution is in scope. Actor message latency, Nota/Wota, package/build latency, and unrelated system services are excluded.

Language and runtime invariants

  • Function arity is at most four. Missing arguments are null-filled. Excess dynamic arguments disrupt; statically certain excess arguments may be a compile error.
  • Parameters and def bindings are immutable.
  • Stoned records/arrays/text cannot be mutated. Compiler optimizations must preserve the required stone checks at every mutation boundary.
  • Pit-to-Pit calls must retain a reifiable, GC-walkable continuation/frame at every actor suspension or safepoint boundary. A native C module may use its native stack internally, but general compiled Pit execution may not depend on an unfreezable native call stack.
  • Numeric overflow, division, modulo/remainder, null, disruption, closure capture, and preemption behavior remain part of the observable contract.
  • No new semantic mcode operation is introduced silently. Target-internal Mach fusion or native lowering may change without changing the portable mcode contract, but is still documented here.

Measurement rules

  • Pit Mach is compared primarily with interpreter/bytecode tiers.
  • Pit native is compared primarily with LuaJIT and native/JIT/AOT tiers such as BEAM JIT, OCaml, C#, Node/Bun, and C where comparable.
  • Timings use warmups plus at least seven samples and report the median. A claimed win should survive repeated fresh-process runs and exceed normal noise. Small wins need generated-code evidence.
  • Every benchmark has an independent expected result where practical. Cross- lane equality alone is insufficient because both backends may consume the same poisoned compiler fact.
  • Microbenchmarks identify mechanisms. Shootouts decide whether the mechanism matters in composed programs.
  • Record/allocation benchmarks must retain their anti-sinking, aperiodic target selection, and unknown-shape properties unless the experiment explicitly measures a static-record ceiling.
  • Record executed Mach instruction counts, native helper calls/code shape, allocation bytes/count, GC count/time, peak live heap when available, frame slots, generated code size, and persistent metadata added by an experiment.

Baseline

Git baseline: 81685c37 (codex/perf-next-baseline). It includes:

  • exact-use-span transport for LICM constants, a correctness repair that does not itself claim a runtime win;
  • Playdate nan32 design documents and a standalone representation fixture, not a converted runtime.

The previous integrated chart in docs/plans/perf-next.md is historical input, not the fresh campaign baseline. This ledger will record a newly measured table before accepting performance deltas.

Acceptance gates

For each experiment:

  1. State the compiler/runtime hypothesis and the expected generated-code change.
  2. Capture before/after focused timings and at least one affected shootout.
  3. Inspect optimized mcode and/or QBE/native assembly to verify the intended change actually occurred.
  4. Run targeted semantic regressions, Mach/native result oracles, and the full default suite. Run fuzz-sensitive gates for optimizer/lowering changes.
  5. Record runtime memory and code-size deltas. Persistent cost must be justified against constrained targets; a host-only speed profile must be explicit.
  6. Keep failed experiments in this ledger, but delete their implementation.

Experiment ledger

IDHypothesisStatusRuntime resultMemory/code tradeoffDisposition
B0Fresh baseline is reproducible and result-correctcomplete1,918/1,918 default tests; fresh rows belowno campaign runtime cost beyond T2 toolingaccepted campaign baseline
N1Loop-carried numeric values can remain unboxed between observable boundariesaccepted locallycomposed raw-on/off: mandelbrot -87.7%, float_math -84.4%, spectralnorm neutral (-0.24%), fannkuch byte-identicalno runtime state; composed assembly mandelbrot -29.0%, float_math -4.8%, spectralnorm -2.7%, fannkuch identicalkeep; exact parameter inference removed the old spectral code-shape penalty
N2Point-specific constants can remove integer carrier loads/storesaccepted locally; full/fuzz greenfannkuch native 199.78 -> 191.03 ms (-4.4% matrix); spectralnorm 119.26 -> 116.63/118.12 ms (-2.2%/-1.0%)fannkuch IL -1.09%/asm -5.48%; spectralnorm IL -0.46%/asm -2.16%; no runtime statekeep; exact-span substitution with conservative use audit
N3Analyze integer ranges in large functions instead of enforcing a compiler-throughput cutoffaccepted locallyroot d_field 14.21 -> 12.05 ms (-15.2%); record_new 23.9 -> 21.02 ms (-12.1%); fannkuch restored to 200.36 msd_field/record_new/fannkuch assembly smaller; compiler time ~4-5x on affected bodies; no target metadata/statekeep; compile speed explicitly out of scope
N4Reuse exact block-local is_num fallthrough facts in native raw arithmeticaccepted locally as a correctness-preserving refinementfannkuch 209.57 -> 200.36 ms and spectralnorm 192.83 -> 182.27 ms; exact resultsfannkuch assembly 124,967 -> 118,423 B; spectralnorm 269,674 -> 255,965 B; compiler maps onlykeep; N1 still gated on spectral profitability
N5Treat hoisted function declarations as callable bindings in closed-world parameter inferenceaccepted locally; full/fuzz greenspectralnorm Mach 280.11 -> 202.70 ms (-27.6%), native 181.96 -> 132.92 ms (-26.9%); composed matrix 202.67/130.65 msspectralnorm optimized instructions 1,432 -> 444, IL -52.0%, assembly -52.3%, frame slots smaller; no added target statekeep; pure compiler proof repair
N6Carry closed-world immutable parameter facts into final range/guard cleanupaccepted locally; full/fuzz greenspectralnorm Mach 201.99 -> 199.04 ms (-1.5%), native 131.21 -> 128.74 ms (-1.9%)IL -8.2%, assembly -7.5%, exact-fact bytes -22.9%; at most four compiler-only fact words/function, stripped from canonical mcodekeep; pure compiler proof consumption
N7Decode a dynamic numeric value and its validity in one representation-specific branch treeaccepted locally; full/fuzz greencurrent spectralnorm native 128.74 -> 124.65 ms (-3.2%); old-context controlled spectral about 182.5 -> 178.5 mscurrent spectral IL -2.3%, assembly -1.6%; no metadata/frame/heap state; shared emitter code onlykeep; exact sticky-null semantics in nan64/f32/fixed
N8Give self-TCO loop phis structured entry/backedge facts and choose raw lanes per valuekeep; composed and validatedtco_self 30.561 -> 5.312 ms (-82.6%); exact result; nonselected shootouts unchanged; forced resume 49/49; full 1,918/1,918; fuzz 3,733/3,733generated IL -11.1%, asm -6.5%; no target state; 32-byte host compiler fact array for the two-arg fixtureraw-int guarded decrement plus raw-double overflow accumulator
N9Build natural-loop value identities and retain exact pointer-free phis across cold inline-call regionsaccepted isolated; full/fuzz greenspectralnorm 120.032 -> 78.544/78.001 ms (-34.6%/-35.0%); fannkuch and loop_nested byte-identical; forced resume/GC 48/48; full 1,918/1,918; fuzz 3,733/3,733spectral IL -3.67%, asm -3.06%, linked __text -1,140 B; no frame/heap/persistent target state; compiler-temporary CFG onlykeep narrow; >6 eligible phis reject to avoid demonstrated spill/code cliff
A1Raise inline size limits and admit immediate-child calls through level-1 capturesrejected; implementation deletedspectralnorm native 172.10 -> 172.20 ms; Mach 305.97 -> 295.53 msIL 13,355 -> 15,578 lines (+16.6%); native allocation traffic 2,656 -> 26 KiBnot worth static growth without speed
A2Inline exact-type array reads instead of calling the dynamic load helperrejected twice; implementation deletedcurrent-tree retry: array_read -0.49%; spectralnorm ~0.3% average and not reproduciblearray_read IL +2.70%/asm +1.42%; spectralnorm IL +3.47%/asm +2.38%micro win does not justify replicated control flow/ROM
A3Share one exact-array arm in the native index helperaccepted locally; strengthened proof gatecomposed spectralnorm on/off/on 119.44/121.18/118.41 ms; array_read 25.01/25.12 msversus N2 alone spectralnorm IL +888 B/asm +577 B; no dynamic memory/statekeep for fastest AOT profile; bounded ROM cost buys a repeatable macro win
A6Keep a proven exact-array numeric element raw through its guard and immediate multiply/divideaccepted locally; full/fuzz greenspectralnorm 122.08 -> 114.51/114.92 ms (-6%); final array_read artifact byte-identical to disabled armspectralnorm IL -1,164 B (-0.68%), assembly +215 B (+0.19%); no target heap/frame/metadata statekeep narrow; broader add/sub arm regressed array_read ~31% and was deleted
A7Fold the compiler-generated checked-array-load/null region in native outputaccepted in isolated experiment; full/fuzz green31 interleaved fannkuch pairs 188.89 -> 184.17 ms (-2.5%); final standalone 183.75 ms exactfannkuch IL -1,021 B, asm -548 B, linked __text -136 B; no target state; transient emitter maps onlykeep; strict shape, exact receiver, private labels, and dead-scratch proof
A8Fold the complete compiler-generated checked-array-store diamond in native outputaccepted in isolated experiment; full/fuzz greenthree interleaved fannkuch processes 185.11 -> 113.58 ms (-38.6%); final dual-lane matrix 117.2 ms exactfannkuch IL -6.3%, asm -2.8%, linked __text -1,124 B; no target heap/frame/metadata statekeep; strict final-index shape and full append/error/barrier semantics; report
C1Freeze-safe direct calls can remove dynamic call overhead on top of the accepted continuation modelqueuedpendingcontinuation metadata is now bounded; direct-call design still must preserve itpending
C2A resolved native call can skip dynamic identity/code lookup and call its symbol directlyrejected; implementation deletedno-inline call_hot 27.06 -> 28.67 ms (+6.0%); polymorphic calls intentionally unchangedIL +179 B/asm +118 B on call_hot; helper added shared text but no stateexisting dynamic helper is already cheaper than a second specialized prep seam
M1Fuse residual pure type-test + exact boolean branch pairs in Machaccepted locallyrecord_field -12.0%; fannkuch -4.5%; spectralnorm -6.7% controlled A/Bno actor/per-code/blob bytes; about +984 B shared textkeep; exact report linked below
M2Allocate AOT side tables in console-sized incrementsaccepted locallyno speed credit; native recursion results unchanged through 200 live frames and GCroots 17,408 -> 544 B; first key cache 1,024 -> 128 B; first code cache 3,072 -> 384 Bkeep
M3Extend target-preserving Mach guard fusion to residual text predicatesaccepted locallyspectralnorm -2.21%; record_field -1.24% controlled A/Bidentical Mach blobs; no dynamic/program state; aligned runtime segments unchangedkeep; exact report linked below
M4Size known-field records against the hash table’s real 75% load limitaccepted locallybinarytrees Mach -12.1%, native -12.9%; dispatch and emitted code unchangedallocation traffic -44.4%, collections 62 -> 33; no metadata or code-size costkeep
M5Consume exact type spans to lower guarded native array stores directlyaccepted locallyfannkuch native -17.5%; 2M numeric stores -28.3%; root reproduced 206.26/15.97 ms+3.70%/+1.97% assembly; 1-19 KiB compiler-only spans; no runtime heap/frame costkeep; correctness barriers separate
M6Decode Mach numeric null as a NaN poison inside the arithmetic fast pathaccepted locallyfloat_math -22.4%; mandelbrot -12.8%; spectralnorm -8.2% in controlled pairs+484 B shared runtime text; no dispatch, bytecode, frame, actor, or per-code memorykeep; native companion remains separate
M7Let pre-compression range analysis consume fresh LICM integer constantsaccepted locallyloop_nested native 29.41 -> 12.17 ms (-58.6% controlled); root 12.17 ms exactIL -15.3%, assembly -9.1%; compiler facts only, no target memorykeep
M8Consume exact per-use integer spans in inline-spliced Mach hostsaccepted locallycall_hot Mach -5.2% repeated-process / -7.6% controlled; root median 13.59 ms, exact result; fannkuch/spectralnorm unchangedabout +1.2 KiB shared runtime text and +200 B serialized facts on call_hot; load-time fact copy is immediately freedkeep; experiment-only disable switch removed
M9Fuse generic Mach arithmetic with its forward jump over cold error coderejected; implementation deletedspectralnorm dispatches 224.4M -> 192.4M, but 277.30 ms fused vs 276.48 ms disabledabout 95 lines/four larger shared handlers; serialized Mach and program state unchangeddispatch reduction did not translate to speed
M10Reorder guarded numeric diamonds so success falls through and cold disruption code is out of lineprototype rejected before timing; implementation deletedcompiler self-realization stalled/failed even under benchmark-only and small-function gatesno target artifact produced; poisoned generated cache required worktree-local cache clearretry only through verified CFG block layout
M11Keep Mach numeric null absorbing through every numeric intrinsic and materialize canonical null/bool constantsaccepted locally; full/fuzz greenexact Mach/native raw oracle restored; intrinsic_ops 23.725 -> 23.770 ms (+0.2%, noise)no heap/frame/per-code state; release __text 1,030,980 -> 1,024,712 B and identical mapped __TEXT segmentkeep; correctness repair with no measured runtime or mapped-memory penalty
M12Lower proven array-receiver/integer-index accesses to compact typed Mach handlersaccepted locally; full/fuzz greenarray_read -6.8%/-9.3%; fannkuch -3.3%/-3.5%; spectralnorm -1.0%/-2.9% in bracketed A/B runssame one-word instructions and blob sizes; no fact sidecar/runtime state; mapped runtime text unchangedkeep; strong Mach win at effectively zero target-memory cost
M13Consume the immediate is_int fallthrough fact at exact-array Mach loadsaccepted locally; full/fuzz greenincremental fannkuch 276.23/275.46 vs 294.61 ms (-6.2%/-6.5%); typed loads 6.16M -> 16.22M+600 B linked __text (+608 B total section content), mapped segment unchanged; no program/runtime statekeep; 600 shared bytes buy another material shootout win
M14Extend the local integer proof through the full checked-array-store diamondrejected; implementation deletedall 14.83M stores typed, but 280.79/275.05 vs 272.06 ms generic (+3.2%/+1.1%)prototype added 1,272 B __text plus 38 B string/unwind content; no program statea predictable receiver branch was cheaper than the larger proof/handler layout
M15Keep existing-capacity Mach array stores inside the VM and reload frames only after growthaccepted locally; full/fuzz greenfannkuch 264.73/267.01 vs 304.50 ms (-13.1%/-12.3%); final no-toggle 261.61 ms+1,480 B shared __text; mapped segment unchanged; no bytecode/program/actor statekeep; one shared fast path beats both generic and typed call-out handlers
M16Inline the pure Mach array-read helper at its three VM call sitesaccepted locally; full/fuzz greenarray_read normally -6% to -8%; fannkuch approximately -2% to -6%; spectralnorm neutrallinked __text -856 B on Darwin; no bytecode/heap/frame/actor/program statekeep; pure shared fast path, 5,623-case differential gate
M17Delete the dead self-callee lookup left behind by self-tail-call eliminationaccepted locally; focused compiler greentco_self Mach 52.13 -> 46.21 ms (-11.3%); native timing neutral5.0M dispatches removed; Mach blob -64 B; native IL -395 B/assembly -232 B; no runtime or compiler metadatakeep; pure post-transform dead-value cleanup
M18Fuse the complete compiler-proven checked-array-store region in Machaccepted locally; full/fuzz greenfannkuch dispatches 225.86M -> 168.03M (-25.6%); exclusive median 265.29 -> 227.44 ms (-14.3%)+3,792 B shared __text vs no fusion; mapped segment/blob/heap/frame unchanged; one-time load +2.06 us/code objectkeep; exact sparse fact replaces duplicate runtime matching
T1Exact daemon identity and endpoint ordering make benchmark processes reliableverified, incomplete cold-boot curenot a language speed changesmall CLI-only code/test increasetooling-only
T2Open native benchmark loading only after a normal daemon bootverifiednot a language speed changeone process-global atomic flagtooling-only campaign capability
T3Make the benchmark driver a typed CLI actor instead of relying on environment selection and a two-second probeverifiednot a language speed change; enables complete shootout measurementno generated-program/runtime costtooling-only
T4Expose point-specific integer proof facts in the native IL dumpverifiednot a language speed change; made the loop_nested boxing cycle directly auditablediagnostic output onlytooling-only
T5Make the IR dump tolerate absent optional facts and show exact constant spansverifiednot a language speed change; unblocks constant-carrier and raw-region auditsdiagnostic output onlytooling-only
T6Keep the contributor fuzz command aligned with the dedicated fuzz actorverifiednot a language speed change; removes a guaranteed validation dead enddocumentation onlytooling-only
T7Print both backend results in focused timing outputverifiednot a language speed change; makes mismatches immediately actionablediagnostic text onlytooling-only
T8Return the raw/optimized statistics that streamline --mode stats already computesverifiednot a language speed change; exposes instruction and slot deltas per functionresponse data onlytooling-only
T9Tiny edits to the large QBE emitter must not crash compiler self-realizationassignedreproducible mcode_lower_func_node/mach_release_frame segfaults before benchmark executionunknowndiagnose underlying bound/lifecycle defect
T10Test execution deadlines must begin after realization, not before compilationverified locallycold vm_suite previously timed out with 0 tests; now compiles past the old boundary and passes 1,086/1,086two timestamps/one optional duration field per live test entry; test-runner actor onlykeep
T11Print a compact per-function fact/guard/load/store/constant-span census on requestverifiednot a language speed change; exposed the all-unknown spectral parameter seam and N2 candidates without megabytes of IRdiagnostic text only and disabled by defaulttooling-only
T12Outer one-shot replies must survive a bounded cold test realizationaccepted locally; focused/full/fuzz green139-second fuzz returned 3,733/3,733 and cold full suite returned 1,918/1,918 instead of dying at 60 secondsone optional numeric request field and descriptor constant; no target-program costkeep bounded typed descriptor/command policy; separate from semantic test deadlines
T13A transient daemon info failure must not destroy a live endpointaccepted locally; native test greenrepeated client failure preserved socket/PID and the next retry succeeded instead of breeding or requiring cleanupno Pit/program memory; five-second client-only retry bound and a few hundred bytes host CLI textkeep; cold listener omission remains separate
T14A native cache entry must identify the exact emitted IL, not only its sourceaccepted locally; focused rebuild/hit verifiedsame source now changes dylib path when compiler output changes and reuses it only for byte-identical ILone compiler-time IL hash and more truthful cache entries; no target/runtime costkeep; prevents benchmarking stale machine code
T15A long differential-fuzz validation must show bounded human progress without corrupting its final replyaccepted locally; 500-seed/full-suite greenseed 20260713 completes 3,733/3,733, exit 0, with progress every 25 programs; JSON remains one objectlog text and one actor-local interval number only; no generated-program/runtime costkeep; tooling-only, interval is configurable
T16Machine-shell/daemon fuzz attempts that never create a fuzz actor are a shop lifecycle failure, not typed argument packingreproduced; not fixed herefresh daemon times out after 120 s with no endpoint; a machine shell prints help but its top-level request is never servicedunknown; separate from language execution and T15assign to shop/listener lifecycle work; do not blame optimizer or fuzz
T17A terminal-owned foreground actor must not be reaped while awaiting cold realizationaccepted locally; focused/full greencaptured test controller died at 60.0239 s while the builder continued; owned starts now rely on the bounded terminal deadline/coupling; forwarding1/1 and full1918/1918no new actor field or target state; detached actors retain idle reapkeep; $start forwards explicit ar_timer, foreground terminal uses -1
T18A failed terminal session must not unlink a live daemon listenerpreservation accepted locally; accept stall remainscommand failure used to remove pit.sock from a live listener; rebuilt client preserved path/PID/listening FD and native predicate passed, though later info still stalledone PID-state check on failure; no Pit or generated-program statekeep; separately diagnose the now-visible accept-loop stall
P1Native execution needs real reduction/suspension safepointsaccepted composed candidateshootout tax normally about 1-2%; recursive native fib about +10%; forced mixed-engine/GC matrix passes+64 B native state/context; lazy 136 B four-entry host chunk; no per-frame word; generated text growskeep for correctness; optimize call accounting and outlined slow paths next

M8: exact Mach facts survive inlining

The earlier emergency response to a composed fannkuch miscompile disabled all Mach type facts in any host that had received an inline splice. Final-index _type_spans now let lowering prove the two operands at each exact use instead. Spliced hosts require exact covering spans; non-spliced hosts may retain the older conservative function-wide fallback. A compact overflow-chain regression returns 3000008629 in both lanes, and the targeted compiler suite passes 75/75.

The accepted implementation has no runtime toggle. The temporary A/B switch used on the experiment branch was removed before integration. Type spans exist only while lowering; the one-byte-per-Mach-instruction selection table remains part of serialized identity but is freed as soon as its choices have been rewritten into opcode bytes. The covered call_hot row improved 5.2% across independent processes (7.6% in the tight controlled pair) without changing its dispatch count. Root integration measured 13.59 ms Mach and 6.75 ms native, with exact results. Fannkuch and spectralnorm gain no typed operations from this fact set, so this change correctly leaves their dynamic behavior unchanged.

Rejected M9: arithmetic plus cold-arm jump fusion

Spectralnorm’s dynamic census attributes 57.7 million of 224.4 million Mach dispatches to unconditional jumps. Most follow successful generic arithmetic and skip the corresponding cold error formatter. A target-private prototype fused add/subtract/multiply/divide with a following forward jump while leaving the second word independently targetable and leaving backedge pause checks untouched. It changed no semantic mcode, serialized Mach word, frame, or heap state.

The prototype worked mechanically: 25.6 million fuse_add_jmp and 6.4 million fuse_mul_jmp executions reduced the census to 192.4 million dispatches. It did not work economically. Eleven-sample controlled medians were 277.30 ms fused and 276.48 ms disabled. The duplicated numeric handlers and resulting runtime layout erased the computed-goto savings. All 95 lines, four target-private opcodes, and the temporary A/B switch were deleted. This is strong evidence that reducing Mach dispatch count without also reducing numeric decode/encode work is not sufficient for this workload.

Rejected M10 prototype: instruction-pattern cold layout

The more promising formulation is to invert the last numeric guard and move its single success operation after a non-fallthrough disruption block. That removes the unconditional jump without enlarging a VM handler, and can improve native hot/cold layout too. A first instruction-array prototype required an exact conditional/success/jump diamond, label-only prefix, and a cold interval whose last executable instruction was disrupt; it transported disruption_pc and ran before final-index facts.

It was still not trustworthy. Compiler self-realization stalled and ultimately reported compile failed, including with the transformation limited to four small functions and then to the benchmark filename. No executable benchmark was produced. Restoring streamline.cm was not enough because the failed realization left generated cache state poisoned; clearing only this worktree’s .pit/build, .pit/cache, .pit/lib, and .pit/tmp restored normal focused compilation and the 13.56 ms exact call_hot result. The entire 177-line prototype was deleted. A retry must operate on verified basic-block identities, re-run CFG verification after layout, and round-trip control-flow destinations; local instruction-pattern proof is not adequate for this transformation.

Tooling findings

T8: paired streamline statistics

Both compiler inspection actors fetched raw mcode for stats mode, then discarded it and returned optimized-only counters despite the documented before/after contract. They now return paired detailed records plus scalar before/after/delta instruction and slot counts. The scalar fields are intentional: the generic CLI renderer otherwise collapses an array of nested records to function names only. pit compile streamline core/internal/heap.ce --mode stats now prints useful per-function rows (for example its main body reports 633 -> 240 instructions and 381 -> 44 slots). The inspect actor also realizes successfully with the same response shape.

T10: separate realization and execution deadlines

Removing the arbitrary large-function range-analysis cutoff made a cold vm_suite compilation exceed the test actor’s 30-second deadline. The runner started entry.start_time before $start, so realization, linking, actor startup, and test execution were all charged to one semantic timeout. Both the full suite and an isolated retry failed with zero VM assertions executed while the builder was still compiling.

The runner now gives realization its own bounded ten-minute deadline and resets the ordinary 30-second (or named slow-test) execution clock on the actor’s greet event. It reports Test realization timed out distinctly from Test timed out, and records setup duration separately. This does not make a hung builder unbounded and does not relax a running test’s deadline. The exact cold case then compiled beyond the former boundary and passed all 1,086 VM checks. The only retained cost is two timestamps and one optional duration number in the test-runner actor’s entry record; generated programs and production target runtime state are unchanged.

The first outer-lifetime experiment put reply_timeout in HELP metadata. The final CLI redesign rejected that layering: daemon and in-process terminal creation now apply one fixed 600-second transport bound, while actor descriptors contain only vocabulary. See docs/plans/perf-cli-reply-timeout.md.

T15/T16: differential-fuzz CLI reliability split

The original report combined two different behaviors. On a working in-process node, typed descriptor packing was correct: fuzz 500 --seed 20260713 reached internal/fuzz.ce, ran all 500 generated programs, and returned 3,733 passed checks, zero failures, seed 20260713, and process exit 0. The actor intentionally sent only its final reply, however, so the warm run emitted nothing for roughly one minute. It was running rather than stalled. A misleading exit-1 observation came from wrapping the child in macOS /usr/bin/time -lp: time itself could not query kern.clockrate in the sandbox. Running the child directly proved that Pit propagated the successful summary as exit 0.

Human fuzz commands now log an immediate start line and a progress line every 25 completed programs. --progress-every N changes that interval. Progress is log traffic, not additional replies, so the command still has exactly one final summary and --json remains one clean JSON object with no progress mixed into stdout. The accepted 500-program run printed progress from 25/500 through 475/500 and then returned the same exact 3,733/3,733 final result with exit 0. There is no generated code, target heap, frame, bytecode, or persistent runtime state cost; only the fuzz actor’s local interval value and diagnostic log text.

The standalone deterministic CLI regression uses two programs and --progress-every 1, keeping the expensive differential work small. It checks the start line, the 1/2 progress line, the final successful summary, and exit 0. A separate typed fixture returns a normal {passed:2, failed:1, total:3} reply; the same regression requires the details to be rendered and the process to exit nonzero. The cold standalone check passed 2/2 in 23.3 seconds, within its 30-second execution bound. The composed default suite passed 1,918/1,918 when run outside the socket-bind sandbox. The first sandboxed run’s six failures were all denied local socket/bind operations, not semantic failures.

The other behavior is independent and remains unfixed. A fresh daemon attempt timed out after the full 120-second readiness window with a live process and no endpoint. Its boot trace reached the shop actor and its observation trace built internal/builder_worker, but no listener-ready event followed. After one successful ps, the next client could likewise find the preserved socket but an unavailable endpoint. An in-process machine shell printed its initial help event, then its top-level fuzz request remained unserviced; the observation trace contained no fuzz actor creation and stopped in shop bootstrap after the builder-worker cache hit. Thus this is not fuzz duration, CLI packing, a fuzz timeout, or an optimizer result. It is a separate shop/listener forward- progress defect. T15 deliberately does not mask it or claim to fix it.

N5: hoisted declarations participate in parameter inference

Pit already had a conservative whole-program parameter inference pass. It accepts a function binding only when exactly one function value reaches it and every reference is a visible direct call; an omitted argument, mixed argument types, or any escape widens the affected parameter back to unknown. Parameters are immutable, so a successful proof is valid for every use in the callee.

The pass handled def f = function(...) but its AST walker did not record an ordinary hoisted function f(...) declaration as the value written to f in the enclosing scope. Later call resolution still knew the exact callees, which made this especially wasteful: every spectralnorm helper was statically called, but all of its array and numeric parameters were emitted as dynamic. The fix is one producer edge in resolve.cm; it changes no language rule, mcode operation, target ABI, runtime structure, or call convention. A regression proves direct hoisted calls lose redundant parameter guards, while a second regression takes the function value through an alias and proves the dynamic guards remain.

On the same root immediately before and after the fix, spectralnorm changed:

MetricBeforeAfterDelta
Mach median280.11 ms202.70 ms-27.6%
native median181.96 ms132.92 ms-26.9%
optimized instructions, all functions1,432444-69.0%
QBE IL bytes396,266190,279-52.0%
native assembly bytes258,683123,373-52.3%
exact type-span live bytes18,6728,944-52.1%
summed compressed frame slots9976-23.2%

The selected shootout matrix then measured spectralnorm at 202.67 ms Mach and 130.65 ms native with exact result 1623647009; executed Mach instructions fell from the prior 224,432,202 census to 134,704,082 (-40.0%). Fannkuch, which has no helper function, remained neutral at 297.00/196.31 ms. The same matrix produced mandelbrot 91.49/9.21 ms and binarytrees 82.32/56.86 ms with exact results. Binarytrees’ recursive parameter proof makes it a smaller win than spectralnorm because allocation and record traversal still dominate.

This is strictly favorable for constrained targets: generated code and frames shrink, no target metadata is added, and the inference map exists only while compiling. The optional PERF_FACTS=1 census added to perf_one.ce reports per-function instruction, guard, load/store, type-span, and resolved-call counts to make future missed proofs visible without affecting normal benchmark runs.

PERF_INSTRS=1 optionally prints optimized instructions, and PERF_FUNCTION=<name> restricts that dump to one function. This made the final two hot-loop guards readable directly instead of requiring a full IR dump.

N6: preserve parameter facts through final range cleanup

N5 used the resolver facts while generating semantic mcode, but then discarded them. Streamline therefore still saw every parameter as unknown. In spectralnorm it could not prove that j = j + 1 stayed in the int32 lane under the immutable integer bound j < n, leaving an is_int(j) check before every array access even though all visible callers passed integer n values.

Raw mcode now carries _param_types, an array of at most four closed-world facts. Streamline consumes it in write-type, array, and range analysis; exact integer parameters seed the range lattice with the conservative full int32 interval. After no-overflow reconciliation, type-check cleanup runs once more before final compaction and regenerates instruction-indexed facts afterward. Canonical mcode deliberately strips _param_types; it is neither portable mcode ABI nor target runtime metadata. Regressions verify that the fact is present in full compiler IR, absent from canonical serialization, removes a bounded index guard, and remains absent when a function escapes.

The immediate spectralnorm pair changed 201.99 -> 199.04 ms Mach (-1.5%) and 131.21 -> 128.74 ms native (-1.9%). QBE IL fell 190,279 -> 174,589 bytes (-8.2%), assembly 123,373 -> 114,160 bytes (-7.5%), optimized instructions 444 -> 428, and exact type-span live bytes 8,944 -> 6,896. The code and fact reductions are strictly favorable on constrained targets. Compiler-temporary cost is one at-most-four-element fact array only for functions with a proven parameter; it disappears from canonical artifacts.

T7: explicit mismatch values

aot_bench/perf_one.ce used to print only the native value after reporting a Mach/native mismatch. It now prints both values on every row. The dynamic-null oracle consequently identifies the current boundary directly as mach_result=15 native_result=0, rather than requiring a second custom driver.

T6: executable fuzz instructions

AGENTS.md instructed compiler/runtime work to run ./pit test fuzz, but the CLI rejects that form because fuzz is a separate actor. The instruction now uses the deterministic campaign form ./pit fuzz 500 --seed 20260713 and says explicitly that fuzz is not a pit test category.

T5: fact-safe IR diagnostics

aot_bench/dumpil.ce disrupted before printing an otherwise valid optimized function whenever _int_bounded, _no_overflow, or another optional fact was absent: json.encode(null) produced null and the diagnostic concatenated that as text. The dump now renders missing facts explicitly as null and includes _const_spans beside write/range facts. A fresh in-process spectralnorm dump completed and exposed the exact physical-slot/instruction intervals needed for the constant-carrier audit. This changes neither compilation nor execution; it is only developer tooling.

T1: POSIX daemon lifecycle

The first fresh benchmark attempt exposed a pre-existing lifecycle race: successive pit commands observed a disappearing socket before the old process had terminated and spawned replacement daemons. Detached benchmark actors then vanished without a result. The in-process fallback also halts the logger on some long fixtures, so it is not a valid general replacement.

The experiment branch therefore merged the already isolated codex/perf-lifecycle repair (70e135a8) as tooling-only work. It:

  • returns the exact detached child PID from the platform spawn API;
  • follows that identity through listener readiness and shutdown instead of treating a pid-file or socket transition alone as authoritative;
  • waits for both the captured process and endpoint to disappear on pit down;
  • refuses to start a replacement while a known process remains alive;
  • recomputes endpoint/process state after stale endpoint cleanup;
  • makes an already-down pit down idempotently successful;
  • bounds readiness at 120 seconds and shutdown at five seconds, reports progress, detects early child death and boot-log errors, and truncates stale boot diagnostics before spawn;
  • adds deterministic native lifecycle state tests.

This code is outside compiled Pit program execution and receives no benchmark credit. Its code-size cost is in the host CLI/runtime binary, not actor heaps or generated program state. It must pass its native test, repeated start/down integration, rebuild, and the default Pit suite before T1 is considered usable.

At campaign commit 2601786e, the deterministic lifecycle executable and Meson test both pass. A cold normal boot nevertheless remained alive without opening its listener for the entire bounded 120-second wait; its boot trace contained only process start and shop birth. After terminating only that exact worktree daemon, one in-process realization warm-up completed in 0.39 seconds and the following normal daemon opened its listener in 0.37 seconds. Thus T1 correctly diagnoses and bounds the failure, but does not claim to fix the underlying cold realization/listener omission.

The same unresolved omission recurred while removing C2. A seed build had correctly regenerated its boot payload, but the next binary restart left the exact pit internal/bootstrap --mcode process alive for 120 seconds with no listener. The lifecycle code produced the expected bounded diagnostic; it did not make progress. The process was verified by exact path/PID and stopped, and the generated payload from the rejected experiment was restored from the committed checkpoint. This remains costly iteration friction for large compiler experiments.

A later real command-path failure isolated the destructive half of the remaining listener problem. pit ps completed, but an immediately following client could not obtain an info reply within 200 ms. The client treated that transient probe failure as proof that pit.sock was stale, unlinked it, then noticed the captured daemon was still alive and correctly refused to breed a replacement. The result was a healthy-enough process made permanently unreachable by its own next client.

T13 makes cleanup conservative: an endpoint may be unlinked only when the captured process is positively known dead. A live process and a missing or invalid identity both preserve the endpoint and fail actionably. The info probe now spans five seconds, long enough to cross ordinary slow-turn pauses; if it still fails, a later retry can recover because the socket remains. The native policy test covers dead/live/unknown identity decisions. Direct and Meson lifecycle tests pass, and the reproduced failure retained both socket and PID; its immediate later retry succeeded. This does not cure a daemon that never opens a listener or a listener that remains unavailable indefinitely. These C functions allocate no Pit values, so the GC-rooting checklist is not applicable. There is no target actor/program memory cost.

T14: emitted IL participates in native cache identity

The focused runner exposed a correctness problem in its own evidence. After a QBE emitter change it printed the new IL and assembly sizes, but compile_native_ir returned the exact dylib path created before the change. The native cache key contained source, target, sanitizer flags, and numeric representation, but not the optimized IR or emitted IL. A benchmark could therefore report new compiler facts beside old machine code until .pit/build was manually deleted.

Native cache identity now includes a BLAKE2 hash of the complete QBE program: data, lazily selected helpers, and functions. The same helper constructs both the hashed text and the text passed to QBE, preventing formatting drift between identity and compilation. The obsolete source-only cache-probe API had no consumers and was deleted. On the composed TCO fixture, the pre-fix source key was 74f65e...; the exact-IL key became d7987e..., and an immediate repeat reused d7987e.... Compiler time gains one linear hash of material it already holds. Target code, runtime memory, actor memory, and executable behavior are unchanged; disk cache usage can temporarily grow when compiler output changes, which is the required behavior rather than a performance trade.

T2: post-boot native benchmark capability

PIT_ALLOW_NATIVE_LOAD=1 is not a sound benchmark bootstrap mode on this machine: the daemon can breed its shop actor but never open the listener. A normal daemon boot is reliable. A second attempt with a distinct opt-in environment marker reproduced the same stall, showing that the worktree’s realization/bootstrap context is sensitive to an altered PIT_* environment. The replacement keeps native symbol loading sealed throughout a normal, unmodified daemon boot and lets the benchmark actor explicitly open the capability only after the listener is ready. The capability is process-wide because dylib handles and their code already have process lifetime; it costs one atomic integer and no actor-heap state.

This is campaign tooling, not a proposed public language API or production security policy. It is intentionally callable without an environment gate in this contained branch so no pre-boot context changes. It must be removed or replaced by a reviewed development capability before any upstream merge.

The toggle was validated by compiling, loading, and timing native modules from a normally booted daemon. Fresh matching rows before the CLI probe ended the multi-row actor were: arith_int Mach 18.39 ms/native 10.69 ms, arith_poly 19.47/7.88 ms, and loop_nested 22.77/28.19 ms. The loop_nested regression relative to historical data must be reproduced before it is interpreted.

After the same explicit warm-up/normal-daemon sequence, the complete default suite passed 1,918/1,918 with the post-boot native-load capability present. This verifies the campaign baseline, not the safety of making the capability a public API.

T3: typed benchmark command

aot_bench/vs.ce now publishes a normal CLI command descriptor and does no work until it receives {type: "run", benches: [...]}. Consequently ./pit aot_bench/vs.ce spectralnorm fannkuch selects rows explicitly, answers the terminal help exchange immediately, and keeps the terminal coupled until the asynchronous native builds and timings reply. It no longer needs a BENCH_ONLY variable in the daemon environment and is no longer killed by the two-second courtesy probe. BENCH_ONLY remains only as an in-process fallback.

The one-row, two-row, and unmatched-name paths were exercised directly. This is a benchmark-tool usability change with no effect on emitted Mach/native programs and receives no performance credit.

T4: integer-proof IL diagnostics

aot_bench/dumpil.ce now prints _int_bounded and instruction-indexed _no_overflow facts alongside _write_types. This is diagnostic output only. It exposed the exact loop_nested native failure: both loop indices and their product are bounded integers, but the loop-carried s slot is globally typed num; QBE converts i*j to a double, adds it to decoded s, calls qbe_new_float64, decodes the result again, performs floating divide/truncate remainder, calls qbe_new_float64 a second time, and decodes once more on every inner iteration. Since % 7 proves s is in [0, 6], the recurrence can stay as a raw integer if range facts flow around the loop phi. This is the focused integer arm of N1 rather than a reason to special-case the benchmark.

Remaining lifecycle/CLI defects

  • A cold daemon can require an in-process warm realization before its listener starts. The bounded lifecycle wait reports this, but does not cure it.
  • The listener can still become unresponsive after a small number of client sessions. T13 prevents the next client from destroying the live endpoint and permits later recovery, but does not yet explain or cure the listener stall.
  • POSIX kill(pid, 0) reports an unreaped zombie as alive. Readiness/shutdown identity checks need a zombie-aware child-state path and a regression.
  • Resolved: bare scripts receive {} and wait for a reply; nonempty argv uses the canonical typed CLI envelope.
  • compile streamline --mode check does not check facts/types; it only lists functions over 200 slots. Generated-code auditing still requires direct mcode/QBE/assembly inspection.
  • Compiler inspection treats a .ce file as a complete actor and therefore rejects the benchmark fragments’ deliberate top-level return. The campaign uses aot_bench/dumpil.ce for those fragments; a future --fragment input mode would make the public compiler tool cover the same workflow.

Fresh baseline evidence

Current-tree rows with matching Mach/native results:

benchmarkMach msnative msMach instructionsallocationGC
arith_int18.3910.6921,000,0491 KiB0
arith_poly19.477.8820,000,0521 KiB0
loop_nested22.7728.1924,010,0491 KiB0
mandelbrot108.3028.0136,900,6541 KiB0
fannkuch297.85241.13268,248,8212 KiB0
spectralnorm305.97172.10266,116,65126 KiB0
binarytrees97.6660.7127,626,43694,869 KiB traffic62

Fresh comparison shootouts (same checksums):

benchmarkLuaJIT msOCaml ms.NET ms
mandelbrot43.533.882.91
binarytrees20.950.943.27
spectralnorm7.3410.583.54
fannkuch17.3313.7610.55

The typed benchmark command made the previously blocked spectralnorm and fannkuch rows reproducible. A repeated loop_nested row was 22.67/28.39 ms, confirming that native currently loses to Mach on this fixture rather than the earlier result being a single noisy sample.

Fresh call-path rows:

benchmarkMach msnative msMach instructionsnative allocation
call_hot (currently inlined)13.587.3918,000,0511 KiB
closure (dynamic)58.0730.4344,000,0972 KiB
fib (recursive)54.2428.7929,617,9461 KiB
tco_self89.1330.3080,000,0551 KiB

Erlang/OTP 29 JIT and Elixir 1.20.2 ports use the same counts/results for call_hot, fib, and tco_self, with two warmups and median-of-seven timing:

benchmarkErlang msElixir ms
call_hot8.068.03
dynamic_call025.0734.43
fib2.122.10
tco_self3.433.43

dynamic_call0 isolates indirect immutable-fun dispatch and has its own checksum; it is not presented as equivalent to Pit’s mutable captured-cell closure benchmark. The useful interpretation is narrower: Pit’s already inlined call_hot is at the BEAM known-call floor, but recursive native calls remain roughly 13.6x slower and Pit’s tail-recursive loop 8.8x slower. A thinner freeze-safe continuation is therefore necessary for recursion, while raw integer loop values and backedge lowering matter at least as much for TCO.

Rejected experiment A1: broad composed-helper inlining

Raising the ordinary hot-body limit from 64 to the existing 800-instruction hard cap, and the per-function inline count from 20 to 100, changed neither spectralnorm’s 266,116,651 executed Mach instructions nor its 13,355-line native IL. Size was not the blocker.

The next arm allowed an immediate child to inline even when it calls a function read from its level-1 parent. The existing splice already rewrites that capture to the parent’s exact slot; true siblings remained rejected. This flattened more of sn_atav -> sn_av/sn_atv -> sn_row_*, cutting native allocation traffic from 2,656 KiB to 26 KiB. It did not move native runtime: 172.10 ms became 172.20 ms. Mach measured 295.53 ms versus 305.97 ms, while its instruction count only fell to 266,052,432 (-0.024%), so most of the apparent timing change is not explained by removed work. Native IL grew from 13,355 to 15,578 lines (+16.6%). The compiler/shop realization also grew from roughly 11 MiB live to 28 MiB in this development fleet.

This is a useful negative result: row-call frame traffic is not spectralnorm’s runtime bottleneck. Broad flattening spends scarce static memory without fixing generic array access and repeated numeric encode/decode. Both source changes were deleted. A later inliner should be driven by the raw-value/array facts it actually unlocks and should outline duplicated cold diagnostics, rather than raising global limits alone.

Accepted experiment M1: Mach guard/branch fusion

The isolated implementation and measurements are recorded in docs/plans/perf-campaign-mach-guard-fusion.md. The root campaign integration reproduced the mechanism and results: mach_guard_fusion returned the golden 290 in both lanes; record_field ran 106.98 ms at 86,000,384 dispatches; fannkuch returned 3000008629 at 232,394,887 dispatches; spectralnorm returned 1623647009 at 227,632,203 dispatches and 282.78 ms. The latter is 7.6% below this root’s 305.97 ms pre-fusion baseline. Normal-daemon fannkuch timing was noisier (298.39 versus 297.85 ms) than the agent’s controlled fresh-process A/B, but its 13.4% dispatch removal reproduced exactly.

The fused handler preserves the predicate’s boolean result slot, leaves the second branch word independently targetable, and performs the same pause check when it takes a backward branch. It changes no semantic mcode, serialized Mach bytes, actor state, or per-code state. The approximately 984-byte shared text cost is justified by 4.5%-12.0% controlled improvements on affected composed programs. _has_inline_splice still disables typed fact handlers because the historical stale fact violation has not been disproven.

Root validation after integration: deterministic lifecycle test passed; the first cold full suite reproduced only the known realization-charged vm_suite timeout (832/833), isolated vm_suite passed 1086/1086, and the warmed full suite passed 1918/1918. The benchmark driver was also fixed to print a null native result without disrupting itself after a backend failure; the terminal still ends the coupled run on fatal native logs, so known-failing rows remain best run in isolation.

Accepted experiment M2: small AOT side tables

The native runtime previously allocated 1,024 persistent PitGCRef entries the first time an actor entered native code. On a 64-bit build the root array plus initialization bitmap is 17,408 bytes, even for a one-frame actor. This is C-side state and was absent from the actor heap counters, so ordinary heap benchmarks concealed it. The chunk size is now 32: 544 bytes for a shallow native actor, a 16,864-byte (96.9%) reduction. Capacity remains unbounded and grows in 32-frame increments; a 120-frame recursion uses four chunks (2,176 bytes) rather than one 17,408-byte chunk.

This changes neither the number of active GC roots nor generated code. It only allocates their backing storage in smaller increments. gc_recur passed at 120 native frames, and gc_recur_live passed at 200 frames while retaining arrays and forcing real collections. The closure, fib, and TCO timing samples remained within the host’s run-to-run noise, so M2 claims no execution-speed win. The runtime dylib file and aligned segments were unchanged; __text was eight bytes smaller. The native stress harness now schedules its non-disrupting deep GC cases before the deliberate panic case, because the current terminal ends a coupled harness after a caught native panic log.

The same allocation policy applies to two other per-actor native side tables. The fallback C-string key cache now starts at eight entries instead of 64 (128 versus 1,024 bytes on 64-bit), and the native function-code cache starts at 16 entries instead of 128 (384 versus 3,072 bytes). Both still double geometrically, so large native programs reach the same capacity; small actors no longer prepay for it. These changes affect closure/module initialization only, not hot lookup structure or generated code. Fresh closure/fib/TCO native medians were 30.26/28.95/29.52 ms versus 30.12/28.92/29.27 before M2, i.e. no measurable change. The additional constants did not change runtime binary size or section sizes. The required default suite passed 1,918/1,918 after the C rebuild. A sandboxed attempt reported six local bind/fd failures; the identical suite with normal local-socket permission was fully green, so those six were environment denials rather than language/runtime regressions.

That reordered gate exposed a pre-existing native correctness failure in gc_closure_alloc: it disrupts not a function with both the old 1,024-entry chunk and the new 32-entry chunk. It is not caused or hidden by M2 and remains a separate compiler/runtime task.

Accepted experiment M3: Mach text-guard fusion

The separately measured implementation and rejected alternatives are recorded in docs/plans/perf-campaign-mach-text-guard.md. is_text followed by an exact boolean branch now uses the same load-time, target-preserving fusion as M1. Controlled spectralnorm pairs removed 3,200,001 dispatches (-1.41%) and improved the median 284.90 -> 278.61 ms (-2.21%); record_field removed 2,000,001 dispatches and improved 104.05 -> 102.76 ms (-1.24%). Fannkuch and mandelbrot had no coverage.

The root integration reproduced the exact spectralnorm count (224,432,202 in the harness observation interval), checksum, and a 276.93 ms sample. The extended immediate/heap/GC regression returned 674 in both lanes. Serialized Mach blobs remain identical; there is no per-actor, frame, or loaded-code allocation, and aligned runtime segments did not grow. The isolated branch passed 1,918/1,918 default tests and 3,733/3,733 fuzz cases. Root’s native record_field lane still hits its pre-existing QBE type-fact error; the Mach count/result are valid and the same native failure predates M3. The combined root branch (M1+M2+M3) subsequently passed 1,918/1,918 default tests and 3,733/3,733 fuzz cases with seed 20260713.

Accepted experiment M4: exact record-literal capacity

Pit_NewRecordCap(n) used to approximate a known-field record as twice n + 1, then round that number up to a power-of-two hash-table capacity. The record implementation actually reserves slot zero and resizes only when the usable 1..mask slots would exceed 75% load. The approximation therefore put the overwhelmingly common two-field literal in a mask-7 table even though two fields fit the existing hash representation at its valid mask-3 limit. On the 64-bit host that is 144 bytes versus 80 bytes per record.

The constructor now chooses the smallest power-of-two-minus-one mask satisfying the exact invariant n * 4 <= mask * 3. This changes no record layout, lookup, mutation, stone, key, or GC semantics. It adds no shapes, templates, side metadata, or generated instructions. A native regression fills capacities at the 0/1/2/3/4/5/6-field boundaries and verifies that none resize; a 20,000-record stress case forces collection while keeping the current record rooted. It passes both the normal collector and a fresh-context nursery run with PIT_GC_NURSERY_VERIFY=1.

Controlled fresh-process binarytrees measurements, three samples per arm:

  • old Mach median 92.71 ms, native median 59.19 ms;
  • new Mach median 81.45 ms (-12.1%), native median 51.54 ms (-12.9%);
  • allocation traffic 94,869 -> 52,703 KiB (-44.4%);
  • collections 62 -> 33, with collection time about 20.9 -> 12.0 ms;
  • Mach dispatch remained 22,910,542, and QBE IL/native assembly sizes were byte-for-byte unchanged (160,457/110,413 bytes).

The checksum remained 674478 in both lanes. The default suite passed 1,918/1,918 and fuzz passed 3,733/3,733 with seed 20260713. Full-shop nursery verification is currently blocked before benchmarks by a known pre-existing old-record-to-nursery missed barrier in shop boot; the isolated fresh-context test verifies this sizing change itself under nursery movement. That separate barrier defect is being handled by the native-store experiment rather than hidden here.

Accepted experiment M5: exact-type guarded native stores

The implementation and full A/B evidence are recorded in docs/plans/perf-native-array-store.md. Slot compression now retains compact [physical slot, first final instruction, last final instruction, type] spans for compiler consumers. QBE uses an exact array fact at a contractually guarded store to take its existing inline indexed-store path, retaining capacity and growth handling but avoiding repeated dynamic receiver/key/stone dispatch. It also omits the generational barrier only for representation-defined immediate RHS classes (int, num, float, bool, and null).

The isolated paired median was 249.690 -> 206.072 ms for native fannkuch (-17.5%) and 22.351 -> 16.034 ms for two million numeric stores (-28.3%). The cost is 4,312 bytes (+3.70%) and 573 bytes (+1.97%) of assembly respectively. Spectralnorm has no qualifying stores and emitted byte-identical code. Exact type-span storage is compiler-only: 1,040 bytes for the store micro, 8,208 for fannkuch, 18,672 for spectralnorm, and 2,064-5,168 for the other shootouts. It is stripped from canonical mcode and adds no target heap, frame, or artifact metadata.

Root integration reproduced fannkuch at 206.261 ms with 120,825 bytes of assembly and the store micro at 15.974 ms with 29,592 bytes, both result-exact. The compiler tests passed 72/72, the default suite 1,918/1,918, and the differential fuzzer 3,733/3,733 with seed 20260713.

The same audit found two pre-existing native pointer paths missing a generational barrier: spare-capacity array push and writes into a captured old frame. Those fixes are a separate commit and retain barriers for pointer or unknown values. The fresh raw-context fixture passes an 8 KiB verified nursery with result 261633 across 17 minor collections. They receive correctness, not performance, credit.

Accepted experiment M6: absorbing numeric null in Mach

The language maps division by zero and other non-representable numeric results to null. A focused oracle found that Mach and native subsequently laundered that null through arithmetic: null + x, null - x, null * x, and null / x produced ordinary non-null values. The correct result is null for all four operations. The regression first creates null dynamically with 1 / 0, so constant folding cannot conceal the backend error.

A naive Mach repair that tested null around every numeric operation was correct but made spectralnorm 7.7% slower and was rejected. The accepted version changes only the existing arithmetic fast-path decoder: tagged integers become doubles, tagged null becomes a NaN poison, and floats decode directly. The existing non-finite result check then maps that poison back to null. It adds no Mach instruction, dispatch, bytecode word, frame slot, or dynamic state.

Controlled paired medians from the isolated experiment were:

  • float_math 18.82 -> 14.60 ms (-22.4%);
  • spectralnorm 286.55 -> 263.16 ms (-8.2%);
  • mandelbrot 105.03 -> 91.62 ms (-12.8%);
  • fannkuch had no meaningful change.

The runtime text grows by 484 bytes. Root integration reproduced float_math at 14.82 ms, spectralnorm at 267.53 ms, and mandelbrot at 91.22 ms, with exact checksums against native. Native remains unchanged by this Mach-only commit and still requires an independently measured correctness repair for the same dynamic-null oracle. Compiler tests passed 73/73 after integration. After M7, the combined root passed 1,918/1,918 default tests and 3,733/3,733 differential fuzz checks with seed 20260713.

Accepted experiment M7: LICM constants in integer range proofs

The final range pass could consume LICM constants only after slot compression, through exact _const_spans. An earlier reconciliation pass decides whether loop-carried arithmetic is provably integer before compression; at that point the hoisted divisor exists only in LICM’s _const_slots map. Because the proof could not see the constant 7, it conservatively typed (s + i * j) % 7 as generic num, and QBE repeatedly boxed, decoded, divided, and truncated the inner-loop recurrence.

LICM allocates each hoisted constant in a fresh logical slot with one producer. The reconciliation range pass now consumes that exact pre-compression fact. Compression then transports it to an instruction-bounded physical-slot span and destroys _const_slots; there is no global or stale physical-slot fallback. A regression verifies the divisor proof, no-overflow instruction fact, bounded accumulator slots, and retained integer write type.

In a controlled fresh pair, loop_nested native improved 29.41 -> 12.17 ms (-58.6%), while QBE IL shrank 15.3% and assembly shrank 9.1%. Root integration reproduced 12.17 ms and exact result 2; its current 24,673-byte IL and 22,749-byte assembly are 18.0% and 13.3% below the immediately preceding root artifact. Mach remains unchanged because this is native integer-region lowering. There is no target metadata, heap, frame, or runtime-code cost.

The same investigation fixed an independent native correctness seam: eq_tol/ne_tol previously read their tolerance directly from a frame slot, which is stale when the operand is promoted, raw, or elided. All three operands now use the backend’s value seam. This receives correctness rather than speed credit. Root validation passed 74/74 compiler tests, 1,918/1,918 default tests, and 3,733/3,733 fuzz checks with seed 20260713. The sandboxed default suite’s six local-bind failures disappeared in the required unrestricted run.

Accepted experiment N1: native raw-double regions

The implementation and its semantic boundary matrix are recorded in docs/plans/perf-raw-numeric.md. On nan64, promoted numeric locals and confined scratch values may remain QBE doubles. A NaN poison represents Pit null inside the raw region; canonical materialization restores tagged int, short float, or null at frame, call, capture, GC, and generic-use seams. F32 and fixed retain their representation-specific tagged lowering. No raw value is stored in a GC slot, and the experiment adds no runtime metadata, frame field, or heap state.

The isolated experiment cleared 1,918 default tests, 7,510 fuzz checks, f32 and fixed representation fixtures, and dedicated arithmetic/null/call/capture/GC oracles. Root reproduced the intended wins more strongly: mandelbrot 27.86 -> 9.21 ms (-67.0%) and float_math 6.38 -> 1.39 ms (-78.2%), with exact results. The exchange is static code: mandelbrot assembly 33,968 -> 40,052 bytes (+17.9%); float_math grows only 541 bytes (+2.0%).

The first root macro matrix found a release-blocking regression. Spectralnorm grew 256,300 -> 269,674 assembly bytes (+5.2%) and regressed 173.22 -> 192.83 ms (+11.3%); fannkuch grew 120,825 -> 124,967 bytes (+3.4%) and regressed 200.74 -> 209.57 ms (+4.4%). The backend currently knows that a slot is broadly num, but not whether this exact value was proven non-null by a preceding guard or was produced by nullable arithmetic. It therefore emits repeated number tests on generic paths that cannot retain a raw value. That checkpoint was not accepted at the time.

Point-local guard provenance (N4), structural raw-island eligibility, and the closed-world parameter repair (N5) removed the bad code shape. A controlled raw-on/raw-off comparison on the composed tree produced:

benchmarkraw onraw offruntimeIL / assembly effect
mandelbrot9.108 ms73.838 ms-87.7%-16.7% / -29.0%
float_math1.853 ms11.904 ms-84.4%-6.3% / -4.8%
spectralnorm125.630 ms125.938 ms-0.24% (neutral)-1.8% / -2.7%
fannkuch239.19 ms245.50 mstiming noisebyte-identical

The temporary raw-disable switch was deleted. Every lane returned the exact golden. Spectralnorm showed host drift in an earlier same-session pair, so it receives only a neutral claim; the static artifact is nevertheless smaller and the old macro penalty is gone. N1 is therefore accepted: it is transformative for float-heavy raw regions and structurally declines to engage in fannkuch.

An independent prerequisite fixes qbe_new_float64: it called the force-float internal encoder instead of public canonical Pit_NewFloat64, so exact integer results from generic native arithmetic lost their integer lane. The focused LCG/remainder oracle now returns 15 and d_field returns 290006 in both lanes.

N7 removes one remaining duplicate dynamic-number dispatch. The old generic path first tested is_num and then separately branched again to decode int or float. Each representation now supplies one value_to_double branch tree that decodes valid numbers and maps all other values directly to the NaN poison. Sticky-null semantics and canonical materialization are unchanged in nan64, f32, and fixed. On the current composed spectralnorm tree this moved native 128.74 -> 124.65 ms (-3.2%), IL 174,589 -> 170,554 bytes (-2.3%), and assembly 114,160 -> 112,376 bytes (-1.6%). It adds no target data or dynamic memory. The composed root passed 79/79 compiler tests, 1,918/1,918 unrestricted default tests, and 3,733/3,733 deterministic fuzz checks with seed 20260713. All seven native canonical/raw boundary rows retained their independent goldens.

M11 then repaired the corresponding Mach oracle. Numeric mcode may legitimately carry null after divide-by-zero or a representation overflow, but modulo, remainder, and the unary numeric handlers decoded that sentinel with the non-null helper and could turn it back into zero. They now use the same null-absorbing decoder as arithmetic. Separately, out-of-range source literals are canonicalized to null before Mach lowering; an access constant containing that value previously emitted no instruction and exposed the zeroed frame slot. Mach now emits LOADNULL (and handles boolean constants completely). The full 8,388,607-bit boundary oracle and all six companion raw/canonical rows agree in both lanes. The focused compiler suite is 80/80. A paired one-million-iteration intrinsic_ops run was neutral (23.725 -> 23.770 ms Mach). There is no dynamic, per-code, frame, or heap cost. On the release Darwin build, __text measured 1,030,980 -> 1,024,712 bytes, the mapped __TEXT segment stayed 1,196,032 bytes, and the file grew by 32 bytes; in other words, no mapped-page increase. Validation completed with 80/80 focused compiler tests, 1,918/1,918 default tests, and 3,733/3,733 deterministic fuzz checks at seed 20260713. The first sandboxed default-suite attempt had six expected loopback-bind denials; the normal-permission rerun was fully green.

Accepted experiment N2: direct integer constants, dead carriers

The exact _const_spans repair initially used LICM constants only for range proofs, raw numeric reads, and constant division. Other native handlers still loaded the physical frame slot, so functions that could not registerize all slots paid for an integer carrier store and repeated carrier loads even though the final instruction index identified the value exactly.

The QBE emitter now reconstructs an int32 constant’s tagged value directly at s_read. It deletes the corresponding access initialization only after a conservative scan of the constant’s exact final-index span proves every read goes through s_read or read_double. Any helper that consumes a physical slot number directly blocks the deletion. Promoted slots are also excluded: QBE already folds those carrier registers, and eliding their seed would leave an uninitialized SSA value. Physical-slot reuse outside the span remains a normal frame/register read. This changes neither semantic mcode nor runtime layouts, and canonical mcode still strips the compiler-only span facts.

Controlled source-toggle measurements on the composed tree gave:

  • fannkuch: 199.78 -> 191.03 ms in the selected matrix (-4.4%); an independent 11-sample focused run measured 190.04 ms. IL fell 172,344 -> 170,463 bytes (-1.09%) and assembly 118,136 -> 111,662 bytes (-5.48%);
  • spectralnorm: an off median of 119.26 ms bracketed on medians of 116.63 and 118.12 ms (-2.2% and -1.0%). IL fell 170,554 -> 169,771 bytes (-0.46%) and focused assembly 112,376 -> 109,948 bytes (-2.16%);
  • binarytrees assembly fell 106,598 -> 104,860 bytes (-1.63%) with identical 57.61 ms matrix medians;
  • loop_nested IL fell by only 28 bytes and assembly was identical, confirming that already-promoted constants receive no artificial speed credit;
  • mandelbrot and float_math artifacts were unchanged.

Both distinct-constant/one-physical-slot and constant-then-runtime-value collision oracles remain exact (352 and 1,000,000 in Mach and native). A new IL regression uses an uninlined call to disable registerization, proves the constant frame store is absent, and proves the tagged literal is substituted at its use. The focused compiler suite is 81/81. There is no heap, frame, side table, or runtime-text cost; affected AOT programs are smaller. Final validation passed 81/81 focused compiler tests, 1,918/1,918 default tests, and 3,733/3,733 deterministic fuzz checks at seed 20260713.

Rejected experiment A2: direct exact-array reads

QBE can use an exact array span to inline forwarding chase, index bounds, and element load instead of calling the dynamic property helper. The prototype was semantically exact and changed no mcode/runtime state, but the existing helper is already compact: array_read improved only 26.04 -> 25.30 ms (-2.8%) while IL grew 2.9% and assembly 1.5%. Spectralnorm grew from 390,232/256,300 to 393,832/257,477 IL/assembly bytes and measured 173.22 -> 174.55 ms. The implementation was deleted. A second retry after N5/N6 reached only 25.348 -> 25.225 ms (-0.49%) on array_read while growing IL 2.70% and assembly 1.42%. Spectralnorm grew IL 3.47% and assembly 2.38%; alternating timing pairs crossed, for only about 0.3% by average and no reproducible win. Future array work should remove surrounding representation/guard traffic as a region, not paste a larger helper body into every access site.

Accepted experiment A3: shared exact-array helper arm

A2’s useful part can be expressed once rather than copied at every read site. The existing private __load_index_ss QBE helper now has an exact mode selected by the complement of its array-slot argument. Its ordinary non-negative ABI is unchanged. The exact arm follows forwarding headers, checks negative and upper bounds, loads the element, and returns null out of bounds; it omits only the receiver and integer-key checks already established by compiler facts.

Composition review found and fixed an important first-version oversight. The initial selector proved only the receiver and therefore could have treated an unknown key as a tagged integer. The accepted selector independently requires both an exact array receiver and exact int key, using immutable _param_types for parameters or final-index _type_spans for locals. A regression exposes an exact array through a function whose key remains externally unknown. A3 keeps that site dynamic; A7 below later validates the key inline before selecting this same exact helper. The dual-lane execution oracle covers an in-bounds read plus negative and exact-upper-bound null results.

On the composed N2 tree, a controlled source toggle gave spectralnorm native medians of 119.44 ms exact, 121.18 ms dynamic, and 118.41 ms exact again. The array_read micro moved 25.12 -> 25.01 ms. At exact sites the spectral artifact shrinks 170,904 -> 170,659 bytes IL and 110,668 -> 110,525 bytes assembly. Including the one shared arm, however, the complete artifact is 888 bytes IL and 577 bytes assembly larger than N2 alone (about 0.52% each). This is the real tradeoff: no heap, frame, actor, side-table, or semantic-mcode change, but a small fixed amount of native text per generated unit. It is worth retaining in the fastest profile for the repeatable spectralnorm gain; a future ROM-minimal profile may omit it. Final validation passed 84/84 focused compiler tests, 1,086/1,086 isolated VM checks, 1,918/1,918 warmed default tests, and 752/752 deterministic differential-fuzzer checks at seed 20260713.

Accepted experiment A7: native checked-array-load regions

Fannkuch still carried three compiler-generated regions of this exact form after A3:

is_int scratch, key
jump_false scratch, load_null
load scratch, exact_array, key
jump load_done
load_null:
null scratch
load_done:
move result, scratch       // optional

The native emitter used to materialize the predicate as a tagged Pit boolean in the frame, reload it for the branch, call a helper into the scratch slot, cross two labels, then copy the element to its semantic destination. The new native-only region recognizer emits one key-tag test, calls A3’s existing exact array helper directly into the semantic result slot, and writes null there on the non-integer edge. The helper still follows forwarding headers and performs negative/upper-bound checks, and the loaded element remains an unchanged tagged PitValue. There is no element-type assumption, raw array storage, mcode change, new helper ABI, or runtime-layout change.

The recognizer is deliberately strict. The receiver must be exact at the load’s final instruction index. Both generated labels must have exactly one explicit predecessor. A trailing scratch move is deleted only if a small CFG liveness walk proves the scratch dead on every successor; otherwise the scratch remains the result. Functions with disruption handlers decline the fold. Promoted keys are spilled before the helper reads the frame. These gates preserve dynamic keys, mixed elements, mutation/aliasing, stoned reads, GC forwarding, null on a non-integer key, and null for negative or out-of-bounds integer keys. A manual IR regression adds a second predecessor to the null label and proves the region stays unfused.

A smaller first arm merely transported the adjacent integer fallthrough fact to the load selector. It raised exact-helper sites from four to seven and reduced fannkuch from 171,432 to 171,309 bytes of QBE IL and from 111,768 to 111,693 bytes of assembly text, but timing was only about 0.3% and crossed noise. It was deleted. Folding the surrounding region is the part that matters.

Two separately compiled dylibs were loaded together and timed in alternating order for 31 pairs. Guard-only fannkuch measured 188.89 ms median; the region fold measured 184.17 ms (-2.5%), with exact result 3000008629 in every run. The final ungated source measured 183.75 ms standalone. Emitted fannkuch shrank 171,432 -> 170,411 bytes of IL, 111,768 -> 111,220 bytes of assembly text, and 30,484 -> 30,348 bytes of linked __text; the 98,304-byte dylib file and mapped segments remain unchanged because of page alignment. There is no target heap, frame, actor, side table, or per-code metadata. The only memory cost is transient compiler maps proportional to labels plus three tiny selected-site records.

The recognizer selects zero sites in array_read, mandelbrot, spectralnorm, and binarytrees, so their emitted programs are unchanged. Final observed medians were array_read 24.65 ms, mandelbrot 9.11 ms, fannkuch 183.36 ms, spectralnorm 120.11 ms, and binarytrees 60.27 ms; all Mach/native results agreed. The widened dual-lane oracle returns 127 while exercising an escaped callable, local exact array, heap text/float/record elements, mutation through an alias, stoning, a text key, a negative key, and the exact upper bound. Focused compiler tests pass 88/88, the warmed default suite passes 1,918/1,918, and deterministic fuzzing passes 7,525/7,525 checks at seed 20260713.

Tooling note: one self-host recompilation of the enlarged qbe_emit.cm crashed in the pre-existing mcode_lower_func_node failure before any benchmark ran. make seed completed successfully and subsequent compiler/full runs were clean. This is not attributed to generated program code, but it materially slowed the experiment and remains worth fixing.

Rejected experiment C2: direct symbols at resolved calls

_resolved_calls already proves a local callee body and arity at an exact call site. A native-only prototype used that fact to prepare a normal GC-scanned stack frame holding the real closure, call $pit_fn_N directly, and leave via the existing LIFO frame path. This retained captured outer-frame behavior, under-application null fill, stack limits, disruption propagation, and the same eventual suspension seam; unknown and polymorphic sites stayed on pit_rt_call_dynamic.

The extra specialization was a loss. With inlining disabled so two million known calls remained, call_hot moved from 27.06 to 28.67 ms (+6.0%). IL grew 36,749 -> 36,928 bytes and assembly 31,627 -> 31,745 bytes. The direct branch saved function-kind/code lookup and an indirect function-pointer call, but added another prep ABI and control merge; the tuned dynamic helper already does its no-GC frame bump, initialization, push, call, and pop compactly. Polymorphic call0_plain and closure did not select the path, as intended. The runtime helper and emitter lowering were deleted. The retained benchmark and PERF_DISABLE_PASSES option make future call experiments falsifiable without confusing inlining with call mechanics.

Accepted experiment M12: typed Mach array indexes

Generic Mach load and store handlers must distinguish arrays from records and validate the key representation before reaching the compact array helper. Streamline already proves both facts at many hot sites. Mach lowering now consumes the same two independent proofs used by native A3: immutable _param_types or an exact final-instruction _type_spans interval must prove the receiver is an array and the key is an integer. Inline-spliced hosts may use exact intervals but may not fall back to function-global write types.

An exact site is emitted directly as the target-internal load_ai or store_ai opcode. This does not add a semantic mcode operation. Each remains one 32-bit Mach instruction, so the measured Mach blobs are unchanged in size (array_read 4,472 bytes, fannkuch 34,032, spectralnorm 30,176). Direct lowering also deliberately avoids the serialized one-byte-per-instruction fact sidecar: programs that had no arithmetic facts do not acquire a fact section merely for array access. The loaded program gains no table, frame slot, heap allocation, or actor state. Lowering temporarily allocates at most three additional one-byte-per-slot type maps plus 12 bytes per retained array span, all freed before the Mach artifact is returned. On the release Darwin build the linked __text happened to move from 1,024,712 to 1,024,628 bytes and the mapped __TEXT segment stayed 1,196,032 bytes; this is treated as zero mapped-ROM/RAM growth, not as a claimed size optimization.

The specialized handlers preserve the generic array helper’s negative and upper-bound behavior, stone check, forwarding repair, resize/GC reload, and write barrier. Debug contracts disrupt if compiler metadata ever selects the handler for a non-array or non-integer. Executable regressions count dispatched opcode names: optimized source must execute both typed load and store, while bare semantic mcode without proof metadata must execute the generic handler.

A temporary compile-time A/B switch, deleted before acceptance, produced these fresh-process, three-run medians with exact independent results and identical dispatch totals:

  • array_read: 23.44 / 25.15 / 22.81 ms (typed/generic/typed), a 6.8-9.3% win;
  • fannkuch: 270.87 / 280.84 / 271.51 ms, a 3.3-3.5% win. Of 17.47 million loads and 14.83 million stores, current proofs select 6.16 million loads and 3.27 million stores;
  • spectralnorm: 194.27 / 200.09 / 198.06 ms, a 1.0-2.9% win; all 6.40 million array loads and 16,000 stores select the typed handlers;
  • binarytrees has no selected array access and is behaviorally unchanged.

The final no-toggle build passed make seed, 86/86 compiler-specific tests, 1,918/1,918 default tests, and 752/752 deterministic differential-fuzz checks at seed 20260713. Generated boot artifacts were restored after proving the seed path so this experimental checkpoint contains only source, tests, and its audit record.

Accepted experiment M13: local integer-guard consumption

M12 exposed the next missing fact without requiring a general dataflow engine. Fannkuch’s array elements are semantically numbers, so an index loaded from an array correctly has type num, not globally int. The generated mcode guards each use with the exact local sequence is_int predicate, key followed by jump_false predicate, error; the following load executes only on the true fallthrough. That short-lived fact was not represented in _type_spans, leaving 10.06 million hot reads on the generic Mach handler.

Mach lowering now recognizes only that exact adjacent three-instruction shape. It requires matching predicate and key slots, jump_false direction, and an independently exact array receiver. A label, intervening instruction, opposite branch, mismatched slot, or unproved receiver declines the optimization. The resulting load uses M12’s existing one-word load_ai; no opcode, mcode, blob byte, fact sidecar, frame slot, or runtime table is added. An executable regression loads an index from an array (therefore only num globally) and proves that both the constant-index read and the locally guarded read dispatch through load_ai with the exact result.

A deleted A/B environment gate gave fresh-process fannkuch medians of 276.23 / 294.61 / 275.46 ms (local proof on/off/on), an incremental 6.2-6.5% win over M12. Typed loads rise from 6,163,834 to 16,223,440; generic loads fall from 11,306,664 to 1,247,058. Total dispatches (225,863,029), the 34,032-byte blob, and result 3000008629 are identical. Array_read and spectralnorm already had global/exact facts, so their opcode counts and artifacts are unchanged.

The release Darwin build grows linked __text from 1,024,628 to 1,025,228 bytes and unwind information by 8 bytes; the mapped __TEXT segment remains 1,196,032 bytes. This approximately 600-byte shared-runtime cost is worthwhile for the additional fannkuch shootout gain, even for constrained targets. The final no-toggle build passed make seed, 87/87 compiler-specific tests, 1,918/1,918 default tests, and 752/752 deterministic differential-fuzz checks at seed 20260713. Generated boot artifacts were restored after the seed gate.

M14 tried to carry the same local integer proof across the compiler’s complete checked-store diamond (is_int, length, lower bound, upper bound, store). A strict recognizer converted all 14.83 million fannkuch stores to store_ai, but the controlled result regressed: 280.79 and 275.05 ms specialized versus 272.06 ms with the local store proof disabled. The prototype also added 1,272 bytes of linked __text plus 38 bytes of string/unwind section content beyond M13. The array receiver branch is evidently predictable enough that the larger lowering recognizer/handler layout has negative value. The recognizer and its measurement switch were deleted; the rejected result is retained here to prevent dispatch-count enthusiasm from reviving it without new evidence.

Accepted experiment M15: allocation-free Mach array stores

M14 showed that deleting only the receiver branch was not useful. The larger cost was shared by both generic and typed handlers: every array store called a C helper, reloaded the current frame, and chased forwarding state even when the array already had capacity and no allocation could occur.

Mach now handles that common case in one shared inline helper. It resolves an old forwarding pointer, preserves the stone check, fills any legal gap with null, updates length, stores the value, and runs the generational write barrier inside a GC-forbidden region. Because that path cannot allocate, the handler writes a resolved receiver back to its frame slot and continues without a frame reload. Capacity exhaustion delegates to the old helper, which retains its rooted value, grow/GC behavior, and post-GC forwarding repair. A stone disruption still reloads the frame before unwinding. The same helper serves generic store, exact store_ai, and the field-fused store’s defensive array arm; there is no compiler fact, bytecode, frame, heap, or actor-state change.

A deleted runtime A/B gate produced fresh-process fannkuch medians of 264.73 / 304.50 / 267.01 ms (fast path on/off/on), a 12.3-13.1% win. The final no-toggle build measured 261.61 ms. The result (3000008629), 225,863,029 dispatches, 34,032-byte Mach blob, allocation, and GC counts are unchanged. Focused dual-lane checks cover existing and growing arrays, old-to-young pointer write barriers/self-reference, null stores, and stoned-array disruption. The release Darwin __text grows from 1,025,228 to 1,026,708 bytes (+1,480); the mapped __TEXT segment remains 1,196,032 bytes. That shared-code cost is a strong trade even for constrained targets because it removes work from every hot existing-capacity array store without adding per-program memory.

The final no-toggle build passed make seed, 55/55 focused Mach tests, 1,918/1,918 default tests, and 752/752 deterministic differential-fuzz checks at seed 20260713. Generated boot artifacts were restored after the seed gate.

Accepted experiment M16: inline pure Mach array reads

M15 made the common array store allocation-free but every array read still crossed a C helper boundary. That helper takes a context it does not use and performs only forwarding chase, bounds checks, and an element load. Mach now uses an internal static inline form of those same operations in generic, typed, and field-fused array-read handlers. Text and record reads retain their existing helpers. There is no allocation, write barrier, semantic mcode, bytecode, frame, heap, actor-state, or per-program metadata change.

The first timing attempt used a runtime switch inside each hot read and was discarded because the switch itself changed the result. A fair compile-time binary comparison on the complete M15 tree measured:

  • array_read candidate 21.77/23.29/23.63 ms, baseline 25.35/25.43/25.69 ms, then candidate 24.15/23.78 ms: normally a 6-8% win, with the best cold pair larger;
  • fannkuch candidate 256.37/253.87 ms, baseline 261.83/273.92 ms, then candidate 255.66/260.62 ms: approximately 2-6% on this noisier shootout;
  • spectralnorm candidate 207.40/201.80/202.56 ms, which is treated as neutral rather than claimed as a win.

Results and dispatch counts remained exact. The release Darwin linked __text unexpectedly shrinks from 1,026,708 to 1,025,852 bytes (-856), likely because the compiler can now eliminate or lay out the old helper/call sequence more compactly; mapped __TEXT remains 1,196,032 bytes. The important memory claim is only that this adds no data or per-program memory. The final build passed make seed, 87/87 compiler tests, 1,918/1,918 default tests, and 5,623/5,623 generated differential cases from 752 iterations at seed 20260713.

Accepted experiment A6: raw exact-array numeric results

A3 still returned a tagged PitValue, after which the generated code stored it to the frame, reloaded it, tested whether it was numeric, and decoded it for the immediate arithmetic operation. The new nan64-only arm is selected only when the receiver is independently proven to be an exact array, the key is independently proven to be an exact integer, and the compiler’s exact control shape is load; jump join; null alternate; join; is_num; jump_false; followed by an immediate multiply or divide. The other arithmetic operand must also be an exact numeric value. If the loaded value remains live after either the success or disruption path, the ordinary tagged path is retained.

One shared QBE helper follows forwarding headers, checks both bounds, loads the element, and decodes it to a raw double. It also writes the original tagged element into its semantic destination frame slot, so GC scans, disruption diagnostics, and frame inspection see the same state as before. Invalid, out-of-bounds, or nonnumeric values produce a quiet-NaN poison in the temporary raw lane; the existing is_num branch rejects that poison and follows the unchanged error edge. The null alternate also writes canonical null to the frame. The helper is emitted lazily only in generated units that contain a proven site. No semantic mcode operation, runtime object layout, actor state, frame slot, heap allocation, or persistent side table was added.

The composed spectralnorm source toggle measured 122.082 ms with the arm off, then 114.509 and 114.919 ms with it on; a final repeated on run measured 114.232 ms. The controlled artifact changed from 170,659 to 169,495 QBE IL bytes (-1,164, -0.68%) and from 110,525 to 110,740 assembly bytes (+215, +0.19%). The small assembly cost is shared helper/control text, not replicated per element access, and is justified by the repeatable approximately 6% macro win. raw_array_numeric independently checks int and float array elements and returns exactly 3.75 in both Mach and native lanes.

A broader first arm allowed addition and subtraction. It made array_read about 31% slower (25.15 -> 33.03 ms), because preserving the tagged result is cheaper for an integer element that immediately takes the integer arithmetic path. That arm was rejected. The final multiply/divide and overflow-proof gate makes array_read byte-for-byte identical to the disabled build: 37,112 IL bytes and 31,725 assembly bytes, with a selected native median of 23.66 ms. Mandelbrot, fannkuch, and binarytrees retain exact results and do not acquire a helper unless their final proof/use shape qualifies.

The liveness audit also found that the shared fixed-shape use table omitted the source operand of get(dest, source, depth). Treating get as unknown made the analysis reject a safe spectral site after an unrelated inlined helper. The table now records source position 2. The dedicated test_qbe_raw_array_liveness_tracks_inlined_get_source fixture contains that shape; deleting the fact makes exactly that test fail because the raw helper is not selected. This correction can only make liveness more accurate for a known read operand; unknown and variable-shape instructions remain conservative.

Final validation passed 87/87 focused compiler tests, 1,918/1,918 default tests, and 3,733/3,733 deterministic fuzz checks at seed 20260713. A broader invalid/OOB native disruption oracle exposed an existing nested-native disruption halt both with A6 enabled and disabled, so it is not attributed to this experiment; the helper’s error semantics are instead covered structurally and by the unchanged generic error edge.

Accepted experiment M17: remove TCO’s consumed callee lookup

Self-tail-call elimination runs after the ordinary optimization cycles. It replaces the resolved call with parameter rebinds and a jump, but it previously left the pure level-one get that loaded the self closure for that call. No later dead-value pass removed it. tco_self therefore chased the outer frame and loaded the same unused function value on every one of five million loop iterations.

The TCO pass now performs one narrow post-rewrite use scan and replaces only an unused, non-captured get destination with a compiler nop. It does not attempt general dead-code elimination and cannot remove allocating, disrupting, or otherwise observable operations. Final nop compaction transports the existing instruction-indexed facts in the usual way.

On the current composed tree the exact Mach result remained 12500002500000; the dynamic census changed from 35,000,025 to 30,000,025 instructions and getup from 5,000,002 to 2. The Mach artifact shrank from 2,240 to 2,176 bytes. An eleven-sample focused run moved 52.125 to 46.212 ms (-11.3%). Native QBE IL shrank 32,756 to 32,361 bytes and assembly 28,139 to 27,907 bytes; native timing remained about 30 ms, so it receives size but no speed credit. There is no runtime code, frame, heap, actor state, bytecode metadata, or compiler side table. A focused compiler regression requires both the TCO loop label and absence of the consumed get; the compiler suite passed 91/91. Full and fuzz gates remain pending composition with the suspension work.

Accepted experiment M18: fuse checked Mach array stores

Fannkuch still executed the compiler’s complete checked-store diamond for every array write: integer guard, length load, lower and upper bounds, store, and the branches around the cold failure arm. After final instruction numbering, streamline now recognizes only that exact region and emits sparse _mach_checked_stores pairs containing its head and nine- or ten-word length. It also proves that the lower bound is exactly zero at that use. Mach lowering validates the compact metadata, preserves load-time incoming-edge and disruption checks, and stamps a target-private fused handler. This adds no semantic mcode operation. The compiler fact is stripped from canonical mcode, and the runtime no longer carries a second full instruction-pattern matcher.

All six fannkuch regions fuse. The exact result remains 3000008629; dynamic Mach instructions fall from 225,863,029 to 168,026,476 (-25.61%). Exclusive fresh-process outer medians moved from 265.290 to 227.443 ms (-14.27%). The cleanup is also faster than the first fusion implementation’s noisy 239.585 ms median, while recovering 3,120 bytes of its linked __text. Native artifacts are unchanged, so M18 claims no native speed improvement.

Against the no-fusion binary, Darwin linked __text grows 1,034,136 to 1,037,928 bytes (+3,792); total __TEXT section content grows 4,227 bytes. The mapped __TEXT segment remains 1,212,416 bytes, the dylib file grows only 32 bytes after padding, and the serialized fannkuch Mach file remains 4,254 bytes. There is no persistent program, heap, frame, or actor-state cost. Parsing the fact and checking incoming edges adds about 2.06 microseconds once when a new code object is loaded; loaded code is cached. This is a strong default trade, but a future extreme sub-288 KiB build-profile audit may compile the feature out. There is deliberately no runtime toggle.

Final validation passed 93/93 focused compiler tests, 1,086/1,086 VM tests, 1,918/1,918 default tests, and 3,733/3,733 deterministic fuzz checks at seed 20260713. Malformed, overlapping, and invalid-head metadata arrays fail closed; the executable regression also covers append-at-length and canonical fact stripping.

Correctness prerequisite: actor reductions and native suspension

The implemented architecture, reduction policy, memory limits, cross-engine matrix, and measured tax are recorded in docs/plans/perf-native-suspension-unified.md.

The composed candidate now amortizes checks at backedges and call boundaries, spills promoted/raw values before slow paths, persists resume segments in Pit frames, unwinds the host C stack, and resumes mixed Mach/native chains under a moving GC. It adds no semantic mcode operation or per-frame word. C modules remain the explicit non-freezable boundary.

The main remaining performance issue is call-only recursion: native fib is about 10% slower because it cannot rely on a loop poll. Slow poll/resume code also grows generated text, notably spectralnorm assembly by about 10%. Future work may change accounting placement or outline cold code, but must retain one bounded observation path around every cycle and across Pit call chains.

Source: plans/archive/perf-2026-07/perf-campaign-2026-07.md