[2/4] #[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)#1159
[2/4] #[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)#1159greenhat wants to merge 10 commits into
#[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)#1159Conversation
f7cec18 to
6998d42
Compare
#[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)#[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)
#[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)#[account(...)] macro generates the native account wrapper (instead of hardcoded Account type)
6998d42 to
367aefc
Compare
|
@bitwalker Rebased and ready. |
bitwalker
left a comment
There was a problem hiding this comment.
Looks good! I have a few notes/questions, but nothing major I think
|
|
||
| /// Method names of the `ActiveAccount` trait (`sdk/base-sys/src/bindings/active_account.rs`). | ||
| /// | ||
| /// Dependency functions mapping to these names are rejected: the generated inherent method would |
There was a problem hiding this comment.
Is this actually true? I believe Rust requires disambiguating with turbofish syntax in such cases (since it cannot know which trait method you are actually intending to call when both are in scope).
You do mention inherent method though, but IIRC the plan was to have component methods defined in the trait implementation, so it's a bit unclear to me why we're generating inherent methods at all vs trait implementations. Account methods like get_id I'd expect to be provided by implementing some kind of Account trait, rather than as inherent methods of the account struct itself. That would also avoid the issue with method resolution.
None of that prevents someone from adding something like get_id or whatever to their account/component struct, but if all of our stuff is trait-implemented, then they'd still be required to disambiguate with turbofish syntax when the conflicting trait is in scope (which would basically always be the case I think).
There was a problem hiding this comment.
You do mention inherent method though, but IIRC the plan was to have component methods defined in the trait implementation, so it's a bit unclear to me why we're generating inherent methods at all vs trait implementations. Account methods like
get_idI'd expect to be provided by implementing some kind ofAccounttrait, rather than as inherent methods of the account struct itself. That would also avoid the issue with method resolution.
The inherent method is defined to route inter-component vs FPI calls on the struct under the #[account(...)] macro on the callee side. It depends on the optional account id (for FPI) field defined in this struct. I don't think we planned to implement it as a trait. Here is an example of such a wrapper for the BasicWallet::receive_asset:
pub fn receive_asset(&self, asset: ::miden::Asset) -> () {
match self.foreign_account_id {
::core::option::Option::Some(__miden_foreign_account_id) => {
__miden_foreign_account_wallet::miden::basic_wallet::miden_basic_wallet::fpi_receive_asset(
__miden_foreign_account_id.prefix,
__miden_foreign_account_id.suffix,
::miden::Word::new([
{
const VALUE: u64 = 14498277730569837599u64 as u64;
::miden_stdlib_sys::Felt::new(VALUE).unwrap()
},
{
const VALUE: u64 = 11350922803923988613u64 as u64;
::miden_stdlib_sys::Felt::new(VALUE).unwrap()
},
{
const VALUE: u64 = 3366787023335181781u64 as u64;
::miden_stdlib_sys::Felt::new(VALUE).unwrap()
},
{
const VALUE: u64 = 15705799660361837834u64 as u64;
::miden_stdlib_sys::Felt::new(VALUE).unwrap()
},
]),
asset,
)
}
::core::option::Option::None => {
__miden_foreign_account_wallet::miden::basic_wallet::miden_basic_wallet::receive_asset(
asset,
)
}
}
}| // User attributes are re-emitted as-is, so drop macro-added derives the user already | ||
| // requested (matched by the trait's final path segment) to avoid duplicate impls. | ||
| let user_derives = user_derived_names(attrs); | ||
| let added_derives = ["Clone", "Copy", "Debug", "Default"] |
There was a problem hiding this comment.
Maybe we should just never emit any of these, and just leave it up to the user - that simplifies things here, and only requires adding one line to account component declarations that need any of these trait impls (which we're seemingly anticipating that they'd be adding anyway).
There was a problem hiding this comment.
We need Default for AccountWrapper::native() to work since AccountWrapper: Default and native() is just Self::default(). I removed the rest in ac7af62
| /// transaction's native account rather than the foreign one. | ||
| #[doc(hidden)] | ||
| #[inline] | ||
| fn __assert_active_account(&self) {} |
There was a problem hiding this comment.
I would make this method required, rather than providing a default no-op implementation, since presumably we'll have to emit impl ActiveAccount for all #[account(...)] structs anyway.
There was a problem hiding this comment.
There's exactly one macro producing dual native/foreign types (#[account]), and it always emits the override (panic! if the foreign account is set). If we make it required, it'd move the no-op implementation into boilerplate, and we'd have to impl ActiveAccount for T { fn __assert_active_account(&self) {} } on each impl.
| message = "`{Self}` is not an account wrapper generated by `#[account(...)]`", | ||
| note = "define a struct with `#[account(...)]` and use it as the entrypoint account parameter" | ||
| )] | ||
| pub trait AccountWrapper: Default { |
There was a problem hiding this comment.
Doesn't this also need ActiveAccount as a supertrait as well? Otherwise you could define an impl of AccountWrapper for something like i32, which we probably don't want to treat as valid.
| pub trait AccountWrapper: Default { | ||
| /// Creates a binding to the transaction's native (active) account. | ||
| #[inline(always)] | ||
| fn native() -> Self { |
There was a problem hiding this comment.
I'm a bit confused about the mixture of native vs active terminology - the latter makes sense to me, the former does not (an account being "native" really conveys nothing meaningful to me at all - "active"/"current" is way more intuitive IMO).
|
Thank you! I addressed all the review notes. Please do another round. |
Closes #1157. Note and tx-script crates previously received an implicitly generated `crate::bindings::Account` struct for the native account, while `#[account(...)]` produced a separate FPI-only caller wrapper. The two APIs were redundant, and the generated type was invisible in user code. `#[account(...)]` now generates a single struct serving both roles: the `#[note]`/`#[tx_script]` entrypoints inject it via `Default::default()`, which binds to the transaction's native (active) account, while `new(account_id)` binds to a foreign account. Each generated dependency method dispatches per call: native imports for the active account, or `execute_foreign_procedure` for a foreign one. The macro also implements `ActiveAccount` for the struct with a guard that panics when built-in active-account operations (`get_id`, vault queries, etc.) are invoked on a foreign binding, since those always target the native account. BREAKING: the auto-generated `crate::bindings::Account` is removed. Entrypoints accept a `&T`/`&mut T` reference to any `#[account(...)]` type instead; the old `Account` type name produces a migration error pointing at the new attribute. `new` is the reserved constructor name, so dependency WIT functions must not be named `new`.
The unified `#[account(...)]` struct exposes both the generated dependency methods and the built-in `ActiveAccount` trait methods. A dependency exporting e.g. `get-id` would generate an inherent `get_id` method that silently wins Rust method resolution over the trait method, changing what `account.get_id()` means and bypassing the foreign-binding guard. Extend the reserved-name validation in `method_ident` to all `ActiveAccount` method names so such dependencies are rejected at macro expansion with an actionable error, consistent with the existing `new` constructor check and the cross-package collision policy.
The macro re-emits the user's attributes on the generated struct and then unconditionally adds `#[derive(Clone, Copy, Debug, Default)]`, so a marker like `#[derive(Clone)] #[account(...)] struct Wallet;` expanded to two `Clone` impls and failed with a conflicting-implementations error pointing at generated code. Keep user attributes verbatim and filter the macro-added derive list against the traits the user already derives (matched by the final path segment), emitting only the missing ones.
…rams The note/tx-script macros recognized the optional account parameter purely by shape (any non-`Word` reference to a path type) and instantiated it via `Default::default()`, so a signature like `account: &mut NotAnAccount` was accepted and the entrypoint received a defaulted dummy value instead of an account binding. Introduce the `AccountWrapper` marker trait, implemented by the `#[account(...)]` macro alongside the `ActiveAccount` guard. The generated entrypoint glue now instantiates the parameter through `AccountWrapper::native()`, so types not generated by `#[account(...)]` fail with a clear trait-bound error instead of compiling silently.
…gnostic The note/tx-script migration error rejected any account parameter whose type was named `Account`, including a perfectly valid `#[account(basic_wallet)] struct Account;` — the most natural migration target for code that used the removed generated type. The check could only ever see the bare name, not what it resolved to, so it was unfixable at macro-expansion time. Drop the name-based check and let the `AccountWrapper` bound decide: a `#[diagnostic::on_unimplemented]` attribute on the trait now points any non-wrapper parameter type at `#[account(...)]` with an actionable note. Code still importing the removed `crate::bindings::Account` keeps failing at the dead import itself.
…ontract Two comments around the tx-script injected parameter still described instantiation via `Default::default()`, while the expansion now goes through `AccountWrapper::native()`. Also reword the `AccountWrapper` doc: it claimed only `#[account(...)]`-generated types are accepted by entrypoints, but the trait is publicly implementable rather than a sealed capability boundary. State the actual contract — the parameter must implement the trait, which the macro does automatically.
The unit tests only prove the note/tx-script parsers no longer reject the name `Account`; nothing exercised the full pipeline for a wrapper using the name of the removed auto-generated type — the natural choice for migrating code. Rename the native caller wrapper in the account-to-account FPI test to `Account` so parsing, macro expansion, and VM execution of that name are covered by an existing test.
The `#[account(...)]` macro auto-derived `Clone`, `Copy`, `Debug`, and `Default` on the wrapper struct, then filtered those against the derives the user already wrote to avoid conflicting impls. Only `Default` is actually needed: the note/tx-script entrypoint account is built through `AccountWrapper::native()` (= `Self::default()`), and the wrapper is a private `Option<AccountId>` that no generated code clones, copies, or formats. Derive only `Default` and leave the conveniences to the user, who can add them in one line when wanted. The duplicate-derive filter collapses to a single guard that drops the macro `Default` when the user already requests it.
`AccountWrapper` only required `Default`, so any `Default` type (`i32`, for instance) could implement it and be accepted as a `#[note]` or `#[tx_script]` entrypoint account parameter. Such a type has none of the active-account operations the entrypoint body relies on, so the mistake surfaced only later as a confusing method-resolution error, or not at all. Require `ActiveAccount` as a supertrait so the bound expresses what an account wrapper is: a type usable as the transaction's native account. The `#[account(...)]` macro already emits both impls, so generated wrappers are unaffected; only unrelated `Default` types are now rejected at the impl site.
The constructor builds the binding for the transaction's own account — the one passed to a `#[note]`/`#[tx_script]` entrypoint — whose methods dispatch to the regular (non-FPI) imports. Naming it `native` leaned on a different protocol axis (`native_account`, the write namespace) and read as a synonym for `active`/`current`, which it is not; the two only coincide for the entrypoint account because no foreign call is in scope. Rename it to `active`, matching the `ActiveAccount` supertrait the binding carries and pairing cleanly with `new(id)` for the foreign case. Drop the misleading "native (active)" wording from the surrounding docs and the guard message so the entrypoint account is described consistently as the active account, as opposed to a foreign one.
f905a16 to
6286525
Compare
Close #1157
This PR is stacked on #1113 and should be merged after it.
Unifies the
#[account(...)]wrapper so a single user-declared type represents both the transaction's native (active) account and a foreign (FPI) account, replacing the auto-generatedcrate::bindings::Accountstruct.#[account(...)]wrapper methods now dispatch at runtime: a wrapper created withnew(account_id)(renamed fromfrom_account) routes calls throughexecute_foreign_procedure, while the wrapper passed to a#[note]/#[tx_script]entrypoint invokes the active account's native imports directly.bindings::Accountstruct is removed; entrypoint account parameters must reference an#[account(...)]type. This is enforced via theAccountWrappermarker trait rather than by type name, so a wrapper may itself be namedAccount, and non-wrapper types fail with a guiding trait-bound diagnostic.ActiveAccountbuilt-ins (e.g.get_id) are rejected at macro expansion to prevent silent method-resolution changes.