Skip to content

Commit 995d038

Browse files
author
Kasim Te
committed
Add NonZero verification harnesses
1 parent b0768ce commit 995d038

4 files changed

Lines changed: 698 additions & 4 deletions

File tree

doc/src/challenges/0012-nonzero.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,52 @@ code, all proofs must automatically ensure the absence of the following undefine
8585

8686
Note: All solutions to verification challenges need to satisfy the criteria established in the [challenge book](../general-rules.md)
8787
in addition to the ones listed above.
88+
89+
## Verification
90+
91+
This challenge is addressed with Kani contracts and proof harnesses in
92+
`library/core/src/num/nonzero.rs` (module `num::nonzero::verify`). All Part 1 and Part 2
93+
functions are verified across all twelve `NonZero` integer types.
94+
95+
### Approach
96+
97+
- **Inherent methods** carry `#[safety::requires]` / `#[safety::ensures]` contracts verified
98+
with `#[kani::proof_for_contract]`. Postconditions tie each result to the corresponding
99+
primitive operation (e.g. `result.get() == old(self).get().swap_bytes()`), going beyond mere
100+
absence of UB.
101+
- **`abs`** is a safe method that overflows-and-panics at `MIN`; following the rule that safe
102+
functions carry no real precondition, it keeps only an `#[ensures]` (no `#[requires]`). The
103+
non-`MIN` value is verified with `#[kani::proof_for_contract]`, and the `MIN`-input panic with a
104+
paired `#[kani::should_panic]` harness.
105+
- **`new_unchecked`** (Part 1) uses a byte-slice contract (generic over `ZeroablePrimitive`)
106+
asserting the stored value equals the input. **`new`** is verified by per-type proof harnesses
107+
asserting the full Part-1 property -- a `NonZero` is created if and only if the input is
108+
nonzero, and the value is preserved. A contract on `new` itself is deliberately avoided:
109+
because `new` is widely called, contract instrumentation is applied at every call site and
110+
regresses unrelated 128-bit harnesses under `-Z loop-contracts`; a per-type proof verifies the
111+
same property without that side effect.
112+
- **Operator-trait impls** (`BitOr` x3, `Neg`) are verified with `#[kani::proof]` plus a
113+
functional assertion, because the `#[safety]` contract proc-macro cannot annotate trait-impl
114+
methods (it parses `ItemFn`, not `ImplItemFn`).
115+
116+
### Bounded verification
117+
118+
Challenge 12 permits bounded verification. A few operations are intractable for CBMC at full
119+
64/128-bit width and are verified over representative bounded domains:
120+
121+
- **`checked_mul` / `saturating_mul`** -- small types full-range; larger types via narrow value
122+
windows (small values, plus windows near MIN/MAX to exercise overflow), mirroring the existing
123+
`unchecked_mul` interval harnesses.
124+
- **`isqrt`** -- small types full-range; larger types with the input bounded (the internal
125+
candidate-square is the cost driver).
126+
- **`checked_pow` / `saturating_pow`** -- Part 2 requires *safety*; the harnesses verify that the
127+
`unsafe { new_unchecked(...) }` is sound (the result is nonzero) over full-range base and
128+
exponent.
129+
130+
### One supporting change outside `nonzero.rs`
131+
132+
The primitive `checked_pow` loop invariant in `int_macros.rs` and `uint_macros.rs` was
133+
strengthened from `true` to `self == 0 || (acc != 0 && base != 0)` (signed) /
134+
`self == 0 || (acc > 0 && base > 0)` (unsigned). This is required so that, under
135+
`-Z loop-contracts`, the loop abstraction preserves the nonzero property that the `NonZero`
136+
wrappers rely on. It does not change runtime behavior.

library/core/src/num/int_macros.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1736,7 +1736,10 @@ macro_rules! int_impl {
17361736
let mut base = self;
17371737
let mut acc: Self = 1;
17381738

1739-
#[safety::loop_invariant(true)]
1739+
// `self == 0` covers the primitive's own harness (e.g. `0.checked_pow(n)`, where
1740+
// `acc` can reach 0); in the NonZero call path `self` is never 0, so it is dead
1741+
// but harmless there. The right disjunct (`acc != 0 && base != 0`) is the inductive core.
1742+
#[safety::loop_invariant(self == 0 || (acc != 0 && base != 0))]
17401743
loop {
17411744
if (exp & 1) == 1 {
17421745
acc = try_opt!(acc.checked_mul(base));

0 commit comments

Comments
 (0)