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}