Skip to content

feat: CFG-aware liveness — VCR-RA-001 allocator substrate (#242) - #268

Merged
avrabe merged 1 commit into
mainfrom
feat/vcr-ra-cfg-liveness
Jun 5, 2026
Merged

avrabe merged 1 commit into
mainfrom
feat/vcr-ra-cfg-liveness

Conversation

@avrabe

@avrabe avrabe commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

North-star increment — VCR-RA-001 substrate (side-by-side, unwired)

First concrete code under VCR-RA-001 (SSA-based register allocator, epic #242). Lands the analysis substrate the allocator's interference graph reads off, without touching any codegen path.

What

liveness::cfg_liveness() builds the label-form basic-block CFG of a selected function and solves per-block live-in/live-out via the classic backward dataflow fixpoint:

live_in[B]  = use[B] ∪ (live_out[B] − def[B])
live_out[B] = ⋃ { live_in[S] : S ∈ succ(B) }

Two registers interfere iff both are live at a common point — so these per-block sets are exactly what the colouring allocator needs to replace the single-pass exhaustion hard-fail (the thing that forces the reciprocal-mult cost-gates, #209/v0.11.20).

Soundness (matches existing liveness.rs discipline)

  • Leaf-only, label-form branches (B/Bhs/Blo/Bcc → Label). Declines (None) on numeric-offset/computed/return branches, calls, and BrTable — never a partial or guessed CFG.
  • Exit-block live_out = ∅; ABI return-register liveness is a documented external input the allocator unions in later.
  • Backward fixpoint terminates: monotone over the finite register lattice.

Why this can't regress anything

Pure analysis — zero non-test callers, no emitted-byte path touched. Every frozen differential fixture (control_step 0x00210a55, flight_algo 0x07FDF307, divseam) is bit-identical by construction. Also derives Ord/Hash for Reg (additive, needed for BTreeSet).

Tests

  • 4 new cfg_liveness unit tests (straight-line inputs, cross-block backward propagation over a conditional branch, single-successor unconditional branch, decline on call/offset).
  • Extended the real-selector-output liveness test with structural CFG soundness checks (gapless block tiling, in-range successors, live-set vector lengths).

Verification

cargo test -p synth-synthesis (59) + -p synth-backend (all) green · clippy clean · fmt clean · workspace builds.

Wiring into codegen is a separate, fully oracle-gated step.

🤖 Generated with Claude Code

…trate (#242)

Add cfg_liveness(): builds the label-form basic-block CFG of a selected
function and solves per-block live-in/live-out via the classic backward
dataflow fixpoint

    live_in[B]  = use[B] ∪ (live_out[B] − def[B])
    live_out[B] = ⋃ { live_in[S] : S ∈ succ(B) }

This is the interference-graph substrate the spill-capable register
allocator (VCR-RA-001) reads off — the next structural step toward
removing the single-pass exhaustion HARD-FAIL that forces the
reciprocal-mult cost-gates (#209/v0.11.20).

Scope / soundness (matches liveness.rs discipline):
 - leaf-only, label-form branches (B/Bhs/Blo/Bcc → Label); declines
   (returns None) on numeric-offset/computed/return branches, calls, and
   BrTable — never a partial or guessed CFG.
 - exit-block live_out is ∅; ABI return-register liveness is a documented
   external input the allocator unions in later (not under-claimed here).

Pure analysis: zero non-test callers, no codegen path touched, so every
frozen differential fixture (control_step 0x00210a55, flight_algo
0x07FDF307, divseam) is bit-identical BY CONSTRUCTION.

Also: derive Ord/Hash for Reg (additive; needed for BTreeSet<Reg> live
sets — an allocator wants register sets anyway).

Tests: 4 new cfg_liveness unit tests (straight-line inputs, cross-block
backward propagation over a conditional branch, single-successor
unconditional branch, decline on call/offset). Extended the real-selector
-output liveness test with structural CFG soundness checks (gapless block
tiling, in-range successors, live-set vector lengths).

Side-by-side / unwired per the north-star migration plan — wiring is a
separate, fully oracle-gated step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@avrabe
avrabe merged commit da13212 into main Jun 5, 2026
13 checks passed
@avrabe
avrabe deleted the feat/vcr-ra-cfg-liveness branch June 5, 2026 06:55
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.71429% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/liveness.rs 96.44% 7 Missing ⚠️
crates/synth-synthesis/src/instruction_selector.rs 84.61% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

avrabe added a commit that referenced this pull request Jun 5, 2026
#242) (#269)

Build on the just-merged CFG-liveness substrate (#268) with the next
allocator piece: interference_graph() derives per-instruction liveness
from cfg_liveness (walking each block backward from its live_out) and
constructs the undirected "cannot share a physical register" graph a
colouring allocator consumes.

Construction (standard, precise):
 - a register DEFINED at an instruction interferes with everything live
   immediately AFTER it (they coexist), and with its co-defs (so Umull's
   rdlo/rdhi get distinct registers).
 - precision payoff: in `add r3, r1, r2` the inputs die at the add, so r3
   does NOT interfere with r1/r2 and may reuse one of their registers — a
   naive "all operands interfere" graph would forbid that reuse.

InterferenceGraph exposes interferes / neighbors / degree / nodes /
edge_count (degree is the Chaitin/Briggs simplify criterion: degree<k
=> k-colourable).

Soundness: returns None exactly when cfg_liveness declines (out-of-scope
construct), so a non-None graph is complete for the whole function. Pure
analysis, zero non-test callers, no codegen path touched => every frozen
differential fixture (control_step 0x00210a55, flight_algo 0x07FDF307,
divseam) is bit-identical BY CONSTRUCTION.

Tests: 5 interference units (simultaneous-live interfere, sequential
reuse does not, co-defs interfere, live-across-branch, declines-when-
liveness-declines). Extended the real-selector-output test to assert the
graph over actual codegen is symmetric + self-loop-free.

Remaining for VCR-RA-001: graph-colouring (simplify/select) -> spill/
reload -> ABI-exit-liveness union -> the oracle-gated wiring step.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jun 5, 2026
…it (#242) (#272)

The register-allocator analysis+decision layer is merged and unwired
(reg_effect → cfg_liveness #268 → interference_graph #269 → color_graph
#270 → color_graph_precolored #271), all bit-identical-by-construction
because nothing calls them.

The wiring is categorically different: it makes the allocator drive codegen,
so it CHANGES emitted bytes and needs the full differential oracle gate. This
design note sequences that crossing before any of it is written:

 - identifies the hard-fail sites the wiring replaces (instruction_selector.rs
   ~362/388/609/4699/4765 — the exhaustion errors that force the cost-gates),
 - the frozen-behaviour invariant (control_step 0x00210A55 / flight_algo
   0x07FDF307 / divseam bit-identical at every sub-step; flag default-off),
 - 5 oracle-gated sub-steps (verify_allocation oracle → spill-cost ranking →
   virtual-reg selector output flag-gated → spill-code insertion → wire-in +
   per-function flip where the differential proves no-regression),
 - the per-step oracle gate, and the non-goals (coalescing/splitting deferred).

Design leads code: the next bounded, safe piece is step 1 (verify_allocation,
still unwired); the rest is the planned consequential effort.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant