Skip to content

Commit 51492f9

Browse files
committed
always treat ensures clauses as closures
1 parent 9b625bb commit 51492f9

3 files changed

Lines changed: 56 additions & 88 deletions

File tree

src/lib.rs

Lines changed: 54 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ use proc_macro::TokenStream;
22
use proc_macro2::{Ident, Span};
33
use quote::{ToTokens, quote};
44
use syn::{
5-
Expr, ItemFn, Pat, Result, ReturnType, Token,
5+
Expr, ExprClosure, ItemFn, Result, ReturnType, Token,
66
parse::{Parse, ParseStream},
77
parse_macro_input,
88
punctuated::Punctuated,
9-
spanned::Spanned,
109
};
1110

1211
/// The main procedural macro for defining contracts on functions.
@@ -35,39 +34,31 @@ pub fn contract(args: TokenStream, input: TokenStream) -> TokenStream {
3534

3635
/// A container for all parsed arguments from the `#[contract]` attribute.
3736
struct ContractArgs {
38-
clauses: Vec<Clause>,
37+
conditions: Vec<Condition>,
3938
// Optional global rename for the return value.
4039
returns_ident: Option<Ident>,
4140
}
4241

43-
/// Represents a single clause, e.g., `requires: x > 0`.
44-
struct Clause {
45-
flavor: ClauseFlavor,
46-
predicate: Expr,
47-
// Optional per-clause rename for the return value (only for `ensures`).
48-
output_binding: Option<Ident>,
49-
}
50-
51-
/// The "flavor" of a clause: precondition, postcondition, or invariant.
52-
#[derive(Clone, Copy, PartialEq, Eq)]
53-
enum ClauseFlavor {
54-
Requires,
55-
Ensures,
56-
Maintains,
42+
/// Represents a single contract condition, e.g., `requires: x > 0`.
43+
enum Condition {
44+
Requires { predicate: Expr },
45+
Ensures { predicate: Expr },
46+
EnsuresClosure { closure: ExprClosure },
47+
Maintains { predicate: Expr },
5748
}
5849

5950
impl Parse for ContractArgs {
6051
/// Custom parser for the contents of `#[contract(...)]`.
6152
fn parse(input: ParseStream) -> Result<Self> {
62-
let mut clauses = Vec::new();
53+
let mut conditions = Vec::new();
6354
let mut returns_ident = None;
6455

65-
// The arguments are a comma-separated list of clauses or a `returns` key.
56+
// The arguments are a comma-separated list of conditions or a `returns` key.
6657
let items = Punctuated::<ContractArgItem, Token![,]>::parse_terminated(input)?;
6758

6859
for item in items {
6960
match item {
70-
ContractArgItem::Clause(clause) => clauses.push(clause),
61+
ContractArgItem::Condition(condition) => conditions.push(condition),
7162
ContractArgItem::Returns(ident) => {
7263
if returns_ident.is_some() {
7364
return Err(syn::Error::new(ident.span(), "duplicate `returns` key"));
@@ -78,15 +69,15 @@ impl Parse for ContractArgs {
7869
}
7970

8071
Ok(ContractArgs {
81-
clauses,
72+
conditions,
8273
returns_ident,
8374
})
8475
}
8576
}
8677

87-
/// An intermediate enum to help parse either a clause or a `returns` key.
78+
/// An intermediate enum to help parse either a condition or a `returns` key.
8879
enum ContractArgItem {
89-
Clause(Clause),
80+
Condition(Condition),
9081
Returns(Ident),
9182
}
9283

@@ -103,57 +94,38 @@ impl Parse for ContractArgItem {
10394
|| lookahead.peek(kw::ensures)
10495
|| lookahead.peek(kw::maintains)
10596
{
106-
// Parse a clause like `requires: predicate` or `ensures: |val| predicate`
107-
Ok(ContractArgItem::Clause(input.parse()?))
97+
// Parse a condition like `requires: predicate` or `ensures: |val| predicate`
98+
Ok(ContractArgItem::Condition(input.parse()?))
10899
} else {
109100
Err(lookahead.error())
110101
}
111102
}
112103
}
113104

114-
impl Parse for Clause {
115-
/// Parses a single clause.
105+
impl Parse for Condition {
106+
/// Parses a single condition.
116107
fn parse(input: ParseStream) -> Result<Self> {
117108
let lookahead = input.lookahead1();
118109
if lookahead.peek(kw::requires) {
119110
input.parse::<kw::requires>()?;
120111
input.parse::<Token![:]>()?;
121-
Ok(Clause {
122-
flavor: ClauseFlavor::Requires,
112+
Ok(Condition::Requires {
123113
predicate: input.parse()?,
124-
output_binding: None,
125114
})
126115
} else if lookahead.peek(kw::ensures) {
127116
input.parse::<kw::ensures>()?;
128117
input.parse::<Token![:]>()?;
129-
let mut output_binding = None;
130-
// Check for the optional `|name|` syntax.
131-
if input.peek(Token![|]) {
132-
input.parse::<Token![|]>()?;
133-
// FIX: Use `Pat::parse_single` instead of `input.parse()`.
134-
let pat = Pat::parse_single(input)?;
135-
if let Pat::Ident(pat_ident) = pat {
136-
output_binding = Some(pat_ident.ident);
137-
} else {
138-
return Err(syn::Error::new(
139-
pat.span(),
140-
"expected a simple identifier for the return value binding",
141-
));
142-
}
143-
input.parse::<Token![|]>()?;
118+
let predicate: Expr = input.parse()?;
119+
if let Expr::Closure(closure) = predicate {
120+
Ok(Condition::EnsuresClosure { closure })
121+
} else {
122+
Ok(Condition::Ensures { predicate })
144123
}
145-
Ok(Clause {
146-
flavor: ClauseFlavor::Ensures,
147-
predicate: input.parse()?,
148-
output_binding,
149-
})
150124
} else if lookahead.peek(kw::maintains) {
151125
input.parse::<kw::maintains>()?;
152126
input.parse::<Token![:]>()?;
153-
Ok(Clause {
154-
flavor: ClauseFlavor::Maintains,
127+
Ok(Condition::Maintains {
155128
predicate: input.parse()?,
156-
output_binding: None,
157129
})
158130
} else {
159131
Err(lookahead.error())
@@ -184,43 +156,15 @@ fn instrument_body(func: &ItemFn, args: &ContractArgs) -> Result<proc_macro2::To
184156
.unwrap_or_else(|| Ident::new("output", Span::call_site()));
185157

186158
// --- Generate Precondition Checks ---
187-
let preconditions = args.clauses.iter().filter_map(|c| {
188-
if c.flavor == ClauseFlavor::Requires || c.flavor == ClauseFlavor::Maintains {
189-
let pred = &c.predicate;
190-
let msg = format!("Precondition failed: {}", pred.to_token_stream());
191-
Some(quote! { assert!(#pred, #msg); })
192-
} else {
193-
None
159+
let preconditions = args.conditions.iter().filter_map(|c| match c {
160+
Condition::Requires { predicate } | Condition::Maintains { predicate } => {
161+
let msg = format!("Precondition failed: {}", predicate.to_token_stream());
162+
Some(quote! { assert!(#predicate, #msg); })
194163
}
164+
_ => None,
195165
});
196166

197167
// --- Generate Postcondition Checks ---
198-
let postconditions = args.clauses.iter().filter_map(|c| {
199-
if c.flavor == ClauseFlavor::Ensures || c.flavor == ClauseFlavor::Maintains {
200-
let pred = &c.predicate;
201-
let msg = format!("Postcondition failed: {}", pred.to_token_stream());
202-
203-
// If the clause has a per-clause `|name|` binding, we create a new
204-
// variable with that name that references the global output variable.
205-
let maybe_rename = if let Some(per_clause_ident) = &c.output_binding {
206-
quote! { let #per_clause_ident = &#global_output_ident; }
207-
} else {
208-
quote! {}
209-
};
210-
211-
Some(quote! {
212-
// This block ensures that if we rename the output, it's only for this assertion.
213-
{
214-
#maybe_rename
215-
assert!(#pred, #msg);
216-
}
217-
})
218-
} else {
219-
None
220-
}
221-
});
222-
223-
// --- Construct the New Body ---
224168
let returns_nothing = match &func.sig.output {
225169
ReturnType::Default => true,
226170
ReturnType::Type(_, ty) => {
@@ -232,6 +176,30 @@ fn instrument_body(func: &ItemFn, args: &ContractArgs) -> Result<proc_macro2::To
232176
}
233177
};
234178

179+
let postconditions = args.conditions.iter().filter_map(|c| match c {
180+
Condition::Maintains { predicate } => {
181+
let msg = format!("Postcondition failed: {}", predicate.to_token_stream());
182+
Some(quote! { assert!(#predicate, #msg); })
183+
}
184+
Condition::Ensures { predicate } => {
185+
if returns_nothing {
186+
return None;
187+
}
188+
let msg = format!("Postcondition failed: {}", predicate.to_token_stream());
189+
Some(quote! { assert!((|#global_output_ident| #predicate)(#global_output_ident), #msg); })
190+
}
191+
Condition::EnsuresClosure { closure } => {
192+
if returns_nothing {
193+
return None;
194+
}
195+
let msg = format!("Postcondition failed: {}", closure.to_token_stream());
196+
Some(quote! { assert!((#closure)(#global_output_ident), #msg); })
197+
}
198+
_ => None, // Ignore `requires`
199+
});
200+
201+
// --- Construct the New Body ---
202+
235203
if returns_nothing {
236204
// Case 1: Function returns `()` or nothing.
237205
if is_async {

tests/pattern_in_closure.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn sort_pair(pair: (i32, i32)) -> (i32, i32) {
1111
}
1212

1313
#[test]
14-
#[should_panic(expected = "Postcondition failed: |(a, b)| a <= b")]
14+
#[should_panic(expected = "Postcondition failed: | (a, b) | a <= b")]
1515
fn test_sort_fail_postcondition() {
1616
sort_pair((2, 5));
1717
}

tests/rename_return_value.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fn test_rename_success() {
2020
}
2121

2222
#[test]
23-
#[should_panic(expected = "Postcondition failed: val % 2 == 0")]
23+
#[should_panic(expected = "Postcondition failed: | val | val % 2 == 0")]
2424
fn test_rename_panics_if_not_even() {
2525
#[contract(returns: result, ensures: |val| val % 2 == 0)]
2626
fn calculate_odd_result(output: i32) -> i32 {

0 commit comments

Comments
 (0)