Repository navigation
Conversation
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It changes soundness-sensitive compiler transformations and CBMC annotation generation across nested control-flow scenarios.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes loop-decreases checking so variable, field, and dereference measures are correctly evaluated and attached to their intended loops.
Changes:
- Preserves decreases bindings through MIR optimization and serializes measures correctly for CBMC.
- Associates clauses with loop heads per function, including nested loops and quantifiers.
- Expands regression coverage and documents unsupported precomputed measures.
| File | Description |
|---|---|
tests/expected/loop-contract/simple_while_loop_decreases.expected |
Requires a successful decreases check. |
tests/expected/loop-contract/loop_loop_decreases.expected |
Requires a successful loop decreases check. |
tests/expected/loop-contract/fixme_decreases_struct_field.rs |
Removes the obsolete failing field test. |
tests/expected/loop-contract/fixme_decreases_struct_field.expected |
Removes obsolete expected failure output. |
tests/expected/loop-contract/decreases_with_old.expected |
Validates decreases checking with old values. |
tests/expected/loop-contract/decreases_with_function_contract.expected |
Validates decreases with function contracts. |
tests/expected/loop-contract/decreases_variable.rs |
Tests variable, for, prev, and dereference measures. |
tests/expected/loop-contract/decreases_variable.expected |
Requires successful checks for those measures. |
tests/expected/loop-contract/decreases_struct_field.rs |
Tests correct and incorrect field measures. |
tests/expected/loop-contract/decreases_struct_field.expected |
Validates field-measure results. |
tests/expected/loop-contract/decreases_no_invariant.rs |
Tests ignored unattached clauses. |
tests/expected/loop-contract/decreases_no_invariant.expected |
Requires the warning and successful harnesses. |
tests/expected/loop-contract/decreases_nested_loops.rs |
Exercises nested-loop clause association. |
tests/expected/loop-contract/decreases_nested_loops.expected |
Validates each nested loop’s result. |
tests/expected/loop-contract/decreases_nested_loops_quantifier.rs |
Tests per-function state during quantifier codegen. |
tests/expected/loop-contract/decreases_nested_loops_quantifier.expected |
Requires the outer-loop failure. |
tests/expected/loop-contract/decreases_mir_opt_level.rs |
Tests binding preservation under MIR optimization. |
tests/expected/loop-contract/decreases_mir_opt_level.expected |
Validates optimized-MIR outcomes. |
tests/expected/loop-contract/decreases_loop_max.expected |
Requires the generated decreases property. |
tests/expected/loop-contract/decreases_fail_wrong_measure.rs |
Isolates increasing variable measures. |
tests/expected/loop-contract/decreases_fail_wrong_measure.expected |
Requires both decreases failures. |
tests/expected/loop-contract/decreases_fail_unsupported_measure.rs |
Tests unsupported precomputed measures. |
tests/expected/loop-contract/decreases_fail_unsupported_measure.expected |
Requires conservative failures. |
tests/expected/loop-contract/decreases_fail_stale_measure.rs |
Isolates an unchanged measure failure. |
tests/expected/loop-contract/decreases_fail_stale_measure.expected |
Requires the specific decreases failure. |
tests/expected/loop-contract/decreases_fail_constant_measure.rs |
Tests literal and constant measures. |
tests/expected/loop-contract/decreases_fail_constant_measure.expected |
Requires constant-measure failures. |
tests/expected/loop-contract/decreases_expr.expected |
Requires the decreases property. |
library/kani_macros/src/sysroot/loop_contracts/mod.rs |
Keeps generated bindings in optimized MIR. |
kani-compiler/src/kani_middle/transform/loop_contracts.rs |
Prevents movement of decreases bindings. |
kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs |
Retrieves clauses by loop head. |
kani-compiler/src/codegen_cprover_gotoc/context/mod.rs |
Exports the decreases state type. |
kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs |
Removes global decreases state. |
kani-compiler/src/codegen_cprover_gotoc/context/current_fn.rs |
Adds per-function clause tracking. |
kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs |
Records live or conservative measures. |
kani-compiler/src/codegen_cprover_gotoc/codegen/function.rs |
Discovers loop heads and reports unattached clauses. |
kani-compiler/src/codegen_cprover_gotoc/codegen/block.rs |
Associates pending clauses with loop heads. |
docs/src/reference/experimental/loop-contracts.md |
Corrects behavior and limitations documentation. |
cprover_bindings/src/irep/to_irep.rs |
Encodes the measure as one annotation operand. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
feliperodri
left a comment
There was a problem hiding this comment.
Thanks for tracking this down to the irep encoding. The fix itself is right; I'm requesting changes for one autoharness issue, below.
On main, a #[kani::loop_decreases] clause whose measure is a variable, a call or a constant produces no decreases check, so a measure that increases, stays equal or wraps still gets VERIFICATION:- SUCCESSFUL. That covers most of the decreases tests and every shape I tried with loop, while let, for, prev, closures and nested loops. With this PR, measures that are variables, fields or dereferences of variables are checked at each iteration. Everything else (checked arithmetic, casts, indexing, calls, if, constants) falls back to the binding, so the check fails, and I agree with failing there rather than passing.
What I ran: the new and updated .expected files against a main build (all 16 fail there and pass here), the loop-contract expected tests and the ui suite on the PR merged with current main, the three lint gates, and about 60 probe harnesses comparing main and the PR (variables, fields, derefs, calls, casts, tuples, every loop form, prev/on_entry, nested and sequential loops, closures, -Zmir-opt-level=0..4). Each wrong measure fails on the decreases check alone. I did not rerun the full regression suites (CI is green on the head) or test on macOS.
Before merge: autoharness treats kani_keep_loop_decreases_binding as a user function and generates a harness for it (inline, with a fn_marker fix I tested). The rest is non-blocking: a compile-time warning when the measure falls back to the binding, a docs bullet that looks stale now, and a couple of test cases.
Two things outside this PR's scope: a for loop with #[kani::loop_decreases] written after #[kani::loop_invariant] still drops the clause without a warning (#4929, which #4947 handles), and kani::index isn't substituted inside a decreases clause. This PR and #4947 will conflict in hooks.rs and goto_ctx.rs, so whichever lands second needs a rebase.
CBMC takes the operands of the `#spec_decreases` irep as the components of the measure, as the C front end encodes `__CPROVER_decreases(e1, ..., en)`. Kani emitted the measure itself, so a measure without operands (a variable, a field, a constant) produced no decreases check, and for other measures CBMC used their operands as the measure (e.g. the operand of a cast). Emit the measure as the only operand. This exposes other issues, which this change also fixes: - If the measure is a variable that is assigned only once, copy propagation merges the `kani_loop_decreases` binding into that variable. Codegen then took the definition of the variable for the binding: it dropped the assignment and used its right-hand side as the measure. If the measure is a constant, the binding is removed. The macro now passes a reference to the binding to a function, so that MIR optimizations (also at `-Zmir-opt-level=2`) keep its assignment, and codegen recognizes the binding by its exact name. - Codegen takes the measure from the right-hand side of the assignment to the binding, which CBMC evaluates before and after each iteration. That is only the measure if the right-hand side reads user variables and arguments, and not temporaries that hold values from before the loop (e.g. the index in `a[i]`), and if it is the only assignment to the binding (not an `if` expression or a call). Otherwise, the measure is now the value of the binding, so that the check fails instead of checking part of the measure, or nothing. - Codegen attached a clause to the next loop latch that it generated, so the clause of an outer loop was checked on an inner loop, and the clause of a loop without a loop invariant on a later loop, possibly of another function. A clause is now attached to the first loop head generated after its binding, and the loop latch takes the clause of its head. This state is kept per function, since codegen may generate a quantifier closure while it generates a function. The loop contract pass no longer copies the binding to the head of an enclosing loop. Kani warns about a clause that is not attached to a loop with a loop invariant. Resolves model-checking#4942
…binding helper from autoharness - Mark `kani_keep_loop_decreases_binding` with a `kanitool::fn_marker`, so that autoharness does not generate a harness for it. Add a script-based test that runs autoharness on a function with a decreases clause and checks that only that function is verified. - When the measure of a decreases clause is computed once, before the loop (checked arithmetic, casts, indexing, calls, `if` expressions, constants), warn at the clause that the decreases check will fail even if the measure decreases, and point to model-checking#4585. The warning is emitted once per clause. - Drop the known-bug entry about combining `loop_decreases` with `loop_modifies`: the fixme test failed because of its `5 - i` measure. Turn it into a regular test with a variable measure. - Add test cases: a `while let` loop with the decreases clause after the loop invariant, with a correct and an increasing measure, a measure that wraps around, and a correct `4 - i` measure, which fails with the warning.
882a3a4 to
3041eb9
Compare
Description
A
#[kani::loop_decreases]clause whose measure is a variable produced noCheck variant decreases after stepproperty, so a wrong measure verified (#4942).CBMC reads the
#spec_decreasesannotation of a loop latch as a list of measures:dfcc_cfg_info.cpptakesget_loop_decreases(loop.latch, ..).operands(), since the C front end stores__CPROVER_decreases(e1, ..., en)as an irep whose operands are the measures. Kani attached the measure expression itself. A measure without operands (a variable, a field, a constant) therefore gave an empty list and no check, and for any other measure CBMC used its operands as the measure. For example,m as u32andhi - loare computed through MIR temporaries before the loop, so CBMC compared a temporary that the loop never changes, and the check could only fail. This has been the case since #4564 (CBMC 6.8.0 also reads the operands).This PR emits the measure as the only operand of the annotation. Once the check is generated, other problems show up, which this PR also fixes:
yindecreases_fail_stale_measure.rs, or a parameter), copy propagation merges thekani_loop_decreasesbinding into that variable, so the binding has no assignment of its own and its debug name is attached to the variable. Codegen then took the definition of the variable (y = x) for the binding: it dropped the assignment and usedxas the measure. If the measure is a constant, the binding is removed. The macro now passes a reference to the binding to a local#[inline(never)]function, which keeps MIR optimizations (also with-Zmir-opt-level=2) from removing its assignment, and codegen recognizes the binding by its exact name instead of a substring of the variable name.a[i]), and if it is the only assignment to the binding (not anifexpression or a call such asx.wrapping_sub(i)). Otherwise the measure is now the value of the binding, which does not change in the loop, so the check fails. Before, such a clause was checked on a partly stale measure or on one branch only, or (for a call) not at all.CurrentFnCtx, since codegen may generate the closure of a quantifier while it generates a function. The loop contract pass no longer copies the binding of an inner loop to the head of the outer loop, which it did for all user variables declared in a loop body.-Z loop-contracts; a loop without a loop invariant is unwound, not abstracted. The reference said that the invarianttrueis used in that case, which was not what happened, so this PR updates it, and replaces the struct-field entry of the known bugs (struct fields now work) with one for measures computed before the loop.Measures that Kani computes before the loop (arithmetic with overflow checks, casts, calls) are still evaluated only once, so a correct measure of that kind still fails the check (#4585). With this PR it fails instead of silently passing in all cases. Measures that are variables, fields or dereferences of variables are checked at each iteration.
Resolves #4942
Testing
decreases_fail_wrong_measure.rsanddecreases_fail_stale_measure.rsfailed for other reasons (an overflow and a loop invariant that does not hold), and their.expectedfiles only requiredVERIFICATION:- FAILED. They are now fixed so that the decreases check is the only failure, and their.expectedfiles require that failure.decreases_fail_wrong_measure.rsuses the reproducer from#[kani::loop_decreases]with a plain variable measure generates no decreases check #4942, plus aforloop..expectedfiles of the tests that verify with a decreases clause (simple_while_loop_decreases,decreases_expr,decreases_loop_max,decreases_with_old,decreases_with_function_contract,loop_loop_decreases) now require the decreases check with status SUCCESS. Onmain, none of them has a decreases check.tests/expected/loop-contract/:decreases_variable.rs: a variable in awhileloop, aforloop and a loop whose invariant usesprev, and a dereference.decreases_struct_field.rs: renamed fromfixme_decreases_struct_field.rs, which now verifies, plus a wrong field.decreases_fail_constant_measure.rs: a literal and aconst.decreases_fail_unsupported_measure.rs: wrongif, method call anda[i]measures.decreases_nested_loops.rs: decreases clauses on both loops, on one of them, and a wrong clause on each.decreases_nested_loops_quantifier.rs: a wrong clause on an outer loop, in a harness with a quantifier.decreases_mir_opt_level.rs: a variable, an unmodified variable and an unmodified parameter, with-Zmir-opt-level=2.decreases_no_invariant.rs: the warning for a loop without a loop invariant and for a loop whose body always exits, and that their clauses are not checked on the next loop..expectedfiles fail withmain, and pass with this PR.fixme_decreases_with_modifies.rs(5 - i) still fails the decreases check (#[kani::loop_decreases]does not support struct field projections or arithmetic expressions #4585),fixme_multi_dim_decreases.rsis still rejected, andfixme_nested_loops_decreases.rsnow checks both decreases clauses with status SUCCESS but still failsCheck assigns clause inclusion, as onmain.decreases_binary_search.rsis unchanged. Its decreases check succeeds, but only because its harness finds the target in the first iteration, so the loop latch is not reached in the step case.cargo fmt --check,scripts/kani-fmt.sh --check,cargo clippy --workspace --tests -- -D warningsandRUSTFLAGS=--cfg=kani_sysroot cargo clippy --workspace -- -D warningspass, and so do theexpected(506 passed, 14 ignored),ui(152 passed),kani(620 passed, 23 ignored) andscript-based-pre(89 passed) suites.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.