Skip to content

Commit b8246ff

Browse files
committed
Initial go at contract support: attribute syntax for preconditions, and an intrinsic for checking it.
Known issues: * My original intent, as described in the MCP (rust-lang/compiler-team#759) was to have a rustc-prefixed attribute namespace (like rustc_contracts::requires). But I could not get things working when I tried to do rewriting via a rustc-prefixed builtin attribute-macro. So for now it is called `contracts::requires`. * The macro is currently expanding to a `core::intrinsics::contract_check` path. I suspect that the expansion will actually want to be conditional based on whether it is expanding within std or outside of std. * The macro itself is very naive; it assumes the first occurrence of `{ ... }` in the token-stream will be the function body (and injects the intrinsic call there). I suspect there are some contracts like `const { ... }` that might mess that up. * The emitted abort message when a contract fails is just "contract failure". Obviously the message should include more details, e.g. what function failed, which contract-expression failed and (maybe) what value caused the failure.
1 parent 5a3e2a4 commit b8246ff

File tree

16 files changed

+216
-5
lines changed

16 files changed

+216
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use rustc_ast::token;
2+
use rustc_ast::tokenstream::{DelimSpan, DelimSpacing, Spacing, TokenStream, TokenTree};
3+
use rustc_errors::ErrorGuaranteed;
4+
use rustc_expand::base::{AttrProcMacro, ExtCtxt};
5+
use rustc_span::symbol::{sym, Symbol};
6+
use rustc_span::Span;
7+
8+
pub struct ExpandRequires;
9+
10+
impl AttrProcMacro for ExpandRequires {
11+
fn expand<'cx>(
12+
&self,
13+
ecx: &'cx mut ExtCtxt<'_>,
14+
span: Span,
15+
annotation: TokenStream,
16+
annotated: TokenStream,
17+
) -> Result<TokenStream, ErrorGuaranteed> {
18+
expand_requires_tts(ecx, span, annotation, annotated)
19+
}
20+
}
21+
22+
fn expand_requires_tts(
23+
_ecx: &mut ExtCtxt<'_>,
24+
attr_span: Span,
25+
annotation: TokenStream,
26+
annotated: TokenStream,
27+
) -> Result<TokenStream, ErrorGuaranteed> {
28+
let mut new_tts = Vec::with_capacity(annotated.len());
29+
let mut cursor = annotated.into_trees();
30+
31+
// XXX this will break if you have a `{ ... }` syntactically prior to the fn body,
32+
// e.g. if a `const { ... }` can occur in a where clause. We need to do something
33+
// smarter for injecting this code into the right place.
34+
while let Some(tt) = cursor.next_ref() {
35+
if let TokenTree::Delimited(sp, spacing, delim @ token::Delimiter::Brace, inner_ts) = tt {
36+
let revised_tt = TokenTree::Delimited(
37+
*sp, *spacing, *delim,
38+
39+
// XXX a constructed ast doesn't actually *carry*
40+
// tokens from which to build a TokenStream.
41+
// So instead, use the original annotation directly.
42+
// TokenStream::from_ast(&ast_for_invoke)
43+
44+
std::iter::once(TokenTree::token_alone(token::Ident(sym::core, token::IdentIsRaw::No), attr_span))
45+
.chain(std::iter::once(TokenTree::token_alone(token::PathSep, attr_span)))
46+
.chain(std::iter::once(TokenTree::token_alone(token::Ident(sym::intrinsics, token::IdentIsRaw::No), attr_span)))
47+
.chain(std::iter::once(TokenTree::token_alone(token::PathSep, attr_span)))
48+
.chain(std::iter::once(TokenTree::token_alone(token::Ident(sym::contract_check, token::IdentIsRaw::No), attr_span)))
49+
.chain(std::iter::once(TokenTree::Delimited(
50+
DelimSpan::from_single(attr_span),
51+
DelimSpacing { open: Spacing::JointHidden, close: Spacing::JointHidden },
52+
token::Delimiter::Parenthesis,
53+
TokenStream::new(
54+
std::iter::once(TokenTree::token_alone(token::BinOp(token::BinOpToken::Or), attr_span))
55+
.chain(std::iter::once(TokenTree::token_alone(token::BinOp(token::BinOpToken::Or), attr_span)))
56+
.chain(std::iter::once(TokenTree::Delimited(DelimSpan::from_single(attr_span),
57+
DelimSpacing { open: Spacing::JointHidden, close: Spacing::JointHidden },
58+
token::Delimiter::Brace,
59+
annotation)))
60+
.chain(std::iter::once(TokenTree::token_alone(token::Comma, attr_span)))
61+
.chain(std::iter::once(TokenTree::token_alone(token::Literal(token::Lit::new(token::LitKind::Str, Symbol::intern("contract failure"), None)), attr_span)))
62+
.collect()
63+
))))
64+
.chain(TokenStream::token_alone(token::Semi, attr_span).trees().cloned())
65+
.chain(inner_ts.trees().cloned())
66+
.collect());
67+
new_tts.push(revised_tt);
68+
break;
69+
} else {
70+
new_tts.push(tt.clone());
71+
continue;
72+
}
73+
};
74+
75+
// Above we injected the intrinsic call. Now just copy over all the other token trees.
76+
while let Some(tt) = cursor.next_ref() {
77+
new_tts.push(tt.clone());
78+
}
79+
Ok(TokenStream::new(new_tts))
80+
}

compiler/rustc_builtin_macros/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod compile_error;
3535
mod concat;
3636
mod concat_bytes;
3737
mod concat_idents;
38+
mod contracts;
3839
mod derive;
3940
mod deriving;
4041
mod edition_panic;
@@ -131,4 +132,5 @@ pub fn register_builtin_macros(resolver: &mut dyn ResolverExpand) {
131132

132133
let client = proc_macro::bridge::client::Client::expand1(proc_macro::quote);
133134
register(sym::quote, SyntaxExtensionKind::Bang(Box::new(BangProcMacro { client })));
135+
register(sym::contracts_requires, SyntaxExtensionKind::Attr(Box::new(contracts::ExpandRequires)));
134136
}

compiler/rustc_expand/src/base.rs

+1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ impl Annotatable {
234234

235235
/// Result of an expansion that may need to be retried.
236236
/// Consider using this for non-`MultiItemModifier` expanders as well.
237+
#[derive(Debug)]
237238
pub enum ExpandResult<T, U> {
238239
/// Expansion produced a result (possibly dummy).
239240
Ready(T),

compiler/rustc_expand/src/expand.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,8 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
699699
}
700700
_ => unreachable!(),
701701
},
702-
InvocationKind::Attr { attr, pos, mut item, derives } => match ext {
702+
InvocationKind::Attr { attr, pos, mut item, derives } => {
703+
match ext {
703704
SyntaxExtensionKind::Attr(expander) => {
704705
self.gate_proc_macro_input(&item);
705706
self.gate_proc_macro_attr_item(span, &item);
@@ -777,7 +778,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
777778
fragment_kind.expect_from_annotatables(iter::once(item))
778779
}
779780
_ => unreachable!(),
780-
},
781+
}},
781782
InvocationKind::Derive { path, item, is_const } => match ext {
782783
SyntaxExtensionKind::Derive(expander)
783784
| SyntaxExtensionKind::LegacyDerive(expander) => {

compiler/rustc_hir_analysis/src/check/intrinsic.rs

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub fn intrinsic_operation_unsafety(tcx: TyCtxt<'_>, intrinsic_id: LocalDefId) -
103103
| sym::saturating_sub
104104
| sym::rotate_left
105105
| sym::rotate_right
106+
| sym::contract_check
106107
| sym::ctpop
107108
| sym::ctlz
108109
| sym::cttz
@@ -653,6 +654,8 @@ pub fn check_intrinsic_type(
653654
sym::simd_shuffle => (3, 0, vec![param(0), param(0), param(1)], param(2)),
654655
sym::simd_shuffle_generic => (2, 1, vec![param(0), param(0)], param(1)),
655656

657+
sym::contract_check => (1, 0, vec![param(0), Ty::new_static_str(tcx)], tcx.types.unit),
658+
656659
other => {
657660
tcx.dcx().emit_err(UnrecognizedIntrinsicFunction { span, name: other });
658661
return;

compiler/rustc_mir_transform/src/lower_intrinsics.rs

+6
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ impl<'tcx> MirPass<'tcx> for LowerIntrinsics {
333333
});
334334
terminator.kind = TerminatorKind::Goto { target };
335335
}
336+
sym::contract_check => {
337+
if tcx.sess.opts.unstable_opts.contract_checking.no_checks() {
338+
let target = target.unwrap();
339+
terminator.kind = TerminatorKind::Goto { target };
340+
}
341+
}
336342
_ => {}
337343
}
338344
}

compiler/rustc_parse/src/validate_attr.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub fn check_attr(features: &Features, psess: &ParseSess, attr: &Attribute) {
7676
// Check input tokens for built-in and key-value attributes.
7777
match attr_info {
7878
// `rustc_dummy` doesn't have any restrictions specific to built-in attributes.
79-
Some(BuiltinAttribute { name, template, .. }) if *name != sym::rustc_dummy => {
79+
Some(BuiltinAttribute { name, template, .. }) if input_restricted_by_parser(*name) => {
8080
match parse_meta(psess, attr) {
8181
Ok(meta) => check_builtin_meta_item(psess, &meta, attr.style, *name, *template),
8282
Err(err) => {
@@ -97,6 +97,11 @@ pub fn check_attr(features: &Features, psess: &ParseSess, attr: &Attribute) {
9797
}
9898
}
9999

100+
fn input_restricted_by_parser(name: Symbol) -> bool {
101+
name != sym::rustc_dummy &&
102+
name != sym::rustc_contracts_requires
103+
}
104+
100105
pub fn parse_meta<'a>(psess: &'a ParseSess, attr: &Attribute) -> PResult<'a, MetaItem> {
101106
let item = attr.get_normal_item();
102107
Ok(MetaItem {

compiler/rustc_session/src/config.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,28 @@ pub enum InstrumentCoverage {
145145
Yes,
146146
}
147147

148+
/// Individual flag values controlled by `-Z contract-checking`.
149+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
150+
pub enum ContractCheckingOptions {
151+
Dynamic,
152+
None,
153+
}
154+
155+
impl Default for ContractCheckingOptions {
156+
fn default() -> Self {
157+
ContractCheckingOptions::None
158+
}
159+
}
160+
161+
impl ContractCheckingOptions {
162+
pub fn no_checks(&self) -> bool {
163+
match self {
164+
ContractCheckingOptions::None => true,
165+
ContractCheckingOptions::Dynamic => false,
166+
}
167+
}
168+
}
169+
148170
/// Individual flag values controlled by `-Z coverage-options`.
149171
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
150172
pub struct CoverageOptions {
@@ -2959,8 +2981,8 @@ pub enum WasiExecModel {
29592981
/// how the hash should be calculated when adding a new command-line argument.
29602982
pub(crate) mod dep_tracking {
29612983
use super::{
2962-
BranchProtection, CFGuard, CFProtection, CollapseMacroDebuginfo, CoverageOptions,
2963-
CrateType, DebugInfo, DebugInfoCompression, ErrorOutputType, FunctionReturn,
2984+
BranchProtection, CFGuard, CFProtection, CollapseMacroDebuginfo, ContractCheckingOptions,
2985+
CoverageOptions, CrateType, DebugInfo, DebugInfoCompression, ErrorOutputType, FunctionReturn,
29642986
InliningThreshold, InstrumentCoverage, InstrumentXRay, LinkerPluginLto, LocationDetail,
29652987
LtoCli, NextSolverConfig, OomStrategy, OptLevel, OutFileName, OutputType, OutputTypes,
29662988
Polonius, RemapPathScopeComponents, ResolveDocLinks, SourceFileHashAlgorithm,
@@ -3035,6 +3057,7 @@ pub(crate) mod dep_tracking {
30353057
CodeModel,
30363058
TlsModel,
30373059
InstrumentCoverage,
3060+
ContractCheckingOptions,
30383061
CoverageOptions,
30393062
InstrumentXRay,
30403063
CrateType,

compiler/rustc_session/src/options.rs

+20
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ mod desc {
395395
pub const parse_optimization_fuel: &str = "crate=integer";
396396
pub const parse_dump_mono_stats: &str = "`markdown` (default) or `json`";
397397
pub const parse_instrument_coverage: &str = parse_bool;
398+
pub const parse_contract_checking_options: &str = "`none` (default) or `dynamic`";
398399
pub const parse_coverage_options: &str =
399400
"`block` | `branch` | `condition` | `mcdc` | `no-mir-spans`";
400401
pub const parse_instrument_xray: &str = "either a boolean (`yes`, `no`, `on`, `off`, etc), or a comma separated list of settings: `always` or `never` (mutually exclusive), `ignore-loops`, `instruction-threshold=N`, `skip-entry`, `skip-exit`";
@@ -967,6 +968,23 @@ mod parse {
967968
true
968969
}
969970

971+
pub(crate) fn parse_contract_checking_options(slot: &mut ContractCheckingOptions, v: Option<&str>) -> bool {
972+
let Some(v) = v else { return true; };
973+
974+
match v {
975+
"none" => {
976+
*slot = ContractCheckingOptions::None;
977+
}
978+
"dynamic" => {
979+
*slot = ContractCheckingOptions::Dynamic;
980+
}
981+
_ => {
982+
return false;
983+
}
984+
}
985+
true
986+
}
987+
970988
pub(crate) fn parse_coverage_options(slot: &mut CoverageOptions, v: Option<&str>) -> bool {
971989
let Some(v) = v else { return true };
972990

@@ -1625,6 +1643,8 @@ options! {
16251643
"the backend to use"),
16261644
combine_cgu: bool = (false, parse_bool, [TRACKED],
16271645
"combine CGUs into a single one"),
1646+
contract_checking: ContractCheckingOptions = (ContractCheckingOptions::default(), parse_contract_checking_options, [TRACKED],
1647+
"select how rustc_contracts are handled: static, dynamic, or no checking (default: none)"),
16281648
coverage_options: CoverageOptions = (CoverageOptions::default(), parse_coverage_options, [TRACKED],
16291649
"control details of coverage instrumentation"),
16301650
crate_attr: Vec<String> = (Vec::new(), parse_string_push, [TRACKED],

compiler/rustc_span/src/symbol.rs

+4
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,8 @@ symbols! {
620620
const_try,
621621
constant,
622622
constructor,
623+
contract_check,
624+
contracts_requires,
623625
convert_identity,
624626
copy,
625627
copy_closures,
@@ -1583,6 +1585,8 @@ symbols! {
15831585
rustc_const_panic_str,
15841586
rustc_const_stable,
15851587
rustc_const_unstable,
1588+
rustc_contracts,
1589+
rustc_contracts_requires,
15861590
rustc_conversion_suggestion,
15871591
rustc_deallocator,
15881592
rustc_def_path,

library/core/src/intrinsics.rs

+8
Original file line numberDiff line numberDiff line change
@@ -2834,6 +2834,14 @@ pub const fn ptr_metadata<P: ptr::Pointee<Metadata = M> + ?Sized, M>(_ptr: *cons
28342834
unreachable!()
28352835
}
28362836

2837+
#[cfg(not(bootstrap))]
2838+
#[rustc_nounwind]
2839+
#[unstable(feature = "rustc_contracts", issue = "none" /* compiler-team#759 */)]
2840+
#[rustc_intrinsic]
2841+
pub fn contract_check<C: FnOnce() -> bool>(c: C, on_fail_msg: &'static str) {
2842+
assert!(c(), "{}", on_fail_msg);
2843+
}
2844+
28372845
// Some functions are defined here because they accidentally got made
28382846
// available in this module on stable. See <https://github.com/rust-lang/rust/issues/15702>.
28392847
// (`transmute` also falls into this category, but it cannot be wrapped due to the

library/core/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ pub mod assert_matches {
295295
pub use crate::macros::{assert_matches, debug_assert_matches};
296296
}
297297

298+
#[cfg(not(bootstrap))]
299+
/// XXX
300+
#[unstable(feature = "rustc_contracts", issue = "none")]
301+
pub mod contracts {
302+
#[unstable(feature = "rustc_contracts", issue = "none")]
303+
pub use crate::macros::builtin::contracts_requires as requires;
304+
}
305+
298306
#[unstable(feature = "cfg_match", issue = "115585")]
299307
pub use crate::macros::cfg_match;
300308

library/core/src/macros/mod.rs

+14
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,20 @@ pub(crate) mod builtin {
16771677
/* compiler built-in */
16781678
}
16791679

1680+
#[cfg(not(bootstrap))]
1681+
/// Attribute macro applied to a function to give it a precondition.
1682+
///
1683+
/// This one is not prefixed with rustc_ because pnkfelix is trying to
1684+
/// debug whether that name is ignored specially which can make things
1685+
/// hard to track down when adding a new feature like this.
1686+
#[unstable(feature = "rustc_contracts", issue = "none")]
1687+
// #[allow_internal_unstable(rustc_attrs)]
1688+
#[allow_internal_unstable(core_intrinsics)]
1689+
#[rustc_builtin_macro]
1690+
pub macro contracts_requires($item:item) {
1691+
/* compiler built-in */
1692+
}
1693+
16801694
/// Attribute macro applied to a function to register it as a handler for allocation failure.
16811695
///
16821696
/// See also [`std::alloc::handle_alloc_error`](../../../std/alloc/fn.handle_alloc_error.html).

library/core/src/prelude/common.rs

+9
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ pub use crate::macros::builtin::{
7979
#[unstable(feature = "derive_const", issue = "none")]
8080
pub use crate::macros::builtin::derive_const;
8181

82+
/*
83+
#[cfg(not(bootstrap))]
84+
#[unstable(feature = "rustc_contracts", issue = "none")]
85+
pub use crate::macros::builtin::{rustc_contracts_requires, rustc_contracts_ensures};
86+
*/
87+
#[cfg(not(bootstrap))]
88+
#[unstable(feature = "rustc_contracts", issue = "none")]
89+
pub use crate::contracts;
90+
8291
#[unstable(
8392
feature = "cfg_accessible",
8493
issue = "64797",

library/std/src/prelude/common.rs

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ pub use core::prelude::v1::{
4545
stringify, trace_macros, Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd,
4646
};
4747

48+
#[cfg(not(bootstrap))]
49+
#[unstable(feature = "rustc_contracts", issue = "none")]
50+
pub use core::prelude::v1::contracts;
51+
4852
#[unstable(
4953
feature = "concat_bytes",
5054
issue = "87555",

tests/ui/contracts.rs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//@ revisions: no_fails catch_fail no_checking
2+
//@ [no_fails] run-pass
3+
//@ [catch_fail] run-fail
4+
//@ [no_checking] run-pass
5+
6+
//@[no_fails] compile-flags: -Zcontract-checking=dynamic
7+
//@[catch_fail] compile-flags: -Zcontract-checking=dynamic
8+
//@[no_checking] compile-flags: -Zcontract-checking=none
9+
10+
#![allow(internal_features)]
11+
#![feature(rustc_attrs, rustc_contracts, core_intrinsics)]
12+
13+
#[contracts::requires(x > 0)]
14+
fn foo(x: i32) {
15+
let _ = x + 2;
16+
}
17+
18+
fn main() {
19+
foo(10);
20+
21+
#[cfg(not(no_fails))]
22+
foo(-10);
23+
}

0 commit comments

Comments
 (0)