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}