@layerzerolabs/framework-solana-anchor-trait 0.2.62 → 0.2.64

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 (50) hide show
  1. package/Cargo.toml +3 -0
  2. package/README.md +316 -0
  3. package/examples/shapes/Cargo.lock +4126 -0
  4. package/examples/shapes/Cargo.toml +16 -0
  5. package/examples/shapes/macros/src/audit.rs +15 -0
  6. package/examples/shapes/macros/src/instructions/clear.rs +19 -0
  7. package/examples/shapes/macros/src/instructions/compute_area.rs +18 -20
  8. package/examples/shapes/macros/src/instructions/compute_perimeter.rs +18 -20
  9. package/examples/shapes/macros/src/instructions/get_dimension.rs +19 -0
  10. package/examples/shapes/macros/src/instructions/get_dimension_count.rs +19 -17
  11. package/examples/shapes/macros/src/instructions/mod.rs +8 -0
  12. package/examples/shapes/macros/src/instructions/reset.rs +50 -0
  13. package/examples/shapes/macros/src/instructions/scale.rs +52 -0
  14. package/examples/shapes/macros/src/lib.rs +14 -11
  15. package/examples/shapes/macros/src/shapes.rs +21 -26
  16. package/examples/shapes/macros/src/state_types.rs +14 -0
  17. package/examples/shapes/programs/ellipse/Cargo.toml +7 -0
  18. package/examples/shapes/programs/ellipse/src/lib.rs +69 -17
  19. package/examples/shapes/programs/ellipse/tests/ellipse/helpers.rs +110 -0
  20. package/examples/shapes/programs/ellipse/tests/ellipse/negative.rs +49 -0
  21. package/examples/shapes/programs/ellipse/tests/ellipse/positive.rs +231 -0
  22. package/examples/shapes/programs/ellipse/tests/ellipse.rs +23 -0
  23. package/examples/shapes/programs/triangle/Cargo.toml +7 -0
  24. package/examples/shapes/programs/triangle/src/lib.rs +69 -16
  25. package/examples/shapes/programs/triangle/tests/triangle/helpers.rs +123 -0
  26. package/examples/shapes/programs/triangle/tests/triangle/negative.rs +49 -0
  27. package/examples/shapes/programs/triangle/tests/triangle/positive.rs +225 -0
  28. package/examples/shapes/programs/triangle/tests/triangle.rs +23 -0
  29. package/examples/shapes/target/deploy/shapes_ellipse-keypair.json +1 -0
  30. package/examples/shapes/target/deploy/shapes_ellipse.so +0 -0
  31. package/examples/shapes/target/deploy/shapes_triangle-keypair.json +1 -0
  32. package/examples/shapes/target/deploy/shapes_triangle.so +0 -0
  33. package/package.json +20 -9
  34. package/src/assembly.rs +41 -6
  35. package/src/generation.rs +108 -131
  36. package/src/lib.rs +6 -4
  37. package/src/overrides.rs +56 -81
  38. package/tests/assembly.rs +154 -23
  39. package/tests/common/mod.rs +68 -14
  40. package/tests/generation.rs +137 -65
  41. package/tests/override.rs +93 -55
  42. package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +32 -5
  43. package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +35 -7
  44. package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +37 -7
  45. package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +40 -9
  46. package/tests/snapshots/snapshots__tests__snapshot_with_extend_macro.snap +82 -0
  47. package/tests/snapshots.rs +51 -18
  48. package/examples/shapes/Anchor.toml +0 -19
  49. package/examples/shapes/programs/ellipse/src/state.rs +0 -9
  50. package/examples/shapes/programs/triangle/src/state.rs +0 -9
package/src/generation.rs CHANGED
@@ -1,60 +1,97 @@
1
1
  //! Instruction code generation with override level resolution.
2
2
  //!
3
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.
4
+ //! - **Override**: Custom handler present → emit default impl (always available), entrypoint
5
+ //! dispatches to implementor's handler using whatever context type the handler declares.
8
6
 
9
7
  use proc_macro2::{Ident, Span, TokenStream};
10
8
  use quote::{quote, ToTokens};
9
+ use syn::{ItemImpl, ItemStruct};
11
10
 
12
- use crate::overrides::{HandlerMeta, InstructionOverrides};
11
+ use crate::overrides::InstructionOverrides;
13
12
  use crate::InstructionSet;
14
13
 
15
14
  /// Descriptor for one instruction's code generation.
16
15
  pub struct InstructionSpec<Ctx> {
17
- /// Parameter type as a token stream (e.g. `Some(quote!(::oapp::params::SetPeerParams))`).
16
+ /// Parameter type as a token stream (e.g. `Some(quote!(::lz_oapp::types::SetPeerParams))`).
18
17
  /// Use `None` for instructions with no params.
19
18
  pub params_type: Option<TokenStream>,
20
19
  /// Return type as a token stream (e.g. `quote!(())` or `quote!([u8; 32])`).
21
20
  pub return_type: TokenStream,
22
- /// When `true`, the entrypoint passes `ctx` / `&ctx` (immutable).
23
- /// When `false` , it passes `mut ctx` / `&mut ctx`.
21
+ /// When `true`, the entrypoint passes `&ctx` (immutable).
22
+ /// When `false`, it passes `&mut ctx`.
24
23
  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.
24
+ /// - `Some(...)` — provided instruction (Default and Override levels valid).
25
+ /// - `None` — **required** instruction: the implementor must provide an override handler.
26
+ /// Omitting the override produces a compile error.
28
27
  pub default_impl: Option<DefaultImpl<Ctx>>,
29
28
  }
30
29
 
31
30
  /// Default impl generation for an instruction (accounts struct + handler).
32
31
  ///
33
- /// Groups the three fields that are only meaningful when an instruction
32
+ /// Groups the fields that are only meaningful when an instruction
34
33
  /// has a framework-provided default implementation.
35
34
  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
35
  /// Name of the default handler method on the context struct, e.g. `format_ident!("apply")`.
41
36
  /// At the Default level, the entrypoint calls `StdAccounts::<this>(...)`.
42
37
  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,
38
+ /// Returns the complete accounts struct and its impl block as typed AST.
39
+ pub generator: fn(&Ctx, &InstructionSpec<Ctx>) -> DefaultAccounts,
49
40
  }
50
41
 
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
- })
42
+ /// Complete accounts struct and its handler impl block.
43
+ ///
44
+ /// The framework uses this template to produce two outputs:
45
+ /// 1. The concrete struct + impl — emitted as-is via [`to_definition`](Self::to_definition).
46
+ /// 2. A `macro_rules!` rule — built by decomposing the struct into attrs/fields and replacing the
47
+ /// name with `$name` via [`to_extend_rule`](Self::to_extend_rule).
48
+ pub struct DefaultAccounts {
49
+ /// Complete struct definition including attributes, name, generics, and fields.
50
+ pub struct_type: ItemStruct,
51
+ /// Complete impl block with handler methods. Must use `Context<Self>` so the
52
+ /// methods work regardless of the struct name.
53
+ pub impl_block: ItemImpl,
54
+ }
55
+
56
+ impl DefaultAccounts {
57
+ /// Returns the struct ident (e.g. `StdSetPeer`) — the single source of truth.
58
+ pub fn accounts_ident(&self) -> &Ident {
59
+ &self.struct_type.ident
60
+ }
61
+
62
+ /// Emits the struct and impl block.
63
+ pub fn to_definition(&self) -> TokenStream {
64
+ let s = &self.struct_type;
65
+ let i = &self.impl_block;
66
+ quote! { #s #i }
67
+ }
68
+
69
+ /// Assembles one `macro_rules!` rule for extending the default accounts.
70
+ ///
71
+ /// Decomposes the struct into attributes and fields, then re-emits with
72
+ /// `$name` replacing the struct ident and `$($extra)*` appended to the fields.
73
+ pub fn to_extend_rule(&self, instruction_name: &str) -> TokenStream {
74
+ let attrs = &self.struct_type.attrs;
75
+ let fields: Vec<_> = match &self.struct_type.fields {
76
+ syn::Fields::Named(f) => f.named.iter().collect(),
77
+ _ => unreachable!("accounts structs always have named fields"),
78
+ };
79
+ let impl_items = &self.impl_block.items;
80
+ let rule_ident = Ident::new(instruction_name, Span::call_site());
81
+
82
+ quote! {
83
+ (#rule_ident, $name:ident, { $($extra:tt)* }) => {
84
+ #(#attrs)*
85
+ pub struct $name<'info> {
86
+ #(#fields,)*
87
+ $($extra)*
88
+ }
89
+
90
+ impl $name<'_> {
91
+ #(#impl_items)*
92
+ }
93
+ };
94
+ }
58
95
  }
59
96
  }
60
97
 
@@ -66,31 +103,18 @@ pub struct GeneratedInstructions {
66
103
  pub default_impls: TokenStream,
67
104
  /// All entrypoint function definitions (placed inside the program module).
68
105
  pub entrypoints: TokenStream,
106
+ /// Collected `macro_rules!` rules for extending default accounts structs.
107
+ pub extend_rules: TokenStream,
69
108
  }
70
109
 
71
110
  impl GeneratedInstructions {
72
111
  fn extend(&mut self, other: Self) {
73
112
  self.default_impls.extend(other.default_impls);
74
113
  self.entrypoints.extend(other.entrypoints);
114
+ self.extend_rules.extend(other.extend_rules);
75
115
  }
76
116
  }
77
117
 
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
118
  /// Generates code for all instructions and merges into a single output.
95
119
  pub fn generate_instructions<Ix: InstructionSet>(
96
120
  ctx: &Ix::Ctx,
@@ -103,7 +127,7 @@ pub fn generate_instructions<Ix: InstructionSet>(
103
127
  Ok(out)
104
128
  }
105
129
 
106
- /// Generates code for a single instruction with override level resolution.
130
+ /// Generates code for a single instruction.
107
131
  ///
108
132
  /// Returns `Err` if the instruction is required (`default_impl = None`)
109
133
  /// and the implementor did not provide an override handler.
@@ -117,110 +141,63 @@ fn generate_instruction<Ix: InstructionSet>(
117
141
 
118
142
  // Required instruction with no override → compile error.
119
143
  if handler.is_none() && spec.default_impl.is_none() {
120
- let cfg = &overrides.config;
121
144
  return Err(syn::Error::new(
122
145
  Span::call_site(),
123
146
  format!(
124
147
  "instruction `{}` has no default implementation; \
125
- provide a handler with #[{}({})]",
148
+ provide a `fn {}(...)` annotated with #[{}]",
126
149
  ix_name.canonical_name(),
127
- cfg.attr_name,
128
150
  ix_name.canonical_name(),
151
+ &overrides.config.attr_name,
129
152
  ),
130
153
  ));
131
154
  }
132
155
 
133
- let fn_ident = syn::Ident::new(ix_name.canonical_name(), Span::call_site());
134
- let return_type = &spec.return_type;
156
+ // Always emit the default accounts when one exists (available even under override).
157
+ // Also collect the extend rule for the macro_rules! arm.
158
+ let default_accounts = spec.default_impl.as_ref().map(|d| (d.generator)(ctx, &spec));
159
+ let (default_impls, extend_rules) = match &default_accounts {
160
+ Some(da) => (da.to_definition(), da.to_extend_rule(ix_name.canonical_name())),
161
+ None => (TokenStream::new(), TokenStream::new()),
162
+ };
135
163
 
136
- let ResolvedOverride { default_impl, fn_generics, ctx_param, call_with_params, call_no_params } =
137
- resolve_override(handler, &spec, ctx);
164
+ // Assemble entrypoint fragments from spec + dispatch target.
165
+ let entry_fn_ident = syn::Ident::new(ix_name.canonical_name(), Span::call_site());
138
166
 
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
- }
167
+ // Resolve dispatch target: override handler vs default handler.
168
+ let (fn_generics, context_type, dispatch) = match handler {
169
+ Some(h) => {
170
+ let wrapper = &h.wrapper_mod;
171
+ let ctx_type = &h.context_type;
172
+ (h.fn_generics.to_token_stream(), quote!(#ctx_type), quote!(#wrapper::#entry_fn_ident))
146
173
  },
147
- None => quote! {
148
- pub fn #fn_ident #fn_generics (#ctx_param) -> Result<#return_type> {
149
- #call_no_params
150
- }
174
+ None => {
175
+ let default = spec.default_impl.as_ref().expect("validated above");
176
+ let accounts = default_accounts.as_ref().unwrap().accounts_ident();
177
+ let handler_fn = &default.handler_ident;
178
+ (TokenStream::new(), quote!(Context<#accounts>), quote!(#accounts::#handler_fn))
151
179
  },
152
180
  };
181
+ let return_type = &spec.return_type;
182
+ let (ref_token, ctx_decl) = if spec.is_view {
183
+ (quote!(&), quote!(ctx: #context_type))
184
+ } else {
185
+ (quote!(&mut), quote!(mut ctx: #context_type))
186
+ };
153
187
 
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)),
188
+ // Emit the entrypoint function, with or without a params argument.
189
+ let entrypoints = match &spec.params_type {
190
+ Some(params_type) => quote! {
191
+ pub fn #entry_fn_ident #fn_generics (#ctx_decl, params: #params_type) -> Result<#return_type> {
192
+ #dispatch(#ref_token ctx, &params)
193
193
  }
194
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)),
195
+ None => quote! {
196
+ pub fn #entry_fn_ident #fn_generics (#ctx_decl) -> Result<#return_type> {
197
+ #dispatch(#ref_token ctx)
211
198
  }
212
199
  },
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
- }
200
+ };
218
201
 
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
- }
202
+ Ok(GeneratedInstructions { default_impls, entrypoints, extend_rules })
226
203
  }
package/src/lib.rs CHANGED
@@ -5,11 +5,13 @@
5
5
  //! with optional default implementations. Each instruction is either **required** (implementor
6
6
  //! must provide a handler) or **provided** (a default implementation is available).
7
7
  //!
8
- //! Provided instructions support three override levels:
8
+ //! Provided instructions support two override levels:
9
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.
10
+ //! - **Default**: No custom handler defined — the framework emits both the default accounts struct
11
+ //! and an entrypoint that invokes the default handler.
12
+ //! - **Override**: Custom handler defined — the framework still emits the default accounts struct
13
+ //! (making it available for composition or reference), but the entrypoint delegates to the custom
14
+ //! handler using whatever context type it declares.
13
15
  //!
14
16
  //! See [`spec.md`](../spec.md) for the full specification.
15
17
  //!
package/src/overrides.rs CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  use itertools::Itertools;
8
8
  use proc_macro2::TokenStream;
9
- use quote::quote;
9
+ use quote::{format_ident, quote};
10
10
  use std::collections::BTreeMap;
11
- use syn::{spanned::Spanned, Attribute, Item, ItemFn, ItemMod, Type};
11
+ use syn::{Attribute, Item, ItemFn, ItemMod, Type};
12
12
 
13
13
  use crate::InstructionSet;
14
14
 
@@ -25,10 +25,6 @@ pub struct OverrideConfig {
25
25
  pub struct HandlerMeta {
26
26
  /// Wrapper module identifier for the relocated handler.
27
27
  pub wrapper_mod: syn::Ident,
28
- /// Original implementor-defined handler function identifier.
29
- pub fn_ident: syn::Ident,
30
- /// Inner type `T` extracted from `Context<...T>`.
31
- pub accounts_type: Type,
32
28
  /// The full `Context<...>` type as written (preserves lifetime args).
33
29
  pub context_type: Type,
34
30
  /// Function-level generics (e.g. `<'info>`) from the override handler.
@@ -37,8 +33,11 @@ pub struct HandlerMeta {
37
33
 
38
34
  /// Collection of custom instruction handlers discovered from a module.
39
35
  pub struct InstructionOverrides<Ix: InstructionSet> {
36
+ /// Map of instruction variants to their handler metadata.
40
37
  handlers: BTreeMap<Ix, HandlerMeta>,
38
+ /// Token stream containing all generated wrapper module definitions.
41
39
  pub wrapper_defs: TokenStream,
40
+ /// Configuration for the override system (attribute name, domain label).
42
41
  pub config: OverrideConfig,
43
42
  }
44
43
 
@@ -52,6 +51,7 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
52
51
  pub fn collect(module: &ItemMod, config: OverrideConfig) -> syn::Result<Self> {
53
52
  let mut collected = Self::new(config);
54
53
 
54
+ // Extract the module content and early return if it's empty.
55
55
  let items = match &module.content {
56
56
  Some((_, items)) => items,
57
57
  None => return Ok(collected),
@@ -59,6 +59,7 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
59
59
 
60
60
  // Walk every function in the module looking for override attributes.
61
61
  for item in items {
62
+ // Only process functions that are annotated with the override attribute.
62
63
  let Item::Fn(fn_item) = item else { continue };
63
64
  let Some(target) = parse_override_target::<Ix>(fn_item, config)? else {
64
65
  continue;
@@ -81,8 +82,8 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
81
82
  collected.handlers.insert(target, meta);
82
83
  }
83
84
 
84
- // Ensure no plain `fn name()` shadows a known instruction name.
85
- collected.reject_name_collisions(module)?;
85
+ // Ensure no unannotated `fn name()` accidentally shadows a known instruction name.
86
+ collected.reject_name_collisions(items)?;
86
87
  Ok(collected)
87
88
  }
88
89
 
@@ -97,10 +98,11 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
97
98
  return TokenStream::new();
98
99
  };
99
100
 
101
+ // Filter out functions that are overridden.
100
102
  let kept: Vec<_> = items
101
103
  .iter()
102
104
  .filter(|item| match item {
103
- Item::Fn(fn_item) => !self.is_overridden(&fn_item.sig.ident),
105
+ Item::Fn(fn_item) => !has_override_attr(fn_item, self.config),
104
106
  _ => true,
105
107
  })
106
108
  .collect();
@@ -108,26 +110,19 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
108
110
  quote!(#(#kept)*)
109
111
  }
110
112
 
111
- /// Checks whether a function ident belongs to an override handler.
112
- fn is_overridden(&self, fn_ident: &syn::Ident) -> bool {
113
- self.handlers.values().any(|h| h.fn_ident == *fn_ident)
114
- }
115
-
116
113
  /// Relocates a handler function into an isolated wrapper module
117
114
  /// and returns the corresponding [`HandlerMeta`].
118
115
  fn wrap_handler(&mut self, target: Ix, fn_item: &ItemFn) -> syn::Result<HandlerMeta> {
119
- let (accounts_type, context_type) = infer_context_type(fn_item)?;
120
- let fn_ident = fn_item.sig.ident.clone();
116
+ let context_type = infer_context_type(fn_item)?;
121
117
  let fn_generics = fn_item.sig.generics.clone();
122
118
 
123
119
  // Generate a deterministic module name: `__<attr>__<instruction>_handler`.
124
- let wrapper_mod = syn::Ident::new(
125
- &format!("__{}__{}_handler", self.config.attr_name, target.canonical_name()),
126
- proc_macro2::Span::call_site(),
127
- );
120
+ let wrapper_mod =
121
+ format_ident!("__{}__{}_handler", self.config.attr_name, target.canonical_name());
128
122
 
129
123
  // Clone the function without the instruction attribute.
130
- let wrapped_fn = strip_override_attr(fn_item, self.config)?;
124
+ let mut wrapped_fn = fn_item.clone();
125
+ wrapped_fn.attrs.retain(|attr| !is_override_attr(attr, self.config));
131
126
 
132
127
  // Emit the wrapper module so the handler lives in its own scope.
133
128
  self.wrapper_defs.extend(quote! {
@@ -137,31 +132,29 @@ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
137
132
  }
138
133
  });
139
134
 
140
- Ok(HandlerMeta { wrapper_mod, fn_ident, accounts_type, context_type, fn_generics })
135
+ Ok(HandlerMeta { wrapper_mod, context_type, fn_generics })
141
136
  }
142
137
 
143
138
  /// Rejects unattributed functions whose name collides with a known instruction.
144
139
  ///
145
140
  /// A bare `fn lz_receive(...)` without `#[oapp_instruction]`
146
141
  /// would silently shadow the generated entrypoint — catch that early.
147
- fn reject_name_collisions(&self, module: &ItemMod) -> syn::Result<()> {
148
- let Some((_, items)) = &module.content else {
149
- return Ok(());
150
- };
151
-
142
+ fn reject_name_collisions(&self, items: &[Item]) -> syn::Result<()> {
152
143
  for &ix in Ix::all() {
153
144
  let name = ix.canonical_name();
154
- let collides = items.iter().any(|item| match item {
155
- Item::Fn(f) => f.sig.ident == name && !has_override_attr(f, self.config),
156
- _ => false,
145
+ let collision = items.iter().find_map(|item| match item {
146
+ Item::Fn(f) if f.sig.ident == name && !has_override_attr(f, self.config) => {
147
+ Some(&f.sig.ident)
148
+ },
149
+ _ => None,
157
150
  });
158
151
 
159
- if collides {
160
- return Err(syn::Error::new(
161
- module.span(),
152
+ if let Some(ident) = collision {
153
+ return Err(syn::Error::new_spanned(
154
+ ident,
162
155
  format!(
163
156
  "`fn {name}(...)` conflicts with {domain} instruction `{name}`: \
164
- use #[{attr}] to override it",
157
+ use #[{attr}] to override it",
165
158
  domain = self.config.domain,
166
159
  attr = self.config.attr_name,
167
160
  ),
@@ -183,22 +176,7 @@ fn has_override_attr(fn_item: &ItemFn, cfg: OverrideConfig) -> bool {
183
176
 
184
177
  /// Returns `true` if `attr`'s last path segment matches the configured attribute name.
185
178
  fn is_override_attr(attr: &Attribute, cfg: OverrideConfig) -> bool {
186
- attr.path().segments.last().is_some_and(|seg| seg.ident == cfg.attr_name)
187
- }
188
-
189
- /// Returns a clone of `fn_item` with the override attribute removed.
190
- fn strip_override_attr(fn_item: &ItemFn, cfg: OverrideConfig) -> syn::Result<ItemFn> {
191
- let mut cleaned = fn_item.clone();
192
- let before = cleaned.attrs.len();
193
- cleaned.attrs.retain(|attr| !is_override_attr(attr, cfg));
194
-
195
- if cleaned.attrs.len() == before {
196
- return Err(syn::Error::new_spanned(
197
- fn_item,
198
- format!("internal error: expected #[{}] attribute on handler function", cfg.attr_name),
199
- ));
200
- }
201
- Ok(cleaned)
179
+ attr.path().is_ident(cfg.attr_name)
202
180
  }
203
181
 
204
182
  /// Parses the override target from a function's override attribute.
@@ -232,11 +210,7 @@ fn parse_override_target<Ix: InstructionSet>(
232
210
  if !matches!(attr.meta, syn::Meta::Path(_)) {
233
211
  return Err(syn::Error::new_spanned(
234
212
  attr,
235
- format!(
236
- "#[{}] does not accept arguments; the instruction name \
237
- is inferred from the handler function name",
238
- cfg.attr_name,
239
- ),
213
+ format!("#[{}] does not accept arguments", cfg.attr_name),
240
214
  ));
241
215
  }
242
216
 
@@ -245,45 +219,46 @@ fn parse_override_target<Ix: InstructionSet>(
245
219
  let ix = Ix::from_ident(fn_ident).ok_or_else(|| {
246
220
  syn::Error::new_spanned(
247
221
  fn_ident,
248
- format!("unsupported {} target `{fn_ident}`", cfg.attr_name,),
222
+ format!("unsupported {} target `{fn_ident}`", cfg.attr_name),
249
223
  )
250
224
  })?;
251
225
 
252
226
  Ok(Some(ix))
253
227
  }
254
228
 
255
- /// Extracts the inner `T` from the first `Context<T>` parameter.
229
+ /// Extracts the `Context<...>` type from the first `&Context<T>` or `&mut Context<T>` parameter.
256
230
  ///
257
- /// Returns `(accounts_type, context_type)` — the inner `T` (for default-context
258
- /// checks) and the full `Context<...>` type as written (to preserve lifetimes in
259
- /// the generated entrypoint).
260
- fn infer_context_type(fn_item: &ItemFn) -> syn::Result<(Type, Type)> {
231
+ /// Override handlers must accept `ctx` by reference — by-value `Context<T>` is rejected.
232
+ /// Returns the `Context<...>` type without the reference wrapper (preserving lifetimes).
233
+ fn infer_context_type(fn_item: &ItemFn) -> syn::Result<Type> {
261
234
  let Some(syn::FnArg::Typed(first)) = fn_item.sig.inputs.first() else {
262
235
  return Err(syn::Error::new_spanned(
263
236
  &fn_item.sig,
264
- "handler must have at least one parameter of type `Context<T>`",
237
+ "handler must have at least one parameter of type `&Context<T>` or `&mut Context<T>`",
265
238
  ));
266
239
  };
267
240
 
268
- let context_type = (*first.ty).clone();
269
- let accounts_type = extract_context_inner_type(&first.ty).ok_or_else(|| {
270
- syn::Error::new_spanned(&first.ty, "expected `Context<T>` as the first parameter")
271
- })?;
272
- Ok((accounts_type, context_type))
241
+ // Require `&Context<T>` or `&mut Context<T>` — reject by-value `Context<T>`.
242
+ let Type::Reference(ref_ty) = first.ty.as_ref() else {
243
+ return Err(syn::Error::new_spanned(
244
+ &first.ty,
245
+ "expected `&Context<T>` or `&mut Context<T>` as the first parameter",
246
+ ));
247
+ };
248
+ let inner_ty = ref_ty.elem.as_ref();
249
+
250
+ // Validate that the referenced type is `Context<...>`.
251
+ if !is_context_type(inner_ty) {
252
+ return Err(syn::Error::new_spanned(
253
+ &first.ty,
254
+ "expected `&Context<T>` or `&mut Context<T>` as the first parameter",
255
+ ));
256
+ }
257
+
258
+ Ok(inner_ty.clone())
273
259
  }
274
260
 
275
- /// Unwraps `Context<T>` or `Context<'a, 'b, 'c, 'info, T>` → `T`.
276
- ///
277
- /// Handles both the simple `Context<T>` form and the lifetime-annotated
278
- /// `Context<'_, '_, '_, 'info, T>` form by finding the last type argument.
279
- fn extract_context_inner_type(ty: &Type) -> Option<Type> {
280
- let Type::Path(tp) = ty else { return None };
281
- let seg = tp.path.segments.last().filter(|s| s.ident == "Context")?;
282
- let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
283
- return None;
284
- };
285
- args.args.iter().rev().find_map(|arg| match arg {
286
- syn::GenericArgument::Type(inner) => Some(inner.clone()),
287
- _ => None,
288
- })
261
+ /// Returns `true` if the type's last path segment is `Context`.
262
+ fn is_context_type(ty: &Type) -> bool {
263
+ matches!(ty, Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == "Context"))
289
264
  }