Skip to content

Commit

Permalink
feat: Add array_refcount and slice_refcount builtins for debugging (
Browse files Browse the repository at this point in the history
  • Loading branch information
jfecher authored Nov 22, 2024
1 parent 8ff4efd commit 45eb756
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 196 deletions.
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());
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 @@ pub(crate) enum Intrinsic {
IsUnconstrained,
DerivePedersenGenerators,
FieldLessThan,
ArrayRefCount,
SliceRefCount,
}

impl std::fmt::Display for Intrinsic {
Expand Down Expand Up @@ -100,6 +102,8 @@ impl std::fmt::Display for Intrinsic {
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 @@ impl Intrinsic {
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 @@ impl Intrinsic {
"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
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 @@ -183,6 +183,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
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 @@ const IGNORED_BRILLIG_TESTS: [&str; 11] = [
];

/// 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

0 comments on commit 45eb756

Please sign in to comment.