Skip to content

Commit 7539831

Browse files
committed
[WIP] CBMC: Add support for using multiple solvers
Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent 6a86580 commit 7539831

205 files changed

Lines changed: 4187 additions & 722 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/cbmc/report.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@
2525
FAIL = "❌"
2626

2727
ProofResult = namedtuple(
28-
"ProofResult", ["name", "status", "current", "previous", "change"]
28+
"ProofResult", ["name", "solver", "status", "current", "previous", "change"]
2929
)
3030

31+
# Solver assigned to baseline runtime entries that lack a `solver` field.
32+
# Older baseline JSONs predate multi-solver support and mostly ran under Z3.
33+
LEGACY_DEFAULT_SOLVER = "Z3"
34+
3135

3236
def get_args():
3337
parser = argparse.ArgumentParser(description="CBMC proof results reporter")
@@ -78,11 +82,11 @@ def fetch_baseline(cfg):
7882
def render_table(rows):
7983
"""Render a markdown table from ProofResult rows."""
8084
lines = [
81-
"| Proof | Status | Current | Previous | Change |",
82-
"|-------|--------|---------|----------|--------|",
85+
"| Proof | Solver | Status | Current | Previous | Change |",
86+
"|-------|--------|--------|---------|----------|--------|",
8387
]
8488
lines.extend(
85-
f"| `{r.name}` | {r.status} | {r.current} | {r.previous} | {r.change} |"
89+
f"| `{r.name}` | {r.solver} | {r.status} | {r.current} | {r.previous} | {r.change} |"
8690
for r in rows
8791
)
8892
return lines
@@ -91,41 +95,68 @@ def render_table(rows):
9195
def classify_proof(r, baseline_runtimes, cfg):
9296
"""Classify a single proof result, returning (ProofResult, is_alert)."""
9397
name = r["name"]
94-
base = baseline_runtimes.get(name, {})
98+
solver = r.get("solver", LEGACY_DEFAULT_SOLVER)
99+
base = baseline_runtimes.get((name, solver), {})
95100
base_val, base_failed = base.get("value"), base.get("status") == "failed"
101+
base_omitted = base.get("status") == "omitted"
102+
103+
# Pair was intentionally not run.
104+
if r.get("status") == "omitted":
105+
# Was passing in the baseline, now omitted: surface as a warning.
106+
if base_val is not None and not base_failed:
107+
return (
108+
ProofResult(name, solver, WARN, "-", f"{base_val}s", "omitted"),
109+
True,
110+
)
111+
return ProofResult(name, solver, OK, "-", "-", "omitted"), False
96112

97113
if r.get("status") == "failed":
98114
prev = "failed" if base_failed else (f"{base_val}s" if base_val else "-")
99-
return ProofResult(name, FAIL, "-", prev, "-"), True
115+
if base_omitted:
116+
prev = "omitted"
117+
return ProofResult(name, solver, FAIL, "-", prev, "-"), True
100118

101119
cur_val = r["value"]
102120
if base_failed:
103-
return ProofResult(name, OK, f"{cur_val}s", "failed", "fixed"), False
121+
return (
122+
ProofResult(name, solver, OK, f"{cur_val}s", "failed", "fixed"),
123+
False,
124+
)
125+
if base_omitted:
126+
return (
127+
ProofResult(name, solver, OK, f"{cur_val}s", "omitted", "new"),
128+
False,
129+
)
104130
if base_val is None:
105-
return ProofResult(name, OK, f"{cur_val}s", "-", "new"), False
131+
return ProofResult(name, solver, OK, f"{cur_val}s", "-", "new"), False
106132

107133
ratio = cur_val / base_val if base_val > 0 else 1
108134
change = f"{(ratio - 1) * 100:+.0f}%" if base_val > 0 else "-"
109135
is_regression = cur_val >= cfg.min_runtime and ratio >= cfg.regression_threshold
110136
status = WARN if is_regression else OK
111137
return (
112-
ProofResult(name, status, f"{cur_val}s", f"{base_val}s", change),
138+
ProofResult(name, solver, status, f"{cur_val}s", f"{base_val}s", change),
113139
is_regression,
114140
)
115141

116142

117143
def compute_total_runtime(data):
118-
"""Compute total runtime from proof results."""
144+
"""Compute total runtime from proof results, ignoring failed/omitted."""
119145
if not data:
120146
return None
121147
return sum(
122-
r["value"] for r in data.get("runtimes", []) if r.get("status") != "failed"
148+
r["value"]
149+
for r in data.get("runtimes", [])
150+
if r.get("status") not in ("failed", "omitted") and "value" in r
123151
)
124152

125153

126154
def build_comment(current, baseline, cfg):
127155
"""Build the PR comment markdown."""
128-
baseline_runtimes = {r["name"]: r for r in (baseline or {}).get("runtimes", [])}
156+
baseline_runtimes = {
157+
(r["name"], r.get("solver", LEGACY_DEFAULT_SOLVER)): r
158+
for r in (baseline or {}).get("runtimes", [])
159+
}
129160
alerts, all_rows = [], []
130161

131162
for r in current.get("runtimes", []):
@@ -136,8 +167,8 @@ def build_comment(current, baseline, cfg):
136167

137168
def sort_key(r):
138169
if r.current == "-":
139-
return -1
140-
return -int(r.current.rstrip("s"))
170+
return (-1, r.name, r.solver)
171+
return (-int(r.current.rstrip("s")), r.name, r.solver)
141172

142173
all_rows.sort(key=sort_key)
143174

@@ -153,6 +184,7 @@ def sort_key(r):
153184
total_status = OK
154185
total_row = ProofResult(
155186
"**TOTAL**",
187+
"-",
156188
total_status,
157189
f"{cur_total}s",
158190
f"{base_total}s" if base_total else "-",

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
pkgs-unstable = inputs.nixpkgs-unstable.legacyPackages.${system};
2626
pkgs-2405 = inputs.nixpkgs-2405.legacyPackages.${system};
2727
util = pkgs.callPackage ./nix/util.nix {
28-
inherit (pkgs) bitwuzla z3;
28+
inherit (pkgs) bitwuzla cvc5 z3;
2929
inherit (pkgs-unstable) cbmc;
3030
# TODO: switch back to stable python3 for slothy once ortools is fixed in 25.11
3131
python3-for-slothy = pkgs-unstable.python3;
@@ -240,7 +240,7 @@
240240
pkgs-unstable = inputs.nixpkgs-unstable.legacyPackages.x86_64-linux;
241241
util = pkgs.callPackage ./nix/util.nix {
242242
inherit pkgs;
243-
inherit (pkgs) bitwuzla z3;
243+
inherit (pkgs) bitwuzla cvc5 z3;
244244
inherit (pkgs-unstable) cbmc;
245245
# TODO: switch back to stable python3 for slothy once ortools is fixed in 25.11
246246
python3-for-slothy = pkgs-unstable.python3;

nix/cbmc/default.nix

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
, fetchFromGitHub
77
, callPackage
88
, bitwuzla
9+
, cvc5
910
, ninja
1011
, z3
1112
}:
@@ -36,7 +37,8 @@ buildEnv {
3637
});
3738

3839
inherit
39-
bitwuzla# 0.8.2
40+
bitwuzla # 0.8.2
41+
cvc5 # 1.3.2
4042
ninja; # 1.13.2
4143
};
4244
}

nix/util.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) The mldsa-native project authors
33
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
44

5-
{ pkgs, cbmc, bitwuzla, z3, python3-for-slothy }:
5+
{ pkgs, cbmc, bitwuzla, cvc5, z3, python3-for-slothy }:
66
rec {
77
glibc-join = p: p.buildPackages.symlinkJoin {
88
name = "glibc-join";
@@ -97,7 +97,7 @@ rec {
9797
};
9898

9999
cbmc_pkgs = pkgs.callPackage ./cbmc {
100-
inherit cbmc bitwuzla z3;
100+
inherit cbmc bitwuzla cvc5 z3;
101101
};
102102

103103
valgrind_varlat = pkgs.callPackage ./valgrind { };

proofs/cbmc/H/Makefile

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ USE_FUNCTION_CONTRACTS+=mld_shake256_release
2828
APPLY_LOOP_CONTRACTS=on
2929
USE_DYNAMIC_FRAMES=1
3030

31-
# Disable any setting of EXTERNAL_SAT_SOLVER, and choose SMT backend instead
32-
EXTERNAL_SAT_SOLVER=
33-
CBMCFLAGS=--smt2
31+
# To add or override per-solver entries in the solver matrix, set the
32+
# variables below for each solver. {SOLVER} is one of the canonical
33+
# solver identifiers (Z3, BITWUZLA, CVC5), declared in Makefile.common as
34+
# CBMC_SOLVERS_ALL.
35+
#
36+
# CBMC_SOLVER_{SOLVER}_ENABLED ?= 0/1
37+
# CBMC_SOLVER_{SOLVER}_FLAGS ?= <extra CBMCFLAGS for {SOLVER}>
38+
# CBMC_SOLVER_{SOLVER}_EXT_SAT ?= <EXTERNAL_SAT_SOLVER for {SOLVER}>
39+
#
40+
# Solvers default to enabled; disable a solver explicitly by setting
41+
# CBMC_SOLVER_{SOLVER}_ENABLED = 0. The per-harness CBMC_DEFAULT_SOLVER
42+
# selects which enabled solver is used when the harness is invoked
43+
# directly (`make report` from this directory). The driver
44+
# run-cbmc-proofs.py overrides CBMC_SOLVER on the command line to fan
45+
# out across all enabled solvers.
46+
#
47+
# Solver matrix
48+
CBMC_DEFAULT_SOLVER = Z3
49+
CBMC_SOLVER_Z3_ENABLED = 1
3450

3551
FUNCTION_NAME = mld_h
3652

0 commit comments

Comments
 (0)