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