@layerzerolabs/framework-solana-anchor-trait 0.2.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/Cargo.toml +18 -0
  2. package/examples/shapes/Anchor.toml +19 -0
  3. package/examples/shapes/macros/Cargo.toml +17 -0
  4. package/examples/shapes/macros/src/instructions/compute_area.rs +50 -0
  5. package/examples/shapes/macros/src/instructions/compute_perimeter.rs +50 -0
  6. package/examples/shapes/macros/src/instructions/get_dimension_count.rs +44 -0
  7. package/examples/shapes/macros/src/instructions/get_name.rs +18 -0
  8. package/examples/shapes/macros/src/instructions/initialize.rs +18 -0
  9. package/examples/shapes/macros/src/instructions/mod.rs +26 -0
  10. package/examples/shapes/macros/src/lib.rs +50 -0
  11. package/examples/shapes/macros/src/shapes.rs +56 -0
  12. package/examples/shapes/programs/ellipse/Cargo.toml +30 -0
  13. package/examples/shapes/programs/ellipse/src/lib.rs +88 -0
  14. package/examples/shapes/programs/ellipse/src/state.rs +9 -0
  15. package/examples/shapes/programs/triangle/Cargo.toml +30 -0
  16. package/examples/shapes/programs/triangle/src/lib.rs +83 -0
  17. package/examples/shapes/programs/triangle/src/state.rs +9 -0
  18. package/package.json +36 -0
  19. package/rust-toolchain.toml +3 -0
  20. package/rustfmt.toml +23 -0
  21. package/src/assembly.rs +63 -0
  22. package/src/generation.rs +226 -0
  23. package/src/lib.rs +126 -0
  24. package/src/overrides.rs +289 -0
  25. package/tests/assembly.rs +339 -0
  26. package/tests/common/mod.rs +206 -0
  27. package/tests/declare_instructions.rs +206 -0
  28. package/tests/generation.rs +358 -0
  29. package/tests/override.rs +414 -0
  30. package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +35 -0
  31. package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +47 -0
  32. package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +42 -0
  33. package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +57 -0
  34. package/tests/snapshots.rs +139 -0
package/rustfmt.toml ADDED
@@ -0,0 +1,23 @@
1
+ # Basic
2
+ hard_tabs = false
3
+ max_width = 100
4
+ use_small_heuristics = "Max"
5
+ # Imports
6
+ imports_granularity = "Preserve"
7
+ reorder_imports = true
8
+ # Consistency
9
+ newline_style = "Unix"
10
+ # Format comments
11
+ comment_width = 100
12
+ wrap_comments = true
13
+ # Misc
14
+ chain_width = 80
15
+ spaces_around_ranges = false
16
+ binop_separator = "Front"
17
+ reorder_impl_items = false
18
+ match_arm_leading_pipes = "Preserve"
19
+ match_arm_blocks = true
20
+ match_block_trailing_comma = true
21
+ trailing_comma = "Vertical"
22
+ trailing_semicolon = true
23
+ use_field_init_shorthand = true
@@ -0,0 +1,63 @@
1
+ //! Final assembly of generated code into the macro output.
2
+
3
+ use proc_macro2::TokenStream;
4
+ use quote::quote;
5
+ use syn::ItemMod;
6
+
7
+ use crate::generation::GeneratedInstructions;
8
+ use crate::overrides::InstructionOverrides;
9
+ use crate::InstructionSet;
10
+
11
+ /// Input for the final assembly step.
12
+ pub struct Assembly<Ix: InstructionSet> {
13
+ /// Token stream emitted before the module (e.g. default state structs, role events).
14
+ pub prelude: TokenStream,
15
+ /// The collected overrides (provides wrapper defs and item filtering).
16
+ pub overrides: InstructionOverrides<Ix>,
17
+ /// The generated instruction code.
18
+ pub instructions: GeneratedInstructions,
19
+ /// Extra attributes placed on the module (e.g. `#[program]`, `#[rbac(...)]`).
20
+ pub extra_mod_attrs: TokenStream,
21
+ /// The original module being transformed.
22
+ pub source_mod: ItemMod,
23
+ }
24
+
25
+ impl<Ix: InstructionSet> Assembly<Ix> {
26
+ /// Assembles the final macro output from all generated parts.
27
+ ///
28
+ /// ```text
29
+ /// prelude // domain-specific definitions (state types, events, etc.)
30
+ /// wrapper_defs // relocated override handler modules
31
+ /// default_impls // generated default accounts structs + handler impls
32
+ /// #[extra_mod_attrs] // e.g. #[program]
33
+ /// #[original_mod_attrs]
34
+ /// mod name {
35
+ /// filtered_items // original items minus relocated handlers
36
+ /// entrypoints // generated instruction entrypoints
37
+ /// }
38
+ /// ```
39
+ pub fn assemble(self) -> TokenStream {
40
+ let Self { prelude, overrides, instructions, extra_mod_attrs, source_mod } = self;
41
+
42
+ let wrapper_defs = &overrides.wrapper_defs;
43
+ let filtered_items = overrides.filtered_items(&source_mod);
44
+ let GeneratedInstructions { default_impls, entrypoints } = &instructions;
45
+
46
+ let ItemMod { attrs: mod_attrs, vis: mod_vis, ident: mod_ident, .. } = &source_mod;
47
+
48
+ quote! {
49
+ #prelude
50
+
51
+ #wrapper_defs
52
+ #default_impls
53
+
54
+ #(#mod_attrs)*
55
+ #extra_mod_attrs
56
+ #mod_vis mod #mod_ident {
57
+ use super::*;
58
+ #filtered_items
59
+ #entrypoints
60
+ }
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,226 @@
1
+ //! Instruction code generation with override level resolution.
2
+ //!
3
+ //! - **Default**: No override → emit default impl + default entrypoint.
4
+ //! - **Handler Override**: Override with default context type → emit default impl, entrypoint
5
+ //! dispatches to implementor's handler.
6
+ //! - **Full Override**: Override with custom context type → skip default impl, entrypoint
7
+ //! dispatches to implementor's handler with custom type.
8
+
9
+ use proc_macro2::{Ident, Span, TokenStream};
10
+ use quote::{quote, ToTokens};
11
+
12
+ use crate::overrides::{HandlerMeta, InstructionOverrides};
13
+ use crate::InstructionSet;
14
+
15
+ /// Descriptor for one instruction's code generation.
16
+ pub struct InstructionSpec<Ctx> {
17
+ /// Parameter type as a token stream (e.g. `Some(quote!(::lz_oapp::params::SetPeerParams))`).
18
+ /// Use `None` for instructions with no params.
19
+ pub params_type: Option<TokenStream>,
20
+ /// Return type as a token stream (e.g. `quote!(())` or `quote!([u8; 32])`).
21
+ pub return_type: TokenStream,
22
+ /// When `true`, the entrypoint passes `ctx` / `&ctx` (immutable).
23
+ /// When `false` , it passes `mut ctx` / `&mut ctx`.
24
+ pub is_view: bool,
25
+ /// - `Some(...)` — provided instruction (all three override levels valid).
26
+ /// - `None` — **required** instruction: the implementor must provide an override handler (only
27
+ /// Full Override is valid). Omitting the override produces a compile error.
28
+ pub default_impl: Option<DefaultImpl<Ctx>>,
29
+ }
30
+
31
+ /// Default impl generation for an instruction (accounts struct + handler).
32
+ ///
33
+ /// Groups the three fields that are only meaningful when an instruction
34
+ /// has a framework-provided default implementation.
35
+ pub struct DefaultImpl<Ctx> {
36
+ /// Name of the default accounts struct, e.g. `format_ident!("StdSetPeer")`.
37
+ /// Also used to detect Handler Override vs Full Override (string comparison with handler's
38
+ /// accounts type).
39
+ pub accounts_ident: Ident,
40
+ /// Name of the default handler method on the context struct, e.g. `format_ident!("apply")`.
41
+ /// At the Default level, the entrypoint calls `StdAccounts::<this>(...)`.
42
+ pub handler_ident: Ident,
43
+ /// Generates the default accounts struct + handler impl.
44
+ /// The impl must define a method matching [`handler_ident`](Self::handler_ident).
45
+ /// For mutable instructions: `fn(&mut Context<Self>, &Params) -> Result<R>`.
46
+ /// For view instructions: `fn(&Context<Self>, &Params) -> Result<R>`.
47
+ /// Omit the params argument when `params_type = None`.
48
+ pub generator: fn(&Ctx, &InstructionSpec<Ctx>) -> TokenStream,
49
+ }
50
+
51
+ impl<Ctx> InstructionSpec<Ctx> {
52
+ /// Returns `true` if the given context's accounts type matches the default accounts ident,
53
+ /// meaning the override handler reuses the default context struct (Handler Override).
54
+ fn uses_default(&self, accounts_type: &syn::Type) -> bool {
55
+ self.default_impl.as_ref().is_some_and(|default| {
56
+ matches!(accounts_type, syn::Type::Path(ty) if ty.path.is_ident(&default.accounts_ident))
57
+ })
58
+ }
59
+ }
60
+
61
+ /// Accumulated output from instruction generation.
62
+ #[derive(Default)]
63
+ pub struct GeneratedInstructions {
64
+ /// All default impl definitions (accounts struct + handler), placed outside the program
65
+ /// module.
66
+ pub default_impls: TokenStream,
67
+ /// All entrypoint function definitions (placed inside the program module).
68
+ pub entrypoints: TokenStream,
69
+ }
70
+
71
+ impl GeneratedInstructions {
72
+ fn extend(&mut self, other: Self) {
73
+ self.default_impls.extend(other.default_impls);
74
+ self.entrypoints.extend(other.entrypoints);
75
+ }
76
+ }
77
+
78
+ /// Resolved override: everything needed to assemble the entrypoint.
79
+ struct ResolvedOverride {
80
+ /// Tokens for the default accounts struct + handler impl (emitted outside the
81
+ /// program module). Empty when the override fully replaces the default context.
82
+ default_impl: TokenStream,
83
+ /// Function-level generics from the override handler (e.g. `<'info>`).
84
+ /// Empty at the Default level since the generated entrypoint needs no extra generics.
85
+ fn_generics: TokenStream,
86
+ /// The `ctx` parameter declaration: `ctx: Context<T>` or `mut ctx: Context<T>`.
87
+ ctx_param: TokenStream,
88
+ /// Body expression for instructions that take params: `handler(ctx, params)`.
89
+ call_with_params: TokenStream,
90
+ /// Body expression for parameter-less instructions: `handler(ctx)`.
91
+ call_no_params: TokenStream,
92
+ }
93
+
94
+ /// Generates code for all instructions and merges into a single output.
95
+ pub fn generate_instructions<Ix: InstructionSet>(
96
+ ctx: &Ix::Ctx,
97
+ overrides: &InstructionOverrides<Ix>,
98
+ ) -> syn::Result<GeneratedInstructions> {
99
+ let mut out = GeneratedInstructions::default();
100
+ for &ix in Ix::all() {
101
+ out.extend(generate_instruction(ix, ctx, overrides)?);
102
+ }
103
+ Ok(out)
104
+ }
105
+
106
+ /// Generates code for a single instruction with override level resolution.
107
+ ///
108
+ /// Returns `Err` if the instruction is required (`default_impl = None`)
109
+ /// and the implementor did not provide an override handler.
110
+ fn generate_instruction<Ix: InstructionSet>(
111
+ ix_name: Ix,
112
+ ctx: &Ix::Ctx,
113
+ overrides: &InstructionOverrides<Ix>,
114
+ ) -> syn::Result<GeneratedInstructions> {
115
+ let spec = ix_name.spec(ctx);
116
+ let handler = overrides.get(ix_name);
117
+
118
+ // Required instruction with no override → compile error.
119
+ if handler.is_none() && spec.default_impl.is_none() {
120
+ let cfg = &overrides.config;
121
+ return Err(syn::Error::new(
122
+ Span::call_site(),
123
+ format!(
124
+ "instruction `{}` has no default implementation; \
125
+ provide a handler with #[{}({})]",
126
+ ix_name.canonical_name(),
127
+ cfg.attr_name,
128
+ ix_name.canonical_name(),
129
+ ),
130
+ ));
131
+ }
132
+
133
+ let fn_ident = syn::Ident::new(ix_name.canonical_name(), Span::call_site());
134
+ let return_type = &spec.return_type;
135
+
136
+ let ResolvedOverride { default_impl, fn_generics, ctx_param, call_with_params, call_no_params } =
137
+ resolve_override(handler, &spec, ctx);
138
+
139
+ // Assemble the entrypoint function, with or without a params argument.
140
+ // Override handlers preserve function generics (e.g. lifetime params for remaining_accounts).
141
+ let entrypoints = match &spec.params_type {
142
+ Some(params_type) => quote! {
143
+ pub fn #fn_ident #fn_generics (#ctx_param, params: #params_type) -> Result<#return_type> {
144
+ #call_with_params
145
+ }
146
+ },
147
+ None => quote! {
148
+ pub fn #fn_ident #fn_generics (#ctx_param) -> Result<#return_type> {
149
+ #call_no_params
150
+ }
151
+ },
152
+ };
153
+
154
+ Ok(GeneratedInstructions { default_impls: default_impl, entrypoints })
155
+ }
156
+
157
+ /// Resolves which override level applies and returns the tokens for each part.
158
+ ///
159
+ /// - **Override present** → dispatch to the implementor's wrapper handler. Emit default impl only
160
+ /// if the handler reuses the default context (Handler Override).
161
+ /// - **No override** → dispatch to the default handler directly.
162
+ fn resolve_override<Ctx>(
163
+ override_handler: Option<&HandlerMeta>,
164
+ spec: &InstructionSpec<Ctx>,
165
+ ctx: &Ctx,
166
+ ) -> ResolvedOverride {
167
+ match (override_handler, &spec.default_impl) {
168
+ // Handler Override or Full Override — the implementor provided a custom handler.
169
+ //
170
+ // Handler Override (context type matches default): emit default impl so the
171
+ // override handler can delegate back to the default accounts struct.
172
+ // Full Override (different context type): skip default impl entirely; the
173
+ // implementor supplies their own accounts struct and handler logic.
174
+ //
175
+ // In both cases the entrypoint dispatches to the implementor's wrapper handler,
176
+ // passing `ctx` and `params` by value (ownership) so they can freely mutate,
177
+ // destructure, or forward them.
178
+ (Some(handler), spec_default) => {
179
+ let default_impl = if spec.uses_default(&handler.accounts_type) {
180
+ (spec_default.as_ref().unwrap().generator)(ctx, spec)
181
+ } else {
182
+ TokenStream::new()
183
+ };
184
+ let wrapper = &handler.wrapper_mod;
185
+ let handler_fn = &handler.fn_ident;
186
+ let context_type = &handler.context_type;
187
+ ResolvedOverride {
188
+ default_impl,
189
+ fn_generics: handler.fn_generics.to_token_stream(),
190
+ ctx_param: ctx_param_tokens(spec.is_view, quote!(#context_type)),
191
+ call_with_params: quote!(#wrapper::#handler_fn(ctx, params)),
192
+ call_no_params: quote!(#wrapper::#handler_fn(ctx)),
193
+ }
194
+ },
195
+ // Default level — no override provided.
196
+ //
197
+ // Emit the framework-provided default impl and call its handler directly.
198
+ // Unlike override handlers, the default handler receives `ctx` by reference
199
+ // (`&` for views, `&mut` for mutable) because it is a method on the default
200
+ // accounts struct, not a standalone function the implementor owns.
201
+ (None, Some(default)) => {
202
+ let accounts = &default.accounts_ident;
203
+ let handler_fn = &default.handler_ident;
204
+ let ref_token = if spec.is_view { quote!(&) } else { quote!(&mut) };
205
+ ResolvedOverride {
206
+ default_impl: (default.generator)(ctx, spec),
207
+ fn_generics: TokenStream::new(),
208
+ ctx_param: ctx_param_tokens(spec.is_view, quote!(Context<#accounts>)),
209
+ call_with_params: quote!(#accounts::#handler_fn(#ref_token ctx, &params)),
210
+ call_no_params: quote!(#accounts::#handler_fn(#ref_token ctx)),
211
+ }
212
+ },
213
+ // Unreachable: `generate_instruction` validates that at least one of
214
+ // override_handler or default_impl is present before calling this function.
215
+ (None, None) => unreachable!("validated above"),
216
+ }
217
+ }
218
+
219
+ /// Generates `ctx: T` or `mut ctx: T` depending on mutability.
220
+ fn ctx_param_tokens(is_view: bool, context_type: TokenStream) -> TokenStream {
221
+ if is_view {
222
+ quote!(ctx: #context_type)
223
+ } else {
224
+ quote!(mut ctx: #context_type)
225
+ }
226
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,126 @@
1
+ //! Trait-like instruction interfaces for Anchor Solana programs.
2
+ //!
3
+ //! Anchor provides no mechanism for enforcing instruction-level interfaces across programs.
4
+ //! This crate introduces **instruction traits**: compile-time enforceable instruction sets
5
+ //! with optional default implementations. Each instruction is either **required** (implementor
6
+ //! must provide a handler) or **provided** (a default implementation is available).
7
+ //!
8
+ //! Provided instructions support three override levels:
9
+ //!
10
+ //! - **Default**: No custom handler — framework emits accounts struct + entrypoint.
11
+ //! - **Handler Override**: Custom handler reuses the default accounts struct.
12
+ //! - **Full Override**: Custom handler with a custom accounts struct.
13
+ //!
14
+ //! See [`spec.md`](../spec.md) for the full specification.
15
+ //!
16
+ //! ## Integration steps for a new macro crate
17
+ //!
18
+ //! 1. Define a `CodegenContext` struct carrying your domain-specific types.
19
+ //! 2. Use [`declare_instructions!`] to declare instructions — this generates an enum implementing
20
+ //! [`InstructionSet`] with `all()`, `from_ident()`, `canonical_name()`, and `spec()`.
21
+ //! 3. Create an [`OverrideConfig`] with your override attribute and domain names.
22
+ //! 4. Wire up the macro entry point following the 4-phase pipeline:
23
+ //! - **Parse**: Attribute args, module, domain types (implementor's responsibility).
24
+ //! - **Collect**: Scan for override handlers with [`InstructionOverrides::collect`].
25
+ //! - **Generate**: Resolve override levels and emit code with [`generate_instructions`].
26
+ //! - **Assemble**: Combine fragments into the final output with [`Assembly::assemble`].
27
+
28
+ mod assembly;
29
+ mod generation;
30
+ mod overrides;
31
+
32
+ pub use assembly::*;
33
+ pub use generation::*;
34
+ pub use overrides::*;
35
+
36
+ /// Trait implemented by each macro crate's instruction name enum.
37
+ ///
38
+ /// Generated by [`declare_instructions!`] or implemented manually.
39
+ pub trait InstructionSet: Copy + Ord + 'static {
40
+ /// The domain-specific codegen context type (e.g. `CodegenContext`).
41
+ type Ctx;
42
+
43
+ /// All known instruction variants in deterministic order.
44
+ fn all() -> &'static [Self];
45
+ /// Parses an instruction name from an attribute argument ident.
46
+ fn from_ident(ident: &syn::Ident) -> Option<Self>;
47
+ /// The canonical function name for this instruction (e.g. `"set_peer"`).
48
+ fn canonical_name(&self) -> &'static str;
49
+ /// Returns the code generation spec for this instruction variant.
50
+ fn spec(self, ctx: &Self::Ctx) -> InstructionSpec<Self::Ctx>;
51
+ }
52
+
53
+ /// Declares all instructions in one place, generating a named enum
54
+ /// implementing [`InstructionSet`].
55
+ ///
56
+ /// Each `Variant => mod_name` pair maps an enum variant to the module
57
+ /// whose `spec()` function provides the codegen descriptor. Appending
58
+ /// `(ctx)` passes the [`CodegenContext`] reference to `spec(&ctx)`;
59
+ /// without it, `spec()` is called with no arguments.
60
+ ///
61
+ /// Callers are responsible for declaring their own `mod` items before
62
+ /// invoking this macro.
63
+ ///
64
+ /// # Example
65
+ ///
66
+ /// ```ignore
67
+ /// mod accept_default_admin;
68
+ /// mod grant_role;
69
+ /// mod renounce_role;
70
+ ///
71
+ /// declare_instructions! {
72
+ /// name = RbacInstructionSet;
73
+ /// context = CodegenContext;
74
+ ///
75
+ /// AcceptDefaultAdmin => accept_default_admin,
76
+ /// GrantRole => grant_role(ctx),
77
+ /// RenounceRole => renounce_role(ctx),
78
+ /// }
79
+ /// ```
80
+ #[macro_export]
81
+ macro_rules! declare_instructions {
82
+ // Helper: dispatch spec call — with ctx marker passes ctx, without calls spec()
83
+ (@spec_call $ctx:ident, $mod_name:ident ( $marker:ident )) => {
84
+ $mod_name::spec(&$ctx)
85
+ };
86
+ (@spec_call $ctx:ident, $mod_name:ident) => {
87
+ $mod_name::spec()
88
+ };
89
+
90
+ (name = $name:ident; context = $ctx:ident; $( $variant:ident => $mod_name:ident $( ( $ctx_marker:ident ) )? ),+ $(,)?) => {
91
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
92
+ pub(crate) enum $name {
93
+ $( $variant, )+
94
+ }
95
+
96
+ impl ::anchor_trait::InstructionSet for $name {
97
+ type Ctx = $ctx;
98
+
99
+ fn all() -> &'static [Self] {
100
+ static ALL: &[$name] = &[
101
+ $( $name::$variant, )+
102
+ ];
103
+ ALL
104
+ }
105
+
106
+ fn from_ident(ident: &syn::Ident) -> Option<Self> {
107
+ match ident.to_string().as_str() {
108
+ $( stringify!($mod_name) => Some(Self::$variant), )+
109
+ _ => None,
110
+ }
111
+ }
112
+
113
+ fn canonical_name(&self) -> &'static str {
114
+ match self {
115
+ $( Self::$variant => stringify!($mod_name), )+
116
+ }
117
+ }
118
+
119
+ fn spec(self, ctx: &$ctx) -> ::anchor_trait::InstructionSpec<$ctx> {
120
+ match self {
121
+ $( Self::$variant => $crate::declare_instructions!(@spec_call ctx, $mod_name $( ( $ctx_marker ) )? ), )+
122
+ }
123
+ }
124
+ }
125
+ };
126
+ }