feat(#242): VCR-RA-003 phase 2 — across-CALL + across-JOIN allocation validation (whole-function)#817
Merged
Merged
Conversation
… validation
Extend the unconditional per-compilation register-allocation validator past
straight-line to WHOLE-FUNCTION control flow:
- Invariant 3 (across-CALL): a caller-saved reg R2/R3/R12 live across a
bl/blx/call (defined before, read after, no intervening redef) is flagged —
the AAPCS boundary destroys it. Anchored on the ABI contract at the call, not
inferred value-identity (the v0.48 vacuity lesson). R0/R1 excluded (return
value). Does NOT touch the shared reg_effect (whose None-on-call meaning the
phase-1 fail-safe depends on).
- Invariant 4 (across-JOIN): a reg live-in to a CF join (>=2 preds) must be
available (must-defined on every incoming path, or an entry live-in). Forward
MUST/intersection availability fixpoint (UNIVERSE-init, not empty) over a
dedicated join CFG that admits bx-lr returns (sink) and calls (fall-through
with a caller-saved def-set). Entry seed = R0-R3, SP, LR, and the RESERVED
regs R9/R10/R11/R12 (linmem base etc. — resident by construction).
- RaFinalVerdict gains NotAttempted { reason } (loud honest decline when the
join CFG can't be built: numeric branch, BrTable, computed Bx, dup label).
NON-FATAL at the arm_backend call site; only Violation hard-errors.
Vacuity-in-practice verified: invariant 4 fires on real branchy codegen
(if_else_result_343, cf_shapes_500, block_brif_483 joins all Consistent).
Caught + fixed a real false positive (block_brif nested: R11 linmem base
defined nowhere in-stream) by seeding the reserved regs. 130-fixture sweep:
0 false positives. Frozen 10/10 byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…gate
Six new ra003_ tests prove the phase-2 checks are NON-VACUOUS:
- ra003_red_caller_saved_live_across_call_is_caught: R2 defined, bl, R2 read →
CallerSavedLiveAcrossCall (asserts the SPECIFIC violation, not Violation|*).
- ra003_green_return_value_used_after_call: bl defines R0, R0 read → Consistent
(the "use the return value" pattern is not a clobber).
- ra003_green_callee_saved_value_across_call: R4 (callee-saved) across bl → clean.
- ra003_red_across_join_clobber_is_caught: R4 defined on then-path, R5 (not R4)
on else-path, join reads R4 → JoinValueNotAvailable{R4} (specific).
- ra003_green_across_join_both_paths_define / _dominator_defined: correct diamonds
stay Consistent (no false positive on both-paths-define or dominator-def).
- ra003_join_declines_on_unmodeled_control_flow: BrTable → NotAttempted (loud
honest decline, never a silent Consistent).
Full workspace green (synth-synthesis lib 641), clippy -D clean, fmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
- ci.yml: append a third step to vcr-ra-003-alloc-validator-gate running scripts/repro/vcr_ra_003_phase2_join_call.py over branchy/mem/call fixtures (silent-on-real-branchy-codegen; the R11-linmem-base false-positive guard). Existing red-first (ra003_) + frozen steps untouched. - scripts/repro/vcr_ra_003_phase2_join_call.py: compiles 7 CF-join / memory / call fixtures through the unconditional validator, asserts 0 false positives. - artifacts/verified-codegen-roadmap.yaml: VCR-RA-003 phase-2 note (invariants 3+4, the NotAttempted honest decline, the reserved-reg seed fix, vacuity-in- practice evidence). Zero new rivet errors (52/102 identical to origin/main). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e eprintln
Advisor caught: all phase-2 verification was --relocatable only; the DEFAULT
optimized path (what the control_step differential actually compiles) had never
run through invariants 3/4.
- Verified the default path: control_step + 7 branchy fixtures × {relocatable,
default} = 16 pairs, 0 false-positive Violations; full 130-fixture default-path
sweep also 0 violations. No blocking issue.
- HONEST SCOPE: on the default path branches are pre-resolved to NUMERIC offsets,
so invariant 4 declines to NotAttempted there (invariants 1-3 still run). It
FIRES on the label-form/relocatable path. Numeric-branch CFG = named follow-up.
- Gate the NotAttempted eprintln behind SYNTH_RA003_VERBOSE: the decline is the
COMMON default-path case (~41/130 fixtures) — unconditional it would spam every
branchy production compile (noise phase 1 never produced). Verdict stays
observable on demand; the repro greps FAILED so it's unaffected.
- repro now sweeps BOTH paths + control_step.wasm; roadmap states the path scope.
Frozen 10/10 byte-identical, clippy 0, control_step default-path clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
# Conflicts: # .github/workflows/ci.yml
…verse-init Advisor caught: universe-init (the load-bearing MUST/intersection fixpoint correction) had zero CONFIRMED exercise as a Consistent-via-fixpoint result on a real back-edge — all prior evidence was forward diamonds. loop_param_bound_663 traces Consistent on the relocatable path, but the sweep conflated Consistent/NotAttempted. ra003_green_loop_back_edge_preserves_availability: a value (R4) defined before a loop and read in the body; the loop header is a join of preheader + back-edge. Verified NON-VACUOUS: flipping avail_out init from universe to empty makes this test FAIL (a false positive — the back-edge starts empty and the intersection drops the dominator-defined R4), so it durably guards the universe-init choice. 18 ra003_ tests pass; frozen 10/10 (test-only, byte-identical); clippy 0; fmt clean. Confirmed ci.yml line-75 PyYAML/actionlint parse note is PRE-EXISTING on origin/main (a `space::` scalar), not this lane's merge resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
VCR-RA-003 phase 2 (epic #242) — make the register-allocation validator WHOLE-FUNCTION
v0.48 (#808) shipped
validate_final_allocation— an UNCONDITIONAL per-compilation register-allocation validator (runs on every ARM compile in the default--features riscvbuild, hard-errors on violation) — but BOUNDED to straight-line segments + spill/reload + callee-saved discipline. This lane extends it past straight-line to control-flow joins and calls.Two new invariants (each anchored on a reference OUTSIDE inferred value-identity — the v0.48 vacuity lesson)
3. Caller-saved preservation across a CALL. A value in
R2/R3/R12defined before abl/blx/calland read after it (no intervening redef) crossed a boundary the AAPCS is contractually allowed to clobber →CallerSavedLiveAcrossCall. The reference is the ABI contract at the call, not an inferred redefinition.R0/R1are excluded (a call DEFINES them: the return value) — a documented false-negative boundary (FN < FP). Reasoned locally; does not modify the sharedreg_effect(whoseNone-on-call meaning the phase-1 callee-saved fail-safe +eliminate_unread_frame_storesboth depend on).4. Value availability across a JOIN. A register live-in to a CF join (≥2 preds) must be AVAILABLE — must-defined on every incoming path, or an entry live-in → else
JoinValueNotAvailable. Forward MUST (intersection) availability fixpoint (universe-init, not empty — it is a must-analysis) over a dedicated join CFG that admitsbx lrreturns (sinks) and calls (fall-through with a caller-saved def-set), so branchy functions with calls/returns stay analyzable. Entry seed = paramsR0-R3,SP,LR, and the RESERVED registersR9/R10/R11/R12(contracts::regalloc::RESERVED_REGS— R11 = linmem base, resident by construction).RaFinalVerdictgainsNotAttempted { reason }— a loud honest decline when the join CFG can't be built (numeric branch,BrTable, computedBx, dup label). NON-FATAL at the arm_backend call site; onlyViolationhard-errors. Decline>guess applied to the checker itself.Red-first evidence (non-vacuity proven BEFORE trusting)
cargo test -p synth-synthesis --lib ra003_— 17 tests. New phase-2:ra003_red_caller_saved_live_across_call_is_caught— R2 defined,bl, R2 read → asserts the SPECIFICCallerSavedLiveAcrossCall(neverViolation | *).ra003_red_across_join_clobber_is_caught— R4 on then-path, R5 (not R4) on else-path, join reads R4 → asserts the SPECIFICJoinValueNotAvailable{R4}.ra003_join_declines_on_unmodeled_control_flow—BrTable→NotAttempted(never a silent Consistent).Silent on real codegen + vacuity-in-practice
if_else_result_343/cf_shapes_500/block_brif_483joins all Consistent, incl. a real R11 availability check) — not unit-tested-but-dead-in-shipping.block_brif_483'snested— astr [r11,#off]at a join, R11 (linmem base) defined nowhere in the stream → fixed by seeding the reserved registers.--relocatable+ control_step & 7 branchy × {relocatable, default} = 16 pairs → 0 false positives.Path scope (honest)
Invariant 4 fires on the label-form /
--relocatablepath (analyzable CFG). On the default optimized path branches are pre-resolved to NUMERIC offsets before the validator runs, so invariant 4 declines toNotAttemptedthere (invariants 1-3 still run on both paths — control_step, the differential's own input, compiles CLEAN on the default path). TheNotAttempteddecline is common on the default path (~41/130 fixtures), so its diagnostic eprintln is gated behindSYNTH_RA003_VERBOSE(production stays quiet, verdict observable on demand). Numeric-branch CFG = named follow-up.Frozen / gates
0x00210A55, flight_algo0x07FDF307) — the validator emits nothing.cargo test --workspacegreen; clippy-D warningsclean;cargo fmt --checkclean.claim_check25/25. rivet: 52 errors / 102 warnings identical to origin/main (pre-existing coverage backlog; this lane adds zero new errors).vcr-ra-003-alloc-validator-gaterunningscripts/repro/vcr_ra_003_phase2_join_call.pyover branchy/mem/call fixtures on BOTH paths (existing red-first + frozen steps untouched).Still loud-flags-unattempted (named follow-ups)
NotAttempted.Merged
origin/main(VCR-SEL-005 #816 + #761 R9/globals base separation); re-verified all gates post-merge.🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L