Skip to content

Commit

Permalink
Support let-else (#1408)
Browse files Browse the repository at this point in the history
* Support let-else.

restrict the never return in exec function

Use NeverToAny to avoid the change in ast_to_sst

Add els in StmtX::Decl && simplify the let-els in ast_simplify

fix bug in vistor change about els

* Add tests about let-else statements

* Fix lifetime checker error due to missing entry in ctxt.var_modes

* Add check_expr_has_mode for else block

* Add a comment for let-else
  • Loading branch information
ziqiaozhou authored Feb 11, 2025
1 parent 5fd2214 commit d48f130
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 37 deletions.
2 changes: 1 addition & 1 deletion source/rust_verify/src/lifetime_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ pub(crate) type Stm = Box<(Span, StmX)>;
#[derive(Debug, Clone)]
pub(crate) enum StmX {
Expr(Exp),
Let(Pattern, Typ, Option<Exp>),
Let(Pattern, Typ, Option<Exp>, Option<Exp>), // (pat, typ, init_expr, else_block)
}

#[derive(Debug)]
Expand Down
13 changes: 10 additions & 3 deletions source/rust_verify/src/lifetime_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,15 +714,22 @@ pub(crate) fn emit_stm(state: &mut EmitState, stm: &Stm) {
emit_exp(state, exp);
state.write(";");
}
StmX::Let(pat, typ, init) => {
StmX::Let(pat, typ, init, els) => {
state.write("let ");
emit_pattern(state, pat);
state.write(": ");
state.write(typ.to_string());
if els.is_none() {
state.write(": ");
state.write(typ.to_string());
}
if let Some(init) = init {
state.write(" = ");
emit_exp(state, init);
}
if let Some(els) = els {
state.write(" else {");
emit_exp(state, els);
state.write("}");
}
state.write(";");
}
}
Expand Down
13 changes: 11 additions & 2 deletions source/rust_verify/src/lifetime_generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1682,8 +1682,11 @@ fn erase_stmt<'tcx>(ctxt: &Context<'tcx>, state: &mut State, stmt: &Stmt<'tcx>)
vec![]
}
}
StmtKind::Let(LetStmt { pat, ty: _, init, els: _, hir_id, span: _, source: _ }) => {
StmtKind::Let(LetStmt { pat, ty: _, init, els, hir_id, span: _, source: _ }) => {
let mode = ctxt.var_modes[&pat.hir_id];
if mode != Mode::Exec && els.is_some() {
panic!("let-else is not supported in spec");
}
if mode == Mode::Spec {
if let Some(init) = init {
if let Some(e) = erase_expr(ctxt, state, true, init) {
Expand All @@ -1699,7 +1702,13 @@ fn erase_stmt<'tcx>(ctxt: &Context<'tcx>, state: &mut State, stmt: &Stmt<'tcx>)
let typ = erase_ty(ctxt, state, &ctxt.types().node_type(*hir_id));
let init_exp =
if let Some(init) = init { erase_expr(ctxt, state, false, init) } else { None };
vec![Box::new((stmt.span, StmX::Let(pat, typ, init_exp)))]
let els_expr = if let Some(els) = els {
erase_block(ctxt, state, false, &els)
.or_else(|| Some(Box::new((els.span, ExpX::Panic))))
} else {
None
};
vec![Box::new((stmt.span, StmX::Let(pat, typ, init_exp, els_expr)))]
}
}
StmtKind::Item(..) => panic!("unexpected statement"),
Expand Down
22 changes: 16 additions & 6 deletions source/rust_verify/src/rust_to_vir_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2493,10 +2493,23 @@ pub(crate) fn let_stmt_to_vir<'tcx>(
bctx: &BodyCtxt<'tcx>,
pattern: &rustc_hir::Pat<'tcx>,
initializer: &Option<&Expr<'tcx>>,
els: &Option<&Block<'tcx>>,
attrs: &[Attribute],
) -> Result<Vec<vir::ast::Stmt>, VirErr> {
let mode = get_var_mode(bctx.mode, attrs);
let infer_mode = parse_attrs_opt(attrs, None).contains(&Attr::InferMode);
let els = if let Some(els) = els {
if matches!(mode, Mode::Spec | Mode::Proof) {
unsupported_err!(els.span, "let-else in spec/proof", els);
}
let init = initializer.unwrap();
let init_type = typ_of_node(bctx, els.span, &init.hir_id, false)?;
let els_typ = typ_of_node(bctx, els.span, &els.hir_id, false)?;
let els_block = block_to_vir(bctx, els, &els.span, &els_typ, ExprModifier::REGULAR)?;
Some(bctx.spanned_typed_new(els.span, &init_type, ExprX::NeverToAny(els_block)))
} else {
None
};
let init = initializer.map(|e| expr_to_vir(bctx, e, ExprModifier::REGULAR)).transpose()?;

if parse_attrs_opt(attrs, Some(&mut *bctx.ctxt.diagnostics.borrow_mut()))
Expand Down Expand Up @@ -2527,7 +2540,7 @@ pub(crate) fn let_stmt_to_vir<'tcx>(

let vir_pattern = pattern_to_vir(bctx, pattern)?;
let mode = if infer_mode { None } else { Some(mode) };
Ok(vec![bctx.spanned_new(pattern.span, StmtX::Decl { pattern: vir_pattern, mode, init })])
Ok(vec![bctx.spanned_new(pattern.span, StmtX::Decl { pattern: vir_pattern, mode, init, els })])
}

fn unwrap_parameter_to_vir<'tcx>(
Expand Down Expand Up @@ -2663,11 +2676,8 @@ pub(crate) fn stmt_to_vir<'tcx>(
unsupported_err!(stmt.span, "internal item statements", stmt)
}
}
StmtKind::Let(LetStmt { pat, ty: _, init, els: None, hir_id: _, span: _, source: _ }) => {
let_stmt_to_vir(bctx, pat, init, bctx.ctxt.tcx.hir().attrs(stmt.hir_id))
}
StmtKind::Let(LetStmt { els: Some(_), .. }) => {
unsupported_err!(stmt.span, "let-else", stmt)
StmtKind::Let(LetStmt { pat, ty: _, init, els, hir_id: _, span: _, source: _ }) => {
let_stmt_to_vir(bctx, pat, init, els, bctx.ctxt.tcx.hir().attrs(stmt.hir_id))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions source/rust_verify/src/rust_to_vir_func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ pub(crate) fn check_item_fn<'tcx>(
pattern: new_binding_pat,
mode: Some(mode),
init: Some(new_init_expr),
els: None,
},
);
mut_params_redecl.push(redecl);
Expand Down
151 changes: 151 additions & 0 deletions source/rust_verify_test/tests/let_else.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#![feature(rustc_private)]
#[macro_use]
mod common;
use common::*;

test_verify_one_file! {
#[test] test_let_else_returns verus_code! {
enum X {
A(usize),
B(bool),
}
spec fn is_a(x: X) -> bool {
match x {
X::A(..) => {
true
}
_ => {
false
}
}
}
fn f(x: X) -> (ret: bool)
ensures is_a(x) == ret
{
let X::A(a) = x else {
let x: bool = false;
return x;
};
return true;
}
} => Ok(())
}

test_verify_one_file! {
#[test] test_let_else_ensures_failed_no_unwind_condition2 verus_code! {
enum X {
A(usize),
B(bool),
}
#[verifier(external_body)]
fn call_panic() -> ! {
panic!();
}
spec fn is_a(x: X) -> bool {
if let X::A(..) = x {
true
} else {
false
}
}
fn f(x: X) -> (ret: bool)
ensures is_a(x) == ret
{
let X::A(a) = x else {
return true; // FAILS
};
return true;
}
} => Err(err) => assert_one_fails(err)
}

test_verify_one_file! {
#[test] test_let_else_dots verus_code! {
struct A {
x: usize,
y: bool,
z: i32,
}
enum X {
U(usize),
B(bool),
A(A, usize, bool),
}
#[verifier(external_body)]
fn call_panic() -> ! {
panic!();
}
spec fn is_a(x: X) -> bool {
if let X::A(..) = x {
true
} else {
false
}
}
spec fn check_x_b(x: X, val1: usize, val2: bool) -> bool {
match x {
X::A(A{x, ..}, ..,b) => {
x == val1 && b == val2
}
_ => {
false
}
}
}
fn f(x: X) -> (ret: (usize, bool))
ensures check_x_b(x, ret.0, ret.1)
no_unwind when is_a(x)
{
let X::A(A {x, ..}, .., b) = x else {
call_panic();
};
(x, b)
}
} => Ok(())
}

test_verify_one_file! {
#[test] let_else_not_supported_in_proof verus_code! {
enum X {
A(usize),
B(bool),
}
proof fn stuff(x: X) -> usize {
let X::A(y) = x else {
return 0;
};
y
}
} => Err(err) => assert_vir_error_msg(err, "The verifier does not yet support the following Rust feature: let-else in spec/proof")
}

test_verify_one_file! {
#[test] let_else_not_supported_in_spec verus_code! {
enum X {
A(usize),
B(bool),
}
spec fn stuff(x: X) -> usize {
let X::A(y) = x else {
return 0;
};
y
}
} => Err(err) => assert_vir_error_msg(err, "The verifier does not yet support the following Rust feature: let-else in spec/proof")
}

test_verify_one_file! {
#[test] let_else_verus_parse_else verus_code! {
enum X {
A(usize),
B(bool),
}
fn stuff(x: X) -> usize {
let X::A(y) = x else {
assert(false); // FAILS
return 0;
};
y
}
} => Err(err) => assert_one_fails(err)
}
11 changes: 0 additions & 11 deletions source/rust_verify_test/tests/match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1218,14 +1218,3 @@ test_verify_one_file! {
//} => Err(err) => assert_one_fails(err)
} => Err(err) => assert_vir_error_msg(err, "Not supported: pattern containing both an or-pattern (|) and an if-guard")
}

test_verify_one_file! {
#[test] let_expr_not_supported verus_code! {
fn stuff(x: Option<u64>) {
let Some(y) = x
else {
loop { }
};
}
} => Err(err) => assert_vir_error_msg(err, "The verifier does not yet support the following Rust feature: let-else")
}
2 changes: 1 addition & 1 deletion source/vir/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ pub enum StmtX {
/// The declaration may contain a pattern;
/// however, ast_simplify replaces all patterns with PatternX::Var
/// (The mode is only allowed to be None for one special case; see modes.rs)
Decl { pattern: Pattern, mode: Option<Mode>, init: Option<Expr> },
Decl { pattern: Pattern, mode: Option<Mode>, init: Option<Expr>, els: Option<Expr> },
}

/// Function parameter
Expand Down
27 changes: 20 additions & 7 deletions source/vir/src/ast_simplify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fn temp_expr(state: &mut State, expr: &Expr) -> (Stmt, Expr) {
let name = temp.clone();
let patternx = PatternX::Var { name, mutable: false };
let pattern = SpannedTyped::new(&expr.span, &expr.typ, patternx);
let decl = StmtX::Decl { pattern, mode: Some(Mode::Exec), init: Some(expr.clone()) };
let decl = StmtX::Decl { pattern, mode: Some(Mode::Exec), init: Some(expr.clone()), els: None };
let temp_decl = Spanned::new(expr.span.clone(), decl);
(temp_decl, SpannedTyped::new(&expr.span, &expr.typ, ExprX::Var(temp)))
}
Expand Down Expand Up @@ -139,7 +139,8 @@ fn pattern_to_exprs(
let patternx = PatternX::Var { name, mutable };
let pattern = SpannedTyped::new(&expr.span, &expr.typ, patternx);
// Mode doesn't matter at this stage; arbitrarily set it to 'exec'
let decl = StmtX::Decl { pattern, mode: Some(Mode::Exec), init: Some(expr.clone()) };
let decl =
StmtX::Decl { pattern, mode: Some(Mode::Exec), init: Some(expr.clone()), els: None };
decls.push(Spanned::new(expr.span.clone(), decl));
}

Expand Down Expand Up @@ -593,17 +594,27 @@ fn tuple_get_field_expr(

fn simplify_one_stmt(ctx: &GlobalCtx, state: &mut State, stmt: &Stmt) -> Result<Vec<Stmt>, VirErr> {
match &stmt.x {
StmtX::Decl { pattern, mode: _, init: None } => match &pattern.x {
StmtX::Decl { pattern, mode: _, init: None, els: None } => match &pattern.x {
PatternX::Var { .. } => Ok(vec![stmt.clone()]),
_ => Err(error(&stmt.span, "let-pattern declaration must have an initializer")),
},
StmtX::Decl { pattern, mode: _, init: Some(init) }
StmtX::Decl { pattern, mode: _, init: Some(init), els }
if !matches!(pattern.x, PatternX::Var { .. }) =>
{
let mut decls: Vec<Stmt> = Vec::new();
let (temp_decl, init) = small_or_temp(state, init);
decls.extend(temp_decl.into_iter());
let _ = pattern_to_exprs(ctx, state, &init, &pattern, &mut decls)?;
let mut decls2: Vec<Stmt> = Vec::new();
let pattern_check = pattern_to_exprs(ctx, state, &init, &pattern, &mut decls2)?;
if let Some(els) = &els {
let e = ExprX::Unary(UnaryOp::Not, pattern_check.clone());
let check = SpannedTyped::new(&pattern_check.span, &pattern_check.typ, e);
let ifx = ExprX::If(check.clone(), els.clone(), Some(init.clone()));
let init = SpannedTyped::new(&els.span, &init.typ, ifx);
let (temp_decl, _) = temp_expr(state, &init);
decls.push(temp_decl);
}
decls.extend(decls2);
Ok(decls)
}
_ => Ok(vec![stmt.clone()]),
Expand Down Expand Up @@ -732,7 +743,8 @@ fn exec_closure_spec_requires(
let patternx = PatternX::Var { name: p.name.clone(), mutable: false };
let pattern = SpannedTyped::new(span, &p.a, patternx);
let tuple_field = tuple_get_field_expr(state, span, &p.a, &tuple_var, params.len(), i);
let decl = StmtX::Decl { pattern, mode: Some(Mode::Spec), init: Some(tuple_field) };
let decl =
StmtX::Decl { pattern, mode: Some(Mode::Spec), init: Some(tuple_field), els: None };
decls.push(Spanned::new(span.clone(), decl));
}

Expand Down Expand Up @@ -792,7 +804,8 @@ fn exec_closure_spec_ensures(
let patternx = PatternX::Var { name: p.name.clone(), mutable: false };
let pattern = SpannedTyped::new(span, &p.a, patternx);
let tuple_field = tuple_get_field_expr(state, span, &p.a, &tuple_var, params.len(), i);
let decl = StmtX::Decl { pattern, mode: Some(Mode::Spec), init: Some(tuple_field) };
let decl =
StmtX::Decl { pattern, mode: Some(Mode::Spec), init: Some(tuple_field), els: None };
decls.push(Spanned::new(span.clone(), decl));
}

Expand Down
5 changes: 4 additions & 1 deletion source/vir/src/ast_to_sst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2257,7 +2257,10 @@ fn stmt_to_stm(
let (stms, exp) = expr_to_stm_opt(ctx, state, expr)?;
Ok((stms, exp, None))
}
StmtX::Decl { pattern, mode: _, init } => {
StmtX::Decl { pattern, mode: _, init, els } => {
if els.is_some() {
panic!("let-else should be simplified in ast_simpllify {:?}.", stmt)
}
let (name, mutable) = match &pattern.x {
PatternX::Var { name, mutable } => (name, mutable),
_ => panic!("internal error: Decl should have been simplified by ast_simplify"),
Expand Down
Loading

0 comments on commit d48f130

Please sign in to comment.