Skip to content

fix(ssa): Mislabeled instructions with side effects in EnableSideEffectsIf removal pass#8355

Merged
jfecher merged 21 commits intomasterfrom
mv/regression_8236
May 9, 2025
Merged

fix(ssa): Mislabeled instructions with side effects in EnableSideEffectsIf removal pass#8355
jfecher merged 21 commits intomasterfrom
mv/regression_8236

Conversation

@vezenovm
Copy link
Contributor

@vezenovm vezenovm commented May 5, 2025

Description

Problem*

Resolves #8236

For the given snippet in the issue we have the following SSA before constant folding:

Details

acir(inline) predicate_pure fn main f0 {
  b0(v8: [u16; 3], v9: [u1; 1]):
    v10 = make_array [u16 59890, u16 17417, u16 14409] : [u16; 3]
    v13 = call f1(v10, u1 1) -> [u1; 1]
    v15 = array_get v8, index u32 0 -> u16
    v16 = cast v15 as u32
    v18 = mod v16, u32 14409
    v19 = array_get v13, index u32 0 -> u1
    enable_side_effects v19
    v20 = array_get v9, index u32 0 -> u1
    v21 = call f1(v8, v20) -> [u1; 1]
    v22 = array_get v9, index u32 0 -> u1
    v23 = not v19
    enable_side_effects v23
    v24 = array_get v8, index u32 0 -> u16
    v25 = cast v24 as u32
    v27 = mod v25, u32 3
    v28 = lt v27, u32 3
    v29 = unchecked_mul v28, v23
    constrain v29 == v23, "Index out of bounds"
    v30 = array_get v8, index v27 -> u16
    v31 = cast v30 as u32
    v33 = lt u32 24993, v31
    enable_side_effects u1 1
    v34 = unchecked_mul v19, v22
    v35 = unchecked_mul v23, v33
    v36 = unchecked_add v34, v35
    return v36
}

After Constant Folding:

Details

acir(inline) predicate_pure fn main f0 {
  b0(v8: [u16; 3], v9: [u1; 1]):
    v10 = make_array [u16 59890, u16 17417, u16 14409] : [u16; 3]
    v13 = call f1(v10, u1 1) -> [u1; 1]
    v15 = array_get v8, index u32 0 -> u16
    v16 = cast v15 as u32
    v18 = mod v16, u32 14409
    v19 = array_get v13, index u32 0 -> u1
    enable_side_effects v19
    v20 = array_get v9, index u32 0 -> u1
    v21 = call f1(v8, v20) -> [u1; 1]
    v22 = not v19
    enable_side_effects v22
    v24 = mod v16, u32 3
    v25 = lt v24, u32 3
    v26 = unchecked_mul v25, v22
    constrain v26 == v22, "Index out of bounds"
    v27 = array_get v8, index v24 -> u16
    v28 = cast v27 as u32
    v30 = lt u32 24993, v28
    enable_side_effects u1 1
    v31 = unchecked_mul v19, v20
    v32 = unchecked_mul v22, v30
    v33 = unchecked_add v31, v32
    return v33
}

After EnableSideEffectsIf removal:

Details

acir(inline) predicate_pure fn main f0 {
  b0(v8: [u16; 3], v9: [u1; 1]):
    v10 = make_array [u16 59890, u16 17417, u16 14409] : [u16; 3]
    v13 = call f1(v10, u1 1) -> [u1; 1]
    v15 = array_get v8, index u32 0 -> u16
    v16 = cast v15 as u32
    v18 = mod v16, u32 14409
    v19 = array_get v13, index u32 0 -> u1
    enable_side_effects v19
    v20 = array_get v9, index u32 0 -> u1
    v21 = call f1(v8, v20) -> [u1; 1]
    v22 = not v19
    v24 = mod v16, u32 3
    v25 = lt v24, u32 3
    enable_side_effects v22
    v26 = unchecked_mul v25, v22
    constrain v26 == v22, "Index out of bounds"
    v27 = array_get v8, index v24 -> u16
    v28 = cast v27 as u32
    v30 = lt u32 24993, v28
    enable_side_effects u1 1
    v31 = unchecked_mul v19, v20
    v32 = unchecked_mul v22, v30
    v33 = unchecked_add v31, v32
    return v33
}

In the initial SSA, can see how v24 = array_get v8, index u32 0 -> u16 was deduplicated to a matching instruction that occurred under a different predicate. The result v24 is then used to compute the dynamic index of another array get. Those instructions calculating the index are being moved incorrectly causing an incorrect index to be computed.

Looking at the new SSA (After EnableSideEffectsIf removal) with the deduplicated array_get, we now use the actual result v15 from v15 = array_get v8, index u32 0 -> u16 when computing v16 = cast v15 as u32 -> v24 = mod v16, u32 3 -> v27 = array_get v8, index v24 -> u16.

After the EnableSideEffectsIf removal pass the modulo v24 = mod v16, u32 3 occurs under a different predicate from where it was previously. If we have a zero predicate our euclidean division returns zero for both the quotient and the remainder.

We have this final SSA with the enable side effects vars simplified:

acir(inline) predicate_pure fn main f0 {
  b0(v8: [u16; 3], v9: [u1; 1]):
    v11 = array_get v8, index u32 0 -> u16
    v12 = cast v11 as u32
    enable_side_effects u1 0
    v14 = array_get v9, index u32 0 -> u1
    v16 = call f1(v8, v14) -> [u1; 1]
    v18 = mod v12, u32 3
    v19 = lt v18, u32 3
    enable_side_effects u1 1
    constrain v19 == u1 1, "Index out of bounds"
    v21 = array_get v8, index v18 -> u16
    v22 = cast v21 as u32
    v24 = lt u32 24993, v22
    enable_side_effects u1 1
    return v24
}

We can see how the modulo is now under a false predicate when it would have been under a true predicate previously. Thus, we are getting a result of zero from the modulo operation instead of one as we would like, triggering an incorrect result.

Summary*

I simply switched to using Instruction::requires_acir_gen_predicate instead of responds_to_side_effects_var where Binary::requires_acir_gen_predicate is called internaly and BinaryOp::Div/BinaryOp::Mod are correctly labeled as always requiring ACIR gen predicates.

I could have went with just updating the responds_to_side_effects_var function inside of remove_enable_side_effects.rs, however, I felt it would be better to lean into Instruction::requires_acir_gen_predicate as they are attempting to do the same thing.
Comments above the call to responds_to_side_effects_var:

// If we hit an instruction which is affected by the side effects var then we must insert the
// `Instruction::EnableSideEffectsIf` before we insert this new instruction.

Comments above Instruction::requires_acir_gen_predicate:

/// If true the instruction will depend on `enable_side_effects` context during acir-gen.

We also had other mismatches between the methods on what instructions returned true for being affected by the side effects var. I felt it is better to be unified here and have alignment on which instructions depend on an ACIR predicate.

Disabling of the side effects var in ACIR gen is also based upon requires_acir_gen_predicate.

Additional Context

The snapshosts make the PR seem much bigger than it is in reality. The majority of the changes are contained within compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs.

Documentation*

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [For Experimental Features] Documentation to be submitted in a separate PR.

PR Checklist*

  • I have tested the changes locally.
  • I have formatted the changes with Prettier and/or cargo fmt on default settings.

@github-actions
Copy link
Contributor

github-actions bot commented May 5, 2025

Changes to circuit sizes

Generated at commit: 23a53fb8925fe5acd3cc8088291ad55d95e448f8, compared to commit: 94e839d82c13c90e7f5989e9ab6cea73ab2ba403

🧾 Summary (10% most significant diffs)

Program ACIR opcodes (+/-) % Circuit size (+/-) %
bench_eddsa_poseidon -412 ✅ -2.28% -412 ✅ -1.88%
to_bytes_integration -206 ✅ -17.28% -206 ✅ -15.20%

Full diff report 👇
Program ACIR opcodes (+/-) % Circuit size (+/-) %
regression 294 (+4) +1.38% 3,836 (-2) -0.05%
conditional_1 2,688 (-8) -0.30% 8,046 (-8) -0.10%
slices 668 (-4) -0.60% 3,767 (-4) -0.11%
hash_to_field 450 (-4) -0.88% 3,485 (-4) -0.11%
regression_7128 509 (-4) -0.78% 3,355 (-4) -0.12%
ram_blowup_regression 114,690 (-1,024) -0.88% 377,161 (-1,024) -0.27%
bench_eddsa_poseidon 17,683 (-412) -2.28% 21,532 (-412) -1.88%
to_bytes_integration 986 (-206) -17.28% 1,149 (-206) -15.20%

@TomAFrench
Copy link
Member

I'd like to add some acirgen tests for this as it's not immediately clear why this is an issue (i.e. why does the predicate attached to the other array_get not handle this?)

@vezenovm
Copy link
Contributor Author

vezenovm commented May 6, 2025

(i.e. why does the predicate attached to the other array_get not handle this?)

EDIT: Oh I see what you mean. The index for v28 = array_get v8, index v25 -> u16 should be handling that predicate appropriately.

It looks like keeping the array get after the enable side effects var simply had the knockdown effect of keeping the modulo under the side effects variable as well. Otherwise, we risked the modulo being placed under a different predicate which is where the issue truly lies. Reference the main PR description for the full walkthrough of this bug.

@vezenovm vezenovm changed the title fix(ssa): Array get deduplicated when result is used under predicate fix(ssa): Mislabeled instructions with side effects in EnableSideEffectsIf removal pass May 6, 2025
@vezenovm vezenovm marked this pull request as ready for review May 6, 2025 18:46
@vezenovm vezenovm requested a review from a team May 6, 2025 18:46
Copy link
Member

@TomAFrench TomAFrench left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found #8387 while reviewing this.

I'm supportive of consolidating onto requires_acir_gen_predicate if we find it fits our needs here although I'm quite leery of the number of instructions this change is affecting.

Ideally we should have tests which show that the new behaviour is desirable for each of these instructions.

@vezenovm vezenovm added Blocked PR is blocked on an issue and removed Blocked PR is blocked on an issue labels May 7, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Execution Time'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 3c50dab Previous: 9dc9cf6 Ratio
rollup-merge 0.004 s 0.003 s 1.33

This comment was automatically generated by workflow using github-action-benchmark.

CC: @TomAFrench

@vezenovm vezenovm requested review from a team and TomAFrench May 8, 2025 19:35
@vezenovm vezenovm reopened this May 9, 2025
Copy link
Contributor

@jfecher jfecher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Fewer functions that do the same thing is good

@jfecher jfecher added this pull request to the merge queue May 9, 2025
Merged via the queue into master with commit 74279a4 May 9, 2025
116 checks passed
@jfecher jfecher deleted the mv/regression_8236 branch May 9, 2025 16:36
github-merge-queue bot pushed a commit to AztecProtocol/aztec-packages that referenced this pull request May 12, 2025
Automated pull of nightly from the
[noir](https://github.com/noir-lang/noir) programming language, a
dependency of Aztec.
BEGIN_COMMIT_OVERRIDE
fix: always type-check turbofish, and error when it's not allowed
(noir-lang/noir#8437)
chore: Release Noir(1.0.0-beta.5)
(noir-lang/noir#7955)
feat(greybox_fuzzer): Parallel fuzz tests
(noir-lang/noir#8432)
fix(ssa): Mislabeled instructions with side effects in
EnableSideEffectsIf removal pass
(noir-lang/noir#8355)
feat: SSA pass impact report
(noir-lang/noir#8393)
chore: bump external pinned commits
(noir-lang/noir#8433)
chore: separate benchmarking from github actions more
(noir-lang/noir#7943)
chore(fuzz): Break up the AST fuzzer `compare` module
(noir-lang/noir#8431)
chore(fuzz): Rename `init_vs_final` to `min_vs_full`
(noir-lang/noir#8430)
fix!: error on tuple mismatch
(noir-lang/noir#8424)
chore: bump external pinned commits
(noir-lang/noir#8429)
chore(acir): Test whether the predicate has an effect on slice
intrinsics (noir-lang/noir#8421)
feat(ssa): Mark transitively dead parameters during DIE
(noir-lang/noir#8254)
fix(ssa_gen): Do not code gen fetching of empty arrays when initializing
the data bus (noir-lang/noir#8426)
chore: remove `.aztec-sync-commit`
(noir-lang/noir#8415)
chore(test): Add more unit tests for
`inline_functions_with_at_most_one_instruction`
(noir-lang/noir#8418)
chore: add minor docs for interpreter
(noir-lang/noir#8397)
fix: print slice composite types surrounded by parentheses
(noir-lang/noir#8412)
feat: Skip SSA passes that contain any of the given messages
(noir-lang/noir#8416)
fix: disable range constraints using the predicate
(noir-lang/noir#8396)
chore: bumping external libraries
(noir-lang/noir#8406)
chore: redo typo PR by shystrui1199
(noir-lang/noir#8405)
feat(test): add `nargo_fuzz_target`
(noir-lang/noir#8308)
fix: allow names to collide in the values/types namespaces
(noir-lang/noir#8286)
fix: Fix sequencing of side-effects in lvalue
(noir-lang/noir#8384)
feat(greybox_fuzzer): Maximum executions parameter added
(noir-lang/noir#8390)
fix: warn on and discard unreachable statements after break and continue
(noir-lang/noir#8382)
fix: add handling for u128 infix ops in interpreter
(noir-lang/noir#8392)
chore: move acirgen tests into separate file
(noir-lang/noir#8376)
feat(fuzz): initial version of comptime vs brillig target for AST fuzzer
(noir-lang/noir#8335)
chore: apply lints to `ast_fuzzer`
(noir-lang/noir#8386)
chore: add note on AI generated PRs in `CONTRIBUTING.md`
(noir-lang/noir#8385)
chore: document flattening pass
(noir-lang/noir#8312)
fix: comptime shift-right overflow is zero
(noir-lang/noir#8380)
feat: let static_assert accept any type for its message
(noir-lang/noir#8322)
fix(expand): output safety comment before statements
(noir-lang/noir#8378)
chore: avoid need to rebuild after running tests
(noir-lang/noir#8379)
chore: bump dependencies (noir-lang/noir#8372)
chore: Add GITHUB_TOKEN to cross build
(noir-lang/noir#8370)
chore: redo typo PR by GarmashAlex
(noir-lang/noir#8364)
chore: remove unsafe code from greybox fuzzer
(noir-lang/noir#8315)
feat: add `--fuzz-timeout` to `nargo test` options
(noir-lang/noir#8326)
chore: bump external pinned commits
(noir-lang/noir#8334)
fix(expand): try to use "Self" in function calls
(noir-lang/noir#8353)
fix: Fix evaluation order of assignments with side-effects in their rhs
(noir-lang/noir#8342)
fix: let comptime Field value carry the field's sign
(noir-lang/noir#8343)
fix: Ordering of items in callstacks
(noir-lang/noir#8338)
chore: add snapshosts for nargo expand tests
(noir-lang/noir#8318)
fix(ownership): Clone global arrays
(noir-lang/noir#8328)
chore: Replace all SSA interpreter panics with error variants
(noir-lang/noir#8311)
feat: Metamorphic AST fuzzing
(noir-lang/noir#8299)
fix: fix some Display implementations for AST nodes
(noir-lang/noir#8316)
chore: remove leftover file
(noir-lang/noir#8313)
fix: uses non-zero points with ec-add-unsafe
(noir-lang/noir#8248)
END_COMMIT_OVERRIDE

---------

Co-authored-by: AztecBot <tech@aztecprotocol.com>
Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com>
github-merge-queue bot pushed a commit to AztecProtocol/aztec-packages that referenced this pull request May 12, 2025
Automated pull of nightly from the
[noir](https://github.com/noir-lang/noir) programming language, a
dependency of Aztec.
BEGIN_COMMIT_OVERRIDE
fix: always type-check turbofish, and error when it's not allowed
(noir-lang/noir#8437)
chore: Release Noir(1.0.0-beta.5)
(noir-lang/noir#7955)
feat(greybox_fuzzer): Parallel fuzz tests
(noir-lang/noir#8432)
fix(ssa): Mislabeled instructions with side effects in
EnableSideEffectsIf removal pass
(noir-lang/noir#8355)
feat: SSA pass impact report
(noir-lang/noir#8393)
chore: bump external pinned commits
(noir-lang/noir#8433)
chore: separate benchmarking from github actions more
(noir-lang/noir#7943)
chore(fuzz): Break up the AST fuzzer `compare` module
(noir-lang/noir#8431)
chore(fuzz): Rename `init_vs_final` to `min_vs_full`
(noir-lang/noir#8430)
fix!: error on tuple mismatch
(noir-lang/noir#8424)
chore: bump external pinned commits
(noir-lang/noir#8429)
chore(acir): Test whether the predicate has an effect on slice
intrinsics (noir-lang/noir#8421)
feat(ssa): Mark transitively dead parameters during DIE
(noir-lang/noir#8254)
fix(ssa_gen): Do not code gen fetching of empty arrays when initializing
the data bus (noir-lang/noir#8426)
chore: remove `.aztec-sync-commit`
(noir-lang/noir#8415)
chore(test): Add more unit tests for
`inline_functions_with_at_most_one_instruction`
(noir-lang/noir#8418)
chore: add minor docs for interpreter
(noir-lang/noir#8397)
fix: print slice composite types surrounded by parentheses
(noir-lang/noir#8412)
feat: Skip SSA passes that contain any of the given messages
(noir-lang/noir#8416)
fix: disable range constraints using the predicate
(noir-lang/noir#8396)
chore: bumping external libraries
(noir-lang/noir#8406)
chore: redo typo PR by shystrui1199
(noir-lang/noir#8405)
feat(test): add `nargo_fuzz_target`
(noir-lang/noir#8308)
fix: allow names to collide in the values/types namespaces
(noir-lang/noir#8286)
fix: Fix sequencing of side-effects in lvalue
(noir-lang/noir#8384)
feat(greybox_fuzzer): Maximum executions parameter added
(noir-lang/noir#8390)
fix: warn on and discard unreachable statements after break and continue
(noir-lang/noir#8382)
fix: add handling for u128 infix ops in interpreter
(noir-lang/noir#8392)
chore: move acirgen tests into separate file
(noir-lang/noir#8376)
feat(fuzz): initial version of comptime vs brillig target for AST fuzzer
(noir-lang/noir#8335)
chore: apply lints to `ast_fuzzer`
(noir-lang/noir#8386)
chore: add note on AI generated PRs in `CONTRIBUTING.md`
(noir-lang/noir#8385)
chore: document flattening pass
(noir-lang/noir#8312)
fix: comptime shift-right overflow is zero
(noir-lang/noir#8380)
feat: let static_assert accept any type for its message
(noir-lang/noir#8322)
fix(expand): output safety comment before statements
(noir-lang/noir#8378)
chore: avoid need to rebuild after running tests
(noir-lang/noir#8379)
chore: bump dependencies (noir-lang/noir#8372)
chore: Add GITHUB_TOKEN to cross build
(noir-lang/noir#8370)
chore: redo typo PR by GarmashAlex
(noir-lang/noir#8364)
chore: remove unsafe code from greybox fuzzer
(noir-lang/noir#8315)
feat: add `--fuzz-timeout` to `nargo test` options
(noir-lang/noir#8326)
chore: bump external pinned commits
(noir-lang/noir#8334)
fix(expand): try to use "Self" in function calls
(noir-lang/noir#8353)
fix: Fix evaluation order of assignments with side-effects in their rhs
(noir-lang/noir#8342)
fix: let comptime Field value carry the field's sign
(noir-lang/noir#8343)
fix: Ordering of items in callstacks
(noir-lang/noir#8338)
chore: add snapshosts for nargo expand tests
(noir-lang/noir#8318)
fix(ownership): Clone global arrays
(noir-lang/noir#8328)
chore: Replace all SSA interpreter panics with error variants
(noir-lang/noir#8311)
feat: Metamorphic AST fuzzing
(noir-lang/noir#8299)
fix: fix some Display implementations for AST nodes
(noir-lang/noir#8316)
chore: remove leftover file
(noir-lang/noir#8313)
fix: uses non-zero points with ec-add-unsafe
(noir-lang/noir#8248)
END_COMMIT_OVERRIDE

---------

Co-authored-by: AztecBot <tech@aztecprotocol.com>
Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.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.

ACIR != Brillig: passing unused parameters

3 participants