Skip to content

Commit df6aea8

Browse files
committed
Introduce the first section of the RMC tutorial (rust-lang#474)
* Introduce the first section of the RMC tutorial * add license boilerplate, correct typos * fix formatting
1 parent 8441e47 commit df6aea8

File tree

6 files changed

+347
-1
lines changed

6 files changed

+347
-1
lines changed

rmc-docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
- [Debugging failures]()
99

1010
- [RMC tutorial](./rmc-tutorial.md)
11+
- [First steps with RMC](./tutorial-first-steps.md)
1112

1213
- [RMC developer documentation]()

rmc-docs/src/rmc-tutorial.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ It will also teach you the basics of "debugging" proof harnesses, which mainly c
1717

1818
1. [Begin with RMC installation.](./install-guide.md) This will take through to running `rmc` on your first Rust program.
1919
2. Consider reading our [tool comparison](./tool-comparison.md) to understand what RMC is.
20-
3. Coming soon: the tutorial.
20+
3. [Work through the tutorial.](./tutorial-first-steps.md)
2121
4. Consider returning to the [tool comparison](./tool-comparison.md) after trying the tutorial to see if any of the abstract ideas have become more concrete.

rmc-docs/src/tutorial-first-steps.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# First steps with RMC
2+
3+
> This tutorial expects you to have followed the RMC [installation instructions](./install-guide.md) first.
4+
5+
RMC is unlike the testing tools you may already be familiar with.
6+
Much of testing is concerned with thinking of new corner cases that need to be covered.
7+
With RMC, all the corner cases are covered from the start, and the new concern is narrowing down the scope to something manageable for the checker.
8+
9+
Consider this first program (which can be found under [`rmc-docs/src/tutorial/rmc-first-steps`](https://github.com/model-checking/rmc/tree/main/rmc-docs/src/tutorial/rmc-first-steps/)):
10+
11+
```rust
12+
{{#include tutorial/rmc-first-steps/src/lib.rs:code}}
13+
```
14+
15+
Think about the test harness you would need to write to test this function.
16+
You would need figure out a whole set of arguments to call the function with that would exercise each branch.
17+
You would need to keep that test harness up-to-date with the code, in case some of the branches change.
18+
And if this function was more complicated—for example, if some of the branches depended on global state—the test harness would be even more onerous to write.
19+
20+
We can try to property test a function like this, but if we're naive about it (and consider all possible `u32` inputs), then it's unlikely we'll ever find the bug.
21+
22+
```rust
23+
{{#include tutorial/rmc-first-steps/src/lib.rs:proptest}}
24+
```
25+
26+
```
27+
# cargo test
28+
[...]
29+
test tests::doesnt_crash ... ok
30+
```
31+
32+
There's only 1 in 4 billion inputs that fail, so it's vanishingly unlikely the property test will find it, even with a million samples.
33+
34+
With RMC, however:
35+
36+
```rust
37+
{{#include tutorial/rmc-first-steps/src/lib.rs:rmc}}
38+
```
39+
40+
```
41+
# cargo rmc
42+
[...]
43+
Runtime decision procedure: 0.00116886s
44+
45+
** Results:
46+
./src/lib.rs function estimate_size
47+
[estimate_size.assertion.1] line 9 Oh no, a failing corner case!: FAILURE
48+
49+
** 1 of 1 failed (2 iterations)
50+
VERIFICATION FAILED
51+
```
52+
53+
RMC has immediately found a failure.
54+
Notably, we haven't had to write explicit assertions in our "proof harness": by default, RMC will find a host of erroneous conditions which include a reachable call to `panic` or a failing `assert`.
55+
56+
By default, RMC only reports failures, not how the failure happened.
57+
This is because, in its full generality, understanding how a failure happened requires exploring a full (potentially large) execution trace.
58+
Here, we've just got some nondeterministic inputs up front, but that's something of a special case that has a "simpler" explanation (just the choice of nondeterministic input).
59+
60+
To see traces, run:
61+
62+
```
63+
rmc --visualize src/lib.rs
64+
open report/html/index.html
65+
```
66+
67+
The first command runs RMC and generates the html-based report in `report/`.
68+
The second command opens that report in your default browser (on mac, on linux desktops try `xdg-open`).
69+
From this report, we can find the trace of the failure and filter through it to find the relevant line (at present time, an unfortunate amount of generated code is present in the trace):
70+
71+
```
72+
let x: u32 = __nondet();
73+
x = 1023u
74+
```
75+
76+
Here we're seeing the line of code and the value assigned in this particular trace.
77+
Like property testing, this is just one example of a failure.
78+
To find more, we'd presumably fix this issue and then re-run RMC.
79+
80+
### Exercise: Try other failures
81+
82+
We put an explicit panic in this function, but it's not the only kind of failure RMC will find.
83+
Try a few other types of errors.
84+
85+
For example, instead of panicking we could try explicitly dereferencing a null pointer:
86+
87+
```rust
88+
unsafe { return *(0 as *const u32) };
89+
```
90+
91+
Notably, however, the Rust compiler emits a warning here:
92+
93+
```
94+
warning: dereferencing a null pointer
95+
--> src/lib.rs:10:29
96+
|
97+
10 | unsafe { return *(0 as *const u32) };
98+
| ^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed
99+
|
100+
= note: `#[warn(deref_nullptr)]` on by default
101+
```
102+
103+
Still, it is just a warning, and we can run the code without test failures just as before.
104+
But RMC still catches the issue:
105+
106+
```
107+
** Results:
108+
./src/lib.rs function estimate_size
109+
[estimate_size.pointer_dereference.1] line 10 dereference failure: pointer NULL in *var_10: FAILURE
110+
111+
** 1 of 1 failed (2 iterations)
112+
VERIFICATION FAILED
113+
```
114+
115+
**Can you find an example where the Rust compiler will not complain, and RMC will?**
116+
117+
<details>
118+
<summary>Click to show one possible answer</summary>
119+
120+
```
121+
return 1 << x;
122+
```
123+
124+
Overflow (addition, multiplication, etc, and this case, [bitshifting by too much](https://github.com/rust-lang/rust/issues/10183)) is also caught by RMC:
125+
126+
```
127+
** Results:
128+
./src/lib.rs function estimate_size
129+
[estimate_size.assertion.1] line 10 attempt to shift left by `move _10`, which would overflow: FAILURE
130+
[estimate_size.undefined-shift.1] line 10 shift distance too large in 1 << var_10: FAILURE
131+
132+
** 2 of 2 failed (2 iterations)
133+
VERIFICATION FAILED
134+
```
135+
136+
</details>
137+
138+
## Assertions, Assumptions, and Harnesses
139+
140+
It seems a bit odd that we can take billions of inputs, but our function clearly only handles up to a few thousand.
141+
Let's codify this fact about our function by asserting some reasonable bound on our input, after we've fixed our bug:
142+
143+
```rust
144+
{{#include tutorial/rmc-first-steps/tests/final-form.rs:code}}
145+
```
146+
147+
Now we have stated our previously implicit expectation: this function should never be called with inputs that are too big.
148+
But if we attempt to verify this, we get a problem:
149+
150+
```
151+
** Results:
152+
./tests/final-form.rs function estimate_size
153+
[estimate_size.assertion.1] line 3 assertion failed: x < 4096: FAILURE
154+
155+
** 1 of 1 failed (2 iterations)
156+
VERIFICATION FAILED
157+
```
158+
159+
We intended this to be a precondition of calling the function, but RMC is treating it like a failure.
160+
If we call this function with too large of a value, it will crash with an assertion failure.
161+
But we know that, that was our intention.
162+
163+
This is the purpose of _proof harnesses_.
164+
Much like property testing (which would also find this assertion failure as a bug), we need to set up our preconditions, call the function in question, then assert our post conditions.
165+
Here's a revised example of the proof harness, one that now succeeds:
166+
167+
```rust
168+
{{#include tutorial/rmc-first-steps/tests/final-form.rs:rmc}}
169+
```
170+
171+
But now we must wonder if we've really fully tested our function.
172+
What if we revise the function, but forget to update the assumption in our proof harness to cover the new range of inputs?
173+
174+
Fortunately, RMC is able to report a coverage metric for each proof harness.
175+
Try running:
176+
177+
```
178+
rmc --visualize tests/final-form.rs
179+
open report/html/index.html
180+
```
181+
182+
The beginning of the report includes coverage information.
183+
Clicking through to the file will show fully-covered lines in green.
184+
Lines not covered by our proof harness will show in red.
185+
186+
1. Try changing the assumption in the proof harness to `x < 2048`. Now the harness won't be testing all possible cases.
187+
2. Rerun `rmc --visualize` on the file
188+
3. Look at the report: you'll see we no longer have 100% coverage of the function.
189+
190+
191+
## Summary
192+
193+
In this section:
194+
195+
1. We saw RMC find panics, assertion failures, and even some other failures like unsafe dereferencing of null pointers.
196+
2. We saw how to get a failing trace using `rmc --visualize`
197+
3. We saw how proof harnesses are used to set up preconditions and assert postconditions.
198+
4. We saw how to obtain coverage metrics and use them to ensure our proofs are covering as much as they should be.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0 OR MIT
3+
[package]
4+
name = "rmc-first-steps"
5+
version = "0.1.0"
6+
edition = "2018"
7+
8+
[dependencies]
9+
10+
[dev-dependencies]
11+
proptest = "1.0.0"
12+
13+
[workspace]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
// ANCHOR: code
5+
fn estimate_size(x: u32) -> u32 {
6+
if x < 256 {
7+
if x < 128 {
8+
return 1;
9+
} else {
10+
return 3;
11+
}
12+
} else if x < 1024 {
13+
if x > 1022 {
14+
panic!("Oh no, a failing corner case!");
15+
} else {
16+
return 5;
17+
}
18+
} else {
19+
if x < 2048 {
20+
return 7;
21+
} else {
22+
return 9;
23+
}
24+
}
25+
}
26+
// ANCHOR_END: code
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::*;
31+
use proptest::prelude::*;
32+
33+
#[test]
34+
fn it_works() {
35+
assert_eq!(estimate_size(1024), 7);
36+
}
37+
38+
// ANCHOR: proptest
39+
proptest! {
40+
#![proptest_config(ProptestConfig::with_cases(10000))]
41+
#[test]
42+
fn doesnt_crash(x: u32) {
43+
estimate_size(x);
44+
}
45+
}
46+
// ANCHOR_END: proptest
47+
}
48+
49+
fn __nondet() -> u32 {
50+
unimplemented!()
51+
}
52+
53+
// ANCHOR: rmc
54+
#[cfg(rmc)]
55+
#[no_mangle]
56+
fn main() {
57+
let x: u32 = __nondet();
58+
estimate_size(x);
59+
}
60+
// ANCHOR_END: rmc
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
// ANCHOR: code
5+
fn estimate_size(x: u32) -> u32 {
6+
assert!(x < 4096);
7+
8+
if x < 256 {
9+
if x < 128 {
10+
return 1;
11+
} else {
12+
return 3;
13+
}
14+
} else if x < 1024 {
15+
if x > 1022 {
16+
return 4;
17+
} else {
18+
return 5;
19+
}
20+
} else {
21+
if x < 2048 {
22+
return 7;
23+
} else {
24+
return 9;
25+
}
26+
}
27+
}
28+
// ANCHOR_END: code
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
use proptest::prelude::*;
34+
35+
#[test]
36+
fn it_works() {
37+
assert_eq!(estimate_size(1024), 7);
38+
}
39+
40+
// ANCHOR: proptest
41+
proptest! {
42+
#![proptest_config(ProptestConfig::with_cases(10000))]
43+
#[test]
44+
fn doesnt_crash(x in 0..4095u32) {
45+
estimate_size(x);
46+
}
47+
}
48+
// ANCHOR_END: proptest
49+
}
50+
51+
fn __nondet() -> u32 {
52+
unimplemented!()
53+
}
54+
fn __VERIFIER_assume(cond: bool) {
55+
unimplemented!()
56+
}
57+
58+
// ANCHOR: rmc
59+
#[cfg(rmc)]
60+
#[no_mangle]
61+
fn main() {
62+
let x: u32 = __nondet();
63+
__VERIFIER_assume(x < 4096);
64+
let y = estimate_size(x);
65+
assert!(y < 10);
66+
}
67+
// ANCHOR_END: rmc
68+
69+
#[cfg(rmc)]
70+
#[no_mangle]
71+
fn failing_main() {
72+
let x: u32 = __nondet();
73+
let y = estimate_size(x);
74+
}

0 commit comments

Comments
 (0)