Skip to main content

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//! # Flag gating
87//!
88//! The driver only invokes this pass when `SYNTH_FACT_SPEC` is set (default
89//! OFF) AND the module carried a parseable `wsc.facts` section. Frozen
90//! fixtures carry no facts section, so every frozen anchor is bit-identical
91//! trivially; with the flag off the pass does not run at all.
92
93use crate::solver::{CheckOutcome, new_solver};
94use crate::term::{BV, Bool};
95use crate::wasm_semantics::WasmSemantics;
96use std::collections::{HashMap, HashSet};
97use synth_core::WasmOp;
98use synth_core::wsc_facts::{FactKind, WscFact};
99
100/// Outcome of specializing one function. `ops`/`block_arity`/`kept` are only
101/// meaningful when [`changed`](Self::changed) — otherwise they echo the input.
102#[derive(Debug)]
103pub struct FactSpecResult {
104    /// The (possibly rewritten) op stream.
105    pub ops: Vec<WasmOp>,
106    /// The blocktype-arity side-table matching `ops` (one entry per
107    /// `Block`/`Loop`/`If` in op order — entries of deleted openers removed).
108    pub block_arity: Vec<(u8, u8)>,
109    /// Indices into the ORIGINAL op stream that were kept, in order. Lets the
110    /// driver filter parallel side-tables (e.g. `op_offsets` for DWARF).
111    pub kept: Vec<usize>,
112    /// One certificate line per ADMITTED elision (logged per function).
113    pub admitted: Vec<String>,
114    /// One line per LOUD DECLINE (the general lowering is emitted for these).
115    pub declined: Vec<String>,
116    /// #494 phase 2b: indices (into the RETURNED `ops` stream) of div/rem
117    /// ops whose divide-by-zero trap guard was certificate-elided
118    /// (`UNSAT(P ∧ divisor == 0)` discharged per site).
119    pub elide_div_zero: Vec<usize>,
120    /// #494 phase 2b: indices (into the RETURNED `ops` stream) of `div_s`
121    /// ops whose `INT_MIN / -1` overflow guard was certificate-elided — a
122    /// SEPARATE obligation (`UNSAT(P ∧ dividend == INT_MIN ∧ divisor == -1)`);
123    /// a divisor-nonzero fact alone never lands here (#633/#634).
124    pub elide_div_ovf: Vec<usize>,
125    /// True when `ops` differs from the input (at least one region deletion).
126    stream_changed: bool,
127}
128
129impl FactSpecResult {
130    /// True when the op STREAM was rewritten (region deletions). Guard-elision
131    /// marks do not rewrite the stream — check
132    /// [`elide_div_zero`](Self::elide_div_zero) /
133    /// [`elide_div_ovf`](Self::elide_div_ovf) separately.
134    pub fn changed(&self) -> bool {
135        self.stream_changed
136    }
137}
138
139/// A symbolic operand-stack slot.
140#[derive(Clone)]
141struct Val {
142    bv: BV,
143    /// Start of the contiguous, side-effect-free op range that produced this
144    /// value — `None` when the producing slice is impure (`local.tee`) or not
145    /// provably contiguous. Only a `Some` slice may be deleted.
146    start: Option<usize>,
147    /// Index of the op that (last) produced this value.
148    created: usize,
149}
150
151/// Specialize one function's op stream against its `wsc.facts` premises.
152///
153/// `block_arity` is the decoder's ordinal side-table (one `(params, results)`
154/// entry per `Block`/`Loop`/`If` in op order); `facts` is the per-function
155/// slice (`CompileConfig::current_func_facts`); `params_i64` is the declared
156/// param-width table (`CompileConfig::current_func_params_i64` — `true` ⇒
157/// param `k` is 64-bit), which fixes the symbolic width of a param
158/// `local.get` (Phase 2b tracks i64 divisors). Total: every input yields a
159/// result — inapplicable shapes surface as loud declines, never errors.
160pub fn specialize_function(
161    func_name: &str,
162    ops: &[WasmOp],
163    block_arity: &[(u8, u8)],
164    facts: &[WscFact],
165    params_i64: &[bool],
166) -> FactSpecResult {
167    let mut pass = Pass::new(func_name, ops, block_arity, facts, params_i64);
168    pass.walk();
169    pass.finish()
170}
171
172/// #494 phase 2b RED-TEAM lever (debug builds ONLY): treat a Sat verdict on
173/// the divide-by-zero guard obligation as an admit anyway. Exists so the
174/// differential oracle can DEMONSTRATE the divergence an unsound admit would
175/// cause (wasmtime traps at divisor == 0, the forced build does not) and then
176/// show the Sat-decline restoring the guard byte-identically. Compiled out of
177/// release builds; every forced admit screams in its certificate line.
178#[cfg(debug_assertions)]
179fn force_admit_unsound() -> bool {
180    std::env::var("SYNTH_FACT_SPEC_FORCE_ADMIT").is_ok_and(|v| v != "0")
181}
182
183#[cfg(not(debug_assertions))]
184fn force_admit_unsound() -> bool {
185    false
186}
187
188struct Pass<'a> {
189    func: &'a str,
190    ops: &'a [WasmOp],
191    block_arity: &'a [(u8, u8)],
192    /// op index → ordinal into `block_arity` (for `Block`/`Loop`/`If` ops).
193    opener_ordinal: HashMap<usize, usize>,
194    /// op index → signed range fact attached to that op's result (raw s64
195    /// bounds; clamped to the value's width at attach time).
196    range_facts: HashMap<usize, (i64, i64)>,
197    /// op indices carrying a divisor-nonzero fact (kind 3): `value ≠ 0`.
198    nonzero_facts: HashSet<usize>,
199    /// Declared param widths (`true` ⇒ 64-bit) — fixes `local.get` widths.
200    params_i64: &'a [bool],
201    sem: WasmSemantics,
202    stack: Vec<Val>,
203    locals: HashMap<u32, BV>,
204    /// Every fresh variable created (name order), for Sat counterexamples.
205    vars: Vec<BV>,
206    fresh: u32,
207    premises: Vec<Bool>,
208    premise_desc: Vec<String>,
209    /// Inclusive op-index ranges to delete (disjoint, ascending).
210    deletions: Vec<(usize, usize)>,
211    admitted: Vec<String>,
212    declined: Vec<String>,
213    /// #494 phase 2b: ORIGINAL op indices marked for zero-guard elision.
214    zero_marks: Vec<usize>,
215    /// #494 phase 2b: ORIGINAL op indices marked for overflow-guard elision.
216    ovf_marks: Vec<usize>,
217}
218
219impl<'a> Pass<'a> {
220    fn new(
221        func: &'a str,
222        ops: &'a [WasmOp],
223        block_arity: &'a [(u8, u8)],
224        facts: &'a [WscFact],
225        params_i64: &'a [bool],
226    ) -> Self {
227        let mut opener_ordinal = HashMap::new();
228        let mut ord = 0usize;
229        for (i, op) in ops.iter().enumerate() {
230            if matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If) {
231                opener_ordinal.insert(i, ord);
232                ord += 1;
233            }
234        }
235        let mut range_facts = HashMap::new();
236        let mut nonzero_facts = HashSet::new();
237        for f in facts {
238            // Out-of-range value_id is vacuous (encoding doc's rule).
239            if (f.value_id as usize) >= ops.len() {
240                continue;
241            }
242            match f.kind {
243                FactKind::ValueRange { lo, hi } => {
244                    // Raw s64 bounds; clamped to the value's width when the
245                    // walk attaches the premise. An inverted bound is vacuous.
246                    if lo <= hi {
247                        range_facts.insert(f.value_id as usize, (lo, hi));
248                    }
249                }
250                // #494 phase 2b: divisor-nonzero (kind 3) — `value ≠ 0`.
251                FactKind::DivisorNonZero => {
252                    nonzero_facts.insert(f.value_id as usize);
253                }
254                _ => {}
255            }
256        }
257        Self {
258            func,
259            ops,
260            block_arity,
261            opener_ordinal,
262            range_facts,
263            nonzero_facts,
264            params_i64,
265            // No memory model needed: memory ops are outside the tracked
266            // fragment (the walk stops there).
267            sem: WasmSemantics::new_with_memory(Vec::new()),
268            stack: Vec::new(),
269            locals: HashMap::new(),
270            vars: Vec::new(),
271            fresh: 0,
272            premises: Vec::new(),
273            premise_desc: Vec::new(),
274            deletions: Vec::new(),
275            admitted: Vec::new(),
276            declined: Vec::new(),
277            zero_marks: Vec::new(),
278            ovf_marks: Vec::new(),
279        }
280    }
281
282    fn fresh_var(&mut self, name: String) -> BV {
283        let v = BV::new_const(name, 32);
284        self.vars.push(v.clone());
285        v
286    }
287
288    fn local_bv(&mut self, idx: u32) -> BV {
289        if let Some(bv) = self.locals.get(&idx) {
290            return bv.clone();
291        }
292        // A not-yet-seen local's width comes from the declared param table
293        // (#494 phase 2b tracks i64 divisors); non-param locals default to
294        // 32 bits — an i64 op reading one fails the width check and declines.
295        let width = if self.params_i64.get(idx as usize).copied().unwrap_or(false) {
296            64
297        } else {
298            32
299        };
300        let v = BV::new_const(format!("fs_l{idx}"), width);
301        self.vars.push(v.clone());
302        self.locals.insert(idx, v.clone());
303        v
304    }
305
306    /// Attach the premises of every fact naming op `i`'s result, at the
307    /// value's own width.
308    fn attach_fact(&mut self, i: usize, bv: &BV) {
309        let width = bv.get_size();
310        if let Some(&(lo, hi)) = self.range_facts.get(&i) {
311            // Clamp the s64 bound to the value's width (the phase-2 rule for
312            // 32-bit values; 64-bit values take the bound verbatim). A bound
313            // that inverts after clamping is impossible for a genuine value
314            // of this width — fact validity is loom's obligation (trust
315            // split), so we keep the phase-2 clamp semantics unchanged.
316            let (lo, hi) = if width == 32 {
317                (
318                    lo.clamp(i64::from(i32::MIN), i64::from(i32::MAX)),
319                    hi.clamp(i64::from(i32::MIN), i64::from(i32::MAX)),
320                )
321            } else {
322                (lo, hi)
323            };
324            let lo_bv = BV::from_i64(lo, width);
325            let hi_bv = BV::from_i64(hi, width);
326            let p = Bool::and(&[&bv.bvsge(&lo_bv), &bv.bvsle(&hi_bv)]);
327            self.premises.push(p);
328            self.premise_desc
329                .push(format!("value(op#{i}) ∈ [{lo}, {hi}] (signed, i{width})"));
330        }
331        if self.nonzero_facts.contains(&i) {
332            // #494 phase 2b: divisor-nonzero (kind 3).
333            let p = bv.ne(BV::from_i64(0, width));
334            self.premises.push(p);
335            self.premise_desc
336                .push(format!("value(op#{i}) ≠ 0 (i{width})"));
337        }
338    }
339
340    fn push(&mut self, bv: BV, start: Option<usize>, created: usize) {
341        self.stack.push(Val { bv, start, created });
342    }
343
344    /// Find the matching `End` for the opener at `i`; also reports whether a
345    /// top-level `Else` occurs. `None` = malformed nesting (stop the walk).
346    fn matching_end(&self, i: usize) -> Option<(usize, bool)> {
347        let mut depth = 0usize;
348        let mut has_else = false;
349        for (j, op) in self.ops.iter().enumerate().skip(i + 1) {
350            match op {
351                WasmOp::Block | WasmOp::Loop | WasmOp::If => depth += 1,
352                WasmOp::Else if depth == 0 => has_else = true,
353                WasmOp::End => {
354                    if depth == 0 {
355                        return Some((j, has_else));
356                    }
357                    depth -= 1;
358                }
359                _ => {}
360            }
361        }
362        None
363    }
364
365    /// Continuation after a DECLINED `if` region `[i..=end]`: the body may or
366    /// may not run, so havoc every local it assigns (any depth) and model its
367    /// block results as fresh variables.
368    fn havoc_region(&mut self, i: usize, end: usize, arity: (u8, u8)) {
369        let ops = self.ops;
370        for op in &ops[i + 1..end] {
371            if let WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) = op {
372                let n = self.fresh;
373                self.fresh += 1;
374                let v = self.fresh_var(format!("fs_h{n}"));
375                self.locals.insert(*idx, v);
376            }
377        }
378        for _ in 0..arity.0 {
379            self.stack.pop();
380        }
381        for k in 0..arity.1 {
382            let n = self.fresh;
383            self.fresh += 1;
384            let v = self.fresh_var(format!("fs_r{n}_{k}"));
385            self.push(v, None, end);
386        }
387    }
388
389    fn decline(&mut self, msg: String) {
390        self.declined
391            .push(format!("{}: {} — general lowering emitted", self.func, msg));
392    }
393
394    /// #494 phase 2b: discharge the per-site div/rem trap-guard obligations
395    /// for the op at `i` (`op_name`, operand width `width`, dividend `a`,
396    /// divisor `b`), recording elision marks for the lowering. TWO independent
397    /// obligations (the #633/#634 two-guard distinction):
398    ///
399    /// - zero guard (every div/rem): `UNSAT(P ∧ divisor == 0)`;
400    /// - overflow guard (`div_s` only): `UNSAT(P ∧ dividend == INT_MIN ∧
401    ///   divisor == -1)` — divisor-nonzero alone NEVER discharges this.
402    ///
403    /// Sat / Unknown / no-premise ⇒ loud decline; the guard is emitted.
404    fn try_elide_div_guards(&mut self, i: usize, op_name: &str, is_div_s: bool, a: &Val, b: &Val) {
405        let width = b.bv.get_size();
406        if self.premises.is_empty() {
407            self.decline(format!(
408                "op#{i} {op_name} — no premise reaches this site; both trap guards retained"
409            ));
410            return;
411        }
412        // Obligation 1: the divide-by-zero guard.
413        let mut solver = new_solver();
414        for p in &self.premises {
415            solver.assert(p);
416        }
417        solver.assert(&b.bv.eq(BV::from_i64(0, width)));
418        match solver.check() {
419            CheckOutcome::Unsat => {
420                self.zero_marks.push(i);
421                self.admitted.push(format!(
422                    "{}: 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 = {}",
423                    self.func,
424                    solver.name(),
425                    self.premise_desc.join(" ∧ "),
426                    b.bv,
427                ));
428            }
429            CheckOutcome::Sat => {
430                let cex = self.counterexample(solver.as_ref());
431                if force_admit_unsound() {
432                    // RED-TEAM lever (debug builds only): admit the Sat site
433                    // anyway so the differential oracle can demonstrate the
434                    // divergence. Screams, and still logs the model.
435                    self.zero_marks.push(i);
436                    self.admitted.push(format!(
437                        "{}: 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",
438                        self.func,
439                    ));
440                } else {
441                    self.decline(format!(
442                        "op#{i} {op_name} — zero-guard obligation Sat (divisor can                          be 0 under P; counterexample: {cex}); guard retained"
443                    ));
444                }
445            }
446            CheckOutcome::Unknown(reason) => {
447                self.decline(format!(
448                    "op#{i} {op_name} — zero-guard obligation Unknown ({reason});                      conservative decline, guard retained"
449                ));
450            }
451        }
452        // Obligation 2: the INT_MIN/-1 overflow guard — div_s only, and a
453        // SEPARATE proof (#633/#634): divisor ≠ 0 does not exclude -1.
454        if !is_div_s {
455            return;
456        }
457        let int_min = if width == 64 {
458            i64::MIN
459        } else {
460            i64::from(i32::MIN)
461        };
462        let mut solver = new_solver();
463        for p in &self.premises {
464            solver.assert(p);
465        }
466        solver.assert(&a.bv.eq(BV::from_i64(int_min, width)));
467        solver.assert(&b.bv.eq(BV::from_i64(-1, width)));
468        match solver.check() {
469            CheckOutcome::Unsat => {
470                self.ovf_marks.push(i);
471                self.admitted.push(format!(
472                    "{}: 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 = {{{}}}",
473                    self.func,
474                    solver.name(),
475                    self.premise_desc.join(" ∧ "),
476                ));
477            }
478            CheckOutcome::Sat => {
479                let cex = self.counterexample(solver.as_ref());
480                self.decline(format!(
481                    "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"
482                ));
483            }
484            CheckOutcome::Unknown(reason) => {
485                self.decline(format!(
486                    "op#{i} {op_name} — overflow-guard obligation Unknown ({reason});                      conservative decline, the #633 overflow guard is RETAINED"
487                ));
488            }
489        }
490    }
491
492    /// Read the model back for an actionable counterexample string.
493    fn counterexample(&self, solver: &dyn crate::solver::BvSolver) -> String {
494        let cex: Vec<String> = self
495            .vars
496            .iter()
497            .filter_map(|v| {
498                let name = format!("{v}");
499                solver.value(v).map(|x| {
500                    if v.get_size() == 64 {
501                        format!("{name}={}", x as u64 as i64)
502                    } else {
503                        format!("{name}={}", x as u32 as i32)
504                    }
505                })
506            })
507            .collect();
508        if cex.is_empty() {
509            "<no model>".to_string()
510        } else {
511            cex.join(", ")
512        }
513    }
514
515    /// Discharge the per-elision obligation for the no-`else` `if` at `i`
516    /// (matching `End` at `end`, condition `cond`). Returns true iff admitted.
517    fn try_elide(&mut self, i: usize, end: usize, cond: &Val) -> bool {
518        if self.premises.is_empty() {
519            self.decline(format!(
520                "op#{i} `if` — no premise reaches this site (no usable value-range fact)"
521            ));
522            return false;
523        }
524        let mut solver = new_solver();
525        for p in &self.premises {
526            solver.assert(p);
527        }
528        let taken = cond.bv.ne(BV::from_i64(0, 32));
529        solver.assert(&taken);
530        match solver.check() {
531            CheckOutcome::Unsat => {
532                let Some(start) = cond.start else {
533                    // Proven dead, but the condition slice has a side effect
534                    // (`local.tee`) or is not provably contiguous — deleting
535                    // it could drop live work. Conservative: keep everything.
536                    self.decline(format!(
537                        "op#{i} `if` proven dead (UNSAT) but its condition slice is not \
538                         erasable (impure or non-contiguous producer)"
539                    ));
540                    return false;
541                };
542                self.deletions.push((start, end));
543                self.admitted.push(format!(
544                    "{}: op#{i} `if` (+condition slice) — ops [{start}..={end}] elided \
545                     ({} ops): UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; \
546                     every Unsat carries an LRAT proof validated by ordeal-lrat); \
547                     P = {{{}}}; cond = {}",
548                    self.func,
549                    end - start + 1,
550                    solver.name(),
551                    self.premise_desc.join(" ∧ "),
552                    cond.bv,
553                ));
554                true
555            }
556            CheckOutcome::Sat => {
557                // Read the model back for an actionable counterexample.
558                let cex: Vec<String> = self
559                    .vars
560                    .iter()
561                    .filter_map(|v| {
562                        let name = format!("{v}");
563                        solver
564                            .value(v)
565                            .map(|x| format!("{name}={}", x as u32 as i32))
566                    })
567                    .collect();
568                self.decline(format!(
569                    "op#{i} `if` — obligation Sat (branch reachable under P; \
570                     counterexample: {})",
571                    if cex.is_empty() {
572                        "<no model>".to_string()
573                    } else {
574                        cex.join(", ")
575                    }
576                ));
577                false
578            }
579            CheckOutcome::Unknown(reason) => {
580                self.decline(format!(
581                    "op#{i} `if` — obligation Unknown ({reason}); conservative decline"
582                ));
583                false
584            }
585        }
586    }
587
588    /// Discharge the select-collapse obligation for the branchless `select`
589    /// at op `i`. The operands are `(val1, val2, cond)` in wasm select order
590    /// (`val1` deepest, `cond` on top); the runtime result is
591    /// `(cond != 0) ? val1 : val2`. When a value-range premise pins the
592    /// condition CONSTANT under `P` the select collapses to one operand — the
593    /// branchless sibling of the Phase-2 no-else `if` elision, and the shape
594    /// gust_mix's `clamp` actually uses (`max`/`min` via `select`).
595    ///
596    /// Two mutually exclusive obligations, both certificate-checked QF_BV.
597    /// `UNSAT(P ∧ cond ≠ 0)` means `cond` is always 0, so the result is `val2`
598    /// (delete `val1`'s producer slice plus the condition-slice-through-
599    /// `select`). `UNSAT(P ∧ cond == 0)` means `cond` is always non-zero, so
600    /// the result is `val1` (delete the `val2`+condition+`select` contiguous
601    /// slice). Anything else (Sat both ways = genuinely non-constant; Unknown;
602    /// an impure/non-contiguous erasable slice) DECLINES LOUDLY and the general
603    /// branchless select stands. Returns the surviving operand's `Val` on
604    /// admit, `None` on decline.
605    fn try_collapse_select(&mut self, i: usize, val1: &Val, val2: &Val, cond: &Val) -> Option<Val> {
606        if self.premises.is_empty() {
607            self.decline(format!(
608                "op#{i} select — no premise reaches this site (no usable value-range fact)"
609            ));
610            return None;
611        }
612        // The condition slice must be a pure, contiguous producer ending
613        // immediately before the `select` — otherwise deleting it could drop
614        // live work (`local.tee`) or leave a gap.
615        let Some(sc) = cond.start else {
616            self.decline(format!(
617                "op#{i} select — condition slice is impure or non-contiguous (not erasable)"
618            ));
619            return None;
620        };
621        if cond.created + 1 != i {
622            self.decline(format!(
623                "op#{i} select — condition is not produced immediately before the select \
624                 (non-contiguous)"
625            ));
626            return None;
627        }
628
629        // Obligation B first (the clamp shape: the guard condition is
630        // constant-FALSE, so the identity operand `val2` survives).
631        let mut solver = new_solver();
632        for p in &self.premises {
633            solver.assert(p);
634        }
635        solver.assert(&cond.bv.ne(BV::from_i64(0, 32)));
636        match solver.check() {
637            CheckOutcome::Unsat => {
638                // cond ≡ 0 ⇒ result = val2. Delete val1's slice AND the
639                // condition-slice-through-select. val2 (kept) sits between.
640                let Some(s1) = val1.start else {
641                    self.decline(format!(
642                        "op#{i} select proven false-arm (UNSAT cond ≠ 0) but the val1 slice \
643                         is not erasable (impure/non-contiguous) — collapse declined"
644                    ));
645                    return None;
646                };
647                if val1.created >= sc {
648                    self.decline(format!(
649                        "op#{i} select — val1 slice overlaps the condition slice; collapse \
650                         declined"
651                    ));
652                    return None;
653                }
654                self.deletions.push((s1, val1.created));
655                self.deletions.push((sc, i));
656                self.admitted.push(format!(
657                    "{}: op#{i} select — collapsed to the false-arm (val2): \
658                     UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; every Unsat \
659                     carries an LRAT proof validated by ordeal-lrat); deleted val1 slice \
660                     [{s1}..={}] + condition/select [{sc}..={i}]; P = {{{}}}; cond = {}",
661                    self.func,
662                    solver.name(),
663                    val1.created,
664                    self.premise_desc.join(" ∧ "),
665                    cond.bv,
666                ));
667                return Some(val2.clone());
668            }
669            CheckOutcome::Sat => { /* cond can be non-zero — try obligation A */ }
670            CheckOutcome::Unknown(reason) => {
671                self.decline(format!(
672                    "op#{i} select — false-arm obligation Unknown ({reason}); conservative \
673                     decline"
674                ));
675                return None;
676            }
677        }
678
679        // Obligation A: cond ≡ non-zero ⇒ result = val1. Delete the
680        // val2+condition+select contiguous slice.
681        let mut solver = new_solver();
682        for p in &self.premises {
683            solver.assert(p);
684        }
685        solver.assert(&cond.bv.eq(BV::from_i64(0, 32)));
686        match solver.check() {
687            CheckOutcome::Unsat => {
688                let Some(s2) = val2.start else {
689                    self.decline(format!(
690                        "op#{i} select proven true-arm (UNSAT cond == 0) but the val2 slice \
691                         is not erasable (impure/non-contiguous) — collapse declined"
692                    ));
693                    return None;
694                };
695                if val2.created + 1 != sc {
696                    self.decline(format!(
697                        "op#{i} select — val2 slice not adjacent to the condition slice \
698                         (non-contiguous); collapse declined"
699                    ));
700                    return None;
701                }
702                // [s2 ..= i] is one contiguous pure range (val2 + cond + select).
703                self.deletions.push((s2, i));
704                self.admitted.push(format!(
705                    "{}: op#{i} select — collapsed to the true-arm (val1): \
706                     UNSAT(P ∧ cond == 0) via {} (certificate-checked QF_BV; every Unsat \
707                     carries an LRAT proof validated by ordeal-lrat); deleted val2/condition/\
708                     select [{s2}..={i}]; P = {{{}}}; cond = {}",
709                    self.func,
710                    solver.name(),
711                    self.premise_desc.join(" ∧ "),
712                    cond.bv,
713                ));
714                Some(val1.clone())
715            }
716            CheckOutcome::Sat => {
717                let cex = self.counterexample(solver.as_ref());
718                self.decline(format!(
719                    "op#{i} select — condition is not constant under P (both arms reachable; \
720                     counterexample: {cex}); branchless select retained"
721                ));
722                None
723            }
724            CheckOutcome::Unknown(reason) => {
725                self.decline(format!(
726                    "op#{i} select — true-arm obligation Unknown ({reason}); conservative \
727                     decline"
728                ));
729                None
730            }
731        }
732    }
733
734    fn walk(&mut self) {
735        if self.range_facts.is_empty() && self.nonzero_facts.is_empty() {
736            self.decline(
737                "no usable value-range or divisor-nonzero fact targets this function".to_string(),
738            );
739            return;
740        }
741        let ops = self.ops;
742        let mut i = 0usize;
743        while i < ops.len() {
744            let op = &ops[i];
745            match op {
746                WasmOp::Nop => {}
747                WasmOp::I32Const(v) => {
748                    let bv = self.sem.encode_op(&WasmOp::I32Const(*v), &[]);
749                    self.attach_fact(i, &bv);
750                    self.push(bv, Some(i), i);
751                }
752                // #494 phase 2b: tracked so an i64 divisor term can be built
753                // (the i64 fragment is const/local/div-rem only — anything
754                // else stops the walk as before).
755                WasmOp::I64Const(v) => {
756                    let bv = self.sem.encode_op(&WasmOp::I64Const(*v), &[]);
757                    self.attach_fact(i, &bv);
758                    self.push(bv, Some(i), i);
759                }
760                WasmOp::LocalGet(idx) => {
761                    let bv = self.local_bv(*idx);
762                    self.attach_fact(i, &bv);
763                    self.push(bv, Some(i), i);
764                }
765                WasmOp::LocalSet(idx) => {
766                    let Some(v) = self.stack.pop() else {
767                        self.decline(format!("op#{i} local.set on empty symbolic stack"));
768                        return;
769                    };
770                    self.locals.insert(*idx, v.bv);
771                }
772                WasmOp::LocalTee(idx) => {
773                    let Some(top) = self.stack.last_mut() else {
774                        self.decline(format!("op#{i} local.tee on empty symbolic stack"));
775                        return;
776                    };
777                    // The tee is a side effect: its slice must never be
778                    // deleted as a "pure condition producer".
779                    top.start = None;
780                    top.created = i;
781                    let bv = top.bv.clone();
782                    self.locals.insert(*idx, bv.clone());
783                    self.attach_fact(i, &bv);
784                }
785                WasmOp::Drop => {
786                    if self.stack.pop().is_none() {
787                        self.decline(format!("op#{i} drop on empty symbolic stack"));
788                        return;
789                    }
790                }
791                WasmOp::I32Eqz => {
792                    let Some(a) = self.stack.pop() else {
793                        self.decline(format!("op#{i} unary op on empty symbolic stack"));
794                        return;
795                    };
796                    if a.bv.get_size() != 32 {
797                        self.decline(format!("op#{i} i32.eqz on a non-32-bit operand"));
798                        return;
799                    }
800                    let bv = self.sem.encode_op(op, &[a.bv]);
801                    self.attach_fact(i, &bv);
802                    let start = a.start.filter(|_| a.created + 1 == i);
803                    self.push(bv, start, i);
804                }
805                // Tracked, trap-free i32 binops (div/rem excluded on purpose:
806                // they can trap, and a deleted slice must be effect-free).
807                WasmOp::I32Add
808                | WasmOp::I32Sub
809                | WasmOp::I32Mul
810                | WasmOp::I32And
811                | WasmOp::I32Or
812                | WasmOp::I32Xor
813                | WasmOp::I32Shl
814                | WasmOp::I32ShrS
815                | WasmOp::I32ShrU
816                | WasmOp::I32Rotl
817                | WasmOp::I32Rotr
818                | WasmOp::I32Eq
819                | WasmOp::I32Ne
820                | WasmOp::I32LtS
821                | WasmOp::I32LtU
822                | WasmOp::I32LeS
823                | WasmOp::I32LeU
824                | WasmOp::I32GtS
825                | WasmOp::I32GtU
826                | WasmOp::I32GeS
827                | WasmOp::I32GeU => {
828                    let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
829                        self.decline(format!("op#{i} binop on underflowing symbolic stack"));
830                        return;
831                    };
832                    if a.bv.get_size() != 32 || b.bv.get_size() != 32 {
833                        self.decline(format!("op#{i} i32 binop on a non-32-bit operand"));
834                        return;
835                    }
836                    let bv = self.sem.encode_op(op, &[a.bv, b.bv]);
837                    self.attach_fact(i, &bv);
838                    // Contiguity proof for the combined producer slice:
839                    // a's slice, immediately followed by b's, immediately
840                    // followed by this op. Anything else ⇒ not erasable.
841                    let start = match (a.start, b.start) {
842                        (Some(sa), Some(sb)) if a.created + 1 == sb && b.created + 1 == i => {
843                            Some(sa)
844                        }
845                        _ => None,
846                    };
847                    self.push(bv, start, i);
848                }
849                // #494 phase 2b: i32/i64 div/rem — TRACKED (upgrading the
850                // phase-2 hard stop), never DELETED. The op can trap, so its
851                // result carries `start = None` (it can never sit inside an
852                // erasable condition slice); the walk instead discharges the
853                // per-site guard obligations (see the module docs' two-guard
854                // distinction) and marks the op for the lowering. Downstream
855                // soundness: if the op traps, nothing after it executes (any
856                // later admitted elision is vacuous on that path); if it does
857                // not, its result is havocked to a fresh variable.
858                WasmOp::I32DivU
859                | WasmOp::I32DivS
860                | WasmOp::I32RemU
861                | WasmOp::I32RemS
862                | WasmOp::I64DivU
863                | WasmOp::I64DivS
864                | WasmOp::I64RemU
865                | WasmOp::I64RemS => {
866                    let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
867                        self.decline(format!("op#{i} div/rem on underflowing symbolic stack"));
868                        return;
869                    };
870                    let (op_name, expect, is_div_s) = match op {
871                        WasmOp::I32DivU => ("i32.div_u", 32, false),
872                        WasmOp::I32DivS => ("i32.div_s", 32, true),
873                        WasmOp::I32RemU => ("i32.rem_u", 32, false),
874                        WasmOp::I32RemS => ("i32.rem_s", 32, false),
875                        WasmOp::I64DivU => ("i64.div_u", 64, false),
876                        WasmOp::I64DivS => ("i64.div_s", 64, true),
877                        WasmOp::I64RemU => ("i64.rem_u", 64, false),
878                        _ => ("i64.rem_s", 64, false),
879                    };
880                    if a.bv.get_size() != expect || b.bv.get_size() != expect {
881                        self.decline(format!(
882                            "op#{i} {op_name} on operands of unexpected width                              (symbolic widths {}/{}, expected {expect})",
883                            a.bv.get_size(),
884                            b.bv.get_size()
885                        ));
886                        return;
887                    }
888                    self.try_elide_div_guards(i, op_name, is_div_s, &a, &b);
889                    // Havoc the result; `start = None` keeps a possibly-
890                    // trapping op out of every erasable condition slice.
891                    let n = self.fresh;
892                    self.fresh += 1;
893                    let v = self.fresh_var(format!("fs_d{n}"));
894                    self.attach_fact(i, &v);
895                    self.push(v, None, i);
896                }
897                WasmOp::If => {
898                    let Some(cond) = self.stack.pop() else {
899                        self.decline(format!("op#{i} `if` on empty symbolic stack"));
900                        return;
901                    };
902                    if cond.bv.get_size() != 32 {
903                        self.decline(format!("op#{i} `if` condition is not 32-bit"));
904                        return;
905                    }
906                    let Some((end, has_else)) = self.matching_end(i) else {
907                        self.decline(format!("op#{i} `if` without matching `end`"));
908                        return;
909                    };
910                    let Some(&ord) = self.opener_ordinal.get(&i) else {
911                        self.decline(format!("op#{i} `if` missing from the opener ordinal map"));
912                        return;
913                    };
914                    let Some(&arity) = self.block_arity.get(ord) else {
915                        self.decline(format!(
916                            "op#{i} `if` has no block_arity entry (side-table desync)"
917                        ));
918                        return;
919                    };
920                    if has_else {
921                        self.decline(format!(
922                            "op#{i} `if`/`else` — only no-else `if` is in Phase-2 scope"
923                        ));
924                        self.havoc_region(i, end, arity);
925                    } else if self.try_elide(i, end, &cond) {
926                        // Region provably never executes: state unchanged
927                        // (params-as-results pass-through is the identity for
928                        // a no-else `if`, whose blocktype has equal
929                        // param/result types by wasm validation).
930                    } else {
931                        self.havoc_region(i, end, arity);
932                    }
933                    i = end + 1;
934                    continue;
935                }
936                // #494 Phase 3: branchless `select` — the sibling of the
937                // Phase-2 no-else `if` elision, and the shape gust_mix's
938                // clamp actually lowers to (`max`/`min` via select). A
939                // value-range premise that pins the condition constant
940                // collapses it to one operand (stream deletion, like `if`).
941                WasmOp::Select => {
942                    let (Some(cond), Some(val2), Some(val1)) =
943                        (self.stack.pop(), self.stack.pop(), self.stack.pop())
944                    else {
945                        self.decline(format!("op#{i} select on underflowing symbolic stack"));
946                        return;
947                    };
948                    // Only the plain i32 `select` (0x1B) is tracked; a typed
949                    // select over i64/f-operands declines and havocs.
950                    if cond.bv.get_size() != 32
951                        || val1.bv.get_size() != 32
952                        || val2.bv.get_size() != 32
953                    {
954                        self.decline(format!(
955                            "op#{i} select on non-32-bit operand(s) — only i32 select is tracked"
956                        ));
957                        let v = self.fresh_var(format!("fs_sel{i}"));
958                        self.push(v, None, i);
959                        i += 1;
960                        continue;
961                    }
962                    match self.try_collapse_select(i, &val1, &val2, &cond) {
963                        // Admitted: the surviving operand's producer slice
964                        // stays; the other operand + condition slice + the
965                        // `select` were recorded for deletion. `start = None`
966                        // keeps the collapsed result out of any later erasable
967                        // slice (conservative).
968                        Some(surviving) => self.push(surviving.bv, None, i),
969                        // Declined (loud): the branchless select stands. Havoc
970                        // the result — a fresh var means any obligation over it
971                        // downstream is Sat, so it can never seed an unsound
972                        // chained collapse.
973                        None => {
974                            let v = self.fresh_var(format!("fs_sel{i}"));
975                            self.push(v, None, i);
976                        }
977                    }
978                }
979                // Function-final `End` (top-level): done.
980                WasmOp::End => break,
981                WasmOp::Return => break,
982                other => {
983                    // First op outside the tracked fragment: stop. Everything
984                    // already admitted was justified independently of what
985                    // follows; declining the REST loudly keeps honesty.
986                    self.decline(format!(
987                        "op#{i} {other:?} is outside the tracked i32 fragment — \
988                         fact tracking stops here (no further elisions in this function)"
989                    ));
990                    return;
991                }
992            }
993            i += 1;
994        }
995    }
996
997    fn finish(self) -> FactSpecResult {
998        let Pass {
999            ops,
1000            block_arity,
1001            deletions,
1002            admitted,
1003            declined,
1004            zero_marks,
1005            ovf_marks,
1006            ..
1007        } = self;
1008        if deletions.is_empty() {
1009            return FactSpecResult {
1010                ops: ops.to_vec(),
1011                block_arity: block_arity.to_vec(),
1012                kept: (0..ops.len()).collect(),
1013                admitted,
1014                declined,
1015                // No rewrite ⇒ original indices ARE the output indices.
1016                elide_div_zero: zero_marks,
1017                elide_div_ovf: ovf_marks,
1018                stream_changed: false,
1019            };
1020        }
1021        let deleted = |i: usize| deletions.iter().any(|&(s, e)| i >= s && i <= e);
1022        let mut out_ops = Vec::with_capacity(ops.len());
1023        let mut out_arity = Vec::with_capacity(block_arity.len());
1024        let mut kept = Vec::with_capacity(ops.len());
1025        let mut ord = 0usize;
1026        for (i, op) in ops.iter().enumerate() {
1027            let is_opener = matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If);
1028            if !deleted(i) {
1029                out_ops.push(op.clone());
1030                kept.push(i);
1031                if is_opener && let Some(&a) = block_arity.get(ord) {
1032                    out_arity.push(a);
1033                }
1034            }
1035            if is_opener {
1036                ord += 1;
1037            }
1038        }
1039        // Remap the guard-elision marks into the REWRITTEN index space. A
1040        // marked div/rem can never sit inside a deleted range (deleted ranges
1041        // are contiguous PURE condition slices plus proven-dead `if` regions
1042        // the walk skipped over; a div result's `start = None` bars it from
1043        // any erasable slice) — the filter below is defense in depth.
1044        let remap = |marks: Vec<usize>| -> Vec<usize> {
1045            marks
1046                .into_iter()
1047                .filter_map(|m| {
1048                    debug_assert!(!deleted(m), "guard mark op#{m} inside a deleted range");
1049                    kept.binary_search(&m).ok()
1050                })
1051                .collect()
1052        };
1053        FactSpecResult {
1054            ops: out_ops,
1055            block_arity: out_arity,
1056            elide_div_zero: remap(zero_marks),
1057            elide_div_ovf: remap(ovf_marks),
1058            kept,
1059            admitted,
1060            declined,
1061            stream_changed: true,
1062        }
1063    }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069    use WasmOp::*;
1070
1071    fn fact(value_id: u32, lo: i64, hi: i64) -> WscFact {
1072        WscFact {
1073            func_index: 0,
1074            value_id,
1075            kind: FactKind::ValueRange { lo, hi },
1076        }
1077    }
1078
1079    /// The gust_mix clamp shape: clamp(ch + 476, 1000, 2000) via two
1080    /// no-else `if`s over a local.
1081    fn clamp_ops() -> Vec<WasmOp> {
1082        vec![
1083            LocalGet(0),    // 0   ch          ← fact target
1084            I32Const(476),  // 1
1085            I32Add,         // 2   v = ch+476
1086            LocalSet(1),    // 3
1087            LocalGet(1),    // 4
1088            I32Const(1000), // 5
1089            I32LtS,         // 6
1090            If,             // 7
1091            I32Const(1000), // 8
1092            LocalSet(1),    // 9
1093            End,            // 10
1094            LocalGet(1),    // 11
1095            I32Const(2000), // 12
1096            I32GtS,         // 13
1097            If,             // 14
1098            I32Const(2000), // 15
1099            LocalSet(1),    // 16
1100            End,            // 17
1101            LocalGet(1),    // 18
1102            End,            // 19
1103        ]
1104    }
1105
1106    const CLAMP_ARITY: &[(u8, u8)] = &[(0, 0), (0, 0)];
1107
1108    #[test]
1109    fn clamp_shape_elides_both_branches_under_the_proven_bound_494() {
1110        let ops = clamp_ops();
1111        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)], &[]);
1112        assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
1113        assert!(r.changed());
1114        assert_eq!(
1115            r.ops,
1116            vec![
1117                LocalGet(0),
1118                I32Const(476),
1119                I32Add,
1120                LocalSet(1),
1121                LocalGet(1),
1122                End
1123            ],
1124            "both clamp comparisons + branches + bodies must be gone"
1125        );
1126        assert_eq!(r.block_arity, vec![], "both If arity entries removed");
1127        assert_eq!(r.kept, vec![0, 1, 2, 3, 18, 19]);
1128        // The certificate evidence trail names the engine and the premise.
1129        for line in &r.admitted {
1130            assert!(line.contains("UNSAT"), "{line}");
1131            assert!(line.contains("certificate-checked"), "{line}");
1132            assert!(line.contains("[524, 1524]"), "{line}");
1133        }
1134    }
1135
1136    #[test]
1137    fn wrong_wide_bound_is_sat_and_declines_loudly_494() {
1138        // ch ∈ [0, 4000] does NOT make the clamp dead (ch=0 → v=476 < 1000):
1139        // the obligation is Sat and BOTH sites decline with a counterexample.
1140        let ops = clamp_ops();
1141        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 0, 4000)], &[]);
1142        assert_eq!(r.admitted.len(), 0);
1143        assert!(!r.changed());
1144        assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
1145        assert_eq!(r.block_arity, CLAMP_ARITY.to_vec());
1146        assert!(
1147            r.declined
1148                .iter()
1149                .any(|d| d.contains("Sat") && d.contains("counterexample")),
1150            "declines must be loud and carry a model: {:?}",
1151            r.declined
1152        );
1153    }
1154
1155    #[test]
1156    fn partially_dead_bound_elides_only_the_proven_branch_494() {
1157        // ch ∈ [524, 4000]: v ≥ 1000 so the LOW clamp is dead, but v can
1158        // exceed 2000 so the HIGH clamp must survive.
1159        let ops = clamp_ops();
1160        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 4000)], &[]);
1161        assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1162        assert_eq!(r.declined.len(), 1);
1163        assert_eq!(
1164            r.ops,
1165            vec![
1166                LocalGet(0),
1167                I32Const(476),
1168                I32Add,
1169                LocalSet(1),
1170                LocalGet(1),
1171                I32Const(2000),
1172                I32GtS,
1173                If,
1174                I32Const(2000),
1175                LocalSet(1),
1176                End,
1177                LocalGet(1),
1178                End,
1179            ]
1180        );
1181        assert_eq!(r.block_arity, vec![(0, 0)], "one If survives");
1182    }
1183
1184    #[test]
1185    fn no_facts_changes_nothing_494() {
1186        let ops = clamp_ops();
1187        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[], &[]);
1188        assert!(!r.changed());
1189        assert_eq!(r.ops, ops);
1190        assert!(!r.declined.is_empty(), "the no-fact case is a loud decline");
1191    }
1192
1193    // ---- #494 Phase 3: branchless select-collapse ----
1194
1195    /// gust_mix's clamp lowered branchlessly via `select` (the shape LLVM
1196    /// emits): `max(v,1000)` = `(v<1000)?1000:v`, `min(v,2000)` =
1197    /// `(v>2000)?2000:v`. No `If`/`End` — no block_arity entries.
1198    fn select_clamp_ops() -> Vec<WasmOp> {
1199        vec![
1200            LocalGet(0),    // 0   ch          ← fact target
1201            I32Const(476),  // 1
1202            I32Add,         // 2   v = ch+476
1203            LocalSet(1),    // 3
1204            I32Const(1000), // 4   val1 (low clamp)
1205            LocalGet(1),    // 5   val2 = v
1206            LocalGet(1),    // 6   cond slice
1207            I32Const(1000), // 7
1208            I32LtS,         // 8   cond = v < 1000
1209            Select,         // 9   → max(v,1000)
1210            LocalSet(1),    // 10
1211            I32Const(2000), // 11  val1 (high clamp)
1212            LocalGet(1),    // 12  val2 = v
1213            LocalGet(1),    // 13  cond slice
1214            I32Const(2000), // 14
1215            I32GtS,         // 15  cond = v > 2000
1216            Select,         // 16  → min(v,2000) = result
1217            End,            // 17
1218        ]
1219    }
1220
1221    #[test]
1222    fn select_clamp_collapses_both_selects_under_the_proven_bound_494() {
1223        let ops = select_clamp_ops();
1224        let r = specialize_function("gust_mix", &ops, &[], &[fact(0, 524, 1524)], &[]);
1225        assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
1226        assert!(r.changed());
1227        assert_eq!(
1228            r.ops,
1229            vec![
1230                LocalGet(0),
1231                I32Const(476),
1232                I32Add,
1233                LocalSet(1),
1234                LocalGet(1), // val2 of select 1 (identity survives)
1235                LocalSet(1),
1236                LocalGet(1), // val2 of select 2 (identity survives)
1237                End,
1238            ],
1239            "both branchless clamps must collapse to the identity operand"
1240        );
1241        assert_eq!(r.kept, vec![0, 1, 2, 3, 5, 10, 12, 17]);
1242        for line in &r.admitted {
1243            assert!(line.contains("UNSAT"), "{line}");
1244            assert!(line.contains("certificate-checked"), "{line}");
1245            assert!(line.contains("select"), "{line}");
1246            assert!(line.contains("[524, 1524]"), "{line}");
1247        }
1248    }
1249
1250    #[test]
1251    fn select_clamp_wrong_bound_is_sat_and_declines_byte_identically_494() {
1252        // ch ∈ [0, 4000]: ch=0 → v=476 < 1000 so the low clamp genuinely
1253        // fires; both selects are non-constant ⇒ loud Sat decline, no rewrite.
1254        let ops = select_clamp_ops();
1255        let r = specialize_function("gust_mix", &ops, &[], &[fact(0, 0, 4000)], &[]);
1256        assert_eq!(r.admitted.len(), 0);
1257        assert!(!r.changed());
1258        assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
1259        assert!(
1260            r.declined
1261                .iter()
1262                .any(|d| d.contains("not constant") && d.contains("counterexample")),
1263            "declines must be loud and carry a model: {:?}",
1264            r.declined
1265        );
1266    }
1267
1268    #[test]
1269    fn select_collapses_to_true_arm_when_condition_proven_nonzero_494() {
1270        // result = cond ? val1 : val2 with cond ≡ 1 (fact ∈ [1,1]) ⇒ keep val1.
1271        let ops = vec![
1272            I32Const(111), // 0  val1
1273            I32Const(222), // 1  val2
1274            LocalGet(0),   // 2  cond ← fact ∈ [1,1] (always non-zero)
1275            Select,        // 3  → val1
1276            End,           // 4
1277        ];
1278        let r = specialize_function("f", &ops, &[], &[fact(2, 1, 1)], &[]);
1279        assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1280        assert_eq!(r.ops, vec![I32Const(111), End]);
1281        assert!(
1282            r.admitted[0].contains("true-arm") && r.admitted[0].contains("cond == 0"),
1283            "{}",
1284            r.admitted[0]
1285        );
1286    }
1287
1288    #[test]
1289    fn select_without_constraining_premise_declines_no_false_collapse_494() {
1290        // A TRUE ValueRange fact exists (so the walk runs) but targets val1,
1291        // not the condition; the select's condition carries no premise ⇒
1292        // non-constant ⇒ Sat decline, byte-identical.
1293        let ops = vec![
1294            I32Const(111), // 0  val1 ← fact ∈ [111,111] (true, non-constraining)
1295            I32Const(222), // 1  val2
1296            LocalGet(0),   // 2  cond — unconstrained
1297            Select,        // 3
1298            End,           // 4
1299        ];
1300        let r = specialize_function("f", &ops, &[], &[fact(0, 111, 111)], &[]);
1301        assert_eq!(r.admitted.len(), 0);
1302        assert!(!r.changed());
1303        assert_eq!(r.ops, ops);
1304    }
1305
1306    #[test]
1307    fn declined_if_havocs_its_locals_no_false_admit_downstream_494() {
1308        // The FIRST if is undecidable (condition on an unconstrained local),
1309        // and its body rewrites local 1 — so the SECOND if (which would be
1310        // dead under the fact alone) must NOT be admitted: local 1 is
1311        // havocked by the declined region.
1312        let ops = vec![
1313            LocalGet(0),    // 0  ← fact ch ∈ [524, 1524]
1314            I32Const(476),  // 1
1315            I32Add,         // 2
1316            LocalSet(1),    // 3
1317            LocalGet(2),    // 4  unconstrained
1318            If,             // 5
1319            I32Const(-9),   // 6
1320            LocalSet(1),    // 7  havocs local 1
1321            End,            // 8
1322            LocalGet(1),    // 9
1323            I32Const(2000), // 10
1324            I32GtS,         // 11
1325            If,             // 12
1326            I32Const(2000), // 13
1327            LocalSet(1),    // 14
1328            End,            // 15
1329            LocalGet(1),    // 16
1330            End,            // 17
1331        ];
1332        let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)], &[]);
1333        assert_eq!(
1334            r.admitted.len(),
1335            0,
1336            "havocked local must block the downstream elision: {:?}",
1337            r.admitted
1338        );
1339        assert_eq!(r.ops, ops);
1340    }
1341
1342    #[test]
1343    fn if_with_else_declines_494() {
1344        let ops = vec![
1345            LocalGet(0), // 0 ← fact forces cond = 0
1346            If,          // 1
1347            I32Const(1), // 2
1348            LocalSet(1), // 3
1349            Else,        // 4
1350            I32Const(2), // 5
1351            LocalSet(1), // 6
1352            End,         // 7
1353            LocalGet(1), // 8
1354            End,         // 9
1355        ];
1356        let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 0, 0)], &[]);
1357        assert_eq!(r.admitted.len(), 0);
1358        assert!(
1359            r.declined.iter().any(|d| d.contains("else")),
1360            "{:?}",
1361            r.declined
1362        );
1363        assert_eq!(r.ops, ops);
1364    }
1365
1366    #[test]
1367    fn nested_opener_inside_elided_body_fixes_block_arity_ordinals_494() {
1368        // A dead outer if contains a nested if: BOTH arity entries vanish and
1369        // the SURVIVING later block keeps its (translated) entry.
1370        let ops = vec![
1371            LocalGet(0), // 0 ← fact [5,5] ⇒ eqz = 0
1372            I32Eqz,      // 1
1373            If,          // 2   (ordinal 0)
1374            LocalGet(0), // 3
1375            If,          // 4   (ordinal 1, nested)
1376            I32Const(7), // 5
1377            LocalSet(1), // 6
1378            End,         // 7
1379            End,         // 8
1380            Block,       // 9   (ordinal 2, survives)
1381            End,         // 10
1382            End,         // 11
1383        ];
1384        let arity = &[(0, 0), (0, 0), (0, 1)];
1385        let r = specialize_function("f", &ops, arity, &[fact(0, 5, 5)], &[]);
1386        assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
1387        // The condition slice starts at op 0 (LocalGet feeds the eqz), so the
1388        // whole deleted range is [0..=8]; only the trailing block survives.
1389        assert_eq!(r.ops, vec![Block, End, End]);
1390        assert_eq!(r.kept, vec![9, 10, 11]);
1391        assert_eq!(
1392            r.block_arity,
1393            vec![(0, 1)],
1394            "only the surviving Block's entry"
1395        );
1396    }
1397
1398    #[test]
1399    fn tee_condition_slice_is_not_erasable_494() {
1400        // cond built through local.tee: proven dead, but deleting the slice
1401        // would lose the local write ⇒ decline (loud), stream unchanged.
1402        let ops = vec![
1403            LocalGet(0), // 0 ← fact [1,1]
1404            LocalTee(1), // 1  side effect in the slice
1405            I32Eqz,      // 2  = 0 under the fact
1406            If,          // 3
1407            I32Const(9), // 4
1408            LocalSet(2), // 5
1409            End,         // 6
1410            End,         // 7
1411        ];
1412        let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 1, 1)], &[]);
1413        assert_eq!(r.admitted.len(), 0);
1414        assert!(
1415            r.declined
1416                .iter()
1417                .any(|d| d.contains("not") && d.contains("erasable")),
1418            "{:?}",
1419            r.declined
1420        );
1421        assert_eq!(r.ops, ops);
1422    }
1423
1424    #[test]
1425    fn untracked_op_stops_tracking_loudly_494() {
1426        let ops = vec![
1427            LocalGet(0),   // 0 ← fact
1428            I64ExtendI32S, // 1  untracked ⇒ stop
1429            Drop,          // 2
1430            End,           // 3
1431        ];
1432        let r = specialize_function("f", &ops, &[], &[fact(0, 1, 2)], &[]);
1433        assert!(!r.changed());
1434        assert!(
1435            r.declined.iter().any(|d| d.contains("outside the tracked")),
1436            "{:?}",
1437            r.declined
1438        );
1439    }
1440
1441    fn nonzero_fact(value_id: u32) -> WscFact {
1442        WscFact {
1443            func_index: 0,
1444            value_id,
1445            kind: FactKind::DivisorNonZero,
1446        }
1447    }
1448
1449    // ================= #494 phase 2b: div/rem trap-guard elision =================
1450
1451    #[test]
1452    fn divisor_range_excluding_zero_elides_zero_guard_all_rem_div_494() {
1453        // div_u, rem_u, rem_s by a param divisor proven ∈ [1, 100]: every
1454        // zero guard falls to UNSAT(P ∧ divisor == 0); the stream itself is
1455        // untouched (marks only).
1456        let ops = vec![
1457            LocalGet(0), // 0  n
1458            LocalGet(1), // 1  d  ← fact [1,100]
1459            I32DivU,     // 2  → zero mark
1460            Drop,        // 3
1461            LocalGet(0), // 4
1462            LocalGet(1), // 5  ← fact [1,100]
1463            I32RemU,     // 6  → zero mark
1464            Drop,        // 7
1465            LocalGet(0), // 8
1466            LocalGet(1), // 9  ← fact [1,100]
1467            I32RemS,     // 10 → zero mark
1468            End,         // 11
1469        ];
1470        let facts = [fact(1, 1, 100), fact(5, 1, 100), fact(9, 1, 100)];
1471        let r = specialize_function("f", &ops, &[], &facts, &[]);
1472        assert_eq!(
1473            r.elide_div_zero,
1474            vec![2, 6, 10],
1475            "declines: {:?}",
1476            r.declined
1477        );
1478        assert_eq!(
1479            r.elide_div_ovf,
1480            Vec::<usize>::new(),
1481            "no div_s in the stream"
1482        );
1483        assert!(!r.changed(), "guard marks never rewrite the op stream");
1484        assert_eq!(r.ops, ops);
1485        assert_eq!(r.admitted.len(), 3);
1486        for line in &r.admitted {
1487            assert!(line.contains("divide-by-zero guard elided"), "{line}");
1488            assert!(line.contains("UNSAT(P ∧ divisor == 0)"), "{line}");
1489            assert!(line.contains("certificate-checked"), "{line}");
1490        }
1491    }
1492
1493    #[test]
1494    fn nonzero_fact_elides_zero_guard_but_retains_div_s_overflow_guard_494() {
1495        // THE TWO-GUARD DISTINCTION (#633/#634): a divisor-nonzero fact (kind
1496        // 3) discharges UNSAT(P ∧ divisor == 0) but NOT the overflow
1497        // obligation — divisor ≠ 0 still admits divisor == -1 with dividend
1498        // == INT_MIN, so the overflow guard is RETAINED with a loud decline.
1499        let ops = vec![
1500            LocalGet(0), // 0
1501            LocalGet(1), // 1 ← divisor-nonzero fact
1502            I32DivS,     // 2
1503            End,         // 3
1504        ];
1505        let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[]);
1506        assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
1507        assert_eq!(
1508            r.elide_div_ovf,
1509            Vec::<usize>::new(),
1510            "divisor ≠ 0 must NOT elide the INT_MIN/-1 overflow guard"
1511        );
1512        assert!(
1513            r.declined
1514                .iter()
1515                .any(|d| d.contains("overflow-guard obligation Sat") && d.contains("RETAINED")),
1516            "{:?}",
1517            r.declined
1518        );
1519    }
1520
1521    #[test]
1522    fn positive_range_discharges_both_div_s_obligations_494() {
1523        // divisor ∈ [1, 100] excludes BOTH 0 and -1 — the two obligations
1524        // are discharged independently and both guards fall.
1525        let ops = vec![LocalGet(0), LocalGet(1), I32DivS, End];
1526        let r = specialize_function("f", &ops, &[], &[fact(1, 1, 100)], &[]);
1527        assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
1528        assert_eq!(r.elide_div_ovf, vec![2]);
1529        assert_eq!(r.admitted.len(), 2, "one certificate line per obligation");
1530        assert!(
1531            r.admitted
1532                .iter()
1533                .any(|a| a.contains("overflow guard elided")
1534                    && a.contains("dividend == INT32_MIN ∧ divisor == -1")),
1535            "{:?}",
1536            r.admitted
1537        );
1538    }
1539
1540    #[test]
1541    fn range_including_zero_is_sat_and_declines_the_zero_guard_494() {
1542        // divisor ∈ [0, 100]: divisor == 0 is P-admissible — the obligation
1543        // is Sat, the decline is loud and carries a model, no mark is set.
1544        let ops = vec![LocalGet(0), LocalGet(1), I32DivU, End];
1545        let r = specialize_function("f", &ops, &[], &[fact(1, 0, 100)], &[]);
1546        assert_eq!(r.elide_div_zero, Vec::<usize>::new());
1547        assert!(
1548            r.declined
1549                .iter()
1550                .any(|d| d.contains("zero-guard obligation Sat") && d.contains("counterexample")),
1551            "{:?}",
1552            r.declined
1553        );
1554    }
1555
1556    #[test]
1557    fn i64_div_s_nonzero_fact_zero_guard_only_overflow_retained_494() {
1558        // Oracle 5 at the pass level: i64.div_s with an i64 param divisor
1559        // carrying a divisor-nonzero fact — the zero guard is proven dead,
1560        // the INT64_MIN/-1 overflow guard (#633/#634) is RETAINED.
1561        let ops = vec![LocalGet(0), LocalGet(1), I64DivS, End];
1562        let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[true, true]);
1563        assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
1564        assert_eq!(
1565            r.elide_div_ovf,
1566            Vec::<usize>::new(),
1567            "i64 overflow guard retained"
1568        );
1569        assert!(
1570            r.declined.iter().any(|d| d.contains("RETAINED")),
1571            "{:?}",
1572            r.declined
1573        );
1574    }
1575
1576    #[test]
1577    fn i64_div_s_positive_range_discharges_both_obligations_494() {
1578        let ops = vec![LocalGet(0), LocalGet(1), I64DivS, End];
1579        let r = specialize_function("f", &ops, &[], &[fact(1, 1, 1000)], &[true, true]);
1580        assert_eq!(r.elide_div_zero, vec![2], "declines: {:?}", r.declined);
1581        assert_eq!(r.elide_div_ovf, vec![2]);
1582    }
1583
1584    #[test]
1585    fn i64_div_on_undeclared_width_declines_no_marks_494() {
1586        // Without the params_i64 table the divisor local is symbolically
1587        // 32-bit — the width check declines rather than building a
1588        // wrong-width obligation.
1589        let ops = vec![LocalGet(0), LocalGet(1), I64DivU, End];
1590        let r = specialize_function("f", &ops, &[], &[nonzero_fact(1)], &[]);
1591        assert_eq!(r.elide_div_zero, Vec::<usize>::new());
1592        assert!(
1593            r.declined.iter().any(|d| d.contains("unexpected width")),
1594            "{:?}",
1595            r.declined
1596        );
1597    }
1598
1599    #[test]
1600    fn div_with_no_premise_declines_loudly_494() {
1601        // The function carries a fact, but no premise reaches the divisor —
1602        // the obligation cannot even be posed; both guards stay.
1603        let ops = vec![
1604            LocalGet(0), // 0 ← fact on the DIVIDEND, not the divisor
1605            LocalGet(1), // 1 unconstrained divisor
1606            I32DivU,     // 2
1607            End,         // 3
1608        ];
1609        // A fact on op 0 (the dividend): premises exist but do not constrain
1610        // the divisor — Sat, decline.
1611        let r = specialize_function("f", &ops, &[], &[fact(0, 1, 5)], &[]);
1612        assert_eq!(r.elide_div_zero, Vec::<usize>::new());
1613        assert!(
1614            r.declined
1615                .iter()
1616                .any(|d| d.contains("zero-guard obligation Sat")),
1617            "{:?}",
1618            r.declined
1619        );
1620    }
1621
1622    #[test]
1623    fn guard_marks_are_remapped_through_a_clamp_elision_494() {
1624        // A clamp elision rewrites the stream; a downstream div's mark must
1625        // land on the REWRITTEN index (the driver feeds the rewritten stream
1626        // to the selector, which keys guards by its own op index).
1627        let ops = vec![
1628            LocalGet(0),    // 0  ← fact [524, 1524]
1629            I32Const(476),  // 1
1630            I32Add,         // 2
1631            LocalSet(1),    // 3
1632            LocalGet(1),    // 4  -+ low clamp (elided 4..=10)
1633            I32Const(1000), // 5   |
1634            I32LtS,         // 6   |
1635            If,             // 7   |
1636            I32Const(1000), // 8   |
1637            LocalSet(1),    // 9   |
1638            End,            // 10 -+
1639            LocalGet(1),    // 11
1640            LocalGet(0),    // 12  divisor = ch ∈ [524, 1524] ⇒ nonzero
1641            I32DivU,        // 13  → zero mark (rewritten index 6)
1642            End,            // 14
1643        ];
1644        let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 524, 1524)], &[]);
1645        assert!(r.changed(), "declines: {:?}", r.declined);
1646        assert_eq!(r.kept, vec![0, 1, 2, 3, 11, 12, 13, 14]);
1647        assert_eq!(
1648            r.ops,
1649            vec![
1650                LocalGet(0),
1651                I32Const(476),
1652                I32Add,
1653                LocalSet(1),
1654                LocalGet(1),
1655                LocalGet(0),
1656                I32DivU,
1657                End
1658            ]
1659        );
1660        assert_eq!(
1661            r.elide_div_zero,
1662            vec![6],
1663            "mark remapped from original op#13 to rewritten op#6"
1664        );
1665    }
1666
1667    #[test]
1668    fn out_of_range_value_id_is_vacuous_494() {
1669        let ops = clamp_ops();
1670        let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(999, 524, 1524)], &[]);
1671        assert!(!r.changed());
1672        assert_eq!(r.ops, ops);
1673    }
1674}