Skip to content

Commit 804cce4

Browse files
committed
Refactor contract builtin macro + error handling
Instead of parsing the different components of a function signature, eagerly look for either the `where` keyword or the function body. - Also address feedback to use `From` instead of `TryFrom` in cranelift contract and ubcheck codegen.
1 parent 6a6c6b8 commit 804cce4

7 files changed

+280
-73
lines changed

Diff for: compiler/rustc_builtin_macros/src/contracts.rs

+71-71
Original file line numberDiff line numberDiff line change
@@ -35,98 +35,98 @@ impl AttrProcMacro for ExpandEnsures {
3535
}
3636
}
3737

38-
fn expand_injecting_circa_where_clause(
38+
/// Expand the function signature to include the contract clause.
39+
///
40+
/// The contracts clause will be injected before the function body and the optional where clause.
41+
/// For that, we search for the body / where token, and invoke the `inject` callback to generate the
42+
/// contract clause in the right place.
43+
///
44+
// FIXME: this kind of manual token tree munging does not have significant precedent among
45+
// rustc builtin macros, probably because most builtin macros use direct AST manipulation to
46+
// accomplish similar goals. But since our attributes need to take arbitrary expressions, and
47+
// our attribute infrastructure does not yet support mixing a token-tree annotation with an AST
48+
// annotated, we end up doing token tree manipulation.
49+
fn expand_contract_clause(
3950
ecx: &mut ExtCtxt<'_>,
4051
attr_span: Span,
4152
annotated: TokenStream,
42-
inject: impl FnOnce(&mut Vec<TokenTree>) -> Result<(), ErrorGuaranteed>,
53+
inject: impl FnOnce(&mut TokenStream) -> Result<(), ErrorGuaranteed>,
4354
) -> Result<TokenStream, ErrorGuaranteed> {
44-
let mut new_tts = Vec::with_capacity(annotated.len());
55+
let mut new_tts = TokenStream::default();
4556
let mut cursor = annotated.iter();
4657

47-
// Find the `fn name<G,...>(x:X,...)` and inject the AST contract forms right after
48-
// the formal parameters (and return type if any).
49-
while let Some(tt) = cursor.next() {
50-
new_tts.push(tt.clone());
51-
if let TokenTree::Token(tok, _) = tt
52-
&& tok.is_ident_named(kw::Fn)
53-
{
54-
break;
55-
}
58+
let is_kw = |tt: &TokenTree, sym: Symbol| {
59+
if let TokenTree::Token(token, _) = tt { token.is_ident_named(sym) } else { false }
60+
};
61+
62+
// Find the `fn` keyword to check if this is a function.
63+
if cursor
64+
.find(|tt| {
65+
new_tts.push_tree((*tt).clone());
66+
is_kw(tt, kw::Fn)
67+
})
68+
.is_none()
69+
{
70+
return Err(ecx
71+
.sess
72+
.dcx()
73+
.span_err(attr_span, "contract annotations can only be used on functions"));
5674
}
5775

58-
// Found the `fn` keyword, now find the formal parameters.
59-
//
60-
// FIXME: can this fail if you have parentheticals in a generics list, like `fn foo<F: Fn(X) -> Y>` ?
61-
while let Some(tt) = cursor.next() {
62-
new_tts.push(tt.clone());
63-
64-
if let TokenTree::Delimited(_, _, token::Delimiter::Parenthesis, _) = tt {
65-
break;
66-
}
67-
if let TokenTree::Token(token::Token { kind: token::TokenKind::Semi, .. }, _) = tt {
68-
panic!("contract attribute applied to fn without parameter list.");
76+
// Found the `fn` keyword, now find either the `where` token or the function body.
77+
let next_tt = loop {
78+
let Some(tt) = cursor.next() else {
79+
return Err(ecx.sess.dcx().span_err(
80+
attr_span,
81+
"contract annotations is only supported in functions with bodies",
82+
));
83+
};
84+
// If `tt` is the last element. Check if it is the function body.
85+
if cursor.peek().is_none() {
86+
if let TokenTree::Delimited(_, _, token::Delimiter::Brace, _) = tt {
87+
break tt;
88+
} else {
89+
return Err(ecx.sess.dcx().span_err(
90+
attr_span,
91+
"contract annotations is only supported in functions with bodies",
92+
));
93+
}
6994
}
70-
}
7195

72-
// There *might* be a return type declaration (and figuring out where that ends would require
73-
// parsing an arbitrary type expression, e.g. `-> Foo<args ...>`
74-
//
75-
// Instead of trying to figure that out, scan ahead and look for the first occurence of a
76-
// `where`, a `{ ... }`, or a `;`.
77-
//
78-
// FIXME: this might still fall into a trap for something like `-> Ctor<T, const { 0 }>`. I
79-
// *think* such cases must be under a Delimited (e.g. `[T; { N }]` or have the braced form
80-
// prefixed by e.g. `const`, so we should still be able to filter them out without having to
81-
// parse the type expression itself. But rather than try to fix things with hacks like that,
82-
// time might be better spent extending the attribute expander to suport tt-annotation atop
83-
// ast-annotated, which would be an elegant way to sidestep all of this.
84-
let mut opt_next_tt = cursor.next();
85-
while let Some(next_tt) = opt_next_tt {
86-
if let TokenTree::Token(tok, _) = next_tt
87-
&& tok.is_ident_named(kw::Where)
88-
{
89-
break;
90-
}
91-
if let TokenTree::Delimited(_, _, token::Delimiter::Brace, _) = next_tt {
92-
break;
93-
}
94-
if let TokenTree::Token(token::Token { kind: token::TokenKind::Semi, .. }, _) = next_tt {
95-
break;
96+
if is_kw(tt, kw::Where) {
97+
break tt;
9698
}
97-
98-
// for anything else, transcribe the tt and keep looking.
99-
new_tts.push(next_tt.clone());
100-
opt_next_tt = cursor.next();
101-
}
99+
new_tts.push_tree(tt.clone());
100+
};
102101

103102
// At this point, we've transcribed everything from the `fn` through the formal parameter list
104103
// and return type declaration, (if any), but `tt` itself has *not* been transcribed.
105104
//
106105
// Now inject the AST contract form.
107106
//
108-
// FIXME: this kind of manual token tree munging does not have significant precedent among
109-
// rustc builtin macros, probably because most builtin macros use direct AST manipulation to
110-
// accomplish similar goals. But since our attributes need to take arbitrary expressions, and
111-
// our attribute infrastructure does not yet support mixing a token-tree annotation with an AST
112-
// annotated, we end up doing token tree manipulation.
113107
inject(&mut new_tts)?;
114108

115-
// Above we injected the internal AST requires/ensures contruct. Now copy over all the other
109+
// Above we injected the internal AST requires/ensures construct. Now copy over all the other
116110
// token trees.
117-
if let Some(tt) = opt_next_tt {
118-
new_tts.push(tt.clone());
119-
}
111+
new_tts.push_tree(next_tt.clone());
120112
while let Some(tt) = cursor.next() {
121-
new_tts.push(tt.clone());
113+
new_tts.push_tree(tt.clone());
114+
if cursor.peek().is_none()
115+
&& !matches!(tt, TokenTree::Delimited(_, _, token::Delimiter::Brace, _))
116+
{
117+
return Err(ecx.sess.dcx().span_err(
118+
attr_span,
119+
"contract annotations is only supported in functions with bodies",
120+
));
121+
}
122122
}
123123

124124
// Record the span as a contract attribute expansion.
125125
// This is used later to stop users from using the extended syntax directly
126126
// which is gated via `rustc_contracts_internals`.
127127
ecx.psess().contract_attribute_spans.push(attr_span);
128128

129-
Ok(TokenStream::new(new_tts))
129+
Ok(new_tts)
130130
}
131131

132132
fn expand_requires_tts(
@@ -135,16 +135,16 @@ fn expand_requires_tts(
135135
annotation: TokenStream,
136136
annotated: TokenStream,
137137
) -> Result<TokenStream, ErrorGuaranteed> {
138-
expand_injecting_circa_where_clause(_ecx, attr_span, annotated, |new_tts| {
139-
new_tts.push(TokenTree::Token(
138+
expand_contract_clause(_ecx, attr_span, annotated, |new_tts| {
139+
new_tts.push_tree(TokenTree::Token(
140140
token::Token::from_ast_ident(Ident::new(kw::RustcContractRequires, attr_span)),
141141
Spacing::Joint,
142142
));
143-
new_tts.push(TokenTree::Token(
143+
new_tts.push_tree(TokenTree::Token(
144144
token::Token::new(token::TokenKind::OrOr, attr_span),
145145
Spacing::Alone,
146146
));
147-
new_tts.push(TokenTree::Delimited(
147+
new_tts.push_tree(TokenTree::Delimited(
148148
DelimSpan::from_single(attr_span),
149149
DelimSpacing::new(Spacing::JointHidden, Spacing::JointHidden),
150150
token::Delimiter::Parenthesis,
@@ -160,12 +160,12 @@ fn expand_ensures_tts(
160160
annotation: TokenStream,
161161
annotated: TokenStream,
162162
) -> Result<TokenStream, ErrorGuaranteed> {
163-
expand_injecting_circa_where_clause(_ecx, attr_span, annotated, |new_tts| {
164-
new_tts.push(TokenTree::Token(
163+
expand_contract_clause(_ecx, attr_span, annotated, |new_tts| {
164+
new_tts.push_tree(TokenTree::Token(
165165
token::Token::from_ast_ident(Ident::new(kw::RustcContractEnsures, attr_span)),
166166
Spacing::Joint,
167167
));
168-
new_tts.push(TokenTree::Delimited(
168+
new_tts.push_tree(TokenTree::Delimited(
169169
DelimSpan::from_single(attr_span),
170170
DelimSpacing::new(Spacing::JointHidden, Spacing::JointHidden),
171171
token::Delimiter::Parenthesis,

Diff for: compiler/rustc_codegen_cranelift/src/base.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ fn codegen_stmt<'tcx>(
868868
NullOp::UbChecks => {
869869
let val = fx.tcx.sess.ub_checks();
870870
let val = CValue::by_val(
871-
fx.bcx.ins().iconst(types::I8, i64::try_from(val).unwrap()),
871+
fx.bcx.ins().iconst(types::I8, i64::from(val)),
872872
fx.layout_of(fx.tcx.types.bool),
873873
);
874874
lval.write_cvalue(fx, val);
@@ -877,7 +877,7 @@ fn codegen_stmt<'tcx>(
877877
NullOp::ContractChecks => {
878878
let val = fx.tcx.sess.contract_checks();
879879
let val = CValue::by_val(
880-
fx.bcx.ins().iconst(types::I8, i64::try_from(val).unwrap()),
880+
fx.bcx.ins().iconst(types::I8, i64::from(val)),
881881
fx.layout_of(fx.tcx.types.bool),
882882
);
883883
lval.write_cvalue(fx, val);
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//! Test for some of the existing limitations and the current error messages.
2+
//! Some of these limitations may be removed in the future.
3+
4+
#![feature(rustc_contracts)]
5+
#![allow(dead_code)]
6+
7+
/// Represent a 5-star system.
8+
struct Stars(u8);
9+
10+
impl Stars {
11+
fn is_valid(&self) -> bool {
12+
self.0 <= 5
13+
}
14+
}
15+
16+
trait ParseStars {
17+
#[core::contracts::ensures(|ret| ret.is_none_or(Stars::is_valid))]
18+
//~^ ERROR contract annotations is only supported in functions with bodies
19+
fn parse_string(input: String) -> Option<Stars>;
20+
21+
#[core::contracts::ensures(|ret| ret.is_none_or(Stars::is_valid))]
22+
//~^ ERROR contract annotations is only supported in functions with bodies
23+
fn parse<T>(input: T) -> Option<Stars> where T: for<'a> Into<&'a str>;
24+
}
25+
26+
fn main() {
27+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error: contract annotations is only supported in functions with bodies
2+
--> $DIR/contract-annotation-limitations.rs:17:5
3+
|
4+
LL | #[core::contracts::ensures(|ret| ret.is_none_or(Stars::is_valid))]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
7+
error: contract annotations is only supported in functions with bodies
8+
--> $DIR/contract-annotation-limitations.rs:21:5
9+
|
10+
LL | #[core::contracts::ensures(|ret| ret.is_none_or(Stars::is_valid))]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: aborting due to 2 previous errors
14+

Diff for: tests/ui/contracts/contract-attributes-generics.rs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Test that contracts can be applied to generic functions.
2+
3+
//@ revisions: unchk_pass chk_pass chk_fail_pre chk_fail_post chk_const_fail
4+
//
5+
//@ [unchk_pass] run-pass
6+
//@ [chk_pass] run-pass
7+
//
8+
//@ [chk_fail_pre] run-fail
9+
//@ [chk_fail_post] run-fail
10+
//@ [chk_const_fail] run-fail
11+
//
12+
//@ [unchk_pass] compile-flags: -Zcontract-checks=no
13+
//
14+
//@ [chk_pass] compile-flags: -Zcontract-checks=yes
15+
//@ [chk_fail_pre] compile-flags: -Zcontract-checks=yes
16+
//@ [chk_fail_post] compile-flags: -Zcontract-checks=yes
17+
//@ [chk_const_fail] compile-flags: -Zcontract-checks=yes
18+
19+
#![feature(rustc_contracts)]
20+
21+
use std::ops::Sub;
22+
23+
/// Dummy fn contract that precondition fails for val < 0, and post-condition fail for val == 1
24+
#[core::contracts::requires(val > 0u8.into())]
25+
#[core::contracts::ensures(|ret| *ret > 0u8.into())]
26+
fn decrement<T>(val: T) -> T
27+
where T: PartialOrd + Sub<Output=T> + From<u8>
28+
{
29+
val - 1u8.into()
30+
}
31+
32+
/// Create a structure that takes a constant parameter.
33+
#[allow(dead_code)]
34+
struct Capped<const MAX: usize>(usize);
35+
36+
/// Now declare a function to create stars which shouldn't exceed 5 stars.
37+
// Add redundant braces to ensure the built-in macro can handle this syntax.
38+
#[allow(unused_braces)]
39+
#[core::contracts::requires(num <= 5)]
40+
unsafe fn stars_unchecked(num: usize) -> Capped<{ 5 }> {
41+
Capped(num)
42+
}
43+
44+
45+
fn main() {
46+
check_decrement();
47+
check_stars();
48+
}
49+
50+
fn check_stars() {
51+
// This should always pass.
52+
let _ = unsafe { stars_unchecked(3) };
53+
54+
// This violates the contract.
55+
#[cfg(any(unchk_pass, chk_const_fail))]
56+
let _ = unsafe { stars_unchecked(10) };
57+
}
58+
59+
fn check_decrement() {
60+
// This should always pass
61+
assert_eq!(decrement(10u8), 9u8);
62+
63+
// This should fail requires but pass with no contract check.
64+
#[cfg(any(unchk_pass, chk_fail_pre))]
65+
assert_eq!(decrement(-2i128), -3i128);
66+
67+
// This should fail ensures but pass with no contract check.
68+
#[cfg(any(unchk_pass, chk_fail_post))]
69+
assert_eq!(decrement(1i32), 0i32);
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Checks for compilation errors related to adding contracts to non-function items.
2+
3+
#![feature(rustc_contracts)]
4+
#![allow(dead_code)]
5+
6+
#[core::contracts::requires(true)]
7+
//~^ ERROR contract annotations can only be used on functions
8+
struct Dummy(usize);
9+
10+
#[core::contracts::ensures(|v| v == 100)]
11+
//~^ ERROR contract annotations can only be used on functions
12+
const MAX_VAL: usize = 100;
13+
14+
// FIXME: Improve the error message here. The macro thinks this is a function.
15+
#[core::contracts::ensures(|v| v == 100)]
16+
//~^ ERROR contract annotations is only supported in functions with bodies
17+
type NewDummy = fn(usize) -> Dummy;
18+
19+
#[core::contracts::ensures(|v| v == 100)]
20+
//~^ ERROR contract annotations is only supported in functions with bodies
21+
const NEW_DUMMY_FN : fn(usize) -> Dummy = || { Dummy(0) };
22+
23+
#[core::contracts::requires(true)]
24+
//~^ ERROR contract annotations can only be used on functions
25+
impl Dummy {
26+
27+
// This should work
28+
#[core::contracts::ensures(|ret| ret.0 == v)]
29+
fn new(v: usize) -> Dummy {
30+
Dummy(v)
31+
}
32+
}
33+
34+
#[core::contracts::ensures(|dummy| dummy.0 > 0)]
35+
//~^ ERROR contract annotations can only be used on functions
36+
impl From<usize> for Dummy {
37+
// This should work.
38+
#[core::contracts::ensures(|ret| ret.0 == v)]
39+
fn from(value: usize) -> Self {
40+
Dummy::new(value)
41+
}
42+
}
43+
44+
/// You should not be able to annotate a trait either.
45+
#[core::contracts::requires(true)]
46+
//~^ ERROR contract annotations can only be used on functions
47+
pub trait DummyBuilder {
48+
fn build() -> Dummy;
49+
}
50+
51+
fn main() {
52+
}

0 commit comments

Comments
 (0)