Skip to content

Commit 78eb465

Browse files
authored
Add #[kani::loop_decreases] for proving loop termination (#4564)
Add `#[kani::loop_decreases]` for proving loop termination via Floyd's method. Loop invariants alone prove partial correctness, they don't guarantee the loop terminates. A decreases clause specifies a measure that must strictly decrease on each iteration, proving total correctness. Maps to CBMC's `#spec_decreases` annotation via `goto-instrument`. ## Problem Without a decreases clause, a loop invariant can verify properties on code after the loop, but if the loop never terminates, that code is unreachable and the property is vacuously true. This is a soundness gap for total correctness proofs. ## Solution ### Proc macro (`kani_macros`) - `#[kani::loop_decreases(expr)]` attribute generates a `let kani_loop_decreases = (expr,);` binding before the loop - Validates the attribute is applied to a loop statement (`while`, `loop`, `for`) - Rejects multi-dimensional measures (`loop_decreases(a, b)`) with a compile error — lexicographic ordering is not yet supported (#4585) ### CBMC bindings (`cprover_bindings`) - `loop_decreases: Option<Expr>` field in `StmtBody::Goto` - `with_loop_decreases()` builder, mirroring `with_loop_invariants()` and `with_loop_modifies()` - `CSpecDecreases` irep serialization ### Compiler codegen (`kani-compiler`) - Detects `kani_loop_decreases` assignments in MIR and stores the expression - Attaches the decreases expression to the loop's goto statement - Emits a warning when `#[kani::loop_decreases]` is used without `-Z loop-contracts` ## Example ```rust #[kani::proof] fn binary_search_terminates() { let arr: [i32; 8] = kani::any(); let target: i32 = kani::any(); let mut lo: usize = 0; let mut hi: usize = arr.len(); #[kani::loop_invariant(lo <= hi && hi <= arr.len())] #[kani::loop_decreases(hi - lo)] while lo < hi { let mid = lo + (hi - lo) / 2; if arr[mid] == target { break; } else if arr[mid] < target { lo = mid + 1; } else { hi = mid; } } } ``` ## Testing **8 passing expected tests:** - `simple_while_loop_decreases` — basic countdown - `loop_loop_decreases` — `loop { ... break }` with decreases - `decreases_expr` — expression-based measure - `decreases_binary_search` — binary search termination - `decreases_loop_max` — function with loop + decreases - `decreases_with_function_contract` — combined with `#[kani::requires]`/`#[kani::ensures]` - `decreases_with_old` — using `on_entry()` in decreases - `decreases_no_flag` — warning emitted without `-Z loop-contracts` **2 expected-fail tests:** - `decreases_fail_wrong_measure` — incorrect measure (increasing instead of decreasing) - `decreases_fail_stale_measure` — stale measure (variable not modified by loop) **4 fixme tests** (known limitations, tracked as issues): - `fixme_decreases_struct_field` — struct field projections (#4584) - `fixme_multi_dim_decreases` — multi-dimensional measures (#4585) - `fixme_decreases_with_modifies` — combined with `loop_modifies` (#4586) - `fixme_nested_loops_decreases` — nested loops with assigns conflict (#4586) ## Known Limitations The following limitations are tracked as separate issues: - **Arithmetic expressions in decreases clauses** (e.g., `19 - p`) fail due to Rust's checked arithmetic producing `StatementExpression` nodes. Workaround: use `wrapping_sub`. (#4584) - **Multi-dimensional decreases** (`#[kani::loop_decreases(a, b)]`) is rejected at compile time; lexicographic ordering is not yet supported. (#4585) - **Combining `loop_decreases` with `loop_modifies`** causes spurious verification failures. Workaround: use `loop_decreases` + `loop_invariant` without `loop_modifies`. (#4586) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
1 parent 78a0630 commit 78eb465

38 files changed

Lines changed: 842 additions & 11 deletions

cprover_bindings/src/goto_program/stmt.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub enum StmtBody {
8686
// applied as loop contracts in CBMC if it is a backward goto.
8787
loop_invariants: Option<Expr>,
8888
loop_modifies: Option<Vec<Expr>>,
89+
loop_decreases: Option<Expr>,
8990
},
9091
/// `if (i) { t } else { e }`
9192
Ifthenelse {
@@ -282,7 +283,7 @@ impl Stmt {
282283
pub fn goto<T: Into<InternedString>>(dest: T, loc: Location) -> Self {
283284
let dest = dest.into();
284285
assert!(!dest.is_empty());
285-
stmt!(Goto { dest, loop_invariants: None, loop_modifies: None }, loc)
286+
stmt!(Goto { dest, loop_invariants: None, loop_modifies: None, loop_decreases: None }, loc)
286287
}
287288

288289
/// `if (i) { t } else { e }` or `if (i) { t }`
@@ -327,13 +328,14 @@ impl Stmt {
327328

328329
/// `goto dest;` with loop invariant
329330
pub fn with_loop_contracts(self, inv: Expr) -> Self {
330-
if let Goto { dest, loop_invariants, loop_modifies } = self.body() {
331+
if let Goto { dest, loop_invariants, loop_modifies, loop_decreases } = self.body() {
331332
assert!(loop_invariants.is_none());
332333
stmt!(
333334
Goto {
334335
dest: *dest,
335336
loop_invariants: Some(inv),
336-
loop_modifies: loop_modifies.clone()
337+
loop_modifies: loop_modifies.clone(),
338+
loop_decreases: loop_decreases.clone()
337339
},
338340
*self.location()
339341
)
@@ -343,20 +345,38 @@ impl Stmt {
343345
}
344346

345347
pub fn with_loop_modifies(self, asg: Vec<Expr>) -> Self {
346-
if let Goto { dest, loop_invariants, loop_modifies } = self.body() {
348+
if let Goto { dest, loop_invariants, loop_modifies, loop_decreases } = self.body() {
347349
assert!(loop_modifies.is_none());
348350
stmt!(
349351
Goto {
350352
dest: *dest,
351353
loop_invariants: loop_invariants.clone(),
352-
loop_modifies: Some(asg)
354+
loop_modifies: Some(asg),
355+
loop_decreases: loop_decreases.clone()
353356
},
354357
*self.location()
355358
)
356359
} else {
357360
unreachable!("Loop assigns should be annotated only to goto stmt")
358361
}
359362
}
363+
364+
pub fn with_loop_decreases(self, decreases: Expr) -> Self {
365+
if let Goto { dest, loop_invariants, loop_modifies, loop_decreases } = self.body() {
366+
assert!(loop_decreases.is_none());
367+
stmt!(
368+
Goto {
369+
dest: *dest,
370+
loop_invariants: loop_invariants.clone(),
371+
loop_modifies: loop_modifies.clone(),
372+
loop_decreases: Some(decreases)
373+
},
374+
*self.location()
375+
)
376+
} else {
377+
unreachable!("Loop decreases should be annotated only to goto stmt")
378+
}
379+
}
360380
}
361381

362382
/// Predicates

cprover_bindings/src/irep/irep_id.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ pub enum IrepId {
595595
Used,
596596
IsUsed,
597597
CSpecLoopInvariant,
598+
CSpecDecreases,
598599
CSpecRequires,
599600
CSpecEnsures,
600601
CSpecAssigns,
@@ -1485,6 +1486,7 @@ impl IrepId {
14851486
IrepId::Used => "used",
14861487
IrepId::IsUsed => "is_used",
14871488
IrepId::CSpecLoopInvariant => "#spec_loop_invariant",
1489+
IrepId::CSpecDecreases => "#spec_decreases",
14881490
IrepId::CSpecRequires => "#spec_requires",
14891491
IrepId::CSpecEnsures => "#spec_ensures",
14901492
IrepId::CSpecAssigns => "#spec_assigns",

cprover_bindings/src/irep/to_irep.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ impl ToIrep for StmtBody {
556556
arguments_irep(arguments.iter(), mm),
557557
],
558558
),
559-
StmtBody::Goto { dest, loop_invariants, loop_modifies } => {
559+
StmtBody::Goto { dest, loop_invariants, loop_modifies, loop_decreases } => {
560560
let inv = loop_invariants
561561
.clone()
562562
.map(|inv| inv.clone().and(Expr::bool_true()).to_irep(mm));
@@ -565,10 +565,12 @@ impl ToIrep for StmtBody {
565565
assigns.iter().map(|assign| assign.to_irep(mm)).collect(),
566566
)])
567567
});
568+
let decreases = loop_decreases.clone().map(|dec| dec.to_irep(mm));
568569
code_irep(IrepId::Goto, vec![])
569570
.with_named_sub(IrepId::Destination, Irep::just_string_id(dest.to_string()))
570571
.with_named_sub_option(IrepId::CSpecLoopInvariant, inv)
571572
.with_named_sub_option(IrepId::CSpecAssigns, assigns.clone())
573+
.with_named_sub_option(IrepId::CSpecDecreases, decreases)
572574
}
573575
StmtBody::Ifthenelse { i, t, e } => code_irep(
574576
IrepId::Ifthenelse,

docs/src/reference/attributes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,6 @@ There are numerous attributes for function and loop contracts. At present, these
248248
- Proof harness for contracts: `#[kani::proof_for_contract(name-of-function)]`
249249
- Verified stubbing: `#[kani::stub_verified]`
250250
- Function contract specification: `#[kani::requires]`, `#[kani::modifies]`, `#[kani::ensures]`, `#[kani::recursion]`
251-
- Loop contract specification: `#[kani::loop_invariant]`, `#[kani::loop_modifies]`.
251+
- Loop contract specification: `#[kani::loop_invariant]`, `#[kani::loop_modifies]`, `#[kani::loop_decreases]`.
252252

253253
See the documentation on [function contracts](./experimental/contracts.md) and [loop contracts](./experimental/loop-contracts.md) for details.

0 commit comments

Comments
 (0)