Skip to content

Commit fd838b2

Browse files
Fix loop-contract transform for post-#145513 deref temporaries (#4659)
rust-lang/rust#145513 (first shipped in nightly-2025-10-13) removed `Rvalue::CopyForDeref` from runtime MIR: the compiler-generated deref temporaries that destructuring patterns like `for (i, &p) in ...` introduce are now assigned via plain `Rvalue::Use(Operand::Copy)`. `replace_first_pat_by_nth_pat` distinguished pattern bindings from deref temporaries purely by rvalue kind, so with the new MIR shape a deref temporary is treated like a pattern binding: its assignment is redirected to the nthpat temporary, leaving the firstpat temporary uninitialized, and the subsequent dereference of that temporary yields spurious "dereference failure" checks (see #4658 for the full analysis). This is what currently breaks the `num::dec2flt::decimal_seq` proofs in model-checking/verify-rust-std#530. The fix pairs up only user variables (pattern bindings, as identified via `var_debug_info`) in the firstprj→nthprj map, and handles unmapped `Use(Copy)` assignments sourced from the firstpat variable exactly like the old `CopyForDeref` case: keep the assigned place, only re-point the rvalue at the nthpat variable. The `CopyForDeref` match arm is kept for defensiveness. Manual testing performed: * New regression test `tests/expected/loop-contract/for_loop_ref_pattern.rs` (modelled after `number_of_digits_decimal_left_shift` from the Rust standard library) fails before the fix with 6 spurious dereference failures and passes with it. * Full `expected` suite: 404 passed / 1 failed — the failure (`shadow/unsupported_num_objects`) is pre-existing and also fails on unmodified `main` (unsupported `__rust_alloc_error_handler` foreign function). * Backported the patch onto d4df833 (the Kani commit verify-rust-std is about to pin) and verified that the two failing verify-rust-std harnesses (`decimal_seq_verify::check_left_shift`, `decimal_seq_verify::check_number_of_digits_decimal_left_shift`) verify successfully. Resolves #4658 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent 1b1d6c1 commit fd838b2

3 files changed

Lines changed: 89 additions & 8 deletions

File tree

kani-compiler/src/kani_middle/transform/loop_contracts.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,20 @@ impl LoopContractPass {
301301
let mut firstprj_nthprj: HashMap<usize, usize> = HashMap::new();
302302
firstprj_nthprj.insert(firstvar, nthvar);
303303

304+
// Only user variables (pattern bindings) are paired up with their nthpat
305+
// counterparts. Compiler-generated temporaries (e.g. the deref temporaries
306+
// that destructuring patterns like `&x` introduce) must not be paired:
307+
// their assignments are kept in place (with the rvalue redirected to
308+
// nthvar) so that later statements reading them remain well-defined.
309+
// Note: before rust-lang/rust#145513 such temporaries were assigned via
310+
// `Rvalue::CopyForDeref` and thus never matched the `Rvalue::Use` pattern
311+
// below; nowadays they are plain `Rvalue::Use(Operand::Copy)` assignments.
312+
let user_vars = self.get_user_defined_variables(body);
304313
for fstmt in firstprj_stmts_copy.iter() {
305314
if let StatementKind::Assign(fprjplace, frval) = &fstmt.kind
306315
&& let Rvalue::Use(Operand::Copy(firstpatplace)) = frval
307316
&& firstpatplace.local == firstvar
317+
&& user_vars.contains(&fprjplace.local)
308318
{
309319
let firstprj = fprjplace.local;
310320
for istmt in nthprj_stmts_copy.iter() {
@@ -342,17 +352,29 @@ impl LoopContractPass {
342352
match frval {
343353
Rvalue::Use(Operand::Copy(firstpatplace)) => {
344354
if firstpatplace.local == firstvar {
345-
let nthprj = firstprj_nthprj.get(&fprjplace.local).unwrap();
346355
let mut nthpatplace = firstpatplace.clone();
347356
nthpatplace.local = nthvar;
348357
let newrval = Rvalue::Use(Operand::Copy(nthpatplace));
349-
new_stmt.kind = StatementKind::Assign(
350-
Place {
351-
local: *nthprj,
352-
projection: fprjplace.projection.clone(),
353-
},
354-
newrval,
355-
);
358+
if let Some(nthprj) = firstprj_nthprj.get(&fprjplace.local) {
359+
// A user pattern binding: redirect the assignment to
360+
// the corresponding nthpat projection variable.
361+
new_stmt.kind = StatementKind::Assign(
362+
Place {
363+
local: *nthprj,
364+
projection: fprjplace.projection.clone(),
365+
},
366+
newrval,
367+
);
368+
} else {
369+
// A compiler-generated temporary (e.g. a deref
370+
// temporary, `Rvalue::CopyForDeref` before
371+
// rust-lang/rust#145513): keep the assigned place and
372+
// only redirect the rvalue to read from nthvar, so
373+
// that following statements dereferencing the
374+
// temporary keep working.
375+
new_stmt.kind =
376+
StatementKind::Assign(fprjplace.clone(), newrval);
377+
}
356378
}
357379
}
358380
Rvalue::CopyForDeref(firstpatplace) => {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
lookup.loop_invariant_base.1\
2+
- Status: SUCCESS\
3+
- Description: "Check invariant before entry for loop lookup.0"
4+
5+
check_ref_pattern_deref_temp.assertion.1\
6+
- Status: SUCCESS\
7+
- Description: "assertion failed: n <= 2"
8+
9+
VERIFICATION:- SUCCESSFUL
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
// kani-flags: -Z loop-contracts
5+
6+
//! Check for-loop invariant for a loop whose pattern destructures the
7+
//! iterator element through a reference, e.g. `for (i, &p) in ...`.
8+
//! Such patterns introduce compiler-generated deref temporaries in the
9+
//! pattern-binding block. Those temporaries used to be assigned via
10+
//! `Rvalue::CopyForDeref`, which rust-lang/rust#145513 turned into plain
11+
//! copies, and the loop-contract transformation has to keep their
12+
//! assignments in place instead of treating them like pattern bindings
13+
//! (see https://github.com/model-checking/kani/issues/4658).
14+
//! This is a regression test for the spurious "dereference failure"
15+
//! checks Kani used to emit for this pattern; it is modelled after
16+
//! `number_of_digits_decimal_left_shift` in the Rust standard library.
17+
18+
#![feature(proc_macro_hygiene)]
19+
#![feature(stmt_expr_attributes)]
20+
21+
const TABLE: [u8; 16] = [5, 2, 5, 1, 2, 5, 6, 2, 5, 3, 1, 2, 5, 1, 5, 6];
22+
23+
fn lookup(digits: &[u8; 16], num_digits: usize, a: usize, b: usize) -> usize {
24+
let num_new_digits: usize = 2;
25+
let pow5 = &TABLE[a..];
26+
27+
#[kani::loop_invariant(num_new_digits > 1)]
28+
for (i, &p5) in pow5.iter().enumerate().take(b - a) {
29+
if i >= num_digits {
30+
return num_new_digits - 1;
31+
} else if digits[i] == p5 {
32+
continue;
33+
} else if digits[i] < p5 {
34+
return num_new_digits - 1;
35+
} else {
36+
return num_new_digits;
37+
}
38+
}
39+
num_new_digits
40+
}
41+
42+
#[kani::proof]
43+
fn check_ref_pattern_deref_temp() {
44+
let digits: [u8; 16] = kani::any();
45+
let num_digits: usize = kani::any_where(|x| *x <= 16);
46+
let a: usize = kani::any_where(|x| *x < 8);
47+
let b: usize = kani::any_where(|x| *x >= a && *x <= 16);
48+
let n = lookup(&digits, num_digits, a, b);
49+
assert!(n <= 2);
50+
}

0 commit comments

Comments
 (0)