Skip to main content

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