Skip to content

Commit c0c576a

Browse files
thanhnguyen-awstautschnigfeliperodri
authored
Add loop invariants and harnesses for DecimalSeq functions (#439)
This PR adds loop invariants and harnesses for DecimalSeq functions Require: for-loop support for Kani: model-checking/kani#4143 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: Michael Tautschnig <[email protected]> Co-authored-by: Michael Tautschnig <[email protected]> Co-authored-by: Felipe R. Monteiro <[email protected]>
1 parent 525c2f0 commit c0c576a

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

library/core/src/num/dec2flt/decimal_seq.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
//! algorithm can be found in "ParseNumberF64 by Simple Decimal Conversion",
1010
//! available online: <https://nigeltao.github.io/blog/2020/parse-number-f64-simple.html>.
1111
12+
use safety::ensures;
13+
14+
#[cfg(kani)]
15+
use crate::forall;
16+
#[cfg(kani)]
17+
use crate::kani;
1218
use crate::num::dec2flt::common::{ByteSlice, is_8digits};
1319

1420
/// A decimal floating-point number, represented as a sequence of decimal digits.
@@ -83,6 +89,7 @@ impl DecimalSeq {
8389
//
8490
// Trim is only called in `right_shift` and `left_shift`.
8591
debug_assert!(self.num_digits <= Self::MAX_DIGITS);
92+
#[cfg_attr(kani, kani::loop_invariant(self.num_digits <= Self::MAX_DIGITS))]
8693
while self.num_digits != 0 && self.digits[self.num_digits - 1] == 0 {
8794
self.num_digits -= 1;
8895
}
@@ -98,6 +105,7 @@ impl DecimalSeq {
98105
let dp = self.decimal_point as usize;
99106
let mut n = 0_u64;
100107

108+
#[cfg_attr(kani, kani::loop_invariant(n < 10u64.pow(kani::index as u32)))]
101109
for i in 0..dp {
102110
n *= 10;
103111
if i < self.num_digits {
@@ -130,6 +138,14 @@ impl DecimalSeq {
130138
let mut write_index = self.num_digits + num_new_digits;
131139
let mut n = 0_u64;
132140

141+
#[cfg_attr(kani, kani::loop_invariant(read_index <= Self::MAX_DIGITS &&
142+
write_index == read_index + num_new_digits &&
143+
n < 10u64 << (shift - 1) &&
144+
(n == 0 || (shift & 63) <= 3 || (shift & 63) >= 61 || write_index > 0) &&
145+
self.num_digits <= Self::MAX_DIGITS &&
146+
self.decimal_point <= self.num_digits as i32 &&
147+
forall!(|i in (0,DecimalSeq::MAX_DIGITS)| self.digits[i] <= 9)
148+
))]
133149
while read_index != 0 {
134150
read_index -= 1;
135151
write_index -= 1;
@@ -144,7 +160,14 @@ impl DecimalSeq {
144160
n = quotient;
145161
}
146162

163+
#[cfg_attr(kani, kani::loop_invariant(self.num_digits <= Self::MAX_DIGITS && self.decimal_point <= self.num_digits as i32))]
164+
// TODO: should add invariant along the lines of
165+
// (n == 0 || write_index > 0) && (prev(n) as usize + 9) / 10 <= prev(write_index))]
166+
// to avoid the below kani::assume
147167
while n > 0 {
168+
// true but hard to write proof with kani currently
169+
#[cfg(kani)]
170+
kani::assume(write_index > 0);
148171
write_index -= 1;
149172
let quotient = n / 10;
150173
let remainder = n - (10 * quotient);
@@ -171,13 +194,15 @@ impl DecimalSeq {
171194
let mut read_index = 0;
172195
let mut write_index = 0;
173196
let mut n = 0_u64;
197+
#[cfg_attr(kani, kani::loop_invariant( n == 0 || (read_index > 0 && read_index <= self.num_digits + 64 - n.leading_zeros() as usize)))]
174198
while (n >> shift) == 0 {
175199
if read_index < self.num_digits {
176200
n = (10 * n) + self.digits[read_index] as u64;
177201
read_index += 1;
178202
} else if n == 0 {
179203
return;
180204
} else {
205+
#[cfg_attr(kani, kani::loop_invariant(n > 0 && read_index <= self.num_digits + 64 - n.leading_zeros() as usize && read_index > 0))]
181206
while (n >> shift) == 0 {
182207
n *= 10;
183208
read_index += 1;
@@ -194,13 +219,18 @@ impl DecimalSeq {
194219
return;
195220
}
196221
let mask = (1_u64 << shift) - 1;
222+
#[cfg_attr(kani, kani::loop_invariant(self.num_digits <= Self::MAX_DIGITS &&
223+
write_index < read_index &&
224+
write_index < Self::MAX_DIGITS - self.num_digits.saturating_sub(read_index)
225+
))]
197226
while read_index < self.num_digits {
198227
let new_digit = (n >> shift) as u8;
199228
n = (10 * (n & mask)) + self.digits[read_index] as u64;
200229
read_index += 1;
201230
self.digits[write_index] = new_digit;
202231
write_index += 1;
203232
}
233+
#[cfg_attr(kani, kani::loop_invariant(write_index <= Self::MAX_DIGITS))]
204234
while n > 0 {
205235
let new_digit = (n >> shift) as u8;
206236
n = 10 * (n & mask);
@@ -297,6 +327,7 @@ pub fn parse_decimal_seq(mut s: &[u8]) -> DecimalSeq {
297327
d
298328
}
299329

330+
#[safety::ensures(|result| (shift & 63) <= 3 || (shift & 63) >= 61 || *result > 0)]
300331
fn number_of_digits_decimal_left_shift(d: &DecimalSeq, mut shift: usize) -> usize {
301332
#[rustfmt::skip]
302333
const TABLE: [u16; 65] = [
@@ -363,6 +394,7 @@ fn number_of_digits_decimal_left_shift(d: &DecimalSeq, mut shift: usize) -> usiz
363394
let pow5_b = (0x7FF & x_b) as usize;
364395
let pow5 = &TABLE_POW5[pow5_a..];
365396

397+
#[cfg_attr(kani, kani::loop_invariant(num_new_digits > 1 || shift <= 3 || shift >= 61))]
366398
for (i, &p5) in pow5.iter().enumerate().take(pow5_b - pow5_a) {
367399
if i >= d.num_digits {
368400
return num_new_digits - 1;
@@ -377,3 +409,55 @@ fn number_of_digits_decimal_left_shift(d: &DecimalSeq, mut shift: usize) -> usiz
377409

378410
num_new_digits
379411
}
412+
413+
#[cfg(kani)]
414+
#[unstable(feature = "kani", issue = "none")]
415+
pub mod decimal_seq_verify {
416+
use super::*;
417+
418+
impl kani::Arbitrary for DecimalSeq {
419+
fn any() -> DecimalSeq {
420+
let mut ret = DecimalSeq {
421+
num_digits: kani::any_where(|x| *x <= DecimalSeq::MAX_DIGITS),
422+
decimal_point: kani::any_where(|x| *x >= 0),
423+
truncated: kani::any(),
424+
digits: kani::any(),
425+
};
426+
kani::assume(ret.decimal_point <= ret.num_digits as i32);
427+
kani::assume(forall!(|i in (0,DecimalSeq::MAX_DIGITS)| ret.digits[i] <= 9));
428+
ret
429+
}
430+
}
431+
432+
#[kani::proof]
433+
fn check_round() {
434+
let mut a: DecimalSeq = kani::any();
435+
a.round();
436+
}
437+
438+
#[kani::proof]
439+
fn check_number_of_digits_decimal_left_shift() {
440+
let mut a: DecimalSeq = kani::any();
441+
let shift: usize = kani::any_where(|x| *x > 0 && *x <= 60);
442+
let n = number_of_digits_decimal_left_shift(&a, shift);
443+
// 19 is the greatest number x such that 10u64^x does not overflow
444+
// It is also TABLE.max << 11
445+
assert!(n <= 19);
446+
assert!(n == 19 || 1u64 << shift < 10u64.pow(n as u32 + 1))
447+
}
448+
449+
#[kani::proof]
450+
fn check_right_shift() {
451+
let mut a: DecimalSeq = kani::any();
452+
//This function is called in parse_long_mantissa function (slow.rs), in which the maximum of shift is 60
453+
let shift: usize = kani::any_where(|x| *x > 0 && *x <= 60);
454+
a.right_shift(shift);
455+
}
456+
457+
#[kani::proof]
458+
fn check_left_shift() {
459+
let mut a: DecimalSeq = kani::any();
460+
let shift: usize = kani::any_where(|x| *x > 0 && *x <= 60);
461+
a.left_shift(shift);
462+
}
463+
}

scripts/run-kani.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ REPO_URL=${KANI_REPO_URL:-$DEFAULT_REPO_URL}
8484
BRANCH_NAME=${KANI_BRANCH_NAME:-$DEFAULT_BRANCH_NAME}
8585

8686
# Unstable arguments to pass to Kani
87-
unstable_args="-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z stubbing"
87+
unstable_args="-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing"
8888

8989
# Variables used for parallel harness verification
9090
# When we say "parallel," we mean two dimensions of parallelization:

0 commit comments

Comments
 (0)