agent-formal-verify 0.0.0-stage → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +157 -0
- package/LICENSE +232 -0
- package/README.md +92 -2
- package/opencode.mjs +45 -0
- package/package.json +42 -4
- package/skills/formal-verify/SKILL.md +125 -0
- package/skills/formal-verify/examples/kani-template/Cargo.lock +7 -0
- package/skills/formal-verify/examples/kani-template/Cargo.toml +7 -0
- package/skills/formal-verify/examples/kani-template/src/lib.mutations +11 -0
- package/skills/formal-verify/examples/kani-template/src/lib.rs +63 -0
- package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.lean +98 -0
- package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.mutations +11 -0
- package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.statements +231 -0
- package/skills/formal-verify/examples/lean-template/lake-manifest.json +6 -0
- package/skills/formal-verify/examples/lean-template/lakefile.toml +6 -0
- package/skills/formal-verify/examples/lean-template/lean-toolchain +1 -0
- package/skills/formal-verify/examples/tla-template/BoundedQueue.matrix +20 -0
- package/skills/formal-verify/examples/tla-template/BoundedQueue.mutations +26 -0
- package/skills/formal-verify/examples/tla-template/BoundedQueue.tla +42 -0
- package/skills/formal-verify/references/lean-brief.md +57 -0
- package/skills/formal-verify/references/rust-brief.md +81 -0
- package/skills/formal-verify/references/setup.md +85 -0
- package/skills/formal-verify/references/tla-brief.md +62 -0
- package/skills/formal-verify/scripts/kani-check.sh +8 -0
- package/skills/formal-verify/scripts/kani.py +250 -0
- package/skills/formal-verify/scripts/lean-check.sh +171 -0
- package/skills/formal-verify/scripts/lean-statements.py +135 -0
- package/skills/formal-verify/scripts/lean-tools.sh +15 -0
- package/skills/formal-verify/scripts/mutate.py +274 -0
- package/skills/formal-verify/scripts/setup.sh +35 -0
- package/skills/formal-verify/scripts/tlc-matrix.sh +172 -0
- package/skills/formal-verify/scripts/tlc-tools.sh +47 -0
- package/skills/formal-verify/scripts/tlc-trace.py +64 -0
- package/skills/setup/SKILL.md +24 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---- MODULE BoundedQueue ----
|
|
2
|
+
(* Copyright (c) 2026 Michael Denyer
|
|
3
|
+
SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
|
|
5
|
+
The counters of a bounded queue with ordered output: `next` counts claimed
|
|
6
|
+
items, `consumed` counts consumed items, and at most `Window` items may be
|
|
7
|
+
in flight. It is the same model as ../lean-template/Model/BoundedQueue.lean.
|
|
8
|
+
TLC checks it for the instances in BoundedQueue.matrix; Lean proves it for
|
|
9
|
+
every size. BoundedQueue.mutations lists the bugs its properties must detect.
|
|
10
|
+
|
|
11
|
+
To adapt it, replace the header with the source files and line ranges the
|
|
12
|
+
model follows, give each thread a program counter, and keep the top-level
|
|
13
|
+
formula named Spec, which tlc-matrix.sh checks. *)
|
|
14
|
+
EXTENDS Naturals
|
|
15
|
+
|
|
16
|
+
CONSTANTS Window, Items
|
|
17
|
+
VARIABLES next, consumed
|
|
18
|
+
vars == <<next, consumed>>
|
|
19
|
+
|
|
20
|
+
Init == next = 0 /\ consumed = 0
|
|
21
|
+
|
|
22
|
+
Claim == /\ next < Items
|
|
23
|
+
/\ next < consumed + Window
|
|
24
|
+
/\ next' = next + 1
|
|
25
|
+
/\ UNCHANGED consumed
|
|
26
|
+
|
|
27
|
+
Consume == /\ consumed < next
|
|
28
|
+
/\ consumed' = consumed + 1
|
|
29
|
+
/\ UNCHANGED next
|
|
30
|
+
|
|
31
|
+
\* Stutter at the end so that termination is not reported as a deadlock.
|
|
32
|
+
Done == consumed = Items /\ UNCHANGED vars
|
|
33
|
+
|
|
34
|
+
Next == Claim \/ Consume \/ Done
|
|
35
|
+
|
|
36
|
+
Spec == Init /\ [][Next]_vars /\ WF_vars(Claim) /\ WF_vars(Consume)
|
|
37
|
+
|
|
38
|
+
TypeOK == next \in 0..Items /\ consumed \in 0..Items
|
|
39
|
+
InWindow == consumed <= next /\ next <= consumed + Window
|
|
40
|
+
WindowFull == next = consumed + Window
|
|
41
|
+
AllConsumed == <>(consumed = Items)
|
|
42
|
+
====
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Lean modelling brief
|
|
2
|
+
|
|
3
|
+
Follow this brief to prove one sequential or arithmetic property with Lean 4. Your task gives the source files and line ranges, the functions with their integer types, the property, the model's `<Name>`, the absolute path of the Lake project and the absolute path `<skill-dir>`.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
Write `Model/<Name>.lean`, `Model/<Name>.mutations` and `Model/<Name>.statements` in the Lake project. Start from the `BoundedQueue` files in `<skill-dir>/examples/lean-template/Model/`. Write no other file, so parallel agents share none. Put every declaration in `namespace <Name>`: the checker audits the project's modules together, and equal names clash. The header comment names the source files and line ranges the model follows and each assumption it makes.
|
|
8
|
+
|
|
9
|
+
Every `.lean` file in the project must belong to a library in the lakefile. With the template's lakefile, that is every file under `Model/`. Add Mathlib only when the arithmetic needs it, since its dependencies can increase setup and build time.
|
|
10
|
+
|
|
11
|
+
## Model
|
|
12
|
+
|
|
13
|
+
1. Transcribe the state type and functions with the code's integer widths, rounding, division and order of operations. Use `UInt32` or `Int` with explicit bounds where appropriate, and `Nat` only when values cannot go negative. Keep the source function names and cite `file:line` in doc comments. Preserve any bug during transcription so the model checks the code as written.
|
|
14
|
+
2. State the property as a `Prop` with a `Decidable` instance. It may describe an invariant, an encoder round trip, an index bound or states the machine must never reach.
|
|
15
|
+
3. Write every theorem statement with `sorry` for its proof, then freeze the model with `bash <skill-dir>/scripts/lean-check.sh <absolute-project-path> --freeze Model/<Name>.lean`. This records types and definition bodies even while proofs are unfinished. The FROZEN line confirms the record, not a proof. Added axioms still fail.
|
|
16
|
+
4. Write and evaluate a bounded exhaustive check before proving the theorem. The template's `badPairs` finds states where one step breaks the invariant; `#eval badPairs 2 6` prints concrete counter-examples and `#guard` fails the build while any exist. Set the bound above the code's slot count, batch size, window and other constants so it includes boundary cases. Add a `#guard` that a bounded search from the initial state reaches the states the property depends on. The template's `reachable` search requires a full window.
|
|
17
|
+
5. Prove one theorem per step or function for every size. Split on the guards, use `simp only [...]` to expose arithmetic, then use `omega` or `decide` for a finite type. If a proof needs a fact about the real code that the model lacks, state it as a theorem hypothesis and name it in a comment. Do not declare it as an `axiom`. An unfinished proof may use `sorry`, but the Lean checker reports it as a failure. The checker also fails every declaration that rests on an axiom beyond `propext`, `Classical.choice` and `Quot.sound`, which also rules out `native_decide`.
|
|
18
|
+
|
|
19
|
+
After a proof closes, print differential test vectors with `#eval`. Include boundary inputs and a spread of in-range inputs, with each input and the model's output on one line. Write a test in the target repository's own framework that calls the production function on those inputs and compares its output. A mismatch is a bug in the production code or the transcription; establish which before reporting. State how many vectors agree and the integer widths in both implementations. If the production comparison was not run, say so beside the proof verdict.
|
|
20
|
+
|
|
21
|
+
When a theorem needs a proof beyond bounded search and the host offers Lean language-server tools for goals and diagnostics, use them to shorten the loop. This plugin neither requires nor configures such a server.
|
|
22
|
+
|
|
23
|
+
## Keep the model small
|
|
24
|
+
|
|
25
|
+
Write each fact once.
|
|
26
|
+
|
|
27
|
+
- Define the bounded inputs once and filter that list for each property, instead of a new nested loop per property.
|
|
28
|
+
- When two proofs share steps, prove the shared fact as one lemma and use it in both.
|
|
29
|
+
- Keep mutations in the mutations file. The model has no flag or second definition for a planted bug.
|
|
30
|
+
|
|
31
|
+
## Check
|
|
32
|
+
|
|
33
|
+
Stay in the target repository and pass absolute paths:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
bash <skill-dir>/scripts/lean-check.sh <absolute-project-path> Model/<Name>.lean
|
|
37
|
+
python3 <skill-dir>/scripts/mutate.py <absolute-project-path>/Model/<Name>.mutations
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The checker enforces the `.statements` file beside each checked source. Changing a theorem type, a definition type or body, or deleting a frozen declaration fails with its name. Adding lemmas and definitions and changing proofs is allowed. Re-freeze only after the production code changes, after a production test contradicts the transcription, or after reviewing a toolchain bump, and explain why in the report. Records are specific to the pinned Lean toolchain. Fingerprints use structural expressions, including the module identity of private references. If two private declarations share a name in one model, give them distinct names before freezing. A project-wide check also rejects records whose source files were deleted. Named-file checks remain limited to those files. A model without a record retains the usual proof and axiom checks, so keep the records under version control. Deleting a record removes that protection.
|
|
41
|
+
|
|
42
|
+
The file name limits the Lean checker to your model, so another agent's unfinished file does not fail your check.
|
|
43
|
+
|
|
44
|
+
List plausible bugs in the mutations file, such as `<` instead of `≤`, a missing `+ 1` or a swapped argument. The mutation runner checks each on a copy of the file and names the theorems and `#guard` lines that fail. Expect both the bounded search's `#guard` and the theorem. It rejects a mutation that stops a definition compiling. Revise the properties behind each MISSED line.
|
|
45
|
+
|
|
46
|
+
Check the reach guard as well as the absence of bad pairs. A search that cannot reach a boundary state does not establish the intended coverage.
|
|
47
|
+
|
|
48
|
+
## Report
|
|
49
|
+
|
|
50
|
+
Report only after reading the output of both commands. If either exits 3 with `UNAVAILABLE`, report "not checked" with the printed remedy; do not claim a pass or a model failure. Give:
|
|
51
|
+
|
|
52
|
+
- the file paths, both commands, the checker's PASS or FAIL line and the mutation runner's output;
|
|
53
|
+
- the states required by the reach guard and the search bound;
|
|
54
|
+
- for each counter-example, concrete inputs to the code's function, the source line of the failing arithmetic, a unit test with those inputs and whether it fails on the production code, whether shipped callers can pass those inputs, and the proposed fix;
|
|
55
|
+
- the `.statements` path and any reason for re-freezing;
|
|
56
|
+
- for a closed proof, its assumptions, including integer widths, bounds and each theorem hypothesis;
|
|
57
|
+
- the number of differential vectors on which the model and production code agree, their integer widths, or an explicit statement that the comparison was not run.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Rust and Kani brief
|
|
2
|
+
|
|
3
|
+
Use Kani for a Rust function whose inputs and loops can be bounded. Call the real
|
|
4
|
+
function from one `#[kani::proof]` harness per property. Do not transcribe its
|
|
5
|
+
implementation. Start from [the bounded queue template](../examples/kani-template/src/lib.rs).
|
|
6
|
+
|
|
7
|
+
## Files
|
|
8
|
+
|
|
9
|
+
Work in the target repository. `<skill-dir>` means the absolute path to this
|
|
10
|
+
plugin's `skills/formal-verify` directory, not a directory in the target crate.
|
|
11
|
+
Own only the assigned harness file and its adjacent `<Name>.mutations` file.
|
|
12
|
+
Coordinate any shared crate or module wiring with its owner. Keep production
|
|
13
|
+
files untouched unless the user has authorized a production fix.
|
|
14
|
+
|
|
15
|
+
Cite the production source paths and line numbers that each property models in
|
|
16
|
+
the harness header. List caller constraints, bounds and stub assumptions there,
|
|
17
|
+
with source citations or an explicit statement that each is an assumption.
|
|
18
|
+
Call the production function through its real crate or module interface.
|
|
19
|
+
|
|
20
|
+
## Model
|
|
21
|
+
|
|
22
|
+
Use `kani::any()` for inputs and `kani::assume` for caller constraints. Record
|
|
23
|
+
integer widths, input bounds and each assumption beside the harness. If a callee
|
|
24
|
+
needs a stub or a function contract, name that assumption in the header comment.
|
|
25
|
+
Put `#[kani::unwind(N)]` on every harness, above its real loop bounds. The checker
|
|
26
|
+
requires this explicit bound and passes it to Kani. A failed unwinding assertion
|
|
27
|
+
means the bound is too low and fails the check.
|
|
28
|
+
|
|
29
|
+
Add `kani::cover!` witnesses for the boundary or blocking states each property
|
|
30
|
+
depends on, such as a full window or an empty queue. The checker rejects
|
|
31
|
+
unsatisfiable, unreachable and undetermined covers, even when Kani exits
|
|
32
|
+
successfully. Ordinary unreachable assertions are allowed because they describe
|
|
33
|
+
impossible panic branches. Every cover must have status `Satisfied`.
|
|
34
|
+
|
|
35
|
+
## Check
|
|
36
|
+
|
|
37
|
+
Stay in the target repository and use absolute paths:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
bash <skill-dir>/scripts/kani-check.sh /absolute/path/to/crate
|
|
41
|
+
python3 <skill-dir>/scripts/mutate.py /absolute/path/to/crate/src/Name.mutations
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Append fully qualified harness names to check only those properties. The helper
|
|
45
|
+
prints one verdict per harness with the unwind bound and number of checks. It
|
|
46
|
+
requires a complete Kani 0.68.0 bundle and Python 3. It checks and invokes the
|
|
47
|
+
bundle's driver directly, without invoking the auto-installing Cargo wrapper.
|
|
48
|
+
A registered Cargo installer is not required after setup. Relative `KANI_HOME`
|
|
49
|
+
paths resolve against the caller's directory before any temporary work begins.
|
|
50
|
+
The helper installs no Kani runtime. `UNAVAILABLE` means not checked; follow the
|
|
51
|
+
printed installation remedy before making any claim about the code. A compilation
|
|
52
|
+
error, missing harness or incomplete verifier report fails.
|
|
53
|
+
|
|
54
|
+
Put `<Name>.mutations` beside the assigned `<Name>.rs`. Use
|
|
55
|
+
`detects <fully-qualified-harness>` for each mutation. The mutation runner copies
|
|
56
|
+
the whole crate to temporary storage and checks the named harness. A compiler
|
|
57
|
+
error never counts as detection. Every discovered harness needs a mutation.
|
|
58
|
+
|
|
59
|
+
## Counter-examples
|
|
60
|
+
|
|
61
|
+
The helper prints failed checks with source locations and Kani's generated
|
|
62
|
+
concrete playback test when available. Copy that test into the harness module in
|
|
63
|
+
a temporary crate copy. Stay in the target repository and run:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
cargo kani playback --manifest-path /absolute/path/to/temporary-crate/Cargo.toml -Z concrete-playback -- <generated-test-name>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A printed test has not yet reproduced the failure. Record whether this command
|
|
70
|
+
actually fails on the expected assertion. Kani cannot generate playback for
|
|
71
|
+
every failure, including unwinding failures; report that limitation explicitly.
|
|
72
|
+
Never disable unwinding checks to obtain a pass.
|
|
73
|
+
|
|
74
|
+
## Report
|
|
75
|
+
|
|
76
|
+
Report each property, input bounds, integer widths, unwind bound, assumptions,
|
|
77
|
+
cover witnesses, mutation results and source location. For each counter-example,
|
|
78
|
+
include its inputs and reproducing test and say whether shipped callers can pass
|
|
79
|
+
those inputs. Use `reproduced`, `reachable at shipped settings` or `model-only`
|
|
80
|
+
as appropriate. A pass covers the stated bounded inputs only. Report unavailable
|
|
81
|
+
tools as not checked with the remedy, never as a pass or a model failure.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Tool setup
|
|
2
|
+
|
|
3
|
+
Run `/agent-formal-verify:setup` once to prepare Java, Python, TLC, elan, Lean, Rust and Kani. In Codex, request this plugin's `setup` skill. The [setup skill](../../setup/SKILL.md) prepares all three verification tools independently of any repository and reuses existing runtimes and caches.
|
|
4
|
+
|
|
5
|
+
The formal-verify skill selects tools from the current repository on every invocation. It reuses prepared tools and installs a missing Lean toolchain through the setup helper when a Lean pin changes. Repository changes do not require the user to rerun the full setup.
|
|
6
|
+
|
|
7
|
+
## Running the helpers
|
|
8
|
+
|
|
9
|
+
The helpers are in the `scripts/` directory of the installed `formal-verify` skill, including a copied skills-only installation. `<skill-dir>` below stands for that skill's absolute path.
|
|
10
|
+
|
|
11
|
+
Stay in the target repository and run each helper with `bash` and its absolute path. Pass absolute paths to model projects and matrices. Keep the installed skill files read-only, and create models and build outputs in the target repository. Shared tool caches can serve several repositories; each repository determines its own tool requirements and Lean pins.
|
|
12
|
+
|
|
13
|
+
## TLA+ and TLC
|
|
14
|
+
|
|
15
|
+
The setup helper needs a JDK, Python 3, and `sha256sum` or `shasum` to verify the TLC JAR. If `java` on PATH has no runtime, as with the macOS stub, set `JAVA` to a working JDK's `java` binary. Copy the [TLA+ template](../examples/tla-template/BoundedQueue.tla) with its `BoundedQueue.matrix` and `BoundedQueue.mutations` into the project to start a spec.
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
bash <skill-dir>/scripts/setup.sh tla
|
|
19
|
+
bash <skill-dir>/scripts/tlc-matrix.sh /path/to/models/Name.matrix
|
|
20
|
+
python3 <skill-dir>/scripts/mutate.py /path/to/models/Name.mutations
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The plugin does not download TLC. Save `tla2tools.jar` from the [TLA+ release](https://github.com/tlaplus/tlaplus/releases/tag/v1.7.4) pinned in `scripts/tlc-tools.sh` as `~/.cache/tla/tla2tools-v1.7.4.jar`; the setup helper verifies its pinned SHA256 and names that path and release when the file is missing or does not match. Set `TLC_CACHE` to choose another cache directory. Set `TLC_JAR` to use an existing JAR; an explicitly supplied JAR is trusted and must already exist.
|
|
24
|
+
|
|
25
|
+
The matrix runner requires the JAR and a working Java runtime. It checks Python 3 before running, so a failing model can always print its reduced trace. It does not download tools. Each run checks the formula its spec names `Spec` and sees only the modules beside that spec. `TLC_WORKERS` controls the TLC worker count.
|
|
26
|
+
|
|
27
|
+
The mutation runner reads `Name.mutations` beside `Name.tla` and `Name.matrix`. For each mutation it copies the spec, replaces the listed text, and runs the matrix runner with the one property the mutation names, so it needs the same tools and settings.
|
|
28
|
+
|
|
29
|
+
## Rust and Kani
|
|
30
|
+
|
|
31
|
+
Install Rust and Cargo, then install Kani 0.68.0, the version pinned in [`scripts/kani.py`](../scripts/kani.py):
|
|
32
|
+
|
|
33
|
+
```shell
|
|
34
|
+
cargo install --locked kani-verifier --version 0.68.0
|
|
35
|
+
cargo kani setup
|
|
36
|
+
bash <skill-dir>/scripts/setup.sh rust
|
|
37
|
+
bash <skill-dir>/scripts/kani-check.sh /path/to/crate
|
|
38
|
+
bash <skill-dir>/scripts/kani-check.sh /path/to/crate proofs::claim_keeps_invariant
|
|
39
|
+
python3 <skill-dir>/scripts/mutate.py /path/to/crate/src/lib.mutations
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Kani supports Linux and macOS on x86_64 and aarch64. See the [pinned installation guide](https://github.com/model-checking/kani/blob/kani-0.68.0/docs/src/install-guide.md) for system prerequisites. The plugin downloads and installs no Kani runtime. `setup.sh rust` checks the complete pinned bundle and invokes its driver directly. Verification and mutations reuse that driver with its bundled Rust toolchain. They never invoke the auto-installing Cargo wrapper, so the Cargo installer registration is unnecessary after setup. A relative `KANI_HOME` resolves against the caller's directory.
|
|
43
|
+
|
|
44
|
+
The Rust checker discovers proof harnesses and checks each selected harness separately. It reports the unwind bound and check count, rejects an insufficient bound and any cover condition that was not satisfied, and prints a concrete playback test for a failed assertion when Kani can produce one. Larger inputs and loops remain outside the reported bounds. Crate builds may obtain dependencies declared in Cargo; the template has none.
|
|
45
|
+
|
|
46
|
+
The mutation runner reads `Name.mutations` beside `Name.rs`, copies the whole crate to a temporary directory, and checks the harness named by `detects`. It leaves the source crate unchanged. Each harness must have a mutation that its property detects.
|
|
47
|
+
|
|
48
|
+
## Lean 4
|
|
49
|
+
|
|
50
|
+
Install [elan](https://github.com/leanprover/elan#installation) through your normal package manager or its documented installer. Copy the Lean template's `lakefile.toml`, `lake-manifest.json` and pinned `lean-toolchain` file into the project once. Each model is one file under `Model/`, started from the [template model](../examples/lean-template/Model/BoundedQueue.lean) and its `BoundedQueue.mutations`.
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
bash <skill-dir>/scripts/setup.sh lean /path/to/lean-project
|
|
54
|
+
bash <skill-dir>/scripts/lean-check.sh /path/to/lean-project
|
|
55
|
+
bash <skill-dir>/scripts/lean-check.sh /path/to/lean-project Model/Name.lean
|
|
56
|
+
python3 <skill-dir>/scripts/mutate.py /path/to/lean-project/Model/Name.mutations
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The setup helper checks the version in the project's current `lean-toolchain` and installs it only when it is not ready; it leaves the global default unchanged. The Lean checker uses `elan run` without its install flag, so a missing toolchain is UNAVAILABLE instead of downloading inside the checker. Lake builds may fetch dependencies declared by the model project. The supplied template has no external Lean packages.
|
|
60
|
+
|
|
61
|
+
The Lean checker builds every `.lean` file in the project by its path, so a file that no Lake library owns fails the build instead of going unchecked. It then compiles a short audit against the modules Lake reports for those files. The checker needs a pin of Lean 4.20.0 or later, the first whose Lake builds a module by its source path, and says so when an older pin fails. The audit lists every declaration that rests on an axiom beyond `propext`, `Classical.choice` and `Quot.sound`, so a model needs no `#print axioms` lines. The audit imports the project's modules together, so each model declares its names in a namespace of its own. A file name after the project limits the build and the audit to that file.
|
|
62
|
+
|
|
63
|
+
The mutation runner reads `Name.mutations` beside `Name.lean`. It elaborates each mutated copy with the project's toolchain and built modules, outside the project, and does not install a toolchain.
|
|
64
|
+
|
|
65
|
+
Helpers print `UNAVAILABLE` and exit 3 when a required tool is missing, unusable or unverified. Report the target as "not checked" and give the printed remedy. A model or input error prints `FAIL` and exits 1; a usage error exits 2. An unavailable run prints no PASS or SUMMARY line.
|
|
66
|
+
|
|
67
|
+
## CI
|
|
68
|
+
|
|
69
|
+
The helpers live in this plugin, not in the target repository. In CI, check out `michael-denyer/agent-formal-verify` at a pinned commit into a separate directory and run the helpers from its `skills/formal-verify/scripts/`. Update that pin deliberately, as for any other tool. A repository that vendors the scripts instead must keep their GPL-3.0-only license headers.
|
|
70
|
+
|
|
71
|
+
Install the chosen runtime and the pinned TLC JAR in the CI environment, run the setup helper, then run the matrix runner, Lean checker or mutation runner as a separate step. Cache elan's toolchain directory if useful, and keep the version pins with the models. Use the same `TLC_JAR` and `JAVA` settings in setup and verification.
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
- uses: actions/checkout@<commit>
|
|
75
|
+
with:
|
|
76
|
+
repository: michael-denyer/agent-formal-verify
|
|
77
|
+
ref: <pinned-commit>
|
|
78
|
+
path: .formal-verify
|
|
79
|
+
- run: bash .formal-verify/skills/formal-verify/scripts/setup.sh tla
|
|
80
|
+
- run: bash .formal-verify/skills/formal-verify/scripts/tlc-matrix.sh "$GITHUB_WORKSPACE/tla/Name.matrix"
|
|
81
|
+
- run: python3 .formal-verify/skills/formal-verify/scripts/mutate.py "$GITHUB_WORKSPACE/tla/Name.mutations"
|
|
82
|
+
- run: bash .formal-verify/skills/formal-verify/scripts/setup.sh lean "$GITHUB_WORKSPACE/lean"
|
|
83
|
+
- run: bash .formal-verify/skills/formal-verify/scripts/lean-check.sh "$GITHUB_WORKSPACE/lean"
|
|
84
|
+
- run: python3 .formal-verify/skills/formal-verify/scripts/mutate.py "$GITHUB_WORKSPACE/lean/Model/Name.mutations"
|
|
85
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# TLA+ modelling brief
|
|
2
|
+
|
|
3
|
+
Follow this brief to model one thread protocol with TLA+ and TLC. Your task gives the source files and line ranges, the threads and shared variables, the model's `<Name>`, the absolute path `<skill-dir>` and any `JAVA` setting. Read the whole of each source file so the model follows the code.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
Write `tla/<Name>.tla`, `tla/<Name>.matrix` and `tla/<Name>.mutations` in the target repository, or the same names in its existing model directory. Start from the three `BoundedQueue` files in `<skill-dir>/examples/tla-template/`. Write no other file, so parallel agents share none. Name the top-level formula `Spec`. The header comment names the source files and line ranges the spec follows and each assumption it makes.
|
|
8
|
+
|
|
9
|
+
## Model
|
|
10
|
+
|
|
11
|
+
Apply each requirement that fits the protocol, and say in the report which did not.
|
|
12
|
+
|
|
13
|
+
- Use one step per critical section under the mutex and one per unlocked phase, such as build, parse or consume. Add an explicit `lock` variable when multiple lock acquisitions can race.
|
|
14
|
+
- Use a program counter per thread. Represent a condition-variable wait as a pc value in the wait set. Only a broadcast, signal or spurious wakeup can leave that set. The woken thread must retake the mutex and reread the state before acting. Give spurious wakeups no fairness requirement. In this encoding, a lost wakeup appears as a liveness violation rather than a TLC deadlock. Explain that in the header and check liveness.
|
|
15
|
+
- Model resource ownership as a variable. Each slot or buffer must be free, in exactly one queue, or held by exactly one thread. Each produced item must be recorded by the consumer, awaiting hand-over, or freed.
|
|
16
|
+
- When `longjmp`, an exception, a panic or `exit()` can leave a critical section without unlocking, use separate variables for the mutex's actual owner and the lock ownership recorded in the thread's live frames. An unwind can leave these inconsistent and block later acquisitions. Preserve that state so the model can detect the hang. List every allocation and exit call that can run under each lock, including out-of-memory exits.
|
|
17
|
+
- Let the consumer call `next` any number of times, stop calling, or call `close` from any idle point. Include close before the first call, after the last item and after an error. When the producer has a failure path, let it fail at any item through a constant set `FailAt`.
|
|
18
|
+
- Check small instances and boundary cases. Use slot counts of 1, 2 and the shipped count, item counts of 0, 1, 3 and 5, and one, two and three workers.
|
|
19
|
+
- Name a reachable state predicate for each blocking or boundary behaviour the properties depend on, such as a full window, an empty queue, a parked waiter or an injected failure. Add `reach <Operator> | <label-substring>` to its matrix section. It selects the first run whose label contains the substring. A command-line filter skips the reach check if it excludes that selected run.
|
|
20
|
+
- Check `TypeOK`, the ownership partition, delivery order, no delivery after end or error, the documented error-ordering guarantee, and no double free or leak at `closed`. For liveness, check that each blocking call returns, `close` terminates from every allowed calling state, and a run without `close` reaches the end. Require weak fairness on thread steps except consumer choices and spurious wakeups. Add strong fairness on a mutex acquisition only when a counter-example shows pure starvation by a spuriously waking peer. Name that assumption in the spec.
|
|
21
|
+
|
|
22
|
+
## Keep the spec small
|
|
23
|
+
|
|
24
|
+
Write each fact once.
|
|
25
|
+
|
|
26
|
+
- Group the variables that one thread owns and name the group. An action then lists the groups it leaves alone, not every variable:
|
|
27
|
+
|
|
28
|
+
```tla
|
|
29
|
+
worker == <<wpc, box>>
|
|
30
|
+
consumer == <<pc, display>>
|
|
31
|
+
vars == <<worker, consumer, done>>
|
|
32
|
+
Notify == /\ wpc = "notify" /\ wpc' = "exit" /\ done' = TRUE
|
|
33
|
+
/\ UNCHANGED <<box, consumer>>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Define a guard or expression that two actions or properties share as one operator.
|
|
37
|
+
- Keep mutations in the mutations file. The spec has no `Mutation` constant and no branch for a planted bug. When one replacement restores the behaviour before a fix, list it as a mutation instead of adding a constant that switches the fix off.
|
|
38
|
+
|
|
39
|
+
## Check
|
|
40
|
+
|
|
41
|
+
Stay in the target repository and pass absolute paths:
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
bash <skill-dir>/scripts/tlc-matrix.sh <absolute-path>/tla/<Name>.matrix
|
|
45
|
+
python3 <skill-dir>/scripts/mutate.py <absolute-path>/tla/<Name>.mutations
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The matrix runner prints PASS or FAIL per run and a SUMMARY line. A failing run prints its error trace, reduced to the variables each step changed.
|
|
49
|
+
|
|
50
|
+
Each reach check prints its own PASS with the shortest witness length in states and transitions, or FAIL when the model never reaches the state. Reach searches use one worker and ignore deadlock so they can finish exploring a stopped model. The SUMMARY counts reach checks separately; its state total covers ordinary runs only. Use `detects reach:<Operator>` for a mutation that removes a required state. Reach checks do not count as properties for mutation coverage.
|
|
51
|
+
|
|
52
|
+
Before reporting a pass, list mutations that represent plausible code bugs in the mutations file. Try replacing `while` with `if`, removing a broadcast, replacing FIFO order with stack order, leaving a flag set, or taking an item without removing it. Each mutation names the one property that must fail on it. The mutation runner applies each to a copy of the spec. Revise a property it reports as UNCOVERED, and the property or the mutation behind each MISSED line.
|
|
53
|
+
|
|
54
|
+
## Report
|
|
55
|
+
|
|
56
|
+
Report only after reading the output of both commands. If either exits 3 with `UNAVAILABLE`, report "not checked" with the printed remedy; do not claim a pass or a model failure. Give:
|
|
57
|
+
|
|
58
|
+
- the file paths, both commands and their SUMMARY lines;
|
|
59
|
+
- each reach predicate, its selected run and witness length;
|
|
60
|
+
- each FAIL line, with every step of its trace mapped to a code event and `file:line`;
|
|
61
|
+
- for each counter-example, whether the shipped constants and callers can reach it;
|
|
62
|
+
- the real-code constraints the model lacks and the properties it could not express.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Copyright (c) 2026 Michael Denyer
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Usage: kani-check.sh <crate-dir> [<fully-qualified-harness> ...]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
[ "$#" -ge 1 ] || { echo "usage: kani-check.sh <crate-dir> [<harness> ...]"; exit 2; }
|
|
7
|
+
command -v python3 > /dev/null || { echo "UNAVAILABLE python3 is required; install Python 3"; exit 3; }
|
|
8
|
+
exec python3 "$(dirname "$0")/kani.py" check -- "$@"
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2026 Michael Denyer
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
"""Run the pinned Kani and validate its versioned discovery and result records."""
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import IntEnum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
STATUSES = {"Success", "Failure", "Unreachable", "Undetermined", "Satisfied", "Unsatisfiable"}
|
|
16
|
+
# Statuses that pass and statuses that refute the property, by check category; any other is inconclusive.
|
|
17
|
+
OUTCOMES = {
|
|
18
|
+
"cover": ({"Satisfied"}, {"Unsatisfiable", "Unreachable"}),
|
|
19
|
+
"unwind": ({"Success", "Unreachable"}, set()),
|
|
20
|
+
}
|
|
21
|
+
PROPERTY_OUTCOMES = ({"Success", "Unreachable"}, {"Failure"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CheckError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Unavailable(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Verdict(IntEnum):
|
|
33
|
+
"""A check's or harness's result, ordered so the worst check decides the harness."""
|
|
34
|
+
PASS = 0
|
|
35
|
+
REFUTED = 1
|
|
36
|
+
INCONCLUSIVE = 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Outcome:
|
|
41
|
+
verdict: Verdict
|
|
42
|
+
checks: int
|
|
43
|
+
failures: list
|
|
44
|
+
output: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def search_path(variable, *directories):
|
|
48
|
+
inherited = [os.environ[variable]] if variable in os.environ else []
|
|
49
|
+
return os.pathsep.join([*(str(directory) for directory in directories), *inherited])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Kani:
|
|
53
|
+
version = "0.68.0"
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
home = Path(os.environ.get("KANI_HOME", Path.home() / ".kani")).resolve()
|
|
57
|
+
bundle = home / f"kani-{self.version}"
|
|
58
|
+
remedy = (f"run cargo install --locked kani-verifier --version {self.version} "
|
|
59
|
+
"then cargo kani setup")
|
|
60
|
+
for binary in ("bin/kani-driver", "bin/kani-compiler", "bin/cbmc", "toolchain/bin/rustc", "toolchain/bin/cargo"):
|
|
61
|
+
path = bundle / binary
|
|
62
|
+
if not path.is_file() or not os.access(path, os.X_OK):
|
|
63
|
+
raise Unavailable(f"Kani {self.version} bundle is incomplete; {remedy}")
|
|
64
|
+
if any(home.glob("*.tar.gz")):
|
|
65
|
+
raise Unavailable(f"incomplete Kani setup at {home}; finish cargo kani setup before checking")
|
|
66
|
+
self.driver = bundle / "bin/kani-driver"
|
|
67
|
+
self.env = os.environ | {
|
|
68
|
+
"KANI_HOME": str(home),
|
|
69
|
+
"RUSTUP_TOOLCHAIN": str((bundle / "toolchain").resolve()),
|
|
70
|
+
"PATH": search_path("PATH", bundle / "bin", bundle / "pyroot/bin", bundle / "toolchain/bin"),
|
|
71
|
+
"PYTHONPATH": search_path("PYTHONPATH", bundle / "pyroot"),
|
|
72
|
+
}
|
|
73
|
+
# A parent cargo (cargo run, cargo test, a build script) exports its rustup toolchain's
|
|
74
|
+
# lib directory here; the bundled compiler must load only the bundled toolchain.
|
|
75
|
+
loader = "DYLD_FALLBACK_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH"
|
|
76
|
+
if loader in self.env:
|
|
77
|
+
self.env[loader] = os.pathsep.join(
|
|
78
|
+
path for path in self.env[loader].split(os.pathsep)
|
|
79
|
+
if not (Path(path).name == "lib" and Path(path).parent.parent.name == "toolchains"))
|
|
80
|
+
try:
|
|
81
|
+
done = self.run("--version")
|
|
82
|
+
except CheckError as error:
|
|
83
|
+
raise Unavailable(f"Kani {self.version} cannot run: {error}; {remedy}") from error
|
|
84
|
+
if done.returncode or not done.stdout.startswith(f"Kani Rust Verifier {self.version} (cargo plugin)\n"):
|
|
85
|
+
raise Unavailable(f"Kani {self.version} is required; {remedy}")
|
|
86
|
+
|
|
87
|
+
def run(self, *arguments, cwd=None):
|
|
88
|
+
try:
|
|
89
|
+
# The driver selects Cargo mode from argv[0]; the Cargo wrapper can auto-install.
|
|
90
|
+
return subprocess.run(
|
|
91
|
+
["cargo-kani", *arguments], executable=str(self.driver), env=self.env,
|
|
92
|
+
cwd=cwd, capture_output=True, text=True, check=False)
|
|
93
|
+
except OSError as error:
|
|
94
|
+
raise CheckError(f"cannot execute Kani: {error}") from error
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def invoke(crate, work, kani, *arguments):
|
|
98
|
+
return kani.run("--manifest-path", str(crate / "Cargo.toml"),
|
|
99
|
+
"--target-dir", str(work / "target"), *arguments, cwd=work)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def read_json(path):
|
|
103
|
+
try:
|
|
104
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
105
|
+
except (OSError, ValueError) as error:
|
|
106
|
+
raise CheckError(f"missing or invalid Kani report {path.name}: {error}") from error
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def discover(crate, work, kani):
|
|
110
|
+
"""Return the unwind bound of every proof harness in crate, by fully qualified name."""
|
|
111
|
+
if not (crate / "Cargo.toml").is_file():
|
|
112
|
+
raise CheckError(f"{crate}/Cargo.toml is missing")
|
|
113
|
+
done = invoke(crate, work, kani, "list", "--format", "json")
|
|
114
|
+
if done.returncode:
|
|
115
|
+
raise CheckError(f"Kani discovery failed:\n{done.stdout}{done.stderr}")
|
|
116
|
+
try:
|
|
117
|
+
report = read_json(work / "kani-list.json")
|
|
118
|
+
if report.get("kani-version") != kani.version or report.get("file-version") != "0.1":
|
|
119
|
+
raise CheckError("unsupported Kani discovery schema or version")
|
|
120
|
+
groups = report.get("standard-harnesses")
|
|
121
|
+
if not isinstance(groups, dict) or not all(
|
|
122
|
+
isinstance(names, list) and all(isinstance(name, str) and name for name in names)
|
|
123
|
+
for names in groups.values()):
|
|
124
|
+
raise CheckError("malformed Kani harness list")
|
|
125
|
+
names = [name for group in groups.values() for name in group]
|
|
126
|
+
if not names:
|
|
127
|
+
raise CheckError("no proof harnesses found")
|
|
128
|
+
if len(set(names)) != len(names):
|
|
129
|
+
raise CheckError("duplicate harness names; select a single crate")
|
|
130
|
+
bounds = {}
|
|
131
|
+
for metadata in (work / "target").rglob("*.kani-metadata.json"):
|
|
132
|
+
for harness in read_json(metadata).get("proof_harnesses", []):
|
|
133
|
+
name = harness.get("pretty_name")
|
|
134
|
+
if name in names:
|
|
135
|
+
bound = harness.get("attributes", {}).get("unwind_value")
|
|
136
|
+
if type(bound) is not int or bound < 1:
|
|
137
|
+
raise CheckError(f"{name} needs #[kani::unwind(N)] with N greater than zero")
|
|
138
|
+
bounds[name] = bound
|
|
139
|
+
except (TypeError, AttributeError) as error:
|
|
140
|
+
raise CheckError(f"malformed Kani discovery record: {error!r}") from error
|
|
141
|
+
if set(bounds) != set(names):
|
|
142
|
+
raise CheckError("Kani did not report unwind metadata for every harness")
|
|
143
|
+
return bounds
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def harness_result(report, kani, name, returncode):
|
|
147
|
+
"""Return the one harness result in report after checking it agrees with itself and the process."""
|
|
148
|
+
if report.get("metadata", {}).get("version") != "1.0" or report["metadata"].get("kani_version") != kani.version:
|
|
149
|
+
raise CheckError("unsupported Kani result schema or version")
|
|
150
|
+
verification = report["verification_results"]
|
|
151
|
+
summary, results = verification["summary"], verification["results"]
|
|
152
|
+
if summary["status"] != "completed" or summary["executed"] != 1 or summary["total_harnesses"] != 1:
|
|
153
|
+
raise CheckError("incomplete Kani verification")
|
|
154
|
+
if len(results) != 1 or results[0]["harness_id"] != name:
|
|
155
|
+
raise CheckError("Kani returned an unexpected harness")
|
|
156
|
+
result = results[0]
|
|
157
|
+
if not isinstance(result["checks"], list) or not result["checks"]:
|
|
158
|
+
raise CheckError("Kani returned no property checks")
|
|
159
|
+
if result["status"] not in {"Success", "Failure"}:
|
|
160
|
+
raise CheckError("unknown Kani harness status")
|
|
161
|
+
failed = result["status"] == "Failure"
|
|
162
|
+
if returncode != int(failed) or summary["failed"] != int(failed) or summary["successful"] != int(not failed):
|
|
163
|
+
raise CheckError("Kani process and result disagree")
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def classify(check):
|
|
168
|
+
category, status = check["category"], check["status"]
|
|
169
|
+
if status not in STATUSES:
|
|
170
|
+
raise CheckError(f"unknown Kani check status {status}")
|
|
171
|
+
passes, refutes = OUTCOMES.get(category, PROPERTY_OUTCOMES)
|
|
172
|
+
if status in passes:
|
|
173
|
+
return Verdict.PASS
|
|
174
|
+
return Verdict.REFUTED if status in refutes else Verdict.INCONCLUSIVE
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def describe(check):
|
|
178
|
+
location = check["location"]
|
|
179
|
+
reason = "bound too low: " if check["category"] == "unwind" else ""
|
|
180
|
+
return (f"{reason}{check['category']} {check['status']}: {check['description']} at "
|
|
181
|
+
f"{location['file']}:{location['line']}:{location['column']}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def verify(crate, work, kani, name, bound):
|
|
185
|
+
"""Check one harness; a report that is malformed or disagrees with the process is inconclusive."""
|
|
186
|
+
result_file = work / "result.json"
|
|
187
|
+
result_file.unlink(missing_ok=True)
|
|
188
|
+
try:
|
|
189
|
+
done = invoke(crate, work, kani, "--harness", name, "--exact", "--unwind", str(bound),
|
|
190
|
+
"-Z", "unstable-options", "--export-json", str(result_file),
|
|
191
|
+
"-Z", "concrete-playback", "--concrete-playback", "print",
|
|
192
|
+
"--output-format", "regular")
|
|
193
|
+
except CheckError as error:
|
|
194
|
+
return Outcome(Verdict.INCONCLUSIVE, 0, [str(error)], "")
|
|
195
|
+
output = done.stdout + done.stderr
|
|
196
|
+
try:
|
|
197
|
+
result = harness_result(read_json(result_file), kani, name, done.returncode)
|
|
198
|
+
verdicts = [(check, classify(check)) for check in result["checks"]]
|
|
199
|
+
verdict = max(verdict for _, verdict in verdicts)
|
|
200
|
+
if verdict is Verdict.PASS and result["status"] != "Success":
|
|
201
|
+
raise CheckError("Kani reports a harness failure that no check explains")
|
|
202
|
+
failures = [describe(check) for check, verdict in verdicts if verdict is not Verdict.PASS]
|
|
203
|
+
except (KeyError, TypeError, AttributeError, CheckError) as error:
|
|
204
|
+
return Outcome(Verdict.INCONCLUSIVE, 0, [f"verification did not complete: {error}"], output)
|
|
205
|
+
return Outcome(verdict, len(verdicts), failures, output)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
210
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
211
|
+
commands.add_parser("ready", help="check that the pinned Kani bundle runs")
|
|
212
|
+
check = commands.add_parser("check", help="verify the crate's proof harnesses")
|
|
213
|
+
check.add_argument("crate")
|
|
214
|
+
check.add_argument("harnesses", nargs="*")
|
|
215
|
+
arguments = parser.parse_args()
|
|
216
|
+
try:
|
|
217
|
+
kani = Kani()
|
|
218
|
+
except Unavailable as error:
|
|
219
|
+
print(f"UNAVAILABLE {error}")
|
|
220
|
+
return 3
|
|
221
|
+
if arguments.command == "ready":
|
|
222
|
+
print(f"READY Kani {kani.version}")
|
|
223
|
+
return 0
|
|
224
|
+
crate, selected = Path(arguments.crate).resolve(), arguments.harnesses
|
|
225
|
+
with tempfile.TemporaryDirectory(prefix="formal-kani-") as temporary:
|
|
226
|
+
work = Path(temporary)
|
|
227
|
+
try:
|
|
228
|
+
bounds = discover(crate, work, kani)
|
|
229
|
+
names = list(dict.fromkeys(selected)) if selected else sorted(bounds)
|
|
230
|
+
unknown = set(names) - set(bounds)
|
|
231
|
+
if unknown:
|
|
232
|
+
raise CheckError("unknown harnesses: " + ", ".join(sorted(unknown)))
|
|
233
|
+
except CheckError as error:
|
|
234
|
+
print(f"FAIL {error}")
|
|
235
|
+
return 1
|
|
236
|
+
passed = 0
|
|
237
|
+
for name in names:
|
|
238
|
+
outcome = verify(crate, work, kani, name, bounds[name])
|
|
239
|
+
ok = outcome.verdict is Verdict.PASS
|
|
240
|
+
passed += ok
|
|
241
|
+
print(f"{'PASS' if ok else 'FAIL'} {name} unwind={bounds[name]} checks={outcome.checks}"
|
|
242
|
+
+ (": " + "; ".join(outcome.failures) if outcome.failures else ""))
|
|
243
|
+
if not ok:
|
|
244
|
+
print(outcome.output.rstrip())
|
|
245
|
+
print(f"SUMMARY {passed} of {len(names)} harnesses passed")
|
|
246
|
+
return 0 if passed == len(names) else 1
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|