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

#[contracts::requires(...)] + #[contracts::ensures(...)] #128045

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
9 changes: 8 additions & 1 deletion compiler/rustc_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3348,11 +3348,18 @@ pub struct Impl {
pub items: ThinVec<P<AssocItem>>,
}

#[derive(Clone, Encodable, Decodable, Debug, Default)]
pub struct FnContract {
pub requires: Option<P<Expr>>,
pub ensures: Option<P<Expr>>,
}

#[derive(Clone, Encodable, Decodable, Debug)]
pub struct Fn {
pub defaultness: Defaultness,
pub generics: Generics,
pub sig: FnSig,
pub contract: Option<P<FnContract>>,
pub body: Option<P<Block>>,
}

Expand Down Expand Up @@ -3650,7 +3657,7 @@ mod size_asserts {
static_assert_size!(Block, 32);
static_assert_size!(Expr, 72);
static_assert_size!(ExprKind, 40);
static_assert_size!(Fn, 160);
static_assert_size!(Fn, 168);
static_assert_size!(ForeignItem, 88);
static_assert_size!(ForeignItemKind, 16);
static_assert_size!(GenericArg, 24);
Expand Down
19 changes: 18 additions & 1 deletion compiler/rustc_ast/src/mut_visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ pub trait MutVisitor: Sized {
walk_flat_map_assoc_item(self, i, ctxt)
}

fn visit_contract(&mut self, c: &mut P<FnContract>) {
walk_contract(self, c);
}

fn visit_fn_decl(&mut self, d: &mut P<FnDecl>) {
walk_fn_decl(self, d);
}
Expand Down Expand Up @@ -958,13 +962,16 @@ fn walk_fn<T: MutVisitor>(vis: &mut T, kind: FnKind<'_>) {
_ctxt,
_ident,
_vis,
Fn { defaultness, generics, body, sig: FnSig { header, decl, span } },
Fn { defaultness, generics, contract, body, sig: FnSig { header, decl, span } },
) => {
// Identifier and visibility are visited as a part of the item.
visit_defaultness(vis, defaultness);
vis.visit_fn_header(header);
vis.visit_generics(generics);
vis.visit_fn_decl(decl);
if let Some(contract) = contract {
vis.visit_contract(contract);
}
if let Some(body) = body {
vis.visit_block(body);
}
Expand All @@ -979,6 +986,16 @@ fn walk_fn<T: MutVisitor>(vis: &mut T, kind: FnKind<'_>) {
}
}

fn walk_contract<T: MutVisitor>(vis: &mut T, contract: &mut P<FnContract>) {
let FnContract { requires, ensures } = contract.deref_mut();
if let Some(pred) = requires {
vis.visit_expr(pred);
}
if let Some(pred) = ensures {
vis.visit_expr(pred);
}
}

fn walk_fn_decl<T: MutVisitor>(vis: &mut T, decl: &mut P<FnDecl>) {
let FnDecl { inputs, output } = decl.deref_mut();
inputs.flat_map_in_place(|param| vis.flat_map_param(param));
Expand Down
17 changes: 16 additions & 1 deletion compiler/rustc_ast/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ pub trait Visitor<'ast>: Sized {
fn visit_closure_binder(&mut self, b: &'ast ClosureBinder) -> Self::Result {
walk_closure_binder(self, b)
}
fn visit_contract(&mut self, c: &'ast FnContract) -> Self::Result {
walk_contract(self, c)
}
fn visit_where_predicate(&mut self, p: &'ast WherePredicate) -> Self::Result {
walk_where_predicate(self, p)
}
Expand Down Expand Up @@ -800,6 +803,17 @@ pub fn walk_closure_binder<'a, V: Visitor<'a>>(
V::Result::output()
}

pub fn walk_contract<'a, V: Visitor<'a>>(visitor: &mut V, c: &'a FnContract) -> V::Result {
let FnContract { requires, ensures } = c;
if let Some(pred) = requires {
visitor.visit_expr(pred);
}
if let Some(pred) = ensures {
visitor.visit_expr(pred);
}
V::Result::output()
}

pub fn walk_where_predicate<'a, V: Visitor<'a>>(
visitor: &mut V,
predicate: &'a WherePredicate,
Expand Down Expand Up @@ -862,12 +876,13 @@ pub fn walk_fn<'a, V: Visitor<'a>>(visitor: &mut V, kind: FnKind<'a>) -> V::Resu
_ctxt,
_ident,
_vis,
Fn { defaultness: _, sig: FnSig { header, decl, span: _ }, generics, body },
Fn { defaultness: _, sig: FnSig { header, decl, span: _ }, generics, contract, body },
) => {
// Identifier and visibility are visited as a part of the item.
try_visit!(visitor.visit_fn_header(header));
try_visit!(visitor.visit_generics(generics));
try_visit!(visitor.visit_fn_decl(decl));
visit_opt!(visitor, visit_contract, contract);
visit_opt!(visitor, visit_block, body);
}
FnKind::Closure(binder, coroutine_kind, decl, body) => {
Expand Down
15 changes: 14 additions & 1 deletion compiler/rustc_ast_lowering/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,20 @@ impl<'hir> LoweringContext<'_, 'hir> {
hir::ExprKind::Continue(self.lower_jump_destination(e.id, *opt_label))
}
ExprKind::Ret(e) => {
let e = e.as_ref().map(|x| self.lower_expr(x));
let mut e = e.as_ref().map(|x| self.lower_expr(x));
if let Some(Some((span, fresh_ident))) = self
.contract
.as_ref()
.map(|c| c.ensures.as_ref().map(|e| (e.expr.span, e.fresh_ident)))
{
let checker_fn = self.expr_ident(span, fresh_ident.0, fresh_ident.2);
let args = if let Some(e) = e {
std::slice::from_ref(e)
} else {
std::slice::from_ref(self.expr_unit(span))
};
e = Some(self.expr_call(span, checker_fn, args));
}
Copy link
Contributor

@oli-obk oli-obk Dec 10, 2024

Choose a reason for hiding this comment

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

returns kind of duplicate logic with the trailing expression, something can be improved here

Copy link
Member Author

Choose a reason for hiding this comment

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

yes I agree. It should also be unified with how the Try operator is implemented. I.e. all three cases (return, ?, and the tail expression) should all call some common method so that code like this can be controlled in one place.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is still unadressed

Copy link
Contributor

Choose a reason for hiding this comment

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

I will address this in my next commit as I add support for Try and Yeet. I don't think we need to handle Yield, since we can only attach contracts to functions, but I'll add a new check to the limitations test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done!

hir::ExprKind::Ret(e)
}
ExprKind::Yeet(sub_expr) => self.lower_expr_yeet(e.span, sub_expr.as_deref()),
Expand Down
112 changes: 108 additions & 4 deletions compiler/rustc_ast_lowering/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,42 @@ impl<'hir> LoweringContext<'_, 'hir> {
sig: FnSig { decl, header, span: fn_sig_span },
generics,
body,
contract,
..
}) => {
self.with_new_scopes(*fn_sig_span, |this| {
assert!(this.contract.is_none());
if let Some(contract) = contract {
let requires = contract.requires.clone();
let ensures = contract.ensures.clone();
let ensures = if let Some(ens) = ensures {
// FIXME: this needs to be a fresh (or illegal) identifier to prevent
// accidental capture of a parameter or global variable.
let checker_ident: Ident =
Ident::from_str_and_span("__ensures_checker", ens.span);
let (checker_pat, checker_hir_id) = this.pat_ident_binding_mode_mut(
ens.span,
checker_ident,
hir::BindingMode::NONE,
);

Some(crate::FnContractLoweringEnsures {
expr: ens,
fresh_ident: (checker_ident, checker_pat, checker_hir_id),
})
} else {
None
};

// Note: `with_new_scopes` will reinstall the outer
// item's contract (if any) after its callback finishes.
this.contract.replace(crate::FnContractLoweringInfo {
span,
requires,
ensures,
});
}

// Note: we don't need to change the return type from `T` to
// `impl Future<Output = T>` here because lower_body
// only cares about the input argument patterns in the function
Expand Down Expand Up @@ -1054,10 +1087,81 @@ impl<'hir> LoweringContext<'_, 'hir> {
body: impl FnOnce(&mut Self) -> hir::Expr<'hir>,
) -> hir::BodyId {
self.lower_body(|this| {
(
this.arena.alloc_from_iter(decl.inputs.iter().map(|x| this.lower_param(x))),
body(this),
)
let params =
this.arena.alloc_from_iter(decl.inputs.iter().map(|x| this.lower_param(x)));
let result = body(this);

let opt_contract = this.contract.take();

// { body }
// ==>
// { rustc_contract_requires(PRECOND); { body } }
let result: hir::Expr<'hir> = if let Some(contract) = opt_contract {
let lit_unit = |this: &mut LoweringContext<'_, 'hir>| {
this.expr(contract.span, hir::ExprKind::Tup(&[]))
};

let precond: hir::Stmt<'hir> = if let Some(req) = contract.requires {
let lowered_req = this.lower_expr_mut(&req);
let precond = this.expr_call_lang_item_fn_mut(
req.span,
hir::LangItem::ContractCheckRequires,
&*arena_vec![this; lowered_req],
);
this.stmt_expr(req.span, precond)
} else {
let u = lit_unit(this);
this.stmt_expr(contract.span, u)
};

let (postcond_checker, result) = if let Some(ens) = contract.ensures {
let crate::FnContractLoweringEnsures { expr: ens, fresh_ident } = ens;
let lowered_ens: hir::Expr<'hir> = this.lower_expr_mut(&ens);
let postcond_checker = this.expr_call_lang_item_fn(
ens.span,
hir::LangItem::ContractBuildCheckEnsures,
&*arena_vec![this; lowered_ens],
);
let checker_binding_pat = fresh_ident.1;
(
this.stmt_let_pat(
None,
ens.span,
Some(postcond_checker),
this.arena.alloc(checker_binding_pat),
hir::LocalSource::Contract,
),
{
let checker_fn =
this.expr_ident(ens.span, fresh_ident.0, fresh_ident.2);
let span = this.mark_span_with_reason(
DesugaringKind::Contract,
ens.span,
None,
);
this.expr_call_mut(
span,
checker_fn,
std::slice::from_ref(this.arena.alloc(result)),
)
},
)
} else {
let u = lit_unit(this);
(this.stmt_expr(contract.span, u), result)
};

let block = this.block_all(
contract.span,
arena_vec![this; precond, postcond_checker],
Some(this.arena.alloc(result)),
);
this.expr_block(block)
} else {
result
};

(params, result)
})
}

Expand Down
20 changes: 20 additions & 0 deletions compiler/rustc_ast_lowering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ mod path;

rustc_fluent_macro::fluent_messages! { "../messages.ftl" }

#[derive(Debug, Clone)]
struct FnContractLoweringInfo<'hir> {
pub span: Span,
pub requires: Option<ast::ptr::P<ast::Expr>>,
pub ensures: Option<FnContractLoweringEnsures<'hir>>,
}

#[derive(Debug, Clone)]
struct FnContractLoweringEnsures<'hir> {
expr: ast::ptr::P<ast::Expr>,
fresh_ident: (Ident, hir::Pat<'hir>, HirId),
}

struct LoweringContext<'a, 'hir> {
tcx: TyCtxt<'hir>,
resolver: &'a mut ResolverAstLowering,
Expand All @@ -100,6 +113,8 @@ struct LoweringContext<'a, 'hir> {
/// Collect items that were created by lowering the current owner.
children: Vec<(LocalDefId, hir::MaybeOwner<'hir>)>,

contract: Option<FnContractLoweringInfo<'hir>>,

coroutine_kind: Option<hir::CoroutineKind>,

/// When inside an `async` context, this is the `HirId` of the
Expand Down Expand Up @@ -148,6 +163,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
bodies: Vec::new(),
attrs: SortedMap::default(),
children: Vec::default(),
contract: None,
current_hir_id_owner: hir::CRATE_OWNER_ID,
item_local_id_counter: hir::ItemLocalId::ZERO,
ident_and_label_to_local_id: Default::default(),
Expand Down Expand Up @@ -834,12 +850,16 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
let was_in_loop_condition = self.is_in_loop_condition;
self.is_in_loop_condition = false;

let old_contract = self.contract.take();

let catch_scope = self.catch_scope.take();
let loop_scope = self.loop_scope.take();
let ret = f(self);
self.catch_scope = catch_scope;
self.loop_scope = loop_scope;

self.contract = old_contract;

self.is_in_loop_condition = was_in_loop_condition;

self.current_item = current_item;
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_ast_passes/src/ast_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
walk_list!(self, visit_attribute, &item.attrs);
return; // Avoid visiting again.
}
ItemKind::Fn(func @ box Fn { defaultness, generics: _, sig, body }) => {
ItemKind::Fn(func @ box Fn { defaultness, generics: _, sig, contract: _, body }) => {
self.check_defaultness(item.span, *defaultness);

let is_intrinsic =
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_ast_passes/src/feature_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ pub fn check_crate(krate: &ast::Crate, sess: &Session, features: &Features) {
gate_all!(pin_ergonomics, "pinned reference syntax is experimental");
gate_all!(unsafe_fields, "`unsafe` fields are experimental");
gate_all!(unsafe_binders, "unsafe binder types are experimental");
gate_all!(rustc_contracts, "contracts are experimental");
gate_all!(rustc_contracts_internals, "contract internal machinery is for internal use only");

if !visitor.features.never_patterns() {
if let Some(spans) = spans.get(&sym::never_patterns) {
Expand Down
21 changes: 20 additions & 1 deletion compiler/rustc_ast_pretty/src/pprust/state/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,13 +650,17 @@ impl<'a> State<'a> {
attrs: &[ast::Attribute],
func: &ast::Fn,
) {
let ast::Fn { defaultness, generics, sig, body } = func;
let ast::Fn { defaultness, generics, sig, contract, body } = func;
if body.is_some() {
self.head("");
}
self.print_visibility(vis);
self.print_defaultness(*defaultness);
self.print_fn(&sig.decl, sig.header, Some(name), generics);
if let Some(contract) = &contract {
self.nbsp();
self.print_contract(contract);
}
if let Some(body) = body {
self.nbsp();
self.print_block_with_attrs(body, attrs);
Expand All @@ -665,6 +669,21 @@ impl<'a> State<'a> {
}
}

fn print_contract(&mut self, contract: &ast::FnContract) {
if let Some(pred) = &contract.requires {
self.word("rustc_requires");
self.popen();
self.print_expr(pred, FixupContext::default());
self.pclose();
}
if let Some(pred) = &contract.ensures {
self.word("rustc_ensures");
self.popen();
self.print_expr(pred, FixupContext::default());
self.pclose();
}
}

pub(crate) fn print_fn(
&mut self,
decl: &ast::FnDecl,
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_borrowck/src/type_check/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
ConstraintCategory::SizedBound,
);
}
&Rvalue::NullaryOp(NullOp::ContractChecks, _) => {}
&Rvalue::NullaryOp(NullOp::UbChecks, _) => {}

Rvalue::ShallowInitBox(operand, ty) => {
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_builtin_macros/src/alloc_error_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ fn generate_handler(cx: &ExtCtxt<'_>, handler: Ident, span: Span, sig_span: Span
defaultness: ast::Defaultness::Final,
sig,
generics: Generics::default(),
contract: None,
body,
}));

Expand Down
Loading
Loading