Skip to content

fix(acir_gen): only compute a fallback offset for a predicate-gated read - #13477

Draft
AztecBot wants to merge 3 commits into
masterfrom
cb/acir-fallback-offset-review-followup
Draft

fix(acir_gen): only compute a fallback offset for a predicate-gated read#13477
AztecBot wants to merge 3 commits into
masterfrom
cb/acir-fallback-offset-review-followup

Conversation

@AztecBot

@AztecBot AztecBot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #13466, addressing review findings from https://gist.github.com/AztecBot/35b0524e4ac8e6274f358243bf8383da. Rebased onto master after #13466 merged.

Every existing ACIR snapshot is unchanged, so the generated circuits are byte-identical to what #13466 produces.

1. The ICE surface

#13466 justifies compute_offset's unreachable! with "a result type matching no field cannot come out of SSA generation … reachable only through hand-written SSA". ssa/validation/mod.rs says otherwise, in a comment written for exactly this hazard:

// This deliberately only covers accesses where a reference is involved.
// Passes are allowed to leave an element access whose *numeric* type no
// longer agrees with the array in code they have already proven
// unreachable (see
// `remove_unreachable_instructions::tests::replaces_array_get_following_conditional_constraint_with_default_if_index_was_defaulted`),

…and the result-type check it guards only runs if array_type.contains_reference(). The cited test builds the shape (v5 = array_get v1, index v4 -> u1 on a [u8; 2], from an index replaced with a default after a proven overflow); that pass then deletes the instruction, so I have not demonstrated one reaching ACIR gen. But the invariant as stated is not the one the compiler enforces, and #13466 removed the fallback that made a mismatch harmless.

Two changes:

  • The offset is now computed by the gating decision, not before it. handle_array_operation computed compute_offset for every read and passed it down, where convert_array_operation_inputs discarded it for a safe index. So a mismatched read at a constant, in-bounds index — the shape a defaulted index produces — aborted compilation over an offset that would never have been used. It now cannot: an ungated read never asks for one. Pinned by type_mismatched_safe_array_get_needs_no_fallback_offset.
  • The remaining no-match case returns InternalError instead of panicking. Same outcome for a genuinely impossible state, but a compiler error with a call stack rather than a raw panic and backtrace, and consistent with how handle_array_operation already reports "should be impossible" states. type_mismatched_array_get_is_an_ice becomes an error assertion instead of #[should_panic].

This is a robustness change, not a proof: if anyone demonstrates the shape reaching ACIR gen for real, an error is still the wrong answer there and the fallback would need to mask instead. Also worth deciding whether the SSA-validator rule #13466 deferred is still wanted — I did not add one, since tightening it would reject SSA the validator intends to accept.

2. Tying the bias to the gating decision

The bias's precondition — "the flat index is raw * predicate" — was violated once inside #13466 itself (its last commit, #13471, plus the two predicated_vector_constant_index* programs, exists because the bias was landing on an index the constant-index shortcut had already resolved). It was held up only by where the code sits. get_flattened_index now carries the offset out of the gating match:

let (var_index, fallback_offset) = match gating {
    IndexGating::Safe => (var_index, None),
    IndexGating::Gated { fallback_offset } => (gated, Some(fallback_offset)),
};

An index that took the ungated arm has no offset in scope below and so cannot be biased. Also records why the bias is measured from slot 0 on both flat-index paths (element_type_sizes[0] is the start of element 0's first field, and a constant element size scales 0 to 0).

3. Test names

regression_NNNN under test_programs/ means a noir issue. regression_1601 and regression_1615 took their numbers from noir-claude and collide with real, unrelated noir issues — #1601 is "Remove examples_failing/pow_const", #1615 is "Refactor Logging to use Brillig". Renamed to predicated_composite_get_wide_slot and predicated_safe_get_stale_predicate, with the real issue URLs in the programs' headers. regression_11744 keeps its name and gains a note that the number is a PR, not an issue.

4. Coverage and comments

  • New snapshot predicated_safe_get_on_heterogeneous_element_is_not_masked: a constant-index read on (u8, Field) under a dynamic predicate reaches its consumer unmultiplied. This is the noir-claude#1615 defect at the ACIR level — predicated_safe_get_stale_predicate covers it end to end, but nothing pinned the circuit shape.
  • New snapshot predicated_get_of_repeated_field_type_biases_to_the_first_match: (u8, u8, Field) read at the second u8 falls back to slot 0, pinning that first-match is intentional rather than incidental.
  • Fixed two comments in pre-existing tests still describing compute_offset as returning Some(0); the one on the array_set test was doubly stale, since a store no longer calls it at all.
  • I did not switch vector_ops.rs's IndexGating::Gated { fallback_offset: 0 } to IndexGating::without_fallback. At that call site there is no is_safe_index to pass, and without_fallback(false) reads worse than the explicit variant. Same reasoning for the array_set arm added here.

Not addressed here

One review finding is about #13466's description rather than its code, and is left for @asterite: the body says the scheme costs "a free linear term instead of a mask per leaf", but the size report showed only regressions (nested_array_dynamic +16 opcodes, nested_array_in_vector +6, regression_struct_array_conditional +2, nothing shrinking). The deleted apply_index_side_effects had a free fast path — no mask at all when slot 0's type was no wider than the leaf's — and the bias costs a fresh witness for the index whenever offset != 0. That is a fine trade for a much simpler invariant; it just is not a cost win.

Tests

  • cargo test -p noirc_evaluator --lib — 1947 pass, 0 fail. No existing ACIR snapshot changed, which is the evidence that the refactor is a no-op on generated circuits.
  • cargo test -p nargo_cli --test execute -- array vector slice conditional regression — 5188 pass, 0 fail (includes all renamed programs across the inliner × Brillig matrix).
  • cargo fmt --all --check and cargo clippy -p noirc_evaluator --all-targets clean.

Created by claudebox · group: slackbot · requested by Tom (@TomAFrench) · Slack thread

`compute_offset` was called for every `array_get` before it was known whether
the index would be gated, so a read at a safe index paid for an offset that
`get_flattened_index` then discarded — and, when the result type described no
field of the element, aborted compilation over an offset it would not have
used. The gating decision now picks the offset, so an ungated read never asks
for one.

The remaining no-match case is reported through `InternalError` rather than
`unreachable!`: the SSA type system does not enforce that an `array_get`'s
result type is one of the element's fields (`ssa::validation` only checks it
when a reference is involved, and deliberately tolerates a numeric mismatch a
pass left in code it proved unreachable), so this is reachable enough to
deserve a compiler error with a call stack instead of a panic.

Also carries the fallback offset out of the gating match in
`get_flattened_index`, so an index that was not multiplied by the predicate has
no offset in scope to be biased by, and pins two untested properties of the
scheme: a safe-index read is never masked, and a repeated field type falls back
to its first occurrence.
`regression_NNNN` under `test_programs/` names a noir issue. Both
`regression_1601` and `regression_1615` took their number from noir-claude, and
both collide with real, unrelated noir issues (#1601 "Remove
`examples_failing/pow_const`", #1615 "Refactor Logging to use Brillig").

Name them for what they pin instead, and record the actual issue URLs in the
programs' header comments. `regression_11744` keeps its name but gains a note
that the number is a PR, not an issue.
@AztecBot AztecBot added claude-review Adversarial ClaudeBox review pending claudebox labels Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-review Adversarial ClaudeBox review pending claudebox

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants