Skip to content

fix(ssa): accept array_set on transitively-fresh swapped loop sibling - #13308

Draft
AztecBot wants to merge 1 commit into
masterfrom
cb/rc-invariant-nested-fresh-swap
Draft

fix(ssa): accept array_set on transitively-fresh swapped loop sibling#13308
AztecBot wants to merge 1 commit into
masterfrom
cb/rc-invariant-nested-fresh-swap

Conversation

@AztecBot

@AztecBot AztecBot commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

The debug-only Brillig rc_invariant::array_set SSA validator produced a false positive on AST-fuzzer seed 0xfe6bbee600100000 (comptime_vs_brillig_direct), aborting compilation after Mem2Reg:

array_set in function func_1 on array v105 has an aliased read of v106 on a forward
path with no preceding `inc_rc`; the in-place mutation would be observable through that alias

Disabling the check and re-running the full comparison shows the Brillig VM and the comptime interpreter agree — the rejected array_set is sound and the validator was over-approximating.

Root cause

The mutated array v105 (outer loop header b21) is rebound on its back-edge (b25: jmp b21(v74, v106)) to v106, the header parameter of a nested loop (b24). v106 mixes two sources: a fresh make_array (v71) on its own back-edge and a forward-edge alias of the mutated v105 on its entry edge (b22: jmp b24(v105, u32 0)). The mutating branch b23 is only reachable after the inner loop has re-defined the loop-carried value from its fresh back-edge source, so the storage v105 mutates is a distinct per-iteration allocation and every array_get v106 read happened on a prior iteration over different fresh storage.

The existing "swapped-in loop sibling" freshness relaxation (the #12929 fix) only recognizes a sibling parameter of the same header that is re-allocated fresh directly each iteration. It does not follow the swap transitively through a second, nested loop header, so it failed to classify v106 as iteration-local fresh.

Fix

Extend the swap exclusion in rc_invariant/mod.rs so a swapped-in sibling also qualifies as a distinct per-iteration storage when it is the loop-carried parameter of a nested loop header, gated by three sound, structural conditions (is_transitively_fresh_sibling):

  1. Every back-edge into the sibling's header re-freshens it with an iteration-local make_array/Call result.
  2. The nested loop is exited only through its header's own branch, so the value threaded out is always a header-entry value of the (non-body) exiting iteration.
  3. The nested header does not read the sibling.

Together these guarantee the swapped-in exit value was never read on its own iteration while every in-loop read observed a distinct earlier allocation, so the source's later in-place mutation has no live aliased read.

Because such a sibling also carries a forward-edge alias of the source into its own loop, find_reachable_aliased_use additionally marks it protected so the forward walk never re-adds it via the add-rule.

Testing

  • accepts_array_set_on_transitively_fresh_swapped_sibling — the reduced seed SSA; red before the fix, green after.
  • end_to_end_nested_swap_without_inner_freshening_is_rejected — soundness canary: the same shape but with the inner back-edge threading the sibling unchanged (non-fresh) must still be rejected, pinning the load-bearing freshening requirement.
  • Full rc_invariant (72) and validation (188) unit suites pass; cargo fmt + clippy clean.
  • End-to-end: NOIR_AST_FUZZER_SEED=0xfe6bbee600100000 CI=1 cargo test -p noir_ast_fuzzer_fuzz comptime_vs_brillig_direct now passes (previously aborted in the validator).

Closes #13280

Closes #13280


Created by claudebox · group: slackbot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AST Fuzzer: rc_invariant::array_set false positive on transitively-fresh swapped loop sibling

1 participant