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}