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//! * `i32.div*`/`i32.rem*` are deliberately NOT tracked: they can trap, and a
58//!   deleted condition slice must be effect-free under ALL inputs, not just
59//!   P-admissible ones (the premise only justifies the branch-direction, not
60//!   trap removal — trap-guard elision is the separate divisor-nonzero fact,
61//!   out of Phase-2 scope).
62//!
63//! # Flag gating
64//!
65//! The driver only invokes this pass when `SYNTH_FACT_SPEC` is set (default
66//! OFF) AND the module carried a parseable `wsc.facts` section. Frozen
67//! fixtures carry no facts section, so every frozen anchor is bit-identical
68//! trivially; with the flag off the pass does not run at all.
69
70use crate::solver::{CheckOutcome, new_solver};
71use crate::term::{BV, Bool};
72use crate::wasm_semantics::WasmSemantics;
73use std::collections::HashMap;
74use synth_core::WasmOp;
75use synth_core::wsc_facts::{FactKind, WscFact};
76
77/// Outcome of specializing one function. `ops`/`block_arity`/`kept` are only
78/// meaningful when [`changed`](Self::changed) — otherwise they echo the input.
79#[derive(Debug)]
80pub struct FactSpecResult {
81    /// The (possibly rewritten) op stream.
82    pub ops: Vec<WasmOp>,
83    /// The blocktype-arity side-table matching `ops` (one entry per
84    /// `Block`/`Loop`/`If` in op order — entries of deleted openers removed).
85    pub block_arity: Vec<(u8, u8)>,
86    /// Indices into the ORIGINAL op stream that were kept, in order. Lets the
87    /// driver filter parallel side-tables (e.g. `op_offsets` for DWARF).
88    pub kept: Vec<usize>,
89    /// One certificate line per ADMITTED elision (logged per function).
90    pub admitted: Vec<String>,
91    /// One line per LOUD DECLINE (the general lowering is emitted for these).
92    pub declined: Vec<String>,
93}
94
95impl FactSpecResult {
96    /// True when at least one elision was admitted (i.e. `ops` differs from
97    /// the input).
98    pub fn changed(&self) -> bool {
99        !self.admitted.is_empty()
100    }
101}
102
103/// A symbolic operand-stack slot.
104#[derive(Clone)]
105struct Val {
106    bv: BV,
107    /// Start of the contiguous, side-effect-free op range that produced this
108    /// value — `None` when the producing slice is impure (`local.tee`) or not
109    /// provably contiguous. Only a `Some` slice may be deleted.
110    start: Option<usize>,
111    /// Index of the op that (last) produced this value.
112    created: usize,
113}
114
115/// Specialize one function's op stream against its `wsc.facts` premises.
116///
117/// `block_arity` is the decoder's ordinal side-table (one `(params, results)`
118/// entry per `Block`/`Loop`/`If` in op order); `facts` is the per-function
119/// slice (`CompileConfig::current_func_facts`). Total: every input yields a
120/// result — inapplicable shapes surface as loud declines, never errors.
121pub fn specialize_function(
122    func_name: &str,
123    ops: &[WasmOp],
124    block_arity: &[(u8, u8)],
125    facts: &[WscFact],
126) -> FactSpecResult {
127    let mut pass = Pass::new(func_name, ops, block_arity, facts);
128    pass.walk();
129    pass.finish()
130}
131
132struct Pass<'a> {
133    func: &'a str,
134    ops: &'a [WasmOp],
135    block_arity: &'a [(u8, u8)],
136    /// op index → ordinal into `block_arity` (for `Block`/`Loop`/`If` ops).
137    opener_ordinal: HashMap<usize, usize>,
138    /// op index → signed i32 range fact attached to that op's result.
139    range_facts: HashMap<usize, (i32, i32)>,
140    sem: WasmSemantics,
141    stack: Vec<Val>,
142    locals: HashMap<u32, BV>,
143    /// Every fresh variable created (name order), for Sat counterexamples.
144    vars: Vec<BV>,
145    fresh: u32,
146    premises: Vec<Bool>,
147    premise_desc: Vec<String>,
148    /// Inclusive op-index ranges to delete (disjoint, ascending).
149    deletions: Vec<(usize, usize)>,
150    admitted: Vec<String>,
151    declined: Vec<String>,
152}
153
154impl<'a> Pass<'a> {
155    fn new(
156        func: &'a str,
157        ops: &'a [WasmOp],
158        block_arity: &'a [(u8, u8)],
159        facts: &'a [WscFact],
160    ) -> Self {
161        let mut opener_ordinal = HashMap::new();
162        let mut ord = 0usize;
163        for (i, op) in ops.iter().enumerate() {
164            if matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If) {
165                opener_ordinal.insert(i, ord);
166                ord += 1;
167            }
168        }
169        let mut range_facts = HashMap::new();
170        for f in facts {
171            if let FactKind::ValueRange { lo, hi } = f.kind {
172                // i32-only prototype: clamp the s64 bound into i32; a bound
173                // wholly outside i32 (or inverted) is vacuous — ignore it
174                // (the encoding doc's rule for unusable facts).
175                let lo = lo.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
176                let hi = hi.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
177                if lo <= hi && (f.value_id as usize) < ops.len() {
178                    range_facts.insert(f.value_id as usize, (lo, hi));
179                }
180            }
181        }
182        Self {
183            func,
184            ops,
185            block_arity,
186            opener_ordinal,
187            range_facts,
188            // No memory model needed: memory ops are outside the tracked
189            // fragment (the walk stops there).
190            sem: WasmSemantics::new_with_memory(Vec::new()),
191            stack: Vec::new(),
192            locals: HashMap::new(),
193            vars: Vec::new(),
194            fresh: 0,
195            premises: Vec::new(),
196            premise_desc: Vec::new(),
197            deletions: Vec::new(),
198            admitted: Vec::new(),
199            declined: Vec::new(),
200        }
201    }
202
203    fn fresh_var(&mut self, name: String) -> BV {
204        let v = BV::new_const(name, 32);
205        self.vars.push(v.clone());
206        v
207    }
208
209    fn local_bv(&mut self, idx: u32) -> BV {
210        if let Some(bv) = self.locals.get(&idx) {
211            return bv.clone();
212        }
213        let v = self.fresh_var(format!("fs_l{idx}"));
214        self.locals.insert(idx, v.clone());
215        v
216    }
217
218    /// Attach a value-range premise if a fact names op `i`'s result.
219    fn attach_fact(&mut self, i: usize, bv: &BV) {
220        if let Some(&(lo, hi)) = self.range_facts.get(&i) {
221            let lo_bv = BV::from_i64(i64::from(lo), 32);
222            let hi_bv = BV::from_i64(i64::from(hi), 32);
223            let p = Bool::and(&[&bv.bvsge(&lo_bv), &bv.bvsle(&hi_bv)]);
224            self.premises.push(p);
225            self.premise_desc
226                .push(format!("value(op#{i}) ∈ [{lo}, {hi}] (signed)"));
227        }
228    }
229
230    fn push(&mut self, bv: BV, start: Option<usize>, created: usize) {
231        self.stack.push(Val { bv, start, created });
232    }
233
234    /// Find the matching `End` for the opener at `i`; also reports whether a
235    /// top-level `Else` occurs. `None` = malformed nesting (stop the walk).
236    fn matching_end(&self, i: usize) -> Option<(usize, bool)> {
237        let mut depth = 0usize;
238        let mut has_else = false;
239        for (j, op) in self.ops.iter().enumerate().skip(i + 1) {
240            match op {
241                WasmOp::Block | WasmOp::Loop | WasmOp::If => depth += 1,
242                WasmOp::Else if depth == 0 => has_else = true,
243                WasmOp::End => {
244                    if depth == 0 {
245                        return Some((j, has_else));
246                    }
247                    depth -= 1;
248                }
249                _ => {}
250            }
251        }
252        None
253    }
254
255    /// Continuation after a DECLINED `if` region `[i..=end]`: the body may or
256    /// may not run, so havoc every local it assigns (any depth) and model its
257    /// block results as fresh variables.
258    fn havoc_region(&mut self, i: usize, end: usize, arity: (u8, u8)) {
259        let ops = self.ops;
260        for op in &ops[i + 1..end] {
261            if let WasmOp::LocalSet(idx) | WasmOp::LocalTee(idx) = op {
262                let n = self.fresh;
263                self.fresh += 1;
264                let v = self.fresh_var(format!("fs_h{n}"));
265                self.locals.insert(*idx, v);
266            }
267        }
268        for _ in 0..arity.0 {
269            self.stack.pop();
270        }
271        for k in 0..arity.1 {
272            let n = self.fresh;
273            self.fresh += 1;
274            let v = self.fresh_var(format!("fs_r{n}_{k}"));
275            self.push(v, None, end);
276        }
277    }
278
279    fn decline(&mut self, msg: String) {
280        self.declined
281            .push(format!("{}: {} — general lowering emitted", self.func, msg));
282    }
283
284    /// Discharge the per-elision obligation for the no-`else` `if` at `i`
285    /// (matching `End` at `end`, condition `cond`). Returns true iff admitted.
286    fn try_elide(&mut self, i: usize, end: usize, cond: &Val) -> bool {
287        if self.premises.is_empty() {
288            self.decline(format!(
289                "op#{i} `if` — no premise reaches this site (no usable value-range fact)"
290            ));
291            return false;
292        }
293        let mut solver = new_solver();
294        for p in &self.premises {
295            solver.assert(p);
296        }
297        let taken = cond.bv.ne(BV::from_i64(0, 32));
298        solver.assert(&taken);
299        match solver.check() {
300            CheckOutcome::Unsat => {
301                let Some(start) = cond.start else {
302                    // Proven dead, but the condition slice has a side effect
303                    // (`local.tee`) or is not provably contiguous — deleting
304                    // it could drop live work. Conservative: keep everything.
305                    self.decline(format!(
306                        "op#{i} `if` proven dead (UNSAT) but its condition slice is not \
307                         erasable (impure or non-contiguous producer)"
308                    ));
309                    return false;
310                };
311                self.deletions.push((start, end));
312                self.admitted.push(format!(
313                    "{}: op#{i} `if` (+condition slice) — ops [{start}..={end}] elided \
314                     ({} ops): UNSAT(P ∧ cond ≠ 0) via {} (certificate-checked QF_BV; \
315                     every Unsat carries an LRAT proof validated by ordeal-lrat); \
316                     P = {{{}}}; cond = {}",
317                    self.func,
318                    end - start + 1,
319                    solver.name(),
320                    self.premise_desc.join(" ∧ "),
321                    cond.bv,
322                ));
323                true
324            }
325            CheckOutcome::Sat => {
326                // Read the model back for an actionable counterexample.
327                let cex: Vec<String> = self
328                    .vars
329                    .iter()
330                    .filter_map(|v| {
331                        let name = format!("{v}");
332                        solver
333                            .value(v)
334                            .map(|x| format!("{name}={}", x as u32 as i32))
335                    })
336                    .collect();
337                self.decline(format!(
338                    "op#{i} `if` — obligation Sat (branch reachable under P; \
339                     counterexample: {})",
340                    if cex.is_empty() {
341                        "<no model>".to_string()
342                    } else {
343                        cex.join(", ")
344                    }
345                ));
346                false
347            }
348            CheckOutcome::Unknown(reason) => {
349                self.decline(format!(
350                    "op#{i} `if` — obligation Unknown ({reason}); conservative decline"
351                ));
352                false
353            }
354        }
355    }
356
357    fn walk(&mut self) {
358        if self.range_facts.is_empty() {
359            self.decline("no usable value-range fact targets this function".to_string());
360            return;
361        }
362        let ops = self.ops;
363        let mut i = 0usize;
364        while i < ops.len() {
365            let op = &ops[i];
366            match op {
367                WasmOp::Nop => {}
368                WasmOp::I32Const(v) => {
369                    let bv = self.sem.encode_op(&WasmOp::I32Const(*v), &[]);
370                    self.attach_fact(i, &bv);
371                    self.push(bv, Some(i), i);
372                }
373                WasmOp::LocalGet(idx) => {
374                    let bv = self.local_bv(*idx);
375                    self.attach_fact(i, &bv);
376                    self.push(bv, Some(i), i);
377                }
378                WasmOp::LocalSet(idx) => {
379                    let Some(v) = self.stack.pop() else {
380                        self.decline(format!("op#{i} local.set on empty symbolic stack"));
381                        return;
382                    };
383                    self.locals.insert(*idx, v.bv);
384                }
385                WasmOp::LocalTee(idx) => {
386                    let Some(top) = self.stack.last_mut() else {
387                        self.decline(format!("op#{i} local.tee on empty symbolic stack"));
388                        return;
389                    };
390                    // The tee is a side effect: its slice must never be
391                    // deleted as a "pure condition producer".
392                    top.start = None;
393                    top.created = i;
394                    let bv = top.bv.clone();
395                    self.locals.insert(*idx, bv.clone());
396                    self.attach_fact(i, &bv);
397                }
398                WasmOp::Drop => {
399                    if self.stack.pop().is_none() {
400                        self.decline(format!("op#{i} drop on empty symbolic stack"));
401                        return;
402                    }
403                }
404                WasmOp::I32Eqz => {
405                    let Some(a) = self.stack.pop() else {
406                        self.decline(format!("op#{i} unary op on empty symbolic stack"));
407                        return;
408                    };
409                    let bv = self.sem.encode_op(op, &[a.bv]);
410                    self.attach_fact(i, &bv);
411                    let start = a.start.filter(|_| a.created + 1 == i);
412                    self.push(bv, start, i);
413                }
414                // Tracked, trap-free i32 binops (div/rem excluded on purpose:
415                // they can trap, and a deleted slice must be effect-free).
416                WasmOp::I32Add
417                | WasmOp::I32Sub
418                | WasmOp::I32Mul
419                | WasmOp::I32And
420                | WasmOp::I32Or
421                | WasmOp::I32Xor
422                | WasmOp::I32Shl
423                | WasmOp::I32ShrS
424                | WasmOp::I32ShrU
425                | WasmOp::I32Rotl
426                | WasmOp::I32Rotr
427                | WasmOp::I32Eq
428                | WasmOp::I32Ne
429                | WasmOp::I32LtS
430                | WasmOp::I32LtU
431                | WasmOp::I32LeS
432                | WasmOp::I32LeU
433                | WasmOp::I32GtS
434                | WasmOp::I32GtU
435                | WasmOp::I32GeS
436                | WasmOp::I32GeU => {
437                    let (Some(b), Some(a)) = (self.stack.pop(), self.stack.pop()) else {
438                        self.decline(format!("op#{i} binop on underflowing symbolic stack"));
439                        return;
440                    };
441                    let bv = self.sem.encode_op(op, &[a.bv, b.bv]);
442                    self.attach_fact(i, &bv);
443                    // Contiguity proof for the combined producer slice:
444                    // a's slice, immediately followed by b's, immediately
445                    // followed by this op. Anything else ⇒ not erasable.
446                    let start = match (a.start, b.start) {
447                        (Some(sa), Some(sb)) if a.created + 1 == sb && b.created + 1 == i => {
448                            Some(sa)
449                        }
450                        _ => None,
451                    };
452                    self.push(bv, start, i);
453                }
454                WasmOp::If => {
455                    let Some(cond) = self.stack.pop() else {
456                        self.decline(format!("op#{i} `if` on empty symbolic stack"));
457                        return;
458                    };
459                    let Some((end, has_else)) = self.matching_end(i) else {
460                        self.decline(format!("op#{i} `if` without matching `end`"));
461                        return;
462                    };
463                    let Some(&ord) = self.opener_ordinal.get(&i) else {
464                        self.decline(format!("op#{i} `if` missing from the opener ordinal map"));
465                        return;
466                    };
467                    let Some(&arity) = self.block_arity.get(ord) else {
468                        self.decline(format!(
469                            "op#{i} `if` has no block_arity entry (side-table desync)"
470                        ));
471                        return;
472                    };
473                    if has_else {
474                        self.decline(format!(
475                            "op#{i} `if`/`else` — only no-else `if` is in Phase-2 scope"
476                        ));
477                        self.havoc_region(i, end, arity);
478                    } else if self.try_elide(i, end, &cond) {
479                        // Region provably never executes: state unchanged
480                        // (params-as-results pass-through is the identity for
481                        // a no-else `if`, whose blocktype has equal
482                        // param/result types by wasm validation).
483                    } else {
484                        self.havoc_region(i, end, arity);
485                    }
486                    i = end + 1;
487                    continue;
488                }
489                // Function-final `End` (top-level): done.
490                WasmOp::End => break,
491                WasmOp::Return => break,
492                other => {
493                    // First op outside the tracked fragment: stop. Everything
494                    // already admitted was justified independently of what
495                    // follows; declining the REST loudly keeps honesty.
496                    self.decline(format!(
497                        "op#{i} {other:?} is outside the tracked i32 fragment — \
498                         fact tracking stops here (no further elisions in this function)"
499                    ));
500                    return;
501                }
502            }
503            i += 1;
504        }
505    }
506
507    fn finish(self) -> FactSpecResult {
508        let Pass {
509            ops,
510            block_arity,
511            deletions,
512            admitted,
513            declined,
514            ..
515        } = self;
516        if deletions.is_empty() {
517            return FactSpecResult {
518                ops: ops.to_vec(),
519                block_arity: block_arity.to_vec(),
520                kept: (0..ops.len()).collect(),
521                admitted,
522                declined,
523            };
524        }
525        let deleted = |i: usize| deletions.iter().any(|&(s, e)| i >= s && i <= e);
526        let mut out_ops = Vec::with_capacity(ops.len());
527        let mut out_arity = Vec::with_capacity(block_arity.len());
528        let mut kept = Vec::with_capacity(ops.len());
529        let mut ord = 0usize;
530        for (i, op) in ops.iter().enumerate() {
531            let is_opener = matches!(op, WasmOp::Block | WasmOp::Loop | WasmOp::If);
532            if !deleted(i) {
533                out_ops.push(op.clone());
534                kept.push(i);
535                if is_opener && let Some(&a) = block_arity.get(ord) {
536                    out_arity.push(a);
537                }
538            }
539            if is_opener {
540                ord += 1;
541            }
542        }
543        FactSpecResult {
544            ops: out_ops,
545            block_arity: out_arity,
546            kept,
547            admitted,
548            declined,
549        }
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use WasmOp::*;
557
558    fn fact(value_id: u32, lo: i64, hi: i64) -> WscFact {
559        WscFact {
560            func_index: 0,
561            value_id,
562            kind: FactKind::ValueRange { lo, hi },
563        }
564    }
565
566    /// The gust_mix clamp shape: clamp(ch + 476, 1000, 2000) via two
567    /// no-else `if`s over a local.
568    fn clamp_ops() -> Vec<WasmOp> {
569        vec![
570            LocalGet(0),    // 0   ch          ← fact target
571            I32Const(476),  // 1
572            I32Add,         // 2   v = ch+476
573            LocalSet(1),    // 3
574            LocalGet(1),    // 4
575            I32Const(1000), // 5
576            I32LtS,         // 6
577            If,             // 7
578            I32Const(1000), // 8
579            LocalSet(1),    // 9
580            End,            // 10
581            LocalGet(1),    // 11
582            I32Const(2000), // 12
583            I32GtS,         // 13
584            If,             // 14
585            I32Const(2000), // 15
586            LocalSet(1),    // 16
587            End,            // 17
588            LocalGet(1),    // 18
589            End,            // 19
590        ]
591    }
592
593    const CLAMP_ARITY: &[(u8, u8)] = &[(0, 0), (0, 0)];
594
595    #[test]
596    fn clamp_shape_elides_both_branches_under_the_proven_bound_494() {
597        let ops = clamp_ops();
598        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)]);
599        assert_eq!(r.admitted.len(), 2, "declines: {:?}", r.declined);
600        assert!(r.changed());
601        assert_eq!(
602            r.ops,
603            vec![
604                LocalGet(0),
605                I32Const(476),
606                I32Add,
607                LocalSet(1),
608                LocalGet(1),
609                End
610            ],
611            "both clamp comparisons + branches + bodies must be gone"
612        );
613        assert_eq!(r.block_arity, vec![], "both If arity entries removed");
614        assert_eq!(r.kept, vec![0, 1, 2, 3, 18, 19]);
615        // The certificate evidence trail names the engine and the premise.
616        for line in &r.admitted {
617            assert!(line.contains("UNSAT"), "{line}");
618            assert!(line.contains("certificate-checked"), "{line}");
619            assert!(line.contains("[524, 1524]"), "{line}");
620        }
621    }
622
623    #[test]
624    fn wrong_wide_bound_is_sat_and_declines_loudly_494() {
625        // ch ∈ [0, 4000] does NOT make the clamp dead (ch=0 → v=476 < 1000):
626        // the obligation is Sat and BOTH sites decline with a counterexample.
627        let ops = clamp_ops();
628        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 0, 4000)]);
629        assert_eq!(r.admitted.len(), 0);
630        assert!(!r.changed());
631        assert_eq!(r.ops, ops, "declined ⇒ byte-identical op stream");
632        assert_eq!(r.block_arity, CLAMP_ARITY.to_vec());
633        assert!(
634            r.declined
635                .iter()
636                .any(|d| d.contains("Sat") && d.contains("counterexample")),
637            "declines must be loud and carry a model: {:?}",
638            r.declined
639        );
640    }
641
642    #[test]
643    fn partially_dead_bound_elides_only_the_proven_branch_494() {
644        // ch ∈ [524, 4000]: v ≥ 1000 so the LOW clamp is dead, but v can
645        // exceed 2000 so the HIGH clamp must survive.
646        let ops = clamp_ops();
647        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[fact(0, 524, 4000)]);
648        assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
649        assert_eq!(r.declined.len(), 1);
650        assert_eq!(
651            r.ops,
652            vec![
653                LocalGet(0),
654                I32Const(476),
655                I32Add,
656                LocalSet(1),
657                LocalGet(1),
658                I32Const(2000),
659                I32GtS,
660                If,
661                I32Const(2000),
662                LocalSet(1),
663                End,
664                LocalGet(1),
665                End,
666            ]
667        );
668        assert_eq!(r.block_arity, vec![(0, 0)], "one If survives");
669    }
670
671    #[test]
672    fn no_facts_changes_nothing_494() {
673        let ops = clamp_ops();
674        let r = specialize_function("gust_mix", &ops, CLAMP_ARITY, &[]);
675        assert!(!r.changed());
676        assert_eq!(r.ops, ops);
677        assert!(!r.declined.is_empty(), "the no-fact case is a loud decline");
678    }
679
680    #[test]
681    fn declined_if_havocs_its_locals_no_false_admit_downstream_494() {
682        // The FIRST if is undecidable (condition on an unconstrained local),
683        // and its body rewrites local 1 — so the SECOND if (which would be
684        // dead under the fact alone) must NOT be admitted: local 1 is
685        // havocked by the declined region.
686        let ops = vec![
687            LocalGet(0),    // 0  ← fact ch ∈ [524, 1524]
688            I32Const(476),  // 1
689            I32Add,         // 2
690            LocalSet(1),    // 3
691            LocalGet(2),    // 4  unconstrained
692            If,             // 5
693            I32Const(-9),   // 6
694            LocalSet(1),    // 7  havocs local 1
695            End,            // 8
696            LocalGet(1),    // 9
697            I32Const(2000), // 10
698            I32GtS,         // 11
699            If,             // 12
700            I32Const(2000), // 13
701            LocalSet(1),    // 14
702            End,            // 15
703            LocalGet(1),    // 16
704            End,            // 17
705        ];
706        let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(0, 524, 1524)]);
707        assert_eq!(
708            r.admitted.len(),
709            0,
710            "havocked local must block the downstream elision: {:?}",
711            r.admitted
712        );
713        assert_eq!(r.ops, ops);
714    }
715
716    #[test]
717    fn if_with_else_declines_494() {
718        let ops = vec![
719            LocalGet(0), // 0 ← fact forces cond = 0
720            If,          // 1
721            I32Const(1), // 2
722            LocalSet(1), // 3
723            Else,        // 4
724            I32Const(2), // 5
725            LocalSet(1), // 6
726            End,         // 7
727            LocalGet(1), // 8
728            End,         // 9
729        ];
730        let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 0, 0)]);
731        assert_eq!(r.admitted.len(), 0);
732        assert!(
733            r.declined.iter().any(|d| d.contains("else")),
734            "{:?}",
735            r.declined
736        );
737        assert_eq!(r.ops, ops);
738    }
739
740    #[test]
741    fn nested_opener_inside_elided_body_fixes_block_arity_ordinals_494() {
742        // A dead outer if contains a nested if: BOTH arity entries vanish and
743        // the SURVIVING later block keeps its (translated) entry.
744        let ops = vec![
745            LocalGet(0), // 0 ← fact [5,5] ⇒ eqz = 0
746            I32Eqz,      // 1
747            If,          // 2   (ordinal 0)
748            LocalGet(0), // 3
749            If,          // 4   (ordinal 1, nested)
750            I32Const(7), // 5
751            LocalSet(1), // 6
752            End,         // 7
753            End,         // 8
754            Block,       // 9   (ordinal 2, survives)
755            End,         // 10
756            End,         // 11
757        ];
758        let arity = &[(0, 0), (0, 0), (0, 1)];
759        let r = specialize_function("f", &ops, arity, &[fact(0, 5, 5)]);
760        assert_eq!(r.admitted.len(), 1, "declines: {:?}", r.declined);
761        // The condition slice starts at op 0 (LocalGet feeds the eqz), so the
762        // whole deleted range is [0..=8]; only the trailing block survives.
763        assert_eq!(r.ops, vec![Block, End, End]);
764        assert_eq!(r.kept, vec![9, 10, 11]);
765        assert_eq!(
766            r.block_arity,
767            vec![(0, 1)],
768            "only the surviving Block's entry"
769        );
770    }
771
772    #[test]
773    fn tee_condition_slice_is_not_erasable_494() {
774        // cond built through local.tee: proven dead, but deleting the slice
775        // would lose the local write ⇒ decline (loud), stream unchanged.
776        let ops = vec![
777            LocalGet(0), // 0 ← fact [1,1]
778            LocalTee(1), // 1  side effect in the slice
779            I32Eqz,      // 2  = 0 under the fact
780            If,          // 3
781            I32Const(9), // 4
782            LocalSet(2), // 5
783            End,         // 6
784            End,         // 7
785        ];
786        let r = specialize_function("f", &ops, &[(0, 0)], &[fact(0, 1, 1)]);
787        assert_eq!(r.admitted.len(), 0);
788        assert!(
789            r.declined
790                .iter()
791                .any(|d| d.contains("not") && d.contains("erasable")),
792            "{:?}",
793            r.declined
794        );
795        assert_eq!(r.ops, ops);
796    }
797
798    #[test]
799    fn untracked_op_stops_tracking_loudly_494() {
800        let ops = vec![
801            LocalGet(0),   // 0 ← fact
802            I64ExtendI32S, // 1  untracked ⇒ stop
803            Drop,          // 2
804            End,           // 3
805        ];
806        let r = specialize_function("f", &ops, &[], &[fact(0, 1, 2)]);
807        assert!(!r.changed());
808        assert!(
809            r.declined.iter().any(|d| d.contains("outside the tracked")),
810            "{:?}",
811            r.declined
812        );
813    }
814
815    #[test]
816    fn out_of_range_value_id_is_vacuous_494() {
817        let ops = clamp_ops();
818        let r = specialize_function("f", &ops, CLAMP_ARITY, &[fact(999, 524, 1524)]);
819        assert!(!r.changed());
820        assert_eq!(r.ops, ops);
821    }
822}