Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
45cafc3
Unit test to show that the generated SSA is wrong
aakoshh Apr 15, 2025
bf10be1
Move test into own module
aakoshh Apr 15, 2025
9673e82
Example of the overflow as a test in unrolling
aakoshh Apr 15, 2025
475f0c8
Use i128 in unrolling
aakoshh Apr 15, 2025
db8f391
Integration test to show that negative ranges are handled
aakoshh Apr 15, 2025
0b5bf80
Add snapshot
aakoshh Apr 15, 2025
b2c510c
Hint at the env var for stdout.txt
aakoshh Apr 15, 2025
0e92d3a
Format Noir test
aakoshh Apr 15, 2025
9cd524c
Add test for non-empty negative range
aakoshh Apr 15, 2025
d995444
Merge branch 'master' into af/8009-fix-range-modulo
aakoshh Apr 15, 2025
f1299c5
fix: Don't skip for loops with negative literal ranges (#8103)
aakoshh Apr 22, 2025
aeb9a9a
Merge remote-tracking branch 'origin/master' into af/8009-fix-range-m…
aakoshh Apr 22, 2025
e99a58b
Change magic numbers into formula
aakoshh Apr 22, 2025
6e04f93
Fix insta
aakoshh Apr 22, 2025
24a2021
Merge remote-tracking branch 'origin/master' into af/8009-fix-range-m…
aakoshh Apr 22, 2025
a72c5d7
Change the test so it's a non-empty range
aakoshh Apr 22, 2025
2c5dcf6
Change the lower bound so we see the effect of the modulo
aakoshh Apr 22, 2025
c2d40fc
Update compiler/noirc_evaluator/src/ssa/ir/integer.rs
aakoshh Apr 22, 2025
5114996
Update compiler/noirc_evaluator/src/ssa/ir/integer.rs
aakoshh Apr 22, 2025
fee93d9
Update compiler/noirc_evaluator/src/ssa/ir/integer.rs
aakoshh Apr 22, 2025
0f6a4c4
Update compiler/noirc_evaluator/src/ssa/ir/integer.rs
aakoshh Apr 22, 2025
70a2ec7
Merge branch 'master' into af/8009-fix-range-modulo
aakoshh Apr 22, 2025
34cb742
Allow u128 for loop index in AST fuzzer
aakoshh Apr 22, 2025
6e9f7c9
Fix formatting
aakoshh Apr 22, 2025
6cee3bf
Fix range generator
aakoshh Apr 22, 2025
a4b3728
Merge branch 'master' into af/8009-fix-range-modulo
aakoshh Apr 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions compiler/noirc_evaluator/src/ssa/ir/dfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::{
instruction::{
Instruction, InstructionId, InstructionResultType, Intrinsic, TerminatorInstruction,
},
integer::IntegerConstant,
map::DenseMap,
types::{NumericType, Type},
value::{Value, ValueId, ValueMapping},
Expand Down Expand Up @@ -582,13 +583,22 @@ impl DataFlowGraph {
}

/// Returns the field element represented by this value if it is a numeric constant.
/// Returns None if the given value is not a numeric constant.
/// Returns `None` if the given value is not a numeric constant.
///
/// Use `get_integer_constant` if the underlying values need to be compared as signed integers.
pub(crate) fn get_numeric_constant(&self, value: ValueId) -> Option<FieldElement> {
self.get_numeric_constant_with_type(value).map(|(value, _typ)| value)
}

/// Similar to `get_numeric_constant` but returns the value as a signed or unsigned integer.
/// Returns `None` if the given value is not an integer constant.
pub(crate) fn get_integer_constant(&self, value: ValueId) -> Option<IntegerConstant> {
self.get_numeric_constant_with_type(value)
.and_then(|(f, t)| IntegerConstant::from_numeric_constant(f, t))
}

/// Returns the field element and type represented by this value if it is a numeric constant.
/// Returns None if the given value is not a numeric constant.
/// Returns `None` if the given value is not a numeric constant.
pub(crate) fn get_numeric_constant_with_type(
&self,
value: ValueId,
Expand Down
17 changes: 15 additions & 2 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ pub(crate) fn eval_constant_binary_op(
/// Values in the range `[0, 2^(bit_size-1))` are interpreted as positive integers
///
/// Values in the range `[2^(bit_size-1), 2^bit_size)` are interpreted as negative integers.
fn try_convert_field_element_to_signed_integer(field: FieldElement, bit_size: u32) -> Option<i128> {
pub(crate) fn try_convert_field_element_to_signed_integer(
field: FieldElement,
bit_size: u32,
) -> Option<i128> {
let unsigned_int = truncate(field.try_into_u128()?, bit_size);

let max_positive_value = 1 << (bit_size - 1);
Expand All @@ -219,7 +222,7 @@ fn try_convert_field_element_to_signed_integer(field: FieldElement, bit_size: u3
Some(signed_int)
}

fn convert_signed_integer_to_field_element(int: i128, bit_size: u32) -> FieldElement {
pub(crate) fn convert_signed_integer_to_field_element(int: i128, bit_size: u32) -> FieldElement {
if int >= 0 {
FieldElement::from(int)
} else {
Expand Down Expand Up @@ -375,4 +378,14 @@ mod test {
assert!(BinaryOp::Shr.get_i128_function()(1, 128).is_none());
assert!(BinaryOp::Shl.get_i128_function()(1, 128).is_none());
}

#[test]
fn test_plus_minus_one_as_field() {
for (i, u) in [(-1i64, u64::MAX), (-2i64, u64::MAX - 1), (1i64, 1u64)] {
let i: i128 = i.into();
let f = convert_signed_integer_to_field_element(i, 64);
assert_eq!(f.to_u128(), u as u128);
assert_eq!(i, try_convert_field_element_to_signed_integer(f, 64).unwrap());
}
}
}
120 changes: 120 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/integer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::cmp::Ordering;

use acvm::{AcirField, FieldElement};
use num_traits::Zero;

use super::{instruction::binary, types::NumericType};

/// A `Signed` or `Unsigned` value of a `Value::NumericConstant`, converted to 128 bits.
///
/// This type can be used in loops and other instances where values have to be compared,
/// with correct handling of negative values.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IntegerConstant {
Signed { value: i128, bit_size: u32 },
Unsigned { value: u128, bit_size: u32 },
}

impl IntegerConstant {
pub(crate) fn from_numeric_constant(field: FieldElement, typ: NumericType) -> Option<Self> {
match typ {
NumericType::Signed { bit_size } => {
binary::try_convert_field_element_to_signed_integer(field, bit_size)
.map(|value| Self::Signed { value, bit_size })
}
NumericType::Unsigned { bit_size } => {
Some(Self::Unsigned { value: field.to_u128(), bit_size })
}
NumericType::NativeField => None,
}
}

/// Convert back into a field.
pub(crate) fn into_numeric_constant(self) -> (FieldElement, NumericType) {
match self {
Self::Signed { value, bit_size } => (
binary::convert_signed_integer_to_field_element(value, bit_size),
NumericType::signed(bit_size),
),
Self::Unsigned { value, bit_size } => {
(FieldElement::from(value), NumericType::unsigned(bit_size))
}
}
}

/// Reduce two constants into a result by applying functions on them if their signedness matches.
pub(crate) fn reduce<T>(
self,
other: Self,
s: impl Fn(i128, i128) -> T,
u: impl Fn(u128, u128) -> T,
) -> Option<T> {
match (self, other) {
(Self::Signed { value: a, .. }, Self::Signed { value: b, .. }) => Some(s(a, b)),
(Self::Unsigned { value: a, .. }, Self::Unsigned { value: b, .. }) => Some(u(a, b)),
_ => None,
}
}

/// Apply functions on signed/unsigned values.
pub(crate) fn apply<T>(&self, s: impl Fn(i128) -> T, u: impl Fn(u128) -> T) -> T {
match self {
Self::Signed { value, .. } => s(*value),
Self::Unsigned { value, .. } => u(*value),
}
}

/// Increment the value by 1, saturating at the maximum value.
pub(crate) fn inc(self) -> Self {
match self {
Self::Signed { value, bit_size } => {
Self::Signed { value: value.saturating_add(1), bit_size }
}
Self::Unsigned { value, bit_size } => {
Self::Unsigned { value: value.saturating_add(1), bit_size }
}
}
}

/// Decrement the value by 1, saturating at the minimum value.
pub(crate) fn dec(self) -> Self {
match self {
Self::Signed { value, bit_size } => {
Self::Signed { value: value.saturating_sub(1), bit_size }
}
Self::Unsigned { value, bit_size } => {
Self::Unsigned { value: value.saturating_sub(1), bit_size }
}
}
}

pub(crate) fn is_zero(&self) -> bool {
match self {
Self::Signed { value, .. } => value.is_zero(),
Self::Unsigned { value, .. } => value.is_zero(),
}
}
}

impl PartialOrd for IntegerConstant {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::Signed { value: a, .. }, Self::Signed { value: b, .. }) => a.partial_cmp(b),
(Self::Signed { value: a, .. }, Self::Unsigned { value: b, .. }) => {
if a.is_negative() {
Some(Ordering::Less)
} else {
(*a).try_into().ok().and_then(|a: u128| a.partial_cmp(b))
}
}
(Self::Unsigned { value: a, .. }, Self::Signed { value: b, .. }) => {
if b.is_negative() {
Some(Ordering::Greater)
} else {
(*b).try_into().ok().and_then(|b: u128| a.partial_cmp(&b))
}
}
(Self::Unsigned { value: a, .. }, Self::Unsigned { value: b, .. }) => a.partial_cmp(b),
}
}
}
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa/ir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod dom;
pub mod function;
pub(crate) mod function_inserter;
pub mod instruction;
pub(crate) mod integer;
pub mod map;
pub(crate) mod post_order;
pub(crate) mod printer;
Expand Down
43 changes: 24 additions & 19 deletions compiler/noirc_evaluator/src/ssa/opt/loop_invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use crate::ssa::{
Binary, BinaryOp, ConstrainError, Instruction, InstructionId,
binary::eval_constant_binary_op,
},
integer::IntegerConstant,
post_order::PostOrder,
types::{NumericType, Type},
value::{Value, ValueId},
Expand Down Expand Up @@ -152,11 +153,11 @@ struct LoopInvariantContext<'f> {
// This map is expected to only ever contain a singular value.
// However, we store it in a map in order to match the definition of
// `outer_induction_variables` as both maps share checks for evaluating binary operations.
current_induction_variables: HashMap<ValueId, (FieldElement, FieldElement)>,
current_induction_variables: HashMap<ValueId, (IntegerConstant, IntegerConstant)>,
// Maps outer loop induction variable -> fixed lower and upper loop bound
// This will be used by inner loops to determine whether they
// have safe operations reliant upon an outer loop's maximum induction variable.
outer_induction_variables: HashMap<ValueId, (FieldElement, FieldElement)>,
outer_induction_variables: HashMap<ValueId, (IntegerConstant, IntegerConstant)>,
// This context struct processes runs across all loops.
// This stores the current loop's pre-header block.
// It is wrapped in an Option as our SSA `Id<T>` does not allow dummy values.
Expand Down Expand Up @@ -396,7 +397,7 @@ impl<'f> LoopInvariantContext<'f> {
let array_typ = self.inserter.function.dfg.type_of_value(*array);
let upper_bound = self.outer_induction_variables.get(index).map(|bounds| bounds.1);
if let (Type::Array(_, len), Some(upper_bound)) = (array_typ, upper_bound) {
upper_bound.to_u128() <= len.into()
upper_bound.apply(|i| i <= len.into(), |i| i <= len.into())
} else {
false
}
Expand All @@ -408,7 +409,9 @@ impl<'f> LoopInvariantContext<'f> {
// If the instruction were to be hoisted out of a loop that never executes it could potentially cause the program to fail when it is not meant to fail.
let bounds = self.current_induction_variables.values().next().copied();
let does_loop_body_execute = bounds
.map(|(lower_bound, upper_bound)| !(upper_bound - lower_bound).is_zero())
.and_then(|(lower_bound, upper_bound)| {
upper_bound.reduce(lower_bound, |u, l| u > l, |u, l| u > l)
})
.unwrap_or(false);
// If we know the loop will be executed these instructions can still only be hoisted if the instructions
// are in a non control dependent block.
Expand Down Expand Up @@ -573,13 +576,13 @@ impl<'f> LoopInvariantContext<'f> {
_ => None,
}?;

let min_iter = self.inserter.function.dfg.make_constant(*lower, NumericType::length_type());
assert!(*upper != FieldElement::zero(), "executing a non executable loop");
let max_iter = self
.inserter
.function
.dfg
.make_constant(*upper - FieldElement::one(), NumericType::length_type());
assert!(!upper.is_zero(), "executing a non executable loop");

let (upper_field, upper_type) = upper.dec().into_numeric_constant();
let (lower_field, lower_type) = lower.into_numeric_constant();

let min_iter = self.inserter.function.dfg.make_constant(lower_field, lower_type);
let max_iter = self.inserter.function.dfg.make_constant(upper_field, upper_type);
if (is_left && self.is_loop_invariant(rhs)) || (!is_left && self.is_loop_invariant(lhs)) {
return Some((is_left, min_iter, max_iter));
}
Expand Down Expand Up @@ -633,9 +636,9 @@ impl<'f> LoopInvariantContext<'f> {
lhs: &ValueId,
rhs: &ValueId,
only_outer_induction: bool,
) -> Option<(bool, FieldElement, FieldElement, FieldElement)> {
let lhs_const = self.inserter.function.dfg.get_numeric_constant_with_type(*lhs);
let rhs_const = self.inserter.function.dfg.get_numeric_constant_with_type(*rhs);
) -> Option<(bool, IntegerConstant, IntegerConstant, IntegerConstant)> {
let lhs_const = self.inserter.function.dfg.get_integer_constant(*lhs);
let rhs_const = self.inserter.function.dfg.get_integer_constant(*rhs);
match (
lhs_const,
rhs_const,
Expand All @@ -648,10 +651,10 @@ impl<'f> LoopInvariantContext<'f> {
.and_then(|v| if only_outer_induction { None } else { Some(v) })
.or(self.outer_induction_variables.get(rhs)),
) {
(Some((lhs, _)), None, None, Some((lower_bound, upper_bound))) => {
(Some(lhs), None, None, Some((lower_bound, upper_bound))) => {
Some((false, lhs, *lower_bound, *upper_bound))
}
(None, Some((rhs, _)), Some((lower_bound, upper_bound)), None) => {
(None, Some(rhs), Some((lower_bound, upper_bound)), None) => {
Some((true, rhs, *lower_bound, *upper_bound))
}
_ => None,
Expand Down Expand Up @@ -704,6 +707,8 @@ impl<'f> LoopInvariantContext<'f> {
// of its inputs to check whether it will ever overflow.
// If so, this will cause `eval_constant_binary_op` to return `None`.
// Therefore a `Some` value shows that this operation is safe.
let lhs = lhs.into_numeric_constant().0;
let rhs = rhs.into_numeric_constant().0;
if eval_constant_binary_op(lhs, rhs, binary.operator, operand_type).is_some() {
// Unchecked version of the binary operation
let unchecked = Instruction::Binary(Binary {
Expand All @@ -730,7 +735,7 @@ impl<'f> LoopInvariantContext<'f> {
true if upper_bound <= value => SimplifyResult::SimplifiedTo(self.true_value),
true if lower_bound >= value => SimplifyResult::SimplifiedTo(self.false_value),
false if lower_bound > value => SimplifyResult::SimplifiedTo(self.true_value),
false if upper_bound <= value + FieldElement::one() => {
false if upper_bound <= value.inc() => {
SimplifyResult::SimplifiedTo(self.false_value)
}
_ => SimplifyResult::None,
Expand All @@ -757,10 +762,10 @@ impl<'f> LoopInvariantContext<'f> {
return false;
};
if left {
if value != FieldElement::zero() {
if !value.is_zero() {
return true;
}
} else if lower != FieldElement::zero() {
} else if !lower.is_zero() {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl Ssa {
/// During normal compilation this is often not the case since prior passes
/// may increase the ID counter so that later passes start at different offsets,
/// even if they contain the same SSA code.
pub(crate) fn normalize_ids(&mut self) {
pub fn normalize_ids(&mut self) {
let mut context = Context::default();
context.populate_functions(&self.functions);
for function in self.functions.values_mut() {
Expand Down
Loading
Loading