Skip to main content

statum_macros/
lib.rs

1//! Proc-macro implementation crate for Statum.
2//!
3//! Most users should depend on [`statum`](https://docs.rs/statum), which
4//! re-exports these macros with the public-facing documentation. This crate
5//! exists so the macro expansion logic can stay separate from the stable runtime
6//! traits in `statum-core`.
7//!
8//! The public macros are:
9//!
10//! - [`state`] for declaring legal lifecycle phases
11//! - [`machine`] for declaring the typed machine and durable context
12//! - [`transition`] for validating legal transition impls
13//! - [`validators`] for rebuilding typed machines from persisted data
14
15#[cfg(doctest)]
16#[doc = include_str!("../README.md")]
17mod readme_doctests {}
18
19mod analysis;
20mod cache;
21mod callsite;
22mod module_path;
23mod parser;
24mod pathing;
25mod query;
26mod syntax;
27
28moddef::moddef!(
29    flat (pub) mod {
30    },
31    flat (pub(crate)) mod {
32        presentation,
33        state,
34        machine,
35        transition,
36        validators
37    }
38);
39
40pub(crate) use presentation::{
41    PresentationAttr, PresentationTypesAttr, parse_present_attrs, parse_presentation_types_attr,
42    strip_present_attrs,
43};
44pub(crate) use syntax::{
45    ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
46    extract_derives, source_file_fingerprint,
47};
48
49use crate::callsite::{current_module_path_opt, module_path_for_span};
50use crate::{
51    LoadedMachineLookupFailure, MachinePath, ambiguous_transition_machine_error,
52    ambiguous_transition_machine_fallback_error, lookup_loaded_machine_in_module,
53    lookup_unique_loaded_machine_by_name,
54};
55use proc_macro::TokenStream;
56use proc_macro2::Span;
57use syn::spanned::Spanned;
58use syn::{Item, ItemImpl, parse_macro_input};
59
60/// Define the legal lifecycle phases for a Statum machine.
61///
62/// Apply `#[state]` to an enum with unit variants, single-field tuple
63/// variants, or named-field variants. Statum generates one marker type per
64/// variant plus the state-family traits used by `#[machine]`, `#[transition]`,
65/// and `#[validators]`.
66#[proc_macro_attribute]
67pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream {
68    let input = parse_macro_input!(item as Item);
69    let input = match input {
70        Item::Enum(item_enum) => item_enum,
71        other => return invalid_state_target_error(&other).into(),
72    };
73
74    // Validate the enum before proceeding
75    if let Some(error) = validate_state_enum(&input) {
76        return error.into();
77    }
78
79    let enum_info = match EnumInfo::from_item_enum(&input) {
80        Ok(info) => info,
81        Err(err) => return err.to_compile_error().into(),
82    };
83
84    // Store metadata in `state_enum_map`
85    store_state_enum(&enum_info);
86
87    // Generate structs and implementations dynamically
88    let expanded = generate_state_impls(&enum_info);
89
90    TokenStream::from(expanded)
91}
92
93/// Define a typed machine that carries durable context across states.
94///
95/// Apply `#[machine]` to a struct whose first generic parameter is the
96/// `#[state]` enum family. Additional type and const generics are supported
97/// after that state generic. Statum generates the typed machine surface,
98/// builders, the machine-scoped `machine::SomeState` enum, a compatibility
99/// alias `machine::State = machine::SomeState`, and helper items such as
100/// `machine::Fields` for heterogeneous batch rebuilds.
101#[proc_macro_attribute]
102pub fn machine(_attr: TokenStream, item: TokenStream) -> TokenStream {
103    let input = parse_macro_input!(item as Item);
104    let input = match input {
105        Item::Struct(item_struct) => item_struct,
106        other => return invalid_machine_target_error(&other).into(),
107    };
108
109    let machine_info = match MachineInfo::from_item_struct(&input) {
110        Ok(info) => info,
111        Err(err) => return err.to_compile_error().into(),
112    };
113
114    // Validate the struct before proceeding
115    if let Some(error) = validate_machine_struct(&input, &machine_info) {
116        return error.into();
117    }
118
119    // Store metadata in `machine_map`
120    store_machine_struct(&machine_info);
121
122    // Generate any required structs or implementations dynamically
123    let expanded = generate_machine_impls(&machine_info, &input);
124
125    TokenStream::from(expanded)
126}
127
128/// Validate and generate legal transitions for one source state.
129///
130/// Apply `#[transition]` to an `impl Machine<CurrentState>` block. Each method
131/// must consume `self` and return a legal `Machine<NextState>` shape or a
132/// source-declared type alias that expands to that shape, or a supported
133/// wrapper around it, such as `Result<Machine<NextState>, E>`,
134/// `Option<Machine<NextState>>`, or
135/// `statum::Branch<Machine<Left>, Machine<Right>>`.
136#[proc_macro_attribute]
137pub fn transition(
138    _attr: proc_macro::TokenStream,
139    item: proc_macro::TokenStream,
140) -> proc_macro::TokenStream {
141    let input = parse_macro_input!(item as ItemImpl);
142
143    // -- Step 1: Parse
144    let tr_impl = match parse_transition_impl(&input) {
145        Ok(parsed) => parsed,
146        Err(err) => return err.into(),
147    };
148
149    let module_path = match resolved_current_module_path(tr_impl.machine_span, "#[transition]") {
150        Ok(path) => path,
151        Err(err) => return err,
152    };
153
154    let machine_path: MachinePath = module_path.clone().into();
155    let machine_info_owned =
156        match lookup_loaded_machine_in_module(&machine_path, &tr_impl.machine_name) {
157            Ok(info) => Some(info),
158            Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
159                return ambiguous_transition_machine_error(
160                    &tr_impl.machine_name,
161                    &module_path,
162                    &candidates,
163                    tr_impl.machine_span,
164                )
165                .into();
166            }
167            Err(LoadedMachineLookupFailure::NotFound) => {
168                match lookup_unique_loaded_machine_by_name(&tr_impl.machine_name) {
169                    Ok(info) => Some(info),
170                    Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
171                        return ambiguous_transition_machine_fallback_error(
172                            &tr_impl.machine_name,
173                            &module_path,
174                            &candidates,
175                            tr_impl.machine_span,
176                        )
177                        .into();
178                    }
179                    Err(LoadedMachineLookupFailure::NotFound) => None,
180                }
181            }
182        };
183    let machine_info = match machine_info_owned.as_ref() {
184        Some(info) => info,
185        None => {
186            return missing_transition_machine_error(
187                &tr_impl.machine_name,
188                &module_path,
189                tr_impl.machine_span,
190            )
191            .into();
192        }
193    };
194
195    if let Some(err) = validate_transition_functions(&tr_impl, machine_info) {
196        return err.into();
197    }
198
199    // -- Step 3: Generate new code
200    let expanded = generate_transition_impl(&input, &tr_impl, machine_info);
201
202    // Combine expanded code with the original `impl` if needed
203    // or simply return the expanded code
204    expanded.into()
205}
206
207/// Rebuild typed machines from persisted data.
208///
209/// Apply `#[validators(Machine)]` to an `impl PersistedRow` block. Statum
210/// expects one `is_{state}` method per state variant and generates
211/// `into_machine()`, `.into_machines()`, and `.into_machines_by(...)` helpers
212/// for typed rehydration. Validator methods can return `Result<T, _>` for
213/// ordinary membership checks or `Validation<T>` when rebuild reports should
214/// carry stable rejection details through `.build_report()` and
215/// `.build_reports()`.
216#[proc_macro_attribute]
217pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
218    let item_impl = parse_macro_input!(item as ItemImpl);
219    let module_path = match resolved_current_module_path(item_impl.self_ty.span(), "#[validators]")
220    {
221        Ok(path) => path,
222        Err(err) => return err,
223    };
224    parse_validators(attr, item_impl, &module_path)
225}
226
227fn resolved_current_module_path(span: Span, macro_name: &str) -> Result<String, TokenStream> {
228    let resolved = module_path_for_span(span)
229        .or_else(current_module_path_opt)
230        .or_else(|| {
231            crate::callsite::source_info_for_span(span)
232                .is_none()
233                .then_some("crate".to_string())
234        });
235
236    resolved.ok_or_else(|| {
237        let message = format!(
238            "Internal error: could not resolve the module path for `{macro_name}` at this call site."
239        );
240        quote::quote_spanned! { span =>
241            compile_error!(#message);
242        }
243        .into()
244    })
245}