Add #[kani::loop_decreases] for proving loop termination#4564
Merged
feliperodri merged 9 commits intomodel-checking:mainfrom Apr 22, 2026
Merged
Add #[kani::loop_decreases] for proving loop termination#4564feliperodri merged 9 commits intomodel-checking:mainfrom
#[kani::loop_decreases] for proving loop termination#4564feliperodri merged 9 commits intomodel-checking:mainfrom
Conversation
adpaco-aws
approved these changes
Apr 22, 2026
Add the foundational support for decreases clauses in the CBMC bindings layer. This is the lowest layer of the implementation, providing the data structures and serialization needed to emit `#spec_decreases` annotations on goto-program loop back-edges. Changes: - Add `CSpecDecreases` variant to `IrepId` enum, mapped to CBMC's `#spec_decreases` irep identifier. - Add `loop_decreases: Option<Expr>` field to the `Goto` variant of `StmtBody`, alongside the existing `loop_invariants` and `loop_modifies` fields. - Add `with_loop_decreases()` builder method on `Stmt`, following the same pattern as `with_loop_contracts()` and `with_loop_modifies()`. - Serialize `loop_decreases` as a `CSpecDecreases` named sub in the irep output, so CBMC's `goto-instrument` can extract and instrument the termination check. CBMC's goto-instrument handles the actual verification by: 1. Recording the measure at loop body entry (old_measure) 2. Recording the measure at loop body exit (new_measure) 3. Asserting new_measure < old_measure (strict decrease) Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Add the user-facing `#[kani::loop_decreases(expr1, expr2, ...)]` attribute for specifying decreases clauses on loops. This follows the same pattern as the existing `#[kani::loop_modifies]` attribute. The macro creates a local binding `let kani_loop_decreases = (expr1, ...);` before the loop statement. The kani-compiler detects this variable name during codegen and attaches the expression to the loop's goto statement. Changes: - Register `loop_decreases` as a `#[proc_macro_attribute]` in lib.rs. - Re-export from the sysroot module for use during sysroot compilation. - Add no-op stub in the regular (non-sysroot) module so the attribute is accepted by rustc/miri/IDE tooling without Kani. - Implement `loop_decreases()` in sysroot/loop_contracts/mod.rs with developer documentation explaining the end-to-end flow. Supports multi-dimensional decreases via comma-separated expressions: `#[kani::loop_decreases(n - i, n - j)]` CBMC compares these using lexicographic ordering. Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Connect the proc macro output to the CBMC bindings by detecting the `kani_loop_decreases` variable during MIR-to-GOTO codegen and attaching it to the loop's goto statement. Changes: - goto_ctx.rs: Add `current_loop_decreases: Option<Expr>` field to `GotocCtx` for tracking the decreases expression across statements. - statement.rs: Detect assignments to variables named `kani_loop_decreases` (created by the proc macro), codegen the RHS expression, store it in `current_loop_decreases`, and emit a skip statement (same pattern as `kani_loop_modifies`). - hooks.rs: In `LoopInvariantRegister::handle()`, after attaching loop_modifies to the goto statement, also attach the decreases clause via `with_loop_decreases()` and clear the stored expression. The end-to-end flow is: proc macro creates `let kani_loop_decreases = (expr);` -> statement.rs detects it and stores the codegen'd expression -> hooks.rs attaches it to the goto irep as #spec_decreases -> CBMC's goto-instrument instruments the termination check Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Add 12 test cases covering the positive (should-pass) scenarios for decreases clauses. Each test has a .rs file and a .expected file that checks for VERIFICATION:- SUCCESSFUL. Test cases and their inspiration: - simple_while_loop_decreases: Basic 1D decreases(x) [Verus basic_while] - multi_dim_decreases: Lexicographic decreases(n-i, n-j) [CBMC docs] - decreases_expr: Arithmetic expression decreases(n-i) [CBMC docs] - nested_loops_decreases: Each loop with own decreases [Verus loop_decreases2] - decreases_with_modifies: Combined with loop_modifies - loop_loop_decreases: Decreases on `loop` (not while) [Verus loop_decreases1] - decreases_struct_field: Struct field projection decreases(c.val) - decreases_binary_search: Binary search decreases(hi-lo) [CBMC docs] - decreases_fib: Fibonacci-style loop [Prusti fib.rs] - decreases_loop_max: Loop max function [Prusti loop_max.rs] - decreases_with_function_contract: Combined with requires/ensures - decreases_with_old: Combined with on_entry() values [Verus] Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Add 4 negative test cases that verify Kani correctly rejects loops with invalid decreases clauses. Each test expects CBMC to report a FAILURE on the `loop_decreases` property class with description 'Check variant decreases after step for loop', and overall VERIFICATION:- FAILED. Test cases: - decreases_fail_non_decreasing: Loop body does not modify the measure variable at all (x stays the same). Detects infinite loops where the variant is stale. - decreases_fail_wrong_measure: Loop body increases the measure instead of decreasing it (x = x + 1). Detects when the user picked the wrong direction for the measure. - decreases_fail_constant: Decreases expression is a literal constant (42u8). A constant never strictly decreases. - decreases_fail_nested_inner: Outer loop correctly decreases, but inner loop body doesn't modify its measure. Detects per-loop termination failures in nested scenarios. Inspired by Verus loop_decreases2. Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Add user documentation for the `#[kani::loop_decreases]` feature and
improve the overall loop contracts documentation with formal verification
context and practical guidance.
New sections in loop-contracts.md:
Partial correctness vs. total correctness:
Frames loop_invariant as partial correctness and loop_decreases as the
upgrade to total correctness. Introduces the four-step methodology
(Establishment, Preservation, Postcondition, Termination) from Floyd's
method, mapping each step to the corresponding Kani attribute.
Decreases clauses (Termination proofs):
- Why termination matters: concrete example of unsound results without
termination proof (proving unreachable assertions).
- Background: Floyd's 1967 method for termination via ranking functions.
- Syntax: single and multi-dimensional (lexicographic) forms.
- Basic and multi-dimensional examples with step-by-step explanation.
- Semantics: how CBMC instruments the check (old_measure vs new_measure).
- Interaction with loop invariants: why they are interdependent.
- Limitations comparison with Dafny and Verus: integer-only measures,
no auto-inference, no recursive function support, no decreases *
escape hatch, no side-effect checking, strict decrease only.
Worked example — Binary search:
Complete walkthrough of all four correctness steps on binary search,
showing how the termination argument depends on the invariant.
Practical guidance:
- How to choose a loop invariant (start from postcondition, include
bounds, include variable relationships).
- Common mistakes: too-weak invariant, too-strong invariant, wrong
decreases clause — with symptoms the user will see in Kani output.
- Tips for decreases clauses: natural measures for common loop patterns,
when to use multi-dimensional decreases.
Also updates:
- attributes.md: Add loop_decreases to the contract attributes list.
- Limitations section: Updated to reference decreases clause limitations.
Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
…ng tests
Ran full test suite locally with `cargo build-dev` + `cargo run -p compiletest`
to reproduce and fix CI failures. All 451 expected tests now pass.
Root causes identified:
- CBMC silently ignores decreases on struct field projections and
multi-dimensional tuple expressions (the irep is emitted but CBMC's
goto-instrument doesn't process complex types in #spec_decreases).
- Negative tests with empty loop bodies don't trigger the decreases
check because CBMC's havoc + assume can find a path that exits the
loop immediately without executing the body.
- The fib test had a too-weak invariant for wrapping arithmetic.
- Nested loops had assigns clause conflicts with the inner loop.
Changes:
- Remove 7 tests that expose real CBMC limitations (to be re-added
when CBMC support improves): decreases_fib, decreases_struct_field,
multi_dim_decreases, nested_loops_decreases, decreases_with_modifies,
decreases_fail_constant, decreases_fail_nested_inner.
- Fix decreases_fail_non_decreasing: use u16 and actually increase x
so the decreases check fires reliably.
- Fix decreases_fail_wrong_measure: use u16 to avoid overflow masking
the decreases failure.
- Fix loop_loop_decreases: rewrite `loop{break}` as `while` to avoid
internal unreachable in loop contract transformation.
- Fix decreases_expr: simplify to use direct variable measure.
- Fix decreases_loop_max: use explicit countdown variable.
- Simplify .expected files to match on Status: FAILURE + VERIFICATION.
Remaining passing tests (verified locally):
simple_while_loop_decreases, decreases_expr, loop_loop_decreases,
decreases_loop_max, decreases_binary_search,
decreases_with_function_contract, decreases_with_old,
decreases_fail_non_decreasing, decreases_fail_wrong_measure
Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Add 4 fixme tests documenting known bugs where decreases clauses do not work correctly. These tests are automatically skipped by the expected test suite (marked as 'ignored, fixme test') so they don't fail CI, but they document the expected failure behavior for when the underlying issues are fixed. Known limitations (all tracked in model-checking#3168): - fixme_decreases_struct_field: CBMC does not process struct field projections in #spec_decreases — the loop_decreases check always fails even when the measure genuinely decreases. - fixme_multi_dim_decreases: CBMC does not perform lexicographic comparison on tuple expressions passed through Kani's irep encoding. - fixme_decreases_with_modifies: Combining loop_decreases with loop_modifies causes assigns clause conflicts. - fixme_nested_loops_decreases: Nested loops with decreases on both inner and outer loops cause assigns clause conflicts. Also updates the Limitations section in loop-contracts.md to document these known bugs with a reference to the tracking issue. Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
01cf232 to
ce4d41a
Compare
… document limitations
Clear current_loop_decreases in the non-loop-contracts codegen path
(hooks.rs) to prevent stale state when the flag is not enabled.
Rename decreases_fail_non_decreasing to decreases_fail_stale_measure
and rewrite to test a genuinely stale measure: the loop variable x
decreases but the measure variable y (set to x's initial value) never
changes. This is distinct from decreases_fail_wrong_measure which
tests a measure that increases.
Add prominent warning in the multi-dimensional decreases documentation
section noting that this feature is not yet fully supported due to
CBMC limitations.
Add comment in loop_loop_decreases.rs documenting that the original
loop{break} form triggers an internal unreachable in the loop contract
transformation, with a link to the tracking issue model-checking#3168.
Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
ce4d41a to
6ecdf75
Compare
thanhnguyen-aws
approved these changes
Apr 22, 2026
Merged
via the queue into
model-checking:main
with commit Apr 22, 2026
78eb465
33 of 34 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_decreasesannotation viagoto-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 alet kani_loop_decreases = (expr,);binding before the loopwhile,loop,for)loop_decreases(a, b)) with a compile error — lexicographic ordering is not yet supported (#[kani::loop_decreases]does not support struct field projections or arithmetic expressions #4585)CBMC bindings (
cprover_bindings)loop_decreases: Option<Expr>field inStmtBody::Gotowith_loop_decreases()builder, mirroringwith_loop_invariants()andwith_loop_modifies()CSpecDecreasesirep serializationCompiler codegen (
kani-compiler)kani_loop_decreasesassignments in MIR and stores the expression#[kani::loop_decreases]is used without-Z loop-contractsExample
Testing
8 passing expected tests:
simple_while_loop_decreases— basic countdownloop_loop_decreases—loop { ... break }with decreasesdecreases_expr— expression-based measuredecreases_binary_search— binary search terminationdecreases_loop_max— function with loop + decreasesdecreases_with_function_contract— combined with#[kani::requires]/#[kani::ensures]decreases_with_old— usingon_entry()in decreasesdecreases_no_flag— warning emitted without-Z loop-contracts2 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 (#[kani::loop_decreases]combined with#[kani::loop_modifies]causes verification failure #4584)fixme_multi_dim_decreases— multi-dimensional measures (#[kani::loop_decreases]does not support struct field projections or arithmetic expressions #4585)fixme_decreases_with_modifies— combined withloop_modifies(Support multi-dimensional (lexicographic) decreases clauses in#[kani::loop_decreases]#4586)fixme_nested_loops_decreases— nested loops with assigns conflict (Support multi-dimensional (lexicographic) decreases clauses in#[kani::loop_decreases]#4586)Known Limitations
The following limitations are tracked as separate issues:
19 - p) fail due to Rust's checked arithmetic producingStatementExpressionnodes. Workaround: usewrapping_sub. (#[kani::loop_decreases]combined with#[kani::loop_modifies]causes verification failure #4584)#[kani::loop_decreases(a, b)]) is rejected at compile time; lexicographic ordering is not yet supported. (#[kani::loop_decreases]does not support struct field projections or arithmetic expressions #4585)loop_decreaseswithloop_modifiescauses spurious verification failures. Workaround: useloop_decreases+loop_invariantwithoutloop_modifies. (Support multi-dimensional (lexicographic) decreases clauses in#[kani::loop_decreases]#4586)By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.