synth_verify/fact_spec.rs
1//! Proof-carrying specialization — VCR-PERF-002 / #494 **Phase 2**: the
2//! single-elision prototype (value-range facts ⇒ dead conditional-branch
3//! elision, the `gust_mix` clamp shape).
4//!
5//! Design source of truth: `docs/design/proof-carrying-specialization.md`
6//! ("How synth consumes facts: the per-elision proof obligation"). loom is the
7//! PROVER (its validator discharged the `wsc.facts` invariants upstream);
8//! synth is a CONDITIONAL optimizer: it never re-derives a fact — it proves
9//! the correctness of its OWN transformation *given* the fact, per elision
10//! site, BEFORE emission, through the ordeal-backed [`BvSolver`]
11//! (certificate-checked pure-Rust QF_BV; every `Unsat` verdict carries an
12//! LRAT proof validated by the trusted `ordeal-lrat` checker before it is
13//! reported).
14//!
15//! # The transformation
16//!
17//! Working on the decoded [`WasmOp`] stream (backend-agnostic — the rewritten
18//! stream feeds whichever selector the driver picks), the pass walks the
19//! function top-level with a symbolic state (stack + locals as QF_BV terms via
20//! the existing [`WasmSemantics`] encoder) and, at every no-`else`
21//! `if … end`, discharges the obligation
22//!
23//! ```text
24//! premise P (every value-range fact reached so far, asserted as a hypothesis)
25//! obligation UNSAT( P ∧ cond ≠ 0 )
26//! ```
27//!
28//! `UNSAT(P ∧ cond ≠ 0)` implies the design doc's
29//! `UNSAT(P ∧ general_lowering(x) ≠ specialized_lowering(x))`: a no-`else`
30//! `if` whose condition is 0 on every P-admissible input never executes its
31//! body, and wasm validation forces a no-`else` `if` blocktype to have
32//! identical param/result types, so the not-taken path is the identity — the
33//! general and specialized lowerings agree on every input P admits. The
34//! stronger query is deliberately used because it is decided in the pure
35//! QF_BV fragment with no control-flow encoding.
36//!
37//! - **UNSAT (certificate-checked)** → the elision is ADMITTED: the
38//! condition-producing op slice (proven pure and contiguous) plus the whole
39//! `if … end` region are deleted; the certificate line is logged per
40//! function (the evidence trail).
41//! - **Sat / Unknown / conflict-budget exceeded / any shape outside the
42//! tracked fragment** → **DECLINE LOUDLY**: the general lowering is
43//! emitted. There is no silent wrong-code path; the conservative fallback
44//! is today's codegen.
45//!
46//! # Soundness of the symbolic tracking (over-approximation discipline)
47//!
48//! * Values produced by ops outside the tracked i32 fragment never enter the
49//! state: the walk STOPS at the first untracked op (no further elisions in
50//! the function; everything already admitted was justified independently).
51//! * Locals are seeded as fresh unconstrained variables (a superset of both
52//! parameter values and the zero-init of non-param locals — sound).
53//! * A DECLINED `if` region may or may not execute: every local it assigns
54//! (at any nesting depth) is havocked to a fresh variable, and its block
55//! results are pushed as fresh variables.
56//! * An ADMITTED `if` region provably never executes, so state is unchanged.
57//! * `div`/`rem` (i32 AND i64) are tracked since Phase 2b (#494
58//! divisor-nonzero), but NEVER deleted: they can trap, and a deleted
59//! condition slice must be effect-free under ALL inputs, not just
60//! P-admissible ones — so a div result always carries `start = None`
61//! (non-erasable) and its value is havocked to a fresh variable. What
62//! Phase 2b adds is per-site TRAP-GUARD elision marks consumed by the
63//! direct selector (see "The div/rem guard obligations" below).
64//!
65//! # The div/rem guard obligations (Phase 2b, #494 divisor-nonzero)
66//!
67//! A `div`/`rem` lowering carries up to TWO trap guards, and they fall to two
68//! INDEPENDENT obligations — this is the #633/#634 two-guard distinction:
69//!
70//! ```text
71//! divide-by-zero guard (div_u/div_s/rem_u/rem_s, i32+i64):
72//! UNSAT( P ∧ divisor == 0 )
73//! INT_MIN/-1 overflow guard (div_s ONLY; rem_s(INT_MIN,-1)==0 never traps):
74//! UNSAT( P ∧ dividend == INT_MIN ∧ divisor == -1 )
75//! ```
76//!
77//! A divisor-nonzero fact (kind 3) discharges the first but NOT the second —
78//! `divisor ≠ 0` does not exclude `divisor == -1`, so the overflow guard is
79//! RETAINED unless the premises independently prove the second obligation
80//! (e.g. a value-range fact `divisor ∈ [1, N]` proves both). Each discharged
81//! obligation becomes a per-site elision mark ([`FactSpecResult::elide_div_zero`]
82//! / [`FactSpecResult::elide_div_ovf`], indices into the RETURNED stream);
83//! the driver threads them to the direct selector, which omits exactly that
84//! guard. Sat / Unknown / no-premise ⇒ loud decline, the guard is emitted.
85//!
86//! # The memory bounds-guard obligation (#494 × #390 `guard_bool`)
87//!
88//! Under `--safety-bounds software` every i32 linear-memory access carries a
89//! 4-instruction inline guard (`ADD ip, addr, #(offset+size-1); CMP ip, R10;
90//! BLO +0; UDF #0` — see `generate_load_with_bounds_check`). When a premise
91//! on the INDEX proves the access in-bounds, the guard is provably dead:
92//!
93//! ```text
94//! UNSAT( P ∧ trap_mem_oob(zext64(index) + offset, size, min_memory_bytes) )
95//! ```
96//!
97//! The encoding is [`crate::trap::trap_mem_oob`] (ordeal 0.9.1's shape,
98//! `addr + size >u mem_bound` wraparound-safe), posed at width 64 — the
99//! 32-bit index is ZERO-extended and the static memarg `offset` added at 64
100//! bits, so the `index + offset` sum can never wrap the obligation into a
101//! false Unsat (WASM's effective address is `i + offset` at infinite
102//! precision for a 32-bit memory). `min_memory_bytes` is the module's
103//! DECLARED minimum linear-memory size: the runtime extent (R10) is always
104//! ≥ the declared minimum, so an access proven inside the minimum is inside
105//! every reachable runtime bound. Like div/rem, a memory op can trap, so it
106//! is TRACKED but never DELETED — the discharged obligation becomes a
107//! per-site mark ([`FactSpecResult::elide_mem_bounds`]) the direct selector
108//! consumes by emitting the access WITHOUT the software guard. Sat /
109//! Unknown / no-premise / unknown memory size ⇒ loud decline, the guard is
110//! emitted. Loaded values are havocked to fresh variables (memory contents
111//! are not modeled); stores need no memory model for the same reason.
112//!
113//! # Flag gating
114//!
115//! The driver only invokes this pass when `SYNTH_FACT_SPEC` is set (default
116//! OFF) AND the module carried a parseable `wsc.facts` section. Frozen
117//! fixtures carry no facts section, so every frozen anchor is bit-identical
118//! trivially; with the flag off the pass does not run at all.
119
120use crate::solver::{CheckOutcome, new_solver};
121use crate::term::{BV, Bool};
122use crate::wasm_semantics::WasmSemantics;
123use std::collections::{HashMap, HashSet};
124use synth_core::WasmOp;
125use synth_core::wsc_facts::{FactKind, WscFact};
126
127/// Outcome of specializing one function. `ops`/`block_arity`/`kept` are only
128/// meaningful when [`changed`](Self::changed) — otherwise they echo the input.
129#[derive(Debug)]
130pub struct FactSpecResult {
131 /// The (possibly rewritten) op stream.
132 pub ops: Vec<WasmOp>,
133 /// The blocktype-arity side-table matching `ops` (one entry per
134 /// `Block`/`Loop`/`If` in op order — entries of deleted openers removed).
135 pub block_arity: Vec<(u8, u8)>,
136 /// Indices into the ORIGINAL op stream that were kept, in order. Lets the
137 /// driver filter parallel side-tables (e.g. `op_offsets` for DWARF).
138 pub kept: Vec<usize>,
139 /// One certificate line per ADMITTED elision (logged per function).
140 pub admitted: Vec<String>,
141 /// One line per LOUD DECLINE (the general lowering is emitted for these).
142 pub declined: Vec<String>,
143 /// #494 phase 2b: indices (into the RETURNED `ops` stream) of div/rem
144 /// ops whose divide-by-zero trap guard was certificate-elided
145 /// (`UNSAT(P ∧ divisor == 0)` discharged per site).
146 pub elide_div_zero: Vec<usize>,
147 /// #494 phase 2b: indices (into the RETURNED `ops` stream) of `div_s`
148 /// ops whose `INT_MIN / -1` overflow guard was certificate-elided — a
149 /// SEPARATE obligation (`UNSAT(P ∧ dividend == INT_MIN ∧ divisor == -1)`);
150 /// a divisor-nonzero fact alone never lands here (#633/#634).
151 pub elide_div_ovf: Vec<usize>,
152 /// #494 bounds-elision (#390 `guard_bool`): indices (into the RETURNED
153 /// `ops` stream) of i32 memory accesses whose `--safety-bounds software`
154 /// guard was certificate-elided —
155 /// `UNSAT(P ∧ trap_mem_oob(zext64(index) + offset, size,
156 /// min_memory_bytes))` discharged per site (ordeal 0.9.1 `trap_mem_oob`
157 /// shape, wraparound-safe 64-bit extension).
158 pub elide_mem_bounds: Vec<usize>,
159 /// True when `ops` differs from the input (at least one region deletion).
160 stream_changed: bool,
161}
162
163impl FactSpecResult {
164 /// True when the op STREAM was rewritten (region deletions). Guard-elision
165 /// marks do not rewrite the stream — check
166 /// [`elide_div_zero`](Self::elide_div_zero) /
167 /// [`elide_div_ovf`](Self::elide_div_ovf) separately.
168 pub fn changed(&self) -> bool {
169 self.stream_changed
170 }
171}
172
173/// A symbolic operand-stack slot.
174#[derive(Clone)]
175struct Val {
176 bv: BV,
177 /// Start of the contiguous, side-effect-free op range that produced this
178 /// value — `None` when the producing slice is impure (`local.tee`) or not
179 /// provably contiguous. Only a `Some` slice may be deleted.
180 start: Option<usize>,
181 /// Index of the op that (last) produced this value.
182 created: usize,
183}
184
185/// Specialize one function's op stream against its `wsc.facts` premises.
186///
187/// `block_arity` is the decoder's ordinal side-table (one `(params, results)`
188/// entry per `Block`/`Loop`/`If` in op order); `facts` is the per-function
189/// slice (`CompileConfig::current_func_facts`); `params_i64` is the declared
190/// param-width table (`CompileConfig::current_func_params_i64` — `true` ⇒
191/// param `k` is 64-bit), which fixes the symbolic width of a param
192/// `local.get` (Phase 2b tracks i64 divisors); `linear_memory_bytes` is the
193/// module's DECLARED minimum linear-memory size in bytes
194/// (`CompileConfig::linear_memory_bytes`; `0` = unknown — every memory
195/// bounds-guard obligation then declines loudly). Total: every input yields
196/// a result — inapplicable shapes surface as loud declines, never errors.
197pub fn specialize_function(
198 func_name: &str,
199 ops: &[WasmOp],
200 block_arity: &[(u8, u8)],
201 facts: &[WscFact],
202 params_i64: &[bool],
203 linear_memory_bytes: u32,
204) -> FactSpecResult {
205 let mut pass = Pass::new(
206 func_name,
207 ops,
208 block_arity,
209 facts,
210 params_i64,
211 linear_memory_bytes,
212 );
213 pass.walk();
214 pass.finish()
215}
216
217/// #494 phase 2b RED-TEAM lever (debug builds ONLY): treat a Sat verdict on
218/// the divide-by-zero guard obligation as an admit anyway. Exists so the
219/// differential oracle can DEMONSTRATE the divergence an unsound admit would
220/// cause (wasmtime traps at divisor == 0, the forced build does not) and then
221/// show the Sat-decline restoring the guard byte-identically. Compiled out of
222/// release builds; every forced admit screams in its certificate line.
223#[cfg(debug_assertions)]
224fn force_admit_unsound() -> bool {
225 std::env::var("SYNTH_FACT_SPEC_FORCE_ADMIT").is_ok_and(|v| v != "0")
226}
227
228#[cfg(not(debug_assertions))]
229fn force_admit_unsound() -> bool {
230 false
231}
232
233struct Pass<'a> {
234 func: &'a str,
235 ops: &'a [WasmOp],
236 block_arity: &'a [(u8, u8)],
237 /// op index → ordinal into `block_arity` (for `Block`/`Loop`/`If` ops).
238 opener_ordinal: HashMap<usize, usize>,
239 /// op index → signed range fact attached to that op's result (raw s64
240 /// bounds; clamped to the value's width at attach time).
241 range_facts: HashMap<usize, (i64, i64)>,
242 /// op indices carrying a divisor-nonzero fact (kind 3): `value ≠ 0`.
243 nonzero_facts: HashSet<usize>,
244 /// Declared param widths (`true` ⇒ 64-bit) — fixes `local.get` widths.
245 params_i64: &'a [bool],
246 /// The module's DECLARED minimum linear-memory size in bytes; `0` =
247 /// unknown (bounds-guard obligations decline).
248 mem_bound: u32,
249 sem: WasmSemantics,
250 stack: Vec<Val>,
251 locals: HashMap<u32, BV>,
252 /// Every fresh variable created (name order), for Sat counterexamples.
253 vars: Vec<BV>,
254 fresh: u32,
255 premises: Vec<Bool>,
256 premise_desc: Vec<String>,
257 /// Inclusive op-index ranges to delete (disjoint, ascending).
258 deletions: Vec<(usize, usize)>,
259 admitted: Vec<String>,
260 declined: Vec<String>,
261 /// #494 phase 2b: ORIGINAL op indices marked for zero-guard elision.
262 zero_marks: Vec<usize>,
263 /// #494 phase 2b: ORIGINAL op indices marked for overflow-guard elision.
264 ovf_marks: Vec<usize>,
265 /// #494 bounds-elision: ORIGINAL op indices of memory accesses whose
266 /// software bounds guard was certificate-proven dead.
267 mem_marks: Vec<usize>,
268}
269
270impl<'a> Pass<'a> {
271 fn new(
272 func: &'a str,
273 ops: &'a [WasmOp],
274 block_arity: &'a [(u8, u8)],
275 facts: &'a [WscFact],
276 params_i64: &'a [bool],
277 mem_bound: u32,
278 ) -> Self {
279 let mut opener_ordinal = HashMap::new();
280 let mut ord = 0usize;
281 for (i, op) in ops.iter().enumerate() {
282 if matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If) {
283 opener_ordinal.insert(i, ord);
284 ord += 1;
285 }
286 }
287 let mut range_facts = HashMap::new();
288 let mut nonzero_facts = HashSet::new();
289 for f in facts {
290 // Out-of-range value_id is vacuous (encoding doc's rule).
291 if (f.value_id as usize) >= ops.len() {
292 continue;
293 }
294 match f.kind {
295 FactKind::ValueRange { lo, hi } => {
296 // Raw s64 bounds; clamped to the value's width when the
297 // walk attaches the premise. An inverted bound is vacuous.
298 if lo <= hi {
299 range_facts.insert(f.value_id as usize, (lo, hi));
300 }
301 }
302 // #494 phase 2b: divisor-nonzero (kind 3) — `value ≠ 0`.
303 FactKind::DivisorNonZero => {
304 nonzero_facts.insert(f.value_id as usize);
305 }
306 _ => {}
307 }
308 }
309 Self {
310 func,
311 ops,
312 block_arity,
313 opener_ordinal,
314 range_facts,
315 nonzero_facts,
316 params_i64,
317 mem_bound,
318 // No memory model needed: loaded values are havocked to fresh
319 // variables (only the ACCESS BOUND is reasoned about, never the
320 // contents), so stores need no modeling either.
321 sem: WasmSemantics::new_with_memory(Vec::new()),
322 stack: Vec::new(),
323 locals: HashMap::new(),
324 vars: Vec::new(),
325 fresh: 0,
326 premises: Vec::new(),
327 premise_desc: Vec::new(),
328 deletions: Vec::new(),
329 admitted: Vec::new(),
330 declined: Vec::new(),
331 zero_marks: Vec::new(),
332 ovf_marks: Vec::new(),
333 mem_marks: Vec::new(),
334 }
335 }
336
337 fn fresh_var(&mut self, name: String) -> BV {
338 let v = BV::new_const(name, 32);
339 self.vars.push(v.clone());
340 v
341 }
342
343 fn local_bv(&mut self, idx: u32) -> BV {
344 if let Some(bv) = self.locals.get(&idx) {
345 return bv.clone();
346 }
347 // A not-yet-seen local's width comes from the declared param table
348 // (#494 phase 2b tracks i64 divisors); non-param locals default to
349 // 32 bits — an i64 op reading one fails the width check and declines.
350 let width = if self.params_i64.get(idx as usize).copied().unwrap_or(false) {
351 64
352 } else {
353 32
354 };
355 let v = BV::new_const(format!("fs_l{idx}"), width);
356 self.vars.push(v.clone());
357 self.locals.insert(idx, v.clone());
358 v
359 }
360
361 /// Attach the premises of every fact naming op `i`'s result, at the
362 /// value's own width.
363 fn attach_fact(&mut self, i: usize, bv: &BV) {
364 let width = bv.get_size();
365 if let Some(&(lo, hi)) = self.range_facts.get(&i) {
366 // Clamp the s64 bound to the value's width (the phase-2 rule for
367 // 32-bit values; 64-bit values take the bound verbatim). A bound
368 // that inverts after clamping is impossible for a genuine value
369 // of this width — fact validity is loom's obligation (trust
370 // split), so we keep the phase-2 clamp semantics unchanged.
371 let (lo, hi) = if width == 32 {
372 (
373 lo.clamp(i64::from(i32::MIN), i64::from(i32::MAX)),
374 hi.clamp(i64::from(i32::MIN), i64::from(i32::MAX)),
375 )
376 } else {
377 (lo, hi)
378 };
379 let lo_bv = BV::from_i64(lo, width);
380 let hi_bv = BV::from_i64(hi, width);
381 let p = Bool::and(&[&bv.bvsge(&lo_bv), &bv.bvsle(&hi_bv)]);
382 self.premises.push(p);
383 self.premise_desc
384 .push(format!("value(op#{i}) ∈ [{lo}, {hi}] (signed, i{width})"));
385 }
386 if self.nonzero_facts.contains(&i) {
387 // #494 phase 2b: divisor-nonzero (kind 3).
388 let p = bv.ne(BV::from_i64(0, width));
389 self.premises.push(p);
390 self.premise_desc
391 .push(format!("value(op#{i}) ≠ 0 (i{width})"));
392 }
393 }
394
395 fn push(&mut self, bv: BV, start: Option<usize>, created: usize) {
396 self.stack.push(Val { bv, start, created });
397 }
398
399 /// Find the matching `End` for the opener at `i`; also reports whether a
400 /// top-level `Else` occurs. `None` = malformed nesting (stop the walk).
401 fn matching_end(&self, i: usize) -> Option<(usize, bool)> {
402 let mut depth = 0usize;
403 let mut has_else = false;
404 for (j, op) in self.ops.iter().enumerate().skip(i + 1) {
405 match op {
406 WasmOp::Block | WasmOp::Loop | WasmOp::If => depth += 1,
407 WasmOp::Else if depth == 0 => has_else = true,
408 WasmOp::End => {
409 if depth == 0 {
410 return Some((j, has_else));
411 }
412 depth -= 1;
413 }
414 _ => {}
415 }
416 }
417 None
418 }
419
420 /// Continuation after a DECLINED `if` region `[i..=end]`: the body may or
421 /// may not run, so havoc every local it assigns (any depth) and model its
422 /// block results as fresh variables.
423 fn havoc_region(&mut self, i: usize, end: usize, arity: (u8, u8)) {
424 let ops = self.ops;
425 for op in &ops[i + 1..end] {
426 if let WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) = op {
427 let n = self.fresh;
428 self.fresh += 1;
429 let v = self.fresh_var(format!("fs_h{n}"));
430 self.locals.insert(*idx, v);
431 }
432 }
433 for _ in 0..arity.0 {
434 self.stack.pop();
435 }
436 for k in 0..arity.1 {
437 let n = self.fresh;
438 self.fresh += 1;
439 let v = self.fresh_var(format!("fs_r{n}_{k}"));
440 self.push(v, None, end);
441 }
442 }
443
444 fn decline(&mut self, msg: String) {
445 self.declined
446 .push(format!("{}: {} — general lowering emitted", self.func, msg));
447 }
448
449 /// #494 phase 2b: discharge the per-site div/rem trap-guard obligations
450 /// for the op at `i` (`op_name`, operand width `width`, dividend `a`,
451 /// divisor `b`), recording elision marks for the lowering. TWO independent
452 /// obligations (the #633/#634 two-guard distinction):
453 ///
454 /// - zero guard (every div/rem): `UNSAT(P ∧ divisor == 0)`;
455 /// - overflow guard (`div_s` only): `UNSAT(P ∧ dividend == INT_MIN ∧
456 /// divisor == -1)` — divisor-nonzero alone NEVER discharges this.
457 ///
458 /// Sat / Unknown / no-premise ⇒ loud decline; the guard is emitted.
459 fn try_elide_div_guards(&mut self, i: usize, op_name: &str, is_div_s: bool, a: &Val, b: &Val) {
460 let width = b.bv.get_size();
461 if self.premises.is_empty() {
462 self.decline(format!(
463 "op#{i} {op_name} — no premise reaches this site; both trap guards retained"
464 ));
465 return;
466 }
467 // Obligation 1: the divide-by-zero guard.
468 let mut solver = new_solver();
469 for p in &self.premises {
470 solver.assert(p);
471 }
472 solver.assert(&b.bv.eq(BV::from_i64(0, width)));
473 match solver.check() {
474 CheckOutcome::Unsat => {
475 self.zero_marks.push(i);
476 self.admitted.push(format!(
477 "{}: op#{i} {op_name} — divide-by-zero guard elided: UNSAT(P ∧ divisor == 0) via {} (certificate-checked QF_BV; every Unsat carries an LRAT proof validated by ordeal-lrat); P = {{{}}}; divisor = {}",
478 self.func,
479 solver.name(),
480 self.premise_desc.join(" ∧ "),
481 b.bv,
482 ));
483 }
484 CheckOutcome::Sat => {
485 let cex = self.counterexample(solver.as_ref());
486 if force_admit_unsound() {
487 // RED-TEAM lever (debug builds only): admit the Sat site
488 // anyway so the differential oracle can demonstrate the
489 // divergence. Screams, and still logs the model.
490 self.zero_marks.push(i);
491 self.admitted.push(format!(
492 "{}: op#{i} {op_name} — divide-by-zero guard elided by UNSOUND FORCED ADMIT (SYNTH_FACT_SPEC_FORCE_ADMIT, red-team oracle lever, debug builds only) — obligation was Sat (counterexample: {cex}); NEVER use in production",
493 self.func,
494 ));
495 } else {
496 self.decline(format!(
497 "op#{i} {op_name} — zero-guard obligation Sat (divisor can be 0 under P; counterexample: {cex}); guard retained"
498 ));
499 }
500 }
501 CheckOutcome::Unknown(reason) => {
502 self.decline(format!(
503 "op#{i} {op_name} — zero-guard obligation Unknown ({reason}); conservative decline, guard retained"
504 ));
505 }
506 }
507 // Obligation 2: the INT_MIN/-1 overflow guard — div_s only, and a
508 // SEPARATE proof (#633/#634): divisor ≠ 0 does not exclude -1.
509 if !is_div_s {
510 return;
511 }
512 let int_min = if width == 64 {
513 i64::MIN
514 } else {
515 i64::from(i32::MIN)
516 };
517 let mut solver = new_solver();
518 for p in &self.premises {
519 solver.assert(p);
520 }
521 solver.assert(&a.bv.eq(BV::from_i64(int_min, width)));
522 solver.assert(&b.bv.eq(BV::from_i64(-1, width)));
523 match solver.check() {
524 CheckOutcome::Unsat => {
525 self.ovf_marks.push(i);
526 self.admitted.push(format!(
527 "{}: op#{i} {op_name} — INT{width}_MIN/-1 overflow guard elided: UNSAT(P ∧ dividend == INT{width}_MIN ∧ divisor == -1) via {} (certificate-checked QF_BV; every Unsat carries an LRAT proof validated by ordeal-lrat); P = {{{}}}",
528 self.func,
529 solver.name(),
530 self.premise_desc.join(" ∧ "),
531 ));
532 }
533 CheckOutcome::Sat => {
534 let cex = self.counterexample(solver.as_ref());
535 self.decline(format!(
536 "op#{i} {op_name} — overflow-guard obligation Sat (dividend == INT{width}_MIN with divisor == -1 is possible under P; counterexample: {cex}); the #633 overflow guard is RETAINED — a divisor-nonzero premise alone never elides it"
537 ));
538 }
539 CheckOutcome::Unknown(reason) => {
540 self.decline(format!(
541 "op#{i} {op_name} — overflow-guard obligation Unknown ({reason}); conservative decline, the #633 overflow guard is RETAINED"
542 ));
543 }
544 }
545 }
546
547 /// #494 bounds-elision (#390 `guard_bool`): discharge the software
548 /// bounds-guard obligation for the i32 memory access at `i` (`op_name`,
549 /// byte width `access_size`, static memarg `offset`, index value `addr`),
550 /// recording an elision mark for the lowering:
551 ///
552 /// ```text
553 /// UNSAT( P ∧ trap_mem_oob(zext64(addr) + offset, access_size,
554 /// min_memory_bytes) )
555 /// ```
556 ///
557 /// The trap condition is [`crate::trap::trap_mem_oob`] (ordeal 0.9.1's
558 /// `addr + size >u mem_bound`, wraparound-safe), posed at width 64: the
559 /// 32-bit index is ZERO-extended and `offset` added at 64 bits, so the
560 /// effective-address sum cannot wrap into a false Unsat — exactly WASM's
561 /// infinite-precision `i + offset`. Proving the access inside the
562 /// DECLARED minimum memory (`mem_bound`) proves it inside every runtime
563 /// extent R10 can hold (runtime size ≥ declared minimum). Sat / Unknown
564 /// / no-premise / unknown memory size ⇒ loud decline; the guard is
565 /// emitted.
566 fn try_elide_mem_bounds(
567 &mut self,
568 i: usize,
569 op_name: &str,
570 access_size: u32,
571 offset: u32,
572 addr: &Val,
573 ) {
574 if self.mem_bound == 0 {
575 self.decline(format!(
576 "op#{i} {op_name} — linear-memory size unknown (no module memory \
577 context); bounds guard retained"
578 ));
579 return;
580 }
581 if self.premises.is_empty() {
582 self.decline(format!(
583 "op#{i} {op_name} — no premise reaches this site; bounds guard retained"
584 ));
585 return;
586 }
587 // Wraparound-safe width extension: zext the 32-bit index to 64 bits,
588 // add the static offset at 64 bits (no 2^64 wrap is reachable:
589 // index < 2^32, offset < 2^32, size ≤ 8).
590 let ea = addr
591 .bv
592 .zero_ext(32)
593 .bvadd(BV::from_u64(u64::from(offset), 64));
594 let trap = crate::trap::trap_mem_oob(
595 &ea,
596 &BV::from_u64(u64::from(access_size), 64),
597 &BV::from_u64(u64::from(self.mem_bound), 64),
598 );
599 let mut solver = new_solver();
600 for p in &self.premises {
601 solver.assert(p);
602 }
603 solver.assert(&trap);
604 match solver.check() {
605 CheckOutcome::Unsat => {
606 self.mem_marks.push(i);
607 self.admitted.push(format!(
608 "{}: op#{i} {op_name} (offset={offset}, {access_size} B) — software \
609 bounds guard elided: UNSAT(P ∧ trap_mem_oob(zext64(index) + offset, \
610 size, {} B declared-min memory)) via {} (certificate-checked QF_BV; \
611 every Unsat carries an LRAT proof validated by ordeal-lrat); \
612 P = {{{}}}; index = {}",
613 self.func,
614 self.mem_bound,
615 solver.name(),
616 self.premise_desc.join(" ∧ "),
617 addr.bv,
618 ));
619 }
620 CheckOutcome::Sat => {
621 let cex = self.counterexample(solver.as_ref());
622 if force_admit_unsound() {
623 // RED-TEAM lever (debug builds only): admit the Sat site
624 // anyway so the differential oracle can demonstrate the
625 // divergence. Screams, and still logs the model.
626 self.mem_marks.push(i);
627 self.admitted.push(format!(
628 "{}: op#{i} {op_name} — bounds guard elided by UNSOUND FORCED \
629 ADMIT (SYNTH_FACT_SPEC_FORCE_ADMIT, red-team oracle lever, \
630 debug builds only) — obligation was Sat (counterexample: \
631 {cex}); NEVER use in production",
632 self.func,
633 ));
634 } else {
635 self.decline(format!(
636 "op#{i} {op_name} — bounds-guard obligation Sat (the access can \
637 exceed the {} B declared-min memory under P; counterexample: \
638 {cex}); guard retained",
639 self.mem_bound,
640 ));
641 }
642 }
643 CheckOutcome::Unknown(reason) => {
644 self.decline(format!(
645 "op#{i} {op_name} — bounds-guard obligation Unknown ({reason}); \
646 conservative decline, guard retained"
647 ));
648 }
649 }
650 }
651
652 /// Read the model back for an actionable counterexample string.
653 fn counterexample(&self, solver: &dyn crate::solver::BvSolver) -> String {
654 let cex: Vec<String> = self
655 .vars
656 .iter()
657 .filter_map(|v| {
658 let name = format!("{v}");
659 solver.value(v).map(|x| {
660 if v.get_size() == 64 {
661 format!("{name}={}", x as u64 as i64)
662 } else {
663 format!("{name}={}", x as u32 as i32)
664 }
665 })
666 })
667 .collect();
668 if cex.is_empty() {
669 "<no model>".to_string()
670 } else {
671 cex.join(", ")
672 }
673 }
674
675 /// Discharge the per-elision obligation for the no-`else` `if` at `i`
676 /// (matching `End` at `end`, condition `cond`). Returns true iff admitted.
677 fn try_elide(&mut self, i: usize, end: usize, cond: &Val) -> bool {
678 if self.premises.is_empty() {
679 self.decline(format!(
680 "op#{i} `if` — no premise reaches this site (no usable value-range fact)"
681 ));
682 return false;
683 }
684 let mut solver = new_solver();
685 for p in &self.premises {
686 solver.assert(p);
687 }
688 let taken = cond.bv.ne(BV::from_i64(0, 32));
689 solver.assert(&taken);
690 match solver.check() {
691 CheckOutcome::Unsat => {
692 let Some(start) = cond.start else {
693 // Proven dead, but the condition slice has a side effect
694 // (`local.tee`) or is not provably contiguous — deleting
695 // it could drop live work. Conservative: keep everything.
696 self.decline(format!(
697 "op#{i} `if` proven dead (UNSAT) but its condition slice is not \
698 erasable (impure or non-contiguous producer)"
699 ));
700 return false;
701 };
702 self.deletions.push((start, end));
703 self.admitted.push(format!(
704 "{}: op#{i} `if` (+condition slice) — ops [{start}..={end}] elided \
705 ({} ops): UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; \
706 every Unsat carries an LRAT proof validated by ordeal-lrat); \
707 P = {{{}}}; cond = {}",
708 self.func,
709 end - start + 1,
710 solver.name(),
711 self.premise_desc.join(" ∧ "),
712 cond.bv,
713 ));
714 true
715 }
716 CheckOutcome::Sat => {
717 // Read the model back for an actionable counterexample.
718 let cex: Vec<String> = self
719 .vars
720 .iter()
721 .filter_map(|v| {
722 let name = format!("{v}");
723 solver
724 .value(v)
725 .map(|x| format!("{name}={}", x as u32 as i32))
726 })
727 .collect();
728 self.decline(format!(
729 "op#{i} `if` — obligation Sat (branch reachable under P; \
730 counterexample: {})",
731 if cex.is_empty() {
732 "<no model>".to_string()
733 } else {
734 cex.join(", ")
735 }
736 ));
737 false
738 }
739 CheckOutcome::Unknown(reason) => {
740 self.decline(format!(
741 "op#{i} `if` — obligation Unknown ({reason}); conservative decline"
742 ));
743 false
744 }
745 }
746 }
747
748 /// Discharge the select-collapse obligation for the branchless `select`
749 /// at op `i`. The operands are `(val1, val2, cond)` in wasm select order
750 /// (`val1` deepest, `cond` on top); the runtime result is
751 /// `(cond != 0) ? val1 : val2`. When a value-range premise pins the
752 /// condition CONSTANT under `P` the select collapses to one operand — the
753 /// branchless sibling of the Phase-2 no-else `if` elision, and the shape
754 /// gust_mix's `clamp` actually uses (`max`/`min` via `select`).
755 ///
756 /// Two mutually exclusive obligations, both certificate-checked QF_BV.
757 /// `UNSAT(P ∧ cond ≠ 0)` means `cond` is always 0, so the result is `val2`
758 /// (delete `val1`'s producer slice plus the condition-slice-through-
759 /// `select`). `UNSAT(P ∧ cond == 0)` means `cond` is always non-zero, so
760 /// the result is `val1` (delete the `val2`+condition+`select` contiguous
761 /// slice). Anything else (Sat both ways = genuinely non-constant; Unknown;
762 /// an impure/non-contiguous erasable slice) DECLINES LOUDLY and the general
763 /// branchless select stands. Returns the surviving operand's `Val` on
764 /// admit, `None` on decline.
765 fn try_collapse_select(&mut self, i: usize, val1: &Val, val2: &Val, cond: &Val) -> Option<Val> {
766 if self.premises.is_empty() {
767 self.decline(format!(
768 "op#{i} select — no premise reaches this site (no usable value-range fact)"
769 ));
770 return None;
771 }
772 // The condition slice must be a pure, contiguous producer ending
773 // immediately before the `select` — otherwise deleting it could drop
774 // live work (`local.tee`) or leave a gap.
775 let Some(sc) = cond.start else {
776 self.decline(format!(
777 "op#{i} select — condition slice is impure or non-contiguous (not erasable)"
778 ));
779 return None;
780 };
781 if cond.created + 1 != i {
782 self.decline(format!(
783 "op#{i} select — condition is not produced immediately before the select \
784 (non-contiguous)"
785 ));
786 return None;
787 }
788
789 // Obligation B first (the clamp shape: the guard condition is
790 // constant-FALSE, so the identity operand `val2` survives).
791 let mut solver = new_solver();
792 for p in &self.premises {
793 solver.assert(p);
794 }
795 solver.assert(&cond.bv.ne(BV::from_i64(0, 32)));
796 match solver.check() {
797 CheckOutcome::Unsat => {
798 // cond ≡ 0 ⇒ result = val2. Delete val1's slice AND the
799 // condition-slice-through-select. val2 (kept) sits between.
800 let Some(s1) = val1.start else {
801 self.decline(format!(
802 "op#{i} select proven false-arm (UNSAT cond ≠ 0) but the val1 slice \
803 is not erasable (impure/non-contiguous) — collapse declined"
804 ));
805 return None;
806 };
807 if val1.created >= sc {
808 self.decline(format!(
809 "op#{i} select — val1 slice overlaps the condition slice; collapse \
810 declined"
811 ));
812 return None;
813 }
814 self.deletions.push((s1, val1.created));
815 self.deletions.push((sc, i));
816 self.admitted.push(format!(
817 "{}: op#{i} select — collapsed to the false-arm (val2): \
818 UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; every Unsat \
819 carries an LRAT proof validated by ordeal-lrat); deleted val1 slice \
820 [{s1}..={}] + condition/select [{sc}..={i}]; P = {{{}}}; cond = {}",
821 self.func,
822 solver.name(),
823 val1.created,
824 self.premise_desc.join(" ∧ "),
825 cond.bv,
826 ));
827 return Some(val2.clone());
828 }
829 CheckOutcome::Sat => { /* cond can be non-zero — try obligation A */ }
830 CheckOutcome::Unknown(reason) => {
831 self.decline(format!(
832 "op#{i} select — false-arm obligation Unknown ({reason}); conservative \
833 decline"
834 ));
835 return None;
836 }
837 }
838
839 // Obligation A: cond ≡ non-zero ⇒ result = val1. Delete the
840 // val2+condition+select contiguous slice.
841 let mut solver = new_solver();
842 for p in &self.premises {
843 solver.assert(p);
844 }
845 solver.assert(&cond.bv.eq(BV::from_i64(0, 32)));
846 match solver.check() {
847 CheckOutcome::Unsat => {
848 let Some(s2) = val2.start else {
849 self.decline(format!(
850 "op#{i} select proven true-arm (UNSAT cond == 0) but the val2 slice \
851 is not erasable (impure/non-contiguous) — collapse declined"
852 ));
853 return None;
854 };
855 if val2.created + 1 != sc {
856 self.decline(format!(
857 "op#{i} select — val2 slice not adjacent to the condition slice \
858 (non-contiguous); collapse declined"
859 ));
860 return None;
861 }
862 // [s2 ..= i] is one contiguous pure range (val2 + cond + select).
863 self.deletions.push((s2, i));
864 self.admitted.push(format!(
865 "{}: op#{i} select — collapsed to the true-arm (val1): \
866 UNSAT(P ∧ cond == 0) via {} (certificate-checked QF_BV; every Unsat \
867 carries an LRAT proof validated by ordeal-lrat); deleted val2/condition/\
868 select [{s2}..={i}]; P = {{{}}}; cond = {}",
869 self.func,
870 solver.name(),
871 self.premise_desc.join(" ∧ "),
872 cond.bv,
873 ));
874 Some(val1.clone())
875 }
876 CheckOutcome::Sat => {
877 let cex = self.counterexample(solver.as_ref());
878 self.decline(format!(
879 "op#{i} select — condition is not constant under P (both arms reachable; \
880 counterexample: {cex}); branchless select retained"
881 ));
882 None
883 }
884 CheckOutcome::Unknown(reason) => {
885 self.decline(format!(
886 "op#{i} select — true-arm obligation Unknown ({reason}); conservative \
887 decline"
888 ));
889 None
890 }
891 }
892 }
893
894 /// #494 Phase 3+: redundant-mask (narrowing) elision. When the value `a`
895 /// is proven narrow enough that `a & b == a` on every P-admissible input
896 /// (`UNSAT(P ∧ (a & b) ≠ a)`), the `i32.and` is the identity — its mask
897 /// operand `b` and the `and` op itself are deleted and `a` flows through
898 /// unchanged. This is the "known-narrow-value ⇒ drop the mask" class: a
899 /// dissolved primitive gives LLVM no range on the value, so it keeps the
900 /// `and`/`uxtb`; the fact makes the mask provably dead. Only the common
901 /// wasm order (`value ; const-mask ; and`, keep `a`) is handled — anything
902 /// else DECLINES LOUDLY and the general `and` stands. Returns the surviving
903 /// operand's `Val` on admit (deletion recorded), `None` on decline.
904 fn try_elide_mask(&mut self, i: usize, a: &Val, b: &Val, result_bv: &BV) -> Option<Val> {
905 if self.premises.is_empty() {
906 self.decline(format!(
907 "op#{i} i32.and — no premise reaches this site (no usable value-range fact)"
908 ));
909 return None;
910 }
911 // The mask operand `b` must be a pure, contiguous producer sitting
912 // immediately between `a`'s slice and the `and` — otherwise deleting
913 // it could drop live work (`local.tee`) or leave a gap.
914 let Some(sb) = b.start else {
915 self.decline(format!(
916 "op#{i} i32.and — mask slice is impure or non-contiguous (not erasable)"
917 ));
918 return None;
919 };
920 if a.created + 1 != sb || b.created + 1 != i {
921 self.decline(format!(
922 "op#{i} i32.and — mask operand is not produced immediately before the and \
923 (non-contiguous)"
924 ));
925 return None;
926 }
927 let mut solver = new_solver();
928 for p in &self.premises {
929 solver.assert(p);
930 }
931 // UNSAT(P ∧ (value & mask) ≠ value) ⇒ the mask never clears a bit of
932 // the value under P ⇒ the `and` is the identity, so the general and
933 // the specialized lowerings (result = value) agree on every P input.
934 solver.assert(&result_bv.ne(&a.bv));
935 match solver.check() {
936 CheckOutcome::Unsat => {
937 self.deletions.push((sb, i));
938 self.admitted.push(format!(
939 "{}: op#{i} i32.and — redundant mask elided (value proven narrow): \
940 UNSAT(P ∧ (value & mask) ≠ value) via {} (certificate-checked QF_BV; \
941 every Unsat carries an LRAT proof validated by ordeal-lrat); deleted \
942 mask/and [{sb}..={i}]; P = {{{}}}; value = {}",
943 self.func,
944 solver.name(),
945 self.premise_desc.join(" ∧ "),
946 a.bv,
947 ));
948 Some(a.clone())
949 }
950 CheckOutcome::Sat => {
951 let cex = self.counterexample(solver.as_ref());
952 self.decline(format!(
953 "op#{i} i32.and — mask is not redundant under P (value can carry a bit \
954 outside the mask; counterexample: {cex}); general and retained"
955 ));
956 None
957 }
958 CheckOutcome::Unknown(reason) => {
959 self.decline(format!(
960 "op#{i} i32.and — mask-redundancy obligation Unknown ({reason}); \
961 conservative decline, general and retained"
962 ));
963 None
964 }
965 }
966 }
967
968 /// #494 Phase 3+ (beyond-parity): constant-divisor `i32.rem_u` IDENTITY
969 /// elision. When the divisor is a *literal* `i32.const C` with `C != 0` and
970 /// the dividend `a` is proven small enough that `a rem_u C == a` on every
971 /// P-admissible input (`UNSAT(P ∧ (a bvurem C) ≠ a)`), the whole `rem_u`
972 /// dissolves to the identity — its const-divisor operand and the op itself
973 /// are deleted and `a` flows through unchanged.
974 ///
975 /// This is the ONE total subset of the "div/rem never deleted" rule: WASM
976 /// `i32.rem_u` traps ONLY on divisor == 0, and rem has no INT_MIN/-1
977 /// overflow (that's `div_s`). A LITERAL nonzero constant divisor makes the
978 /// op unconditionally total — no-trap comes from the literal directly, NOT
979 /// from P — so unlike the variable-divisor path (which only elides the
980 /// *guard* and keeps the op), the whole effect-free op is deletable exactly
981 /// like the redundant mask. A variable divisor proven nonzero would NOT
982 /// qualify (its zero-guard is a control-flow effect); only a syntactic
983 /// `i32.const C, C != 0` is accepted.
984 ///
985 /// clang cannot do this: a dissolved primitive gives LLVM no range on the
986 /// dividend, so `-Os` lowers `x % C` (non-pow2 C) to the full
987 /// reciprocal-multiply-subtract sequence (movw+movt+umull+lsr+mov+mls,
988 /// ~22 B on Thumb-2); the WASM-level fact makes the entire sequence dead.
989 ///
990 /// Only the common wasm order (`dividend ; i32.const C ; rem_u`) is handled;
991 /// anything else DECLINES LOUDLY and the general div/rem path stands.
992 /// Returns the surviving dividend's `Val` on admit (deletion recorded),
993 /// `None` on decline.
994 fn try_elide_const_rem(&mut self, i: usize, a: &Val, b: &Val, result_bv: &BV) -> Option<Val> {
995 // Divisor MUST be a syntactic literal `i32.const C` with `C != 0` — the
996 // no-trap obligation is discharged by the literal itself, never by P.
997 let WasmOp::I32Const(c) = self.ops[b.created] else {
998 self.decline(format!(
999 "op#{i} i32.rem_u — divisor is not a literal i32.const (a variable divisor \
1000 carries a trapping zero-guard; the whole op is not effect-free — general \
1001 rem_u retained)"
1002 ));
1003 return None;
1004 };
1005 if c == 0 {
1006 self.decline(format!(
1007 "op#{i} i32.rem_u — literal divisor is 0 (traps unconditionally); general \
1008 rem_u retained"
1009 ));
1010 return None;
1011 }
1012 if self.premises.is_empty() {
1013 self.decline(format!(
1014 "op#{i} i32.rem_u — no premise reaches this site (no usable value-range fact \
1015 on the dividend)"
1016 ));
1017 return None;
1018 }
1019 // The const-divisor operand `b` must be a pure, contiguous producer
1020 // sitting immediately between `a`'s slice and the `rem_u` — otherwise
1021 // deleting it could drop live work or leave a gap. (An `i32.const`
1022 // always has a pure single-op slice; this mirrors try_elide_mask.)
1023 let Some(sb) = b.start else {
1024 self.decline(format!(
1025 "op#{i} i32.rem_u — const-divisor slice is impure or non-contiguous \
1026 (not erasable)"
1027 ));
1028 return None;
1029 };
1030 if a.created + 1 != sb || b.created + 1 != i {
1031 self.decline(format!(
1032 "op#{i} i32.rem_u — const divisor is not produced immediately before the \
1033 rem_u (non-contiguous)"
1034 ));
1035 return None;
1036 }
1037 let mut solver = new_solver();
1038 for p in &self.premises {
1039 solver.assert(p);
1040 }
1041 // UNSAT(P ∧ (dividend bvurem C) ≠ dividend) ⇒ the modulo never changes
1042 // the dividend under P ⇒ the rem_u is the identity, so the general and
1043 // the specialized (result = dividend) lowerings agree on every P input.
1044 // `result_bv` is the real `a.bv.bvurem(C)` term (WasmSemantics), so the
1045 // check is non-vacuous.
1046 solver.assert(&result_bv.ne(&a.bv));
1047 match solver.check() {
1048 CheckOutcome::Unsat => {
1049 self.deletions.push((sb, i));
1050 self.admitted.push(format!(
1051 "{}: op#{i} i32.rem_u — constant-divisor modulo elided to identity \
1052 (dividend proven < divisor): divisor is literal i32.const {c} (≠0 ⇒ \
1053 no trap), UNSAT(P ∧ (dividend bvurem {c}) ≠ dividend) via {} \
1054 (certificate-checked QF_BV; every Unsat carries an LRAT proof validated \
1055 by ordeal-lrat); deleted const/rem_u [{sb}..={i}]; P = {{{}}}; \
1056 dividend = {}",
1057 self.func,
1058 solver.name(),
1059 self.premise_desc.join(" ∧ "),
1060 a.bv,
1061 ));
1062 Some(a.clone())
1063 }
1064 CheckOutcome::Sat => {
1065 let cex = self.counterexample(solver.as_ref());
1066 self.decline(format!(
1067 "op#{i} i32.rem_u — modulo is not the identity under P (dividend can \
1068 reach or exceed the divisor {c}; counterexample: {cex}); general rem_u \
1069 retained"
1070 ));
1071 None
1072 }
1073 CheckOutcome::Unknown(reason) => {
1074 self.decline(format!(
1075 "op#{i} i32.rem_u — identity obligation Unknown ({reason}); conservative \
1076 decline, general rem_u retained"
1077 ));
1078 None
1079 }
1080 }
1081 }
1082
1083 fn walk(&mut self) {
1084 if self.range_facts.is_empty() && self.nonzero_facts.is_empty() {
1085 self.decline(
1086 "no usable value-range or divisor-nonzero fact targets this function".to_string(),
1087 );
1088 return;
1089 }
1090 let ops = self.ops;
1091 let mut i = 0usize;
1092 while i < ops.len() {
1093 let op = &ops[i];
1094 match op {
1095 WasmOp::Nop => {}
1096 WasmOp::I32Const(v) => {
1097 let bv = self.sem.encode_op(&WasmOp::I32Const(*v), &[]);
1098 self.attach_fact(i, &bv);
1099 self.push(bv, Some(i), i);
1100 }
1101 // #494 phase 2b: tracked so an i64 divisor term can be built
1102 // (the i64 fragment is const/local/div-rem only — anything
1103 // else stops the walk as before).
1104 WasmOp::I64Const(v) => {
1105 let bv = self.sem.encode_op(&WasmOp::I64Const(*v), &[]);
1106 self.attach_fact(i, &bv);
1107 self.push(bv, Some(i), i);
1108 }
1109 WasmOp::LocalGet(idx) => {
1110 let bv = self.local_bv(*idx);
1111 self.attach_fact(i, &bv);
1112 self.push(bv, Some(i), i);
1113 }
1114 WasmOp::LocalSet(idx) => {
1115 let Some(v) = self.stack.pop() else {
1116 self.decline(format!("op#{i} local.set on empty symbolic stack"));
1117 return;
1118 };
1119 self.locals.insert(*idx, v.bv);
1120 }
1121 WasmOp::LocalTee(idx) => {
1122 let Some(top) = self.stack.last_mut() else {
1123 self.decline(format!("op#{i} local.tee on empty symbolic stack"));
1124 return;
1125 };
1126 // The tee is a side effect: its slice must never be
1127 // deleted as a "pure condition producer".
1128 top.start = None;
1129 top.created = i;
1130 let bv = top.bv.clone();
1131 self.locals.insert(*idx, bv.clone());
1132 self.attach_fact(i, &bv);
1133 }
1134 WasmOp::Drop => {
1135 if self.stack.pop().is_none() {
1136 self.decline(format!("op#{i} drop on empty symbolic stack"));
1137 return;
1138 }
1139 }
1140 WasmOp::I32Eqz => {
1141 let Some(a) = self.stack.pop() else {
1142 self.decline(format!("op#{i} unary op on empty symbolic stack"));
1143 return;
1144 };
1145 if a.bv.get_size() != 32 {
1146 self.decline(format!("op#{i} i32.eqz on a non-32-bit operand"));
1147 return;
1148 }
1149 let bv = self.sem.encode_op(op, &[a.bv]);
1150 self.attach_fact(i, &bv);
1151 let start = a.start.filter(|_| a.created + 1 == i);
1152 self.push(bv, start, i);
1153 }
1154 // #494 Phase 3+: `i32.and` — tracked like the other trap-free
1155 // binops, but additionally attempts the redundant-mask elision
1156 // (a proven-narrow value makes the mask the identity). A
1157 // NON-eliding `and` pushes the EXACT `(bv, start, i)` the
1158 // general binop arm below produces — so flag-off stays
1159 // byte-identical.
1160 WasmOp::I32And => {
1161 let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
1162 self.decline(format!("op#{i} binop on underflowing symbolic stack"));
1163 return;
1164 };
1165 if a.bv.get_size() != 32 || b.bv.get_size() != 32 {
1166 self.decline(format!("op#{i} i32 binop on a non-32-bit operand"));
1167 return;
1168 }
1169 let bv = self.sem.encode_op(op, &[a.bv.clone(), b.bv.clone()]);
1170 self.attach_fact(i, &bv);
1171 // The general binop arm's contiguity proof, verbatim.
1172 let start = match (a.start, b.start) {
1173 (Some(sa), Some(sb)) if a.created + 1 == sb && b.created + 1 == i => {
1174 Some(sa)
1175 }
1176 _ => None,
1177 };
1178 match self.try_elide_mask(i, &a, &b, &bv) {
1179 // Admitted: the mask/and slice was recorded for
1180 // deletion; the surviving value flows through. `start =
1181 // None` keeps the collapsed result out of any later
1182 // erasable slice (conservative, like select-collapse).
1183 Some(surviving) => self.push(surviving.bv, None, i),
1184 // Declined (loud): the general `and` stands, pushed
1185 // exactly as the general binop arm would.
1186 None => self.push(bv, start, i),
1187 }
1188 }
1189 // Tracked, trap-free i32 binops (div/rem excluded on purpose:
1190 // they can trap, and a deleted slice must be effect-free).
1191 WasmOp::I32Add
1192 | WasmOp::I32Sub
1193 | WasmOp::I32Mul
1194 | WasmOp::I32Or
1195 | WasmOp::I32Xor
1196 | WasmOp::I32Shl
1197 | WasmOp::I32ShrS
1198 | WasmOp::I32ShrU
1199 | WasmOp::I32Rotl
1200 | WasmOp::I32Rotr
1201 | WasmOp::I32Eq
1202 | WasmOp::I32Ne
1203 | WasmOp::I32LtS
1204 | WasmOp::I32LtU
1205 | WasmOp::I32LeS
1206 | WasmOp::I32LeU
1207 | WasmOp::I32GtS
1208 | WasmOp::I32GtU
1209 | WasmOp::I32GeS
1210 | WasmOp::I32GeU => {
1211 let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
1212 self.decline(format!("op#{i} binop on underflowing symbolic stack"));
1213 return;
1214 };
1215 if a.bv.get_size() != 32 || b.bv.get_size() != 32 {
1216 self.decline(format!("op#{i} i32 binop on a non-32-bit operand"));
1217 return;
1218 }
1219 let bv = self.sem.encode_op(op, &[a.bv, b.bv]);
1220 self.attach_fact(i, &bv);
1221 // Contiguity proof for the combined producer slice:
1222 // a's slice, immediately followed by b's, immediately
1223 // followed by this op. Anything else ⇒ not erasable.
1224 let start = match (a.start, b.start) {
1225 (Some(sa), Some(sb)) if a.created + 1 == sb && b.created + 1 == i => {
1226 Some(sa)
1227 }
1228 _ => None,
1229 };
1230 self.push(bv, start, i);
1231 }
1232 // #494 phase 2b: i32/i64 div/rem — TRACKED (upgrading the
1233 // phase-2 hard stop), never DELETED. The op can trap, so its
1234 // result carries `start = None` (it can never sit inside an
1235 // erasable condition slice); the walk instead discharges the
1236 // per-site guard obligations (see the module docs' two-guard
1237 // distinction) and marks the op for the lowering. Downstream
1238 // soundness: if the op traps, nothing after it executes (any
1239 // later admitted elision is vacuous on that path); if it does
1240 // not, its result is havocked to a fresh variable.
1241 // #494 Phase 3+ (beyond-parity): `i32.rem_u` — tracked like the
1242 // other div/rem ops, but additionally attempts the
1243 // constant-divisor IDENTITY elision (a literal nonzero divisor
1244 // + a proven-narrow dividend makes the modulo the identity, and
1245 // the whole effect-free op is deletable — the ONE total subset
1246 // of the "div/rem never deleted" rule). A NON-eliding rem_u
1247 // takes the EXACT general div/rem path below (guard obligations
1248 // + havoc), so flag-off stays byte-identical.
1249 WasmOp::I32RemU => {
1250 let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
1251 self.decline(format!("op#{i} div/rem on underflowing symbolic stack"));
1252 return;
1253 };
1254 if a.bv.get_size() != 32 || b.bv.get_size() != 32 {
1255 self.decline(format!(
1256 "op#{i} i32.rem_u on operands of unexpected width (symbolic widths \
1257 {}/{}, expected 32)",
1258 a.bv.get_size(),
1259 b.bv.get_size()
1260 ));
1261 return;
1262 }
1263 // The real bvurem term (WasmSemantics) — non-vacuous VC.
1264 let bv = self.sem.encode_op(op, &[a.bv.clone(), b.bv.clone()]);
1265 match self.try_elide_const_rem(i, &a, &b, &bv) {
1266 // Admitted: the const/rem_u slice was recorded for
1267 // deletion; the surviving dividend flows through.
1268 // `start = None` keeps the collapsed result out of any
1269 // later erasable slice (conservative, like the mask
1270 // and select-collapse admits).
1271 Some(surviving) => self.push(surviving.bv, None, i),
1272 // Declined (loud): fall to the EXACT general div/rem
1273 // path — discharge the guard obligations, then havoc.
1274 None => {
1275 self.try_elide_div_guards(i, "i32.rem_u", false, &a, &b);
1276 let n = self.fresh;
1277 self.fresh += 1;
1278 let v = self.fresh_var(format!("fs_d{n}"));
1279 self.attach_fact(i, &v);
1280 self.push(v, None, i);
1281 }
1282 }
1283 }
1284 // #494 phase 2b: i32/i64 div/rem — TRACKED (upgrading the
1285 // phase-2 hard stop), never DELETED. The op can trap, so its
1286 // result carries `start = None` (it can never sit inside an
1287 // erasable condition slice); the walk instead discharges the
1288 // per-site guard obligations (see the module docs' two-guard
1289 // distinction) and marks the op for the lowering.
1290 WasmOp::I32DivU
1291 | WasmOp::I32DivS
1292 | WasmOp::I32RemS
1293 | WasmOp::I64DivU
1294 | WasmOp::I64DivS
1295 | WasmOp::I64RemU
1296 | WasmOp::I64RemS => {
1297 let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
1298 self.decline(format!("op#{i} div/rem on underflowing symbolic stack"));
1299 return;
1300 };
1301 let (op_name, expect, is_div_s) = match op {
1302 WasmOp::I32DivU => ("i32.div_u", 32, false),
1303 WasmOp::I32DivS => ("i32.div_s", 32, true),
1304 WasmOp::I32RemS => ("i32.rem_s", 32, false),
1305 WasmOp::I64DivU => ("i64.div_u", 64, false),
1306 WasmOp::I64DivS => ("i64.div_s", 64, true),
1307 WasmOp::I64RemU => ("i64.rem_u", 64, false),
1308 _ => ("i64.rem_s", 64, false),
1309 };
1310 if a.bv.get_size() != expect || b.bv.get_size() != expect {
1311 self.decline(format!(
1312 "op#{i} {op_name} on operands of unexpected width (symbolic widths {}/{}, expected {expect})",
1313 a.bv.get_size(),
1314 b.bv.get_size()
1315 ));
1316 return;
1317 }
1318 self.try_elide_div_guards(i, op_name, is_div_s, &a, &b);
1319 // Havoc the result; `start = None` keeps a possibly-
1320 // trapping op out of every erasable condition slice.
1321 let n = self.fresh;
1322 self.fresh += 1;
1323 let v = self.fresh_var(format!("fs_d{n}"));
1324 self.attach_fact(i, &v);
1325 self.push(v, None, i);
1326 }
1327 // #494 bounds-elision: i32 memory LOADS — TRACKED, never
1328 // DELETED (an OOB access traps, so the op is not effect-free;
1329 // same discipline as div/rem). The walk discharges the
1330 // per-site software bounds-guard obligation and marks the op
1331 // for the lowering; the loaded value is havocked to a fresh
1332 // variable (memory contents are not modeled) with
1333 // `start = None` (a possibly-trapping op never sits inside an
1334 // erasable condition slice). Downstream soundness: if the
1335 // access traps, nothing after it executes (any later admitted
1336 // elision is vacuous on that path).
1337 WasmOp::I32Load { offset, .. }
1338 | WasmOp::I32Load8S { offset, .. }
1339 | WasmOp::I32Load8U { offset, .. }
1340 | WasmOp::I32Load16S { offset, .. }
1341 | WasmOp::I32Load16U { offset, .. } => {
1342 let Some(a) = self.stack.pop() else {
1343 self.decline(format!("op#{i} memory load on empty symbolic stack"));
1344 return;
1345 };
1346 if a.bv.get_size() != 32 {
1347 self.decline(format!("op#{i} memory load on a non-32-bit index"));
1348 return;
1349 }
1350 let (op_name, size) = match op {
1351 WasmOp::I32Load { .. } => ("i32.load", 4),
1352 WasmOp::I32Load8S { .. } => ("i32.load8_s", 1),
1353 WasmOp::I32Load8U { .. } => ("i32.load8_u", 1),
1354 WasmOp::I32Load16S { .. } => ("i32.load16_s", 2),
1355 _ => ("i32.load16_u", 2),
1356 };
1357 self.try_elide_mem_bounds(i, op_name, size, *offset, &a);
1358 // Havoc the loaded value; `start = None` keeps a possibly-
1359 // trapping op out of every erasable condition slice.
1360 let n = self.fresh;
1361 self.fresh += 1;
1362 let v = self.fresh_var(format!("fs_m{n}"));
1363 self.attach_fact(i, &v);
1364 self.push(v, None, i);
1365 }
1366 // #494 bounds-elision: i32 memory STORES — same obligation
1367 // over the index. No memory model is needed: loads are always
1368 // havocked, so a store's effect on the symbolic state is
1369 // vacuous; only the guard obligation is discharged.
1370 WasmOp::I32Store { offset, .. }
1371 | WasmOp::I32Store8 { offset, .. }
1372 | WasmOp::I32Store16 { offset, .. } => {
1373 let (Some(val), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
1374 self.decline(format!(
1375 "op#{i} memory store on underflowing symbolic stack"
1376 ));
1377 return;
1378 };
1379 if a.bv.get_size() != 32 || val.bv.get_size() != 32 {
1380 self.decline(format!("op#{i} memory store on a non-32-bit operand"));
1381 return;
1382 }
1383 let (op_name, size) = match op {
1384 WasmOp::I32Store { .. } => ("i32.store", 4),
1385 WasmOp::I32Store8 { .. } => ("i32.store8", 1),
1386 _ => ("i32.store16", 2),
1387 };
1388 self.try_elide_mem_bounds(i, op_name, size, *offset, &a);
1389 // Store pushes nothing.
1390 }
1391 WasmOp::If => {
1392 let Some(cond) = self.stack.pop() else {
1393 self.decline(format!("op#{i} `if` on empty symbolic stack"));
1394 return;
1395 };
1396 if cond.bv.get_size() != 32 {
1397 self.decline(format!("op#{i} `if` condition is not 32-bit"));
1398 return;
1399 }
1400 let Some((end, has_else)) = self.matching_end(i) else {
1401 self.decline(format!("op#{i} `if` without matching `end`"));
1402 return;
1403 };
1404 let Some(&ord) = self.opener_ordinal.get(&i) else {
1405 self.decline(format!("op#{i} `if` missing from the opener ordinal map"));
1406 return;
1407 };
1408 let Some(&arity) = self.block_arity.get(ord) else {
1409 self.decline(format!(
1410 "op#{i} `if` has no block_arity entry (side-table desync)"
1411 ));
1412 return;
1413 };
1414 if has_else {
1415 self.decline(format!(
1416 "op#{i} `if`/`else` — only no-else `if` is in Phase-2 scope"
1417 ));
1418 self.havoc_region(i, end, arity);
1419 } else if self.try_elide(i, end, &cond) {
1420 // Region provably never executes: state unchanged
1421 // (params-as-results pass-through is the identity for
1422 // a no-else `if`, whose blocktype has equal
1423 // param/result types by wasm validation).
1424 } else {
1425 self.havoc_region(i, end, arity);
1426 }
1427 i = end + 1;
1428 continue;
1429 }
1430 // #494 Phase 3: branchless `select` — the sibling of the
1431 // Phase-2 no-else `if` elision, and the shape gust_mix's
1432 // clamp actually lowers to (`max`/`min` via select). A
1433 // value-range premise that pins the condition constant
1434 // collapses it to one operand (stream deletion, like `if`).
1435 WasmOp::Select => {
1436 let (Some(cond), Some(val2), Some(val1)) =
1437 (self.stack.pop(), self.stack.pop(), self.stack.pop())
1438 else {
1439 self.decline(format!("op#{i} select on underflowing symbolic stack"));
1440 return;
1441 };
1442 // Only the plain i32 `select` (0x1B) is tracked; a typed
1443 // select over i64/f-operands declines and havocs.
1444 if cond.bv.get_size() != 32
1445 || val1.bv.get_size() != 32
1446 || val2.bv.get_size() != 32
1447 {
1448 self.decline(format!(
1449 "op#{i} select on non-32-bit operand(s) — only i32 select is tracked"
1450 ));
1451 let v = self.fresh_var(format!("fs_sel{i}"));
1452 self.push(v, None, i);
1453 i += 1;
1454 continue;
1455 }
1456 match self.try_collapse_select(i, &val1, &val2, &cond) {
1457 // Admitted: the surviving operand's producer slice
1458 // stays; the other operand + condition slice + the
1459 // `select` were recorded for deletion. `start = None`
1460 // keeps the collapsed result out of any later erasable
1461 // slice (conservative).
1462 Some(surviving) => self.push(surviving.bv, None, i),
1463 // Declined (loud): the branchless select stands. Havoc
1464 // the result — a fresh var means any obligation over it
1465 // downstream is Sat, so it can never seed an unsound
1466 // chained collapse.
1467 None => {
1468 let v = self.fresh_var(format!("fs_sel{i}"));
1469 self.push(v, None, i);
1470 }
1471 }
1472 }
1473 // Function-final `End` (top-level): done.
1474 WasmOp::End => break,
1475 WasmOp::Return => break,
1476 other => {
1477 // First op outside the tracked fragment: stop. Everything
1478 // already admitted was justified independently of what
1479 // follows; declining the REST loudly keeps honesty.
1480 self.decline(format!(
1481 "op#{i} {other:?} is outside the tracked i32 fragment — \
1482 fact tracking stops here (no further elisions in this function)"
1483 ));
1484 return;
1485 }
1486 }
1487 i += 1;
1488 }
1489 }
1490
1491 fn finish(self) -> FactSpecResult {
1492 let Pass {
1493 ops,
1494 block_arity,
1495 deletions,
1496 admitted,
1497 declined,
1498 zero_marks,
1499 ovf_marks,
1500 mem_marks,
1501 ..
1502 } = self;
1503 if deletions.is_empty() {
1504 return FactSpecResult {
1505 ops: ops.to_vec(),
1506 block_arity: block_arity.to_vec(),
1507 kept: (0..ops.len()).collect(),
1508 admitted,
1509 declined,
1510 // No rewrite ⇒ original indices ARE the output indices.
1511 elide_div_zero: zero_marks,
1512 elide_div_ovf: ovf_marks,
1513 elide_mem_bounds: mem_marks,
1514 stream_changed: false,
1515 };
1516 }
1517 let deleted = |i: usize| deletions.iter().any(|&(s, e)| i >= s && i <= e);
1518 let mut out_ops = Vec::with_capacity(ops.len());
1519 let mut out_arity = Vec::with_capacity(block_arity.len());
1520 let mut kept = Vec::with_capacity(ops.len());
1521 let mut ord = 0usize;
1522 for (i, op) in ops.iter().enumerate() {
1523 let is_opener = matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If);
1524 if !deleted(i) {
1525 out_ops.push(op.clone());
1526 kept.push(i);
1527 if is_opener && let Some(&a) = block_arity.get(ord) {
1528 out_arity.push(a);
1529 }
1530 }
1531 if is_opener {
1532 ord += 1;
1533 }
1534 }
1535 // Remap the guard-elision marks into the REWRITTEN index space. A
1536 // marked div/rem can never sit inside a deleted range (deleted ranges
1537 // are contiguous PURE condition slices plus proven-dead `if` regions
1538 // the walk skipped over; a div result's `start = None` bars it from
1539 // any erasable slice) — the filter below is defense in depth.
1540 let remap = |marks: Vec<usize>| -> Vec<usize> {
1541 marks
1542 .into_iter()
1543 .filter_map(|m| {
1544 debug_assert!(!deleted(m), "guard mark op#{m} inside a deleted range");
1545 kept.binary_search(&m).ok()
1546 })
1547 .collect()
1548 };
1549 FactSpecResult {
1550 ops: out_ops,
1551 block_arity: out_arity,
1552 elide_div_zero: remap(zero_marks),
1553 elide_div_ovf: remap(ovf_marks),
1554 elide_mem_bounds: remap(mem_marks),
1555 kept,
1556 admitted,
1557 declined,
1558 stream_changed: true,
1559 }
1560 }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565 use super::*;
1566 use WasmOp::*;
1567
1568 fn fact(value_id: u32, lo: i64, hi: i64) -> WscFact {
1569 WscFact {
1570 func_index: 0,
1571 value_id,
1572 kind: FactKind::ValueRange { lo, hi },
1573 }
1574 }
1575
1576 /// The gust_mix clamp shape: clamp(ch + 476, 1000, 2000) via two
1577 /// no-else `if`s over a local.
1578 fn clamp_ops() -> Vec<WasmOp> {
1579 vec![
1580 LocalGet(0), // 0 ch ← fact target
1581 I32Const(476), // 1
1582 I32Add, // 2 v = ch+476
1583 LocalSet(1), // 3
1584 LocalGet(1), // 4
1585 I32Const(1000), // 5
1586 I32LtS, // 6
1587 If, // 7
1588 I32Const(1000), // 8
1589 LocalSet(1), // 9
1590 End, // 10
1591 LocalGet(1), // 11
1592 I32Const(2000), // 12
1593 I32GtS, // 13
1594 If, // 14
1595 I32Const(2000), // 15
1596 LocalSet(1), // 16
1597 End, // 17
1598 LocalGet(1), // 18
1599 End, // 19
1600 ]
1601 }
1602
1603 const CLAMP_ARITY: &[(u8, u8)] = &[(0, 0), (0, 0)];
1604
1605 #[test]
1606 fn clamp_shape_elides_both_branches_under_the_proven_bound_494() {
1607 let ops = clamp_ops();
1608 let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)], &[], 0);
1609 assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
1610 assert!(r.changed());
1611 assert_eq!(
1612 r.ops,
1613 vec![
1614 LocalGet(0),
1615 I32Const(476),
1616 I32Add,
1617 LocalSet(1),
1618 LocalGet(1),
1619 End
1620 ],
1621 "both clamp comparisons + branches + bodies must be gone"
1622 );
1623 assert_eq!(r.block_arity, vec![], "both If arity entries removed");
1624 assert_eq!(r.kept, vec![0, 1, 2, 3, 18, 19]);
1625 // The certificate evidence trail names the engine and the premise.
1626 for line in &r.admitted {
1627 assert!(line.contains("UNSAT"), "{line}");
1628 assert!(line.contains("certificate-checked"), "{line}");
1629 assert!(line.contains("[524, 1524]"), "{line}");
1630 }
1631 }
1632
1633 #[test]
1634 fn wrong_wide_bound_is_sat_and_declines_loudly_494() {
1635 // ch ∈ [0, 4000] does NOT make the clamp dead (ch=0 → v=476 < 1000):
1636 // the obligation is Sat and BOTH sites decline with a counterexample.
1637 let ops = clamp_ops();
1638 let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 0, 4000)], &[], 0);
1639 assert_eq!(r.admitted.len(), 0);
1640 assert!(!r.changed());
1641 assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
1642 assert_eq!(r.block_arity, CLAMP_ARITY.to_vec());
1643 assert!(
1644 r.declined
1645 .iter()
1646 .any(|d| d.contains("Sat") && d.contains("counterexample")),
1647 "declines must be loud and carry a model: {:?}",
1648 r.declined
1649 );
1650 }
1651
1652 #[test]
1653 fn partially_dead_bound_elides_only_the_proven_branch_494() {
1654 // ch ∈ [524, 4000]: v ≥ 1000 so the LOW clamp is dead, but v can
1655 // exceed 2000 so the HIGH clamp must survive.
1656 let ops = clamp_ops();
1657 let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 4000)], &[], 0);
1658 assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1659 assert_eq!(r.declined.len(), 1);
1660 assert_eq!(
1661 r.ops,
1662 vec![
1663 LocalGet(0),
1664 I32Const(476),
1665 I32Add,
1666 LocalSet(1),
1667 LocalGet(1),
1668 I32Const(2000),
1669 I32GtS,
1670 If,
1671 I32Const(2000),
1672 LocalSet(1),
1673 End,
1674 LocalGet(1),
1675 End,
1676 ]
1677 );
1678 assert_eq!(r.block_arity, vec![(0, 0)], "one If survives");
1679 }
1680
1681 #[test]
1682 fn no_facts_changes_nothing_494() {
1683 let ops = clamp_ops();
1684 let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[], &[], 0);
1685 assert!(!r.changed());
1686 assert_eq!(r.ops, ops);
1687 assert!(!r.declined.is_empty(), "the no-fact case is a loud decline");
1688 }
1689
1690 // ---- #494 Phase 3: branchless select-collapse ----
1691
1692 /// gust_mix's clamp lowered branchlessly via `select` (the shape LLVM
1693 /// emits): `max(v,1000)` = `(v<1000)?1000:v`, `min(v,2000)` =
1694 /// `(v>2000)?2000:v`. No `If`/`End` — no block_arity entries.
1695 fn select_clamp_ops() -> Vec<WasmOp> {
1696 vec![
1697 LocalGet(0), // 0 ch ← fact target
1698 I32Const(476), // 1
1699 I32Add, // 2 v = ch+476
1700 LocalSet(1), // 3
1701 I32Const(1000), // 4 val1 (low clamp)
1702 LocalGet(1), // 5 val2 = v
1703 LocalGet(1), // 6 cond slice
1704 I32Const(1000), // 7
1705 I32LtS, // 8 cond = v < 1000
1706 Select, // 9 → max(v,1000)
1707 LocalSet(1), // 10
1708 I32Const(2000), // 11 val1 (high clamp)
1709 LocalGet(1), // 12 val2 = v
1710 LocalGet(1), // 13 cond slice
1711 I32Const(2000), // 14
1712 I32GtS, // 15 cond = v > 2000
1713 Select, // 16 → min(v,2000) = result
1714 End, // 17
1715 ]
1716 }
1717
1718 #[test]
1719 fn select_clamp_collapses_both_selects_under_the_proven_bound_494() {
1720 let ops = select_clamp_ops();
1721 let r = specialize_function("gust_mix", &ops, &[], &[fact(0, 524, 1524)], &[], 0);
1722 assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
1723 assert!(r.changed());
1724 assert_eq!(
1725 r.ops,
1726 vec![
1727 LocalGet(0),
1728 I32Const(476),
1729 I32Add,
1730 LocalSet(1),
1731 LocalGet(1), // val2 of select 1 (identity survives)
1732 LocalSet(1),
1733 LocalGet(1), // val2 of select 2 (identity survives)
1734 End,
1735 ],
1736 "both branchless clamps must collapse to the identity operand"
1737 );
1738 assert_eq!(r.kept, vec![0, 1, 2, 3, 5, 10, 12, 17]);
1739 for line in &r.admitted {
1740 assert!(line.contains("UNSAT"), "{line}");
1741 assert!(line.contains("certificate-checked"), "{line}");
1742 assert!(line.contains("select"), "{line}");
1743 assert!(line.contains("[524, 1524]"), "{line}");
1744 }
1745 }
1746
1747 #[test]
1748 fn select_clamp_wrong_bound_is_sat_and_declines_byte_identically_494() {
1749 // ch ∈ [0, 4000]: ch=0 → v=476 < 1000 so the low clamp genuinely
1750 // fires; both selects are non-constant ⇒ loud Sat decline, no rewrite.
1751 let ops = select_clamp_ops();
1752 let r = specialize_function("gust_mix", &ops, &[], &[fact(0, 0, 4000)], &[], 0);
1753 assert_eq!(r.admitted.len(), 0);
1754 assert!(!r.changed());
1755 assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
1756 assert!(
1757 r.declined
1758 .iter()
1759 .any(|d| d.contains("not constant") && d.contains("counterexample")),
1760 "declines must be loud and carry a model: {:?}",
1761 r.declined
1762 );
1763 }
1764
1765 #[test]
1766 fn select_collapses_to_true_arm_when_condition_proven_nonzero_494() {
1767 // result = cond ? val1 : val2 with cond ≡ 1 (fact ∈ [1,1]) ⇒ keep val1.
1768 let ops = vec![
1769 I32Const(111), // 0 val1
1770 I32Const(222), // 1 val2
1771 LocalGet(0), // 2 cond ← fact ∈ [1,1] (always non-zero)
1772 Select, // 3 → val1
1773 End, // 4
1774 ];
1775 let r = specialize_function("f", &ops, &[], &[fact(2, 1, 1)], &[], 0);
1776 assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1777 assert_eq!(r.ops, vec![I32Const(111), End]);
1778 assert!(
1779 r.admitted[0].contains("true-arm") && r.admitted[0].contains("cond == 0"),
1780 "{}",
1781 r.admitted[0]
1782 );
1783 }
1784
1785 #[test]
1786 fn select_without_constraining_premise_declines_no_false_collapse_494() {
1787 // A TRUE ValueRange fact exists (so the walk runs) but targets val1,
1788 // not the condition; the select's condition carries no premise ⇒
1789 // non-constant ⇒ Sat decline, byte-identical.
1790 let ops = vec![
1791 I32Const(111), // 0 val1 ← fact ∈ [111,111] (true, non-constraining)
1792 I32Const(222), // 1 val2
1793 LocalGet(0), // 2 cond — unconstrained
1794 Select, // 3
1795 End, // 4
1796 ];
1797 let r = specialize_function("f", &ops, &[], &[fact(0, 111, 111)], &[], 0);
1798 assert_eq!(r.admitted.len(), 0);
1799 assert!(!r.changed());
1800 assert_eq!(r.ops, ops);
1801 }
1802
1803 #[test]
1804 fn declined_if_havocs_its_locals_no_false_admit_downstream_494() {
1805 // The FIRST if is undecidable (condition on an unconstrained local),
1806 // and its body rewrites local 1 — so the SECOND if (which would be
1807 // dead under the fact alone) must NOT be admitted: local 1 is
1808 // havocked by the declined region.
1809 let ops = vec![
1810 LocalGet(0), // 0 ← fact ch ∈ [524, 1524]
1811 I32Const(476), // 1
1812 I32Add, // 2
1813 LocalSet(1), // 3
1814 LocalGet(2), // 4 unconstrained
1815 If, // 5
1816 I32Const(-9), // 6
1817 LocalSet(1), // 7 havocs local 1
1818 End, // 8
1819 LocalGet(1), // 9
1820 I32Const(2000), // 10
1821 I32GtS, // 11
1822 If, // 12
1823 I32Const(2000), // 13
1824 LocalSet(1), // 14
1825 End, // 15
1826 LocalGet(1), // 16
1827 End, // 17
1828 ];
1829 let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)], &[], 0);
1830 assert_eq!(
1831 r.admitted.len(),
1832 0,
1833 "havocked local must block the downstream elision: {:?}",
1834 r.admitted
1835 );
1836 assert_eq!(r.ops, ops);
1837 }
1838
1839 #[test]
1840 fn if_with_else_declines_494() {
1841 let ops = vec![
1842 LocalGet(0), // 0 ← fact forces cond = 0
1843 If, // 1
1844 I32Const(1), // 2
1845 LocalSet(1), // 3
1846 Else, // 4
1847 I32Const(2), // 5
1848 LocalSet(1), // 6
1849 End, // 7
1850 LocalGet(1), // 8
1851 End, // 9
1852 ];
1853 let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 0, 0)], &[], 0);
1854 assert_eq!(r.admitted.len(), 0);
1855 assert!(
1856 r.declined.iter().any(|d| d.contains("else")),
1857 "{:?}",
1858 r.declined
1859 );
1860 assert_eq!(r.ops, ops);
1861 }
1862
1863 #[test]
1864 fn nested_opener_inside_elided_body_fixes_block_arity_ordinals_494() {
1865 // A dead outer if contains a nested if: BOTH arity entries vanish and
1866 // the SURVIVING later block keeps its (translated) entry.
1867 let ops = vec![
1868 LocalGet(0), // 0 ← fact [5,5] ⇒ eqz = 0
1869 I32Eqz, // 1
1870 If, // 2 (ordinal 0)
1871 LocalGet(0), // 3
1872 If, // 4 (ordinal 1, nested)
1873 I32Const(7), // 5
1874 LocalSet(1), // 6
1875 End, // 7
1876 End, // 8
1877 Block, // 9 (ordinal 2, survives)
1878 End, // 10
1879 End, // 11
1880 ];
1881 let arity = &[(0, 0), (0, 0), (0, 1)];
1882 let r = specialize_function("f", &ops, arity, &[fact(0, 5, 5)], &[], 0);
1883 assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1884 // The condition slice starts at op 0 (LocalGet feeds the eqz), so the
1885 // whole deleted range is [0..=8]; only the trailing block survives.
1886 assert_eq!(r.ops, vec![Block, End, End]);
1887 assert_eq!(r.kept, vec![9, 10, 11]);
1888 assert_eq!(
1889 r.block_arity,
1890 vec![(0, 1)],
1891 "only the surviving Block's entry"
1892 );
1893 }
1894
1895 #[test]
1896 fn tee_condition_slice_is_not_erasable_494() {
1897 // cond built through local.tee: proven dead, but deleting the slice
1898 // would lose the local write ⇒ decline (loud), stream unchanged.
1899 let ops = vec![
1900 LocalGet(0), // 0 ← fact [1,1]
1901 LocalTee(1), // 1 side effect in the slice
1902 I32Eqz, // 2 = 0 under the fact
1903 If, // 3
1904 I32Const(9), // 4
1905 LocalSet(2), // 5
1906 End, // 6
1907 End, // 7
1908 ];
1909 let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 1, 1)], &[], 0);
1910 assert_eq!(r.admitted.len(), 0);
1911 assert!(
1912 r.declined
1913 .iter()
1914 .any(|d| d.contains("not") && d.contains("erasable")),
1915 "{:?}",
1916 r.declined
1917 );
1918 assert_eq!(r.ops, ops);
1919 }
1920
1921 #[test]
1922 fn untracked_op_stops_tracking_loudly_494() {
1923 let ops = vec![
1924 LocalGet(0), // 0 ← fact
1925 I64ExtendI32S, // 1 untracked ⇒ stop
1926 Drop, // 2
1927 End, // 3
1928 ];
1929 let r = specialize_function("f", &ops, &[], &[fact(0, 1, 2)], &[], 0);
1930 assert!(!r.changed());
1931 assert!(
1932 r.declined.iter().any(|d| d.contains("outside the tracked")),
1933 "{:?}",
1934 r.declined
1935 );
1936 }
1937
1938 fn nonzero_fact(value_id: u32) -> WscFact {
1939 WscFact {
1940 func_index: 0,
1941 value_id,
1942 kind: FactKind::DivisorNonZero,
1943 }
1944 }
1945
1946 // ================= #494 phase 2b: div/rem trap-guard elision =================
1947
1948 #[test]
1949 fn divisor_range_excluding_zero_elides_zero_guard_all_rem_div_494() {
1950 // div_u, rem_u, rem_s by a param divisor proven ∈ [1, 100]: every
1951 // zero guard falls to UNSAT(P ∧ divisor == 0); the stream itself is
1952 // untouched (marks only).
1953 let ops = vec![
1954 LocalGet(0), // 0 n
1955 LocalGet(1), // 1 d ← fact [1,100]
1956 I32DivU, // 2 → zero mark
1957 Drop, // 3
1958 LocalGet(0), // 4
1959 LocalGet(1), // 5 ← fact [1,100]
1960 I32RemU, // 6 → zero mark
1961 Drop, // 7
1962 LocalGet(0), // 8
1963 LocalGet(1), // 9 ← fact [1,100]
1964 I32RemS, // 10 → zero mark
1965 End, // 11
1966 ];
1967 let facts = [fact(1, 1, 100), fact(5, 1, 100), fact(9, 1, 100)];
1968 let r = specialize_function("f", &ops, &[], &facts, &[], 0);
1969 assert_eq!(
1970 r.elide_div_zero,
1971 vec![2, 6, 10],
1972 "declines: {:?}",
1973 r.declined
1974 );
1975 assert_eq!(
1976 r.elide_div_ovf,
1977 Vec::<usize>::new(),
1978 "no div_s in the stream"
1979 );
1980 assert!(!r.changed(), "guard marks never rewrite the op stream");
1981 assert_eq!(r.ops, ops);
1982 assert_eq!(r.admitted.len(), 3);
1983 for line in &r.admitted {
1984 assert!(line.contains("divide-by-zero guard elided"), "{line}");
1985 assert!(line.contains("UNSAT(P ∧ divisor == 0)"), "{line}");
1986 assert!(line.contains("certificate-checked"), "{line}");
1987 }
1988 }
1989
1990 #[test]
1991 fn nonzero_fact_elides_zero_guard_but_retains_div_s_overflow_guard_494() {
1992 // THE TWO-GUARD DISTINCTION (#633/#634): a divisor-nonzero fact (kind
1993 // 3) discharges UNSAT(P ∧ divisor == 0) but NOT the overflow
1994 // obligation — divisor ≠ 0 still admits divisor == -1 with dividend
1995 // == INT_MIN, so the overflow guard is RETAINED with a loud decline.
1996 let ops = vec![
1997 LocalGet(0), // 0
1998 LocalGet(1), // 1 ← divisor-nonzero fact
1999 I32DivS, // 2
2000 End, // 3
2001 ];
2002 let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[], 0);
2003 assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
2004 assert_eq!(
2005 r.elide_div_ovf,
2006 Vec::<usize>::new(),
2007 "divisor ≠ 0 must NOT elide the INT_MIN/-1 overflow guard"
2008 );
2009 assert!(
2010 r.declined
2011 .iter()
2012 .any(|d| d.contains("overflow-guard obligation Sat") && d.contains("RETAINED")),
2013 "{:?}",
2014 r.declined
2015 );
2016 }
2017
2018 #[test]
2019 fn positive_range_discharges_both_div_s_obligations_494() {
2020 // divisor ∈ [1, 100] excludes BOTH 0 and -1 — the two obligations
2021 // are discharged independently and both guards fall.
2022 let ops = vec![LocalGet(0), LocalGet(1), I32DivS, End];
2023 let r = specialize_function("f", &ops, &[], &[fact(1, 1, 100)], &[], 0);
2024 assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
2025 assert_eq!(r.elide_div_ovf, vec![2]);
2026 assert_eq!(r.admitted.len(), 2, "one certificate line per obligation");
2027 assert!(
2028 r.admitted
2029 .iter()
2030 .any(|a| a.contains("overflow guard elided")
2031 && a.contains("dividend == INT32_MIN ∧ divisor == -1")),
2032 "{:?}",
2033 r.admitted
2034 );
2035 }
2036
2037 #[test]
2038 fn range_including_zero_is_sat_and_declines_the_zero_guard_494() {
2039 // divisor ∈ [0, 100]: divisor == 0 is P-admissible — the obligation
2040 // is Sat, the decline is loud and carries a model, no mark is set.
2041 let ops = vec![LocalGet(0), LocalGet(1), I32DivU, End];
2042 let r = specialize_function("f", &ops, &[], &[fact(1, 0, 100)], &[], 0);
2043 assert_eq!(r.elide_div_zero, Vec::<usize>::new());
2044 assert!(
2045 r.declined
2046 .iter()
2047 .any(|d| d.contains("zero-guard obligation Sat") && d.contains("counterexample")),
2048 "{:?}",
2049 r.declined
2050 );
2051 }
2052
2053 #[test]
2054 fn i64_div_s_nonzero_fact_zero_guard_only_overflow_retained_494() {
2055 // Oracle 5 at the pass level: i64.div_s with an i64 param divisor
2056 // carrying a divisor-nonzero fact — the zero guard is proven dead,
2057 // the INT64_MIN/-1 overflow guard (#633/#634) is RETAINED.
2058 let ops = vec![LocalGet(0), LocalGet(1), I64DivS, End];
2059 let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[true, true], 0);
2060 assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
2061 assert_eq!(
2062 r.elide_div_ovf,
2063 Vec::<usize>::new(),
2064 "i64 overflow guard retained"
2065 );
2066 assert!(
2067 r.declined.iter().any(|d| d.contains("RETAINED")),
2068 "{:?}",
2069 r.declined
2070 );
2071 }
2072
2073 #[test]
2074 fn i64_div_s_positive_range_discharges_both_obligations_494() {
2075 let ops = vec![LocalGet(0), LocalGet(1), I64DivS, End];
2076 let r = specialize_function("f", &ops, &[], &[fact(1, 1, 1000)], &[true, true], 0);
2077 assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
2078 assert_eq!(r.elide_div_ovf, vec![2]);
2079 }
2080
2081 #[test]
2082 fn i64_div_on_undeclared_width_declines_no_marks_494() {
2083 // Without the params_i64 table the divisor local is symbolically
2084 // 32-bit — the width check declines rather than building a
2085 // wrong-width obligation.
2086 let ops = vec![LocalGet(0), LocalGet(1), I64DivU, End];
2087 let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[], 0);
2088 assert_eq!(r.elide_div_zero, Vec::<usize>::new());
2089 assert!(
2090 r.declined.iter().any(|d| d.contains("unexpected width")),
2091 "{:?}",
2092 r.declined
2093 );
2094 }
2095
2096 #[test]
2097 fn div_with_no_premise_declines_loudly_494() {
2098 // The function carries a fact, but no premise reaches the divisor —
2099 // the obligation cannot even be posed; both guards stay.
2100 let ops = vec![
2101 LocalGet(0), // 0 ← fact on the DIVIDEND, not the divisor
2102 LocalGet(1), // 1 unconstrained divisor
2103 I32DivU, // 2
2104 End, // 3
2105 ];
2106 // A fact on op 0 (the dividend): premises exist but do not constrain
2107 // the divisor — Sat, decline.
2108 let r = specialize_function("f", &ops, &[], &[fact(0, 1, 5)], &[], 0);
2109 assert_eq!(r.elide_div_zero, Vec::<usize>::new());
2110 assert!(
2111 r.declined
2112 .iter()
2113 .any(|d| d.contains("zero-guard obligation Sat")),
2114 "{:?}",
2115 r.declined
2116 );
2117 }
2118
2119 #[test]
2120 fn guard_marks_are_remapped_through_a_clamp_elision_494() {
2121 // A clamp elision rewrites the stream; a downstream div's mark must
2122 // land on the REWRITTEN index (the driver feeds the rewritten stream
2123 // to the selector, which keys guards by its own op index).
2124 let ops = vec![
2125 LocalGet(0), // 0 ← fact [524, 1524]
2126 I32Const(476), // 1
2127 I32Add, // 2
2128 LocalSet(1), // 3
2129 LocalGet(1), // 4 -+ low clamp (elided 4..=10)
2130 I32Const(1000), // 5 |
2131 I32LtS, // 6 |
2132 If, // 7 |
2133 I32Const(1000), // 8 |
2134 LocalSet(1), // 9 |
2135 End, // 10 -+
2136 LocalGet(1), // 11
2137 LocalGet(0), // 12 divisor = ch ∈ [524, 1524] ⇒ nonzero
2138 I32DivU, // 13 → zero mark (rewritten index 6)
2139 End, // 14
2140 ];
2141 let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 524, 1524)], &[], 0);
2142 assert!(r.changed(), "declines: {:?}", r.declined);
2143 assert_eq!(r.kept, vec![0, 1, 2, 3, 11, 12, 13, 14]);
2144 assert_eq!(
2145 r.ops,
2146 vec![
2147 LocalGet(0),
2148 I32Const(476),
2149 I32Add,
2150 LocalSet(1),
2151 LocalGet(1),
2152 LocalGet(0),
2153 I32DivU,
2154 End
2155 ]
2156 );
2157 assert_eq!(
2158 r.elide_div_zero,
2159 vec![6],
2160 "mark remapped from original op#13 to rewritten op#6"
2161 );
2162 }
2163
2164 // ============ #494 Phase 3+: redundant-mask (narrowing) elision ============
2165
2166 /// A representative dissolved DSP kernel: pack two proven-11-bit lanes,
2167 /// `lo | (hi << 11)`. Both `& 0x7FF` masks are redundant under the lane
2168 /// bounds — LLVM keeps them (no range on the params); the facts drop them.
2169 fn pack_lanes_ops() -> Vec<WasmOp> {
2170 vec![
2171 LocalGet(0), // 0 lo ← fact [0, 2047]
2172 I32Const(0x7FF), // 1
2173 I32And, // 2 lo & 0x7FF (redundant)
2174 LocalGet(1), // 3 hi ← fact [0, 2047]
2175 I32Const(0x7FF), // 4
2176 I32And, // 5 hi & 0x7FF (redundant)
2177 I32Const(11), // 6
2178 I32Shl, // 7 hi << 11
2179 I32Or, // 8 lo | (hi << 11)
2180 End, // 9
2181 ]
2182 }
2183
2184 #[test]
2185 fn narrow_value_elides_redundant_mask_494() {
2186 // lo, hi ∈ [0, 2047] ⇒ `x & 0x7FF == x`: both masks fall to
2187 // UNSAT(P ∧ (value & mask) ≠ value); each deletes its `const;and` pair.
2188 let ops = pack_lanes_ops();
2189 let r = specialize_function(
2190 "gust_kernel",
2191 &ops,
2192 &[],
2193 &[fact(0, 0, 2047), fact(3, 0, 2047)],
2194 &[],
2195 0,
2196 );
2197 assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
2198 assert!(r.changed());
2199 assert_eq!(
2200 r.ops,
2201 vec![
2202 LocalGet(0), // lo flows through the elided mask
2203 LocalGet(1), // hi flows through the elided mask
2204 I32Const(11),
2205 I32Shl,
2206 I32Or,
2207 End,
2208 ],
2209 "both redundant masks must be gone, the arithmetic intact"
2210 );
2211 assert_eq!(r.kept, vec![0, 3, 6, 7, 8, 9]);
2212 for line in &r.admitted {
2213 assert!(line.contains("UNSAT(P ∧ (value & mask) ≠ value)"), "{line}");
2214 assert!(line.contains("certificate-checked"), "{line}");
2215 assert!(line.contains("redundant mask elided"), "{line}");
2216 }
2217 }
2218
2219 #[test]
2220 fn wide_bound_makes_mask_live_and_declines_byte_identically_494() {
2221 // lo ∈ [0, 0xFFF]: value 0x800 has bit 11 set, OUTSIDE the 0x7FF mask,
2222 // so `x & 0x7FF != x` is Sat — the mask is genuinely live. BOTH sites
2223 // decline loudly with a counterexample; the stream is byte-identical.
2224 let ops = pack_lanes_ops();
2225 let r = specialize_function(
2226 "gust_kernel",
2227 &ops,
2228 &[],
2229 &[fact(0, 0, 0xFFF), fact(3, 0, 0xFFF)],
2230 &[],
2231 0,
2232 );
2233 assert_eq!(r.admitted.len(), 0);
2234 assert!(!r.changed());
2235 assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
2236 assert!(
2237 r.declined
2238 .iter()
2239 .any(|d| d.contains("not redundant") && d.contains("counterexample")),
2240 "declines must be loud and carry a model: {:?}",
2241 r.declined
2242 );
2243 }
2244
2245 #[test]
2246 fn mask_without_constraining_premise_declines_no_false_elision_494() {
2247 // A TRUE ValueRange fact exists (so the walk runs) but targets the mask
2248 // const, not the value; the masked value carries no premise ⇒ the
2249 // obligation is Sat ⇒ loud decline, byte-identical.
2250 let ops = vec![
2251 LocalGet(0), // 0 value — unconstrained
2252 I32Const(0x7FF), // 1 mask ← fact [0x7FF, 0x7FF] (true, non-constraining)
2253 I32And, // 2
2254 End, // 3
2255 ];
2256 let r = specialize_function("f", &ops, &[], &[fact(1, 0x7FF, 0x7FF)], &[], 0);
2257 assert_eq!(r.admitted.len(), 0);
2258 assert!(!r.changed());
2259 assert_eq!(r.ops, ops);
2260 }
2261
2262 #[test]
2263 fn signed_narrow_bound_that_admits_negative_keeps_the_mask_494() {
2264 // value ∈ [-1, 2047]: -1 is all-ones, so `-1 & 0x7FF = 0x7FF != -1` —
2265 // the obligation is Sat and the mask is (correctly) retained. Guards
2266 // against a naive "hi ≤ mask" shortcut that ignores the sign bit.
2267 let ops = vec![
2268 LocalGet(0), // 0 ← fact [-1, 2047]
2269 I32Const(0x7FF), // 1
2270 I32And, // 2
2271 End, // 3
2272 ];
2273 let r = specialize_function("f", &ops, &[], &[fact(0, -1, 2047)], &[], 0);
2274 assert_eq!(
2275 r.admitted.len(),
2276 0,
2277 "a negative value fails the mask identity"
2278 );
2279 assert!(!r.changed());
2280 assert_eq!(r.ops, ops);
2281 }
2282
2283 #[test]
2284 fn mask_elision_no_facts_changes_nothing_494() {
2285 let ops = pack_lanes_ops();
2286 let r = specialize_function("gust_kernel", &ops, &[], &[], &[], 0);
2287 assert!(!r.changed());
2288 assert_eq!(r.ops, ops);
2289 assert!(!r.declined.is_empty(), "the no-fact case is a loud decline");
2290 }
2291
2292 #[test]
2293 fn out_of_range_value_id_is_vacuous_494() {
2294 let ops = clamp_ops();
2295 let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(999, 524, 1524)], &[], 0);
2296 assert!(!r.changed());
2297 assert_eq!(r.ops, ops);
2298 }
2299
2300 // ========= #494 bounds-elision (#390 guard_bool): memory bounds guards =========
2301
2302 /// The gust_poll shape: a record array indexed by a proven-bounded slot —
2303 /// `base = slot*16 + 256`, then field loads/stores at static offsets.
2304 fn poll_ops() -> Vec<WasmOp> {
2305 vec![
2306 LocalGet(0), // 0 slot ← fact target
2307 I32Const(4), // 1
2308 I32Shl, // 2 slot*16
2309 I32Const(256), // 3
2310 I32Add, // 4 base
2311 LocalSet(1), // 5
2312 LocalGet(1), // 6
2313 I32Load8U {
2314 offset: 0,
2315 align: 0,
2316 }, // 7 → mark (byte)
2317 LocalGet(1), // 8
2318 I32Load {
2319 offset: 4,
2320 align: 2,
2321 }, // 9 → mark (word)
2322 I32Add, // 10
2323 LocalGet(1), // 11
2324 I32Const(7), // 12
2325 I32Store16 {
2326 offset: 2,
2327 align: 1,
2328 }, // 13 → mark (halfword store)
2329 End, // 14
2330 ]
2331 }
2332
2333 #[test]
2334 fn bounded_index_elides_all_mem_bounds_guards_494() {
2335 // slot ∈ [0, 63] ⇒ base ∈ [256, 1264]; every access's last byte is
2336 // < 65536, so all three obligations fall to
2337 // UNSAT(P ∧ trap_mem_oob(...)). Marks only — the stream is untouched.
2338 let ops = poll_ops();
2339 let r = specialize_function("poll", &ops, &[], &[fact(0, 0, 63)], &[], 65536);
2340 assert_eq!(
2341 r.elide_mem_bounds,
2342 vec![7, 9, 13],
2343 "declines: {:?}",
2344 r.declined
2345 );
2346 assert!(!r.changed(), "guard marks never rewrite the op stream");
2347 assert_eq!(r.ops, ops);
2348 assert_eq!(r.admitted.len(), 3);
2349 for line in &r.admitted {
2350 assert!(line.contains("bounds guard elided"), "{line}");
2351 assert!(line.contains("trap_mem_oob"), "{line}");
2352 assert!(line.contains("certificate-checked"), "{line}");
2353 assert!(line.contains("[0, 63]"), "{line}");
2354 }
2355 }
2356
2357 #[test]
2358 fn oob_admissible_bound_is_sat_and_declines_494() {
2359 // slot ∈ [0, 8192]: slot = 4096 ⇒ base = 65792 > 65536 — the access
2360 // can genuinely escape, the obligation is Sat, the guard stays.
2361 let ops = poll_ops();
2362 let r = specialize_function("poll", &ops, &[], &[fact(0, 0, 8192)], &[], 65536);
2363 assert_eq!(r.elide_mem_bounds, Vec::<usize>::new());
2364 assert_eq!(r.admitted.len(), 0);
2365 assert!(
2366 r.declined
2367 .iter()
2368 .any(|d| d.contains("bounds-guard obligation Sat") && d.contains("counterexample")),
2369 "declines must be loud and carry a model: {:?}",
2370 r.declined
2371 );
2372 }
2373
2374 #[test]
2375 fn unknown_memory_size_declines_mem_bounds_494() {
2376 // linear_memory_bytes == 0 (no module memory context): the bound of
2377 // the obligation does not exist — decline loudly, never guess.
2378 let ops = poll_ops();
2379 let r = specialize_function("poll", &ops, &[], &[fact(0, 0, 63)], &[], 0);
2380 assert_eq!(r.elide_mem_bounds, Vec::<usize>::new());
2381 assert!(
2382 r.declined
2383 .iter()
2384 .any(|d| d.contains("linear-memory size unknown")),
2385 "{:?}",
2386 r.declined
2387 );
2388 }
2389
2390 #[test]
2391 fn unconstrained_index_declines_mem_bounds_494() {
2392 // A TRUE fact exists (so the walk runs) but targets the stored VALUE,
2393 // not the index — the index is unconstrained ⇒ Sat ⇒ loud decline.
2394 let ops = vec![
2395 LocalGet(0), // 0 index — unconstrained
2396 LocalGet(1), // 1 value ← fact (non-constraining)
2397 I32Store {
2398 offset: 0,
2399 align: 2,
2400 }, // 2
2401 End, // 3
2402 ];
2403 let r = specialize_function("f", &ops, &[], &[fact(1, 0, 63)], &[], 65536);
2404 assert_eq!(r.elide_mem_bounds, Vec::<usize>::new());
2405 assert!(
2406 r.declined
2407 .iter()
2408 .any(|d| d.contains("bounds-guard obligation Sat")),
2409 "{:?}",
2410 r.declined
2411 );
2412 }
2413
2414 #[test]
2415 fn wraparound_index_plus_offset_never_falsely_unsat_494() {
2416 // THE WIDTH-EXTENSION GOTCHA: index = -256 (0xFFFFFF00 unsigned) with
2417 // offset = 0x200. A naive 32-bit encoding wraps the effective address
2418 // to 0x104 — "in bounds" — and would falsely elide; WASM's effective
2419 // address is infinite-precision (4294967040 + 512 > 65536 ⇒ TRAPS).
2420 // The 64-bit zero-extension keeps the obligation Sat ⇒ guard retained.
2421 let ops = vec![
2422 LocalGet(0), // 0 ← fact [-256, -256]
2423 I32Load {
2424 offset: 0x200,
2425 align: 2,
2426 }, // 1
2427 End, // 2
2428 ];
2429 let r = specialize_function("f", &ops, &[], &[fact(0, -256, -256)], &[], 65536);
2430 assert_eq!(
2431 r.elide_mem_bounds,
2432 Vec::<usize>::new(),
2433 "a wrapped effective address must NOT elide the guard: {:?}",
2434 r.admitted
2435 );
2436 assert!(
2437 r.declined
2438 .iter()
2439 .any(|d| d.contains("bounds-guard obligation Sat")),
2440 "{:?}",
2441 r.declined
2442 );
2443 }
2444
2445 #[test]
2446 fn access_ending_exactly_at_bound_is_in_bounds_boundary_494() {
2447 // Boundary semantics: a 4-byte load at index 65532 ends exactly AT
2448 // the bound (addr + size == mem_bound) — in bounds per WASM (§4.4.5:
2449 // trap iff ea + size > mem size) and per the guard's `>u`. One past
2450 // (65533) must decline.
2451 let ops = vec![
2452 LocalGet(0), // 0 ← fact
2453 I32Load {
2454 offset: 0,
2455 align: 2,
2456 }, // 1
2457 End, // 2
2458 ];
2459 let ok = specialize_function("f", &ops, &[], &[fact(0, 0, 65532)], &[], 65536);
2460 assert_eq!(ok.elide_mem_bounds, vec![1], "declines: {:?}", ok.declined);
2461 let over = specialize_function("f", &ops, &[], &[fact(0, 0, 65533)], &[], 65536);
2462 assert_eq!(over.elide_mem_bounds, Vec::<usize>::new());
2463 }
2464
2465 #[test]
2466 fn mem_marks_are_remapped_through_a_clamp_elision_494() {
2467 // A clamp elision rewrites the stream; a downstream load's mark must
2468 // land on the REWRITTEN index (the selector keys by its own op index).
2469 let ops = vec![
2470 LocalGet(0), // 0 ← fact [524, 1524]
2471 I32Const(476), // 1
2472 I32Add, // 2
2473 LocalSet(1), // 3
2474 LocalGet(1), // 4 -+ low clamp (elided 4..=10)
2475 I32Const(1000), // 5 |
2476 I32LtS, // 6 |
2477 If, // 7 |
2478 I32Const(1000), // 8 |
2479 LocalSet(1), // 9 |
2480 End, // 10 -+
2481 LocalGet(0), // 11 index = ch ∈ [524, 1524]
2482 I32Load {
2483 offset: 0,
2484 align: 2,
2485 }, // 12 → mark (rewritten index 5)
2486 End, // 13
2487 ];
2488 let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 524, 1524)], &[], 65536);
2489 assert!(r.changed(), "declines: {:?}", r.declined);
2490 assert_eq!(r.kept, vec![0, 1, 2, 3, 11, 12, 13]);
2491 assert_eq!(
2492 r.elide_mem_bounds,
2493 vec![5],
2494 "mark remapped from original op#12 to rewritten op#5"
2495 );
2496 }
2497
2498 // ============ #494 Phase 3+: constant-divisor rem_u identity =============
2499
2500 fn const_rem_ops() -> Vec<WasmOp> {
2501 // `x rem_u 1000` — under x ∈ [0, 999] the modulo is the identity.
2502 vec![
2503 LocalGet(0), // 0 x ← fact target
2504 I32Const(1000), // 1 divisor (literal, ≠0)
2505 I32RemU, // 2 x % 1000
2506 End, // 3
2507 ]
2508 }
2509
2510 #[test]
2511 fn const_rem_u_identity_elided_under_proven_narrow_dividend_494() {
2512 let ops = const_rem_ops();
2513 let r = specialize_function("gust_scale", &ops, &[], &[fact(0, 0, 999)], &[], 0);
2514 assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
2515 assert!(r.changed());
2516 assert_eq!(
2517 r.ops,
2518 vec![LocalGet(0), End],
2519 "the const-divisor + rem_u dissolve to the identity dividend"
2520 );
2521 assert_eq!(r.kept, vec![0, 3]);
2522 // Never a div-guard mark: the op was DELETED, not guarded.
2523 assert!(r.elide_div_zero.is_empty());
2524 assert!(r.elide_div_ovf.is_empty());
2525 let line = &r.admitted[0];
2526 assert!(line.contains("i32.rem_u"), "{line}");
2527 assert!(line.contains("elided to identity"), "{line}");
2528 assert!(line.contains("literal i32.const 1000"), "{line}");
2529 assert!(
2530 line.contains("UNSAT(P ∧ (dividend bvurem 1000) ≠ dividend)"),
2531 "{line}"
2532 );
2533 assert!(line.contains("certificate-checked"), "{line}");
2534 }
2535
2536 #[test]
2537 fn const_rem_u_wrong_bound_is_sat_and_declines_byte_identically_494() {
2538 // x ∈ [0, 4000] admits x ≥ 1000, so x % 1000 ≠ x — Sat ⇒ loud decline.
2539 // The stream is untouched and the general div/rem path runs (marks the
2540 // zero guard? no — divisor is a nonzero literal, so the zero-guard
2541 // obligation is discharged by the guard elision, but the OP survives).
2542 let ops = const_rem_ops();
2543 let r = specialize_function("gust_scale", &ops, &[], &[fact(0, 0, 4000)], &[], 0);
2544 assert_eq!(
2545 r.admitted
2546 .iter()
2547 .filter(|l| l.contains("elided to identity"))
2548 .count(),
2549 0,
2550 "identity must NOT be admitted under a too-wide bound"
2551 );
2552 assert!(!r.stream_changed, "declined identity ⇒ op stream untouched");
2553 assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
2554 assert!(
2555 r.declined
2556 .iter()
2557 .any(|d| d.contains("not the identity") && d.contains("counterexample")),
2558 "identity decline must be loud and carry a model: {:?}",
2559 r.declined
2560 );
2561 }
2562
2563 #[test]
2564 fn const_rem_u_variable_divisor_declines_identity_and_keeps_op_494() {
2565 // Divisor is a param (local.get 1), NOT a literal — the op carries a
2566 // trapping zero-guard, so the whole op is NOT deletable. Identity
2567 // declines; the general div/rem path stands (op survives). A
2568 // divisor-nonzero fact still discharges the ZERO GUARD (guard elision),
2569 // but never the op deletion.
2570 let ops = vec![
2571 LocalGet(0), // 0 x ← range fact [0, 999]
2572 LocalGet(1), // 1 d ← divisor-nonzero fact
2573 I32RemU, // 2 x % d
2574 End, // 3
2575 ];
2576 let facts = [fact(0, 0, 999), nonzero_fact(1)];
2577 let r = specialize_function("f", &ops, &[], &facts, &[], 0);
2578 assert!(!r.stream_changed, "variable divisor ⇒ op never deleted");
2579 assert_eq!(r.ops, ops);
2580 assert_eq!(
2581 r.elide_div_zero,
2582 vec![2],
2583 "zero guard still discharged by the nonzero fact"
2584 );
2585 assert!(
2586 r.declined
2587 .iter()
2588 .any(|d| d.contains("not a literal i32.const")),
2589 "identity must decline on a non-literal divisor: {:?}",
2590 r.declined
2591 );
2592 }
2593
2594 #[test]
2595 fn const_rem_u_no_fact_declines_byte_identically_494() {
2596 // No range fact on the dividend ⇒ identity declines; the general path
2597 // runs. The literal divisor is nonzero so the zero-guard is elidable
2598 // ONLY with a fact — here there's none, so nothing is admitted and the
2599 // stream is untouched.
2600 let ops = const_rem_ops();
2601 let r = specialize_function("gust_scale", &ops, &[], &[fact(99, 0, 0)], &[], 0);
2602 assert!(!r.stream_changed, "no dividend fact ⇒ op stream untouched");
2603 assert_eq!(r.ops, ops);
2604 assert_eq!(
2605 r.admitted
2606 .iter()
2607 .filter(|l| l.contains("elided to identity"))
2608 .count(),
2609 0
2610 );
2611 }
2612
2613 #[test]
2614 fn const_rem_u_pow2_divisor_still_elides_under_narrow_bound_494() {
2615 // Even a power-of-two divisor elides when the dividend is proven narrow
2616 // (the win is smaller — clang folds pow2 rem to an `and` — but the
2617 // proof is identical and sound). x ∈ [0, 255], x rem_u 256 == x.
2618 let ops = vec![
2619 LocalGet(0), // 0 ← fact [0, 255]
2620 I32Const(256), // 1
2621 I32RemU, // 2
2622 End, // 3
2623 ];
2624 let r = specialize_function("f", &ops, &[], &[fact(0, 0, 255)], &[], 0);
2625 assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
2626 assert_eq!(r.ops, vec![LocalGet(0), End]);
2627 }
2628}