Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable contracts for const functions #138374

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions compiler/rustc_ast_lowering/src/expr.rs
Original file line number Diff line number Diff line change
@@ -397,12 +397,17 @@ impl<'hir> LoweringContext<'_, 'hir> {
&mut self,
expr: &'hir hir::Expr<'hir>,
span: Span,
check_ident: Ident,
check_hir_id: HirId,
cond_ident: Ident,
cond_hir_id: HirId,
) -> &'hir hir::Expr<'hir> {
let checker_fn = self.expr_ident(span, check_ident, check_hir_id);
let cond_fn = self.expr_ident(span, cond_ident, cond_hir_id);
let span = self.mark_span_with_reason(DesugaringKind::Contract, span, None);
self.expr_call(span, checker_fn, std::slice::from_ref(expr))
let call_expr = self.expr_call_lang_item_fn_mut(
span,
hir::LangItem::ContractCheckEnsures,
arena_vec![self; *expr, *cond_fn],
);
self.arena.alloc(call_expr)
}

pub(crate) fn lower_const_block(&mut self, c: &AnonConst) -> hir::ConstBlock {
1 change: 1 addition & 0 deletions compiler/rustc_hir/src/lang_items.rs
Original file line number Diff line number Diff line change
@@ -434,6 +434,7 @@ language_item_table! {
// Experimental lang items for implementing contract pre- and post-condition checking.
ContractBuildCheckEnsures, sym::contract_build_check_ensures, contract_build_check_ensures_fn, Target::Fn, GenericRequirement::None;
ContractCheckRequires, sym::contract_check_requires, contract_check_requires_fn, Target::Fn, GenericRequirement::None;
ContractCheckEnsures, sym::contract_check_ensures, contract_check_ensures_fn, Target::Fn, GenericRequirement::None;
}

/// The requirement imposed on the generics of a lang item
12 changes: 4 additions & 8 deletions compiler/rustc_hir_analysis/src/check/intrinsic.rs
Original file line number Diff line number Diff line change
@@ -232,15 +232,11 @@ pub fn check_intrinsic_type(
};
(n_tps, 0, 0, inputs, output, hir::Safety::Unsafe)
} else if intrinsic_name == sym::contract_check_ensures {
// contract_check_ensures::<'a, Ret, C>(&'a Ret, C)
// where C: impl Fn(&'a Ret) -> bool,
// contract_check_ensures::<Ret, C>(Ret, C) -> Ret
// where C: for<'a> Fn(&'a Ret) -> bool,
//
// so: two type params, one lifetime param, 0 const params, two inputs, no return

let p = generics.param_at(0, tcx);
let r = ty::Region::new_early_param(tcx, p.to_early_bound_region_data());
let ref_ret = Ty::new_imm_ref(tcx, r, param(1));
(2, 1, 0, vec![ref_ret, param(2)], tcx.types.unit, hir::Safety::Safe)
// so: two type params, 0 lifetime param, 0 const params, two inputs, no return
(2, 0, 0, vec![param(0), param(1)], param(0), hir::Safety::Safe)
} else {
let safety = intrinsic_operation_unsafety(tcx, intrinsic_id);
let (n_tps, n_cts, inputs, output) = match intrinsic_name {
13 changes: 6 additions & 7 deletions library/core/src/contracts.rs
Original file line number Diff line number Diff line change
@@ -5,16 +5,15 @@ pub use crate::macros::builtin::{contracts_ensures as ensures, contracts_require
/// Emitted by rustc as a desugaring of `#[ensures(PRED)] fn foo() -> R { ... [return R;] ... }`
/// into: `fn foo() { let _check = build_check_ensures(|ret| PRED) ... [return _check(R);] ... }`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now outdated, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me see if we can get rid of it completely

/// (including the implicit return of the tail expression, if any).
///
/// This call helps with type inference for the predicate.
#[unstable(feature = "contracts_internals", issue = "128044" /* compiler-team#759 */)]
#[rustc_const_unstable(feature = "contracts", issue = "128044")]
#[lang = "contract_build_check_ensures"]
#[track_caller]
pub fn build_check_ensures<Ret, C>(cond: C) -> impl (Fn(Ret) -> Ret) + Copy
pub const fn build_check_ensures<Ret, C>(cond: C) -> C
where
Comment on lines +14 to 15
Copy link
Member

@RalfJung RalfJung Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this function? It's just the identity function?

This call helps with type inference for the predicate.

Surely there's a better way to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely. Let me give it a try

C: for<'a> Fn(&'a Ret) -> bool + Copy + 'static,
C: Fn(&Ret) -> bool + Copy + 'static,
{
#[track_caller]
move |ret| {
crate::intrinsics::contract_check_ensures(&ret, cond);
ret
}
cond
}
47 changes: 41 additions & 6 deletions library/core/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
@@ -3288,20 +3288,55 @@ pub const fn contract_checks() -> bool {
///
/// By default, if `contract_checks` is enabled, this will panic with no unwind if the condition
/// returns false.
#[unstable(feature = "contracts_internals", issue = "128044" /* compiler-team#759 */)]
///
/// Note that this function is a no-op during constant evaluation.
#[unstable(feature = "contracts_internals", issue = "128044")]
#[rustc_const_unstable(feature = "contracts", issue = "128044")]
#[lang = "contract_check_requires"]
#[rustc_intrinsic]
pub fn contract_check_requires<C: Fn() -> bool>(cond: C) {
if contract_checks() && !cond() {
// Emit no unwind panic in case this was a safety requirement.
crate::panicking::panic_nounwind("failed requires check");
}
pub const fn contract_check_requires<C: Fn() -> bool + Copy>(cond: C) {
const_eval_select!(
@capture[C: Fn() -> bool + Copy] { cond: C } :
if const {
// Do nothing
} else {
if contract_checks() && !cond() {
// Emit no unwind panic in case this was a safety requirement.
crate::panicking::panic_nounwind("failed requires check");
}
}
)
}

/// Check if the post-condition `cond` has been met.
///
/// By default, if `contract_checks` is enabled, this will panic with no unwind if the condition
/// returns false.
///
/// Note that this function is a no-op during constant evaluation.
#[cfg(not(bootstrap))]
#[unstable(feature = "contracts_internals", issue = "128044")]
#[rustc_const_unstable(feature = "contracts", issue = "128044")]
Comment on lines +3318 to +3319
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using two different feature gates here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, for some reason the const stability check isn't working with contracts_internal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "isn't working" mean?

We used to have a bug where using a language feature for const stability didn't work (the feature would not get enabled properly), but I thought we had fixed that...

#[lang = "contract_check_ensures"]
#[rustc_intrinsic]
pub const fn contract_check_ensures<Ret, C: Fn(&Ret) -> bool + Copy>(ret: Ret, cond: C) -> Ret {
const_eval_select!(
@capture[Ret, C: Fn(&Ret) -> bool + Copy] { ret: Ret, cond: C } -> Ret :
if const {
// Do nothing
ret
} else {
if contract_checks() && !cond(&ret) {
// Emit no unwind panic in case this was a safety requirement.
crate::panicking::panic_nounwind("failed ensures check");
}
ret
}
)
}

/// This is the old version of contract_check_ensures kept here for bootstrap only.
#[cfg(bootstrap)]
#[unstable(feature = "contracts_internals", issue = "128044" /* compiler-team#759 */)]
#[rustc_intrinsic]
pub fn contract_check_ensures<'a, Ret, C: Fn(&'a Ret) -> bool>(ret: &'a Ret, cond: C) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't you just add the Copy here? Why is cfg(bootstrap) needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we changed the return value and the arguments of this function.

1 change: 0 additions & 1 deletion library/core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -113,7 +113,6 @@
#![feature(bigint_helper_methods)]
#![feature(bstr)]
#![feature(bstr_internals)]
#![feature(closure_track_caller)]
#![feature(const_carrying_mul_add)]
#![feature(const_eval_select)]
#![feature(core_intrinsics)]
13 changes: 12 additions & 1 deletion tests/ui/contracts/contract-attributes-nest.chk_pass.stderr
Original file line number Diff line number Diff line change
@@ -7,5 +7,16 @@ LL | #![feature(contracts)]
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted
warning: unreachable expression
--> $DIR/contract-attributes-nest.rs:23:1
|
LL | #[core::contracts::ensures(|ret| *ret > 100)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable expression
...
LL | return x.baz + 50;
| ----------------- any code following this expression is unreachable
|
= note: `#[warn(unreachable_code)]` on by default

warning: 2 warnings emitted

1 change: 1 addition & 0 deletions tests/ui/contracts/contract-attributes-nest.rs
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@

#[core::contracts::requires(x.baz > 0)]
#[core::contracts::ensures(|ret| *ret > 100)]
//~^ WARN unreachable expression [unreachable_code]
fn nest(x: Baz) -> i32
{
loop {
Original file line number Diff line number Diff line change
@@ -7,5 +7,16 @@ LL | #![feature(contracts)]
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted
warning: unreachable expression
--> $DIR/contract-attributes-nest.rs:23:1
|
LL | #[core::contracts::ensures(|ret| *ret > 100)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable expression
...
LL | return x.baz + 50;
| ----------------- any code following this expression is unreachable
|
= note: `#[warn(unreachable_code)]` on by default

warning: 2 warnings emitted

Original file line number Diff line number Diff line change
@@ -7,5 +7,16 @@ LL | #![feature(contracts)]
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted
warning: unreachable expression
--> $DIR/contract-attributes-nest.rs:23:1
|
LL | #[core::contracts::ensures(|ret| *ret > 100)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable expression
...
LL | return x.baz + 50;
| ----------------- any code following this expression is unreachable
|
= note: `#[warn(unreachable_code)]` on by default

warning: 2 warnings emitted

13 changes: 12 additions & 1 deletion tests/ui/contracts/contract-attributes-nest.unchk_pass.stderr
Original file line number Diff line number Diff line change
@@ -7,5 +7,16 @@ LL | #![feature(contracts)]
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted
warning: unreachable expression
--> $DIR/contract-attributes-nest.rs:23:1
|
LL | #[core::contracts::ensures(|ret| *ret > 100)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable expression
...
LL | return x.baz + 50;
| ----------------- any code following this expression is unreachable
|
= note: `#[warn(unreachable_code)]` on by default

warning: 2 warnings emitted

11 changes: 11 additions & 0 deletions tests/ui/contracts/contract-const-fn.all_pass.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
warning: the feature `contracts` is incomplete and may not be safe to use and/or cause compiler crashes
--> $DIR/contract-const-fn.rs:17:12
|
LL | #![feature(contracts)]
| ^^^^^^^^^
|
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted

56 changes: 56 additions & 0 deletions tests/ui/contracts/contract-const-fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Check if we can annotate a constant function with contracts.
//!
//! The contract is only checked at runtime, and it will not fail if evaluated statically.
//! This is an existing limitation due to the existing architecture and the lack of constant
//! closures.
//!
//@ revisions: all_pass runtime_fail_pre runtime_fail_post
//
//@ [all_pass] run-pass
//
//@ [runtime_fail_pre] run-fail
//@ [runtime_fail_post] run-fail
//
//@ [all_pass] compile-flags: -Zcontract-checks=yes
//@ [runtime_fail_pre] compile-flags: -Zcontract-checks=yes
//@ [runtime_fail_post] compile-flags: -Zcontract-checks=yes
#![feature(contracts)]
//~^ WARN the feature `contracts` is incomplete and may not be safe to use and/or cause compiler crashes [incomplete_features]

extern crate core;
use core::contracts::*;

#[requires(x < 100)]
const fn less_than_100(x: u8) -> u8 {
x
}

// This is wrong on purpose.
#[ensures(|ret| *ret)]
const fn always_true(b: bool) -> bool {
b
}

const ZERO: u8 = less_than_100(0);
// This is no-op because the contract cannot be checked at compilation time.
const TWO_HUNDRED: u8 = less_than_100(200);

/// Example from <https://github.com/rust-lang/rust/issues/136925>.
#[ensures(move |ret: &u32| *ret > x)]
const fn broken_sum(x: u32, y: u32) -> u32 {
x + y
}

fn main() {
assert_eq!(ZERO, 0);
assert_eq!(TWO_HUNDRED, 200);
assert_eq!(broken_sum(0, 1), 1);
assert_eq!(always_true(true), true);

#[cfg(runtime_fail_post)]
let _ok = always_true(false);

// Runtime check should fail.
#[cfg(runtime_fail_pre)]
let _200 = less_than_100(200);
}
11 changes: 11 additions & 0 deletions tests/ui/contracts/contract-const-fn.runtime_fail_post.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
warning: the feature `contracts` is incomplete and may not be safe to use and/or cause compiler crashes
--> $DIR/contract-const-fn.rs:17:12
|
LL | #![feature(contracts)]
| ^^^^^^^^^
|
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted

11 changes: 11 additions & 0 deletions tests/ui/contracts/contract-const-fn.runtime_fail_pre.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
warning: the feature `contracts` is incomplete and may not be safe to use and/or cause compiler crashes
--> $DIR/contract-const-fn.rs:17:12
|
LL | #![feature(contracts)]
| ^^^^^^^^^
|
= note: see issue #128044 <https://github.com/rust-lang/rust/issues/128044> for more information
= note: `#[warn(incomplete_features)]` on by default

warning: 1 warning emitted

Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
fn nest(x: Baz) -> i32
contract_requires(|| x.baz > 0)
contract_ensures(|ret| *ret > 100)
//~^ WARN unreachable expression [unreachable_code]
{
loop {
return x.baz + 50;
6 changes: 3 additions & 3 deletions tests/ui/contracts/internal_machinery/contract-intrinsics.rs
Original file line number Diff line number Diff line change
@@ -26,11 +26,11 @@ fn main() {
#[cfg(any(default, unchk_pass, chk_fail_requires))]
core::intrinsics::contract_check_requires(|| false);

let doubles_to_two = { let old = 2; move |ret| ret + ret == old };
let doubles_to_two = { let old = 2; move |ret: &u32 | ret + ret == old };
// Always pass
core::intrinsics::contract_check_ensures(&1, doubles_to_two);
core::intrinsics::contract_check_ensures(1, doubles_to_two);

// Fail if enabled
#[cfg(any(default, unchk_pass, chk_fail_ensures))]
core::intrinsics::contract_check_ensures(&2, doubles_to_two);
core::intrinsics::contract_check_ensures(2, doubles_to_two);
}
4 changes: 2 additions & 2 deletions tests/ui/contracts/internal_machinery/contract-lang-items.rs
Original file line number Diff line number Diff line change
@@ -15,14 +15,14 @@
#![feature(contracts)] // to access core::contracts
//~^ WARN the feature `contracts` is incomplete and may not be safe to use and/or cause compiler crashes [incomplete_features]
#![feature(contracts_internals)] // to access check_requires lang item

#![feature(core_intrinsics)]
fn foo(x: Baz) -> i32 {
let injected_checker = {
core::contracts::build_check_ensures(|ret| *ret > 100)
};

let ret = x.baz + 50;
injected_checker(ret)
core::intrinsics::contract_check_ensures(ret, injected_checker)
}

struct Baz { baz: i32 }
Loading