From 50d1bf9dfaa09f783cfa12cc8c42c49e469adaeb Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 08:31:28 +0000 Subject: [PATCH 01/26] feat: richer EphemeralArray and TransientArray APIs Adds empty(), map, filter, any, all, find and read_as to EphemeralArray (spun off from #23928) and mirrors them on TransientArray. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 423 ++++++++++++++++++ .../aztec-nr/aztec/src/transient/mod.nr | 423 ++++++++++++++++++ 2 files changed, 846 insertions(+) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 2b64fe54f4a3..74cc36b813aa 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,4 +1,5 @@ use crate::oracle::ephemeral; +use crate::oracle::random::random; use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; @@ -39,6 +40,15 @@ impl EphemeralArray { Self::at(slot).clear() } + /// Returns an empty ephemeral array at a fresh, randomly allocated slot. + /// + /// Use this when the caller does not need a specific slot: the random slot is isolated from every other ephemeral + /// array with overwhelming probability. Prefer [`EphemeralArray::empty_at`] when the slot must be a known value + /// (e.g. one shared with an oracle). + pub unconstrained fn empty() -> Self { + Self::empty_at(random()) + } + /// Returns the number of elements stored in the array. pub unconstrained fn len(self) -> u32 { ephemeral::len_oracle(self.slot) @@ -110,6 +120,104 @@ impl EphemeralArray { f(i, self.get(i)); } } + + /// Applies `f` to every element and collects the results into a fresh ephemeral array. + /// + /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly + /// allocated slot (see [`EphemeralArray::empty`]). The source array is left unchanged, and the result is isolated + /// from it, so `map` can be chained. + pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> EphemeralArray + where + T: Deserialize, + U: Serialize, + { + let dest: EphemeralArray = EphemeralArray::empty(); + let n = self.len(); + for i in 0..n { + dest.push(f(self.get(i))); + } + dest + } + + /// Collects every element satisfying the predicate `f` into a fresh ephemeral array. + /// + /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a + /// freshly allocated slot (see [`EphemeralArray::empty`]). Relative order is preserved. The source array is left + /// unchanged, and the result is isolated from it, so `filter` can be chained. + pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self + where + T: Serialize + Deserialize, + { + let dest: Self = EphemeralArray::empty(); + let n = self.len(); + for i in 0..n { + let value = self.get(i); + if f(value) { + dest.push(value); + } + } + dest + } + + /// Returns `true` if at least one element satisfies the predicate `f`. + /// + /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in + /// terms of [`EphemeralArray::filter`]: it keeps the matching elements and checks whether any survived. This is + /// not short-circuiting — every element is tested even after the first match. + pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() != 0 + } + + /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). + /// + /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of + /// [`EphemeralArray::filter`]: every element matches exactly when filtering keeps all of them. This is not + /// short-circuiting — every element is tested even after the first failure. + pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() == self.len() + } + + /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. + /// + /// Mirrors Rust's `Iterator::find`. Defined in terms of [`EphemeralArray::filter`], which preserves order, so the + /// first kept element is the first match. This is not short-circuiting — every element is tested even after the + /// match is found. + pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option + where + T: Serialize + Deserialize, + { + let matches = self.filter(f); + if matches.len() != 0 { + Option::some(matches.get(0)) + } else { + Option::none() + } + } +} + +impl EphemeralArray { + /// Deserializes the whole array into a `T`. + /// + /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the + /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on + /// `EphemeralArray` because deserialization reconstructs a type from raw fields. + pub unconstrained fn read_as(self) -> T + where + T: Deserialize, + { + assert_eq(self.len(), ::N, "EphemeralArray length mismatch for read_as"); + let mut fields: [Field; ::N] = [0; ::N]; + for i in 0..::N { + fields[i] = self.get(i); + } + Deserialize::deserialize(fields) + } } /// Serializes an `EphemeralArray` as its slot identifier, allowing oracle function signatures to use @@ -141,6 +249,7 @@ impl Deserialize for EphemeralArray { } mod test { + use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; use super::EphemeralArray; @@ -372,6 +481,35 @@ mod test { }); } + #[test] + unconstrained fn read_as_reconstructs_serialized_value() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + + let value = MockStruct::new(5, 6); + let serialized = value.serialize(); + for i in 0..serialized.len() { + array.push(serialized[i]); + } + + let reconstructed: MockStruct = array.read_as(); + assert_eq(reconstructed, value); + }); + } + + #[test(should_fail_with = "length mismatch")] + unconstrained fn read_as_rejects_length_mismatch() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + // MockStruct deserializes from 2 fields, so a single field is too short. + array.push(1); + + let _: MockStruct = array.read_as(); + }); + } + #[test] unconstrained fn empty_at_wipes_previous_data() { let env = TestEnvironment::new(); @@ -415,4 +553,289 @@ mod test { assert_eq(fresh.get(0), 4); }); } + + #[test] + unconstrained fn empty_allocates_distinct_slots() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let a: EphemeralArray = EphemeralArray::empty(); + let b: EphemeralArray = EphemeralArray::empty(); + + assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); + + a.push(1); + b.push(2); + assert_eq(a.len(), 1); + assert_eq(a.get(0), 1); + assert_eq(b.len(), 1); + assert_eq(b.get(0), 2); + }); + } + + #[test] + unconstrained fn map_transforms_each_element() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let doubled: EphemeralArray = source.map(|x| x * 2); + + assert_eq(doubled.len(), 3); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(1), 4); + assert_eq(doubled.get(2), 6); + + // The source array is left untouched. + assert_eq(source.len(), 3); + assert_eq(source.get(0), 1); + }); + } + + #[test] + unconstrained fn map_empty_source_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + let mapped: EphemeralArray = source.map(|x| x * 2); + assert_eq(mapped.len(), 0); + }); + } + + #[test] + unconstrained fn map_to_different_type() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(5); + source.push(7); + + let structs: EphemeralArray = source.map(|x| MockStruct::new(x, x + 1)); + + assert_eq(structs.len(), 2); + assert_eq(structs.get(0), MockStruct::new(5, 6)); + assert_eq(structs.get(1), MockStruct::new(7, 8)); + }); + } + + #[test] + unconstrained fn map_results_are_isolated() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. + let doubled: EphemeralArray = source.map(|x| x * 2); + let tripled: EphemeralArray = source.map(|x| x * 3); + + assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(2), 6); + assert_eq(tripled.get(0), 3); + assert_eq(tripled.get(2), 9); + }); + } + + #[test] + unconstrained fn filter_keeps_matching_elements_in_order() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(2); + source.push(5); + + let kept: EphemeralArray = source.filter(|x| x != 2); + + assert_eq(kept.len(), 3); + assert_eq(kept.get(0), 1); + assert_eq(kept.get(1), 3); + assert_eq(kept.get(2), 5); + + // The source array is left untouched. + assert_eq(source.len(), 5); + assert_eq(source.get(1), 2); + }); + } + + #[test] + unconstrained fn filter_empty_source_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + let kept: EphemeralArray = source.filter(|_| true); + assert_eq(kept.len(), 0); + }); + } + + #[test] + unconstrained fn filter_none_match_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let kept: EphemeralArray = source.filter(|_| false); + assert_eq(kept.len(), 0); + }); + } + + #[test] + unconstrained fn filter_works_with_multi_field_type() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(MockStruct::new(1, 10)); + source.push(MockStruct::new(2, 20)); + source.push(MockStruct::new(3, 30)); + + let kept: EphemeralArray = source.filter(|s: MockStruct| s.a != 2); + + assert_eq(kept.len(), 2); + assert_eq(kept.get(0), MockStruct::new(1, 10)); + assert_eq(kept.get(1), MockStruct::new(3, 30)); + }); + } + + #[test] + unconstrained fn filter_results_are_isolated() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: EphemeralArray = EphemeralArray::empty_at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(4); + + // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. + let odds: EphemeralArray = source.filter(|x| (x != 2) & (x != 4)); + let evens: EphemeralArray = source.filter(|x| (x != 1) & (x != 3)); + + assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); + assert_eq(odds.len(), 2); + assert_eq(odds.get(0), 1); + assert_eq(odds.get(1), 3); + assert_eq(evens.len(), 2); + assert_eq(evens.get(0), 2); + assert_eq(evens.get(1), 4); + }); + } + + #[test] + unconstrained fn any_is_true_when_an_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.any(|x| x == 2)); + }); + } + + #[test] + unconstrained fn any_is_false_when_no_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.any(|x| x == 9)); + }); + } + + #[test] + unconstrained fn any_on_empty_array_is_false() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + assert(!array.any(|_| true)); + }); + } + + #[test] + unconstrained fn all_is_true_when_every_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.all(|x| x != 0)); + }); + } + + #[test] + unconstrained fn all_is_false_when_one_element_fails() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.all(|x| x != 2)); + }); + } + + #[test] + unconstrained fn all_on_empty_array_is_true() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + assert(array.all(|_| false)); + }); + } + + #[test] + unconstrained fn find_returns_first_matching_element() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + array.push(4); + + // Both 2 and 4 match; find must return the first one in order. + let found = array.find(|x| (x == 2) | (x == 4)); + assert(found.is_some()); + assert_eq(found.unwrap(), 2); + }); + } + + #[test] + unconstrained fn find_returns_none_when_no_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.find(|x| x == 9).is_none()); + }); + } + + #[test] + unconstrained fn find_on_empty_array_is_none() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::empty_at(SLOT); + assert(array.find(|_| true).is_none()); + }); + } } diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index d143b0c294ed..ff70f59e4263 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,3 +1,4 @@ +use crate::oracle::random::random; use crate::oracle::transient; use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; @@ -40,6 +41,15 @@ impl TransientArray { Self { slot } } + /// Returns an empty transient array at a fresh, randomly allocated slot. + /// + /// Use this when the caller does not need a specific slot: the random slot is isolated from every other transient + /// array with overwhelming probability. Prefer [`TransientArray::at`] when the slot must be a known value (e.g. + /// one agreed upon with another frame of the same contract). + pub unconstrained fn empty() -> Self { + Self::at(random()).clear() + } + /// Returns the number of elements stored in the array. pub unconstrained fn len(self) -> u32 { transient::len_oracle(self.slot) @@ -107,6 +117,104 @@ impl TransientArray { f(i, self.get(i)); } } + + /// Applies `f` to every element and collects the results into a fresh transient array. + /// + /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly + /// allocated slot (see [`TransientArray::empty`]). The source array is left unchanged, and the result is isolated + /// from it, so `map` can be chained. + pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> TransientArray + where + T: Deserialize, + U: Serialize, + { + let dest: TransientArray = TransientArray::empty(); + let n = self.len(); + for i in 0..n { + dest.push(f(self.get(i))); + } + dest + } + + /// Collects every element satisfying the predicate `f` into a fresh transient array. + /// + /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a + /// freshly allocated slot (see [`TransientArray::empty`]). Relative order is preserved. The source array is left + /// unchanged, and the result is isolated from it, so `filter` can be chained. + pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self + where + T: Serialize + Deserialize, + { + let dest: Self = TransientArray::empty(); + let n = self.len(); + for i in 0..n { + let value = self.get(i); + if f(value) { + dest.push(value); + } + } + dest + } + + /// Returns `true` if at least one element satisfies the predicate `f`. + /// + /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in + /// terms of [`TransientArray::filter`]: it keeps the matching elements and checks whether any survived. This is + /// not short-circuiting — every element is tested even after the first match. + pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() != 0 + } + + /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). + /// + /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of + /// [`TransientArray::filter`]: every element matches exactly when filtering keeps all of them. This is not + /// short-circuiting — every element is tested even after the first failure. + pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() == self.len() + } + + /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. + /// + /// Mirrors Rust's `Iterator::find`. Defined in terms of [`TransientArray::filter`], which preserves order, so the + /// first kept element is the first match. This is not short-circuiting — every element is tested even after the + /// match is found. + pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option + where + T: Serialize + Deserialize, + { + let matches = self.filter(f); + if matches.len() != 0 { + Option::some(matches.get(0)) + } else { + Option::none() + } + } +} + +impl TransientArray { + /// Deserializes the whole array into a `T`. + /// + /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the + /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on + /// `TransientArray` because deserialization reconstructs a type from raw fields. + pub unconstrained fn read_as(self) -> T + where + T: Deserialize, + { + assert_eq(self.len(), ::N, "TransientArray length mismatch for read_as"); + let mut fields: [Field; ::N] = [0; ::N]; + for i in 0..::N { + fields[i] = self.get(i); + } + Deserialize::deserialize(fields) + } } /// Serializes a `TransientArray` as its slot identifier, allowing oracle function signatures to use `TransientArray` @@ -165,6 +273,7 @@ pub unconstrained fn delete(slot: Field) { } mod test { + use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; use super::{delete, load, store, TransientArray}; @@ -402,6 +511,35 @@ mod test { }); } + #[test] + unconstrained fn read_as_reconstructs_serialized_value() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + + let value = MockStruct::new(5, 6); + let serialized = value.serialize(); + for i in 0..serialized.len() { + array.push(serialized[i]); + } + + let reconstructed: MockStruct = array.read_as(); + assert_eq(reconstructed, value); + }); + } + + #[test(should_fail_with = "length mismatch")] + unconstrained fn read_as_rejects_length_mismatch() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + // MockStruct deserializes from 2 fields, so a single field is too short. + array.push(1); + + let _: MockStruct = array.read_as(); + }); + } + #[test] unconstrained fn clear_returns_self() { let env = TestEnvironment::new(); @@ -432,6 +570,291 @@ mod test { }); } + #[test] + unconstrained fn empty_allocates_distinct_slots() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let a: TransientArray = TransientArray::empty(); + let b: TransientArray = TransientArray::empty(); + + assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); + + a.push(1); + b.push(2); + assert_eq(a.len(), 1); + assert_eq(a.get(0), 1); + assert_eq(b.len(), 1); + assert_eq(b.get(0), 2); + }); + } + + #[test] + unconstrained fn map_transforms_each_element() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let doubled: TransientArray = source.map(|x| x * 2); + + assert_eq(doubled.len(), 3); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(1), 4); + assert_eq(doubled.get(2), 6); + + // The source array is left untouched. + assert_eq(source.len(), 3); + assert_eq(source.get(0), 1); + }); + } + + #[test] + unconstrained fn map_empty_source_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + let mapped: TransientArray = source.map(|x| x * 2); + assert_eq(mapped.len(), 0); + }); + } + + #[test] + unconstrained fn map_to_different_type() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(5); + source.push(7); + + let structs: TransientArray = source.map(|x| MockStruct::new(x, x + 1)); + + assert_eq(structs.len(), 2); + assert_eq(structs.get(0), MockStruct::new(5, 6)); + assert_eq(structs.get(1), MockStruct::new(7, 8)); + }); + } + + #[test] + unconstrained fn map_results_are_isolated() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. + let doubled: TransientArray = source.map(|x| x * 2); + let tripled: TransientArray = source.map(|x| x * 3); + + assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(2), 6); + assert_eq(tripled.get(0), 3); + assert_eq(tripled.get(2), 9); + }); + } + + #[test] + unconstrained fn filter_keeps_matching_elements_in_order() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(2); + source.push(5); + + let kept: TransientArray = source.filter(|x| x != 2); + + assert_eq(kept.len(), 3); + assert_eq(kept.get(0), 1); + assert_eq(kept.get(1), 3); + assert_eq(kept.get(2), 5); + + // The source array is left untouched. + assert_eq(source.len(), 5); + assert_eq(source.get(1), 2); + }); + } + + #[test] + unconstrained fn filter_empty_source_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + let kept: TransientArray = source.filter(|_| true); + assert_eq(kept.len(), 0); + }); + } + + #[test] + unconstrained fn filter_none_match_gives_empty_dest() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let kept: TransientArray = source.filter(|_| false); + assert_eq(kept.len(), 0); + }); + } + + #[test] + unconstrained fn filter_works_with_multi_field_type() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(MockStruct::new(1, 10)); + source.push(MockStruct::new(2, 20)); + source.push(MockStruct::new(3, 30)); + + let kept: TransientArray = source.filter(|s: MockStruct| s.a != 2); + + assert_eq(kept.len(), 2); + assert_eq(kept.get(0), MockStruct::new(1, 10)); + assert_eq(kept.get(1), MockStruct::new(3, 30)); + }); + } + + #[test] + unconstrained fn filter_results_are_isolated() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: TransientArray = TransientArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(4); + + // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. + let odds: TransientArray = source.filter(|x| (x != 2) & (x != 4)); + let evens: TransientArray = source.filter(|x| (x != 1) & (x != 3)); + + assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); + assert_eq(odds.len(), 2); + assert_eq(odds.get(0), 1); + assert_eq(odds.get(1), 3); + assert_eq(evens.len(), 2); + assert_eq(evens.get(0), 2); + assert_eq(evens.get(1), 4); + }); + } + + #[test] + unconstrained fn any_is_true_when_an_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.any(|x| x == 2)); + }); + } + + #[test] + unconstrained fn any_is_false_when_no_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.any(|x| x == 9)); + }); + } + + #[test] + unconstrained fn any_on_empty_array_is_false() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + assert(!array.any(|_| true)); + }); + } + + #[test] + unconstrained fn all_is_true_when_every_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.all(|x| x != 0)); + }); + } + + #[test] + unconstrained fn all_is_false_when_one_element_fails() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.all(|x| x != 2)); + }); + } + + #[test] + unconstrained fn all_on_empty_array_is_true() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + assert(array.all(|_| false)); + }); + } + + #[test] + unconstrained fn find_returns_first_matching_element() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + array.push(4); + + // Both 2 and 4 match; find must return the first one in order. + let found = array.find(|x| (x == 2) | (x == 4)); + assert(found.is_some()); + assert_eq(found.unwrap(), 2); + }); + } + + #[test] + unconstrained fn find_returns_none_when_no_element_matches() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.find(|x| x == 9).is_none()); + }); + } + + #[test] + unconstrained fn find_on_empty_array_is_none() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: TransientArray = TransientArray::at(SLOT); + assert(array.find(|_| true).is_none()); + }); + } + #[test] unconstrained fn store_and_load() { let env = TestEnvironment::new(); From fe86aae52b6b862824d02af9d0729f10a5b17c0f Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 09:54:45 +0000 Subject: [PATCH 02/26] refactor: unify EphemeralArray and TransientArray over OracleArray Both types are now aliases of a single OracleArray generic, parameterized by an ArrayOracle backend trait whose impls wrap the existing #[oracle] declarations. Foreign-call names and the TS-side services are untouched; monomorphization emits the same calls as before. empty_at stays ephemeral-only via a specialized impl. Test suites are byte-for-byte unchanged. Expansion snapshots regenerated: nargo expand now prints the resolved OracleArray paths. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 232 ++------------- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec-nr/aztec/src/oracle_array/mod.nr | 265 ++++++++++++++++++ .../aztec-nr/aztec/src/transient/mod.nr | 224 ++------------- .../invalid_note/snapshots__stderr.snap | 5 +- .../amm_contract/snapshots__expanded.snap | 3 +- .../snapshots__expanded.snap | 3 +- .../snapshots__expanded.snap | 3 +- .../snapshots__expanded.snap | 3 +- .../snapshots__expanded.snap | 3 +- .../token_contract/snapshots__expanded.snap | 3 +- 11 files changed, 329 insertions(+), 416 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 74cc36b813aa..86b3e63f36a8 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,7 +1,5 @@ use crate::oracle::ephemeral; -use crate::oracle::random::random; -use crate::protocol::traits::{Deserialize, Serialize}; -use crate::protocol::utils::{reader::Reader, writer::Writer}; +use crate::oracle_array::{ArrayOracle, OracleArray}; /// A dynamically sized array that exists only during a single contract call frame. /// @@ -24,227 +22,49 @@ use crate::protocol::utils::{reader::Reader, writer::Writer}; /// For data that must be shared across all frames of the same contract (private and utility) within one top-level PXE /// call (transaction simulation or utility call) but not persisted, use /// [`TransientArray`](crate::transient::TransientArray). -pub struct EphemeralArray { - pub slot: Field, -} +pub type EphemeralArray = OracleArray; -impl EphemeralArray { - /// Returns a handle to an ephemeral array at the given slot, which may already contain data (e.g. populated - /// by an oracle). - pub unconstrained fn at(slot: Field) -> Self { - Self { slot } - } +/// Routes [`OracleArray`] operations to the ephemeral array oracles, scoping arrays to a single contract call frame. +pub struct EphemeralOracle {} - /// Returns an empty ephemeral array at the given slot, clearing any pre-existing data. - pub unconstrained fn empty_at(slot: Field) -> Self { - Self::at(slot).clear() +impl ArrayOracle for EphemeralOracle { + unconstrained fn len_oracle(slot: Field) -> u32 { + ephemeral::len_oracle(slot) } - /// Returns an empty ephemeral array at a fresh, randomly allocated slot. - /// - /// Use this when the caller does not need a specific slot: the random slot is isolated from every other ephemeral - /// array with overwhelming probability. Prefer [`EphemeralArray::empty_at`] when the slot must be a known value - /// (e.g. one shared with an oracle). - pub unconstrained fn empty() -> Self { - Self::empty_at(random()) + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { + ephemeral::push_oracle(slot, values) } - /// Returns the number of elements stored in the array. - pub unconstrained fn len(self) -> u32 { - ephemeral::len_oracle(self.slot) + unconstrained fn pop_oracle(slot: Field) -> [Field; N] { + ephemeral::pop_oracle(slot) } - /// Stores a value at the end of the array. - pub unconstrained fn push(self, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - let _ = ephemeral::push_oracle(self.slot, serialized); - } - - /// Removes and returns the last element. Panics if the array is empty. - pub unconstrained fn pop(self) -> T - where - T: Deserialize, - { - let serialized = ephemeral::pop_oracle(self.slot); - Deserialize::deserialize(serialized) - } - - /// Retrieves the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn get(self, index: u32) -> T - where - T: Deserialize, - { - let serialized = ephemeral::get_oracle(self.slot, index); - Deserialize::deserialize(serialized) - } - - /// Overwrites the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn set(self, index: u32, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - ephemeral::set_oracle(self.slot, index, serialized); - } - - /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. - pub unconstrained fn remove(self, index: u32) { - ephemeral::remove_oracle(self.slot, index); - } - - /// Removes all elements from the array and returns self for chaining. - /// - /// Prefer [`EphemeralArray::empty_at`] when the intent is to start with a fresh array. - pub unconstrained fn clear(self) -> Self { - ephemeral::clear_oracle(self.slot); - self - } - - /// Calls a function on each element of the array. - /// - /// The function `f` is called once with each array value and its corresponding index. Iteration proceeds - /// backwards so that it is safe to remove the current element (and only the current element) inside the - /// callback. - /// - /// It is **not** safe to push new elements from inside the callback. - pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) - where - T: Deserialize, - { - let mut i = self.len(); - while i > 0 { - i -= 1; - f(i, self.get(i)); - } - } - - /// Applies `f` to every element and collects the results into a fresh ephemeral array. - /// - /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly - /// allocated slot (see [`EphemeralArray::empty`]). The source array is left unchanged, and the result is isolated - /// from it, so `map` can be chained. - pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> EphemeralArray - where - T: Deserialize, - U: Serialize, - { - let dest: EphemeralArray = EphemeralArray::empty(); - let n = self.len(); - for i in 0..n { - dest.push(f(self.get(i))); - } - dest - } - - /// Collects every element satisfying the predicate `f` into a fresh ephemeral array. - /// - /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a - /// freshly allocated slot (see [`EphemeralArray::empty`]). Relative order is preserved. The source array is left - /// unchanged, and the result is isolated from it, so `filter` can be chained. - pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self - where - T: Serialize + Deserialize, - { - let dest: Self = EphemeralArray::empty(); - let n = self.len(); - for i in 0..n { - let value = self.get(i); - if f(value) { - dest.push(value); - } - } - dest + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { + ephemeral::get_oracle(slot, index) } - /// Returns `true` if at least one element satisfies the predicate `f`. - /// - /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in - /// terms of [`EphemeralArray::filter`]: it keeps the matching elements and checks whether any survived. This is - /// not short-circuiting — every element is tested even after the first match. - pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool - where - T: Serialize + Deserialize, - { - self.filter(f).len() != 0 + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { + ephemeral::set_oracle(slot, index, values) } - /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). - /// - /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of - /// [`EphemeralArray::filter`]: every element matches exactly when filtering keeps all of them. This is not - /// short-circuiting — every element is tested even after the first failure. - pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool - where - T: Serialize + Deserialize, - { - self.filter(f).len() == self.len() + unconstrained fn remove_oracle(slot: Field, index: u32) { + ephemeral::remove_oracle(slot, index) } - /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. - /// - /// Mirrors Rust's `Iterator::find`. Defined in terms of [`EphemeralArray::filter`], which preserves order, so the - /// first kept element is the first match. This is not short-circuiting — every element is tested even after the - /// match is found. - pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option - where - T: Serialize + Deserialize, - { - let matches = self.filter(f); - if matches.len() != 0 { - Option::some(matches.get(0)) - } else { - Option::none() - } + unconstrained fn clear_oracle(slot: Field) { + ephemeral::clear_oracle(slot) } } -impl EphemeralArray { - /// Deserializes the whole array into a `T`. +impl OracleArray { + /// Returns an empty ephemeral array at the given slot, clearing any pre-existing data. /// - /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the - /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on - /// `EphemeralArray` because deserialization reconstructs a type from raw fields. - pub unconstrained fn read_as(self) -> T - where - T: Deserialize, - { - assert_eq(self.len(), ::N, "EphemeralArray length mismatch for read_as"); - let mut fields: [Field; ::N] = [0; ::N]; - for i in 0..::N { - fields[i] = self.get(i); - } - Deserialize::deserialize(fields) - } -} - -/// Serializes an `EphemeralArray` as its slot identifier, allowing oracle function signatures to use -/// `EphemeralArray` instead of opaque `Field` slots. -impl Serialize for EphemeralArray { - let N: u32 = 1; - - fn serialize(self) -> [Field; Self::N] { - [self.slot] - } - - fn stream_serialize(self, writer: &mut Writer) { - writer.write(self.slot); - } -} - -/// Deserializes a single Field into an `EphemeralArray` handle, treating the field value as the slot identifier. -/// This is the inverse of [`Serialize`]. -impl Deserialize for EphemeralArray { - let N: u32 = 1; - - fn deserialize(fields: [Field; Self::N]) -> Self { - Self { slot: fields[0] } - } - - fn stream_deserialize(reader: &mut Reader) -> Self { - Self { slot: reader.read() } + /// This is deliberately ephemeral-only: a [`TransientArray`](crate::transient::TransientArray) slot may hold data + /// written by another frame of the same contract, so wiping a known slot there stays explicit via + /// `TransientArray::at(slot).clear()`. + pub unconstrained fn empty_at(slot: Field) -> Self { + Self::at(slot).clear() } } diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 51d250108832..507cd51bed80 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -39,6 +39,7 @@ pub mod nullifier; pub mod oracle; pub mod state_vars; pub mod capsules; +pub mod oracle_array; pub mod ephemeral; pub mod transient; pub mod event; diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr new file mode 100644 index 000000000000..572df64f87f3 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr @@ -0,0 +1,265 @@ +use crate::oracle::random::random; +use crate::protocol::traits::{Deserialize, Serialize}; +use crate::protocol::utils::{reader::Reader, writer::Writer}; + +/// Oracle backend for an [`OracleArray`]: the set of PXE-side operations that implement its storage. +/// +/// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the +/// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one +/// contract call frame, while [`TransientArray`](crate::transient::TransientArray) arrays are shared across all frames +/// of the same contract within one top-level PXE call). Implementations are thin wrappers around `#[oracle]` +/// declarations, so method dispatch is resolved at compile time via monomorphization and each array type emits its own +/// foreign calls. +pub trait ArrayOracle { + /// Returns the number of elements in the array at `slot`. + unconstrained fn len_oracle(slot: Field) -> u32; + + /// Appends a serialized element to the array at `slot` and returns the new length. + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32; + + /// Removes and returns the last serialized element of the array at `slot`. + unconstrained fn pop_oracle(slot: Field) -> [Field; N]; + + /// Returns the serialized element at the given index of the array at `slot`. + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N]; + + /// Overwrites the serialized element at the given index of the array at `slot`. + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]); + + /// Removes the element at the given index of the array at `slot`, shifting subsequent elements backward. + unconstrained fn remove_oracle(slot: Field, index: u32); + + /// Removes all elements from the array at `slot`. + unconstrained fn clear_oracle(slot: Field); +} + +/// A dynamically sized array backed by PXE-side in-memory storage via an [`ArrayOracle`] backend. +/// +/// Arrays are identified by a slot, and each logical operation (push, pop, get, etc.) is a single oracle call. The +/// `Oracle` backend determines the array's lifetime and visibility; contracts should not use this type directly but +/// rather one of its aliases: [`EphemeralArray`](crate::ephemeral::EphemeralArray) (scoped to a single contract call +/// frame) or [`TransientArray`](crate::transient::TransientArray) (shared across all frames of the same contract +/// within one top-level PXE call). +pub struct OracleArray { + pub slot: Field, +} + +impl OracleArray +where + Oracle: ArrayOracle, +{ + /// Returns a handle to the array at the given slot, which may already contain data (e.g. populated by an oracle + /// or by an earlier frame, depending on the backend's visibility). + pub unconstrained fn at(slot: Field) -> Self { + Self { slot } + } + + /// Returns an empty array at a fresh, randomly allocated slot. + /// + /// Use this when the caller does not need a specific slot: the random slot is isolated from every other array of + /// the same backend with overwhelming probability. Prefer [`OracleArray::at`] when the slot must be a known value + /// (e.g. one shared with an oracle or another call frame). + pub unconstrained fn empty() -> Self { + Self::at(random()).clear() + } + + /// Returns the number of elements stored in the array. + pub unconstrained fn len(self) -> u32 { + Oracle::len_oracle(self.slot) + } + + /// Stores a value at the end of the array. + pub unconstrained fn push(self, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + let _ = Oracle::push_oracle(self.slot, serialized); + } + + /// Removes and returns the last element. Panics if the array is empty. + pub unconstrained fn pop(self) -> T + where + T: Deserialize, + { + let serialized = Oracle::pop_oracle(self.slot); + Deserialize::deserialize(serialized) + } + + /// Retrieves the value stored at `index`. Panics if the index is out of bounds. + pub unconstrained fn get(self, index: u32) -> T + where + T: Deserialize, + { + let serialized = Oracle::get_oracle(self.slot, index); + Deserialize::deserialize(serialized) + } + + /// Overwrites the value stored at `index`. Panics if the index is out of bounds. + pub unconstrained fn set(self, index: u32, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + Oracle::set_oracle(self.slot, index, serialized); + } + + /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. + pub unconstrained fn remove(self, index: u32) { + Oracle::remove_oracle(self.slot, index); + } + + /// Removes all elements from the array and returns self for chaining (e.g. `OracleArray::at(slot).clear()` to + /// get a guaranteed-empty array at a given slot). + pub unconstrained fn clear(self) -> Self { + Oracle::clear_oracle(self.slot); + self + } + + /// Calls a function on each element of the array. + /// + /// The function `f` is called once with each array value and its corresponding index. Iteration proceeds + /// backwards so that it is safe to remove the current element (and only the current element) inside the + /// callback. + /// + /// It is **not** safe to push new elements from inside the callback. + pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) + where + T: Deserialize, + { + let mut i = self.len(); + while i > 0 { + i -= 1; + f(i, self.get(i)); + } + } + + /// Applies `f` to every element and collects the results into a fresh array. + /// + /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly + /// allocated slot (see [`OracleArray::empty`]). The source array is left unchanged, and the result is isolated + /// from it, so `map` can be chained. + pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> OracleArray + where + T: Deserialize, + U: Serialize, + { + let dest: OracleArray = OracleArray::empty(); + let n = self.len(); + for i in 0..n { + dest.push(f(self.get(i))); + } + dest + } + + /// Collects every element satisfying the predicate `f` into a fresh array. + /// + /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a + /// freshly allocated slot (see [`OracleArray::empty`]). Relative order is preserved. The source array is left + /// unchanged, and the result is isolated from it, so `filter` can be chained. + pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self + where + T: Serialize + Deserialize, + { + let dest: Self = OracleArray::empty(); + let n = self.len(); + for i in 0..n { + let value = self.get(i); + if f(value) { + dest.push(value); + } + } + dest + } + + /// Returns `true` if at least one element satisfies the predicate `f`. + /// + /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in + /// terms of [`OracleArray::filter`]: it keeps the matching elements and checks whether any survived. This is not + /// short-circuiting — every element is tested even after the first match. + pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() != 0 + } + + /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). + /// + /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of + /// [`OracleArray::filter`]: every element matches exactly when filtering keeps all of them. This is not + /// short-circuiting — every element is tested even after the first failure. + pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() == self.len() + } + + /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. + /// + /// Mirrors Rust's `Iterator::find`. Defined in terms of [`OracleArray::filter`], which preserves order, so the + /// first kept element is the first match. This is not short-circuiting — every element is tested even after the + /// match is found. + pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option + where + T: Serialize + Deserialize, + { + let matches = self.filter(f); + if matches.len() != 0 { + Option::some(matches.get(0)) + } else { + Option::none() + } + } +} + +impl OracleArray +where + Oracle: ArrayOracle, +{ + /// Deserializes the whole array into a `T`. + /// + /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the + /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on arrays of + /// `Field` because deserialization reconstructs a type from raw fields. + pub unconstrained fn read_as(self) -> T + where + T: Deserialize, + { + assert_eq(self.len(), ::N, "OracleArray length mismatch for read_as"); + let mut fields: [Field; ::N] = [0; ::N]; + for i in 0..::N { + fields[i] = self.get(i); + } + Deserialize::deserialize(fields) + } +} + +/// Serializes an `OracleArray` as its slot identifier, allowing oracle function signatures to use array types +/// instead of opaque `Field` slots. +impl Serialize for OracleArray { + let N: u32 = 1; + + fn serialize(self) -> [Field; Self::N] { + [self.slot] + } + + fn stream_serialize(self, writer: &mut Writer) { + writer.write(self.slot); + } +} + +/// Deserializes a single Field into an `OracleArray` handle, treating the field value as the slot identifier. +/// This is the inverse of [`Serialize`]. +impl Deserialize for OracleArray { + let N: u32 = 1; + + fn deserialize(fields: [Field; Self::N]) -> Self { + Self { slot: fields[0] } + } + + fn stream_deserialize(reader: &mut Reader) -> Self { + Self { slot: reader.read() } + } +} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index ff70f59e4263..204fb253624c 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,7 +1,6 @@ -use crate::oracle::random::random; use crate::oracle::transient; +use crate::oracle_array::{ArrayOracle, OracleArray}; use crate::protocol::traits::{Deserialize, Serialize}; -use crate::protocol::utils::{reader::Reader, writer::Writer}; /// A dynamically sized array that lives for the duration of a single top-level PXE call. /// @@ -30,218 +29,39 @@ use crate::protocol::utils::{reader::Reader, writer::Writer}; /// Use this to pass not-to-be-persisted data between a contract's own frames (private or utility) within one top-level /// PXE call. For data confined to a single call frame, prefer [`EphemeralArray`](crate::ephemeral::EphemeralArray). /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). -pub struct TransientArray { - pub slot: Field, -} +pub type TransientArray = OracleArray; -impl TransientArray { - /// Returns a handle to a transient array at the given slot, which may already contain data pushed by an earlier - /// frame of the same contract in this top-level PXE call. - pub unconstrained fn at(slot: Field) -> Self { - Self { slot } - } - - /// Returns an empty transient array at a fresh, randomly allocated slot. - /// - /// Use this when the caller does not need a specific slot: the random slot is isolated from every other transient - /// array with overwhelming probability. Prefer [`TransientArray::at`] when the slot must be a known value (e.g. - /// one agreed upon with another frame of the same contract). - pub unconstrained fn empty() -> Self { - Self::at(random()).clear() - } - - /// Returns the number of elements stored in the array. - pub unconstrained fn len(self) -> u32 { - transient::len_oracle(self.slot) - } - - /// Stores a value at the end of the array. - pub unconstrained fn push(self, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - let _ = transient::push_oracle(self.slot, serialized); - } - - /// Removes and returns the last element. Panics if the array is empty. - pub unconstrained fn pop(self) -> T - where - T: Deserialize, - { - let serialized = transient::pop_oracle(self.slot); - Deserialize::deserialize(serialized) - } - - /// Retrieves the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn get(self, index: u32) -> T - where - T: Deserialize, - { - let serialized = transient::get_oracle(self.slot, index); - Deserialize::deserialize(serialized) - } - - /// Overwrites the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn set(self, index: u32, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - transient::set_oracle(self.slot, index, serialized); - } - - /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. - pub unconstrained fn remove(self, index: u32) { - transient::remove_oracle(self.slot, index); - } - - /// Removes all elements from the array and returns self for chaining (e.g. `TransientArray::at(slot).clear()` - /// to get a guaranteed-empty array at a given slot). - pub unconstrained fn clear(self) -> Self { - transient::clear_oracle(self.slot); - self - } - - /// Calls a function on each element of the array. - /// - /// Iteration proceeds backwards so it is safe to remove the current element (and only the current element) inside - /// the callback. It is **not** safe to push new elements from inside the callback. - pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) - where - T: Deserialize, - { - let mut i = self.len(); - while i > 0 { - i -= 1; - f(i, self.get(i)); - } - } - - /// Applies `f` to every element and collects the results into a fresh transient array. - /// - /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly - /// allocated slot (see [`TransientArray::empty`]). The source array is left unchanged, and the result is isolated - /// from it, so `map` can be chained. - pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> TransientArray - where - T: Deserialize, - U: Serialize, - { - let dest: TransientArray = TransientArray::empty(); - let n = self.len(); - for i in 0..n { - dest.push(f(self.get(i))); - } - dest - } - - /// Collects every element satisfying the predicate `f` into a fresh transient array. - /// - /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a - /// freshly allocated slot (see [`TransientArray::empty`]). Relative order is preserved. The source array is left - /// unchanged, and the result is isolated from it, so `filter` can be chained. - pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self - where - T: Serialize + Deserialize, - { - let dest: Self = TransientArray::empty(); - let n = self.len(); - for i in 0..n { - let value = self.get(i); - if f(value) { - dest.push(value); - } - } - dest - } - - /// Returns `true` if at least one element satisfies the predicate `f`. - /// - /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in - /// terms of [`TransientArray::filter`]: it keeps the matching elements and checks whether any survived. This is - /// not short-circuiting — every element is tested even after the first match. - pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool - where - T: Serialize + Deserialize, - { - self.filter(f).len() != 0 - } - - /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). - /// - /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of - /// [`TransientArray::filter`]: every element matches exactly when filtering keeps all of them. This is not - /// short-circuiting — every element is tested even after the first failure. - pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool - where - T: Serialize + Deserialize, - { - self.filter(f).len() == self.len() - } - - /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. - /// - /// Mirrors Rust's `Iterator::find`. Defined in terms of [`TransientArray::filter`], which preserves order, so the - /// first kept element is the first match. This is not short-circuiting — every element is tested even after the - /// match is found. - pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option - where - T: Serialize + Deserialize, - { - let matches = self.filter(f); - if matches.len() != 0 { - Option::some(matches.get(0)) - } else { - Option::none() - } - } -} +/// Routes [`OracleArray`] operations to the transient array oracles, sharing arrays across all call frames of the +/// same contract within one top-level PXE call. +pub struct TransientOracle {} -impl TransientArray { - /// Deserializes the whole array into a `T`. - /// - /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the - /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on - /// `TransientArray` because deserialization reconstructs a type from raw fields. - pub unconstrained fn read_as(self) -> T - where - T: Deserialize, - { - assert_eq(self.len(), ::N, "TransientArray length mismatch for read_as"); - let mut fields: [Field; ::N] = [0; ::N]; - for i in 0..::N { - fields[i] = self.get(i); - } - Deserialize::deserialize(fields) +impl ArrayOracle for TransientOracle { + unconstrained fn len_oracle(slot: Field) -> u32 { + transient::len_oracle(slot) } -} -/// Serializes a `TransientArray` as its slot identifier, allowing oracle function signatures to use `TransientArray` -/// instead of opaque `Field` slots. -impl Serialize for TransientArray { - let N: u32 = 1; + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { + transient::push_oracle(slot, values) + } - fn serialize(self) -> [Field; Self::N] { - [self.slot] + unconstrained fn pop_oracle(slot: Field) -> [Field; N] { + transient::pop_oracle(slot) } - fn stream_serialize(self, writer: &mut Writer) { - writer.write(self.slot); + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { + transient::get_oracle(slot, index) } -} -/// Deserializes a single Field into a `TransientArray` handle, treating the field value as the slot identifier. -/// This is the inverse of [`Serialize`]. -impl Deserialize for TransientArray { - let N: u32 = 1; + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { + transient::set_oracle(slot, index, values) + } - fn deserialize(fields: [Field; Self::N]) -> Self { - Self { slot: fields[0] } + unconstrained fn remove_oracle(slot: Field, index: u32) { + transient::remove_oracle(slot, index) } - fn stream_deserialize(reader: &mut Reader) -> Self { - Self { slot: reader.read() } + unconstrained fn clear_oracle(slot: Field) { + transient::clear_oracle(slot) } } diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap index 0e2fc502906b..c318cf3a0956 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 118 expression: stderr --- error: InvalidNote has a packed length of 9 fields, which exceeds the maximum allowed length of 8 fields. See https://docs.aztec.network/errors/4 @@ -13,8 +14,8 @@ error: InvalidNote has a packed length of 9 fields, which exceeds the maximum al at /noir-projects/aztec-nr/aztec/src/macros/aztec.nr:: 2: do_sync_state at /noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr:: - 3: EphemeralArray::for_each - at /noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr:: + 3: OracleArray::for_each + at /noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr:: 4: do_sync_state at /noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr:: 5: process_message_ciphertext diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap index c44ddd355c2f..3ec1b3682c61 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -833,7 +834,7 @@ pub contract AMM { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap index c7f2843131a6..d2b5233aa173 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -332,7 +333,7 @@ contract AvmGadgetsTest { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap index cdfa8a6786b4..ab5416d3eb75 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -1718,7 +1719,7 @@ pub contract AvmTest { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap index 445edf3cc082..ae15dd5d32cf 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -308,7 +309,7 @@ pub contract PublicFnsWithEmitRepro { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap index 0f8dae5d076b..e26096f956fc 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -267,7 +268,7 @@ contract StorageProofTest { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap index 61239ae02de8..e99dc969d8f8 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap @@ -1,5 +1,6 @@ --- source: tests/snapshots.rs +assertion_line: 159 expression: stdout --- @@ -1043,7 +1044,7 @@ pub contract Token { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::oracle_array::OracleArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { From f75105743ddfc06efef5c3727c3af3c9b03402f4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 10:15:33 +0000 Subject: [PATCH 03/26] refactor: share OracleArray test bodies across backends Each behavior check is written once in oracle_array/test_helpers.nr, generic over the ArrayOracle backend; the ephemeral and transient test modules keep one named #[test] wrapper per behavior so counts, names and CI filters are unchanged. Backend-specific tests (empty_at, store/load/delete) stay with their backend. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 464 ++---------- .../aztec-nr/aztec/src/oracle_array/mod.nr | 2 + .../aztec/src/oracle_array/test_helpers.nr | 659 ++++++++++++++++++ .../aztec-nr/aztec/src/transient/mod.nr | 472 ++----------- 4 files changed, 745 insertions(+), 852 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 86b3e63f36a8..6ae5ca8a0ed6 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -69,265 +69,94 @@ impl OracleArray { } mod test { - use crate::protocol::traits::Serialize; + use crate::oracle_array::test_helpers::SLOT; + use crate::oracle_array::test_helpers as checks; use crate::test::helpers::test_environment::TestEnvironment; - use crate::test::mocks::MockStruct; - use super::EphemeralArray; - - global SLOT: Field = 1230; - global OTHER_SLOT: Field = 5670; + use super::{EphemeralArray, EphemeralOracle}; #[test] unconstrained fn empty_array() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - assert_eq(array.len(), 0); - }); + checks::check_empty_array::(); } #[test(should_fail_with = "out of bounds")] unconstrained fn empty_array_read() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - let _: Field = array.get(0); - }); + checks::check_empty_array_read::(); } #[test(should_fail_with = "is empty")] unconstrained fn empty_array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - let _: Field = array.pop(); - }); + checks::check_empty_array_pop::(); } #[test] unconstrained fn array_push() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); + checks::check_array_push::(); } #[test(should_fail_with = "out of bounds")] unconstrained fn read_past_len() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - - let _ = array.get(1); - }); + checks::check_read_past_len::(); } #[test] unconstrained fn array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.push(10); - - let popped: Field = array.pop(); - assert_eq(popped, 10); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); + checks::check_array_pop::(); } #[test] unconstrained fn array_set() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.set(0, 99); - assert_eq(array.get(0), 99); - }); + checks::check_array_set::(); } #[test] unconstrained fn array_remove_last() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.remove(0); - assert_eq(array.len(), 0); - }); + checks::check_array_remove_last::(); } #[test] unconstrained fn array_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - assert_eq(array.len(), 3); - - array.remove(1); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 7); - assert_eq(array.get(1), 9); - }); + checks::check_array_remove_some::(); } #[test] unconstrained fn array_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - array.remove(1); - array.remove(1); - array.remove(0); - - assert_eq(array.len(), 0); - }); + checks::check_array_remove_all::(); } #[test] unconstrained fn for_each_called_with_all_elements() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); - array.for_each(|index, value| { called_with.push((index, value)); }); - - assert_eq(called_with.len(), 3); - assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); - assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); - assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); - }); + checks::check_for_each_called_with_all_elements::(); } #[test] unconstrained fn for_each_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { - if index == 1 { - array.remove(index); - } - }); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 4); - assert_eq(array.get(1), 6); - }); + checks::check_for_each_remove_some::(); } #[test] unconstrained fn for_each_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { array.remove(index); }); - - assert_eq(array.len(), 0); - }); + checks::check_for_each_remove_all::(); } #[test] unconstrained fn different_slots_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array_a = EphemeralArray::at(SLOT); - let array_b = EphemeralArray::at(OTHER_SLOT); - - array_a.push(10); - array_a.push(20); - array_b.push(99); - - assert_eq(array_a.len(), 2); - assert_eq(array_a.get(0), 10); - assert_eq(array_a.get(1), 20); - - assert_eq(array_b.len(), 1); - assert_eq(array_b.get(0), 99); - }); + checks::check_different_slots_are_isolated::(); } #[test] unconstrained fn works_with_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - - let a = MockStruct::new(5, 6); - let b = MockStruct::new(7, 8); - array.push(a); - array.push(b); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), a); - assert_eq(array.get(1), b); - - let popped: MockStruct = array.pop(); - assert_eq(popped, b); - assert_eq(array.len(), 1); - }); + checks::check_works_with_multi_field_type::(); } #[test] unconstrained fn read_as_reconstructs_serialized_value() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - - let value = MockStruct::new(5, 6); - let serialized = value.serialize(); - for i in 0..serialized.len() { - array.push(serialized[i]); - } - - let reconstructed: MockStruct = array.read_as(); - assert_eq(reconstructed, value); - }); + checks::check_read_as_reconstructs_serialized_value::(); } #[test(should_fail_with = "length mismatch")] unconstrained fn read_as_rejects_length_mismatch() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - // MockStruct deserializes from 2 fields, so a single field is too short. - array.push(1); - - let _: MockStruct = array.read_as(); - }); + checks::check_read_as_rejects_length_mismatch::(); } #[test] @@ -345,317 +174,106 @@ mod test { #[test] unconstrained fn clear_returns_self() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT).clear(); - assert_eq(array.len(), 0); - - array.push(42); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); + checks::check_clear_returns_self::(); } #[test] unconstrained fn clear_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - assert_eq(array.len(), 3); - - // Clear the same slot, previous data should be gone. - let fresh: EphemeralArray = EphemeralArray::at(SLOT).clear(); - assert_eq(fresh.len(), 0); - fresh.push(4); - assert_eq(fresh.get(0), 4); - }); + checks::check_clear_wipes_previous_data::(); } #[test] unconstrained fn empty_allocates_distinct_slots() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let a: EphemeralArray = EphemeralArray::empty(); - let b: EphemeralArray = EphemeralArray::empty(); - - assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); - - a.push(1); - b.push(2); - assert_eq(a.len(), 1); - assert_eq(a.get(0), 1); - assert_eq(b.len(), 1); - assert_eq(b.get(0), 2); - }); + checks::check_empty_allocates_distinct_slots::(); } #[test] unconstrained fn map_transforms_each_element() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - let doubled: EphemeralArray = source.map(|x| x * 2); - - assert_eq(doubled.len(), 3); - assert_eq(doubled.get(0), 2); - assert_eq(doubled.get(1), 4); - assert_eq(doubled.get(2), 6); - - // The source array is left untouched. - assert_eq(source.len(), 3); - assert_eq(source.get(0), 1); - }); + checks::check_map_transforms_each_element::(); } #[test] unconstrained fn map_empty_source_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - let mapped: EphemeralArray = source.map(|x| x * 2); - assert_eq(mapped.len(), 0); - }); + checks::check_map_empty_source_gives_empty_dest::(); } #[test] unconstrained fn map_to_different_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(5); - source.push(7); - - let structs: EphemeralArray = source.map(|x| MockStruct::new(x, x + 1)); - - assert_eq(structs.len(), 2); - assert_eq(structs.get(0), MockStruct::new(5, 6)); - assert_eq(structs.get(1), MockStruct::new(7, 8)); - }); + checks::check_map_to_different_type::(); } #[test] unconstrained fn map_results_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. - let doubled: EphemeralArray = source.map(|x| x * 2); - let tripled: EphemeralArray = source.map(|x| x * 3); - - assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); - assert_eq(doubled.get(0), 2); - assert_eq(doubled.get(2), 6); - assert_eq(tripled.get(0), 3); - assert_eq(tripled.get(2), 9); - }); + checks::check_map_results_are_isolated::(); } #[test] unconstrained fn filter_keeps_matching_elements_in_order() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(1); - source.push(2); - source.push(3); - source.push(2); - source.push(5); - - let kept: EphemeralArray = source.filter(|x| x != 2); - - assert_eq(kept.len(), 3); - assert_eq(kept.get(0), 1); - assert_eq(kept.get(1), 3); - assert_eq(kept.get(2), 5); - - // The source array is left untouched. - assert_eq(source.len(), 5); - assert_eq(source.get(1), 2); - }); + checks::check_filter_keeps_matching_elements_in_order::(); } #[test] unconstrained fn filter_empty_source_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - let kept: EphemeralArray = source.filter(|_| true); - assert_eq(kept.len(), 0); - }); + checks::check_filter_empty_source_gives_empty_dest::(); } #[test] unconstrained fn filter_none_match_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - let kept: EphemeralArray = source.filter(|_| false); - assert_eq(kept.len(), 0); - }); + checks::check_filter_none_match_gives_empty_dest::(); } #[test] unconstrained fn filter_works_with_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(MockStruct::new(1, 10)); - source.push(MockStruct::new(2, 20)); - source.push(MockStruct::new(3, 30)); - - let kept: EphemeralArray = source.filter(|s: MockStruct| s.a != 2); - - assert_eq(kept.len(), 2); - assert_eq(kept.get(0), MockStruct::new(1, 10)); - assert_eq(kept.get(1), MockStruct::new(3, 30)); - }); + checks::check_filter_works_with_multi_field_type::(); } #[test] unconstrained fn filter_results_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: EphemeralArray = EphemeralArray::empty_at(SLOT); - source.push(1); - source.push(2); - source.push(3); - source.push(4); - - // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. - let odds: EphemeralArray = source.filter(|x| (x != 2) & (x != 4)); - let evens: EphemeralArray = source.filter(|x| (x != 1) & (x != 3)); - - assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); - assert_eq(odds.len(), 2); - assert_eq(odds.get(0), 1); - assert_eq(odds.get(1), 3); - assert_eq(evens.len(), 2); - assert_eq(evens.get(0), 2); - assert_eq(evens.get(1), 4); - }); + checks::check_filter_results_are_isolated::(); } #[test] unconstrained fn any_is_true_when_an_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.any(|x| x == 2)); - }); + checks::check_any_is_true_when_an_element_matches::(); } #[test] unconstrained fn any_is_false_when_no_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(!array.any(|x| x == 9)); - }); + checks::check_any_is_false_when_no_element_matches::(); } #[test] unconstrained fn any_on_empty_array_is_false() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - assert(!array.any(|_| true)); - }); + checks::check_any_on_empty_array_is_false::(); } #[test] unconstrained fn all_is_true_when_every_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.all(|x| x != 0)); - }); + checks::check_all_is_true_when_every_element_matches::(); } #[test] unconstrained fn all_is_false_when_one_element_fails() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(!array.all(|x| x != 2)); - }); + checks::check_all_is_false_when_one_element_fails::(); } #[test] unconstrained fn all_on_empty_array_is_true() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - assert(array.all(|_| false)); - }); + checks::check_all_on_empty_array_is_true::(); } #[test] unconstrained fn find_returns_first_matching_element() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - array.push(4); - - // Both 2 and 4 match; find must return the first one in order. - let found = array.find(|x| (x == 2) | (x == 4)); - assert(found.is_some()); - assert_eq(found.unwrap(), 2); - }); + checks::check_find_returns_first_matching_element::(); } #[test] unconstrained fn find_returns_none_when_no_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.find(|x| x == 9).is_none()); - }); + checks::check_find_returns_none_when_no_element_matches::(); } #[test] unconstrained fn find_on_empty_array_is_none() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::empty_at(SLOT); - assert(array.find(|_| true).is_none()); - }); + checks::check_find_on_empty_array_is_none::(); } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr index 572df64f87f3..52173cfcb4d2 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr @@ -1,3 +1,5 @@ +pub(crate) mod test_helpers; + use crate::oracle::random::random; use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr new file mode 100644 index 000000000000..24a53aac0832 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -0,0 +1,659 @@ +//! Shared test suite for [`OracleArray`] backends. +//! +//! Each check exercises one behavior of the shared [`OracleArray`] API and is generic over the [`ArrayOracle`] +//! backend, so the ephemeral and transient test suites instantiate the same checks against their respective oracles +//! via thin `#[test]` wrappers. Tests for backend-specific API (e.g. `empty_at` on ephemeral arrays, or `store` / +//! `load` on transient ones) live with the backend instead. + +use crate::oracle_array::{ArrayOracle, OracleArray}; +use crate::protocol::traits::Serialize; +use crate::test::helpers::test_environment::TestEnvironment; +use crate::test::mocks::MockStruct; + +pub(crate) global SLOT: Field = 1230; +pub(crate) global OTHER_SLOT: Field = 5670; + +pub(crate) unconstrained fn check_empty_array() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn check_empty_array_read() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + let _: Field = array.get(0); + }); +} + +pub(crate) unconstrained fn check_empty_array_pop() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + let _: Field = array.pop(); + }); +} + +pub(crate) unconstrained fn check_array_push() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); +} + +pub(crate) unconstrained fn check_read_past_len() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(5); + + let _ = array.get(1); + }); +} + +pub(crate) unconstrained fn check_array_pop() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(5); + array.push(10); + + let popped: Field = array.pop(); + assert_eq(popped, 10); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); +} + +pub(crate) unconstrained fn check_array_set() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(5); + array.set(0, 99); + assert_eq(array.get(0), 99); + }); +} + +pub(crate) unconstrained fn check_array_remove_last() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(5); + array.remove(0); + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn check_array_remove_some() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + }); +} + +pub(crate) unconstrained fn check_array_remove_all() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn check_for_each_called_with_all_elements() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); + array.for_each(|index, value| { called_with.push((index, value)); }); + + assert_eq(called_with.len(), 3); + assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); + assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); + assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); + }); +} + +pub(crate) unconstrained fn check_for_each_remove_some() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { + if index == 1 { + array.remove(index); + } + }); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 4); + assert_eq(array.get(1), 6); + }); +} + +pub(crate) unconstrained fn check_for_each_remove_all() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { array.remove(index); }); + + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn check_different_slots_are_isolated() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array_a: OracleArray = OracleArray::at(SLOT); + let array_b: OracleArray = OracleArray::at(OTHER_SLOT); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + }); +} + +pub(crate) unconstrained fn check_works_with_multi_field_type() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + let a = MockStruct::new(5, 6); + let b = MockStruct::new(7, 8); + array.push(a); + array.push(b); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), a); + assert_eq(array.get(1), b); + + let popped: MockStruct = array.pop(); + assert_eq(popped, b); + assert_eq(array.len(), 1); + }); +} + +pub(crate) unconstrained fn check_read_as_reconstructs_serialized_value() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + + let value = MockStruct::new(5, 6); + let serialized = value.serialize(); + for i in 0..serialized.len() { + array.push(serialized[i]); + } + + let reconstructed: MockStruct = array.read_as(); + assert_eq(reconstructed, value); + }); +} + +pub(crate) unconstrained fn check_read_as_rejects_length_mismatch() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + // MockStruct deserializes from 2 fields, so a single field is too short. + array.push(1); + + let _: MockStruct = array.read_as(); + }); +} + +pub(crate) unconstrained fn check_clear_returns_self() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT).clear(); + assert_eq(array.len(), 0); + + array.push(42); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 42); + }); +} + +pub(crate) unconstrained fn check_clear_wipes_previous_data() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + assert_eq(array.len(), 3); + + // Clear the same slot, previous data should be gone. + let fresh: OracleArray = OracleArray::at(SLOT).clear(); + assert_eq(fresh.len(), 0); + fresh.push(4); + assert_eq(fresh.get(0), 4); + }); +} + +pub(crate) unconstrained fn check_empty_allocates_distinct_slots() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let a: OracleArray = OracleArray::empty(); + let b: OracleArray = OracleArray::empty(); + + assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); + + a.push(1); + b.push(2); + assert_eq(a.len(), 1); + assert_eq(a.get(0), 1); + assert_eq(b.len(), 1); + assert_eq(b.get(0), 2); + }); +} + +pub(crate) unconstrained fn check_map_transforms_each_element() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let doubled: OracleArray = source.map(|x| x * 2); + + assert_eq(doubled.len(), 3); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(1), 4); + assert_eq(doubled.get(2), 6); + + // The source array is left untouched. + assert_eq(source.len(), 3); + assert_eq(source.get(0), 1); + }); +} + +pub(crate) unconstrained fn check_map_empty_source_gives_empty_dest() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + let mapped: OracleArray = source.map(|x| x * 2); + assert_eq(mapped.len(), 0); + }); +} + +pub(crate) unconstrained fn check_map_to_different_type() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(5); + source.push(7); + + let structs: OracleArray = source.map(|x| MockStruct::new(x, x + 1)); + + assert_eq(structs.len(), 2); + assert_eq(structs.get(0), MockStruct::new(5, 6)); + assert_eq(structs.get(1), MockStruct::new(7, 8)); + }); +} + +pub(crate) unconstrained fn check_map_results_are_isolated() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. + let doubled: OracleArray = source.map(|x| x * 2); + let tripled: OracleArray = source.map(|x| x * 3); + + assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(2), 6); + assert_eq(tripled.get(0), 3); + assert_eq(tripled.get(2), 9); + }); +} + +pub(crate) unconstrained fn check_filter_keeps_matching_elements_in_order() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(2); + source.push(5); + + let kept: OracleArray = source.filter(|x| x != 2); + + assert_eq(kept.len(), 3); + assert_eq(kept.get(0), 1); + assert_eq(kept.get(1), 3); + assert_eq(kept.get(2), 5); + + // The source array is left untouched. + assert_eq(source.len(), 5); + assert_eq(source.get(1), 2); + }); +} + +pub(crate) unconstrained fn check_filter_empty_source_gives_empty_dest() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + let kept: OracleArray = source.filter(|_| true); + assert_eq(kept.len(), 0); + }); +} + +pub(crate) unconstrained fn check_filter_none_match_gives_empty_dest() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let kept: OracleArray = source.filter(|_| false); + assert_eq(kept.len(), 0); + }); +} + +pub(crate) unconstrained fn check_filter_works_with_multi_field_type() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(MockStruct::new(1, 10)); + source.push(MockStruct::new(2, 20)); + source.push(MockStruct::new(3, 30)); + + let kept: OracleArray = source.filter(|s: MockStruct| s.a != 2); + + assert_eq(kept.len(), 2); + assert_eq(kept.get(0), MockStruct::new(1, 10)); + assert_eq(kept.get(1), MockStruct::new(3, 30)); + }); +} + +pub(crate) unconstrained fn check_filter_results_are_isolated() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: OracleArray = OracleArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(4); + + // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. + let odds: OracleArray = source.filter(|x| (x != 2) & (x != 4)); + let evens: OracleArray = source.filter(|x| (x != 1) & (x != 3)); + + assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); + assert_eq(odds.len(), 2); + assert_eq(odds.get(0), 1); + assert_eq(odds.get(1), 3); + assert_eq(evens.len(), 2); + assert_eq(evens.get(0), 2); + assert_eq(evens.get(1), 4); + }); +} + +pub(crate) unconstrained fn check_any_is_true_when_an_element_matches() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.any(|x| x == 2)); + }); +} + +pub(crate) unconstrained fn check_any_is_false_when_no_element_matches() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.any(|x| x == 9)); + }); +} + +pub(crate) unconstrained fn check_any_on_empty_array_is_false() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + assert(!array.any(|_| true)); + }); +} + +pub(crate) unconstrained fn check_all_is_true_when_every_element_matches() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.all(|x| x != 0)); + }); +} + +pub(crate) unconstrained fn check_all_is_false_when_one_element_fails() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.all(|x| x != 2)); + }); +} + +pub(crate) unconstrained fn check_all_on_empty_array_is_true() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + assert(array.all(|_| false)); + }); +} + +pub(crate) unconstrained fn check_find_returns_first_matching_element() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + array.push(4); + + // Both 2 and 4 match; find must return the first one in order. + let found = array.find(|x| (x == 2) | (x == 4)); + assert(found.is_some()); + assert_eq(found.unwrap(), 2); + }); +} + +pub(crate) unconstrained fn check_find_returns_none_when_no_element_matches() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.find(|x| x == 9).is_none()); + }); +} + +pub(crate) unconstrained fn check_find_on_empty_array_is_none() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + assert(array.find(|_| true).is_none()); + }); +} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 204fb253624c..4079ae8652e5 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -93,586 +93,200 @@ pub unconstrained fn delete(slot: Field) { } mod test { - use crate::protocol::traits::Serialize; + use crate::oracle_array::test_helpers::SLOT; + use crate::oracle_array::test_helpers as checks; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; - use super::{delete, load, store, TransientArray}; - - global SLOT: Field = 1230; - global OTHER_SLOT: Field = 5670; + use super::{delete, load, store, TransientArray, TransientOracle}; #[test] unconstrained fn empty_array() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - assert_eq(array.len(), 0); - }); + checks::check_empty_array::(); } #[test(should_fail_with = "out of bounds")] unconstrained fn empty_array_read() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - let _: Field = array.get(0); - }); + checks::check_empty_array_read::(); } #[test(should_fail_with = "is empty")] unconstrained fn empty_array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - let _: Field = array.pop(); - }); + checks::check_empty_array_pop::(); } #[test] unconstrained fn array_push() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); + checks::check_array_push::(); } #[test(should_fail_with = "out of bounds")] unconstrained fn read_past_len() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - - let _ = array.get(1); - }); + checks::check_read_past_len::(); } #[test] - unconstrained fn set_and_remove() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(7); - array.push(8); - array.push(9); - array.set(0, 99); - assert_eq(array.get(0), 99); - array.remove(1); - assert_eq(array.len(), 2); - assert_eq(array.get(0), 99); - assert_eq(array.get(1), 9); - }); + unconstrained fn array_pop() { + checks::check_array_pop::(); } #[test] - unconstrained fn array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - array.push(10); - - let popped: Field = array.pop(); - assert_eq(popped, 10); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); + unconstrained fn array_set() { + checks::check_array_set::(); } #[test] unconstrained fn array_remove_last() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - array.remove(0); - assert_eq(array.len(), 0); - }); + checks::check_array_remove_last::(); } #[test] unconstrained fn array_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - assert_eq(array.len(), 3); - - array.remove(1); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 7); - assert_eq(array.get(1), 9); - }); + checks::check_array_remove_some::(); } #[test] unconstrained fn array_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - array.remove(1); - array.remove(1); - array.remove(0); - - assert_eq(array.len(), 0); - }); + checks::check_array_remove_all::(); } #[test] unconstrained fn for_each_called_with_all_elements() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); - array.for_each(|index, value| { called_with.push((index, value)); }); - - assert_eq(called_with.len(), 3); - assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); - assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); - assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); - }); + checks::check_for_each_called_with_all_elements::(); } #[test] unconstrained fn for_each_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { - if index == 1 { - array.remove(index); - } - }); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 4); - assert_eq(array.get(1), 6); - }); + checks::check_for_each_remove_some::(); } #[test] unconstrained fn for_each_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { array.remove(index); }); - - assert_eq(array.len(), 0); - }); + checks::check_for_each_remove_all::(); } #[test] unconstrained fn different_slots_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array_a = TransientArray::at(SLOT); - let array_b = TransientArray::at(OTHER_SLOT); - - array_a.push(10); - array_a.push(20); - array_b.push(99); - - assert_eq(array_a.len(), 2); - assert_eq(array_a.get(0), 10); - assert_eq(array_a.get(1), 20); - - assert_eq(array_b.len(), 1); - assert_eq(array_b.get(0), 99); - }); + checks::check_different_slots_are_isolated::(); } #[test] unconstrained fn works_with_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - - let a = MockStruct::new(5, 6); - let b = MockStruct::new(7, 8); - array.push(a); - array.push(b); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), a); - assert_eq(array.get(1), b); - - let popped: MockStruct = array.pop(); - assert_eq(popped, b); - assert_eq(array.len(), 1); - }); + checks::check_works_with_multi_field_type::(); } #[test] unconstrained fn read_as_reconstructs_serialized_value() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - - let value = MockStruct::new(5, 6); - let serialized = value.serialize(); - for i in 0..serialized.len() { - array.push(serialized[i]); - } - - let reconstructed: MockStruct = array.read_as(); - assert_eq(reconstructed, value); - }); + checks::check_read_as_reconstructs_serialized_value::(); } #[test(should_fail_with = "length mismatch")] unconstrained fn read_as_rejects_length_mismatch() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - // MockStruct deserializes from 2 fields, so a single field is too short. - array.push(1); - - let _: MockStruct = array.read_as(); - }); + checks::check_read_as_rejects_length_mismatch::(); } #[test] unconstrained fn clear_returns_self() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT).clear(); - assert_eq(array.len(), 0); - - array.push(42); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); + checks::check_clear_returns_self::(); } #[test] unconstrained fn clear_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - assert_eq(array.len(), 3); - - let fresh: TransientArray = TransientArray::at(SLOT).clear(); - assert_eq(fresh.len(), 0); - fresh.push(4); - assert_eq(fresh.get(0), 4); - }); + checks::check_clear_wipes_previous_data::(); } #[test] unconstrained fn empty_allocates_distinct_slots() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let a: TransientArray = TransientArray::empty(); - let b: TransientArray = TransientArray::empty(); - - assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); - - a.push(1); - b.push(2); - assert_eq(a.len(), 1); - assert_eq(a.get(0), 1); - assert_eq(b.len(), 1); - assert_eq(b.get(0), 2); - }); + checks::check_empty_allocates_distinct_slots::(); } #[test] unconstrained fn map_transforms_each_element() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - let doubled: TransientArray = source.map(|x| x * 2); - - assert_eq(doubled.len(), 3); - assert_eq(doubled.get(0), 2); - assert_eq(doubled.get(1), 4); - assert_eq(doubled.get(2), 6); - - // The source array is left untouched. - assert_eq(source.len(), 3); - assert_eq(source.get(0), 1); - }); + checks::check_map_transforms_each_element::(); } #[test] unconstrained fn map_empty_source_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - let mapped: TransientArray = source.map(|x| x * 2); - assert_eq(mapped.len(), 0); - }); + checks::check_map_empty_source_gives_empty_dest::(); } #[test] unconstrained fn map_to_different_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(5); - source.push(7); - - let structs: TransientArray = source.map(|x| MockStruct::new(x, x + 1)); - - assert_eq(structs.len(), 2); - assert_eq(structs.get(0), MockStruct::new(5, 6)); - assert_eq(structs.get(1), MockStruct::new(7, 8)); - }); + checks::check_map_to_different_type::(); } #[test] unconstrained fn map_results_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. - let doubled: TransientArray = source.map(|x| x * 2); - let tripled: TransientArray = source.map(|x| x * 3); - - assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); - assert_eq(doubled.get(0), 2); - assert_eq(doubled.get(2), 6); - assert_eq(tripled.get(0), 3); - assert_eq(tripled.get(2), 9); - }); + checks::check_map_results_are_isolated::(); } #[test] unconstrained fn filter_keeps_matching_elements_in_order() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(1); - source.push(2); - source.push(3); - source.push(2); - source.push(5); - - let kept: TransientArray = source.filter(|x| x != 2); - - assert_eq(kept.len(), 3); - assert_eq(kept.get(0), 1); - assert_eq(kept.get(1), 3); - assert_eq(kept.get(2), 5); - - // The source array is left untouched. - assert_eq(source.len(), 5); - assert_eq(source.get(1), 2); - }); + checks::check_filter_keeps_matching_elements_in_order::(); } #[test] unconstrained fn filter_empty_source_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - let kept: TransientArray = source.filter(|_| true); - assert_eq(kept.len(), 0); - }); + checks::check_filter_empty_source_gives_empty_dest::(); } #[test] unconstrained fn filter_none_match_gives_empty_dest() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(1); - source.push(2); - source.push(3); - - let kept: TransientArray = source.filter(|_| false); - assert_eq(kept.len(), 0); - }); + checks::check_filter_none_match_gives_empty_dest::(); } #[test] unconstrained fn filter_works_with_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(MockStruct::new(1, 10)); - source.push(MockStruct::new(2, 20)); - source.push(MockStruct::new(3, 30)); - - let kept: TransientArray = source.filter(|s: MockStruct| s.a != 2); - - assert_eq(kept.len(), 2); - assert_eq(kept.get(0), MockStruct::new(1, 10)); - assert_eq(kept.get(1), MockStruct::new(3, 30)); - }); + checks::check_filter_works_with_multi_field_type::(); } #[test] unconstrained fn filter_results_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let source: TransientArray = TransientArray::at(SLOT); - source.push(1); - source.push(2); - source.push(3); - source.push(4); - - // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. - let odds: TransientArray = source.filter(|x| (x != 2) & (x != 4)); - let evens: TransientArray = source.filter(|x| (x != 1) & (x != 3)); - - assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); - assert_eq(odds.len(), 2); - assert_eq(odds.get(0), 1); - assert_eq(odds.get(1), 3); - assert_eq(evens.len(), 2); - assert_eq(evens.get(0), 2); - assert_eq(evens.get(1), 4); - }); + checks::check_filter_results_are_isolated::(); } #[test] unconstrained fn any_is_true_when_an_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.any(|x| x == 2)); - }); + checks::check_any_is_true_when_an_element_matches::(); } #[test] unconstrained fn any_is_false_when_no_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(!array.any(|x| x == 9)); - }); + checks::check_any_is_false_when_no_element_matches::(); } #[test] unconstrained fn any_on_empty_array_is_false() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - assert(!array.any(|_| true)); - }); + checks::check_any_on_empty_array_is_false::(); } #[test] unconstrained fn all_is_true_when_every_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.all(|x| x != 0)); - }); + checks::check_all_is_true_when_every_element_matches::(); } #[test] unconstrained fn all_is_false_when_one_element_fails() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(!array.all(|x| x != 2)); - }); + checks::check_all_is_false_when_one_element_fails::(); } #[test] unconstrained fn all_on_empty_array_is_true() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - assert(array.all(|_| false)); - }); + checks::check_all_on_empty_array_is_true::(); } #[test] unconstrained fn find_returns_first_matching_element() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - array.push(4); - - // Both 2 and 4 match; find must return the first one in order. - let found = array.find(|x| (x == 2) | (x == 4)); - assert(found.is_some()); - assert_eq(found.unwrap(), 2); - }); + checks::check_find_returns_first_matching_element::(); } #[test] unconstrained fn find_returns_none_when_no_element_matches() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - - assert(array.find(|x| x == 9).is_none()); - }); + checks::check_find_returns_none_when_no_element_matches::(); } #[test] unconstrained fn find_on_empty_array_is_none() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - assert(array.find(|_| true).is_none()); - }); + checks::check_find_on_empty_array_is_none::(); } #[test] From 0b346bf9156d72eee4f697264ed780438c316307 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 10:24:12 +0000 Subject: [PATCH 04/26] feat: empty_at for transient arrays Moves empty_at from the ephemeral-only impl into the shared OracleArray impl, making it available on TransientArray as well. Its doc notes that for cross-frame backends it wipes data other frames may have written. The empty_at test moves to the shared suite and runs on both backends. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 25 ++----------------- .../aztec-nr/aztec/src/oracle_array/mod.nr | 19 ++++++++++---- .../aztec/src/oracle_array/test_helpers.nr | 15 +++++++++++ .../aztec-nr/aztec/src/transient/mod.nr | 5 ++++ 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 6ae5ca8a0ed6..ad3dcb066d9c 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -57,22 +57,9 @@ impl ArrayOracle for EphemeralOracle { } } -impl OracleArray { - /// Returns an empty ephemeral array at the given slot, clearing any pre-existing data. - /// - /// This is deliberately ephemeral-only: a [`TransientArray`](crate::transient::TransientArray) slot may hold data - /// written by another frame of the same contract, so wiping a known slot there stays explicit via - /// `TransientArray::at(slot).clear()`. - pub unconstrained fn empty_at(slot: Field) -> Self { - Self::at(slot).clear() - } -} - mod test { - use crate::oracle_array::test_helpers::SLOT; use crate::oracle_array::test_helpers as checks; - use crate::test::helpers::test_environment::TestEnvironment; - use super::{EphemeralArray, EphemeralOracle}; + use super::EphemeralOracle; #[test] unconstrained fn empty_array() { @@ -161,15 +148,7 @@ mod test { #[test] unconstrained fn empty_at_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - array.push(1); - assert_eq(array.len(), 1); - - let fresh: EphemeralArray = EphemeralArray::empty_at(SLOT); - assert_eq(fresh.len(), 0); - }); + checks::check_empty_at_wipes_previous_data::(); } #[test] diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr index 52173cfcb4d2..bd1cfe75adc7 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr @@ -56,13 +56,21 @@ where Self { slot } } + /// Returns an empty array at the given slot, clearing any pre-existing data. + /// + /// For backends whose arrays are visible beyond a single call frame (e.g. transient arrays), this wipes data + /// other frames of the same contract may have written at the slot. + pub unconstrained fn empty_at(slot: Field) -> Self { + Self::at(slot).clear() + } + /// Returns an empty array at a fresh, randomly allocated slot. /// /// Use this when the caller does not need a specific slot: the random slot is isolated from every other array of - /// the same backend with overwhelming probability. Prefer [`OracleArray::at`] when the slot must be a known value - /// (e.g. one shared with an oracle or another call frame). + /// the same backend with overwhelming probability. Prefer [`OracleArray::empty_at`] when the slot must be a known + /// value (e.g. one shared with an oracle or another call frame). pub unconstrained fn empty() -> Self { - Self::at(random()).clear() + Self::empty_at(random()) } /// Returns the number of elements stored in the array. @@ -111,8 +119,9 @@ where Oracle::remove_oracle(self.slot, index); } - /// Removes all elements from the array and returns self for chaining (e.g. `OracleArray::at(slot).clear()` to - /// get a guaranteed-empty array at a given slot). + /// Removes all elements from the array and returns self for chaining. + /// + /// Prefer [`OracleArray::empty_at`] when the intent is to start with a fresh array. pub unconstrained fn clear(self) -> Self { Oracle::clear_oracle(self.slot); self diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index 24a53aac0832..e76130764008 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -657,3 +657,18 @@ where assert(array.find(|_| true).is_none()); }); } + +pub(crate) unconstrained fn check_empty_at_wipes_previous_data() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: OracleArray = OracleArray::at(SLOT); + array.push(1); + assert_eq(array.len(), 1); + + let fresh: OracleArray = OracleArray::empty_at(SLOT); + assert_eq(fresh.len(), 0); + }); +} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 4079ae8652e5..c4be81762e8a 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -289,6 +289,11 @@ mod test { checks::check_find_on_empty_array_is_none::(); } + #[test] + unconstrained fn empty_at_wipes_previous_data() { + checks::check_empty_at_wipes_previous_data::(); + } + #[test] unconstrained fn store_and_load() { let env = TestEnvironment::new(); From 0f9f79fe3eac430733a759b7a075a6197ea60a81 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 10:35:23 +0000 Subject: [PATCH 05/26] feat: store/load/delete for ephemeral arrays, symmetric array APIs Generic store/load/delete cores live in oracle_array; ephemeral and transient expose them via thin module-level wrappers, so both backends now have identical public surfaces (alias + oracle marker + store/load/delete). The store/load test bodies move to the shared suite; each backend keeps a store_and_load smoke test through its own wrappers. Also fixes the stale test_helpers doc that still described empty_at and store/load as backend-specific. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 65 +++++++++++++- .../aztec-nr/aztec/src/oracle_array/mod.nr | 34 ++++++++ .../aztec/src/oracle_array/test_helpers.nr | 84 +++++++++++++++++-- .../aztec-nr/aztec/src/transient/mod.nr | 58 +++---------- 4 files changed, 187 insertions(+), 54 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index ad3dcb066d9c..006ad55d2240 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,5 +1,6 @@ use crate::oracle::ephemeral; use crate::oracle_array::{ArrayOracle, OracleArray}; +use crate::protocol::traits::{Deserialize, Serialize}; /// A dynamically sized array that exists only during a single contract call frame. /// @@ -57,9 +58,32 @@ impl ArrayOracle for EphemeralOracle { } } +/// Stores a single value at `slot`, overwriting any value previously stored there. +pub unconstrained fn store(slot: Field, value: T) +where + T: Serialize, +{ + crate::oracle_array::store::(slot, value) +} + +/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. +pub unconstrained fn load(slot: Field) -> Option +where + T: Deserialize, +{ + crate::oracle_array::load::(slot) +} + +/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. +pub unconstrained fn delete(slot: Field) { + crate::oracle_array::delete::(slot) +} + mod test { + use crate::oracle_array::test_helpers::SLOT; use crate::oracle_array::test_helpers as checks; - use super::EphemeralOracle; + use crate::test::helpers::test_environment::TestEnvironment; + use super::{EphemeralOracle, load, store}; #[test] unconstrained fn empty_array() { @@ -255,4 +279,43 @@ mod test { unconstrained fn find_on_empty_array_is_none() { checks::check_find_on_empty_array_is_none::(); } + + #[test] + unconstrained fn store_and_load() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + store(SLOT, 42); + assert_eq(load(SLOT), Option::some(42)); + }); + } + + #[test] + unconstrained fn load_empty_returns_none() { + checks::check_load_empty_returns_none::(); + } + + #[test] + unconstrained fn store_overwrites() { + checks::check_store_overwrites::(); + } + + #[test] + unconstrained fn delete_removes_value() { + checks::check_delete_removes_value::(); + } + + #[test] + unconstrained fn delete_empty_is_noop() { + checks::check_delete_empty_is_noop::(); + } + + #[test] + unconstrained fn store_and_load_multi_field_type() { + checks::check_store_and_load_multi_field_type::(); + } + + #[test] + unconstrained fn stored_value_is_visible_as_a_length_one_array() { + checks::check_stored_value_is_visible_as_a_length_one_array::(); + } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr index bd1cfe75adc7..39851bbcbe83 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr @@ -274,3 +274,37 @@ impl Deserialize for OracleArray { Self { slot: reader.read() } } } + +/// Stores a single value at `slot`, overwriting any value previously stored there. +/// +/// The value is stored as a length-one array at the slot: read it back with [`load`] and remove it with [`delete`]. +pub(crate) unconstrained fn store(slot: Field, value: T) +where + T: Serialize, + Oracle: ArrayOracle, +{ + let array: OracleArray = OracleArray::at(slot); + array.clear().push(value); +} + +/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. +pub(crate) unconstrained fn load(slot: Field) -> Option +where + T: Deserialize, + Oracle: ArrayOracle, +{ + let array: OracleArray = OracleArray::at(slot); + if array.len() == 0 { + Option::none() + } else { + Option::some(array.get(0)) + } +} + +/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. +pub(crate) unconstrained fn delete(slot: Field) +where + Oracle: ArrayOracle, +{ + Oracle::clear_oracle(slot) +} diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index e76130764008..be1c9536c7c9 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -1,11 +1,12 @@ //! Shared test suite for [`OracleArray`] backends. //! -//! Each check exercises one behavior of the shared [`OracleArray`] API and is generic over the [`ArrayOracle`] -//! backend, so the ephemeral and transient test suites instantiate the same checks against their respective oracles -//! via thin `#[test]` wrappers. Tests for backend-specific API (e.g. `empty_at` on ephemeral arrays, or `store` / -//! `load` on transient ones) live with the backend instead. +//! Each check exercises one behavior of the shared [`OracleArray`] API (or of the generic `store` / `load` / `delete` +//! helpers) and is generic over the [`ArrayOracle`] backend, so the ephemeral and transient test suites instantiate +//! the same checks against their respective oracles via thin `#[test]` wrappers. Each backend module additionally +//! keeps a smoke test that routes through its own public helpers (e.g. `transient::store` / `transient::load`) to +//! cover the delegation wiring. -use crate::oracle_array::{ArrayOracle, OracleArray}; +use crate::oracle_array::{ArrayOracle, delete, load, OracleArray, store}; use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; @@ -672,3 +673,76 @@ where assert_eq(fresh.len(), 0); }); } + +pub(crate) unconstrained fn check_load_empty_returns_none() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: Option = load::(SLOT); + assert_eq(value, Option::none()); + }); +} + +pub(crate) unconstrained fn check_store_overwrites() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + store::(SLOT, 1); + store::(SLOT, 2); + assert_eq(load::(SLOT), Option::some(2)); + }); +} + +pub(crate) unconstrained fn check_delete_removes_value() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + store::(SLOT, 42); + delete::(SLOT); + let value: Option = load::(SLOT); + assert_eq(value, Option::none()); + }); +} + +pub(crate) unconstrained fn check_delete_empty_is_noop() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + delete::(SLOT); + let value: Option = load::(SLOT); + assert_eq(value, Option::none()); + }); +} + +pub(crate) unconstrained fn check_store_and_load_multi_field_type() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value = MockStruct::new(5, 6); + store::(SLOT, value); + assert_eq(load::(SLOT), Option::some(value)); + }); +} + +pub(crate) unconstrained fn check_stored_value_is_visible_as_a_length_one_array() +where + Oracle: ArrayOracle, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + store::(SLOT, 42); + let array: OracleArray = OracleArray::at(SLOT); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 42); + }); +} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index c4be81762e8a..ad30325b2518 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -70,8 +70,7 @@ pub unconstrained fn store(slot: Field, value: T) where T: Serialize, { - let array: TransientArray = TransientArray::at(slot); - array.clear().push(value); + crate::oracle_array::store::(slot, value) } /// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. @@ -79,25 +78,19 @@ pub unconstrained fn load(slot: Field) -> Option where T: Deserialize, { - let array: TransientArray = TransientArray::at(slot); - if array.len() == 0 { - Option::none() - } else { - Option::some(array.get(0)) - } + crate::oracle_array::load::(slot) } /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub unconstrained fn delete(slot: Field) { - transient::clear_oracle(slot); + crate::oracle_array::delete::(slot) } mod test { use crate::oracle_array::test_helpers::SLOT; use crate::oracle_array::test_helpers as checks; use crate::test::helpers::test_environment::TestEnvironment; - use crate::test::mocks::MockStruct; - use super::{delete, load, store, TransientArray, TransientOracle}; + use super::{load, store, TransientOracle}; #[test] unconstrained fn empty_array() { @@ -305,62 +298,31 @@ mod test { #[test] unconstrained fn load_empty_returns_none() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let value: Option = load(SLOT); - assert_eq(value, Option::none()); - }); + checks::check_load_empty_returns_none::(); } #[test] unconstrained fn store_overwrites() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - store(SLOT, 1); - store(SLOT, 2); - assert_eq(load(SLOT), Option::some(2)); - }); + checks::check_store_overwrites::(); } #[test] unconstrained fn delete_removes_value() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - store(SLOT, 42); - delete(SLOT); - let value: Option = load(SLOT); - assert_eq(value, Option::none()); - }); + checks::check_delete_removes_value::(); } #[test] unconstrained fn delete_empty_is_noop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - delete(SLOT); - let value: Option = load(SLOT); - assert_eq(value, Option::none()); - }); + checks::check_delete_empty_is_noop::(); } #[test] unconstrained fn store_and_load_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let value = MockStruct::new(5, 6); - store(SLOT, value); - assert_eq(load(SLOT), Option::some(value)); - }); + checks::check_store_and_load_multi_field_type::(); } #[test] unconstrained fn stored_value_is_visible_as_a_length_one_array() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - store(SLOT, 42); - let array: TransientArray = TransientArray::at(SLOT); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); + checks::check_stored_value_is_visible_as_a_length_one_array::(); } } From c4857f976badc5a4079268687b932785a3fe6d6d Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 11:01:07 +0000 Subject: [PATCH 06/26] refactor: generate per-backend OracleArray tests with a comptime macro Backend test modules no longer list one wrapper per shared check. Annotating a test module with ephemeral_oracle_array_tests / transient_oracle_array_tests reflects over test_helpers and emits one #[test] per unconstrained check with that backend's oracle swapped in, so new checks automatically run against every backend and the suites cannot drift. should_fail_with messages come from a single manifest in test_helpers; an unregistered failing check fails loudly rather than silently. Generated test names match the previous hand-written ones. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 229 +----------------- .../aztec/src/oracle_array/test_helpers.nr | 154 ++++++++---- .../aztec-nr/aztec/src/transient/mod.nr | 229 +----------------- 3 files changed, 108 insertions(+), 504 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 006ad55d2240..e444f29198ea 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -79,206 +79,11 @@ pub unconstrained fn delete(slot: Field) { crate::oracle_array::delete::(slot) } +#[crate::oracle_array::test_helpers::ephemeral_oracle_array_tests] mod test { use crate::oracle_array::test_helpers::SLOT; - use crate::oracle_array::test_helpers as checks; use crate::test::helpers::test_environment::TestEnvironment; - use super::{EphemeralOracle, load, store}; - - #[test] - unconstrained fn empty_array() { - checks::check_empty_array::(); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn empty_array_read() { - checks::check_empty_array_read::(); - } - - #[test(should_fail_with = "is empty")] - unconstrained fn empty_array_pop() { - checks::check_empty_array_pop::(); - } - - #[test] - unconstrained fn array_push() { - checks::check_array_push::(); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn read_past_len() { - checks::check_read_past_len::(); - } - - #[test] - unconstrained fn array_pop() { - checks::check_array_pop::(); - } - - #[test] - unconstrained fn array_set() { - checks::check_array_set::(); - } - - #[test] - unconstrained fn array_remove_last() { - checks::check_array_remove_last::(); - } - - #[test] - unconstrained fn array_remove_some() { - checks::check_array_remove_some::(); - } - - #[test] - unconstrained fn array_remove_all() { - checks::check_array_remove_all::(); - } - - #[test] - unconstrained fn for_each_called_with_all_elements() { - checks::check_for_each_called_with_all_elements::(); - } - - #[test] - unconstrained fn for_each_remove_some() { - checks::check_for_each_remove_some::(); - } - - #[test] - unconstrained fn for_each_remove_all() { - checks::check_for_each_remove_all::(); - } - - #[test] - unconstrained fn different_slots_are_isolated() { - checks::check_different_slots_are_isolated::(); - } - - #[test] - unconstrained fn works_with_multi_field_type() { - checks::check_works_with_multi_field_type::(); - } - - #[test] - unconstrained fn read_as_reconstructs_serialized_value() { - checks::check_read_as_reconstructs_serialized_value::(); - } - - #[test(should_fail_with = "length mismatch")] - unconstrained fn read_as_rejects_length_mismatch() { - checks::check_read_as_rejects_length_mismatch::(); - } - - #[test] - unconstrained fn empty_at_wipes_previous_data() { - checks::check_empty_at_wipes_previous_data::(); - } - - #[test] - unconstrained fn clear_returns_self() { - checks::check_clear_returns_self::(); - } - - #[test] - unconstrained fn clear_wipes_previous_data() { - checks::check_clear_wipes_previous_data::(); - } - - #[test] - unconstrained fn empty_allocates_distinct_slots() { - checks::check_empty_allocates_distinct_slots::(); - } - - #[test] - unconstrained fn map_transforms_each_element() { - checks::check_map_transforms_each_element::(); - } - - #[test] - unconstrained fn map_empty_source_gives_empty_dest() { - checks::check_map_empty_source_gives_empty_dest::(); - } - - #[test] - unconstrained fn map_to_different_type() { - checks::check_map_to_different_type::(); - } - - #[test] - unconstrained fn map_results_are_isolated() { - checks::check_map_results_are_isolated::(); - } - - #[test] - unconstrained fn filter_keeps_matching_elements_in_order() { - checks::check_filter_keeps_matching_elements_in_order::(); - } - - #[test] - unconstrained fn filter_empty_source_gives_empty_dest() { - checks::check_filter_empty_source_gives_empty_dest::(); - } - - #[test] - unconstrained fn filter_none_match_gives_empty_dest() { - checks::check_filter_none_match_gives_empty_dest::(); - } - - #[test] - unconstrained fn filter_works_with_multi_field_type() { - checks::check_filter_works_with_multi_field_type::(); - } - - #[test] - unconstrained fn filter_results_are_isolated() { - checks::check_filter_results_are_isolated::(); - } - - #[test] - unconstrained fn any_is_true_when_an_element_matches() { - checks::check_any_is_true_when_an_element_matches::(); - } - - #[test] - unconstrained fn any_is_false_when_no_element_matches() { - checks::check_any_is_false_when_no_element_matches::(); - } - - #[test] - unconstrained fn any_on_empty_array_is_false() { - checks::check_any_on_empty_array_is_false::(); - } - - #[test] - unconstrained fn all_is_true_when_every_element_matches() { - checks::check_all_is_true_when_every_element_matches::(); - } - - #[test] - unconstrained fn all_is_false_when_one_element_fails() { - checks::check_all_is_false_when_one_element_fails::(); - } - - #[test] - unconstrained fn all_on_empty_array_is_true() { - checks::check_all_on_empty_array_is_true::(); - } - - #[test] - unconstrained fn find_returns_first_matching_element() { - checks::check_find_returns_first_matching_element::(); - } - - #[test] - unconstrained fn find_returns_none_when_no_element_matches() { - checks::check_find_returns_none_when_no_element_matches::(); - } - - #[test] - unconstrained fn find_on_empty_array_is_none() { - checks::check_find_on_empty_array_is_none::(); - } + use super::{load, store}; #[test] unconstrained fn store_and_load() { @@ -288,34 +93,4 @@ mod test { assert_eq(load(SLOT), Option::some(42)); }); } - - #[test] - unconstrained fn load_empty_returns_none() { - checks::check_load_empty_returns_none::(); - } - - #[test] - unconstrained fn store_overwrites() { - checks::check_store_overwrites::(); - } - - #[test] - unconstrained fn delete_removes_value() { - checks::check_delete_removes_value::(); - } - - #[test] - unconstrained fn delete_empty_is_noop() { - checks::check_delete_empty_is_noop::(); - } - - #[test] - unconstrained fn store_and_load_multi_field_type() { - checks::check_store_and_load_multi_field_type::(); - } - - #[test] - unconstrained fn stored_value_is_visible_as_a_length_one_array() { - checks::check_stored_value_is_visible_as_a_length_one_array::(); - } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index be1c9536c7c9..6e36a38dc0de 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -1,10 +1,13 @@ //! Shared test suite for [`OracleArray`] backends. //! -//! Each check exercises one behavior of the shared [`OracleArray`] API (or of the generic `store` / `load` / `delete` -//! helpers) and is generic over the [`ArrayOracle`] backend, so the ephemeral and transient test suites instantiate -//! the same checks against their respective oracles via thin `#[test]` wrappers. Each backend module additionally -//! keeps a smoke test that routes through its own public helpers (e.g. `transient::store` / `transient::load`) to -//! cover the delegation wiring. +//! Every `unconstrained` function in this module checks one behavior of the shared [`OracleArray`] API (or of the +//! generic `store` / `load` / `delete` helpers) and is generic over the [`ArrayOracle`] backend. Backend test modules +//! do not reference the checks one by one: annotating a module with `ephemeral_oracle_array_tests` / +//! `transient_oracle_array_tests` generates one `#[test]` per check with that backend's oracle swapped in, so a check +//! added here automatically runs against every backend. Checks that are expected to fail must be registered in +//! `should_fail_message`, which supplies the message for the generated `#[test(should_fail_with = ...)]` attribute. +//! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. +//! `transient::store` / `transient::load`) to cover the delegation wiring. use crate::oracle_array::{ArrayOracle, delete, load, OracleArray, store}; use crate::protocol::traits::Serialize; @@ -14,7 +17,7 @@ use crate::test::mocks::MockStruct; pub(crate) global SLOT: Field = 1230; pub(crate) global OTHER_SLOT: Field = 5670; -pub(crate) unconstrained fn check_empty_array() +pub(crate) unconstrained fn empty_array() where Oracle: ArrayOracle, { @@ -25,7 +28,7 @@ where }); } -pub(crate) unconstrained fn check_empty_array_read() +pub(crate) unconstrained fn empty_array_read() where Oracle: ArrayOracle, { @@ -36,7 +39,7 @@ where }); } -pub(crate) unconstrained fn check_empty_array_pop() +pub(crate) unconstrained fn empty_array_pop() where Oracle: ArrayOracle, { @@ -47,7 +50,7 @@ where }); } -pub(crate) unconstrained fn check_array_push() +pub(crate) unconstrained fn array_push() where Oracle: ArrayOracle, { @@ -61,7 +64,7 @@ where }); } -pub(crate) unconstrained fn check_read_past_len() +pub(crate) unconstrained fn read_past_len() where Oracle: ArrayOracle, { @@ -74,7 +77,7 @@ where }); } -pub(crate) unconstrained fn check_array_pop() +pub(crate) unconstrained fn array_pop() where Oracle: ArrayOracle, { @@ -91,7 +94,7 @@ where }); } -pub(crate) unconstrained fn check_array_set() +pub(crate) unconstrained fn array_set() where Oracle: ArrayOracle, { @@ -104,7 +107,7 @@ where }); } -pub(crate) unconstrained fn check_array_remove_last() +pub(crate) unconstrained fn array_remove_last() where Oracle: ArrayOracle, { @@ -117,7 +120,7 @@ where }); } -pub(crate) unconstrained fn check_array_remove_some() +pub(crate) unconstrained fn array_remove_some() where Oracle: ArrayOracle, { @@ -139,7 +142,7 @@ where }); } -pub(crate) unconstrained fn check_array_remove_all() +pub(crate) unconstrained fn array_remove_all() where Oracle: ArrayOracle, { @@ -159,7 +162,7 @@ where }); } -pub(crate) unconstrained fn check_for_each_called_with_all_elements() +pub(crate) unconstrained fn for_each_called_with_all_elements() where Oracle: ArrayOracle, { @@ -181,7 +184,7 @@ where }); } -pub(crate) unconstrained fn check_for_each_remove_some() +pub(crate) unconstrained fn for_each_remove_some() where Oracle: ArrayOracle, { @@ -205,7 +208,7 @@ where }); } -pub(crate) unconstrained fn check_for_each_remove_all() +pub(crate) unconstrained fn for_each_remove_all() where Oracle: ArrayOracle, { @@ -223,7 +226,7 @@ where }); } -pub(crate) unconstrained fn check_different_slots_are_isolated() +pub(crate) unconstrained fn different_slots_are_isolated() where Oracle: ArrayOracle, { @@ -245,7 +248,7 @@ where }); } -pub(crate) unconstrained fn check_works_with_multi_field_type() +pub(crate) unconstrained fn works_with_multi_field_type() where Oracle: ArrayOracle, { @@ -268,7 +271,7 @@ where }); } -pub(crate) unconstrained fn check_read_as_reconstructs_serialized_value() +pub(crate) unconstrained fn read_as_reconstructs_serialized_value() where Oracle: ArrayOracle, { @@ -287,7 +290,7 @@ where }); } -pub(crate) unconstrained fn check_read_as_rejects_length_mismatch() +pub(crate) unconstrained fn read_as_rejects_length_mismatch() where Oracle: ArrayOracle, { @@ -301,7 +304,7 @@ where }); } -pub(crate) unconstrained fn check_clear_returns_self() +pub(crate) unconstrained fn clear_returns_self() where Oracle: ArrayOracle, { @@ -316,7 +319,7 @@ where }); } -pub(crate) unconstrained fn check_clear_wipes_previous_data() +pub(crate) unconstrained fn clear_wipes_previous_data() where Oracle: ArrayOracle, { @@ -336,7 +339,7 @@ where }); } -pub(crate) unconstrained fn check_empty_allocates_distinct_slots() +pub(crate) unconstrained fn empty_allocates_distinct_slots() where Oracle: ArrayOracle, { @@ -356,7 +359,7 @@ where }); } -pub(crate) unconstrained fn check_map_transforms_each_element() +pub(crate) unconstrained fn map_transforms_each_element() where Oracle: ArrayOracle, { @@ -380,7 +383,7 @@ where }); } -pub(crate) unconstrained fn check_map_empty_source_gives_empty_dest() +pub(crate) unconstrained fn map_empty_source_gives_empty_dest() where Oracle: ArrayOracle, { @@ -392,7 +395,7 @@ where }); } -pub(crate) unconstrained fn check_map_to_different_type() +pub(crate) unconstrained fn map_to_different_type() where Oracle: ArrayOracle, { @@ -410,7 +413,7 @@ where }); } -pub(crate) unconstrained fn check_map_results_are_isolated() +pub(crate) unconstrained fn map_results_are_isolated() where Oracle: ArrayOracle, { @@ -433,7 +436,7 @@ where }); } -pub(crate) unconstrained fn check_filter_keeps_matching_elements_in_order() +pub(crate) unconstrained fn filter_keeps_matching_elements_in_order() where Oracle: ArrayOracle, { @@ -459,7 +462,7 @@ where }); } -pub(crate) unconstrained fn check_filter_empty_source_gives_empty_dest() +pub(crate) unconstrained fn filter_empty_source_gives_empty_dest() where Oracle: ArrayOracle, { @@ -471,7 +474,7 @@ where }); } -pub(crate) unconstrained fn check_filter_none_match_gives_empty_dest() +pub(crate) unconstrained fn filter_none_match_gives_empty_dest() where Oracle: ArrayOracle, { @@ -487,7 +490,7 @@ where }); } -pub(crate) unconstrained fn check_filter_works_with_multi_field_type() +pub(crate) unconstrained fn filter_works_with_multi_field_type() where Oracle: ArrayOracle, { @@ -506,7 +509,7 @@ where }); } -pub(crate) unconstrained fn check_filter_results_are_isolated() +pub(crate) unconstrained fn filter_results_are_isolated() where Oracle: ArrayOracle, { @@ -532,7 +535,7 @@ where }); } -pub(crate) unconstrained fn check_any_is_true_when_an_element_matches() +pub(crate) unconstrained fn any_is_true_when_an_element_matches() where Oracle: ArrayOracle, { @@ -547,7 +550,7 @@ where }); } -pub(crate) unconstrained fn check_any_is_false_when_no_element_matches() +pub(crate) unconstrained fn any_is_false_when_no_element_matches() where Oracle: ArrayOracle, { @@ -562,7 +565,7 @@ where }); } -pub(crate) unconstrained fn check_any_on_empty_array_is_false() +pub(crate) unconstrained fn any_on_empty_array_is_false() where Oracle: ArrayOracle, { @@ -573,7 +576,7 @@ where }); } -pub(crate) unconstrained fn check_all_is_true_when_every_element_matches() +pub(crate) unconstrained fn all_is_true_when_every_element_matches() where Oracle: ArrayOracle, { @@ -588,7 +591,7 @@ where }); } -pub(crate) unconstrained fn check_all_is_false_when_one_element_fails() +pub(crate) unconstrained fn all_is_false_when_one_element_fails() where Oracle: ArrayOracle, { @@ -603,7 +606,7 @@ where }); } -pub(crate) unconstrained fn check_all_on_empty_array_is_true() +pub(crate) unconstrained fn all_on_empty_array_is_true() where Oracle: ArrayOracle, { @@ -614,7 +617,7 @@ where }); } -pub(crate) unconstrained fn check_find_returns_first_matching_element() +pub(crate) unconstrained fn find_returns_first_matching_element() where Oracle: ArrayOracle, { @@ -633,7 +636,7 @@ where }); } -pub(crate) unconstrained fn check_find_returns_none_when_no_element_matches() +pub(crate) unconstrained fn find_returns_none_when_no_element_matches() where Oracle: ArrayOracle, { @@ -648,7 +651,7 @@ where }); } -pub(crate) unconstrained fn check_find_on_empty_array_is_none() +pub(crate) unconstrained fn find_on_empty_array_is_none() where Oracle: ArrayOracle, { @@ -659,7 +662,7 @@ where }); } -pub(crate) unconstrained fn check_empty_at_wipes_previous_data() +pub(crate) unconstrained fn empty_at_wipes_previous_data() where Oracle: ArrayOracle, { @@ -674,7 +677,7 @@ where }); } -pub(crate) unconstrained fn check_load_empty_returns_none() +pub(crate) unconstrained fn load_empty_returns_none() where Oracle: ArrayOracle, { @@ -685,7 +688,7 @@ where }); } -pub(crate) unconstrained fn check_store_overwrites() +pub(crate) unconstrained fn store_overwrites() where Oracle: ArrayOracle, { @@ -697,7 +700,7 @@ where }); } -pub(crate) unconstrained fn check_delete_removes_value() +pub(crate) unconstrained fn delete_removes_value() where Oracle: ArrayOracle, { @@ -710,7 +713,7 @@ where }); } -pub(crate) unconstrained fn check_delete_empty_is_noop() +pub(crate) unconstrained fn delete_empty_is_noop() where Oracle: ArrayOracle, { @@ -722,7 +725,7 @@ where }); } -pub(crate) unconstrained fn check_store_and_load_multi_field_type() +pub(crate) unconstrained fn store_and_load_multi_field_type() where Oracle: ArrayOracle, { @@ -734,7 +737,7 @@ where }); } -pub(crate) unconstrained fn check_stored_value_is_visible_as_a_length_one_array() +pub(crate) unconstrained fn stored_value_is_visible_as_a_length_one_array() where Oracle: ArrayOracle, { @@ -746,3 +749,54 @@ where assert_eq(array.get(0), 42); }); } + +/// Returns the `should_fail_with` message for checks that are expected to fail, or `Option::none()` for checks that +/// are expected to pass. +comptime fn should_fail_message(name: Quoted) -> Option { + if name == quote { empty_array_read } { + Option::some(quote { "out of bounds" }) + } else if name == quote { empty_array_pop } { + Option::some(quote { "is empty" }) + } else if name == quote { read_past_len } { + Option::some(quote { "out of bounds" }) + } else if name == quote { read_as_rejects_length_mismatch } { + Option::some(quote { "length mismatch" }) + } else { + Option::none() + } +} + +comptime fn oracle_array_tests(oracle: Quoted) -> Quoted { + let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap(); + let mut tests = quote {}; + for f in helpers.functions() { + if f.is_unconstrained() { + let name = f.name(); + let fail = should_fail_message(name); + let attr = if fail.is_some() { + let msg = fail.unwrap(); + quote { #[test(should_fail_with = $msg)] } + } else { + quote { #[test] } + }; + tests = quote { + $tests + $attr + unconstrained fn $name() { + crate::oracle_array::test_helpers::$name::<$oracle>(); + } + }; + } + } + tests +} + +/// Generates the shared [`OracleArray`] test suite against the ephemeral backend. Apply to a test module. +pub(crate) comptime fn ephemeral_oracle_array_tests(_m: Module) -> Quoted { + oracle_array_tests(quote { crate::ephemeral::EphemeralOracle }) +} + +/// Generates the shared [`OracleArray`] test suite against the transient backend. Apply to a test module. +pub(crate) comptime fn transient_oracle_array_tests(_m: Module) -> Quoted { + oracle_array_tests(quote { crate::transient::TransientOracle }) +} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index ad30325b2518..0f1c807b6c2c 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -86,206 +86,11 @@ pub unconstrained fn delete(slot: Field) { crate::oracle_array::delete::(slot) } +#[crate::oracle_array::test_helpers::transient_oracle_array_tests] mod test { use crate::oracle_array::test_helpers::SLOT; - use crate::oracle_array::test_helpers as checks; use crate::test::helpers::test_environment::TestEnvironment; - use super::{load, store, TransientOracle}; - - #[test] - unconstrained fn empty_array() { - checks::check_empty_array::(); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn empty_array_read() { - checks::check_empty_array_read::(); - } - - #[test(should_fail_with = "is empty")] - unconstrained fn empty_array_pop() { - checks::check_empty_array_pop::(); - } - - #[test] - unconstrained fn array_push() { - checks::check_array_push::(); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn read_past_len() { - checks::check_read_past_len::(); - } - - #[test] - unconstrained fn array_pop() { - checks::check_array_pop::(); - } - - #[test] - unconstrained fn array_set() { - checks::check_array_set::(); - } - - #[test] - unconstrained fn array_remove_last() { - checks::check_array_remove_last::(); - } - - #[test] - unconstrained fn array_remove_some() { - checks::check_array_remove_some::(); - } - - #[test] - unconstrained fn array_remove_all() { - checks::check_array_remove_all::(); - } - - #[test] - unconstrained fn for_each_called_with_all_elements() { - checks::check_for_each_called_with_all_elements::(); - } - - #[test] - unconstrained fn for_each_remove_some() { - checks::check_for_each_remove_some::(); - } - - #[test] - unconstrained fn for_each_remove_all() { - checks::check_for_each_remove_all::(); - } - - #[test] - unconstrained fn different_slots_are_isolated() { - checks::check_different_slots_are_isolated::(); - } - - #[test] - unconstrained fn works_with_multi_field_type() { - checks::check_works_with_multi_field_type::(); - } - - #[test] - unconstrained fn read_as_reconstructs_serialized_value() { - checks::check_read_as_reconstructs_serialized_value::(); - } - - #[test(should_fail_with = "length mismatch")] - unconstrained fn read_as_rejects_length_mismatch() { - checks::check_read_as_rejects_length_mismatch::(); - } - - #[test] - unconstrained fn clear_returns_self() { - checks::check_clear_returns_self::(); - } - - #[test] - unconstrained fn clear_wipes_previous_data() { - checks::check_clear_wipes_previous_data::(); - } - - #[test] - unconstrained fn empty_allocates_distinct_slots() { - checks::check_empty_allocates_distinct_slots::(); - } - - #[test] - unconstrained fn map_transforms_each_element() { - checks::check_map_transforms_each_element::(); - } - - #[test] - unconstrained fn map_empty_source_gives_empty_dest() { - checks::check_map_empty_source_gives_empty_dest::(); - } - - #[test] - unconstrained fn map_to_different_type() { - checks::check_map_to_different_type::(); - } - - #[test] - unconstrained fn map_results_are_isolated() { - checks::check_map_results_are_isolated::(); - } - - #[test] - unconstrained fn filter_keeps_matching_elements_in_order() { - checks::check_filter_keeps_matching_elements_in_order::(); - } - - #[test] - unconstrained fn filter_empty_source_gives_empty_dest() { - checks::check_filter_empty_source_gives_empty_dest::(); - } - - #[test] - unconstrained fn filter_none_match_gives_empty_dest() { - checks::check_filter_none_match_gives_empty_dest::(); - } - - #[test] - unconstrained fn filter_works_with_multi_field_type() { - checks::check_filter_works_with_multi_field_type::(); - } - - #[test] - unconstrained fn filter_results_are_isolated() { - checks::check_filter_results_are_isolated::(); - } - - #[test] - unconstrained fn any_is_true_when_an_element_matches() { - checks::check_any_is_true_when_an_element_matches::(); - } - - #[test] - unconstrained fn any_is_false_when_no_element_matches() { - checks::check_any_is_false_when_no_element_matches::(); - } - - #[test] - unconstrained fn any_on_empty_array_is_false() { - checks::check_any_on_empty_array_is_false::(); - } - - #[test] - unconstrained fn all_is_true_when_every_element_matches() { - checks::check_all_is_true_when_every_element_matches::(); - } - - #[test] - unconstrained fn all_is_false_when_one_element_fails() { - checks::check_all_is_false_when_one_element_fails::(); - } - - #[test] - unconstrained fn all_on_empty_array_is_true() { - checks::check_all_on_empty_array_is_true::(); - } - - #[test] - unconstrained fn find_returns_first_matching_element() { - checks::check_find_returns_first_matching_element::(); - } - - #[test] - unconstrained fn find_returns_none_when_no_element_matches() { - checks::check_find_returns_none_when_no_element_matches::(); - } - - #[test] - unconstrained fn find_on_empty_array_is_none() { - checks::check_find_on_empty_array_is_none::(); - } - - #[test] - unconstrained fn empty_at_wipes_previous_data() { - checks::check_empty_at_wipes_previous_data::(); - } + use super::{load, store}; #[test] unconstrained fn store_and_load() { @@ -295,34 +100,4 @@ mod test { assert_eq(load(SLOT), Option::some(42)); }); } - - #[test] - unconstrained fn load_empty_returns_none() { - checks::check_load_empty_returns_none::(); - } - - #[test] - unconstrained fn store_overwrites() { - checks::check_store_overwrites::(); - } - - #[test] - unconstrained fn delete_removes_value() { - checks::check_delete_removes_value::(); - } - - #[test] - unconstrained fn delete_empty_is_noop() { - checks::check_delete_empty_is_noop::(); - } - - #[test] - unconstrained fn store_and_load_multi_field_type() { - checks::check_store_and_load_multi_field_type::(); - } - - #[test] - unconstrained fn stored_value_is_visible_as_a_length_one_array() { - checks::check_stored_value_is_visible_as_a_length_one_array::(); - } } From 74132209d93ab4e1a29f92914f202555b05a8d44 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 11:10:27 +0000 Subject: [PATCH 07/26] refactor: single parameterized oracle_array_tests macro Replaces the two per-backend attribute functions with one oracle_array_tests(module, oracle) attribute that takes the backend's oracle as a quoted path at the annotation site. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 2 +- .../aztec/src/oracle_array/test_helpers.nr | 18 +++++------------- .../aztec-nr/aztec/src/transient/mod.nr | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index e444f29198ea..bad14d5df485 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -79,7 +79,7 @@ pub unconstrained fn delete(slot: Field) { crate::oracle_array::delete::(slot) } -#[crate::oracle_array::test_helpers::ephemeral_oracle_array_tests] +#[crate::oracle_array::test_helpers::oracle_array_tests(quote { crate::ephemeral::EphemeralOracle })] mod test { use crate::oracle_array::test_helpers::SLOT; use crate::test::helpers::test_environment::TestEnvironment; diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index 6e36a38dc0de..4e66f211b670 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -2,8 +2,8 @@ //! //! Every `unconstrained` function in this module checks one behavior of the shared [`OracleArray`] API (or of the //! generic `store` / `load` / `delete` helpers) and is generic over the [`ArrayOracle`] backend. Backend test modules -//! do not reference the checks one by one: annotating a module with `ephemeral_oracle_array_tests` / -//! `transient_oracle_array_tests` generates one `#[test]` per check with that backend's oracle swapped in, so a check +//! do not reference the checks one by one: annotating a module with `oracle_array_tests` (passing the backend's +//! oracle as a quoted path) generates one `#[test]` per check with that oracle swapped in, so a check //! added here automatically runs against every backend. Checks that are expected to fail must be registered in //! `should_fail_message`, which supplies the message for the generated `#[test(should_fail_with = ...)]` attribute. //! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. @@ -766,7 +766,9 @@ comptime fn should_fail_message(name: Quoted) -> Option { } } -comptime fn oracle_array_tests(oracle: Quoted) -> Quoted { +/// Generates the shared [`OracleArray`] test suite against the given backend. Apply to a test module, passing the +/// backend's oracle as a quoted path: `#[oracle_array_tests(quote { crate::ephemeral::EphemeralOracle })]`. +pub(crate) comptime fn oracle_array_tests(_m: Module, oracle: Quoted) -> Quoted { let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap(); let mut tests = quote {}; for f in helpers.functions() { @@ -790,13 +792,3 @@ comptime fn oracle_array_tests(oracle: Quoted) -> Quoted { } tests } - -/// Generates the shared [`OracleArray`] test suite against the ephemeral backend. Apply to a test module. -pub(crate) comptime fn ephemeral_oracle_array_tests(_m: Module) -> Quoted { - oracle_array_tests(quote { crate::ephemeral::EphemeralOracle }) -} - -/// Generates the shared [`OracleArray`] test suite against the transient backend. Apply to a test module. -pub(crate) comptime fn transient_oracle_array_tests(_m: Module) -> Quoted { - oracle_array_tests(quote { crate::transient::TransientOracle }) -} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 0f1c807b6c2c..e4a14969b6b1 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -86,7 +86,7 @@ pub unconstrained fn delete(slot: Field) { crate::oracle_array::delete::(slot) } -#[crate::oracle_array::test_helpers::transient_oracle_array_tests] +#[crate::oracle_array::test_helpers::oracle_array_tests(quote { crate::transient::TransientOracle })] mod test { use crate::oracle_array::test_helpers::SLOT; use crate::test::helpers::test_environment::TestEnvironment; From d49fc3f414a021d6fe5f644db1eb2dae3c5a3eb6 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Wed, 10 Jun 2026 11:27:18 +0000 Subject: [PATCH 08/26] refactor: should_fail_ prefix for failing oracle array checks --- .../aztec/src/oracle_array/test_helpers.nr | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index 4e66f211b670..5eb44214fc47 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -1,11 +1,8 @@ //! Shared test suite for [`OracleArray`] backends. //! -//! Every `unconstrained` function in this module checks one behavior of the shared [`OracleArray`] API (or of the -//! generic `store` / `load` / `delete` helpers) and is generic over the [`ArrayOracle`] backend. Backend test modules -//! do not reference the checks one by one: annotating a module with `oracle_array_tests` (passing the backend's -//! oracle as a quoted path) generates one `#[test]` per check with that oracle swapped in, so a check -//! added here automatically runs against every backend. Checks that are expected to fail must be registered in -//! `should_fail_message`, which supplies the message for the generated `#[test(should_fail_with = ...)]` attribute. +//! Every function in this module checks one behavior of the shared [`OracleArray`] API (or of the generic +//! `store` / `load` / `delete` kv helpers) and is generic over the [`ArrayOracle`] backend. +//! //! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. //! `transient::store` / `transient::load`) to cover the delegation wiring. @@ -28,7 +25,7 @@ where }); } -pub(crate) unconstrained fn empty_array_read() +pub(crate) unconstrained fn should_fail_empty_array_read() where Oracle: ArrayOracle, { @@ -39,7 +36,7 @@ where }); } -pub(crate) unconstrained fn empty_array_pop() +pub(crate) unconstrained fn should_fail_empty_array_pop() where Oracle: ArrayOracle, { @@ -64,7 +61,7 @@ where }); } -pub(crate) unconstrained fn read_past_len() +pub(crate) unconstrained fn should_fail_read_past_len() where Oracle: ArrayOracle, { @@ -290,7 +287,7 @@ where }); } -pub(crate) unconstrained fn read_as_rejects_length_mismatch() +pub(crate) unconstrained fn should_fail_read_as_rejects_length_mismatch() where Oracle: ArrayOracle, { @@ -753,13 +750,13 @@ where /// Returns the `should_fail_with` message for checks that are expected to fail, or `Option::none()` for checks that /// are expected to pass. comptime fn should_fail_message(name: Quoted) -> Option { - if name == quote { empty_array_read } { + if name == quote { should_fail_empty_array_read } { Option::some(quote { "out of bounds" }) - } else if name == quote { empty_array_pop } { + } else if name == quote { should_fail_empty_array_pop } { Option::some(quote { "is empty" }) - } else if name == quote { read_past_len } { + } else if name == quote { should_fail_read_past_len } { Option::some(quote { "out of bounds" }) - } else if name == quote { read_as_rejects_length_mismatch } { + } else if name == quote { should_fail_read_as_rejects_length_mismatch } { Option::some(quote { "length mismatch" }) } else { Option::none() From 7845f7b9c6193c9bb0963532bd1530ed859efe6c Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 08:23:23 +0000 Subject: [PATCH 09/26] refactor(aztec-nr): rename oracle::ephemeral module to ephemeral_oracles The module name collided with crate::ephemeral, which owns the EphemeralArray type. Name the oracle module after what it holds -- the #[oracle(...)] declarations -- so the array module keeps the plain ephemeral name and the two are unambiguous at use sites. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 16 ++++++++-------- .../{ephemeral.nr => ephemeral_oracles.nr} | 0 noir-projects/aztec-nr/aztec/src/oracle/mod.nr | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename noir-projects/aztec-nr/aztec/src/oracle/{ephemeral.nr => ephemeral_oracles.nr} (100%) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index bad14d5df485..c67fcd05de9c 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,4 +1,4 @@ -use crate::oracle::ephemeral; +use crate::oracle::ephemeral_oracles; use crate::oracle_array::{ArrayOracle, OracleArray}; use crate::protocol::traits::{Deserialize, Serialize}; @@ -30,31 +30,31 @@ pub struct EphemeralOracle {} impl ArrayOracle for EphemeralOracle { unconstrained fn len_oracle(slot: Field) -> u32 { - ephemeral::len_oracle(slot) + ephemeral_oracles::len_oracle(slot) } unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { - ephemeral::push_oracle(slot, values) + ephemeral_oracles::push_oracle(slot, values) } unconstrained fn pop_oracle(slot: Field) -> [Field; N] { - ephemeral::pop_oracle(slot) + ephemeral_oracles::pop_oracle(slot) } unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { - ephemeral::get_oracle(slot, index) + ephemeral_oracles::get_oracle(slot, index) } unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { - ephemeral::set_oracle(slot, index, values) + ephemeral_oracles::set_oracle(slot, index, values) } unconstrained fn remove_oracle(slot: Field, index: u32) { - ephemeral::remove_oracle(slot, index) + ephemeral_oracles::remove_oracle(slot, index) } unconstrained fn clear_oracle(slot: Field) { - ephemeral::clear_oracle(slot) + ephemeral_oracles::clear_oracle(slot) } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr similarity index 100% rename from noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr rename to noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 85953142230d..8174bfab6c5b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -7,7 +7,7 @@ pub mod block_header; pub mod call_private_function; pub mod call_utility_function; pub mod capsules; -pub mod ephemeral; +pub mod ephemeral_oracles; pub mod transient; pub mod contract_sync; pub mod public_call; From b5948b019d2d6a53e7d5dca649c2699c306ab1db Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 08:26:29 +0000 Subject: [PATCH 10/26] refactor(aztec-nr): rename oracle::transient module to transient_oracles The module name collided with crate::transient, which owns the TransientArray type. Name the oracle module after what it holds -- the #[oracle(...)] declarations -- so the array module keeps the plain transient name and the two are unambiguous at use sites. --- noir-projects/aztec-nr/aztec/src/oracle/mod.nr | 2 +- .../{transient.nr => transient_oracles.nr} | 0 .../aztec-nr/aztec/src/transient/mod.nr | 16 ++++++++-------- 3 files changed, 9 insertions(+), 9 deletions(-) rename noir-projects/aztec-nr/aztec/src/oracle/{transient.nr => transient_oracles.nr} (100%) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 8174bfab6c5b..3ff645c72d56 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -8,7 +8,7 @@ pub mod call_private_function; pub mod call_utility_function; pub mod capsules; pub mod ephemeral_oracles; -pub mod transient; +pub mod transient_oracles; pub mod contract_sync; pub mod public_call; pub mod tx_phase; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/transient.nr b/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr similarity index 100% rename from noir-projects/aztec-nr/aztec/src/oracle/transient.nr rename to noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index e4a14969b6b1..4b0202d7f402 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,4 +1,4 @@ -use crate::oracle::transient; +use crate::oracle::transient_oracles; use crate::oracle_array::{ArrayOracle, OracleArray}; use crate::protocol::traits::{Deserialize, Serialize}; @@ -37,31 +37,31 @@ pub struct TransientOracle {} impl ArrayOracle for TransientOracle { unconstrained fn len_oracle(slot: Field) -> u32 { - transient::len_oracle(slot) + transient_oracles::len_oracle(slot) } unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { - transient::push_oracle(slot, values) + transient_oracles::push_oracle(slot, values) } unconstrained fn pop_oracle(slot: Field) -> [Field; N] { - transient::pop_oracle(slot) + transient_oracles::pop_oracle(slot) } unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { - transient::get_oracle(slot, index) + transient_oracles::get_oracle(slot, index) } unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { - transient::set_oracle(slot, index, values) + transient_oracles::set_oracle(slot, index, values) } unconstrained fn remove_oracle(slot: Field, index: u32) { - transient::remove_oracle(slot, index) + transient_oracles::remove_oracle(slot, index) } unconstrained fn clear_oracle(slot: Field) { - transient::clear_oracle(slot) + transient_oracles::clear_oracle(slot) } } From 62ad8548d8965e2928c94769f08a449860f0cf9b Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 08:56:08 +0000 Subject: [PATCH 11/26] refactor(aztec-nr): rename OracleArray struct to UnconstrainedArray OracleArray and ArrayOracle were near-anagrams, forcing readers to disambiguate which was the struct and which the backend trait. Keep ArrayOracle (it names the set of oracles that implement an array) and rename the struct to UnconstrainedArray, after the property every backend shares: its operations are unconstrained foreign calls into PXE-side storage, so the data is host-provided and must be verified before being trusted in constrained code. The oracle_array module, the ArrayOracle trait, and the Oracle type parameter are unchanged. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 7 +- .../aztec-nr/aztec/src/oracle_array/mod.nr | 47 +++---- .../aztec/src/oracle_array/test_helpers.nr | 119 +++++++++--------- .../aztec-nr/aztec/src/transient/mod.nr | 7 +- 4 files changed, 94 insertions(+), 86 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index c67fcd05de9c..aaa9704d52e2 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,5 +1,5 @@ use crate::oracle::ephemeral_oracles; -use crate::oracle_array::{ArrayOracle, OracleArray}; +use crate::oracle_array::{ArrayOracle, UnconstrainedArray}; use crate::protocol::traits::{Deserialize, Serialize}; /// A dynamically sized array that exists only during a single contract call frame. @@ -23,9 +23,10 @@ use crate::protocol::traits::{Deserialize, Serialize}; /// For data that must be shared across all frames of the same contract (private and utility) within one top-level PXE /// call (transaction simulation or utility call) but not persisted, use /// [`TransientArray`](crate::transient::TransientArray). -pub type EphemeralArray = OracleArray; +pub type EphemeralArray = UnconstrainedArray; -/// Routes [`OracleArray`] operations to the ephemeral array oracles, scoping arrays to a single contract call frame. +/// Routes [`UnconstrainedArray`] operations to the ephemeral array oracles, scoping arrays to a single contract call +/// frame. pub struct EphemeralOracle {} impl ArrayOracle for EphemeralOracle { diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr index 39851bbcbe83..b6e27b7fc218 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr @@ -4,7 +4,7 @@ use crate::oracle::random::random; use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; -/// Oracle backend for an [`OracleArray`]: the set of PXE-side operations that implement its storage. +/// Oracle backend for an [`UnconstrainedArray`]: the set of PXE-side operations that implement its storage. /// /// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the /// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one @@ -42,11 +42,11 @@ pub trait ArrayOracle { /// rather one of its aliases: [`EphemeralArray`](crate::ephemeral::EphemeralArray) (scoped to a single contract call /// frame) or [`TransientArray`](crate::transient::TransientArray) (shared across all frames of the same contract /// within one top-level PXE call). -pub struct OracleArray { +pub struct UnconstrainedArray { pub slot: Field, } -impl OracleArray +impl UnconstrainedArray where Oracle: ArrayOracle, { @@ -67,7 +67,8 @@ where /// Returns an empty array at a fresh, randomly allocated slot. /// /// Use this when the caller does not need a specific slot: the random slot is isolated from every other array of - /// the same backend with overwhelming probability. Prefer [`OracleArray::empty_at`] when the slot must be a known + /// the same backend with overwhelming probability. Prefer [`UnconstrainedArray::empty_at`] when the slot must be a + /// known /// value (e.g. one shared with an oracle or another call frame). pub unconstrained fn empty() -> Self { Self::empty_at(random()) @@ -121,7 +122,7 @@ where /// Removes all elements from the array and returns self for chaining. /// - /// Prefer [`OracleArray::empty_at`] when the intent is to start with a fresh array. + /// Prefer [`UnconstrainedArray::empty_at`] when the intent is to start with a fresh array. pub unconstrained fn clear(self) -> Self { Oracle::clear_oracle(self.slot); self @@ -148,14 +149,15 @@ where /// Applies `f` to every element and collects the results into a fresh array. /// /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly - /// allocated slot (see [`OracleArray::empty`]). The source array is left unchanged, and the result is isolated + /// allocated slot (see [`UnconstrainedArray::empty`]). The source array is left unchanged, and the result is + /// isolated /// from it, so `map` can be chained. - pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> OracleArray + pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> UnconstrainedArray where T: Deserialize, U: Serialize, { - let dest: OracleArray = OracleArray::empty(); + let dest: UnconstrainedArray = UnconstrainedArray::empty(); let n = self.len(); for i in 0..n { dest.push(f(self.get(i))); @@ -166,13 +168,14 @@ where /// Collects every element satisfying the predicate `f` into a fresh array. /// /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a - /// freshly allocated slot (see [`OracleArray::empty`]). Relative order is preserved. The source array is left + /// freshly allocated slot (see [`UnconstrainedArray::empty`]). Relative order is preserved. The source array is + /// left /// unchanged, and the result is isolated from it, so `filter` can be chained. pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self where T: Serialize + Deserialize, { - let dest: Self = OracleArray::empty(); + let dest: Self = UnconstrainedArray::empty(); let n = self.len(); for i in 0..n { let value = self.get(i); @@ -186,7 +189,8 @@ where /// Returns `true` if at least one element satisfies the predicate `f`. /// /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in - /// terms of [`OracleArray::filter`]: it keeps the matching elements and checks whether any survived. This is not + /// terms of [`UnconstrainedArray::filter`]: it keeps the matching elements and checks whether any survived. This is + /// not /// short-circuiting — every element is tested even after the first match. pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool where @@ -198,7 +202,7 @@ where /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). /// /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of - /// [`OracleArray::filter`]: every element matches exactly when filtering keeps all of them. This is not + /// [`UnconstrainedArray::filter`]: every element matches exactly when filtering keeps all of them. This is not /// short-circuiting — every element is tested even after the first failure. pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool where @@ -209,7 +213,8 @@ where /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. /// - /// Mirrors Rust's `Iterator::find`. Defined in terms of [`OracleArray::filter`], which preserves order, so the + /// Mirrors Rust's `Iterator::find`. Defined in terms of [`UnconstrainedArray::filter`], which preserves order, so + /// the /// first kept element is the first match. This is not short-circuiting — every element is tested even after the /// match is found. pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option @@ -225,7 +230,7 @@ where } } -impl OracleArray +impl UnconstrainedArray where Oracle: ArrayOracle, { @@ -238,7 +243,7 @@ where where T: Deserialize, { - assert_eq(self.len(), ::N, "OracleArray length mismatch for read_as"); + assert_eq(self.len(), ::N, "UnconstrainedArray length mismatch for read_as"); let mut fields: [Field; ::N] = [0; ::N]; for i in 0..::N { fields[i] = self.get(i); @@ -247,9 +252,9 @@ where } } -/// Serializes an `OracleArray` as its slot identifier, allowing oracle function signatures to use array types +/// Serializes an `UnconstrainedArray` as its slot identifier, allowing oracle function signatures to use array types /// instead of opaque `Field` slots. -impl Serialize for OracleArray { +impl Serialize for UnconstrainedArray { let N: u32 = 1; fn serialize(self) -> [Field; Self::N] { @@ -261,9 +266,9 @@ impl Serialize for OracleArray { } } -/// Deserializes a single Field into an `OracleArray` handle, treating the field value as the slot identifier. +/// Deserializes a single Field into an `UnconstrainedArray` handle, treating the field value as the slot identifier. /// This is the inverse of [`Serialize`]. -impl Deserialize for OracleArray { +impl Deserialize for UnconstrainedArray { let N: u32 = 1; fn deserialize(fields: [Field; Self::N]) -> Self { @@ -283,7 +288,7 @@ where T: Serialize, Oracle: ArrayOracle, { - let array: OracleArray = OracleArray::at(slot); + let array: UnconstrainedArray = UnconstrainedArray::at(slot); array.clear().push(value); } @@ -293,7 +298,7 @@ where T: Deserialize, Oracle: ArrayOracle, { - let array: OracleArray = OracleArray::at(slot); + let array: UnconstrainedArray = UnconstrainedArray::at(slot); if array.len() == 0 { Option::none() } else { diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr index 5eb44214fc47..7d56b6e972cf 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr @@ -1,12 +1,12 @@ -//! Shared test suite for [`OracleArray`] backends. +//! Shared test suite for [`UnconstrainedArray`] backends. //! -//! Every function in this module checks one behavior of the shared [`OracleArray`] API (or of the generic +//! Every function in this module checks one behavior of the shared [`UnconstrainedArray`] API (or of the generic //! `store` / `load` / `delete` kv helpers) and is generic over the [`ArrayOracle`] backend. //! //! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. //! `transient::store` / `transient::load`) to cover the delegation wiring. -use crate::oracle_array::{ArrayOracle, delete, load, OracleArray, store}; +use crate::oracle_array::{ArrayOracle, delete, load, store, UnconstrainedArray}; use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; @@ -20,7 +20,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); assert_eq(array.len(), 0); }); } @@ -31,7 +31,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); let _: Field = array.get(0); }); } @@ -42,7 +42,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); let _: Field = array.pop(); }); } @@ -53,7 +53,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(5); assert_eq(array.len(), 1); @@ -67,7 +67,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(5); let _ = array.get(1); @@ -80,7 +80,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(5); array.push(10); @@ -97,7 +97,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(5); array.set(0, 99); assert_eq(array.get(0), 99); @@ -110,7 +110,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(5); array.remove(0); assert_eq(array.len(), 0); @@ -123,7 +123,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(7); array.push(8); @@ -145,7 +145,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(7); array.push(8); @@ -165,7 +165,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(4); array.push(5); @@ -187,7 +187,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(4); array.push(5); @@ -211,7 +211,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(4); array.push(5); @@ -229,8 +229,8 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array_a: OracleArray = OracleArray::at(SLOT); - let array_b: OracleArray = OracleArray::at(OTHER_SLOT); + let array_a: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let array_b: UnconstrainedArray = UnconstrainedArray::at(OTHER_SLOT); array_a.push(10); array_a.push(20); @@ -251,7 +251,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); let a = MockStruct::new(5, 6); let b = MockStruct::new(7, 8); @@ -274,7 +274,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); let value = MockStruct::new(5, 6); let serialized = value.serialize(); @@ -293,7 +293,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); // MockStruct deserializes from 2 fields, so a single field is too short. array.push(1); @@ -307,7 +307,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT).clear(); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT).clear(); assert_eq(array.len(), 0); array.push(42); @@ -322,14 +322,14 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); assert_eq(array.len(), 3); // Clear the same slot, previous data should be gone. - let fresh: OracleArray = OracleArray::at(SLOT).clear(); + let fresh: UnconstrainedArray = UnconstrainedArray::at(SLOT).clear(); assert_eq(fresh.len(), 0); fresh.push(4); assert_eq(fresh.get(0), 4); @@ -342,8 +342,8 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let a: OracleArray = OracleArray::empty(); - let b: OracleArray = OracleArray::empty(); + let a: UnconstrainedArray = UnconstrainedArray::empty(); + let b: UnconstrainedArray = UnconstrainedArray::empty(); assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); @@ -362,12 +362,12 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(1); source.push(2); source.push(3); - let doubled: OracleArray = source.map(|x| x * 2); + let doubled: UnconstrainedArray = source.map(|x| x * 2); assert_eq(doubled.len(), 3); assert_eq(doubled.get(0), 2); @@ -386,8 +386,8 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); - let mapped: OracleArray = source.map(|x| x * 2); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let mapped: UnconstrainedArray = source.map(|x| x * 2); assert_eq(mapped.len(), 0); }); } @@ -398,11 +398,11 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(5); source.push(7); - let structs: OracleArray = source.map(|x| MockStruct::new(x, x + 1)); + let structs: UnconstrainedArray = source.map(|x| MockStruct::new(x, x + 1)); assert_eq(structs.len(), 2); assert_eq(structs.get(0), MockStruct::new(5, 6)); @@ -416,14 +416,14 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(1); source.push(2); source.push(3); // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. - let doubled: OracleArray = source.map(|x| x * 2); - let tripled: OracleArray = source.map(|x| x * 3); + let doubled: UnconstrainedArray = source.map(|x| x * 2); + let tripled: UnconstrainedArray = source.map(|x| x * 3); assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); assert_eq(doubled.get(0), 2); @@ -439,14 +439,14 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(1); source.push(2); source.push(3); source.push(2); source.push(5); - let kept: OracleArray = source.filter(|x| x != 2); + let kept: UnconstrainedArray = source.filter(|x| x != 2); assert_eq(kept.len(), 3); assert_eq(kept.get(0), 1); @@ -465,8 +465,8 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); - let kept: OracleArray = source.filter(|_| true); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let kept: UnconstrainedArray = source.filter(|_| true); assert_eq(kept.len(), 0); }); } @@ -477,12 +477,12 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(1); source.push(2); source.push(3); - let kept: OracleArray = source.filter(|_| false); + let kept: UnconstrainedArray = source.filter(|_| false); assert_eq(kept.len(), 0); }); } @@ -493,12 +493,12 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(MockStruct::new(1, 10)); source.push(MockStruct::new(2, 20)); source.push(MockStruct::new(3, 30)); - let kept: OracleArray = source.filter(|s: MockStruct| s.a != 2); + let kept: UnconstrainedArray = source.filter(|s: MockStruct| s.a != 2); assert_eq(kept.len(), 2); assert_eq(kept.get(0), MockStruct::new(1, 10)); @@ -512,15 +512,15 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let source: OracleArray = OracleArray::at(SLOT); + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); source.push(1); source.push(2); source.push(3); source.push(4); // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. - let odds: OracleArray = source.filter(|x| (x != 2) & (x != 4)); - let evens: OracleArray = source.filter(|x| (x != 1) & (x != 3)); + let odds: UnconstrainedArray = source.filter(|x| (x != 2) & (x != 4)); + let evens: UnconstrainedArray = source.filter(|x| (x != 1) & (x != 3)); assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); assert_eq(odds.len(), 2); @@ -538,7 +538,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -553,7 +553,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -568,7 +568,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); assert(!array.any(|_| true)); }); } @@ -579,7 +579,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -594,7 +594,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -609,7 +609,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); assert(array.all(|_| false)); }); } @@ -620,7 +620,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -639,7 +639,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); array.push(2); array.push(3); @@ -654,7 +654,7 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); assert(array.find(|_| true).is_none()); }); } @@ -665,11 +665,11 @@ where { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); array.push(1); assert_eq(array.len(), 1); - let fresh: OracleArray = OracleArray::empty_at(SLOT); + let fresh: UnconstrainedArray = UnconstrainedArray::empty_at(SLOT); assert_eq(fresh.len(), 0); }); } @@ -741,7 +741,7 @@ where let env = TestEnvironment::new(); env.utility_context(|_| { store::(SLOT, 42); - let array: OracleArray = OracleArray::at(SLOT); + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); assert_eq(array.len(), 1); assert_eq(array.get(0), 42); }); @@ -763,7 +763,8 @@ comptime fn should_fail_message(name: Quoted) -> Option { } } -/// Generates the shared [`OracleArray`] test suite against the given backend. Apply to a test module, passing the +/// Generates the shared [`UnconstrainedArray`] test suite against the given backend. Apply to a test module, passing +/// the /// backend's oracle as a quoted path: `#[oracle_array_tests(quote { crate::ephemeral::EphemeralOracle })]`. pub(crate) comptime fn oracle_array_tests(_m: Module, oracle: Quoted) -> Quoted { let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap(); diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 4b0202d7f402..7cf53154da3a 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,5 +1,5 @@ use crate::oracle::transient_oracles; -use crate::oracle_array::{ArrayOracle, OracleArray}; +use crate::oracle_array::{ArrayOracle, UnconstrainedArray}; use crate::protocol::traits::{Deserialize, Serialize}; /// A dynamically sized array that lives for the duration of a single top-level PXE call. @@ -29,9 +29,10 @@ use crate::protocol::traits::{Deserialize, Serialize}; /// Use this to pass not-to-be-persisted data between a contract's own frames (private or utility) within one top-level /// PXE call. For data confined to a single call frame, prefer [`EphemeralArray`](crate::ephemeral::EphemeralArray). /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). -pub type TransientArray = OracleArray; +pub type TransientArray = UnconstrainedArray; -/// Routes [`OracleArray`] operations to the transient array oracles, sharing arrays across all call frames of the +/// Routes [`UnconstrainedArray`] operations to the transient array oracles, sharing arrays across all call frames of +/// the /// same contract within one top-level PXE call. pub struct TransientOracle {} From e8ad51f1ec508d4250e9aa44b98be842fdaf65c1 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 09:01:29 +0000 Subject: [PATCH 12/26] refactor(aztec-nr): rename oracle_array module to unconstrained_array Follows the struct rename: the module and its test macro were still named after the old OracleArray struct. Rename the module directory to unconstrained_array and the oracle_array_tests macro to unconstrained_array_tests so the module mirrors the type it houses. The ArrayOracle trait and the store/load/delete helpers keep their names. --- noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr | 12 ++++++------ noir-projects/aztec-nr/aztec/src/lib.nr | 2 +- noir-projects/aztec-nr/aztec/src/transient/mod.nr | 12 ++++++------ .../src/{oracle_array => unconstrained_array}/mod.nr | 0 .../test_helpers.nr | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) rename noir-projects/aztec-nr/aztec/src/{oracle_array => unconstrained_array}/mod.nr (100%) rename noir-projects/aztec-nr/aztec/src/{oracle_array => unconstrained_array}/test_helpers.nr (97%) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index aaa9704d52e2..e99b3a252114 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,6 +1,6 @@ use crate::oracle::ephemeral_oracles; -use crate::oracle_array::{ArrayOracle, UnconstrainedArray}; use crate::protocol::traits::{Deserialize, Serialize}; +use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// A dynamically sized array that exists only during a single contract call frame. /// @@ -64,7 +64,7 @@ pub unconstrained fn store(slot: Field, value: T) where T: Serialize, { - crate::oracle_array::store::(slot, value) + crate::unconstrained_array::store::(slot, value) } /// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. @@ -72,18 +72,18 @@ pub unconstrained fn load(slot: Field) -> Option where T: Deserialize, { - crate::oracle_array::load::(slot) + crate::unconstrained_array::load::(slot) } /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub unconstrained fn delete(slot: Field) { - crate::oracle_array::delete::(slot) + crate::unconstrained_array::delete::(slot) } -#[crate::oracle_array::test_helpers::oracle_array_tests(quote { crate::ephemeral::EphemeralOracle })] +#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracle })] mod test { - use crate::oracle_array::test_helpers::SLOT; use crate::test::helpers::test_environment::TestEnvironment; + use crate::unconstrained_array::test_helpers::SLOT; use super::{load, store}; #[test] diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 507cd51bed80..387f820d22da 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -39,7 +39,7 @@ pub mod nullifier; pub mod oracle; pub mod state_vars; pub mod capsules; -pub mod oracle_array; +pub mod unconstrained_array; pub mod ephemeral; pub mod transient; pub mod event; diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 7cf53154da3a..57d2a9353dde 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,6 +1,6 @@ use crate::oracle::transient_oracles; -use crate::oracle_array::{ArrayOracle, UnconstrainedArray}; use crate::protocol::traits::{Deserialize, Serialize}; +use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// A dynamically sized array that lives for the duration of a single top-level PXE call. /// @@ -71,7 +71,7 @@ pub unconstrained fn store(slot: Field, value: T) where T: Serialize, { - crate::oracle_array::store::(slot, value) + crate::unconstrained_array::store::(slot, value) } /// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. @@ -79,18 +79,18 @@ pub unconstrained fn load(slot: Field) -> Option where T: Deserialize, { - crate::oracle_array::load::(slot) + crate::unconstrained_array::load::(slot) } /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub unconstrained fn delete(slot: Field) { - crate::oracle_array::delete::(slot) + crate::unconstrained_array::delete::(slot) } -#[crate::oracle_array::test_helpers::oracle_array_tests(quote { crate::transient::TransientOracle })] +#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracle })] mod test { - use crate::oracle_array::test_helpers::SLOT; use crate::test::helpers::test_environment::TestEnvironment; + use crate::unconstrained_array::test_helpers::SLOT; use super::{load, store}; #[test] diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr similarity index 100% rename from noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr rename to noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr diff --git a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr similarity index 97% rename from noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr rename to noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr index 7d56b6e972cf..678126b56d2d 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -6,10 +6,10 @@ //! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. //! `transient::store` / `transient::load`) to cover the delegation wiring. -use crate::oracle_array::{ArrayOracle, delete, load, store, UnconstrainedArray}; use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; +use crate::unconstrained_array::{ArrayOracle, delete, load, store, UnconstrainedArray}; pub(crate) global SLOT: Field = 1230; pub(crate) global OTHER_SLOT: Field = 5670; @@ -765,9 +765,9 @@ comptime fn should_fail_message(name: Quoted) -> Option { /// Generates the shared [`UnconstrainedArray`] test suite against the given backend. Apply to a test module, passing /// the -/// backend's oracle as a quoted path: `#[oracle_array_tests(quote { crate::ephemeral::EphemeralOracle })]`. -pub(crate) comptime fn oracle_array_tests(_m: Module, oracle: Quoted) -> Quoted { - let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap(); +/// backend's oracle as a quoted path: `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracle })]`. +pub(crate) comptime fn unconstrained_array_tests(_m: Module, oracle: Quoted) -> Quoted { + let helpers = quote { crate::unconstrained_array::test_helpers }.as_module().unwrap(); let mut tests = quote {}; for f in helpers.functions() { if f.is_unconstrained() { @@ -783,7 +783,7 @@ pub(crate) comptime fn oracle_array_tests(_m: Module, oracle: Quoted) -> Quoted $tests $attr unconstrained fn $name() { - crate::oracle_array::test_helpers::$name::<$oracle>(); + crate::unconstrained_array::test_helpers::$name::<$oracle>(); } }; } From 91772da91c7355e80ddfdd8cda779e4f927a6f66 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 09:09:47 +0000 Subject: [PATCH 13/26] docs(aztec-nr): reflow doc comments orphaned by the UnconstrainedArray rename UnconstrainedArray is longer than the old OracleArray, so several doc lines that referenced the type (or its method links) tipped past the 120-column limit. nargo fmt breaks an over-long comment line by pushing only the trailing word onto a new line, which left mid-paragraph orphans like '/// not' and '/// the'. Rejoin those words and rewrap the affected paragraphs at natural points, keeping every line within 120 columns. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 4 ++-- .../aztec-nr/aztec/src/transient/mod.nr | 5 ++--- .../aztec/src/unconstrained_array/mod.nr | 17 ++++++----------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index e99b3a252114..8dd2acaa35ae 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -25,8 +25,8 @@ use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// [`TransientArray`](crate::transient::TransientArray). pub type EphemeralArray = UnconstrainedArray; -/// Routes [`UnconstrainedArray`] operations to the ephemeral array oracles, scoping arrays to a single contract call -/// frame. +/// Routes [`UnconstrainedArray`] operations to the ephemeral array oracles, scoping arrays to a single contract +/// call frame. pub struct EphemeralOracle {} impl ArrayOracle for EphemeralOracle { diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 57d2a9353dde..b01b2381e90e 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -31,9 +31,8 @@ use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). pub type TransientArray = UnconstrainedArray; -/// Routes [`UnconstrainedArray`] operations to the transient array oracles, sharing arrays across all call frames of -/// the -/// same contract within one top-level PXE call. +/// Routes [`UnconstrainedArray`] operations to the transient array oracles, sharing arrays across all call frames +/// of the same contract within one top-level PXE call. pub struct TransientOracle {} impl ArrayOracle for TransientOracle { diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index b6e27b7fc218..f9e4f61fa166 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -68,8 +68,7 @@ where /// /// Use this when the caller does not need a specific slot: the random slot is isolated from every other array of /// the same backend with overwhelming probability. Prefer [`UnconstrainedArray::empty_at`] when the slot must be a - /// known - /// value (e.g. one shared with an oracle or another call frame). + /// known value (e.g. one shared with an oracle or another call frame). pub unconstrained fn empty() -> Self { Self::empty_at(random()) } @@ -150,8 +149,7 @@ where /// /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly /// allocated slot (see [`UnconstrainedArray::empty`]). The source array is left unchanged, and the result is - /// isolated - /// from it, so `map` can be chained. + /// isolated from it, so `map` can be chained. pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> UnconstrainedArray where T: Deserialize, @@ -169,8 +167,7 @@ where /// /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a /// freshly allocated slot (see [`UnconstrainedArray::empty`]). Relative order is preserved. The source array is - /// left - /// unchanged, and the result is isolated from it, so `filter` can be chained. + /// left unchanged, and the result is isolated from it, so `filter` can be chained. pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self where T: Serialize + Deserialize, @@ -190,8 +187,7 @@ where /// /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in /// terms of [`UnconstrainedArray::filter`]: it keeps the matching elements and checks whether any survived. This is - /// not - /// short-circuiting — every element is tested even after the first match. + /// not short-circuiting — every element is tested even after the first match. pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool where T: Serialize + Deserialize, @@ -214,9 +210,8 @@ where /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. /// /// Mirrors Rust's `Iterator::find`. Defined in terms of [`UnconstrainedArray::filter`], which preserves order, so - /// the - /// first kept element is the first match. This is not short-circuiting — every element is tested even after the - /// match is found. + /// the first kept element is the first match. This is not short-circuiting — every element is tested even after + /// the match is found. pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option where T: Serialize + Deserialize, From 15a7e004235986da39d1064ee0f3787ce5b87ee4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 09:17:23 +0000 Subject: [PATCH 14/26] refactor(aztec-nr): pluralize ArrayOracles, EphemeralOracles, TransientOracles Each of these names a set of oracle operations -- the full backend an array needs (ArrayOracles) and the concrete ephemeral/transient bundles that implement it -- so the plural reads truer and matches the ephemeral_oracles/transient_oracles module names. The singular Oracle remains as the type parameter: one backend slot filled by one bundle. --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 16 ++-- .../aztec-nr/aztec/src/transient/mod.nr | 16 ++-- .../aztec/src/unconstrained_array/mod.nr | 21 ++-- .../src/unconstrained_array/test_helpers.nr | 96 +++++++++---------- 4 files changed, 71 insertions(+), 78 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 8dd2acaa35ae..1dc263e46726 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,6 +1,6 @@ use crate::oracle::ephemeral_oracles; use crate::protocol::traits::{Deserialize, Serialize}; -use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// A dynamically sized array that exists only during a single contract call frame. /// @@ -23,13 +23,13 @@ use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// For data that must be shared across all frames of the same contract (private and utility) within one top-level PXE /// call (transaction simulation or utility call) but not persisted, use /// [`TransientArray`](crate::transient::TransientArray). -pub type EphemeralArray = UnconstrainedArray; +pub type EphemeralArray = UnconstrainedArray; /// Routes [`UnconstrainedArray`] operations to the ephemeral array oracles, scoping arrays to a single contract /// call frame. -pub struct EphemeralOracle {} +pub struct EphemeralOracles {} -impl ArrayOracle for EphemeralOracle { +impl ArrayOracles for EphemeralOracles { unconstrained fn len_oracle(slot: Field) -> u32 { ephemeral_oracles::len_oracle(slot) } @@ -64,7 +64,7 @@ pub unconstrained fn store(slot: Field, value: T) where T: Serialize, { - crate::unconstrained_array::store::(slot, value) + crate::unconstrained_array::store::(slot, value) } /// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. @@ -72,15 +72,15 @@ pub unconstrained fn load(slot: Field) -> Option where T: Deserialize, { - crate::unconstrained_array::load::(slot) + crate::unconstrained_array::load::(slot) } /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub unconstrained fn delete(slot: Field) { - crate::unconstrained_array::delete::(slot) + crate::unconstrained_array::delete::(slot) } -#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracle })] +#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })] mod test { use crate::test::helpers::test_environment::TestEnvironment; use crate::unconstrained_array::test_helpers::SLOT; diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index b01b2381e90e..43d7ee2b1666 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,6 +1,6 @@ use crate::oracle::transient_oracles; use crate::protocol::traits::{Deserialize, Serialize}; -use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// A dynamically sized array that lives for the duration of a single top-level PXE call. /// @@ -29,13 +29,13 @@ use crate::unconstrained_array::{ArrayOracle, UnconstrainedArray}; /// Use this to pass not-to-be-persisted data between a contract's own frames (private or utility) within one top-level /// PXE call. For data confined to a single call frame, prefer [`EphemeralArray`](crate::ephemeral::EphemeralArray). /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). -pub type TransientArray = UnconstrainedArray; +pub type TransientArray = UnconstrainedArray; /// Routes [`UnconstrainedArray`] operations to the transient array oracles, sharing arrays across all call frames /// of the same contract within one top-level PXE call. -pub struct TransientOracle {} +pub struct TransientOracles {} -impl ArrayOracle for TransientOracle { +impl ArrayOracles for TransientOracles { unconstrained fn len_oracle(slot: Field) -> u32 { transient_oracles::len_oracle(slot) } @@ -70,7 +70,7 @@ pub unconstrained fn store(slot: Field, value: T) where T: Serialize, { - crate::unconstrained_array::store::(slot, value) + crate::unconstrained_array::store::(slot, value) } /// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. @@ -78,15 +78,15 @@ pub unconstrained fn load(slot: Field) -> Option where T: Deserialize, { - crate::unconstrained_array::load::(slot) + crate::unconstrained_array::load::(slot) } /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub unconstrained fn delete(slot: Field) { - crate::unconstrained_array::delete::(slot) + crate::unconstrained_array::delete::(slot) } -#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracle })] +#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracles })] mod test { use crate::test::helpers::test_environment::TestEnvironment; use crate::unconstrained_array::test_helpers::SLOT; diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index f9e4f61fa166..aff0208d2d5f 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -5,14 +5,7 @@ use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; /// Oracle backend for an [`UnconstrainedArray`]: the set of PXE-side operations that implement its storage. -/// -/// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the -/// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one -/// contract call frame, while [`TransientArray`](crate::transient::TransientArray) arrays are shared across all frames -/// of the same contract within one top-level PXE call). Implementations are thin wrappers around `#[oracle]` -/// declarations, so method dispatch is resolved at compile time via monomorphization and each array type emits its own -/// foreign calls. -pub trait ArrayOracle { +pub trait ArrayOracles { /// Returns the number of elements in the array at `slot`. unconstrained fn len_oracle(slot: Field) -> u32; @@ -35,7 +28,7 @@ pub trait ArrayOracle { unconstrained fn clear_oracle(slot: Field); } -/// A dynamically sized array backed by PXE-side in-memory storage via an [`ArrayOracle`] backend. +/// A dynamically sized array backed by PXE-side in-memory storage via an [`ArrayOracles`] backend. /// /// Arrays are identified by a slot, and each logical operation (push, pop, get, etc.) is a single oracle call. The /// `Oracle` backend determines the array's lifetime and visibility; contracts should not use this type directly but @@ -48,7 +41,7 @@ pub struct UnconstrainedArray { impl UnconstrainedArray where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { /// Returns a handle to the array at the given slot, which may already contain data (e.g. populated by an oracle /// or by an earlier frame, depending on the backend's visibility). @@ -227,7 +220,7 @@ where impl UnconstrainedArray where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { /// Deserializes the whole array into a `T`. /// @@ -281,7 +274,7 @@ impl Deserialize for UnconstrainedArray { pub(crate) unconstrained fn store(slot: Field, value: T) where T: Serialize, - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let array: UnconstrainedArray = UnconstrainedArray::at(slot); array.clear().push(value); @@ -291,7 +284,7 @@ where pub(crate) unconstrained fn load(slot: Field) -> Option where T: Deserialize, - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let array: UnconstrainedArray = UnconstrainedArray::at(slot); if array.len() == 0 { @@ -304,7 +297,7 @@ where /// Deletes the value stored at `slot`. Does nothing if the slot is already empty. pub(crate) unconstrained fn delete(slot: Field) where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { Oracle::clear_oracle(slot) } diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr index 678126b56d2d..1ed1398884ce 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -1,7 +1,7 @@ //! Shared test suite for [`UnconstrainedArray`] backends. //! //! Every function in this module checks one behavior of the shared [`UnconstrainedArray`] API (or of the generic -//! `store` / `load` / `delete` kv helpers) and is generic over the [`ArrayOracle`] backend. +//! `store` / `load` / `delete` kv helpers) and is generic over the [`ArrayOracles`] backend. //! //! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. //! `transient::store` / `transient::load`) to cover the delegation wiring. @@ -9,14 +9,14 @@ use crate::protocol::traits::Serialize; use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; -use crate::unconstrained_array::{ArrayOracle, delete, load, store, UnconstrainedArray}; +use crate::unconstrained_array::{ArrayOracles, delete, load, store, UnconstrainedArray}; pub(crate) global SLOT: Field = 1230; pub(crate) global OTHER_SLOT: Field = 5670; pub(crate) unconstrained fn empty_array() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -27,7 +27,7 @@ where pub(crate) unconstrained fn should_fail_empty_array_read() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -38,7 +38,7 @@ where pub(crate) unconstrained fn should_fail_empty_array_pop() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -49,7 +49,7 @@ where pub(crate) unconstrained fn array_push() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -63,7 +63,7 @@ where pub(crate) unconstrained fn should_fail_read_past_len() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -76,7 +76,7 @@ where pub(crate) unconstrained fn array_pop() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -93,7 +93,7 @@ where pub(crate) unconstrained fn array_set() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -106,7 +106,7 @@ where pub(crate) unconstrained fn array_remove_last() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -119,7 +119,7 @@ where pub(crate) unconstrained fn array_remove_some() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -141,7 +141,7 @@ where pub(crate) unconstrained fn array_remove_all() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -161,7 +161,7 @@ where pub(crate) unconstrained fn for_each_called_with_all_elements() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -183,7 +183,7 @@ where pub(crate) unconstrained fn for_each_remove_some() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -207,7 +207,7 @@ where pub(crate) unconstrained fn for_each_remove_all() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -225,7 +225,7 @@ where pub(crate) unconstrained fn different_slots_are_isolated() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -247,7 +247,7 @@ where pub(crate) unconstrained fn works_with_multi_field_type() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -270,7 +270,7 @@ where pub(crate) unconstrained fn read_as_reconstructs_serialized_value() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -289,7 +289,7 @@ where pub(crate) unconstrained fn should_fail_read_as_rejects_length_mismatch() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -303,7 +303,7 @@ where pub(crate) unconstrained fn clear_returns_self() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -318,7 +318,7 @@ where pub(crate) unconstrained fn clear_wipes_previous_data() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -338,7 +338,7 @@ where pub(crate) unconstrained fn empty_allocates_distinct_slots() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -358,7 +358,7 @@ where pub(crate) unconstrained fn map_transforms_each_element() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -382,7 +382,7 @@ where pub(crate) unconstrained fn map_empty_source_gives_empty_dest() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -394,7 +394,7 @@ where pub(crate) unconstrained fn map_to_different_type() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -412,7 +412,7 @@ where pub(crate) unconstrained fn map_results_are_isolated() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -435,7 +435,7 @@ where pub(crate) unconstrained fn filter_keeps_matching_elements_in_order() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -461,7 +461,7 @@ where pub(crate) unconstrained fn filter_empty_source_gives_empty_dest() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -473,7 +473,7 @@ where pub(crate) unconstrained fn filter_none_match_gives_empty_dest() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -489,7 +489,7 @@ where pub(crate) unconstrained fn filter_works_with_multi_field_type() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -508,7 +508,7 @@ where pub(crate) unconstrained fn filter_results_are_isolated() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -534,7 +534,7 @@ where pub(crate) unconstrained fn any_is_true_when_an_element_matches() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -549,7 +549,7 @@ where pub(crate) unconstrained fn any_is_false_when_no_element_matches() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -564,7 +564,7 @@ where pub(crate) unconstrained fn any_on_empty_array_is_false() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -575,7 +575,7 @@ where pub(crate) unconstrained fn all_is_true_when_every_element_matches() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -590,7 +590,7 @@ where pub(crate) unconstrained fn all_is_false_when_one_element_fails() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -605,7 +605,7 @@ where pub(crate) unconstrained fn all_on_empty_array_is_true() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -616,7 +616,7 @@ where pub(crate) unconstrained fn find_returns_first_matching_element() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -635,7 +635,7 @@ where pub(crate) unconstrained fn find_returns_none_when_no_element_matches() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -650,7 +650,7 @@ where pub(crate) unconstrained fn find_on_empty_array_is_none() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -661,7 +661,7 @@ where pub(crate) unconstrained fn empty_at_wipes_previous_data() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -676,7 +676,7 @@ where pub(crate) unconstrained fn load_empty_returns_none() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -687,7 +687,7 @@ where pub(crate) unconstrained fn store_overwrites() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -699,7 +699,7 @@ where pub(crate) unconstrained fn delete_removes_value() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -712,7 +712,7 @@ where pub(crate) unconstrained fn delete_empty_is_noop() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -724,7 +724,7 @@ where pub(crate) unconstrained fn store_and_load_multi_field_type() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -736,7 +736,7 @@ where pub(crate) unconstrained fn stored_value_is_visible_as_a_length_one_array() where - Oracle: ArrayOracle, + Oracle: ArrayOracles, { let env = TestEnvironment::new(); env.utility_context(|_| { @@ -765,7 +765,7 @@ comptime fn should_fail_message(name: Quoted) -> Option { /// Generates the shared [`UnconstrainedArray`] test suite against the given backend. Apply to a test module, passing /// the -/// backend's oracle as a quoted path: `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracle })]`. +/// backend's oracle as a quoted path: `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })]`. pub(crate) comptime fn unconstrained_array_tests(_m: Module, oracle: Quoted) -> Quoted { let helpers = quote { crate::unconstrained_array::test_helpers }.as_module().unwrap(); let mut tests = quote {}; From aa90bebd4f51085297c935e8304c2cc921dc6ec4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 09:21:47 +0000 Subject: [PATCH 15/26] docs(aztec-nr): restore ArrayOracles trait doc paragraph The explanatory paragraph on the ArrayOracles trait was dropped during the previous pluralize commit (a formatter re-stage race on the shared working tree); the rename itself only swapped the identifier. Restore the paragraph verbatim. nargo fmt run directly keeps it intact. --- .../aztec-nr/aztec/src/unconstrained_array/mod.nr | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index aff0208d2d5f..aa1d5319686c 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -5,6 +5,13 @@ use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; /// Oracle backend for an [`UnconstrainedArray`]: the set of PXE-side operations that implement its storage. +/// +/// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the +/// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one +/// contract call frame, while [`TransientArray`](crate::transient::TransientArray) arrays are shared across all frames +/// of the same contract within one top-level PXE call). Implementations are thin wrappers around `#[oracle]` +/// declarations, so method dispatch is resolved at compile time via monomorphization and each array type emits its own +/// foreign calls. pub trait ArrayOracles { /// Returns the number of elements in the array at `slot`. unconstrained fn len_oracle(slot: Field) -> u32; From e174bcee3559669c34b2834c6c3dd5ca3d426a75 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 09:52:01 +0000 Subject: [PATCH 16/26] properly document panics --- .../aztec/src/oracle/ephemeral_oracles.nr | 8 ++++---- .../aztec/src/oracle/transient_oracles.nr | 8 ++++---- .../aztec/src/unconstrained_array/mod.nr | 19 ++++++++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr index 9f2721fc53c8..cb053b599841 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr @@ -8,15 +8,15 @@ #[oracle(aztec_utl_pushEphemeral)] pub(crate) unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 {} -/// Removes and returns the last serialized element from the ephemeral array. +/// Removes and returns the last serialized element from the ephemeral array. Panics if the array is empty. #[oracle(aztec_utl_popEphemeral)] pub(crate) unconstrained fn pop_oracle(slot: Field) -> [Field; N] {} -/// Returns the serialized element at the given index. +/// Returns the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_getEphemeral)] pub(crate) unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] {} -/// Overwrites the serialized element at the given index. +/// Overwrites the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_setEphemeral)] pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) {} @@ -24,7 +24,7 @@ pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, valu #[oracle(aztec_utl_getEphemeralLen)] pub(crate) unconstrained fn len_oracle(slot: Field) -> u32 {} -/// Removes the element at the given index, shifting subsequent elements backward. +/// Removes the element at the given index, shifting subsequent elements backward. Panics if `index` is out of bounds. #[oracle(aztec_utl_removeEphemeral)] pub(crate) unconstrained fn remove_oracle(slot: Field, index: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr b/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr index c7db29c51b2d..9c76dca0e635 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr @@ -9,15 +9,15 @@ #[oracle(aztec_utl_pushTransient)] pub(crate) unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 {} -/// Removes and returns the last serialized element from the transient array. +/// Removes and returns the last serialized element from the transient array. Panics if the array is empty. #[oracle(aztec_utl_popTransient)] pub(crate) unconstrained fn pop_oracle(slot: Field) -> [Field; N] {} -/// Returns the serialized element at the given index. +/// Returns the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_getTransient)] pub(crate) unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] {} -/// Overwrites the serialized element at the given index. +/// Overwrites the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_setTransient)] pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) {} @@ -25,7 +25,7 @@ pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, valu #[oracle(aztec_utl_getTransientLen)] pub(crate) unconstrained fn len_oracle(slot: Field) -> u32 {} -/// Removes the element at the given index, shifting subsequent elements backward. +/// Removes the element at the given index, shifting subsequent elements backward. Panics if `index` is out of bounds. #[oracle(aztec_utl_removeTransient)] pub(crate) unconstrained fn remove_oracle(slot: Field, index: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index aa1d5319686c..fafceb17674b 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -19,16 +19,20 @@ pub trait ArrayOracles { /// Appends a serialized element to the array at `slot` and returns the new length. unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32; - /// Removes and returns the last serialized element of the array at `slot`. + /// Removes and returns the last serialized element of the array at `slot`. Implementors must panic if the + /// array is empty. unconstrained fn pop_oracle(slot: Field) -> [Field; N]; - /// Returns the serialized element at the given index of the array at `slot`. + /// Returns the serialized element at the given index of the array at `slot`. Implementors must panic if `index` + /// is out of bounds. unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N]; - /// Overwrites the serialized element at the given index of the array at `slot`. + /// Overwrites the serialized element at the given index of the array at `slot`. Implementors must panic if + /// `index` is out of bounds. unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]); /// Removes the element at the given index of the array at `slot`, shifting subsequent elements backward. + /// Implementors must panic if `index` is out of bounds. unconstrained fn remove_oracle(slot: Field, index: u32); /// Removes all elements from the array at `slot`. @@ -87,7 +91,7 @@ where let _ = Oracle::push_oracle(self.slot, serialized); } - /// Removes and returns the last element. Panics if the array is empty. + /// Removes and returns the last element. Implementors are required to panic if the array is empty. pub unconstrained fn pop(self) -> T where T: Deserialize, @@ -96,7 +100,7 @@ where Deserialize::deserialize(serialized) } - /// Retrieves the value stored at `index`. Panics if the index is out of bounds. + /// Retrieves the value stored at `index`. Implementors are required to panic if the index is out of bounds. pub unconstrained fn get(self, index: u32) -> T where T: Deserialize, @@ -105,7 +109,7 @@ where Deserialize::deserialize(serialized) } - /// Overwrites the value stored at `index`. Panics if the index is out of bounds. + /// Overwrites the value stored at `index`. Implementors are required to panic if the index is out of bounds. pub unconstrained fn set(self, index: u32, value: T) where T: Serialize, @@ -114,7 +118,8 @@ where Oracle::set_oracle(self.slot, index, serialized); } - /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. + /// Removes the element at `index`, shifting subsequent elements backward. Implementors are required to panic if + /// the index is out of bounds. pub unconstrained fn remove(self, index: u32) { Oracle::remove_oracle(self.slot, index); } From 950a4a2ed435418340dda3741252116a990af066 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 10:21:53 +0000 Subject: [PATCH 17/26] empty does not need to clear --- noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index fafceb17674b..f05f03ab01af 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -74,7 +74,7 @@ where /// the same backend with overwhelming probability. Prefer [`UnconstrainedArray::empty_at`] when the slot must be a /// known value (e.g. one shared with an oracle or another call frame). pub unconstrained fn empty() -> Self { - Self::empty_at(random()) + Self::at(random()) } /// Returns the number of elements stored in the array. @@ -125,8 +125,6 @@ where } /// Removes all elements from the array and returns self for chaining. - /// - /// Prefer [`UnconstrainedArray::empty_at`] when the intent is to start with a fresh array. pub unconstrained fn clear(self) -> Self { Oracle::clear_oracle(self.slot); self From c85569f8d9ea3f5370339d179572a4ca9f1fd929 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 11:19:03 +0000 Subject: [PATCH 18/26] make find short-circuit --- .../aztec/src/unconstrained_array/mod.nr | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index f05f03ab01af..77e202e26efd 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -211,20 +211,21 @@ where } /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. - /// - /// Mirrors Rust's `Iterator::find`. Defined in terms of [`UnconstrainedArray::filter`], which preserves order, so - /// the first kept element is the first match. This is not short-circuiting — every element is tested even after - /// the match is found. pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option where - T: Serialize + Deserialize, + T: Deserialize, { - let matches = self.filter(f); - if matches.len() != 0 { - Option::some(matches.get(0)) - } else { - Option::none() + let n = self.len(); + let mut result: Option = Option::none(); + let mut i = 0; + while (i < n) & result.is_none() { + let value = self.get(i); + if f(value) { + result = Option::some(value); + } + i += 1; } + result } } From 0ca93c66ef43362b370dbe7c940b42d925120650 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 11:23:59 +0000 Subject: [PATCH 19/26] remove read_as for now --- .../aztec/src/unconstrained_array/mod.nr | 22 ------------ .../src/unconstrained_array/test_helpers.nr | 35 ------------------- 2 files changed, 57 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index 77e202e26efd..c0d53a12f4de 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -229,28 +229,6 @@ where } } -impl UnconstrainedArray -where - Oracle: ArrayOracles, -{ - /// Deserializes the whole array into a `T`. - /// - /// Asserts the array holds exactly `::N` fields, then reconstructs a `T` from them. This is the - /// read-back counterpart to pushing a value's serialized fields onto the array, and is defined only on arrays of - /// `Field` because deserialization reconstructs a type from raw fields. - pub unconstrained fn read_as(self) -> T - where - T: Deserialize, - { - assert_eq(self.len(), ::N, "UnconstrainedArray length mismatch for read_as"); - let mut fields: [Field; ::N] = [0; ::N]; - for i in 0..::N { - fields[i] = self.get(i); - } - Deserialize::deserialize(fields) - } -} - /// Serializes an `UnconstrainedArray` as its slot identifier, allowing oracle function signatures to use array types /// instead of opaque `Field` slots. impl Serialize for UnconstrainedArray { diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr index 1ed1398884ce..dd87da5d2af2 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -268,39 +268,6 @@ where }); } -pub(crate) unconstrained fn read_as_reconstructs_serialized_value() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); - - let value = MockStruct::new(5, 6); - let serialized = value.serialize(); - for i in 0..serialized.len() { - array.push(serialized[i]); - } - - let reconstructed: MockStruct = array.read_as(); - assert_eq(reconstructed, value); - }); -} - -pub(crate) unconstrained fn should_fail_read_as_rejects_length_mismatch() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); - // MockStruct deserializes from 2 fields, so a single field is too short. - array.push(1); - - let _: MockStruct = array.read_as(); - }); -} - pub(crate) unconstrained fn clear_returns_self() where Oracle: ArrayOracles, @@ -756,8 +723,6 @@ comptime fn should_fail_message(name: Quoted) -> Option { Option::some(quote { "is empty" }) } else if name == quote { should_fail_read_past_len } { Option::some(quote { "out of bounds" }) - } else if name == quote { should_fail_read_as_rejects_length_mismatch } { - Option::some(quote { "length mismatch" }) } else { Option::none() } From 5fac01ba70fd6f8e318490b1a12f31a913684344 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 11:57:56 +0000 Subject: [PATCH 20/26] move Serialize/Deserialize traits from UnconstrainedArray to EphemeralArray --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 28 ++++++++++++++++++ .../aztec/src/unconstrained_array/mod.nr | 29 ------------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 1dc263e46726..80207dab8aa7 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,5 +1,6 @@ use crate::oracle::ephemeral_oracles; use crate::protocol::traits::{Deserialize, Serialize}; +use crate::protocol::utils::{reader::Reader, writer::Writer}; use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// A dynamically sized array that exists only during a single contract call frame. @@ -59,6 +60,33 @@ impl ArrayOracles for EphemeralOracles { } } +/// Serializes an `EphemeralArray` as its slot, allowing oracle function signatures to use ephemeral array types +/// instead of opaque `Field` slots. +impl Serialize for UnconstrainedArray { + let N: u32 = 1; + + fn serialize(self) -> [Field; Self::N] { + [self.slot] + } + + fn stream_serialize(self, writer: &mut Writer) { + writer.write(self.slot); + } +} + +/// Deserializes a single Field into an `EphemeralArray` handle, treating the field value as the slot identifier. +impl Deserialize for UnconstrainedArray { + let N: u32 = 1; + + fn deserialize(fields: [Field; Self::N]) -> Self { + Self { slot: fields[0] } + } + + fn stream_deserialize(reader: &mut Reader) -> Self { + Self { slot: reader.read() } + } +} + /// Stores a single value at `slot`, overwriting any value previously stored there. pub unconstrained fn store(slot: Field, value: T) where diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index c0d53a12f4de..7f7c32cb6368 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -2,7 +2,6 @@ pub(crate) mod test_helpers; use crate::oracle::random::random; use crate::protocol::traits::{Deserialize, Serialize}; -use crate::protocol::utils::{reader::Reader, writer::Writer}; /// Oracle backend for an [`UnconstrainedArray`]: the set of PXE-side operations that implement its storage. /// @@ -229,34 +228,6 @@ where } } -/// Serializes an `UnconstrainedArray` as its slot identifier, allowing oracle function signatures to use array types -/// instead of opaque `Field` slots. -impl Serialize for UnconstrainedArray { - let N: u32 = 1; - - fn serialize(self) -> [Field; Self::N] { - [self.slot] - } - - fn stream_serialize(self, writer: &mut Writer) { - writer.write(self.slot); - } -} - -/// Deserializes a single Field into an `UnconstrainedArray` handle, treating the field value as the slot identifier. -/// This is the inverse of [`Serialize`]. -impl Deserialize for UnconstrainedArray { - let N: u32 = 1; - - fn deserialize(fields: [Field; Self::N]) -> Self { - Self { slot: fields[0] } - } - - fn stream_deserialize(reader: &mut Reader) -> Self { - Self { slot: reader.read() } - } -} - /// Stores a single value at `slot`, overwriting any value previously stored there. /// /// The value is stored as a length-one array at the slot: read it back with [`load`] and remove it with [`delete`]. From f7cbb0175b0b878b1536cb5f6268023949953c9c Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 12:50:20 +0000 Subject: [PATCH 21/26] refactor kv functions --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 38 +---- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec-nr/aztec/src/transient/mod.nr | 150 ++++++++++++++---- .../aztec/src/unconstrained_array/mod.nr | 34 ---- .../src/unconstrained_array/test_helpers.nr | 84 +--------- 5 files changed, 126 insertions(+), 181 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 80207dab8aa7..87ce7cda2d39 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -26,8 +26,6 @@ use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// [`TransientArray`](crate::transient::TransientArray). pub type EphemeralArray = UnconstrainedArray; -/// Routes [`UnconstrainedArray`] operations to the ephemeral array oracles, scoping arrays to a single contract -/// call frame. pub struct EphemeralOracles {} impl ArrayOracles for EphemeralOracles { @@ -87,39 +85,5 @@ impl Deserialize for UnconstrainedArray { } } -/// Stores a single value at `slot`, overwriting any value previously stored there. -pub unconstrained fn store(slot: Field, value: T) -where - T: Serialize, -{ - crate::unconstrained_array::store::(slot, value) -} - -/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. -pub unconstrained fn load(slot: Field) -> Option -where - T: Deserialize, -{ - crate::unconstrained_array::load::(slot) -} - -/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. -pub unconstrained fn delete(slot: Field) { - crate::unconstrained_array::delete::(slot) -} - #[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })] -mod test { - use crate::test::helpers::test_environment::TestEnvironment; - use crate::unconstrained_array::test_helpers::SLOT; - use super::{load, store}; - - #[test] - unconstrained fn store_and_load() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - store(SLOT, 42); - assert_eq(load(SLOT), Option::some(42)); - }); - } -} +mod test {} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 387f820d22da..1a164ce8a1e2 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -40,6 +40,7 @@ pub mod oracle; pub mod state_vars; pub mod capsules; pub mod unconstrained_array; +pub mod unconstrained_value; pub mod ephemeral; pub mod transient; pub mod event; diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 43d7ee2b1666..57cb24091f4e 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -31,8 +31,53 @@ use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). pub type TransientArray = UnconstrainedArray; -/// Routes [`UnconstrainedArray`] operations to the transient array oracles, sharing arrays across all call frames -/// of the same contract within one top-level PXE call. +/// A single value that lives for the duration of a single top-level PXE call. +/// +/// Shared across all call frames of the same contract (private and utility alike) within one top-level PXE call. +/// A contract can only ever access its own transient values; other contracts called within the same top-level PXE call +/// cannot see them. +/// +/// Values share the slot space with [`TransientArray`]s: a value and an array at the same slot alias each other, so +/// use distinct slots. +pub struct TransientValue { + slot: Field, +} + +impl TransientValue { + /// Returns a handle to the value at the given slot, which may already hold data stored by an earlier frame. + pub unconstrained fn at(slot: Field) -> Self { + Self { slot } + } + + /// Stores a value at the slot, overwriting any value previously stored there. + pub unconstrained fn store(self, value: T) + where + T: Serialize, + { + transient_oracles::clear_oracle(self.slot); + let _ = transient_oracles::push_oracle(self.slot, value.serialize()); + } + + /// Returns the value previously stored at the slot with [`TransientValue::store`], or `Option::none()` if the + /// slot holds no value. + pub unconstrained fn read(self) -> Option + where + T: Deserialize, + { + if transient_oracles::len_oracle(self.slot) == 0 { + Option::none() + } else { + let serialized = transient_oracles::get_oracle(self.slot, 0); + Option::some(Deserialize::deserialize(serialized)) + } + } + + /// Deletes the stored value. Does nothing if the slot holds no value. + pub unconstrained fn delete(self) { + transient_oracles::clear_oracle(self.slot) + } +} + pub struct TransientOracles {} impl ArrayOracles for TransientOracles { @@ -65,39 +110,88 @@ impl ArrayOracles for TransientOracles { } } -/// Stores a single value at `slot`, overwriting any value previously stored there. -pub unconstrained fn store(slot: Field, value: T) -where - T: Serialize, -{ - crate::unconstrained_array::store::(slot, value) -} +#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracles })] +mod test {} -/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. -pub unconstrained fn load(slot: Field) -> Option -where - T: Deserialize, -{ - crate::unconstrained_array::load::(slot) -} +mod value_test { + use crate::test::helpers::test_environment::TestEnvironment; + use crate::test::mocks::MockStruct; + use super::{TransientArray, TransientValue}; -/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. -pub unconstrained fn delete(slot: Field) { - crate::unconstrained_array::delete::(slot) -} + global SLOT: Field = 9870; -#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracles })] -mod test { - use crate::test::helpers::test_environment::TestEnvironment; - use crate::unconstrained_array::test_helpers::SLOT; - use super::{load, store}; + #[test] + unconstrained fn read_empty_returns_none() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + assert_eq(value.read(), Option::none()); + }); + } + + #[test] + unconstrained fn store_and_read_roundtrips() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + assert_eq(value.read(), Option::some(42)); + }); + } + + #[test] + unconstrained fn store_overwrites() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + value.store(1); + value.store(2); + assert_eq(value.read(), Option::some(2)); + }); + } + + #[test] + unconstrained fn delete_removes_value() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + value.delete(); + assert_eq(value.read(), Option::none()); + }); + } + + #[test] + unconstrained fn delete_empty_is_noop() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + value.delete(); + assert_eq(value.read(), Option::none()); + }); + } #[test] - unconstrained fn store_and_load() { + unconstrained fn works_with_multi_field_type() { let env = TestEnvironment::new(); env.utility_context(|_| { - store(SLOT, 42); - assert_eq(load(SLOT), Option::some(42)); + let value: TransientValue = TransientValue::at(SLOT); + let stored = MockStruct::new(5, 6); + value.store(stored); + assert_eq(value.read(), Option::some(stored)); + }); + } + + #[test] + unconstrained fn stored_value_is_visible_as_a_length_one_array() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + + let array: TransientArray = TransientArray::at(SLOT); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 42); }); } } diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index 7f7c32cb6368..af5da1e47893 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -227,37 +227,3 @@ where result } } - -/// Stores a single value at `slot`, overwriting any value previously stored there. -/// -/// The value is stored as a length-one array at the slot: read it back with [`load`] and remove it with [`delete`]. -pub(crate) unconstrained fn store(slot: Field, value: T) -where - T: Serialize, - Oracle: ArrayOracles, -{ - let array: UnconstrainedArray = UnconstrainedArray::at(slot); - array.clear().push(value); -} - -/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. -pub(crate) unconstrained fn load(slot: Field) -> Option -where - T: Deserialize, - Oracle: ArrayOracles, -{ - let array: UnconstrainedArray = UnconstrainedArray::at(slot); - if array.len() == 0 { - Option::none() - } else { - Option::some(array.get(0)) - } -} - -/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. -pub(crate) unconstrained fn delete(slot: Field) -where - Oracle: ArrayOracles, -{ - Oracle::clear_oracle(slot) -} diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr index dd87da5d2af2..680e26178fc7 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -1,15 +1,8 @@ //! Shared test suite for [`UnconstrainedArray`] backends. -//! -//! Every function in this module checks one behavior of the shared [`UnconstrainedArray`] API (or of the generic -//! `store` / `load` / `delete` kv helpers) and is generic over the [`ArrayOracles`] backend. -//! -//! Each backend module additionally keeps a hand-written smoke test routing through its own public helpers (e.g. -//! `transient::store` / `transient::load`) to cover the delegation wiring. - -use crate::protocol::traits::Serialize; + use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; -use crate::unconstrained_array::{ArrayOracles, delete, load, store, UnconstrainedArray}; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; pub(crate) global SLOT: Field = 1230; pub(crate) global OTHER_SLOT: Field = 5670; @@ -641,79 +634,6 @@ where }); } -pub(crate) unconstrained fn load_empty_returns_none() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - let value: Option = load::(SLOT); - assert_eq(value, Option::none()); - }); -} - -pub(crate) unconstrained fn store_overwrites() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - store::(SLOT, 1); - store::(SLOT, 2); - assert_eq(load::(SLOT), Option::some(2)); - }); -} - -pub(crate) unconstrained fn delete_removes_value() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - store::(SLOT, 42); - delete::(SLOT); - let value: Option = load::(SLOT); - assert_eq(value, Option::none()); - }); -} - -pub(crate) unconstrained fn delete_empty_is_noop() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - delete::(SLOT); - let value: Option = load::(SLOT); - assert_eq(value, Option::none()); - }); -} - -pub(crate) unconstrained fn store_and_load_multi_field_type() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - let value = MockStruct::new(5, 6); - store::(SLOT, value); - assert_eq(load::(SLOT), Option::some(value)); - }); -} - -pub(crate) unconstrained fn stored_value_is_visible_as_a_length_one_array() -where - Oracle: ArrayOracles, -{ - let env = TestEnvironment::new(); - env.utility_context(|_| { - store::(SLOT, 42); - let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); -} - /// Returns the `should_fail_with` message for checks that are expected to fail, or `Option::none()` for checks that /// are expected to pass. comptime fn should_fail_message(name: Quoted) -> Option { From 1523392085e7fbe8c6b3a06e7884ea52c85eb907 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 12:53:10 +0000 Subject: [PATCH 22/26] commit missing file --- noir-projects/aztec-nr/aztec/src/lib.nr | 1 - 1 file changed, 1 deletion(-) diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 1a164ce8a1e2..387f820d22da 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -40,7 +40,6 @@ pub mod oracle; pub mod state_vars; pub mod capsules; pub mod unconstrained_array; -pub mod unconstrained_value; pub mod ephemeral; pub mod transient; pub mod event; From 788bd2f83ed69477002ab3f34c5a835cd53f564f Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 13:12:07 +0000 Subject: [PATCH 23/26] refactor test helpers --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 2 +- .../aztec-nr/aztec/src/transient/mod.nr | 2 +- .../aztec/src/unconstrained_array/mod.nr | 1 + .../src/unconstrained_array/test_helpers.nr | 53 ++++--------------- 4 files changed, 14 insertions(+), 44 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 87ce7cda2d39..8a666c3366da 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -85,5 +85,5 @@ impl Deserialize for UnconstrainedArray { } } -#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })] +#[crate::unconstrained_array::test_suite::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })] mod test {} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 57cb24091f4e..688e279e7c03 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -110,7 +110,7 @@ impl ArrayOracles for TransientOracles { } } -#[crate::unconstrained_array::test_helpers::unconstrained_array_tests(quote { crate::transient::TransientOracles })] +#[crate::unconstrained_array::test_suite::unconstrained_array_tests(quote { crate::transient::TransientOracles })] mod test {} mod value_test { diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index af5da1e47893..622688c76b82 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -1,4 +1,5 @@ pub(crate) mod test_helpers; +pub(crate) mod test_suite; use crate::oracle::random::random; use crate::protocol::traits::{Deserialize, Serialize}; diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr index 680e26178fc7..cc7c96e753fd 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -1,4 +1,12 @@ //! Shared test suite for [`UnconstrainedArray`] backends. +//! +//! Every function in this module must be an unconstrained test helper, generic over the [`ArrayOracles`] backend. +//! The [`unconstrained_array_tests`](crate::unconstrained_array::test_suite::unconstrained_array_tests) macro turns +//! each helper into a `#[test]` for every backend. +//! +//! To add a test, write a new unconstrained helper here. If it is expected to fail, mark it with +//! [`should_fail_test`](crate::unconstrained_array::test_suite::should_fail_test) and the expected message; otherwise +//! the generated test expects it to pass. use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; @@ -18,6 +26,7 @@ where }); } +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "out of bounds" })] pub(crate) unconstrained fn should_fail_empty_array_read() where Oracle: ArrayOracles, @@ -29,6 +38,7 @@ where }); } +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "is empty" })] pub(crate) unconstrained fn should_fail_empty_array_pop() where Oracle: ArrayOracles, @@ -54,6 +64,7 @@ where }); } +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "out of bounds" })] pub(crate) unconstrained fn should_fail_read_past_len() where Oracle: ArrayOracles, @@ -633,45 +644,3 @@ where assert_eq(fresh.len(), 0); }); } - -/// Returns the `should_fail_with` message for checks that are expected to fail, or `Option::none()` for checks that -/// are expected to pass. -comptime fn should_fail_message(name: Quoted) -> Option { - if name == quote { should_fail_empty_array_read } { - Option::some(quote { "out of bounds" }) - } else if name == quote { should_fail_empty_array_pop } { - Option::some(quote { "is empty" }) - } else if name == quote { should_fail_read_past_len } { - Option::some(quote { "out of bounds" }) - } else { - Option::none() - } -} - -/// Generates the shared [`UnconstrainedArray`] test suite against the given backend. Apply to a test module, passing -/// the -/// backend's oracle as a quoted path: `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })]`. -pub(crate) comptime fn unconstrained_array_tests(_m: Module, oracle: Quoted) -> Quoted { - let helpers = quote { crate::unconstrained_array::test_helpers }.as_module().unwrap(); - let mut tests = quote {}; - for f in helpers.functions() { - if f.is_unconstrained() { - let name = f.name(); - let fail = should_fail_message(name); - let attr = if fail.is_some() { - let msg = fail.unwrap(); - quote { #[test(should_fail_with = $msg)] } - } else { - quote { #[test] } - }; - tests = quote { - $tests - $attr - unconstrained fn $name() { - crate::unconstrained_array::test_helpers::$name::<$oracle>(); - } - }; - } - } - tests -} From 51d06cb361ff1985f31e83da38674264efacb7f8 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 13:15:51 +0000 Subject: [PATCH 24/26] another one --- .../src/unconstrained_array/test_suite.nr | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr new file mode 100644 index 000000000000..b4f1eea93f87 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr @@ -0,0 +1,46 @@ +//! Generates a shared [`UnconstrainedArray`](crate::unconstrained_array::UnconstrainedArray) +//! test suite from the helpers in [`test_helpers`](crate::unconstrained_array::test_helpers). + +use crate::utils::cmap::CHashMap; + +/// Maps test helpers marked with [`should_fail_test`] to their expected failure message. +pub(crate) comptime mut global EXPECTED_FAILURES: CHashMap = CHashMap::new(); + +/// Marks a test helper as expected to fail with the given message, passed as a quoted string literal: +/// `#[should_fail_test(quote { "out of bounds" })]`. The tests generated from the helper get +/// `#[test(should_fail_with = ...)]` instead of plain `#[test]`. +pub(crate) comptime fn should_fail_test(f: FunctionDefinition, message: Quoted) { + EXPECTED_FAILURES.insert(f, message); +} + +/// Generates a shared [`UnconstrainedArray`](crate::unconstrained_array::UnconstrainedArray) test suite against the +/// given backend. Apply to a test module, passing the backend's oracle as a quoted path: +/// `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })]`. +pub(crate) comptime fn unconstrained_array_tests(_m: Module, oracle: Quoted) -> Quoted { + let helpers = quote { crate::unconstrained_array::test_helpers }.as_module().unwrap(); + let mut tests = quote {}; + for f in helpers.functions() { + let name = f.name(); + if f.is_unconstrained() { + let fail = EXPECTED_FAILURES.get(f); + let attr = if fail.is_some() { + let msg = fail.unwrap(); + quote { #[test(should_fail_with = $msg)] } + } else { + quote { #[test] } + }; + tests = quote { + $tests + $attr + unconstrained fn $name() { + crate::unconstrained_array::test_helpers::$name::<$oracle>(); + } + }; + } else { + panic( + f"unexpected non-unconstrained function `{name}` in test_helpers: every test helper must be unconstrained", + ) + } + } + tests +} From 261faceea0f46931bc911853ae81051152e32672 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 13:31:34 +0000 Subject: [PATCH 25/26] tighten access --- noir-projects/aztec-nr/aztec/src/oracle/mod.nr | 4 ++-- noir-projects/aztec-nr/aztec/src/transient/mod.nr | 2 +- .../aztec-nr/aztec/src/unconstrained_array/mod.nr | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 3ff645c72d56..6d677346a72f 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -7,8 +7,8 @@ pub mod block_header; pub mod call_private_function; pub mod call_utility_function; pub mod capsules; -pub mod ephemeral_oracles; -pub mod transient_oracles; +pub(crate) mod ephemeral_oracles; +pub(crate) mod transient_oracles; pub mod contract_sync; pub mod public_call; pub mod tx_phase; diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index 688e279e7c03..569f0731e252 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -49,7 +49,7 @@ impl TransientValue { Self { slot } } - /// Stores a value at the slot, overwriting any value previously stored there. + /// Stores a value at the given slot. pub unconstrained fn store(self, value: T) where T: Serialize, diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index 622688c76b82..01f0d9e88014 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -9,10 +9,8 @@ use crate::protocol::traits::{Deserialize, Serialize}; /// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the /// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one /// contract call frame, while [`TransientArray`](crate::transient::TransientArray) arrays are shared across all frames -/// of the same contract within one top-level PXE call). Implementations are thin wrappers around `#[oracle]` -/// declarations, so method dispatch is resolved at compile time via monomorphization and each array type emits its own -/// foreign calls. -pub trait ArrayOracles { +/// of the same contract within one top-level PXE call). +pub(crate) trait ArrayOracles { /// Returns the number of elements in the array at `slot`. unconstrained fn len_oracle(slot: Field) -> u32; @@ -47,7 +45,7 @@ pub trait ArrayOracles { /// frame) or [`TransientArray`](crate::transient::TransientArray) (shared across all frames of the same contract /// within one top-level PXE call). pub struct UnconstrainedArray { - pub slot: Field, + pub(crate) slot: Field, } impl UnconstrainedArray From 8e9e6eea29a0a68d23d8d471bda4cb76fe9cf6b1 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 11 Jun 2026 13:36:15 +0000 Subject: [PATCH 26/26] remove excess comments --- .../aztec/src/unconstrained_array/mod.nr | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr index 01f0d9e88014..139d80bb42da 100644 --- a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -147,10 +147,6 @@ where } /// Applies `f` to every element and collects the results into a fresh array. - /// - /// Reads each element in order, transforms it with `f`, and pushes the result onto a new array at a freshly - /// allocated slot (see [`UnconstrainedArray::empty`]). The source array is left unchanged, and the result is - /// isolated from it, so `map` can be chained. pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> UnconstrainedArray where T: Deserialize, @@ -165,10 +161,6 @@ where } /// Collects every element satisfying the predicate `f` into a fresh array. - /// - /// Reads each element in order, keeps those for which `f` returns `true`, and pushes them onto a new array at a - /// freshly allocated slot (see [`UnconstrainedArray::empty`]). Relative order is preserved. The source array is - /// left unchanged, and the result is isolated from it, so `filter` can be chained. pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self where T: Serialize + Deserialize, @@ -185,10 +177,6 @@ where } /// Returns `true` if at least one element satisfies the predicate `f`. - /// - /// Matches the `any` combinator on Noir's [`array`](std::array), slice, and `BoundedVec` collections. Defined in - /// terms of [`UnconstrainedArray::filter`]: it keeps the matching elements and checks whether any survived. This is - /// not short-circuiting — every element is tested even after the first match. pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool where T: Serialize + Deserialize, @@ -197,10 +185,6 @@ where } /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). - /// - /// Matches the `all` combinator on Noir's [`array`](std::array) and slice collections. Defined in terms of - /// [`UnconstrainedArray::filter`]: every element matches exactly when filtering keeps all of them. This is not - /// short-circuiting — every element is tested even after the first failure. pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool where T: Serialize + Deserialize,