Skip to main content

arkhe_forge_macros/
lib.rs

1//! Runtime-layer derive + attribute macros for ArkheForge.
2//!
3//! Three derives — `#[derive(ArkheComponent)]`, `#[derive(ArkheAction)]`,
4//! `#[derive(ArkheEvent)]` — emit the sealed-trait impl plus the marker-trait
5//! impl pinning `TYPE_CODE` / `SCHEMA_VERSION` (and, for `ArkheAction`,
6//! `BAND` / `IDEMPOTENT`).
7//!
8//! One attribute — `#[arkhe_pure]` — asserts that an `Action::compute`
9//! body conforms to E14.L1 Subset-Rust purity (clock / RNG / I/O / FFI
10//! deny-list). Backed by `arkhe-subset-rust-check`.
11//!
12//! ## Compile-time validation
13//!
14//! * `#[arkhe(type_code = N)]` mandatory; `N` must lie in the appropriate
15//!   reserved sub-range (or the shell-scoped extension range).
16//! * First named struct field must be `schema_version: u16` (wire version tag).
17//! * `#[arkhe(band = K)]` mandatory for `ArkheAction`; `K ∈ {1, 2, 3}`.
18//! * `#[arkhe(idempotent)]` opt-in on `ArkheAction` — requires the struct
19//!   to carry an `idempotency_key` field.
20//! * Field-level `#[arkhe(canonical_sort)]` on `ArkheComponent` / `ArkheEvent`
21//!   is allowed only on `Vec<T>` / `BTreeSet<T>` fields.
22//!
23//! ## Namespace
24//!
25//! Do not confuse with `arkhe-macros` (L0 kernel derives). This crate
26//! targets the Runtime traits in `arkhe_forge_core::{component, action,
27//! event, sealed}`; the L0 derive crate is orthogonal.
28
29#![forbid(unsafe_code)]
30
31use darling::{FromDeriveInput, FromField};
32use proc_macro::TokenStream;
33use proc_macro2::TokenStream as TokenStream2;
34use quote::quote;
35use syn::{
36    parse_macro_input, punctuated::Punctuated, spanned::Spanned, Data, DataStruct, DeriveInput,
37    Expr, Field, Fields, GenericArgument, Ident, Lit, PathArguments, Token, Type, TypeArray,
38};
39
40// ===================== Component =====================
41
42#[derive(FromDeriveInput)]
43#[darling(attributes(arkhe), supports(struct_named))]
44struct ComponentAttrs {
45    type_code: u32,
46    #[darling(default = "default_schema_version")]
47    schema_version: u16,
48}
49
50#[derive(FromField, Default)]
51#[darling(attributes(arkhe), default)]
52struct FieldAttrs {
53    canonical_sort: bool,
54}
55
56fn default_schema_version() -> u16 {
57    1
58}
59
60/// Derive `ArkheComponent` — emits the sealed-trait impl and the marker-trait
61/// impl pinning `TYPE_CODE` and `SCHEMA_VERSION`.
62///
63/// See crate-level documentation for attribute grammar and validation rules.
64#[proc_macro_derive(ArkheComponent, attributes(arkhe))]
65pub fn derive_arkhe_component(input: TokenStream) -> TokenStream {
66    let input = parse_macro_input!(input as DeriveInput);
67    let attrs = match ComponentAttrs::from_derive_input(&input) {
68        Ok(a) => a,
69        Err(e) => return e.write_errors().into(),
70    };
71    match derive_component_impl(&input, attrs) {
72        Ok(ts) => ts.into(),
73        Err(err) => err.to_compile_error().into(),
74    }
75}
76
77fn derive_component_impl(input: &DeriveInput, attrs: ComponentAttrs) -> syn::Result<TokenStream2> {
78    validate_type_code(attrs.type_code, TypeCodeKind::Component, &input.ident)?;
79    validate_schema_version_first_field(&input.data, &input.ident)?;
80    validate_canonical_sort_fields(&input.data)?;
81
82    let name = &input.ident;
83    let (impl_g, ty_g, where_g) = input.generics.split_for_impl();
84    let type_code = attrs.type_code;
85    let schema_version = attrs.schema_version;
86
87    Ok(quote! {
88        #[automatically_derived]
89        impl #impl_g ::arkhe_forge_core::__sealed::__Sealed
90            for #name #ty_g #where_g {}
91
92        #[automatically_derived]
93        impl #impl_g ::arkhe_forge_core::component::ArkheComponent
94            for #name #ty_g #where_g
95        {
96            const TYPE_CODE: u32 = #type_code;
97            const SCHEMA_VERSION: u16 = #schema_version;
98        }
99    })
100}
101
102// ===================== Action =====================
103
104#[derive(FromDeriveInput)]
105#[darling(attributes(arkhe), supports(struct_named))]
106struct ActionAttrs {
107    type_code: u32,
108    #[darling(default = "default_schema_version")]
109    schema_version: u16,
110    band: u8,
111    #[darling(default)]
112    idempotent: bool,
113}
114
115/// Derive `ArkheAction` — emits the sealed-trait impl and the marker-trait
116/// impl pinning `TYPE_CODE`, `SCHEMA_VERSION`, `BAND`, `IDEMPOTENT`.
117///
118/// See crate-level documentation for attribute grammar and validation rules.
119#[proc_macro_derive(ArkheAction, attributes(arkhe))]
120pub fn derive_arkhe_action(input: TokenStream) -> TokenStream {
121    let input = parse_macro_input!(input as DeriveInput);
122    let attrs = match ActionAttrs::from_derive_input(&input) {
123        Ok(a) => a,
124        Err(e) => return e.write_errors().into(),
125    };
126    match derive_action_impl(&input, attrs) {
127        Ok(ts) => ts.into(),
128        Err(err) => err.to_compile_error().into(),
129    }
130}
131
132fn derive_action_impl(input: &DeriveInput, attrs: ActionAttrs) -> syn::Result<TokenStream2> {
133    validate_type_code(attrs.type_code, TypeCodeKind::Action, &input.ident)?;
134    validate_schema_version_first_field(&input.data, &input.ident)?;
135    validate_band(attrs.band, &input.ident)?;
136    if attrs.idempotent {
137        validate_idempotency_key_field(&input.data, &input.ident)?;
138    }
139
140    let name = &input.ident;
141    let (impl_g, ty_g, where_g) = input.generics.split_for_impl();
142    let type_code = attrs.type_code;
143    let schema_version = attrs.schema_version;
144    let band = attrs.band;
145    let idempotent = attrs.idempotent;
146
147    Ok(quote! {
148        #[automatically_derived]
149        impl #impl_g ::arkhe_forge_core::__sealed::__Sealed
150            for #name #ty_g #where_g {}
151
152        #[automatically_derived]
153        impl #impl_g ::arkhe_forge_core::action::ArkheAction
154            for #name #ty_g #where_g
155        {
156            const TYPE_CODE: u32 = #type_code;
157            const SCHEMA_VERSION: u16 = #schema_version;
158            const BAND: ::arkhe_forge_core::action::Band = #band;
159            const IDEMPOTENT: bool = #idempotent;
160        }
161
162        // Kernel-side trait stack — paired with the forge-side stack
163        // above so `Kernel::register_action::<Self>()` accepts this
164        // type. The kernel's blanket
165        // `impl<T: ActionDeriv + ActionCompute> Action for T` then
166        // supplies the postcard-canonical default methods.
167        //
168        // `_sealed::Sealed` is `#[doc(hidden)] pub` in `arkhe-kernel`.
169        // arkhe-macros (the kernel's own derive crate, crates.io 0.13)
170        // is the conventional emitter; arkhe-forge-macros is the
171        // workspace-internal sibling and emits the same shape under
172        // the same A11 sanctioned-derive convention.
173        #[automatically_derived]
174        impl #impl_g ::arkhe_kernel::state::traits::_sealed::Sealed
175            for #name #ty_g #where_g {}
176
177        #[automatically_derived]
178        impl #impl_g ::arkhe_kernel::state::traits::ActionDeriv
179            for #name #ty_g #where_g
180        {
181            const TYPE_CODE: ::arkhe_kernel::abi::TypeCode =
182                ::arkhe_kernel::abi::TypeCode(#type_code);
183            const SCHEMA_VERSION: u32 = #schema_version as u32;
184        }
185
186        #[automatically_derived]
187        impl #impl_g ::arkhe_kernel::state::traits::ActionCompute
188            for #name #ty_g #where_g
189        {
190            fn compute(
191                &self,
192                ctx: &::arkhe_kernel::state::ActionContext<'_>,
193            ) -> ::std::vec::Vec<::arkhe_kernel::state::Op> {
194                ::arkhe_forge_core::bridge::kernel_compute(self, ctx)
195            }
196        }
197    })
198}
199
200// ===================== Event =====================
201
202#[derive(FromDeriveInput)]
203#[darling(attributes(arkhe), supports(struct_named))]
204struct EventAttrs {
205    type_code: u32,
206    #[darling(default = "default_schema_version")]
207    schema_version: u16,
208}
209
210/// Derive `ArkheEvent` — emits the sealed-trait impl and the marker-trait
211/// impl pinning `TYPE_CODE` and `SCHEMA_VERSION`.
212///
213/// See crate-level documentation for attribute grammar and validation rules.
214#[proc_macro_derive(ArkheEvent, attributes(arkhe))]
215pub fn derive_arkhe_event(input: TokenStream) -> TokenStream {
216    let input = parse_macro_input!(input as DeriveInput);
217    let attrs = match EventAttrs::from_derive_input(&input) {
218        Ok(a) => a,
219        Err(e) => return e.write_errors().into(),
220    };
221    match derive_event_impl(&input, attrs) {
222        Ok(ts) => ts.into(),
223        Err(err) => err.to_compile_error().into(),
224    }
225}
226
227fn derive_event_impl(input: &DeriveInput, attrs: EventAttrs) -> syn::Result<TokenStream2> {
228    validate_type_code(attrs.type_code, TypeCodeKind::Event, &input.ident)?;
229    validate_schema_version_first_field(&input.data, &input.ident)?;
230    validate_canonical_sort_fields(&input.data)?;
231
232    let name = &input.ident;
233    let (impl_g, ty_g, where_g) = input.generics.split_for_impl();
234    let type_code = attrs.type_code;
235    let schema_version = attrs.schema_version;
236
237    Ok(quote! {
238        #[automatically_derived]
239        impl #impl_g ::arkhe_forge_core::__sealed::__Sealed
240            for #name #ty_g #where_g {}
241
242        #[automatically_derived]
243        impl #impl_g ::arkhe_forge_core::event::ArkheEvent
244            for #name #ty_g #where_g
245        {
246            const TYPE_CODE: u32 = #type_code;
247            const SCHEMA_VERSION: u16 = #schema_version;
248        }
249    })
250}
251
252// ===================== Pure (E14.L1 Subset-Rust) =====================
253
254/// `#[arkhe_pure]` — attribute macro asserting that an `Action::compute`-
255/// style function body conforms to E14.L1 Subset-Rust purity. Backed by
256/// `arkhe-subset-rust-check`; emits a `compile_error!` per violation
257/// site. Spec anchor: E14.L1 (MC).
258///
259/// Uses the default `arkhe-subset-rust-check` policy (clock + RNG +
260/// I/O + FFI deny-list); additive deny-list extensions through the
261/// `Policy::*` constructors are non-breaking.
262///
263/// ## Usage
264///
265/// ```ignore
266/// use arkhe_forge_core::arkhe_pure;
267///
268/// #[arkhe_pure]
269/// fn compute(a: u32, b: u32) -> u32 {
270///     a.wrapping_add(b).wrapping_mul(2)   // pure — passes
271/// }
272///
273/// // The following triggers a compile error:
274/// //   #[arkhe_pure]
275/// //   fn compute_bad() -> u128 {
276/// //       std::time::Instant::now().elapsed().as_nanos()  // E14.L1 violation
277/// //   }
278/// ```
279///
280/// The macro re-emits the original function unchanged on success, so it
281/// has zero runtime cost and stacks with other attributes (`#[inline]`,
282/// `#[cfg(...)]`, etc.).
283#[proc_macro_attribute]
284pub fn arkhe_pure(_args: TokenStream, item: TokenStream) -> TokenStream {
285    let item_clone = item.clone();
286    let item_fn = parse_macro_input!(item_clone as syn::ItemFn);
287    let violations = arkhe_subset_rust_check::check_purity_default(&item_fn);
288    if violations.is_empty() {
289        return item;
290    }
291    let errors: TokenStream2 = violations
292        .into_iter()
293        .map(|v| {
294            let msg = format!(
295                "E14.L1 Subset-Rust purity violation: \
296                 `{}` ({}). \
297                 see arkhe-subset-rust-check policy.",
298                v.denied_path, v.reason
299            );
300            quote! { ::core::compile_error!(#msg); }
301        })
302        .collect();
303    let original: TokenStream2 = item.into();
304    quote! {
305        #errors
306        #original
307    }
308    .into()
309}
310
311// ===================== Validation helpers =====================
312
313#[derive(Copy, Clone)]
314enum TypeCodeKind {
315    Component,
316    Action,
317    Event,
318}
319
320impl TypeCodeKind {
321    fn core_range(self) -> (u32, u32) {
322        match self {
323            Self::Component => (0x0003_0000, 0x0003_0EFF),
324            Self::Action => (0x0001_0000, 0x0001_FFFF),
325            Self::Event => (0x0003_0F00, 0x0003_FFFF),
326        }
327    }
328    fn label(self) -> &'static str {
329        match self {
330            Self::Component => "ArkheComponent",
331            Self::Action => "ArkheAction",
332            Self::Event => "ArkheEvent",
333        }
334    }
335}
336
337const SHELL_RANGE: (u32, u32) = (0x0100_0000, 0xEFFF_FFFF);
338
339fn validate_type_code(type_code: u32, kind: TypeCodeKind, name: &Ident) -> syn::Result<()> {
340    let (core_lo, core_hi) = kind.core_range();
341    let (shell_lo, shell_hi) = SHELL_RANGE;
342    let in_core = (core_lo..=core_hi).contains(&type_code);
343    let in_shell = (shell_lo..=shell_hi).contains(&type_code);
344    if in_core || in_shell {
345        return Ok(());
346    }
347    Err(syn::Error::new(
348        name.span(),
349        format!(
350            "{} type_code 0x{:08X} out of range; core: 0x{:08X}..=0x{:08X}, shell-scoped: 0x{:08X}..=0x{:08X}",
351            kind.label(),
352            type_code,
353            core_lo,
354            core_hi,
355            shell_lo,
356            shell_hi,
357        ),
358    ))
359}
360
361fn named_fields<'a>(data: &'a Data, name: &Ident) -> syn::Result<&'a Punctuated<Field, Token![,]>> {
362    match data {
363        Data::Struct(DataStruct {
364            fields: Fields::Named(f),
365            ..
366        }) => Ok(&f.named),
367        _ => Err(syn::Error::new(
368            name.span(),
369            "arkhe-forge-macros derives require a struct with named fields",
370        )),
371    }
372}
373
374fn validate_schema_version_first_field(data: &Data, name: &Ident) -> syn::Result<()> {
375    let fields = named_fields(data, name)?;
376    let first = fields.first().ok_or_else(|| {
377        syn::Error::new(
378            name.span(),
379            "struct must have `schema_version: u16` as its first field",
380        )
381    })?;
382    let ident = first.ident.as_ref().ok_or_else(|| {
383        syn::Error::new(first.span(), "first field must be named `schema_version`")
384    })?;
385    if ident != "schema_version" {
386        return Err(syn::Error::new(
387            ident.span(),
388            format!(
389                "first field must be named `schema_version`, got `{}`",
390                ident
391            ),
392        ));
393    }
394    if !is_u16(&first.ty) {
395        return Err(syn::Error::new(
396            first.ty.span(),
397            "`schema_version` field must be of type `u16`",
398        ));
399    }
400    Ok(())
401}
402
403fn is_u16(ty: &Type) -> bool {
404    if let Type::Path(tp) = ty {
405        if tp.qself.is_none() {
406            if let Some(seg) = tp.path.segments.last() {
407                return seg.ident == "u16" && seg.arguments.is_empty();
408            }
409        }
410    }
411    false
412}
413
414fn validate_band(band: u8, name: &Ident) -> syn::Result<()> {
415    if (1..=3).contains(&band) {
416        return Ok(());
417    }
418    Err(syn::Error::new(
419        name.span(),
420        format!(
421            "ArkheAction band must be 1 (Core), 2 (Projection), or 3 (Protocol); got {}",
422            band
423        ),
424    ))
425}
426
427fn validate_idempotency_key_field(data: &Data, name: &Ident) -> syn::Result<()> {
428    let fields = named_fields(data, name)?;
429    for field in fields {
430        let Some(ident) = &field.ident else {
431            continue;
432        };
433        if ident != "idempotency_key" {
434            continue;
435        }
436        if !is_option_byte_array_16(&field.ty) {
437            return Err(syn::Error::new(
438                field.ty.span(),
439                "`idempotency_key` field must have the exact type `Option<[u8; 16]>`",
440            ));
441        }
442        return Ok(());
443    }
444    Err(syn::Error::new(
445        name.span(),
446        "#[arkhe(idempotent)] requires field `idempotency_key: Option<[u8; 16]>`",
447    ))
448}
449
450/// Match `Option<[u8; 16]>` — last path segment `Option`, single angle-bracketed
451/// generic argument that is `[u8; 16]`. Rejects fully-qualified path forms
452/// (`std::option::Option<...>`) since the canonical Rust prelude exposes
453/// `Option` unqualified — derive consumers should follow suit.
454fn is_option_byte_array_16(ty: &Type) -> bool {
455    let Type::Path(tp) = ty else {
456        return false;
457    };
458    if tp.qself.is_some() {
459        return false;
460    }
461    let Some(seg) = tp.path.segments.last() else {
462        return false;
463    };
464    if seg.ident != "Option" {
465        return false;
466    }
467    let PathArguments::AngleBracketed(generic) = &seg.arguments else {
468        return false;
469    };
470    if generic.args.len() != 1 {
471        return false;
472    }
473    let GenericArgument::Type(inner) = &generic.args[0] else {
474        return false;
475    };
476    is_byte_array_16(inner)
477}
478
479fn is_byte_array_16(ty: &Type) -> bool {
480    let Type::Array(TypeArray { elem, len, .. }) = ty else {
481        return false;
482    };
483    if !is_u8(elem) {
484        return false;
485    }
486    let Expr::Lit(lit) = len else {
487        return false;
488    };
489    let Lit::Int(int) = &lit.lit else {
490        return false;
491    };
492    int.base10_parse::<usize>().is_ok_and(|n| n == 16)
493}
494
495fn is_u8(ty: &Type) -> bool {
496    let Type::Path(tp) = ty else {
497        return false;
498    };
499    if tp.qself.is_some() {
500        return false;
501    }
502    tp.path
503        .segments
504        .last()
505        .is_some_and(|s| s.ident == "u8" && s.arguments.is_empty())
506}
507
508fn validate_canonical_sort_fields(data: &Data) -> syn::Result<()> {
509    let fields = match data {
510        Data::Struct(DataStruct {
511            fields: Fields::Named(f),
512            ..
513        }) => &f.named,
514        _ => return Ok(()),
515    };
516    for field in fields {
517        let field_attrs = FieldAttrs::from_field(field)
518            .map_err(|e| syn::Error::new(field.span(), e.to_string()))?;
519        if field_attrs.canonical_sort && !is_vec_or_btreeset(&field.ty) {
520            return Err(syn::Error::new(
521                field.ty.span(),
522                "#[arkhe(canonical_sort)] is allowed only on `Vec<T>` or `BTreeSet<T>` fields",
523            ));
524        }
525    }
526    Ok(())
527}
528
529fn is_vec_or_btreeset(ty: &Type) -> bool {
530    if let Type::Path(tp) = ty {
531        if tp.qself.is_none() {
532            if let Some(seg) = tp.path.segments.last() {
533                return seg.ident == "Vec" || seg.ident == "BTreeSet";
534            }
535        }
536    }
537    false
538}