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