Skip to content

feat: richer EphemeralArray and TransientArray APIs#23982

Open
mverzilli wants to merge 9 commits into
merge-train/fairies-v5from
martin/richer-ephemeral-and-transient-array-apis
Open

feat: richer EphemeralArray and TransientArray APIs#23982
mverzilli wants to merge 9 commits into
merge-train/fairies-v5from
martin/richer-ephemeral-and-transient-array-apis

Conversation

@mverzilli

@mverzilli mverzilli commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Introduces new functions and combinators for EphemeralArray and TransientArray:

  • empty: instantiates an empty array on a random slot
  • map
  • filter
  • any: (ie: filter(a).len() > 0)
  • all: (ie: filter(a).len() == a.len())
  • find: (ie: filter and return first match, if exists)

Introduces an OracleArray abstraction so that implementations only differ on the set of oracles they use.
It also includes a generic module of tests that each OracleArray implementation can then simply macro expand.

Closes F-685

Adds empty(), map, filter, any, all, find and read_as to EphemeralArray
(spun off from #23928) and mirrors them on TransientArray.
@mverzilli mverzilli requested a review from nventuro as a code owner June 10, 2026 08:43
Both types are now aliases of a single OracleArray<T, Oracle> 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.
Comment thread noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr Outdated
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.
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.
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.
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.
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.
@mverzilli mverzilli requested a review from nchamo June 10, 2026 11:37
nchamo
nchamo previously approved these changes Jun 10, 2026

@nchamo nchamo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work!

Comment thread noir-projects/aztec-nr/aztec/src/oracle_array/mod.nr
Comment thread noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr
@nventuro nventuro dismissed nchamo’s stale review June 10, 2026 13:41

still reviewing myself

Comment on lines +12 to +14
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of an odd comment, I'd remove it.

Comment on lines +49 to +51
impl<T, Oracle> OracleArray<T, Oracle>
where
Oracle: ArrayOracle,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OracleArray type implementing the ArrayOracle trait sounds very confusing, this will surely lead to trouble. The trait is about oracles that implement array-like functionality, whereas the actual type is not about oracles but rather unconstrained ephemeral/transient/persistent arrays, which happen to be implemented via oracles and databases/memory.

let _ = Oracle::push_oracle(self.slot, serialized);
}

/// Removes and returns the last element. Panics if the array is empty.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs here say that this panics, but it doesn't - it just calls the trait fn. The trait fn should therefore be required (documented) to panic. Many such cases.


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had some places where we used a "conventional" slot and needed to make sure that it was clean. In those cases we had array.at(...).clear(), so this is just a convenience for that. I should check if we still need this pattern, with some of @nchamo's recent improvements it might be the case that this is no longer needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with using the random slot to be honest

Comment on lines +132 to +136
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe get rid of this? IIRC it originated back when each operation was a db transaction, and I wanted to minimize how many of those we had. But how that we have the memory staging area this is perhaps pointless, and we may be able to remove this footgun.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely this was a cargo cult from CapsuleArray, will look into it

/// 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<T, Oracle>(slot: Field, value: T)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we we need this family of fns? It feels odd to have these specific constructs for arrays of len 1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I implemented TransientArray, Álvaro asked for a simple store/load mechanism, and I didn't feel it was worth adding more oracles to provide it.

I ACK it is kind of weird that this is the module though, I could just make it a separate module. Naming it is a bit of a headache though.

This is analogous to Capsule and CapsuleArray, but we can't just call this Transient or Ephemeral since those are adjectives. Maybe TransientCell and EphemeralCell, but then again, more jargon.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think this might make more sense as a type with ::new(), ::store(T) and ::read() -> Option(T) - it currently looks too much like an array and that could cause confusion. It'd also neatly separate that as a concept. I'd also not mention at all that under the hood these are arrays.

Comment thread noir-projects/aztec-nr/aztec/src/oracle_array/test_helpers.nr
let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap();
let mut tests = quote {};
for f in helpers.functions() {
if f.is_unconstrained() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else panic

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm I'm now thinking that maybe the reason why this if has to be here is to avoid catching this very fn (or the should_fail_message fn). We should filter those out explicitly I think? Or use attributes.

Comment on lines +752 to +763
comptime fn should_fail_message(name: Quoted) -> Option<Quoted> {
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 if name == quote { should_fail_read_as_rejects_length_mismatch } {
Option::some(quote { "length mismatch" })
} else {
Option::none()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module deserves a comment, particularly relating to this function. We should explain at the top a) how this module works and how to write a new test, and b) specifically that if we want to write a failing test, its name needs to be added to this function. Otherwise what one might think is a failing test would actually not be.

Now that I think of it, this is a bit of a brittle mechanism. What if we used a custom marker attribute e.g. should_fail_test or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will see what's possible!

Self::at(slot).clear()
impl ArrayOracle for EphemeralOracle {
unconstrained fn len_oracle(slot: Field) -> u32 {
ephemeral::len_oracle(slot)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd revisit the naming here, it's very confusing to be referring to an ephemeral module from inside (another) ephemeral module.

This to some extent originates from the so-far unchallenged decision of having a single oracle module with all oracles there, instead of having them be spread out e.g. ephemeral::oracles. It does make it easier to find all oracles, but it leads to these things - I'm not convinced the tradeoff is great.

At any rate, if we don't want to revisit that then what I'd do is import the oracle mod, not oracle::ephemeral, and change these callsites to oracle::ephemeral::len_oracle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does sound like a good idea to invert the way, I agree it causes weirdness. I will try to make things locally better here but happy to address the general oracle layout on its own PR. It might be perceived a bit as a kind of annotying change with little ROI though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agred, hence why I never pushed on that. I don't think any externals rely on those though, it's just a matter of tidyness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants