1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//! #935 — `synth verify` must report the rules it APPLIED AND DECLINED, not
//! only the ones it verified, so a consumer can compute a coverage
//! denominator.
//!
//! Before this gate, a module whose functions contained e.g. 29 `i32.const`
//! printed `Verification summary: 8 verified, 0 failed, 0 unknown` — reading
//! as complete while the const rule (whose Rocq theorem was Admitted, #933)
//! was skipped WITHOUT REPORTING. The vacuity class this locks out: a report
//! that only ever prints zeros for declines.
//!
//! Non-vacuity contract enforced here:
//! - the fixture has KNOWN declines (`i32.const`, `local.get`) and the report
//! must SHOW them, with non-zero counts and machine reasons;
//! - `i32.shl` must be VERIFIED, not declined (#981): its lowering is routed
//! to the #975-modelled register-shift ops, so a reappearing
//! `immediate-shift-encoding` decline is a wiring regression;
//! - the summary denominator must equal verified + failed + unknown + declined;
//! - declined register-operations must name the Rocq theorem they defer to
//! (the #933 join key: a decline is only covered if that theorem is Qed).
//!
//! Requires the `verify` feature (the SMT half must actually run):
//! `cargo test -p synth-cli --features verify --test verify_report_935`.
use std::process::Command;
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
/// Fixture with a deliberately known op census in the exported function:
/// 4× i32.const, 2× local.get (declined), plus SMT-verifiable
/// and/add/sub/shl (`i32.shl` verified since #981).
const FIXTURE_WAT: &str = r#"(module
(func (export "mix") (param i32 i32) (result i32)
local.get 0
i32.const 255
i32.and
local.get 1
i32.const 3
i32.shl
i32.add
i32.const 65536
i32.sub
i32.const 1
i32.add))
"#;
struct Artifacts {
dir: std::path::PathBuf,
stdout: String,
report: serde_json::Value,
}
fn run_verify_with_report(tag: &str) -> Artifacts {
let dir = std::env::temp_dir().join(format!(
"synth_verify_report_935_{}_{}",
tag,
std::process::id()
));
std::fs::create_dir_all(&dir).expect("create temp dir");
let wat = dir.join("fixture.wat");
let elf = dir.join("fixture.elf");
let report_path = dir.join("report.json");
std::fs::write(&wat, FIXTURE_WAT).expect("write fixture wat");
let compile = Command::new(synth())
.args([
"compile",
wat.to_str().unwrap(),
"-o",
elf.to_str().unwrap(),
"--all-exports",
])
.output()
.expect("run synth compile");
assert!(
compile.status.success(),
"compile failed:\n{}\n{}",
String::from_utf8_lossy(&compile.stdout),
String::from_utf8_lossy(&compile.stderr)
);
let verify = Command::new(synth())
.args([
"verify",
wat.to_str().unwrap(),
elf.to_str().unwrap(),
"--emit-verify-report",
report_path.to_str().unwrap(),
])
.output()
.expect("run synth verify");
let stdout = String::from_utf8_lossy(&verify.stdout).into_owned();
assert!(
verify.status.success(),
"verify failed:\n{}\n{}",
stdout,
String::from_utf8_lossy(&verify.stderr)
);
let report: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(&report_path).expect("sidecar must be written"),
)
.expect("sidecar must be valid JSON");
Artifacts {
dir,
stdout,
report,
}
}
#[test]
fn declines_are_reported_with_counts_reasons_and_rocq_join_keys() {
let a = run_verify_with_report("declines");
// Console: the summary must carry the decline half of the denominator.
assert!(
a.stdout.contains("declined"),
"summary must mention declines:\n{}",
a.stdout
);
assert!(
a.stdout.contains("Module rule inventory:"),
"module-level inventory line missing:\n{}",
a.stdout
);
// The known declines must be NAMED with counts on the console too.
assert!(
a.stdout.contains("I32Const × 4: register-operation"),
"i32.const decline (count 4) not reported:\n{}",
a.stdout
);
// Sidecar: schema + the known declines with counts, reasons, join keys.
assert_eq!(a.report["schema"], "synth-verify-v1");
let rules = a.report["functions"][0]["rules"]
.as_array()
.expect("rules array");
let find = |name: &str| {
rules
.iter()
.find(|r| r["rule"] == name)
.unwrap_or_else(|| panic!("rule {name} missing from inventory"))
};
let konst = find("I32Const");
assert_eq!(konst["status"], "declined");
assert_eq!(konst["reason"], "register-operation");
assert_eq!(konst["rocq_theorem"], "i32_const_correct");
assert_eq!(konst["count"], 4, "i32.const occurrence count must be real");
let lget = find("LocalGet");
assert_eq!(lget["status"], "declined");
assert_eq!(lget["reason"], "register-operation");
assert_eq!(lget["rocq_theorem"], "local_get_correct");
assert_eq!(lget["count"], 2);
// #981: i32.shl is no longer declined — the shift rules are routed to the
// #975-modelled `Rm<7:0>` register-shift ops and SMT-verified against the
// SHIPPED sel_dsl lowering (AND #31 + LSL (reg)). This assertion is the
// red-first gate: on the pre-#981 wiring it fails with
// status == "declined", reason == "immediate-shift-encoding".
let shl = find("I32Shl");
assert_eq!(shl["status"], "verified");
assert_eq!(shl["smt_rule"], "i32.shl → AND #31 + LSL (reg)");
// The SMT half still runs and reports — verified rules are in the SAME
// inventory (one object, one denominator).
let and = find("I32And");
assert_eq!(and["status"], "verified");
assert_eq!(and["smt_rule"], "i32.and → AND");
let _ = std::fs::remove_dir_all(&a.dir);
}
#[test]
fn summary_denominator_includes_declines_and_is_internally_consistent() {
let a = run_verify_with_report("denominator");
let s = &a.report["summary"];
let verified = s["verified"].as_u64().unwrap();
let failed = s["failed"].as_u64().unwrap();
let unknown = s["unknown"].as_u64().unwrap();
let declined = s["declined"].as_u64().unwrap();
let kinds = s["applied_rule_kinds"].as_u64().unwrap();
// The vacuity class this issue exists for: declines silently zero. The
// fixture is BUILT to decline — a zero here means the reporter went
// vacuous again.
assert!(
declined > 0,
"fixture has known declines; a zero declined count is the #935 vacuity"
);
// Denominator: every applied rule kind is accounted for, no fourth bucket.
assert_eq!(
kinds,
verified + failed + unknown + declined,
"applied_rule_kinds must equal the sum of all statuses"
);
// Cross-check the summary against the per-function records it aggregates.
let mut per_fn_declined = 0u64;
let mut per_fn_total = 0u64;
for f in a.report["functions"].as_array().unwrap() {
for r in f["rules"].as_array().unwrap() {
per_fn_total += 1;
if r["status"] == "declined" {
per_fn_declined += 1;
}
}
}
assert_eq!(declined, per_fn_declined);
assert_eq!(kinds, per_fn_total);
// Per-reason instruction totals must be non-zero for the known classes.
let by_reason = &s["declined_by_reason"];
assert!(
by_reason["register-operation"]["instructions"]
.as_u64()
.unwrap()
>= 6,
"register-operation must count the 4 consts + 2 local.gets"
);
let _ = std::fs::remove_dir_all(&a.dir);
}