solzempic_macros/
lib.rs

1//! Procedural macros for the Solzempic framework.
2//!
3//! This crate provides compile-time code generation for Solana programs built
4//! with Solzempic. The macros reduce boilerplate while maintaining zero runtime
5//! overhead through compile-time expansion.
6//!
7//! # Available Macros
8//!
9//! | Macro | Type | Purpose |
10//! |-------|------|---------|
11//! | [`SolzempicDispatch`] | Attribute | Dispatch enum + framework types |
12//! | [`instruction`] | Attribute | Instruction trait implementations |
13//! | [`Account`] | Derive | Account struct with discriminator |
14//!
15//! # Quick Start
16//!
17//! ```ignore
18//! use solzempic::SolzempicDispatch;
19//!
20//! // 1. Define dispatch enum (generates framework types)
21//! #[SolzempicDispatch("Your11111111111111111111111111111111111111")]
22//! pub enum MyInstruction {
23//!     Initialize = 0,
24//!     Transfer = 1,
25//! }
26//!
27//! // 2. Define instruction struct
28//! pub struct Transfer<'a> {
29//!     from: AccountRefMut<'a, TokenAccount>,
30//!     to: AccountRefMut<'a, TokenAccount>,
31//! }
32//!
33//! // 3. Implement with #[instruction] macro
34//! #[instruction(TransferParams)]
35//! impl<'a> Transfer<'a> {
36//!     fn build(accounts: &'a [AccountInfo], params: &TransferParams) -> Result<Self, ProgramError> {
37//!         // Parse accounts...
38//!     }
39//!
40//!     fn validate(&self, program_id: &Pubkey, params: &TransferParams) -> ProgramResult {
41//!         // Validate state...
42//!     }
43//!
44//!     fn execute(&self, program_id: &Pubkey, params: &TransferParams) -> ProgramResult {
45//!         // Execute logic...
46//!     }
47//! }
48//!
49//! // 4. In entrypoint:
50//! MyInstruction::process(program_id, accounts, instruction_data)?;
51//! ```
52//!
53//! # Generated Code
54//!
55//! ## From `SolzempicDispatch`
56//!
57//! - `ID` - Program ID constant
58//! - `Solzempic` - Framework type implementing `Framework` trait
59//! - `AccountRef<'a, T>` - Type alias for read-only accounts
60//! - `AccountRefMut<'a, T>` - Type alias for writable accounts
61//! - `ShardRefContext<'a, T>` - Type alias for shard triplets
62//! - `id()` - Returns `&'static Pubkey`
63//! - `TryFrom<u8>` - Discriminator parsing
64//! - `dispatch()` - Handler dispatch (after enum construction)
65//! - `process()` - Direct dispatch (more efficient)
66//!
67//! ## From `instruction`
68//!
69//! - `InstructionParams` impl with associated `Params` type
70//! - `Instruction<'a>` impl with `build`, `validate`, `execute` methods
71//!
72//! ## From `Account` derive
73//!
74//! - `#[repr(C)]` for stable memory layout
75//! - `Clone`, `Copy`, `Pod`, `Zeroable` derives
76//! - Prepended `discriminator: [u8; 8]` field
77//! - `Loadable` impl for zero-copy loading
78//!
79//! # Performance
80//!
81//! All macros expand at compile time with zero runtime cost. The `process()`
82//! method is more efficient than `dispatch()` because it avoids constructing
83//! the enum variant before dispatching.
84
85use proc_macro::TokenStream;
86use quote::quote;
87use syn::{parse_macro_input, Data, DeriveInput, Fields, Expr, Lit, ItemImpl, ImplItem, ItemStruct, Type};
88
89/// Attribute macro for complete Solana program setup.
90///
91/// This is the main entry point for defining a Solzempic program. It generates
92/// everything needed: program ID, framework types, dispatch enum, entrypoint,
93/// and process_instruction function.
94///
95/// # Generated Items
96///
97/// | Item | Type | Description |
98/// |------|------|-------------|
99/// | `ID` | `Pubkey` | Program ID constant |
100/// | `Solzempic` | `struct` | Framework type implementing `Framework` trait |
101/// | `AccountRef<'a, T>` | `type` | Read-only account wrapper alias |
102/// | `AccountRefMut<'a, T>` | `type` | Writable account wrapper alias |
103/// | `ShardRefContext<'a, T>` | `type` | Shard triplet context alias |
104/// | `id()` | `fn` | Returns `&'static Pubkey` |
105/// | `process_instruction` | `fn` | Program entrypoint handler |
106/// | `entrypoint!` | macro | Registers the entrypoint (unless `no-entrypoint` feature) |
107///
108/// # Example
109///
110/// ```ignore
111/// use solzempic::SolzempicEntrypoint;
112///
113/// #[SolzempicEntrypoint("Your11111111111111111111111111111111111111")]
114/// pub enum MyInstruction {
115///     Initialize = 0,
116///     Transfer = 1,
117/// }
118/// ```
119///
120/// This single attribute generates a complete program setup.
121///
122/// # Panics
123///
124/// Compile-time panics if:
125/// - Applied to a non-enum type
126/// - No program ID provided in attribute
127/// - Variant lacks explicit discriminant value
128/// Parse account specs from #[accounts(...)] attribute on enum variant.
129/// Format: #[accounts(name: constraint, name2: constraint2, ...)]
130/// Constraints: mut (writable), signer, mut_signer (both), program, or empty (readonly)
131fn parse_variant_accounts(attrs: &[syn::Attribute]) -> Vec<(String, bool, bool, bool)> {
132    let mut accounts = Vec::new();
133
134    for attr in attrs {
135        if attr.path().is_ident("accounts") {
136            // Parse the content as comma-separated name: constraint pairs
137            let content = attr.meta.require_list()
138                .expect("#[accounts(...)] requires a list");
139
140            let tokens_str = content.tokens.to_string();
141
142            // Parse "name: constraint, name2: constraint2" format
143            for part in tokens_str.split(',') {
144                let part = part.trim();
145                if part.is_empty() { continue; }
146
147                let (name, constraint) = if let Some(colon_pos) = part.find(':') {
148                    let name = part[..colon_pos].trim().to_string();
149                    let constraint = part[colon_pos + 1..].trim().to_string();
150                    (name, constraint)
151                } else {
152                    (part.to_string(), String::new())
153                };
154
155                let is_signer = constraint == "signer" || constraint == "mut_signer";
156                let is_writable = constraint == "mut" || constraint == "mut_signer";
157                let is_program = constraint == "program";
158
159                accounts.push((name, is_signer, is_writable, is_program));
160            }
161        }
162    }
163
164    accounts
165}
166
167#[proc_macro_attribute]
168#[allow(non_snake_case)]
169pub fn SolzempicEntrypoint(attr: TokenStream, item: TokenStream) -> TokenStream {
170    let input = parse_macro_input!(item as DeriveInput);
171    let enum_name = &input.ident;
172    let vis = &input.vis;
173    let attrs = &input.attrs;
174
175    // Parse the program ID from attribute - either a string literal or an identifier
176    let program_id_tokens: proc_macro2::TokenStream = if attr.is_empty() {
177        panic!("SolzempicEntrypoint requires a program ID, e.g. #[SolzempicEntrypoint(\"Your111...\")]");
178    } else {
179        let attr_str = attr.to_string();
180        let trimmed = attr_str.trim();
181        if trimmed.starts_with('"') && trimmed.ends_with('"') {
182            // String literal: convert to pinocchio_pubkey::pubkey!() call
183            let pubkey_str = &trimmed[1..trimmed.len()-1];
184            let pubkey_str_lit = syn::LitStr::new(pubkey_str, proc_macro2::Span::call_site());
185            quote! { ::pinocchio_pubkey::pubkey!(#pubkey_str_lit) }
186        } else {
187            // Identifier: use directly
188            let ident: syn::Ident = syn::parse(attr.clone())
189                .expect("SolzempicEntrypoint attribute must be a string literal or identifier");
190            quote! { #ident }
191        }
192    };
193
194    let variants = match &input.data {
195        Data::Enum(data_enum) => &data_enum.variants,
196        _ => panic!("SolzempicEntrypoint can only be applied to enums"),
197    };
198
199    // Collect variant info (name, discriminator, and accounts)
200    let variant_info: Vec<_> = variants.iter().map(|variant| {
201        let variant_name = &variant.ident;
202        let discriminant = variant.discriminant.as_ref()
203            .expect("SolzempicEntrypoint requires explicit discriminant values");
204        let disc_expr = &discriminant.1;
205        let accounts = parse_variant_accounts(&variant.attrs);
206        (variant_name, disc_expr, accounts)
207    }).collect();
208
209    // Generate TryFrom<u8> match arms
210    let try_from_arms = variant_info.iter().map(|(name, disc, _)| {
211        quote! { #disc => Ok(#enum_name::#name), }
212    });
213
214    // Generate dispatch match arms (for backward compat)
215    let dispatch_arms = variant_info.iter().map(|(name, _, _)| {
216        quote! {
217            #enum_name::#name => <#name<'_> as ::solzempic::Instruction<'_>>::process(program_id, accounts, data),
218        }
219    });
220
221    // Generate process match arms (direct discriminator to handler)
222    let process_arms = variant_info.iter().map(|(name, disc, _)| {
223        quote! {
224            #disc => <#name<'_> as ::solzempic::Instruction<'_>>::process(program_id, accounts, &data[1..]),
225        }
226    });
227
228    // Generate variant definitions for the enum with Shank #[account(...)] attributes
229    let variant_defs = variant_info.iter().map(|(name, disc, accounts)| {
230        // Generate #[account(idx, constraints, name="...")] attributes for Shank
231        let account_attrs: Vec<proc_macro2::TokenStream> = accounts.iter().enumerate().map(|(idx, (acc_name, is_signer, is_writable, _is_program))| {
232            let mut constraints = Vec::new();
233            if *is_writable { constraints.push(quote! { writable }); }
234            if *is_signer { constraints.push(quote! { signer }); }
235
236            let idx_lit = syn::LitInt::new(&idx.to_string(), proc_macro2::Span::call_site());
237            let name_lit = syn::LitStr::new(acc_name, proc_macro2::Span::call_site());
238
239            if constraints.is_empty() {
240                quote! { #[account(#idx_lit, name = #name_lit)] }
241            } else {
242                quote! { #[account(#idx_lit, #(#constraints),*, name = #name_lit)] }
243            }
244        }).collect();
245
246        quote! {
247            #(#account_attrs)*
248            #name = #disc
249        }
250    });
251
252    let expanded = quote! {
253        /// Program ID
254        pub const ID: ::solana_address::Address = ::solana_address::Address::new_from_array(#program_id_tokens);
255
256        /// Program-specific framework implementation.
257        pub struct Solzempic;
258
259        impl ::solzempic::Framework for Solzempic {
260            const PROGRAM_ID: ::solana_address::Address = ID;
261        }
262
263        /// Read-only account wrapper with ownership validation.
264        pub type AccountRef<'a, T> = ::solzempic::AccountRef<'a, T, Solzempic>;
265
266        /// Writable account wrapper with ownership validation.
267        pub type AccountRefMut<'a, T> = ::solzempic::AccountRefMut<'a, T, Solzempic>;
268
269        /// Context for sharded data structures.
270        pub type ShardRefContext<'a, T> = ::solzempic::ShardRefContext<'a, T, Solzempic>;
271
272        /// Returns the program ID.
273        #[inline]
274        pub fn id() -> &'static ::solana_address::Address {
275            &ID
276        }
277
278        #(#attrs)*
279        #[repr(u8)]
280        #[cfg_attr(feature = "shank", derive(::shank::ShankInstruction))]
281        #vis enum #enum_name {
282            #(#variant_defs),*
283        }
284
285        impl ::core::convert::TryFrom<u8> for #enum_name {
286            type Error = ::pinocchio::error::ProgramError;
287
288            #[inline]
289            fn try_from(value: u8) -> Result<Self, Self::Error> {
290                match value {
291                    #(#try_from_arms)*
292                    _ => Err(::pinocchio::error::ProgramError::InvalidInstructionData),
293                }
294            }
295        }
296
297        impl #enum_name {
298            /// Dispatch to handler (use after TryFrom conversion)
299            #[inline]
300            pub fn dispatch(
301                self,
302                program_id: &::solana_address::Address,
303                accounts: &[::pinocchio::AccountView],
304                data: &[u8],
305            ) -> ::pinocchio::ProgramResult {
306                match self {
307                    #(#dispatch_arms)*
308                }
309            }
310
311            /// Process instruction data directly (more efficient - skips enum construction)
312            #[inline]
313            pub fn process(
314                program_id: &::solana_address::Address,
315                accounts: &[::pinocchio::AccountView],
316                data: &[u8],
317            ) -> ::pinocchio::ProgramResult {
318                let discriminator = *data.first()
319                    .ok_or(::pinocchio::error::ProgramError::InvalidInstructionData)?;
320                match discriminator {
321                    #(#process_arms)*
322                    _ => Err(::pinocchio::error::ProgramError::InvalidInstructionData),
323                }
324            }
325        }
326
327        /// Program entrypoint
328        #[inline]
329        pub fn process_instruction(
330            program_id: &::solana_address::Address,
331            accounts: &[::pinocchio::AccountView],
332            instruction_data: &[u8],
333        ) -> ::pinocchio::ProgramResult {
334            #enum_name::process(program_id, accounts, instruction_data)
335        }
336
337        #[cfg(not(feature = "no-entrypoint"))]
338        ::pinocchio::entrypoint!(process_instruction);
339
340    };
341
342    TokenStream::from(expanded)
343}
344
345/// Attribute macro for instruction impl blocks.
346///
347/// Transforms a regular impl block into `InstructionParams` and `Instruction<'a>`
348/// trait implementations, enabling integration with the dispatch system.
349///
350/// # Three-Phase Pattern
351///
352/// Instructions follow a three-phase execution model:
353///
354/// | Phase | Method | Purpose |
355/// |-------|--------|---------|
356/// | 1 | `build` | Parse accounts, create instruction struct |
357/// | 2 | `validate` | Check invariants, verify state |
358/// | 3 | `execute` | Perform state mutations |
359///
360/// This separation enables clear responsibility boundaries and easier testing.
361///
362/// # Required Methods
363///
364/// All three methods must be implemented:
365///
366/// ```ignore
367/// fn build(accounts: &'a [AccountInfo], params: &Params) -> Result<Self, ProgramError>
368/// fn validate(&self, program_id: &Pubkey, params: &Params) -> ProgramResult
369/// fn execute(&self, program_id: &Pubkey, params: &Params) -> ProgramResult
370/// ```
371///
372/// # Generated Code
373///
374/// From a single impl block, generates:
375///
376/// 1. `impl InstructionParams for MyInstruction<'_>` - Associates params type
377/// 2. `impl<'a> Instruction<'a> for MyInstruction<'a>` - Full instruction trait
378///
379/// The `Instruction::process` default method calls all three phases in order.
380///
381/// # Example
382///
383/// ```ignore
384/// use solzempic::{instruction, AccountRefMut, Signer};
385///
386/// /// Parameters for the transfer instruction.
387/// #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
388/// #[repr(C)]
389/// pub struct TransferParams {
390///     pub amount: u64,
391/// }
392///
393/// /// Transfer tokens between accounts.
394/// pub struct Transfer<'a> {
395///     from: AccountRefMut<'a, TokenAccount>,
396///     to: AccountRefMut<'a, TokenAccount>,
397///     authority: Signer<'a>,
398/// }
399///
400/// #[instruction(TransferParams)]
401/// impl<'a> Transfer<'a> {
402///     fn build(accounts: &'a [AccountInfo], _params: &TransferParams) -> Result<Self, ProgramError> {
403///         Ok(Self {
404///             from: AccountRefMut::load(&accounts[0])?,
405///             to: AccountRefMut::load(&accounts[1])?,
406///             authority: Signer::wrap(&accounts[2])?,
407///         })
408///     }
409///
410///     fn validate(&self, _program_id: &Pubkey, params: &TransferParams) -> ProgramResult {
411///         // Verify authority owns source account
412///         if self.from.get().owner != *self.authority.key() {
413///             return Err(ProgramError::InvalidAccountOwner);
414///         }
415///         // Verify sufficient balance
416///         if self.from.get().amount() < params.amount {
417///             return Err(ProgramError::InsufficientFunds);
418///         }
419///         Ok(())
420///     }
421///
422///     fn execute(&self, _program_id: &Pubkey, params: &TransferParams) -> ProgramResult {
423///         // Perform transfer via CPI...
424///         Ok(())
425///     }
426/// }
427/// ```
428///
429/// # Panics
430///
431/// Compile-time panics if:
432/// - No params type provided in attribute (for impl blocks)
433/// - Applied to non-struct/non-impl
434#[proc_macro_attribute]
435pub fn instruction(attr: TokenStream, item: TokenStream) -> TokenStream {
436    // Try to parse as struct first, then as impl block
437    let item_clone = item.clone();
438
439    if let Ok(input) = syn::parse::<ItemStruct>(item_clone) {
440        // It's a struct - generate Shank account metadata
441        return instruction_struct_impl(attr, input);
442    }
443
444    // Otherwise treat as impl block
445    let params_type: syn::Path = syn::parse(attr)
446        .expect("instruction macro on impl requires params type, e.g. #[instruction(MyParams)]");
447    let input = parse_macro_input!(item as ItemImpl);
448
449    // Extract the struct name from the impl
450    let struct_type = &input.self_ty;
451
452    // Extract struct name without lifetime for InstructionParams impl
453    let struct_name = match struct_type.as_ref() {
454        syn::Type::Path(type_path) => &type_path.path.segments.last().unwrap().ident,
455        _ => panic!("instruction macro requires a struct type"),
456    };
457
458    // Extract the methods
459    let methods: Vec<_> = input.items.iter().filter_map(|item| {
460        if let ImplItem::Fn(method) = item {
461            Some(method)
462        } else {
463            None
464        }
465    }).collect();
466
467    let expanded = quote! {
468        impl ::solzempic::InstructionParams for #struct_name<'_> {
469            type Params = #params_type;
470        }
471
472        impl<'a> ::solzempic::Instruction<'a> for #struct_name<'a> {
473            #(#methods)*
474        }
475    };
476
477    TokenStream::from(expanded)
478}
479
480/// Internal implementation for instruction struct
481fn instruction_struct_impl(attr: TokenStream, input: ItemStruct) -> TokenStream {
482    let struct_name = &input.ident;
483    let vis = &input.vis;
484    let attrs = &input.attrs;
485    let generics = &input.generics;
486
487    // Parse optional starting index from attribute (defaults to 0)
488    let start_index: usize = if attr.is_empty() {
489        0
490    } else {
491        syn::parse::<syn::LitInt>(attr)
492            .map(|lit| lit.base10_parse::<usize>().unwrap_or(0))
493            .unwrap_or(0)
494    };
495
496    let fields = match &input.fields {
497        Fields::Named(fields_named) => &fields_named.named,
498        _ => panic!("instruction macro on struct only supports named fields"),
499    };
500
501    // Analyze each field and determine account constraints
502    let mut account_metas: Vec<proc_macro2::TokenStream> = Vec::new();
503    let mut shank_attr_strings: Vec<String> = Vec::new();
504    let mut current_idx = start_index;
505
506    for field in fields.iter() {
507        let field_name = field.ident.as_ref().expect("Named field required");
508        let field_name_str = field_name.to_string();
509        let field_ty = &field.ty;
510
511        let (is_signer, is_writable, is_program, expand_count) = analyze_field_type(field_ty);
512
513        if expand_count > 1 {
514            let shard_names = ["left_shard", "current_shard", "right_shard"];
515            for (i, shard_name) in shard_names.iter().enumerate() {
516                let idx = current_idx + i;
517                account_metas.push(quote! {
518                    ::solzempic::ShankAccountMeta {
519                        index: #idx,
520                        name: #shard_name,
521                        is_signer: false,
522                        is_writable: true,
523                        is_program: false,
524                    }
525                });
526                shank_attr_strings.push(format!("#[account({}, writable, name=\"{}\")]", idx, shard_name));
527            }
528            current_idx += expand_count;
529        } else {
530            let mut constraints = Vec::new();
531            if is_writable { constraints.push("writable"); }
532            if is_signer { constraints.push("signer"); }
533
534            let constraints_str = if constraints.is_empty() {
535                String::new()
536            } else {
537                format!(", {}", constraints.join(", "))
538            };
539
540            shank_attr_strings.push(format!("#[account({}{}, name=\"{}\")]", current_idx, constraints_str, field_name_str));
541
542            account_metas.push(quote! {
543                ::solzempic::ShankAccountMeta {
544                    index: #current_idx,
545                    name: #field_name_str,
546                    is_signer: #is_signer,
547                    is_writable: #is_writable,
548                    is_program: #is_program,
549                }
550            });
551            current_idx += 1;
552        }
553    }
554
555    let num_accounts = account_metas.len();
556    let shank_output = shank_attr_strings.join("\n    ");
557
558    let field_defs = fields.iter().map(|f| {
559        let field_name = &f.ident;
560        let field_ty = &f.ty;
561        let field_vis = &f.vis;
562        let field_attrs = &f.attrs;
563        quote! {
564            #(#field_attrs)*
565            #field_vis #field_name: #field_ty
566        }
567    });
568
569    let expanded = quote! {
570        #(#attrs)*
571        #vis struct #struct_name #generics {
572            #(#field_defs),*
573        }
574
575        impl #struct_name<'_> {
576            pub const NUM_ACCOUNTS: usize = #num_accounts;
577
578            pub const SHANK_ACCOUNTS: [::solzempic::ShankAccountMeta; #num_accounts] = [
579                #(#account_metas),*
580            ];
581
582            pub fn shank_accounts() -> &'static str {
583                #shank_output
584            }
585        }
586    };
587
588    TokenStream::from(expanded)
589}
590
591/// Derive macro for account structs with automatic discriminator handling.
592///
593/// This macro transforms a simple struct definition into a zero-copy-safe
594/// account type with all necessary traits and discriminator validation.
595///
596/// # What It Generates
597///
598/// From your struct definition, the macro produces:
599///
600/// | Generated | Purpose |
601/// |-----------|---------|
602/// | `#[repr(C)]` | Stable, predictable memory layout |
603/// | `Clone`, `Copy` | Value semantics |
604/// | `Pod`, `Zeroable` | Safe zero-copy casting via bytemuck |
605/// | `discriminator` field | 8-byte type identifier (prepended) |
606/// | `Loadable` impl | Zero-copy loading with validation |
607///
608/// # Account Layout
609///
610/// The discriminator is prepended to your fields:
611///
612/// ```text
613/// Original:                    Generated:
614/// struct Counter {             struct Counter {
615///     owner: Pubkey,      →        discriminator: [u8; 8],  // Added
616///     count: u64,                  owner: Pubkey,
617/// }                                count: u64,
618///                              }
619/// ```
620///
621/// # Discriminator Values
622///
623/// Use unique discriminator values (0-255) for each account type in your
624/// program. This prevents account type confusion attacks.
625///
626/// | Value | Recommendation |
627/// |-------|----------------|
628/// | 0 | Reserved (uninitialized) |
629/// | 1-255 | Your account types |
630///
631/// # Required Attribute
632///
633/// The `#[account(discriminator = N)]` attribute is required:
634///
635/// ```ignore
636/// #[derive(Account)]
637/// #[account(discriminator = 1)]  // Required!
638/// pub struct MyAccount { ... }
639/// ```
640///
641/// # Field Requirements
642///
643/// All fields must be `Pod`-safe (no padding, alignment 1 or power-of-2):
644///
645/// | Safe Types | Unsafe Types |
646/// |------------|--------------|
647/// | `u8`, `u16`, `u32`, `u64`, `u128` | `bool` (use `u8`) |
648/// | `i8`, `i16`, `i32`, `i64`, `i128` | `enum` (use `#[repr(u8)]`) |
649/// | `[u8; N]`, `Pubkey` | `String`, `Vec<T>` |
650/// | Other `Pod` structs | References, Box, Rc |
651///
652/// # Example
653///
654/// ```ignore
655/// use solzempic::Account;
656/// use pinocchio::pubkey::Pubkey;
657///
658/// /// A simple counter account.
659/// #[derive(Account)]
660/// #[account(discriminator = 1)]
661/// pub struct Counter {
662///     /// The authority who can increment.
663///     pub authority: Pubkey,
664///     /// Current count value.
665///     pub count: u64,
666/// }
667///
668/// /// User profile with multiple fields.
669/// #[derive(Account)]
670/// #[account(discriminator = 2)]
671/// pub struct UserProfile {
672///     pub owner: Pubkey,
673///     pub created_at: i64,
674///     pub points: u64,
675///     pub level: u8,
676///     pub _padding: [u8; 7],  // Explicit padding for alignment
677/// }
678/// ```
679///
680/// # Usage with AccountRef
681///
682/// ```ignore
683/// fn increment(accounts: &[AccountInfo]) -> ProgramResult {
684///     let counter = AccountRefMut::<Counter>::load(&accounts[0])?;
685///
686///     // Discriminator is automatically validated during load
687///     counter.get_mut().count += 1;
688///     Ok(())
689/// }
690/// ```
691///
692/// # Panics
693///
694/// Compile-time panics if:
695/// - `#[account(discriminator = N)]` attribute is missing
696/// - Applied to non-struct (enum, union)
697/// - Struct has unnamed fields (tuple struct)
698#[proc_macro_derive(Account, attributes(account))]
699pub fn derive_account(input: TokenStream) -> TokenStream {
700    let input = parse_macro_input!(input as DeriveInput);
701    let name = &input.ident;
702    let vis = &input.vis;
703
704    // Extract the discriminator value from #[account(discriminator = N)] attribute
705    let discriminator = extract_discriminator(&input.attrs)
706        .expect("Account derive requires #[account(discriminator = N)] attribute");
707
708    // Get the struct fields
709    let fields = match &input.data {
710        Data::Struct(data_struct) => match &data_struct.fields {
711            Fields::Named(fields_named) => &fields_named.named,
712            _ => panic!("Account derive only supports structs with named fields"),
713        },
714        _ => panic!("Account derive only supports structs"),
715    };
716
717    // Generate the new struct with discriminator field prepended
718    let field_defs = fields.iter().map(|f| {
719        let field_name = &f.ident;
720        let field_ty = &f.ty;
721        let field_vis = &f.vis;
722        let attrs = &f.attrs;
723        quote! {
724            #(#attrs)*
725            #field_vis #field_name: #field_ty
726        }
727    });
728
729    let expanded = quote! {
730        #[repr(C)]
731        #[derive(Clone, Copy, ::bytemuck::Pod, ::bytemuck::Zeroable)]
732        #[cfg_attr(feature = "shank", derive(::shank::ShankAccount))]
733        #vis struct #name {
734            /// Account discriminator (8 bytes)
735            pub discriminator: [u8; 8],
736            #(#field_defs),*
737        }
738
739        impl #name {
740            /// The discriminator value for this account type.
741            pub const DISCRIMINATOR_VALUE: u8 = #discriminator;
742
743            /// The discriminator as an 8-byte array.
744            pub const DISCRIMINATOR_BYTES: [u8; 8] = [#discriminator, 0, 0, 0, 0, 0, 0, 0];
745
746            /// Check if data has the correct discriminator.
747            #[inline]
748            pub fn check_discriminator(data: &[u8]) -> bool {
749                !data.is_empty() && data[0] == #discriminator
750            }
751        }
752
753        impl ::solzempic::Loadable for #name {
754            const DISCRIMINATOR: u8 = #discriminator;
755        }
756    };
757
758    TokenStream::from(expanded)
759}
760
761/// Analyzes a field type to determine Shank constraints.
762/// Returns (is_signer, is_writable, is_program, expand_count)
763fn analyze_field_type(ty: &Type) -> (bool, bool, bool, usize) {
764    match ty {
765        Type::Path(type_path) => {
766            if let Some(segment) = type_path.path.segments.last() {
767                let type_name = segment.ident.to_string();
768                match type_name.as_str() {
769                    // Signer types
770                    "Signer" => (true, false, false, 1),
771                    "MutSigner" => (true, true, false, 1),  // signer + writable
772
773                    // Writable account types
774                    "AccountRefMut" => (false, true, false, 1),
775                    "TokenAccountRefMut" => (false, true, false, 1),
776                    "Writable" => (false, true, false, 1),
777
778                    // Readonly account types
779                    "AccountRef" => (false, false, false, 1),
780                    "TokenAccountRef" => (false, false, false, 1),
781                    "Mint" => (false, false, false, 1),
782                    "ValidatedAccount" => (false, false, false, 1),
783                    "ReadOnly" => (false, false, false, 1),
784
785                    // Program types
786                    "SystemProgram" => (false, false, true, 1),
787                    "TokenProgram" => (false, false, true, 1),
788                    "AtaProgram" => (false, false, true, 1),
789                    "Token2022Program" => (false, false, true, 1),
790
791                    // Shard context expands to 3 accounts
792                    "ShardRefContext" => (false, true, false, 3),
793
794                    _ => (false, false, false, 1),
795                }
796            } else {
797                (false, false, false, 1)
798            }
799        }
800        Type::Reference(_) => {
801            // &'a AccountView - default to readonly (use Writable<'a> for writable)
802            (false, false, false, 1)
803        }
804        _ => (false, false, false, 1),
805    }
806}
807
808/// Attribute macro for account structs.
809///
810/// Adds `#[repr(C)]`, `#[derive(Clone, Copy)]`, unsafe Pod/Zeroable impls, and optionally
811/// `#[derive(ShankAccount)]` (when `shank` feature is enabled).
812///
813/// If a discriminator is provided, also generates `impl Loadable`.
814///
815/// Uses unsafe impl for Pod/Zeroable to support structs with manually-verified padding.
816///
817/// # Example
818///
819/// ```ignore
820/// // Without discriminator (just Pod/Zeroable):
821/// #[account]
822/// pub struct Market {
823///     pub discriminator: [u8; 8],
824///     pub admin: Pubkey,
825/// }
826///
827/// // With discriminator (also generates impl Loadable):
828/// #[account(discriminator = AccountType::Market)]
829/// pub struct Market {
830///     pub discriminator: [u8; 8],
831///     pub admin: Pubkey,
832/// }
833/// ```
834#[proc_macro_attribute]
835pub fn account(attr: TokenStream, item: TokenStream) -> TokenStream {
836    let input = parse_macro_input!(item as ItemStruct);
837    let name = &input.ident;
838    let vis = &input.vis;
839    let attrs = &input.attrs;
840    let generics = &input.generics;
841
842    // Parse discriminator from attribute if provided
843    let discriminator_expr: Option<syn::Expr> = if attr.is_empty() {
844        None
845    } else {
846        let attr_str = attr.to_string();
847        // Parse "discriminator = <expr>"
848        if let Some(eq_pos) = attr_str.find('=') {
849            let expr_str = attr_str[eq_pos + 1..].trim();
850            syn::parse_str(expr_str).ok()
851        } else {
852            None
853        }
854    };
855
856    let fields = match &input.fields {
857        Fields::Named(fields_named) => &fields_named.named,
858        _ => panic!("account macro only supports structs with named fields"),
859    };
860
861    let field_defs = fields.iter().map(|f| {
862        let field_name = &f.ident;
863        let field_ty = &f.ty;
864        let field_vis = &f.vis;
865        let field_attrs = &f.attrs;
866        quote! {
867            #(#field_attrs)*
868            #field_vis #field_name: #field_ty
869        }
870    });
871
872    // Check if struct has a discriminator field
873    let has_discriminator_field = fields.iter().any(|f| {
874        f.ident.as_ref().map(|i| i == "discriminator").unwrap_or(false)
875    });
876
877    // Generate Loadable impl if discriminator provided
878    let loadable_impl = discriminator_expr.map(|disc| {
879        let account_impl = if has_discriminator_field {
880            quote! {
881                impl ::solzempic::traits::Account for #name {
882                    const DISCRIMINATOR: u8 = #disc as u8;
883                    const LEN: usize = ::core::mem::size_of::<Self>();
884
885                    #[inline]
886                    fn discriminator(&self) -> &[u8; 8] {
887                        &self.discriminator
888                    }
889                }
890            }
891        } else {
892            quote! {}
893        };
894
895        quote! {
896            impl ::solzempic::Loadable for #name {
897                const DISCRIMINATOR: u8 = #disc as u8;
898            }
899
900            #account_impl
901        }
902    });
903
904    let expanded = quote! {
905        #[repr(C)]
906        #[derive(Clone, Copy)]
907        #[cfg_attr(feature = "shank", derive(::shank::ShankAccount))]
908        #(#attrs)*
909        #vis struct #name #generics {
910            #(#field_defs),*
911        }
912
913        // Safety: Struct is #[repr(C)] - caller ensures no uninitialized padding
914        unsafe impl ::bytemuck::Pod for #name {}
915        unsafe impl ::bytemuck::Zeroable for #name {}
916
917        #loadable_impl
918    };
919
920    TokenStream::from(expanded)
921}
922
923/// Extract discriminator value from `#[account(discriminator = N)]` attribute.
924///
925/// Parses the attribute list looking for the `account` attribute with a
926/// `discriminator` key-value pair. The value must be a u8 integer literal.
927///
928/// # Returns
929///
930/// - `Some(n)` if a valid discriminator attribute is found
931/// - `None` if the attribute is missing or malformed
932///
933/// # Example Attribute Formats
934///
935/// ```ignore
936/// #[account(discriminator = 1)]     // ✓ Valid
937/// #[account(discriminator = 255)]   // ✓ Valid
938/// #[account(discriminator = 256)]   // ✗ Overflow (not u8)
939/// #[account(discriminator = "1")]   // ✗ String, not integer
940/// ```
941fn extract_discriminator(attrs: &[syn::Attribute]) -> Option<u8> {
942    for attr in attrs {
943        if attr.path().is_ident("account") {
944            let nested = attr.parse_args_with(
945                syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
946            ).ok()?;
947
948            for meta in nested {
949                if let syn::Meta::NameValue(nv) = meta {
950                    if nv.path.is_ident("discriminator") {
951                        if let Expr::Lit(expr_lit) = &nv.value {
952                            if let Lit::Int(lit_int) = &expr_lit.lit {
953                                return lit_int.base10_parse::<u8>().ok();
954                            }
955                        }
956                    }
957                }
958            }
959        }
960    }
961    None
962}