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

Structured native numeric lanes for self-TCO

Status: contained performance experiment, validated and ready for composition review.

Result

Self-tail recursion already becomes an ordinary backedge in semantic mcode, but the native emitter treated its parameter slots as tagged frame memory. In tco_self, five million iterations repeatedly decoded, checked, encoded, and stored both n and acc. This experiment keeps the loop phis in the native representation each value actually needs:

  • n is a raw signed integer. Its external entry is a tagged int and every recursive edge is the guarded recurrence n = n - 1 on the n > 0 path.
  • acc is a raw double. It enters as an int but acc + n may leave int32, so treating it as an integer would be wrong. Normal nan64 canonicalization still applies when the value becomes observable.

The semantic mcode instruction set, Mach encoding, runtime object layout, frame layout, and public ABI are unchanged.

Proof and lowering

The proof is deliberately split at the same seam as the program:

  1. Resolver parameter inference already rejects functions whose callable value escapes. It now retains a second optimizer-only type vector containing only calls from outside the callee. Self calls still participate in the ordinary parameter merge. For sumto, the ordinary phi types are num,num, while the external entries are int,int.
  2. The existing TCO pass records the label of the loop it creates. This is lowering identity, not a new operation.
  3. QBE builds the compact instruction-index CFG it needs for the candidate. An integral parameter is accepted only if every backedge assigns it from the exact recurrence p - 1, every external entry is int, no other definition exists, and the recurrence is unreachable when the false edge of p <= 0 is removed. That reachability test is the dominance proof for p > 0.
  4. The proven subtraction receives a point-specific no-overflow fact. The parameter register and comparison therefore remain untagged. Other numeric TCO parameters use the existing raw-double/NaN-poison lane.
  5. All existing spill/reload paths remain authoritative. A suspension at the backedge materializes live raw values into tagged GC-scanned frame cells, stamps the resume segment, and decodes them again after resume. Calls, captures, handlers, and unknown definitions conservatively reject or spill.

This is the smallest useful structured mid-end slice found in the campaign: it creates value identity for an entry/backedge phi and proves one edge predicate, without asking slot-global write types to represent two different values.

Controlled performance

Apple arm64, composed suspension+A6+A7 checkpoint, perf_one.ce, native-only, three sequential 11-sample arms in an exclusive timing window:

native tco_self armmedianchange from taggedQBE ILassembly
tagged parameters30.561 msbaseline34,066 B29,168 B
both parameters raw double19.600 ms-35.9%33,070 B29,714 B
n raw int, acc raw double5.312 ms-82.6%30,283 B27,271 B

All arms returned exactly 12500002500000. The split representation is also 72.9% faster than the all-double arm. It is within about 55% of the recorded OTP 29 JIT result (3.43 ms), rather than 8.9x slower as the old 30.30 ms row was. The generated artifact becomes smaller: IL -11.1%, assembly -6.5%.

The broad first prototype promoted numeric parameters in all eligible functions. It improved TCO but made ordinary recursive fib materially worse because every real call had to spill/reload promoted arguments. That arm was deleted. Parameter promotion now requires a loop identity emitted by TCO.

Nonselected macro checks remained on their composed artifact paths and returned their exact goldens: loop_nested 12.247 ms (27,255 B IL), mandelbrot 8.750 ms (63,628 B), spectralnorm 116.023 ms (184,086 B), and fannkuch 183.650 ms (178,984 B). These are no-change checks, not claimed wins.

Memory tradeoff

There is no generated-program heap, frame, actor, context, or per-code metadata cost. The target carries fewer instructions for the selected loop.

Compiler-only retained data adds one parameter-entry type array to a function whose closed-world external calls establish at least one type, plus one TCO entry-label field for a transformed function. For this two-argument fixture on host nan64, the array is 32 bytes (16-byte array header plus two PitValue words); its type strings and entry label already exist as shared compiler texts. The QBE CFG worklists/maps are temporary. Compiler time and temporary memory were not gated in this campaign, but none of this reaches N64/GBA executables.

Correctness gates

  • Exact fact regression: ordinary recursive phi type differs from external entry type, and TCO identity survives optimization.
  • Exact QBE regression: the decrement phi is l raw integer while the accumulator phi is d raw double.
  • Dual-lane golden: tco_self matches Mach at 12500002500000.
  • Forced reduction/GC fixture: native_suspend_tco_raw.ce repeatedly suspends the selected loop, collects while suspended, and resumes. The final composed runtime returned exactly 1250025000 across 49 suspensions and 49 forced collections, with maximum observed native depth 2.
  • Focused compiler tests passed 95/95 after adding rejection cases for a float external entry and for a decrement not dominated by a positive guard.
  • The full default suite passed 1,918/1,918. The first sandboxed attempt reached 1,901 passes but denied six local socket binds; the unrestricted rerun was clean, so those failures were environmental rather than compiler regressions.
  • Deterministic fuzz passed 3,733/3,733 at seed 20260713.

The final branch also composes the two native suspension follow-ups: resumed Mach disruptions remain on the Mach path, and the common native frame-entry path avoids activation work that only resumed calls need. Neither changes the TCO proof or representation; they make the suspension checkpoint used by this experiment authoritative.

Next extension

The valuable abstraction is not “TCO special casing”; it is a compact phi with entry facts, edge facts, and a representation choice. The next native experiment should reuse this infrastructure for ordinary loop-carried locals and for recursive call arguments that cannot be eliminated into a backedge. Captured/address-taken values and suspension seams must continue to materialize.

Source: plans/archive/perf-2026-07/perf-structured-tco-numeric.md