Skip to main content

aristo_macros/
lib.rs

1//! Aristo proc-macros.
2//!
3//! Intentionally thin: this crate runs during downstream compile time, so
4//! heavy work (project-wide cycle detection, B5b signature validation,
5//! index IO) lives in `aristo-cli`. The macros here do single-annotation
6//! validation (when the `aristo_check` cargo feature is on — default) and
7//! `include_str!` injection (`aristo_doc`, slice 30).
8//!
9//! The macros parse their arguments into `IntentArgs` / `AssumeArgs`, run
10//! validation per `validate.rs`, and (on success) emit the wrapped item
11//! unchanged — they have no runtime effect, only compile-time signal. The
12//! argument shape mirrors the subset of `aristo_core::index::IntentEntry` /
13//! `AssumeEntry` that the developer writes by hand (text, verify, parent,
14//! id) — `aristo stamp` populates the rest from source position.
15
16mod inject;
17#[cfg(feature = "aristo_instrument")]
18mod instrument;
19mod validate;
20
21use proc_macro::TokenStream;
22use quote::quote;
23use syn::parse::{Parse, ParseStream};
24use syn::{Expr, LitStr, Token};
25
26/// Parsed `#[aristo::intent("text", verify = ..., parent = ..., id = ...)]`.
27///
28/// Parsing is always-on; validation is gated by the `aristo_check` cargo
29/// feature (see `validate.rs`).
30#[derive(Default)]
31pub(crate) struct IntentArgs {
32    pub(crate) text: Option<LitStr>,
33    pub(crate) verify: Option<Expr>,
34    #[allow(dead_code)] // parent shape validation lands with slice 32
35    pub(crate) parent: Option<Expr>,
36    pub(crate) id: Option<LitStr>,
37}
38
39impl Parse for IntentArgs {
40    fn parse(input: ParseStream) -> syn::Result<Self> {
41        let mut args = IntentArgs::default();
42        if input.is_empty() {
43            return Ok(args);
44        }
45
46        // First argument is positional: a string literal carrying the
47        // annotation text. (Mockup 01 form; required at validation time.)
48        args.text = Some(input.parse::<LitStr>()?);
49
50        while input.peek(Token![,]) {
51            input.parse::<Token![,]>()?;
52            if input.is_empty() {
53                break; // trailing comma
54            }
55            let key: syn::Ident = input.parse()?;
56            input.parse::<Token![=]>()?;
57            match key.to_string().as_str() {
58                "verify" => args.verify = Some(input.parse()?),
59                "parent" => args.parent = Some(input.parse()?),
60                "id" => args.id = Some(input.parse()?),
61                other => {
62                    return Err(syn::Error::new(
63                        key.span(),
64                        format!(
65                            "unknown `intent` argument `{other}`; expected one of: verify, parent, id"
66                        ),
67                    ));
68                }
69            }
70        }
71        Ok(args)
72    }
73}
74
75/// `#[aristo::intent("...", verify = ..., parent = ..., id = ...)]`
76///
77/// Item-level annotation describing what a function / module / struct /
78/// impl / trait does. Pass-through during slice 6 — emits the wrapped item
79/// unchanged.
80#[proc_macro_attribute]
81pub fn intent(attr: TokenStream, item: TokenStream) -> TokenStream {
82    let args = match syn::parse::<IntentArgs>(attr) {
83        Ok(a) => a,
84        Err(err) => return err.to_compile_error().into(),
85    };
86    if let Err(err) = validate::validate_intent(&args) {
87        return err.to_compile_error().into();
88    }
89    match inject::doc_attribute_or_error(args.id.as_ref()) {
90        Ok(prefix) => {
91            let item_ts = proc_macro2::TokenStream::from(item);
92            quote!(#prefix #item_ts).into()
93        }
94        Err(err) => err.to_compile_error().into(),
95    }
96}
97
98/// `aristo::intent_stmt!("...", verify = ..., parent = ..., id = ...);`
99///
100/// Sub-item annotation: used inside a function body to attach intent to a
101/// statement, block, or loop that the attribute form can't reach. Per
102/// mockup 01 the parameter shape is identical to the attribute form;
103/// expansion is empty (compile-time annotation only — no runtime trace).
104///
105/// Naming note: Rust requires distinct fn names for attribute and function-
106/// like proc-macros within a single crate (E0428). Convention in the
107/// ecosystem (tokio: `#[tokio::main]` + `tokio::select!`; tracing:
108/// `#[tracing::instrument]` + `tracing::trace!`) is to use different names
109/// per kind. We follow that with the `_stmt` suffix to make the statement-
110/// position context explicit at the call site.
111#[proc_macro]
112pub fn intent_stmt(input: TokenStream) -> TokenStream {
113    match syn::parse::<IntentArgs>(input).and_then(|args| validate::validate_intent(&args)) {
114        Ok(()) => TokenStream::new(),
115        Err(err) => err.to_compile_error().into(),
116    }
117}
118
119/// Parsed `#[aristo::assume("text", parent = ..., id = ...)]`.
120///
121/// `assume` is `intent` minus `verify` per A5 — assumptions describe
122/// invariants the code RELIES ON (OS guarantees, library contracts,
123/// upstream invariants); they aren't verification targets, so passing
124/// `verify` is a category error caught at parse time with a friendly
125/// message (the user is probably reaching for `intent`).
126#[derive(Default)]
127pub(crate) struct AssumeArgs {
128    pub(crate) text: Option<LitStr>,
129    #[allow(dead_code)] // parent shape validation lands with slice 32
130    pub(crate) parent: Option<Expr>,
131    pub(crate) id: Option<LitStr>,
132}
133
134impl Parse for AssumeArgs {
135    fn parse(input: ParseStream) -> syn::Result<Self> {
136        let mut args = AssumeArgs::default();
137        if input.is_empty() {
138            return Ok(args);
139        }
140        args.text = Some(input.parse::<LitStr>()?);
141        while input.peek(Token![,]) {
142            input.parse::<Token![,]>()?;
143            if input.is_empty() {
144                break;
145            }
146            let key: syn::Ident = input.parse()?;
147            input.parse::<Token![=]>()?;
148            match key.to_string().as_str() {
149                "parent" => args.parent = Some(input.parse()?),
150                "id" => args.id = Some(input.parse()?),
151                "verify" => {
152                    return Err(syn::Error::new(
153                        key.span(),
154                        "`verify` is not allowed on `assume` (A5): assumptions describe \
155                         invariants you rely on, not properties to be verified. \
156                         Use `intent` if you meant a verifiable claim.",
157                    ));
158                }
159                other => {
160                    return Err(syn::Error::new(
161                        key.span(),
162                        format!("unknown `assume` argument `{other}`; expected one of: parent, id"),
163                    ));
164                }
165            }
166        }
167        Ok(args)
168    }
169}
170
171/// `#[aristo::assume("...", parent = ..., id = ...)]`
172///
173/// Item-level assumption: state an invariant the code relies on but does
174/// not itself enforce (an OS guarantee, a library contract, an upstream
175/// caller's promise). No `verify` argument — see `AssumeArgs` doc above.
176/// Pass-through during slice 6.
177#[proc_macro_attribute]
178pub fn assume(attr: TokenStream, item: TokenStream) -> TokenStream {
179    let args = match syn::parse::<AssumeArgs>(attr) {
180        Ok(a) => a,
181        Err(err) => return err.to_compile_error().into(),
182    };
183    if let Err(err) = validate::validate_assume(&args) {
184        return err.to_compile_error().into();
185    }
186    match inject::doc_attribute_or_error(args.id.as_ref()) {
187        Ok(prefix) => {
188            let item_ts = proc_macro2::TokenStream::from(item);
189            quote!(#prefix #item_ts).into()
190        }
191        Err(err) => err.to_compile_error().into(),
192    }
193}
194
195/// `aristo::assume_stmt!("...", parent = ..., id = ...);`
196///
197/// Sub-item assumption: used inside a function body to attach an
198/// assumption to a statement, block, or loop. Same shape as the attribute
199/// form (no `verify` per A5); empty expansion. Naming follows the
200/// `_stmt` convention from `intent_stmt!`.
201#[proc_macro]
202pub fn assume_stmt(input: TokenStream) -> TokenStream {
203    match syn::parse::<AssumeArgs>(input).and_then(|args| validate::validate_assume(&args)) {
204        Ok(()) => TokenStream::new(),
205        Err(err) => err.to_compile_error().into(),
206    }
207}
208
209/// `#[derive(aristo::instrument::Inspect)]`
210///
211/// Derive macro emitting an `inspect_<field>()` snapshot accessor per
212/// `#[inspect]`-tagged field. Type-agnostic — it never inspects the field
213/// type. Bare `#[inspect]` clones the field (any `Clone` type); `#[inspect(ret
214/// = T, with = <projector>)]` projects it through any `Fn(&FieldType) -> T`
215/// (a named path or inline closure). `name = "..."` overrides the suffix.
216/// See `instrument::inspect` for the full contract.
217#[cfg(feature = "aristo_instrument")]
218#[proc_macro_derive(Inspect, attributes(inspect))]
219pub fn instrument_inspect(input: TokenStream) -> TokenStream {
220    instrument::inspect::derive(input)
221}
222
223/// `#[aristo::instrument::expose_pub(as = "...")]`
224///
225/// Attribute macro that emits a feature-gated `pub` wrapper around a
226/// `pub(crate)` function (slice 38) or a sibling `pub` twin of a
227/// `pub(crate)` type or `impl` block (slice 39). Slice 36 is a
228/// pass-through stub.
229#[cfg(feature = "aristo_instrument")]
230#[proc_macro_attribute]
231pub fn expose_pub(attr: TokenStream, item: TokenStream) -> TokenStream {
232    instrument::expose_pub::attribute(attr, item)
233}
234
235/// `aristo::instrument::yield_point!("label")`
236///
237/// Function-like macro that emits a call into the runtime hook
238/// `aristo::instrument::__yield_point` when the `aristo_instrument`
239/// feature is on; expands to nothing otherwise. Slice 36 is a stub
240/// (empty expansion either way); slice 40 wires the runtime call.
241#[cfg(feature = "aristo_instrument")]
242#[proc_macro]
243pub fn yield_point(input: TokenStream) -> TokenStream {
244    instrument::yield_point::function_like(input)
245}
246
247/// `aristo::instrument::fault_point!("label")`
248///
249/// Function-like macro that emits a call into the fault-decision hook
250/// `aristo::instrument::__fault_point`, returning the harness's `Decision`
251/// for the SUT to branch on. Observe-only callers want `yield_point!`; this
252/// is for injecting interior faults. Gated on `aristo_instrument`.
253#[cfg(feature = "aristo_instrument")]
254#[proc_macro]
255pub fn fault_point(input: TokenStream) -> TokenStream {
256    instrument::fault_point::function_like(input)
257}