Skip to content

Commit 89cdf76

Browse files
vezenovmaakoshh
andauthored
fix(licm): Check whether the loop is executed when hoisting with a predicate (#8546)
Co-authored-by: Akosh Farkash <aakoshh@gmail.com>
1 parent ef6d866 commit 89cdf76

File tree

28 files changed

+857
-44
lines changed

28 files changed

+857
-44
lines changed

compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,23 @@ impl<'f> LoopInvariantContext<'f> {
353353

354354
let can_be_hoisted = can_be_hoisted(&instruction, self.inserter.function, false)
355355
|| matches!(instruction, MakeArray { .. })
356-
|| (can_be_hoisted(&instruction, self.inserter.function, true)
357-
&& !self.current_block_control_dependent)
356+
|| self.can_be_hoisted_with_control_dependence(&instruction, self.inserter.function)
358357
|| self.can_be_hoisted_from_loop_bounds(&instruction);
359358

360359
is_loop_invariant && can_be_hoisted
361360
}
362361

362+
/// Check [can_be_hoisted] with extra control dependence information that
363+
/// lives within the context of this pass.
364+
fn can_be_hoisted_with_control_dependence(
365+
&self,
366+
instruction: &Instruction,
367+
function: &Function,
368+
) -> bool {
369+
can_be_hoisted(instruction, function, true)
370+
&& self.can_hoist_control_dependent_instruction()
371+
}
372+
363373
/// Keep track of a loop induction variable and respective upper bound.
364374
/// In the case of a nested loop, this will be used by later loops to determine
365375
/// whether they have operations reliant upon the maximum induction variable.
@@ -403,24 +413,29 @@ impl<'f> LoopInvariantContext<'f> {
403413
}
404414
Binary(binary) => self.can_evaluate_binary_op(binary),
405415
Constrain(..) | ConstrainNotEqual(..) | RangeCheck { .. } => {
406-
// If we know the loop will be executed we can still only hoist if we are in a non control dependent block.
407-
!self.current_block_control_dependent && self.does_loop_body_execute()
416+
self.can_hoist_control_dependent_instruction()
408417
}
409418
Call { func, .. } => {
410419
let purity = match self.inserter.function.dfg[*func] {
411420
Value::Intrinsic(intrinsic) => Some(intrinsic.purity()),
412421
Value::Function(id) => self.inserter.function.dfg.purity_of(id),
413422
_ => None,
414423
};
415-
// If we know the loop will be executed we can still only hoist if we are in a non control dependent block.
416424
matches!(purity, Some(Purity::PureWithPredicate))
417-
&& !self.current_block_control_dependent
418-
&& self.does_loop_body_execute()
425+
&& self.can_hoist_control_dependent_instruction()
419426
}
420427
_ => false,
421428
}
422429
}
423430

431+
/// A control dependent instruction (e.g. constrain or division) has more strict conditions for hoisting.
432+
/// This function check the following conditions:
433+
/// - The current block is non control dependent
434+
/// - The loop body is guaranteed to be executed
435+
fn can_hoist_control_dependent_instruction(&self) -> bool {
436+
!self.current_block_control_dependent && self.does_loop_body_execute()
437+
}
438+
424439
/// Determine whether the loop body is guaranteed to execute.
425440
/// We know a loop body will execute if we have constant loop bounds where the upper bound
426441
/// is greater than the lower bound.
@@ -819,10 +834,18 @@ impl<'f> LoopInvariantContext<'f> {
819834
/// and its predicate, rather than just the instruction. Setting this means instructions that
820835
/// rely on predicates can be hoisted as well.
821836
///
837+
/// # Preconditions
822838
/// Certain instructions can be hoisted because they implicitly depend on a predicate.
823839
/// However, to avoid tight coupling between passes, we make the hoisting
824840
/// conditional on whether the caller wants the predicate to be taken into account or not.
825841
///
842+
/// Even if we know the predicate is the same for an instruction's block and a loop's header block,
843+
/// the caller of this methods needs to be careful as a loop may still never be executed.
844+
/// This is because an loop with dynamic bounds may never execute its loop body.
845+
/// If the instruction were to trigger a failure, our program may fail inadvertently.
846+
/// If we know a loop's upper bound is greater than its lower bound we can hoist these instructions,
847+
/// but it is left to the caller of this method to account for this case.
848+
///
826849
/// This differs from `can_be_deduplicated` as that method assumes there is a matching instruction
827850
/// with the same inputs. Hoisting is for lone instructions, meaning a mislabeled hoist could cause
828851
/// unexpected failures if the instruction was never meant to be executed.
@@ -850,19 +873,13 @@ fn can_be_hoisted(
850873
};
851874
match purity {
852875
Some(Purity::Pure) => true,
853-
Some(Purity::PureWithPredicate) => false,
876+
Some(Purity::PureWithPredicate) => hoist_with_predicate,
854877
Some(Purity::Impure) => false,
855878
None => false,
856879
}
857880
}
858881

859-
// We cannot hoist these instructions, even if we know the predicate is the same.
860-
// This is because an loop with dynamic bounds may never execute its loop body.
861-
// If the instruction were to trigger a failure, our program may fail inadvertently.
862-
// If we know a loop's upper bound is greater than its lower bound we can hoist these instructions,
863-
// but we do not want to assume that the caller of this method has accounted
864-
// for this case. Thus, we block hoisting on these instructions.
865-
Constrain(..) | ConstrainNotEqual(..) | RangeCheck { .. } => false,
882+
Constrain(..) | ConstrainNotEqual(..) | RangeCheck { .. } => hoist_with_predicate,
866883

867884
// Noop instructions can always be hoisted, although they're more likely to be
868885
// removed entirely.
@@ -1723,8 +1740,8 @@ mod control_dependence {
17231740
v3 = lt v2, u32 0
17241741
jmpif v3 then: loop_body, else: exit
17251742
loop_body():
1726-
v6 = mul v0, v1
1727-
v7 = mul v6, v0
1743+
v6 = unchecked_mul v0, v1
1744+
v7 = unchecked_mul v6, v0
17281745
constrain v7 == u32 12
17291746
v10 = unchecked_add v2, u32 1
17301747
jmp loop(v10)
@@ -1744,8 +1761,8 @@ mod control_dependence {
17441761
let expected = "
17451762
brillig(inline) fn main f0 {
17461763
entry(v0: u32, v1: u32):
1747-
v3 = mul v0, v1
1748-
v4 = mul v3, v0
1764+
v3 = unchecked_mul v0, v1
1765+
v4 = unchecked_mul v3, v0
17491766
jmp loop(u32 0)
17501767
loop(v2: u32):
17511768
jmpif u1 0 then: loop_body, else: exit
@@ -1773,8 +1790,8 @@ mod control_dependence {
17731790
v3 = lt v2, u32 1
17741791
jmpif v3 then: loop_body, else: exit
17751792
loop_body():
1776-
v6 = mul v0, v1
1777-
v7 = mul v6, v0
1793+
v6 = unchecked_mul v0, v1
1794+
v7 = unchecked_mul v6, v0
17781795
constrain v7 == u32 12
17791796
v10 = unchecked_add v2, u32 1
17801797
jmp loop(v10)
@@ -1793,8 +1810,8 @@ mod control_dependence {
17931810
let expected = "
17941811
brillig(inline) fn main f0 {
17951812
entry(v0: u32, v1: u32):
1796-
v3 = mul v0, v1
1797-
v4 = mul v3, v0
1813+
v3 = unchecked_mul v0, v1
1814+
v4 = unchecked_mul v3, v0
17981815
jmp loop(u32 1)
17991816
loop(v2: u32):
18001817
v7 = eq v2, u32 0
@@ -1823,8 +1840,8 @@ mod control_dependence {
18231840
v3 = lt v2, v1
18241841
jmpif v3 then: loop_body, else: exit
18251842
loop_body():
1826-
v6 = mul v0, v1
1827-
v7 = mul v6, v0
1843+
v6 = unchecked_mul v0, v1
1844+
v7 = unchecked_mul v6, v0
18281845
constrain v7 == u32 12
18291846
v10 = unchecked_add v2, u32 1
18301847
jmp loop(v10)
@@ -1844,8 +1861,8 @@ mod control_dependence {
18441861
let expected = "
18451862
brillig(inline) fn main f0 {
18461863
entry(v0: u32, v1: u32):
1847-
v3 = mul v0, v1
1848-
v4 = mul v3, v0
1864+
v3 = unchecked_mul v0, v1
1865+
v4 = unchecked_mul v3, v0
18491866
jmp loop(u32 0)
18501867
loop(v2: u32):
18511868
v6 = lt v2, v1
@@ -1876,8 +1893,8 @@ mod control_dependence {
18761893
v3 = lt v2, v1
18771894
jmpif v3 then: loop_body, else: exit
18781895
loop_body():
1879-
v6 = mul v0, v1
1880-
v7 = mul v6, v0
1896+
v6 = unchecked_mul v0, v1
1897+
v7 = unchecked_mul v6, v0
18811898
call f1()
18821899
v10 = unchecked_add v2, u32 1
18831900
jmp loop(v10)
@@ -1898,8 +1915,8 @@ mod control_dependence {
18981915
assert_ssa_snapshot!(ssa, @r"
18991916
brillig(inline) predicate_pure fn main f0 {
19001917
b0(v0: u32, v1: u32):
1901-
v3 = mul v0, v1
1902-
v4 = mul v3, v0
1918+
v3 = unchecked_mul v0, v1
1919+
v4 = unchecked_mul v3, v0
19031920
jmp b1(u32 0)
19041921
b1(v2: u32):
19051922
v6 = lt v2, v1
@@ -2203,7 +2220,6 @@ mod control_dependence {
22032220
";
22042221

22052222
let ssa = Ssa::from_str(src).unwrap();
2206-
22072223
let ssa = ssa.loop_invariant_code_motion();
22082224

22092225
assert_ssa_snapshot!(ssa, @r"
@@ -2222,4 +2238,31 @@ mod control_dependence {
22222238
}
22232239
");
22242240
}
2241+
2242+
#[test]
2243+
fn do_not_hoist_non_control_dependent_div_in_non_executed_loop() {
2244+
let src = "
2245+
brillig(inline) fn main f0 {
2246+
b0(v0: u32):
2247+
v1 = allocate -> &mut Field
2248+
store Field 0 at v1
2249+
jmp b1()
2250+
b1():
2251+
v3 = load v1 -> Field
2252+
v4 = eq v3, Field 0
2253+
jmpif v4 then: b2, else: b3
2254+
b2():
2255+
return
2256+
b3():
2257+
v6 = div u32 5, v0
2258+
jmp b1()
2259+
}
2260+
";
2261+
2262+
let ssa = Ssa::from_str(src).unwrap();
2263+
let ssa = ssa.loop_invariant_code_motion();
2264+
2265+
// We expect the SSA to be unchanged
2266+
assert_normalized_ssa_equals(ssa, src);
2267+
}
22252268
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "loop_break_regression_8319"
3+
type = "bin"
4+
authors = [""]
5+
6+
[dependencies]

test_programs/execution_success/loop_break_regression_8319/Prover.toml

Whitespace-only changes.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Regression for issue #8319 (https://github.com/noir-lang/noir/issues/8319)
2+
unconstrained fn main() {
3+
func_4(([982714762, 1608230664, 671108204]))
4+
}
5+
6+
unconstrained fn func_4(a: [u32; 3]) {
7+
{
8+
let mut idx_b = 0;
9+
loop {
10+
if (idx_b == 6) {
11+
break
12+
} else {
13+
idx_b = (idx_b + 1);
14+
{
15+
let mut idx_c = 0;
16+
loop {
17+
if (idx_c == 7) {
18+
break
19+
} else {
20+
idx_c = (idx_c + 1);
21+
for idx_d in ((a[2] / 1193346642) % 4)..(a[1] % 4) {
22+
{
23+
let mut idx_g = 0;
24+
loop {
25+
if (idx_g == 3) {
26+
break
27+
} else {
28+
idx_g = (idx_g + 1);
29+
let h = (a[0] % idx_d);
30+
println(h);
31+
}
32+
}
33+
};
34+
break;
35+
}
36+
}
37+
}
38+
};
39+
}
40+
}
41+
}
42+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "while_loop_break_regression_8521"
3+
type = "bin"
4+
authors = [""]
5+
6+
[dependencies]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x = 0
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Regression for issue #8521 (https://github.com/noir-lang/noir/issues/8521)
2+
// We do not expect division by zero in the while loop to be hoisted
3+
// x = 0
4+
unconstrained fn main(x: u32) -> pub str<3> {
5+
let mut idx_b = 0;
6+
while true {
7+
if (idx_b == 0) {
8+
break
9+
} else {
10+
idx_b = (idx_b + 1);
11+
let c = (5 / x);
12+
println(c);
13+
}
14+
}
15+
"SQF"
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[while_loop_break_regression_8521] Circuit output: String("SQF")

tooling/nargo_cli/tests/snapshots/execution_success/loop_break_regression_8319/execute__tests__force_brillig_false_inliner_-9223372036854775808.snap

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tooling/nargo_cli/tests/snapshots/execution_success/loop_break_regression_8319/execute__tests__force_brillig_false_inliner_0.snap

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)