feat: richer EphemeralArray and TransientArray APIs#23982
Conversation
Adds empty(), map, filter, any, all, find and read_as to EphemeralArray (spun off from #23928) and mirrors them on TransientArray.
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.
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.
…d-transient-array-apis
| /// 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. |
There was a problem hiding this comment.
This is a bit of an odd comment, I'd remove it.
| impl<T, Oracle> OracleArray<T, Oracle> | ||
| where | ||
| Oracle: ArrayOracle, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I'm ok with using the random slot to be honest
| /// 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Why do we we need this family of fns? It feels odd to have these specific constructs for arrays of len 1.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| let helpers = quote { crate::oracle_array::test_helpers }.as_module().unwrap(); | ||
| let mut tests = quote {}; | ||
| for f in helpers.functions() { | ||
| if f.is_unconstrained() { |
There was a problem hiding this comment.
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.
| 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() | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Will see what's possible!
| Self::at(slot).clear() | ||
| impl ArrayOracle for EphemeralOracle { | ||
| unconstrained fn len_oracle(slot: Field) -> u32 { | ||
| ephemeral::len_oracle(slot) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Introduces new functions and combinators for EphemeralArray and TransientArray:
empty: instantiates an empty array on a random slotmapfilterany: (ie:filter(a).len() > 0)all: (ie:filter(a).len() == a.len())find: (ie: filter and return first match, if exists)Introduces an
OracleArrayabstraction so that implementations only differ on the set of oracles they use.It also includes a generic module of tests that each
OracleArrayimplementation can then simply macro expand.Closes F-685