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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//! Trap-preservation obligations (VCR-VER-002, synth #166 / ordeal#59).
//!
//! WASM operations like `div_s`, `load`/`store`, `call_indirect`,
//! `unreachable`, and the float→int truncations (`iN.trunc_fM_s/u`) are
//! **partial**: they trap on some inputs. A validator that
//! proves only *value* equivalence over a total model cannot see a lowering that
//! **drops a trap** — deleting a trapping guard looks value-equal
//! (synth#633/#666/#665/#642/#709). This module is the thin synth-facing layer
//! over [`ordeal::trap`]: it maps synth's [`WasmOp`]s to trap conditions built
//! over synth's own [`BV`]/[`Bool`] terms, and exposes the trap-preservation
//! gate so "the trap survived the lowering" becomes a checkable obligation.
//!
//! # Boundary (unchanged from ordeal#59)
//!
//! ordeal *classifies* operand/pointer bits — it never models op *values* (those
//! are consumer-supplied) and does no floating-point arithmetic. Every builder
//! here is a `Bool`/`BV` over the existing closed QF_BV fragment. A verdict of
//! [`TrapVerdict::Preserved`] is an ordeal `Unsat` whose LRAT certificate is
//! re-checked before it is returned, so soundness is that of the normal
//! certificate-checked pipeline.
//!
//! # Which VC for which op class
//!
//! - **div/rem** — the ARM lowering carries a value (the quotient/remainder), so
//! the full [`prove_trap_equivalence`] (trap clause **and** guarded value
//! clause) applies.
//! - **load/store, call_indirect, unreachable** — synth models no memory
//! *contents* nor table *values*, so these use
//! [`prove_trap_condition_equivalence`] (trap clause only). This is the
//! ordeal#59 agreement.
//! - **float→int trunc** (`i32/i64.trunc_f{32,64}_{s,u}`, Phase B) — the trap
//! predicate is a pure bit-pattern classifier over the float OPERAND's bits
//! (NaN/±∞ exponent patterns + sign-split monotonic magnitude thresholds,
//! ordeal 0.9.1's `trap_trunc`; floats enter as BV32/BV64, no FP theory).
//! synth's QF_BV model carries no float→int *value* function, so this class
//! uses [`prove_trap_condition_equivalence`] (trap clause only) — exactly
//! the #709 soundness surface: ARM `VCVT` saturates where WASM traps, so a
//! lowering that keeps the saturated value but drops the guard is the bug
//! shape this clause rejects.
use crate;
use CheckResult;
use trap as ot;
use WasmOp;
pub use ;
/// A value paired with the condition under which the op **traps** instead of
/// producing it — the synth-`BV`/`Bool` mirror of [`ordeal::trap::DefineOrTrap`].
/// `value` is the op's result (e.g. the ARM quotient); `may_trap` is one of the
/// trap-condition builders below.
/// The type-check mode of a `call_indirect`, per its table — the synth-`BV`
/// mirror of [`ordeal::trap::TypeTrap`].
/// The operands of a `call_indirect` trap check (WASM §4.4.8) — the synth-`BV`
/// mirror of [`ordeal::trap::CallIndirect`].
/// The verdict of a trap-preservation gate.
// ---------------------------------------------------------------------------
// Trap-condition builders (WasmOp → Bool over operand bits)
// ---------------------------------------------------------------------------
/// Map a division/remainder [`WasmOp`] (i32 or i64) to its [`DivOp`]; `None`
/// for any non-div/rem op.
/// Trap condition for a div/rem op: divide-by-zero (all four) plus
/// `INT_MIN / -1` signed overflow (`div_s` ONLY). The width is taken from
/// `dividend` — pass 32-bit terms for i32 ops, 64-bit for i64.
///
/// # ordeal 0.9.1 divergence (upstream bug, reported as ordeal#72)
///
/// `ordeal::trap::trap_div(DivOp::RemS)` includes the `INT_MIN / -1`
/// overflow clause, but WASM Core §4.4.1 `irem_s` does NOT trap there — it
/// returns 0 (only `idiv_s` traps on overflow). synth's own models agree
/// (`I32.rems` in `coq/Synth/Common/Integers.v` carries no overflow guard;
/// the shipped `rem_s` lowering emits only the ÷0 guard). The divergence was
/// invisible while BOTH sides of the VC used the builder (consistent
/// wrongness); the #166 derived-ARM-trap gate exposed it by rejecting the
/// CORRECT shipped `rem_s` lowering with an INT_MIN/-1 counterexample. Until
/// the ordeal#72 fix ships, `RemS` is built through the zero-only path
/// (`RemU`'s trap condition — the ÷0 test is a bit-pattern equality, so
/// signedness does not change it).
/// Map a float→int truncation [`WasmOp`] to its
/// `(float format, integer target, signedness)` triple; `None` for any
/// non-trunc op. Covers all eight trunc variants synth's decoder produces
/// (`i64.trunc_f32_s/u` gained `WasmOp` variants + an ARM lowering in #869).
/// Trap condition for `iN.trunc_fM_s/u` (WASM float→int truncation, #709):
/// `NaN ∨ ±∞ ∨ out-of-range` classified purely over the float operand's
/// **bit pattern** (`bits` is the BV32/BV64 the float travels as — no FP
/// theory). Pass the triple from [`trunc_op`]. `bits` must be exactly
/// `fmt.total_bits()` wide (32 for f32, 64 for f64) — a width mismatch is an
/// internal bug, so it panics loud rather than returning an ill-sorted term.
/// Trap condition for `unreachable`: an unconditional trap.
/// Trap condition for an OOB `load`/`store`: a `size`-byte access at `addr`
/// exceeds `mem_bound` (`addr + size >u mem_bound`, wraparound-safe). `addr`,
/// `size`, and `mem_bound` must share a width; `mem_bound` is synth's symbolic
/// native-pointer linear-memory extent.
/// Trap condition for `call_indirect`: `bounds ∨ null-slot ∨ type`.
// ---------------------------------------------------------------------------
// The gate
// ---------------------------------------------------------------------------
/// Discharge a trap VC under the **same per-query wall-clock deadline** as the
/// [`crate::solver`] seam (#848/#849).
///
/// `ordeal::trap::prove_trap_{equivalence,condition_equivalence}` delegate to
/// the UNBOUNDED `Solver::prove_valid`, so calling them directly would leave
/// the hardest VC class in the whole validator — the 64-bit `bvsrem`/`bvurem`
/// div/rem value VCs — with no wall-clock floor at all. That is precisely the
/// #849 hang path (4–6 h CI runs). So synth builds the identical goal term
/// (`ot::trap_equivalence_vc` / `ot::trap_condition_equivalence`) and runs the
/// standard validity-as-UNSAT encoding itself, bounded by
/// `SYNTH_ORDEAL_DEADLINE_MS`.
///
/// A deadline expiry yields [`TrapVerdict::Unknown`] — conservative, never
/// `Preserved`. The certificate re-check in [`verdict`] is untouched, so the
/// bound costs completeness only.
///
/// NOTE this path is ordeal-only by construction: it does NOT go through
/// [`crate::solver::new_solver`], so the `SYNTH_SOLVER_DIFF` Z3 cross-check
/// never sees these VCs. That is why the CI "Z3 Verification" job hung in
/// #849 too — it runs the same ordeal-backed trap tests, not a Z3 solve.
/// Routing trap VCs through the differential seam is a separate follow-up.
///
/// Limitation (same as the seam): the deadline governs the SAT *search*, not
/// bit-blasting.
/// Full trap-preservation gate (trap clause **and** guarded value clause) — for
/// ops whose value synth models (div/rem). [`TrapVerdict::Preserved`] ⟹ the
/// lowering preserves both traps and values.
/// Trap-clause-only gate (`orig.may_trap ⇔ opt.may_trap`) — for ops whose value
/// synth does not model (load/store, call_indirect, unreachable, float→int
/// trunc).
/// [`TrapVerdict::Preserved`] ⟹ the lowering neither drops nor spuriously adds
/// the trap.