Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add array_refcount and slice_refcount builtins for debugging #6584

Merged
merged 8 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions compiler/noirc_evaluator/src/acir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,13 @@ impl<'a> Context<'a> {
Intrinsic::FieldLessThan => {
unreachable!("FieldLessThan can only be called in unconstrained")
}
Intrinsic::ArrayRefCount | Intrinsic::SliceRefCount => {
let zero = self.acir_context.add_constant(FieldElement::zero());
michaeljklein marked this conversation as resolved.
Show resolved Hide resolved
Ok(vec![AcirValue::Var(
zero,
AcirType::NumericType(NumericType::Unsigned { bit_size: 32 }),
)])
}
}
}

Expand Down
427 changes: 234 additions & 193 deletions compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,18 @@ impl Context {
| Intrinsic::IsUnconstrained => {}
Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::ArrayRefCount
| Intrinsic::AsField
| Intrinsic::AsSlice
| Intrinsic::BlackBox(..)
| Intrinsic::DerivePedersenGenerators
| Intrinsic::FromField
| Intrinsic::SliceInsert
| Intrinsic::SlicePushBack
| Intrinsic::SlicePushFront
| Intrinsic::SlicePopBack
| Intrinsic::SlicePopFront
| Intrinsic::SliceInsert
| Intrinsic::SliceRefCount
| Intrinsic::SliceRemove
| Intrinsic::StaticAssert
| Intrinsic::StrAsBytes
Expand Down
10 changes: 10 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
IsUnconstrained,
DerivePedersenGenerators,
FieldLessThan,
ArrayRefCount,
SliceRefCount,
}

impl std::fmt::Display for Intrinsic {
Expand Down Expand Up @@ -100,6 +102,8 @@
Intrinsic::IsUnconstrained => write!(f, "is_unconstrained"),
Intrinsic::DerivePedersenGenerators => write!(f, "derive_pedersen_generators"),
Intrinsic::FieldLessThan => write!(f, "field_less_than"),
Intrinsic::ArrayRefCount => write!(f, "array_refcount"),
Intrinsic::SliceRefCount => write!(f, "slice_refcount"),
}
}
}
Expand All @@ -113,6 +117,10 @@
Intrinsic::AssertConstant
| Intrinsic::StaticAssert
| Intrinsic::ApplyRangeConstraint
// Array & slice ref counts are treated as having side effects since they operate
// on hidden variables on otherwise identical array values.
| Intrinsic::ArrayRefCount
| Intrinsic::SliceRefCount
| Intrinsic::AsWitness => true,

// These apply a constraint that the input must fit into a specified number of limbs.
Expand Down Expand Up @@ -171,6 +179,8 @@
"is_unconstrained" => Some(Intrinsic::IsUnconstrained),
"derive_pedersen_generators" => Some(Intrinsic::DerivePedersenGenerators),
"field_less_than" => Some(Intrinsic::FieldLessThan),
"array_refcount" => Some(Intrinsic::ArrayRefCount),
"slice_refcount" => Some(Intrinsic::SliceRefCount),

other => BlackBoxFunc::lookup(other).map(Intrinsic::BlackBox),
}
Expand Down Expand Up @@ -345,7 +355,7 @@
// We can deduplicate these instructions if we know the predicate is also the same.
Constrain(..) | RangeCheck { .. } => deduplicate_with_predicate,

// This should never be side-effectful

Check warning on line 358 in compiler/noirc_evaluator/src/ssa/ir/instruction.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (effectful)
MakeArray { .. } => true,

// These can have different behavior depending on the EnableSideEffectsIf context.
Expand Down
2 changes: 2 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ pub(super) fn simplify_call(
SimplifyResult::None
}
}
Intrinsic::ArrayRefCount => SimplifyResult::None,
Intrinsic::SliceRefCount => SimplifyResult::None,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ impl Context {
| Intrinsic::AsWitness
| Intrinsic::IsUnconstrained
| Intrinsic::DerivePedersenGenerators
| Intrinsic::ArrayRefCount
| Intrinsic::SliceRefCount
| Intrinsic::FieldLessThan => false,
},

Expand Down
2 changes: 2 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ fn slice_capacity_change(
| Intrinsic::DerivePedersenGenerators
| Intrinsic::ToBits(_)
| Intrinsic::ToRadix(_)
| Intrinsic::ArrayRefCount
| Intrinsic::SliceRefCount
| Intrinsic::FieldLessThan => SizeChange::None,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl<'local, 'context> Interpreter<'local, 'context> {
"apply_range_constraint" => foreign::apply_range_constraint(arguments, location),
"array_as_str_unchecked" => array_as_str_unchecked(interner, arguments, location),
"array_len" => array_len(interner, arguments, location),
"array_refcount" => Ok(Value::U32(0)),
"assert_constant" => Ok(Value::Bool(true)),
"as_slice" => as_slice(interner, arguments, location),
"ctstring_eq" => ctstring_eq(arguments, location),
Expand Down Expand Up @@ -167,6 +168,7 @@ impl<'local, 'context> Interpreter<'local, 'context> {
"slice_pop_front" => slice_pop_front(interner, arguments, location, call_stack),
"slice_push_back" => slice_push_back(interner, arguments, location),
"slice_push_front" => slice_push_front(interner, arguments, location),
"slice_refcount" => Ok(Value::U32(0)),
"slice_remove" => slice_remove(interner, arguments, location, call_stack),
"str_as_bytes" => str_as_bytes(interner, arguments, location),
"str_as_ctstring" => str_as_ctstring(interner, arguments, location),
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"quantile",
"quasiquote",
"rangemap",
"refcount",
"repr",
"reqwest",
"rfind",
Expand Down
30 changes: 30 additions & 0 deletions docs/docs/noir/standard_library/mem.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,33 @@ by checking this equality once `N`, `A`, `B` are fully resolved.

Note that since this safety check is performed after type checking rather than during, no error is issued if the function
containing `checked_transmute` is never called.

# `std::mem::array_refcount`

```rust
fn array_refcount<T, let N: u32>(array: [T; N]) -> u32 {}
```

Returns the internal reference count of an array value in unconstrained code.

Arrays only have reference count in unconstrained code - using this anywhere
else will return zero.

This function is mostly intended for debugging compiler optimizations but can also be used
to find where array copies may be happening in unconstrained code by placing it before array
mutations.

# `std::mem::slice_refcount`

```rust
fn slice_refcount<T>(slice: [T]) -> u32 {}
```

Returns the internal reference count of a slice value in unconstrained code.

Slices only have reference count in unconstrained code - using this anywhere
else will return zero.

This function is mostly intended for debugging compiler optimizations but can also be used
to find where slice copies may be happening in unconstrained code by placing it before slice
mutations.
14 changes: 14 additions & 0 deletions noir_stdlib/src/mem.nr
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ pub fn zeroed<T>() -> T {}
/// that it is equal to the previous.
#[builtin(checked_transmute)]
pub fn checked_transmute<T, U>(value: T) -> U {}

/// Returns the internal reference count of an array value in unconstrained code.
///
/// Arrays only have reference count in unconstrained code - using this anywhere
/// else will return zero.
#[builtin(array_refcount)]
pub fn array_refcount<T, let N: u32>(array: [T; N]) -> u32 {}

/// Returns the internal reference count of a slice value in unconstrained code.
///
/// Slices only have reference count in unconstrained code - using this anywhere
/// else will return zero.
#[builtin(slice_refcount)]
pub fn slice_refcount<T>(slice: [T]) -> u32 {}
7 changes: 7 additions & 0 deletions test_programs/execution_success/reference_counts/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "reference_counts"
type = "bin"
authors = [""]
compiler_version = ">=0.35.0"

[dependencies]
2 changes: 2 additions & 0 deletions test_programs/execution_success/reference_counts/Prover.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
x = 5
b = true
40 changes: 40 additions & 0 deletions test_programs/execution_success/reference_counts/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
fn main() {
let mut array = [0, 1, 2];
assert_refcount(array, 1);

borrow(array, std::mem::array_refcount(array));
borrow_mut(&mut array, std::mem::array_refcount(array));
copy_mut(array, std::mem::array_refcount(array));
}

fn borrow(array: [Field; 3], rc_before_call: u32) {
assert_refcount(array, rc_before_call);
println(array[0]);
}

fn borrow_mut(array: &mut [Field; 3], rc_before_call: u32) {
assert_refcount(*array, rc_before_call + 0); // Issue! This should be rc_before_call + 1
michaeljklein marked this conversation as resolved.
Show resolved Hide resolved
array[0] = 5;
println(array[0]);
}

fn copy_mut(mut array: [Field; 3], rc_before_call: u32) {
assert_refcount(array, rc_before_call + 0); // Issue! This should be rc_before_call + 1
array[0] = 6;
println(array[0]);
}

fn assert_refcount(array: [Field; 3], expected: u32) {
let count = std::mem::array_refcount(array);

// All refcounts are zero when running this as a constrained program
if std::runtime::is_unconstrained() {
if count != expected {
// Brillig doesn't print the actual & expected arguments on assertion failure
println(f"actual = {count}, expected = {expected}");
}
assert_eq(count, expected);
} else {
assert_eq(count, 0);
}
}
3 changes: 2 additions & 1 deletion tooling/debugger/ignored-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ brillig_references
debug_logs
is_unconstrained
macros
reference_counts
references
regression_4709
reference_only_used_as_alias
brillig_rc_regression_6123
brillig_rc_regression_6123
6 changes: 5 additions & 1 deletion tooling/nargo_cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@
];

/// Tests which aren't expected to work with the default inliner cases.
const INLINER_MIN_OVERRIDES: [(&str, i64); 1] = [
const INLINER_MIN_OVERRIDES: [(&str, i64); 2] = [
// 0 works if PoseidonHasher::write is tagged as `inline_always`, otherwise 22.
("eddsa", 0),
// (#6583): The RcTracker in the DIE SSA pass is removing inc_rcs that are still needed.
// This triggers differently depending on the optimization level (although all are wrong),
// so we arbitrarily only run with the inlined versions.
("reference_counts", 0),
];

/// Some tests are expected to have warnings
Expand Down Expand Up @@ -172,9 +176,9 @@
let test_cases = test_cases.join("\n");

// We need to isolate test cases in the same group, otherwise they overwrite each other's artifacts.
// On CI we use `cargo nextest`, which runs tests in different processes; for this we use a file lock.

Check warning on line 179 in tooling/nargo_cli/build.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (nextest)
// Locally we might be using `cargo test`, which run tests in the same process; in this case the file lock
// wouldn't work, becuase the process itself has the lock, and it looks like it can have N instances without

Check warning on line 181 in tooling/nargo_cli/build.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (becuase)
// any problems; for this reason we also use a `Mutex`.
let mutex_name = format! {"TEST_MUTEX_{}", test_name.to_uppercase()};
write!(
Expand Down
Loading