1#![cfg_attr(not(doctest), doc = include_str!("../README.md"))]
4
5use proc_macro2::Span;
6use quote::{ToTokens, quote};
7use syn::{
8 Attribute, Block, Expr, ExprClosure, Ident, Meta, Pat, Token,
9 parse::{Parse, ParseStream, Result},
10 parse_quote,
11 punctuated::Punctuated,
12 spanned::Spanned,
13};
14
15#[derive(Debug)]
17pub struct Spec {
18 pub requires: Vec<Condition>,
20 pub maintains: Vec<Condition>,
22 pub ensures: Vec<ConditionClosure>,
24}
25
26#[derive(Debug)]
28pub struct Condition {
29 pub expr: Expr,
31 pub cfg: Option<Meta>,
36}
37
38#[derive(Debug)]
40pub struct ConditionClosure {
41 pub closure: ExprClosure,
43 pub cfg: Option<Meta>,
48}
49
50impl Parse for Spec {
51 fn parse(input: ParseStream) -> Result<Self> {
52 let args = Punctuated::<SpecArg, Token![,]>::parse_terminated(input)?;
53
54 let mut last_arg_order: Option<ArgOrder> = None;
55 let mut requires: Vec<Condition> = vec![];
56 let mut maintains: Vec<Condition> = vec![];
57 let mut binds_pattern: Option<Pat> = None;
58 let mut ensures_exprs: Vec<Condition> = vec![];
59
60 for arg in args {
61 let current_arg_order = arg.get_order();
62 if let Some(last_order) = last_arg_order {
63 if current_arg_order < last_order {
64 return Err(syn::Error::new(
65 arg.get_keyword_span(),
66 "parameters are out of order: their order must be `requires`, `maintains`, `binds`, `ensures`",
67 ));
68 }
69 }
70 last_arg_order = Some(current_arg_order);
71
72 match arg {
73 SpecArg::Requires { cfg, expr, .. } => {
74 if let Expr::Array(conditions) = expr {
75 requires.extend(conditions.elems.into_iter().map(|expr| Condition {
76 expr,
77 cfg: cfg.clone(),
78 }));
79 } else {
80 requires.push(Condition { expr, cfg });
81 }
82 }
83 SpecArg::Maintains { cfg, expr, .. } => {
84 if let Expr::Array(conditions) = expr {
85 maintains.extend(conditions.elems.into_iter().map(|expr| Condition {
86 expr,
87 cfg: cfg.clone(),
88 }));
89 } else {
90 maintains.push(Condition { expr, cfg });
91 }
92 }
93 SpecArg::Binds { keyword, pattern } => {
94 if binds_pattern.is_some() {
95 return Err(syn::Error::new(
96 keyword.span(),
97 "multiple `binds` parameters are not allowed",
98 ));
99 }
100 binds_pattern = Some(pattern);
101 }
102 SpecArg::Ensures { cfg, expr, .. } => {
103 if let Expr::Array(conditions) = expr {
104 ensures_exprs.extend(conditions.elems.into_iter().map(|expr| Condition {
105 expr,
106 cfg: cfg.clone(),
107 }));
108 } else {
109 ensures_exprs.push(Condition { expr, cfg });
110 }
111 }
112 }
113 }
114
115 let default_closure_pattern = binds_pattern
116 .as_ref()
117 .map(|p| p.to_token_stream())
118 .unwrap_or_else(|| quote! { output });
119
120 let ensures = ensures_exprs
121 .into_iter()
122 .map(|condition| {
123 let closure: ExprClosure = if let Expr::Closure(closure) = condition.expr {
124 closure
125 } else {
126 let inner_condition = condition.expr;
128 parse_quote! { |#default_closure_pattern| #inner_condition }
129 };
130 Ok(ConditionClosure {
131 closure,
132 cfg: condition.cfg,
133 })
134 })
135 .collect::<Result<Vec<ConditionClosure>>>()?;
136
137 Ok(Spec {
138 requires,
139 maintains,
140 ensures,
141 })
142 }
143}
144
145#[derive(PartialEq, PartialOrd, Clone, Copy, Debug)]
146enum ArgOrder {
147 Requires,
148 Maintains,
149 Binds,
150 Ensures,
151}
152
153enum SpecArg {
155 Requires {
156 keyword: kw::requires,
157 cfg: Option<Meta>,
158 expr: Expr,
159 },
160 Ensures {
161 keyword: kw::ensures,
162 cfg: Option<Meta>,
163 expr: Expr,
164 },
165 Maintains {
166 keyword: kw::maintains,
167 cfg: Option<Meta>,
168 expr: Expr,
169 },
170 Binds {
171 keyword: kw::binds,
172 pattern: Pat,
173 },
174}
175
176impl SpecArg {
177 fn get_order(&self) -> ArgOrder {
178 match self {
179 SpecArg::Requires { .. } => ArgOrder::Requires,
180 SpecArg::Maintains { .. } => ArgOrder::Maintains,
181 SpecArg::Binds { .. } => ArgOrder::Binds,
182 SpecArg::Ensures { .. } => ArgOrder::Ensures,
183 }
184 }
185
186 fn get_keyword_span(&self) -> Span {
187 match self {
188 SpecArg::Requires { keyword, .. } => keyword.span,
189 SpecArg::Ensures { keyword, .. } => keyword.span,
190 SpecArg::Maintains { keyword, .. } => keyword.span,
191 SpecArg::Binds { keyword, .. } => keyword.span,
192 }
193 }
194}
195
196impl Parse for SpecArg {
197 fn parse(input: ParseStream) -> Result<Self> {
198 let attrs = input.call(Attribute::parse_outer)?;
199 let cfg = parse_cfg_attribute(&attrs)?;
200
201 let lookahead = input.lookahead1();
202 if lookahead.peek(kw::binds) {
203 if cfg.is_some() {
204 return Err(syn::Error::new(
205 attrs[0].span(),
206 "`cfg` attribute is not supported on `binds`",
207 ));
208 }
209
210 let keyword = input.parse::<kw::binds>()?;
212 input.parse::<Token![:]>()?;
213 Ok(SpecArg::Binds {
214 keyword,
215 pattern: Pat::parse_single(input)?,
216 })
217 } else if lookahead.peek(kw::requires) {
218 let keyword = input.parse::<kw::requires>()?;
220 input.parse::<Token![:]>()?;
221 Ok(SpecArg::Requires {
222 keyword,
223 cfg,
224 expr: input.parse()?,
225 })
226 } else if lookahead.peek(kw::maintains) {
227 let keyword = input.parse::<kw::maintains>()?;
229 input.parse::<Token![:]>()?;
230 Ok(SpecArg::Maintains {
231 keyword,
232 cfg,
233 expr: input.parse()?,
234 })
235 } else if lookahead.peek(kw::ensures) {
236 let keyword = input.parse::<kw::ensures>()?;
238 input.parse::<Token![:]>()?;
239 Ok(SpecArg::Ensures {
240 keyword,
241 cfg,
242 expr: input.parse()?,
243 })
244 } else {
245 Err(lookahead.error())
246 }
247 }
248}
249
250fn parse_cfg_attribute(attrs: &[Attribute]) -> Result<Option<Meta>> {
251 let mut cfg_attrs: Vec<Meta> = vec![];
252
253 for attr in attrs {
254 if attr.path().is_ident("cfg") {
255 cfg_attrs.push(attr.parse_args()?);
256 } else {
257 return Err(syn::Error::new(
258 attr.span(),
259 "unsupported attribute; only `cfg` is allowed",
260 ));
261 }
262 }
263
264 if cfg_attrs.len() > 1 {
265 return Err(syn::Error::new(
266 cfg_attrs[1].span(),
267 "multiple `cfg` attributes are not supported",
268 ));
269 }
270
271 Ok(cfg_attrs.pop())
272}
273
274mod kw {
277 syn::custom_keyword!(requires);
278 syn::custom_keyword!(maintains);
279 syn::custom_keyword!(binds);
280 syn::custom_keyword!(ensures);
281}
282
283pub fn instrument_fn_body(spec: &Spec, original_body: &Block, is_async: bool) -> Result<Block> {
285 let binding_ident = Ident::new("__anodized_output", Span::mixed_site());
287
288 let preconditions = spec
290 .requires
291 .iter()
292 .map(|condition| {
293 let expr = &condition.expr;
294 let msg = format!("Precondition failed: {}", expr.to_token_stream());
295 let assert = quote! { assert!(#expr, #msg); };
296 if let Some(cfg) = &condition.cfg {
297 quote! { if cfg!(#cfg) { #assert } }
298 } else {
299 assert
300 }
301 })
302 .chain(spec.maintains.iter().map(|condition| {
303 let expr = &condition.expr;
304 let msg = format!("Pre-invariant failed: {}", expr.to_token_stream());
305 let assert = quote! { assert!(#expr, #msg); };
306 if let Some(cfg) = &condition.cfg {
307 quote! { if cfg!(#cfg) { #assert } }
308 } else {
309 assert
310 }
311 }));
312
313 let postconditions = spec
315 .maintains
316 .iter()
317 .map(|condition| {
318 let expr = &condition.expr;
319 let msg = format!("Post-invariant failed: {}", expr.to_token_stream());
320 let assert = quote! { assert!(#expr, #msg); };
321 if let Some(cfg) = &condition.cfg {
322 quote! { if cfg!(#cfg) { #assert } }
323 } else {
324 assert
325 }
326 })
327 .chain(spec.ensures.iter().map(|condition_closure| {
328 let closure = &condition_closure.closure;
329 let msg = format!("Postcondition failed: {}", closure.to_token_stream());
330 let assert = quote! { assert!((#closure)(#binding_ident), #msg); };
331 if let Some(cfg) = &condition_closure.cfg {
332 quote! { if cfg!(#cfg) { #assert } }
333 } else {
334 assert
335 }
336 }));
337
338 let body_expr = if is_async {
340 quote! { async #original_body.await }
341 } else {
342 quote! { #original_body }
343 };
344
345 Ok(parse_quote! {
346 {
347 #(#preconditions)*
348 let #binding_ident = #body_expr;
349 #(#postconditions)*
350 #binding_ident
351 }
352 })
353}
354
355#[cfg(test)]
356mod test_parse_spec;
357
358#[cfg(test)]
359mod test_instrument_fn;
360
361#[cfg(test)]
362mod test_util;