Skip to content

Extension authors must hand-write an #[instruction] shim because the framework's own is a bare pass-through #271

Description

@vpavlin

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions