Every SPEL extension library ships its own ~8-line #[instruction] proc macro whose only job is to strip #[account(...)] attributes off handler parameters. The framework already exports an #[instruction], and already contains the stripping logic — but the two are not connected, so each author reimplements it identically.
Raised by @mmlado on #257: "Every author still copies those eight lines by hand, and exporting the shim from the framework for extensions to re-export would remove the boilerplate rather than explain it. Worth filing on its own."
Why the obvious thing doesn't work
spel_framework_macros::instruction (lib.rs:111) is a bare pass-through:
#[proc_macro_attribute]
pub fn instruction(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
That is correct for its documented purpose — inside #[lez_program] it never expands at all, because the module-level macro consumes the whole module first and strips both #[instruction] and #[account(...)] itself in generate_handler_fns (lib.rs:1225-1230).
But an extension library's handlers live outside #[lez_program], so there the attribute does expand, and a pass-through leaves the #[account(...)] attrs for rustc:
use spel_framework_macros::instruction;
#[instruction]
pub fn set_paused(
#[account(mut, pda = literal("pause"))] state: u32,
#[account(signer)] admin: u32,
paused: bool,
) -> u32 { 0 }
error: cannot find attribute `account` in this scope
error: cannot find attribute `account` in this scope
error: could not compile `shim-test` (lib) due to 2 previous errors
So the author is pushed into standing up a proc-macro = true sub-crate with syn/quote/proc-macro2 and rewriting the strip by hand. That is what spel-admin-authority does, what my pause extension on review/ext-mechanism does, and what the extension-author guide (logos-co/logos-docs#355) documents verbatim.
Proposed fix
Have the exported #[instruction] do the strip when it expands:
#[proc_macro_attribute]
pub fn instruction(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut func = parse_macro_input!(item as ItemFn);
for input in &mut func.sig.inputs {
if let FnArg::Typed(pat_type) = input {
pat_type.attrs.retain(|a| !a.path().is_ident("account"));
}
}
quote!(#func).into()
}
Verified locally:
- the failing example above compiles with it,
cargo test -p spel-framework-macros -p spel-framework-core stays green (130 tests across both).
It is inert for the #[lez_program] path, which never expands this macro, so the change only affects standalone use — which today is broken anyway.
Extension libraries could then pub use spel_framework::instruction; alongside their marker attribute, and drop the sub-crate entirely unless they also ship a gate attribute.
Scope
Independent of #257 — applies whether or not the extension mechanism lands, since standalone #[instruction] is documented as supported (docs/reference/macros.md: "a no-op when used standalone") and is currently unusable with #[account(...)] params.
Worth deciding alongside: whether docs/reference/macros.md should say the standalone behaviour strips helper attributes, rather than calling it a no-op.
Every SPEL extension library ships its own ~8-line
#[instruction]proc macro whose only job is to strip#[account(...)]attributes off handler parameters. The framework already exports an#[instruction], and already contains the stripping logic — but the two are not connected, so each author reimplements it identically.Raised by @mmlado on #257: "Every author still copies those eight lines by hand, and exporting the shim from the framework for extensions to re-export would remove the boilerplate rather than explain it. Worth filing on its own."
Why the obvious thing doesn't work
spel_framework_macros::instruction(lib.rs:111) is a bare pass-through:That is correct for its documented purpose — inside
#[lez_program]it never expands at all, because the module-level macro consumes the whole module first and strips both#[instruction]and#[account(...)]itself ingenerate_handler_fns(lib.rs:1225-1230).But an extension library's handlers live outside
#[lez_program], so there the attribute does expand, and a pass-through leaves the#[account(...)]attrs for rustc:So the author is pushed into standing up a
proc-macro = truesub-crate withsyn/quote/proc-macro2and rewriting the strip by hand. That is whatspel-admin-authoritydoes, what my pause extension onreview/ext-mechanismdoes, and what the extension-author guide (logos-co/logos-docs#355) documents verbatim.Proposed fix
Have the exported
#[instruction]do the strip when it expands:Verified locally:
cargo test -p spel-framework-macros -p spel-framework-corestays green (130 tests across both).It is inert for the
#[lez_program]path, which never expands this macro, so the change only affects standalone use — which today is broken anyway.Extension libraries could then
pub use spel_framework::instruction;alongside their marker attribute, and drop the sub-crate entirely unless they also ship a gate attribute.Scope
Independent of #257 — applies whether or not the extension mechanism lands, since standalone
#[instruction]is documented as supported (docs/reference/macros.md: "a no-op when used standalone") and is currently unusable with#[account(...)]params.Worth deciding alongside: whether
docs/reference/macros.mdshould say the standalone behaviour strips helper attributes, rather than calling it a no-op.