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;
17mod validate;
18
19use proc_macro::TokenStream;
20use quote::quote;
21use syn::parse::{Parse, ParseStream};
22use syn::{Expr, LitStr, Token};
23
24/// Parsed `#[aristo::intent("text", verify = ..., parent = ..., id = ...)]`.
25///
26/// Parsing is always-on; validation is gated by the `aristo_check` cargo
27/// feature (see `validate.rs`).
28#[derive(Default)]
29pub(crate) struct IntentArgs {
30    pub(crate) text: Option<LitStr>,
31    pub(crate) verify: Option<Expr>,
32    #[allow(dead_code)] // parent shape validation lands with slice 32
33    pub(crate) parent: Option<Expr>,
34    pub(crate) id: Option<LitStr>,
35}
36
37impl Parse for IntentArgs {
38    fn parse(input: ParseStream) -> syn::Result<Self> {
39        let mut args = IntentArgs::default();
40        if input.is_empty() {
41            return Ok(args);
42        }
43
44        // First argument is positional: a string literal carrying the
45        // annotation text. (Mockup 01 form; required at validation time.)
46        args.text = Some(input.parse::<LitStr>()?);
47
48        while input.peek(Token![,]) {
49            input.parse::<Token![,]>()?;
50            if input.is_empty() {
51                break; // trailing comma
52            }
53            let key: syn::Ident = input.parse()?;
54            input.parse::<Token![=]>()?;
55            match key.to_string().as_str() {
56                "verify" => args.verify = Some(input.parse()?),
57                "parent" => args.parent = Some(input.parse()?),
58                "id" => args.id = Some(input.parse()?),
59                other => {
60                    return Err(syn::Error::new(
61                        key.span(),
62                        format!(
63                            "unknown `intent` argument `{other}`; expected one of: verify, parent, id"
64                        ),
65                    ));
66                }
67            }
68        }
69        Ok(args)
70    }
71}
72
73/// `#[aristo::intent("...", verify = ..., parent = ..., id = ...)]`
74///
75/// Item-level annotation describing what a function / module / struct /
76/// impl / trait does. Pass-through during slice 6 — emits the wrapped item
77/// unchanged.
78#[proc_macro_attribute]
79pub fn intent(attr: TokenStream, item: TokenStream) -> TokenStream {
80    let args = match syn::parse::<IntentArgs>(attr) {
81        Ok(a) => a,
82        Err(err) => return err.to_compile_error().into(),
83    };
84    if let Err(err) = validate::validate_intent(&args) {
85        return err.to_compile_error().into();
86    }
87    match inject::doc_attribute_or_error(args.id.as_ref()) {
88        Ok(prefix) => {
89            let item_ts = proc_macro2::TokenStream::from(item);
90            quote!(#prefix #item_ts).into()
91        }
92        Err(err) => err.to_compile_error().into(),
93    }
94}
95
96/// `aristo::intent_stmt!("...", verify = ..., parent = ..., id = ...);`
97///
98/// Sub-item annotation: used inside a function body to attach intent to a
99/// statement, block, or loop that the attribute form can't reach. Per
100/// mockup 01 the parameter shape is identical to the attribute form;
101/// expansion is empty (compile-time annotation only — no runtime trace).
102///
103/// Naming note: Rust requires distinct fn names for attribute and function-
104/// like proc-macros within a single crate (E0428). Convention in the
105/// ecosystem (tokio: `#[tokio::main]` + `tokio::select!`; tracing:
106/// `#[tracing::instrument]` + `tracing::trace!`) is to use different names
107/// per kind. We follow that with the `_stmt` suffix to make the statement-
108/// position context explicit at the call site.
109#[proc_macro]
110pub fn intent_stmt(input: TokenStream) -> TokenStream {
111    match syn::parse::<IntentArgs>(input).and_then(|args| validate::validate_intent(&args)) {
112        Ok(()) => TokenStream::new(),
113        Err(err) => err.to_compile_error().into(),
114    }
115}
116
117/// Parsed `#[aristo::assume("text", parent = ..., id = ...)]`.
118///
119/// `assume` is `intent` minus `verify` per A5 — assumptions describe
120/// invariants the code RELIES ON (OS guarantees, library contracts,
121/// upstream invariants); they aren't verification targets, so passing
122/// `verify` is a category error caught at parse time with a friendly
123/// message (the user is probably reaching for `intent`).
124#[derive(Default)]
125pub(crate) struct AssumeArgs {
126    pub(crate) text: Option<LitStr>,
127    #[allow(dead_code)] // parent shape validation lands with slice 32
128    pub(crate) parent: Option<Expr>,
129    pub(crate) id: Option<LitStr>,
130}
131
132impl Parse for AssumeArgs {
133    fn parse(input: ParseStream) -> syn::Result<Self> {
134        let mut args = AssumeArgs::default();
135        if input.is_empty() {
136            return Ok(args);
137        }
138        args.text = Some(input.parse::<LitStr>()?);
139        while input.peek(Token![,]) {
140            input.parse::<Token![,]>()?;
141            if input.is_empty() {
142                break;
143            }
144            let key: syn::Ident = input.parse()?;
145            input.parse::<Token![=]>()?;
146            match key.to_string().as_str() {
147                "parent" => args.parent = Some(input.parse()?),
148                "id" => args.id = Some(input.parse()?),
149                "verify" => {
150                    return Err(syn::Error::new(
151                        key.span(),
152                        "`verify` is not allowed on `assume` (A5): assumptions describe \
153                         invariants you rely on, not properties to be verified. \
154                         Use `intent` if you meant a verifiable claim.",
155                    ));
156                }
157                other => {
158                    return Err(syn::Error::new(
159                        key.span(),
160                        format!("unknown `assume` argument `{other}`; expected one of: parent, id"),
161                    ));
162                }
163            }
164        }
165        Ok(args)
166    }
167}
168
169/// `#[aristo::assume("...", parent = ..., id = ...)]`
170///
171/// Item-level assumption: state an invariant the code relies on but does
172/// not itself enforce (an OS guarantee, a library contract, an upstream
173/// caller's promise). No `verify` argument — see `AssumeArgs` doc above.
174/// Pass-through during slice 6.
175#[proc_macro_attribute]
176pub fn assume(attr: TokenStream, item: TokenStream) -> TokenStream {
177    let args = match syn::parse::<AssumeArgs>(attr) {
178        Ok(a) => a,
179        Err(err) => return err.to_compile_error().into(),
180    };
181    if let Err(err) = validate::validate_assume(&args) {
182        return err.to_compile_error().into();
183    }
184    match inject::doc_attribute_or_error(args.id.as_ref()) {
185        Ok(prefix) => {
186            let item_ts = proc_macro2::TokenStream::from(item);
187            quote!(#prefix #item_ts).into()
188        }
189        Err(err) => err.to_compile_error().into(),
190    }
191}
192
193/// `aristo::assume_stmt!("...", parent = ..., id = ...);`
194///
195/// Sub-item assumption: used inside a function body to attach an
196/// assumption to a statement, block, or loop. Same shape as the attribute
197/// form (no `verify` per A5); empty expansion. Naming follows the
198/// `_stmt` convention from `intent_stmt!`.
199#[proc_macro]
200pub fn assume_stmt(input: TokenStream) -> TokenStream {
201    match syn::parse::<AssumeArgs>(input).and_then(|args| validate::validate_assume(&args)) {
202        Ok(()) => TokenStream::new(),
203        Err(err) => err.to_compile_error().into(),
204    }
205}