Skip to content

Commit a9b1eea

Browse files
jaylorchCopilot
andauthored
Verify Number with Verus (#774)
feat(verus): Verification of `number` Signed-off-by: Jay Lorch <jaylorch@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 409a36f commit a9b1eea

21 files changed

Lines changed: 2690 additions & 63 deletions

.dir-locals.el

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
---
2+
name: verus-verification
3+
description: >-
4+
Rigorous Verus specification and proof work for Regorus. Use when adding,
5+
strengthening, debugging, or reviewing Verus contracts, proofs, external-body
6+
boundaries, assume_specification declarations, BigInt or Number models, or
7+
minimal Verus bug reproducers. Preserves executable behavior while minimizing
8+
trusted assumptions and verifier workarounds.
9+
---
10+
11+
# Regorus Verus Verification
12+
13+
Use this workflow for proof-oriented changes in Regorus, especially `Number`,
14+
`BigInt`, arithmetic, conversions, and policy-critical value semantics.
15+
16+
The objective is not merely to make Verus pass. The objective is to establish an
17+
exact, useful contract for the real executable implementation with the smallest
18+
honest trusted boundary.
19+
20+
## Core Rules
21+
22+
1. **Specify executable semantics exactly.**
23+
- Model every meaningful result variant and error path.
24+
- Preserve distinctions such as integer versus float representation.
25+
- For floating-point operations, specify IEEE-754 behavior rather than ideal
26+
real arithmetic.
27+
- Do not weaken a contract just because the stronger proof is inconvenient.
28+
29+
2. **Prove bodies whenever Verus supports them.**
30+
- Prefer a verified implementation over `assume_specification`.
31+
- Remove a trusted assumption once the implementation carries a proved spec.
32+
- Never describe an `external_body` function as body-proved.
33+
34+
3. **Preserve executable behavior.**
35+
- Before editing, compare the function with `main` or the relevant base.
36+
- Keep executable statements unchanged unless the task explicitly requires a
37+
runtime fix.
38+
- Never use conditional compilation to give Verus and ordinary Rust different
39+
executable bodies or behavior. If Verus cannot verify the shared body,
40+
retain the narrowest `external_body` boundary and document the unsupported
41+
construct.
42+
- Put ghost reasoning in `proof!` blocks. Move proof work to the beginning of
43+
the function when it depends only on inputs.
44+
- Afterward, inspect the focused diff against the base and confirm that only
45+
contracts and erased proof code differ, unless a runtime change was intended.
46+
47+
4. **Do not use preconditions to hide valid edge cases.**
48+
- Check minimum signed values, maximum values, zero, and representation
49+
boundaries explicitly.
50+
- Negating `i32::MIN` as `i32` overflows, but its magnitude $2^31$ fits in
51+
`u32`. Widen before negation, for example `(-(e as i64)) as u32`.
52+
- If the API can compute a valid result, prove and compute it instead of
53+
excluding the input or returning an invented error.
54+
55+
5. **Reuse existing semantic models.**
56+
- Search `src/verify/` before adding an uninterpreted spec function.
57+
- Prefer established models such as `pow2`, `NumberView`,
58+
`to_f64_lossy_ensures`, and BigInt view/spec traits.
59+
- If the same mathematical value can have representation-dependent runtime
60+
behavior, quantify over the concrete modeled value rather than pretending
61+
the view alone determines the result.
62+
- When a view deliberately merges concrete variants, use a relational
63+
postcondition for representation-sensitive operations. For example,
64+
`NumberView::Integer` merges `Int`, `UInt`, and `BigInt`, whose lossy float
65+
conversions and boundary behavior need not be a function of the view alone.
66+
- Propagate that relation through callers with existential result witnesses.
67+
Do not recover hidden representation by existentially inventing a concrete
68+
`Number` whose view matches; that leaks internals and may choose a witness
69+
unrelated to the executable receiver.
70+
71+
6. **Minimize and explain trust.**
72+
- Use `external_body` only at the smallest unsupported boundary.
73+
- Give an exact postcondition, not merely positivity or successful return,
74+
whenever downstream proofs depend on exact behavior.
75+
- Add a short comment naming the concrete verifier limitation, for example:
76+
overloaded `<<=`/`>>=` is unsupported, or overloaded `!` on external
77+
`BigInt` crashes this Verus version.
78+
- Avoid broad external wrappers around otherwise verifiable callers.
79+
80+
## Workflow
81+
82+
### 1. Establish the Runtime Baseline
83+
84+
Start with the function, its helper contracts, its callers, and any existing
85+
trusted specification.
86+
87+
```bash
88+
git show main:path/to/file.rs
89+
rg -n 'function_name|assume_specification|relevant_helper' src tests
90+
```
91+
92+
Record one falsifiable hypothesis:
93+
- what the exact behavior should be;
94+
- which helper contracts it depends on;
95+
- the cheapest verification or runtime check that could disprove it.
96+
97+
Do not map the whole subsystem before making a small grounded edit.
98+
99+
### 2. Write the Contract Before the Proof
100+
101+
The contract should answer:
102+
- Which inputs return `Ok`, `Err`, `Some`, or `None`?
103+
- What exact mathematical value is represented?
104+
- Is the result an integer or float variant?
105+
- Which rounding, overflow, saturation, or lossy-conversion rule applies?
106+
- Are multiple concrete representations possible for the same view?
107+
108+
For arithmetic returning `Result`, avoid vague contracts such as only
109+
`result is Ok` when the exact value is knowable.
110+
111+
Write contracts around semantic inputs first, then state the result with
112+
`result matches ...`, `result is None`, or `result is Err`. This is usually
113+
clearer than matching every input/result tuple and separately excluding each
114+
impossible result variant.
115+
116+
If an abstract view erases representation but the API result depends on it,
117+
allow the honest overlap in the postcondition and explain the boundary. For
118+
example, at `+/-2^53`, primitive and BigInt-backed `Number` values with the same
119+
view can legitimately differ between `Some` and `None` in an exact-float API.
120+
121+
For BigInt operators, provide exact operator models and prove the caller against
122+
them. For division producing a float, model the exact lossy conversions used by
123+
the executable code.
124+
125+
### 3. Remove Redundant Trust
126+
127+
Search for existing assumptions:
128+
129+
```bash
130+
rg -n 'assume_specification.*function_name|uninterp spec fn' src/verify src
131+
```
132+
133+
When moving a spec onto a body-verified function:
134+
- delete the old `assume_specification` in the same change;
135+
- ensure no duplicate specification remains;
136+
- strengthen helper contracts only as much as the body proof requires.
137+
138+
An external helper may remain trusted when Verus cannot translate its syntax,
139+
but its contract must expose all facts needed by verified callers.
140+
141+
### 4. Keep Proofs Separate From Execution
142+
143+
Prefer this shape:
144+
145+
```rust
146+
pub fn operation(input: i32) -> Result<Number> {
147+
proof! {
148+
// Input-only lemmas, cast equalities, and arithmetic facts.
149+
}
150+
151+
// Original executable body.
152+
}
153+
```
154+
155+
Use local proof blocks later only when facts genuinely depend on an executable
156+
value produced at that point.
157+
158+
Do not introduce executable temporaries solely to help a proof. If a temporary
159+
is ghost-only, keep it inside `proof!`.
160+
161+
### 5. Handle Casts and Boundaries Explicitly
162+
163+
Verus often needs explicit facts connecting machine integers and mathematical
164+
integers/naturals:
165+
166+
```rust
167+
assert((e as u32) as nat == e as nat);
168+
```
169+
170+
For negative signed values, widen before negating:
171+
172+
```rust
173+
let magnitude = (-(e as i64)) as u32;
174+
```
175+
176+
Then prove:
177+
- the magnitude is positive;
178+
- its cast equals the intended mathematical magnitude;
179+
- required power/division lemmas apply;
180+
- remainder is nonzero when the runtime should choose floating division.
181+
182+
Check memory implications separately. A mathematically valid BigInt may be very
183+
large. Prove extreme paths, but do not execute resource-heavy regression tests
184+
unless the cost is acceptable and intentional. Test the conversion and a smaller
185+
representative behavior instead.
186+
187+
For signed division and remainder, model Rust semantics with `rust_div` and
188+
`rust_rem`; mathematical `/` and `%` do not capture truncation toward zero for
189+
all negative inputs. Bridge primitive operator specs such as `RemSpec` to those
190+
models with focused lemmas. Handle `MIN / -1` before either `/` or `%`, because
191+
both machine operations overflow, and prove the exact quotient fits before
192+
connecting a mathematical result to `checked_div` or a narrowing cast.
193+
194+
### 6. Use Verification Attributes Deliberately
195+
196+
- `#[verus_verify]` on an `impl` applies to all methods in that impl.
197+
- Do not split adjacent inherent impls merely to change verification scope when
198+
one impl-level annotation plus narrow method overrides is clearer.
199+
- Use `#[verus_verify(external)]` only when an item must remain entirely outside
200+
verification and has a separate specification.
201+
- Use `#[verus_verify(external_body)]` when Verus should trust a stated contract
202+
but cannot verify the implementation body.
203+
- Method-level attributes can override the impl-wide default.
204+
205+
Before diagnosing missing internal markers or macro bugs, inspect braces and
206+
attributes. Confirm the method is actually inside the annotated impl.
207+
208+
### 7. Preserve Production Macros
209+
210+
Do not replace `bail!`, `anyhow!`, or formatting in the executable body merely
211+
to make translation easier. Inspect the macro expansion and specify the
212+
smallest unsupported pieces. For `anyhow!`, this may mean narrow specifications
213+
for `Arguments::from_str`, `format_err`, and `must_use`; if verified callers
214+
only rely on taking the error branch, those assumptions need not promise
215+
anything about the error value.
216+
217+
After a Verus upgrade, retry the original macro and previously externalized
218+
bodies. Translation support changes, so stale shims and `external_body`
219+
annotations should not become permanent trusted surface by inertia.
220+
221+
## Diagnosing Verus Failures
222+
223+
### Trigger Failure
224+
225+
Before repairing or replacing a rejected trigger, check whether the quantifier
226+
is semantically necessary. If its bound variables merely name fields of fixed
227+
arguments through equalities such as `lhs == NumberView::Integer(integer_lhs)`,
228+
match on those arguments and state the branch-specific condition directly. This
229+
preserves the contract while removing the quantifier, its trigger, and needless
230+
solver instantiation. Use a natural or artificial trigger only when the contract
231+
genuinely ranges over multiple values that are not determined by fixed inputs.
232+
233+
### Translation or Compiler Failure
234+
235+
1. Reduce to the exact operator, type, attribute, and impl context.
236+
2. Test a one-file reproducer with the same relevant structure.
237+
3. Do not introduce macros, missing impl annotations, or different ownership
238+
patterns unless they exist in the failing code.
239+
4. If a small candidate passes, it is not a reproducer. Keep reducing the real
240+
context or state that the failure was caused by local annotation structure.
241+
5. Inspect `~/verus` only after the local code path is understood.
242+
243+
A valid verifier bug report must:
244+
- fail on the stated Verus version;
245+
- contain no unrelated repository dependencies when avoidable;
246+
- reproduce the same failure mechanism;
247+
- document any workaround retained in Regorus.
248+
249+
### Proof Failure
250+
251+
Treat the first focused failure as evidence:
252+
- failed arithmetic safety means the implementation has an unhandled machine
253+
boundary or needs a justified precondition;
254+
- failed postcondition may indicate a missing helper fact, a representation
255+
mismatch, or an incorrect contract;
256+
- unsupported library internals should be isolated in the narrowest helper, not
257+
used to externalize the verified caller.
258+
259+
Do not respond to a failed proof by immediately weakening the postcondition.
260+
First trace a concrete input through the runtime behavior.
261+
262+
## Validation
263+
264+
After the first substantive edit, immediately run the narrowest check:
265+
266+
```bash
267+
cargo verus verify \
268+
--fwd-verus-args-to roots -- --verify-module number
269+
```
270+
271+
Use a fresh target directory when checking for stale macro or compiler behavior.
272+
273+
After the focused proof passes:
274+
275+
```bash
276+
cargo test focused_test_name
277+
cargo fmt --all -- --check
278+
git diff --check
279+
```
280+
281+
For broader or final validation, use repository commands as appropriate:
282+
283+
```bash
284+
cargo xtask fmt
285+
cargo xtask clippy
286+
cargo xtask ci-debug
287+
```
288+
289+
Report verification counts accurately. Distinguish:
290+
- body-verified functions;
291+
- external-body contracts;
292+
- trusted assumptions;
293+
- runtime tests actually executed;
294+
- extreme tests skipped due to resource cost.
295+
296+
## Completion Checklist
297+
298+
- [ ] Contract matches exact executable semantics.
299+
- [ ] Integer/float and `Undefined` distinctions remain intact where relevant.
300+
- [ ] Minimum/maximum signed values and casts were considered.
301+
- [ ] Original executable body is preserved unless a runtime bug was fixed.
302+
- [ ] Proof-only code is inside `proof!` and placed early when possible.
303+
- [ ] No redundant uninterpreted helper or trusted assumption remains.
304+
- [ ] Every `external_body` has the narrowest useful exact contract and a reason.
305+
- [ ] Impl-level verification annotations cover the intended methods without
306+
unnecessary splits.
307+
- [ ] Any claimed verifier reproducer is representative and independently fails.
308+
- [ ] Focused Verus verification passes.
309+
- [ ] Relevant runtime tests, formatting, and diff checks pass.

.github/workflows/verus.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ jobs:
3535
shell: bash
3636
run: |
3737
set -euxo pipefail
38-
asset_url=https://github.com/verus-lang/verus/releases/download/release%2F0.2026.08.09.92f466f/verus-0.2026.08.09.92f466f-x86-linux.zip
39-
asset_sha256=2f5a41c553f424aacdd732339e9d125563716a0b003c27730f75d6f81a282cef
38+
asset_url=https://github.com/verus-lang/verus/releases/download/release%2F0.2026.08.23.fbbbbcf/verus-0.2026.08.23.fbbbbcf-x86-linux.zip
39+
asset_sha256=b65483714e6bf2ae72bfe7c7199e1c608495a941306b979260e9ed585d5899c9
4040
test -n "$asset_url"
4141
curl -fsSL "$asset_url" -o verus.zip
4242
@@ -77,4 +77,4 @@ jobs:
7777
export PATH="$(dirname "$cargo_verus_bin"):$(dirname "$verus_bin"):$PATH"
7878
cargo verus --help
7979
cargo fetch --locked
80-
cargo verus verify --locked --features verus
80+
cargo verus verify --locked

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ bindings/ruby/bin/
5454
bindings/java/.classpath
5555
bindings/java/.project
5656
bindings/java/.settings/
57+
58+
# Emacs backup files
59+
*~

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)