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}