Skip to main content

hopper_derive/
lib.rs

1//! Optional proc macro DX layer for Hopper.
2//!
3//! Provides both the canonical `#[hopper::state]`, `#[hopper::context]`,
4//! `#[hopper::program]` surface and the legacy `#[hopper_state]`,
5//! `#[hopper_context]`, `#[hopper_program]` aliases. All entry points generate
6//! zero-cost code targeting Hopper's runtime primitives.
7//!
8//! **Not required.** Every feature these macros provide is achievable through
9//! Hopper's declarative `macro_rules!` macros or hand-written code. These
10//! exist purely for developer velocity. The generated code compiles to the
11//! exact same pointer arithmetic as raw Pinocchio.
12//!
13//! # Design Philosophy
14//!
15//! - **Macros generate code, not behavior.** No hidden runtime logic.
16//! - **Everything inlines.** No function calls that wouldn't exist in hand-written code.
17//! - **No heap.** Generated code is `no_std`, `no_alloc`.
18//! - **Optional.** Core Hopper never depends on this crate.
19
20extern crate proc_macro;
21
22mod args;
23mod constant;
24mod context;
25mod crank;
26mod declare_program;
27mod dynamic;
28mod dynamic_account;
29mod error;
30mod event;
31mod init_space;
32mod migrate;
33mod pod;
34mod program;
35mod state;
36
37use proc_macro::TokenStream;
38
39/// Generate a `SegmentMap` implementation for a zero-copy layout struct.
40///
41/// Computes field offsets at compile time and emits a const segment table.
42/// The generated code is zero-cost. Segment lookups resolve to const loads.
43///
44/// # Example
45///
46/// ```ignore
47/// #[hopper_state]
48/// #[repr(C)]
49/// pub struct Vault {
50///     pub authority: [u8; 32],  // TypedAddress<Authority>
51///     pub balance: [u8; 8],     // WireU64
52///     pub bump: u8,
53/// }
54///
55/// // Generated:
56/// // impl SegmentMap for Vault { ... }
57/// // const VAULT_SEGMENTS: ... (for direct access)
58/// ```
59#[proc_macro_attribute]
60pub fn hopper_state(attr: TokenStream, item: TokenStream) -> TokenStream {
61    state::expand(attr.into(), item.into())
62        .unwrap_or_else(|e| e.to_compile_error())
63        .into()
64}
65
66#[proc_macro_attribute]
67pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
68    hopper_state(attr, item)
69}
70
71/// Derive `const INIT_SPACE: usize = size_of::<Self>()` on a struct.
72///
73/// Anchor-parity derive: programs that have an Anchor-shaped
74/// `#[account(init, payer = X, space = 8 + Foo::INIT_SPACE)]`
75/// expression can port to Hopper without reshaping the size
76/// computation. For types already declared with `#[hopper::state]`
77/// the same constant is emitted automatically; this derive exists
78/// for hand-authored `#[repr(C)]` Pod structs that want to
79/// participate in the pattern without adopting the full state
80/// attribute.
81///
82/// # Example
83///
84/// ```ignore
85/// #[derive(HopperInitSpace)]
86/// #[repr(C)]
87/// pub struct Profile {
88///     pub bump: u8,
89///     pub authority: [u8; 32],
90/// }
91///
92/// // Generated:
93/// // impl Profile {
94/// //     pub const INIT_SPACE: usize = core::mem::size_of::<Self>();
95/// // }
96/// ```
97#[proc_macro_derive(HopperInitSpace)]
98pub fn derive_hopper_init_space(input: TokenStream) -> TokenStream {
99    init_space::expand(input.into())
100        .unwrap_or_else(|e| e.to_compile_error())
101        .into()
102}
103
104/// Anchor / Quasar naming alias for [`state`].
105///
106/// Declare a zero-copy account layout with the familiar `#[account]`
107/// spelling. Functionally identical to `#[hopper::state]`. The same
108/// `#[account(mut(...))]` field-attribute syntax inside a
109/// `#[hopper::context]` keeps working because field-level attrs are
110/// consumed by the outer macro, not by this proc macro.
111#[proc_macro_attribute]
112pub fn account(attr: TokenStream, item: TokenStream) -> TokenStream {
113    hopper_state(attr, item)
114}
115
116/// Generate typed context accessors with segment-level borrow tracking.
117///
118/// Each field annotated with `#[account(mut(field1, field2))]` gets accessor
119/// methods that:
120/// 1. Look up the segment by const offset (no string matching)
121/// 2. Register a segment borrow in the registry
122/// 3. Return a typed reference via pointer cast
123///
124/// # Example
125///
126/// ```ignore
127/// #[hopper_context]
128/// pub struct Deposit {
129///     #[account(signer, mut)]
130///     pub depositor: AccountView,
131///
132///     #[account(mut(balance))]
133///     pub vault: Vault,
134/// }
135///
136/// // Generated:
137/// // impl<'a> Deposit<'a> {
138/// //     pub fn vault_balance_mut(&mut self) -> Result<RefMut<WireU64>, ProgramError> { ... }
139/// // }
140/// ```
141#[proc_macro_attribute]
142pub fn hopper_context(attr: TokenStream, item: TokenStream) -> TokenStream {
143    context::expand(attr.into(), item.into())
144        .unwrap_or_else(|e| e.to_compile_error())
145        .into()
146}
147
148#[proc_macro_attribute]
149pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
150    hopper_context(attr, item)
151}
152
153/// Anchor-style plural alias for [`context`].
154///
155/// Anchor writes `#[derive(Accounts)]` on the accounts struct. Hopper
156/// uses attribute macros instead of derive macros, so this is the
157/// closest naturally-spelled alias: `#[accounts]`. Functionally
158/// identical to `#[hopper::context]`. Pick whichever spelling reads
159/// best to the rest of your codebase.
160#[proc_macro_attribute]
161pub fn accounts(attr: TokenStream, item: TokenStream) -> TokenStream {
162    hopper_context(attr, item)
163}
164
165/// `#[derive(Accounts)]` - Anchor-spelled drop-in for `#[hopper::context]`.
166///
167/// Functionally identical to the attribute form: every constraint Hopper
168/// recognises (`init`, `init_if_needed`, `mut`, `signer`, `seeds`, `bump`,
169/// `payer`, `space`, `has_one`, `owner`, `address`, `constraint`,
170/// `token::*`, `mint::*`, `associated_token::*`, the Token-2022 extension
171/// gates, `dup`, `sweep`, `executable`, `rent_exempt`, `realloc`, `zero`,
172/// `close`) all work in the derive form. Hopper-specific authoring sugar
173/// - segment-tagged `mut(field, …)`, `read(field, …)`, the inline
174/// `#[hopper::pipeline]` / `#[hopper::receipt]` / `#[hopper::invariant]`
175/// stack - also works untouched.
176///
177/// The derive registers `account`, `signer`, `instruction`, and `validate`
178/// as helper attributes so the existing `#[account(...)]`, `#[signer]`,
179/// `#[instruction(...)]`, and `#[validate]` field/struct annotations
180/// compile without an extra `use` line. Helper attributes are silently
181/// consumed by `rustc` once the derive runs, just like Anchor's setup.
182///
183/// # When to use which spelling
184///
185/// - **`#[hopper::context]`** when you want Hopper-native vocabulary and
186///   are starting from scratch.
187/// - **`#[derive(Accounts)]`** when you're porting from Anchor or want a
188///   spelling that matches the broader Solana-Rust convention. The
189///   `Accounts` symbol comes from `hopper::prelude::*`.
190///
191/// # Example
192///
193/// ```ignore
194/// use hopper::prelude::*;
195///
196/// #[derive(Accounts)]
197/// #[instruction(amount: u64)]
198/// pub struct Deposit {
199///     #[account(mut)]
200///     pub vault: Vault,
201///
202///     #[signer]
203///     pub authority: AccountView,
204///
205///     pub system_program: AccountView,
206/// }
207/// ```
208///
209/// The generated code is identical to `#[hopper::context]` on the same
210/// struct - same binder type, same accessors, same constraint validation
211/// pipeline. No runtime difference between the two spellings.
212#[proc_macro_derive(Accounts, attributes(account, signer, instruction, validate))]
213pub fn derive_accounts(input: TokenStream) -> TokenStream {
214    context::expand_for_derive(input.into())
215        .unwrap_or_else(|e| e.to_compile_error())
216        .into()
217}
218
219/// Generate a dispatch table for a Hopper program module.
220///
221/// Maps instruction discriminator bytes to handler functions, generating
222/// a clean entrypoint with minimal branching.
223///
224/// # Example
225///
226/// ```ignore
227/// #[hopper_program]
228/// mod vault {
229///     pub fn deposit(ctx: &mut Context, amount: u64) -> ProgramResult { ... }
230///     pub fn withdraw(ctx: &mut Context, amount: u64) -> ProgramResult { ... }
231/// }
232///
233/// // Generated:
234/// // pub fn __hopper_dispatch(program_id, accounts, data) -> ProgramResult { ... }
235/// ```
236#[proc_macro_attribute]
237pub fn hopper_program(attr: TokenStream, item: TokenStream) -> TokenStream {
238    program::expand(attr.into(), item.into())
239        .unwrap_or_else(|e| e.to_compile_error())
240        .into()
241}
242
243#[proc_macro_attribute]
244pub fn program(attr: TokenStream, item: TokenStream) -> TokenStream {
245    hopper_program(attr, item)
246}
247
248/// Derive the Hopper zero-copy marker contract for a user-defined struct.
249///
250/// Unlike `#[hopper::state]` (which emits the full Hopper layout: 16-byte
251/// header, layout_id, schema export, typed load helpers), `#[hopper::pod]`
252/// is the minimal opt-in: it asserts that the struct satisfies the
253/// Pod + FixedLayout + alignment-1 + non-padded + non-zero-sized contract
254/// at compile time, and emits the matching `unsafe impl Pod` and
255/// `impl FixedLayout` so it can participate in every Hopper segment /
256/// raw access API.
257///
258/// This is the Hopper Safety Audit's "derive macros for Pod and layout"
259/// recommendation delivered standalone: use it on sub-structs, wire
260/// helpers, or any `#[repr(C)]` overlay that isn't a full top-level
261/// account layout.
262///
263/// # Example
264///
265/// ```ignore
266/// #[hopper::pod]
267/// #[repr(C)]
268/// pub struct Cursor {
269///     pub head: WireU64,
270///     pub tail: WireU64,
271///     pub capacity: WireU64,
272/// }
273///
274/// // Now usable as:
275/// let c: Ref<'_, Cursor> = account.segment_ref::<Cursor>(&mut borrows, 0, 24)?;
276/// ```
277#[proc_macro_attribute]
278pub fn hopper_pod(attr: TokenStream, item: TokenStream) -> TokenStream {
279    pod::expand(attr.into(), item.into())
280        .unwrap_or_else(|e| e.to_compile_error())
281        .into()
282}
283
284/// Short alias: `#[hopper::pod]`. Functionally identical to `#[hopper_pod]`.
285#[proc_macro_attribute]
286pub fn pod(attr: TokenStream, item: TokenStream) -> TokenStream {
287    hopper_pod(attr, item)
288}
289
290/// Declare a schema-epoch migration edge.
291///
292/// Decorates a function of signature
293/// `fn(&mut [u8]) -> Result<(), ProgramError>` that mutates an
294/// account body in-place from schema epoch `from` to epoch `to`.
295/// The macro emits the fn unchanged plus a paired
296/// `<FN_NAME>_EDGE: hopper_runtime::MigrationEdge` constant so the
297/// layout author can compose edges via `hopper::layout_migrations!`.
298///
299/// Closes Hopper Safety Audit innovation I4 ("Schema epoch with
300/// in-place migration helpers"). Runtime chain application and
301/// atomic-per-edge `schema_epoch` bump live in
302/// `hopper_runtime::migrate`.
303///
304/// # Example
305///
306/// ```ignore
307/// #[hopper::migrate(from = 1, to = 2)]
308/// pub fn vault_v1_to_v2(body: &mut [u8]) -> ProgramResult {
309///     // Reinterpret bytes to match the epoch-2 shape.
310///     Ok(())
311/// }
312/// ```
313#[proc_macro_attribute]
314pub fn hopper_migrate(attr: TokenStream, item: TokenStream) -> TokenStream {
315    migrate::expand(attr.into(), item.into())
316        .unwrap_or_else(|e| e.to_compile_error())
317        .into()
318}
319
320/// Short alias: `#[hopper::migrate]`. Functionally identical to
321/// `#[hopper_migrate]`.
322#[proc_macro_attribute]
323pub fn migrate(attr: TokenStream, item: TokenStream) -> TokenStream {
324    hopper_migrate(attr, item)
325}
326
327// -----------------------------------------------------------------------------
328// Newly added derives (added alongside the existing surface, not replacing it):
329//   - `#[hopper::event]`. segment-tagged events with a stable tag byte.
330//   - `#[hopper::error]`. error codes linked to invariant IDs.
331//   - `#[hopper::args]`. borrowing zero-copy instruction argument parser.
332//   - `#[hopper::dynamic]`- field-level dynamic tail opt-in.
333// -----------------------------------------------------------------------------
334
335/// Derive a Hopper event: emits a stable 1-byte tag, optional segment source,
336/// a `NAME` string, a `FIELD_COUNT` const, and an `as_bytes(&self)` view for
337/// the framework's log-emission pathway.
338///
339/// # Example
340/// ```ignore
341/// #[hopper::event(tag = 7, segment = 1)]
342/// #[repr(C)]
343/// pub struct Deposited {
344///     pub amount: [u8; 8],
345///     pub depositor: [u8; 32],
346/// }
347/// ```
348#[proc_macro_attribute]
349pub fn hopper_event(attr: TokenStream, item: TokenStream) -> TokenStream {
350    event::expand(attr.into(), item.into())
351        .unwrap_or_else(|e| e.to_compile_error())
352        .into()
353}
354
355/// Short alias: `#[hopper::event]`.
356#[proc_macro_attribute]
357pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream {
358    hopper_event(attr, item)
359}
360
361/// Mark an instruction handler as an autonomous crank.
362///
363/// Attaches a `"Crank"` capability tag to the instruction descriptor
364/// in the program manifest and optionally captures
365/// `seeds(account_name = [...])` hints so a keeper-bot CLI can
366/// resolve every PDA without per-program config.
367///
368/// Cranks must be zero-arg handlers. Any value argument is a
369/// compile-time error, because the crank runner cannot invent
370/// instruction data on behalf of the caller.
371#[proc_macro_attribute]
372pub fn hopper_crank(attr: TokenStream, item: TokenStream) -> TokenStream {
373    crank::expand(attr.into(), item.into())
374        .unwrap_or_else(|e| e.to_compile_error())
375        .into()
376}
377
378/// Short alias: `#[hopper::crank]`.
379#[proc_macro_attribute]
380pub fn crank(attr: TokenStream, item: TokenStream) -> TokenStream {
381    hopper_crank(attr, item)
382}
383
384/// Generate a typed CPI surface from an on-disk Hopper manifest.
385///
386/// ```ignore
387/// hopper::declare_program!(amm, "idl/amm.json");
388/// ```
389///
390/// Emits a module with `PROGRAM_NAME`, `PROGRAM_ID_STR`, a
391/// `FINGERPRINT: [u8; 32]` compile-time manifest-hash const, and one
392/// builder per instruction. See the declare_program module for the
393/// full contract.
394#[proc_macro]
395pub fn declare_program(input: TokenStream) -> TokenStream {
396    declare_program::expand(input.into())
397        .unwrap_or_else(|e| e.to_compile_error())
398        .into()
399}
400
401/// Derive a Hopper error-code enum. Emits `code()`, `variant_name()`,
402/// `From<T> for u32`, and two const tables (`CODE_TABLE`, `INVARIANT_TABLE`)
403/// that the schema crate surfaces in the manifest.
404///
405/// Per-variant `#[invariant = "name"]` attributes are the innovation: when
406/// a runtime invariant check fails, the corresponding error carries the
407/// invariant name, and the off-chain SDK can render "Invariant `x` failed"
408/// instead of an opaque hex code.
409///
410/// # Example
411/// ```ignore
412/// #[hopper::error]
413/// #[repr(u32)]
414/// pub enum VaultError {
415///     #[invariant = "balance_nonzero"]
416///     InsufficientBalance = 0x1001,
417///     MigrationRequired,   // auto-assigned stable code
418/// }
419/// ```
420#[proc_macro_attribute]
421pub fn hopper_error(attr: TokenStream, item: TokenStream) -> TokenStream {
422    error::expand(attr.into(), item.into())
423        .unwrap_or_else(|e| e.to_compile_error())
424        .into()
425}
426
427/// Short alias: `#[hopper::error]`.
428#[proc_macro_attribute]
429pub fn error(attr: TokenStream, item: TokenStream) -> TokenStream {
430    hopper_error(attr, item)
431}
432
433/// Derive a zero-copy borrowing parser for an instruction argument struct.
434///
435/// Emits `parse(&[u8]) -> Result<&Self, ArgParseError>`, `PACKED_SIZE`,
436/// `ARG_DESCRIPTORS`, and `CU_HINT`. The `cu` attribute lets a program
437/// declare a compute-unit budget clients can inspect via the manifest before
438/// submission.
439///
440/// # Example
441/// ```ignore
442/// #[hopper::args(cu = 1200)]
443/// #[repr(C)]
444/// pub struct DepositArgs {
445///     pub amount: [u8; 8],
446///     pub memo:   [u8; 16],
447/// }
448/// ```
449#[proc_macro_attribute]
450pub fn hopper_args(attr: TokenStream, item: TokenStream) -> TokenStream {
451    args::expand(attr.into(), item.into())
452        .unwrap_or_else(|e| e.to_compile_error())
453        .into()
454}
455
456/// Short alias: `#[hopper::args]`.
457#[proc_macro_attribute]
458pub fn args(attr: TokenStream, item: TokenStream) -> TokenStream {
459    hopper_args(attr, item)
460}
461
462/// Declare which field of a `#[repr(C)]` struct is the dynamic-tail region.
463///
464/// Attaches to the **struct**, not the field, because stable Rust does not
465/// permit `#[proc_macro_attribute]` macros on struct fields. The field name
466/// is passed as a string via `field = "<name>"`.
467///
468/// # Example
469///
470/// ```ignore
471/// #[hopper::dynamic(field = "entries")]
472/// #[derive(Clone, Copy)]
473/// #[hopper::state]
474/// #[repr(C)]
475/// pub struct Ledger {
476///     pub head: WireU64,
477///     pub tail: WireU64,
478///     pub entries: DynamicRegion<LedgerEntry>,
479/// }
480/// ```
481#[proc_macro_attribute]
482pub fn hopper_dynamic(attr: TokenStream, item: TokenStream) -> TokenStream {
483    dynamic::expand(attr.into(), item.into())
484        .unwrap_or_else(|e| e.to_compile_error())
485        .into()
486}
487
488/// Short alias: `#[hopper::dynamic(field = "…")]`.
489#[proc_macro_attribute]
490pub fn dynamic(attr: TokenStream, item: TokenStream) -> TokenStream {
491    hopper_dynamic(attr, item)
492}
493
494/// Quasar-style bounded dynamic fields lowered into Hopper's fixed-body +
495/// compact-tail account layout.
496///
497/// Fields annotated with `#[tail(string<N>)]` or `#[tail(vec<T, N>)]`
498/// are removed from the fixed body, encoded into a generated `NameTail`, and
499/// exposed through generated view / owned editor helpers. `Address` / `Pubkey`
500/// vectors keep borrowed-slice views; other `T: TailElement` vectors return
501/// `HopperVec<T, N>` values. The account still uses Hopper's canonical
502/// `[body][u32 tail_len][tail_payload]` wire format.
503///
504/// # Example
505///
506/// ```ignore
507/// #[hopper::dynamic_account(disc = 7, version = 1)]
508/// pub struct Multisig {
509///     pub creator: Address,
510///     pub threshold: u8,
511///
512///     #[tail(string<32>)]
513///     pub label: String,
514///
515///     #[tail(vec<Address, 10>)]
516///     pub signers: Vec<Address>,
517///
518///     #[tail(vec<u16, 10>)]
519///     pub weights: Vec<u16>,
520/// }
521/// ```
522#[proc_macro_attribute]
523pub fn hopper_dynamic_account(attr: TokenStream, item: TokenStream) -> TokenStream {
524    dynamic_account::expand(attr.into(), item.into())
525        .unwrap_or_else(|e| e.to_compile_error())
526        .into()
527}
528
529/// Short alias: `#[hopper::dynamic_account]`.
530#[proc_macro_attribute]
531pub fn dynamic_account(attr: TokenStream, item: TokenStream) -> TokenStream {
532    hopper_dynamic_account(attr, item)
533}
534
535/// Mark a `pub const` so it is surfaced in the Anchor IDL `"constants"`
536/// array.
537///
538/// Anchor-compatible surface for `#[constant]`. The original constant is
539/// preserved unchanged; alongside it a hidden
540/// `__HOPPER_CONST_<NAME>: hopper_schema::ConstantDescriptor` sibling
541/// is emitted, capturing the stringified type and initializer
542/// expression. Collect the descriptors into a slice and hand it to
543/// `hopper_schema::AnchorIdlWithConstants` (or
544/// `AnchorIdlFromManifestWithConstants`) when emitting the IDL.
545///
546/// # Example
547///
548/// ```ignore
549/// #[hopper::constant]
550/// pub const MAX_DEPOSIT: u64 = 1_000_000;
551///
552/// // A hidden sibling const is emitted:
553/// // pub const __HOPPER_CONST_MAX_DEPOSIT: ConstantDescriptor = ...;
554///
555/// pub const PROGRAM_CONSTANTS: &[ConstantDescriptor] = &[__HOPPER_CONST_MAX_DEPOSIT];
556/// ```
557#[proc_macro_attribute]
558pub fn hopper_constant(attr: TokenStream, item: TokenStream) -> TokenStream {
559    constant::expand(attr.into(), item.into())
560        .unwrap_or_else(|e| e.to_compile_error())
561        .into()
562}
563
564/// Short alias: `#[hopper::constant]`.
565#[proc_macro_attribute]
566pub fn constant(attr: TokenStream, item: TokenStream) -> TokenStream {
567    hopper_constant(attr, item)
568}