1#![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#[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#[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#[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#[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 #[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#[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#[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#[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#[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
450fn 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}