Skip to main content

synth_verify/
translation_validator.rs

1//! Translation Validator - Proves equivalence between WASM and ARM code
2//!
3//! This module implements SMT-based translation validation inspired by Alive2.
4//! For each synthesis rule WASM → ARM, we prove that the ARM code has
5//! semantically equivalent behavior to the WASM code.
6//!
7//! # Verification Approach
8//!
9//! 1. Create symbolic inputs for both WASM and ARM
10//! 2. Encode WASM semantics as SMT formula phi_wasm
11//! 3. Encode ARM semantics as SMT formula phi_arm
12//! 4. Assert: phi_wasm(inputs) == phi_arm(inputs)
13//! 5. Check satisfiability - if UNSAT, then equivalence is proven
14//!
15//! # Example
16//!
17//! For the rule: WASM `i32.add` -> ARM `ADD Rd, Rn, Rm`
18//!
19//! We prove: forall a,b. i32.add(a, b) == ADD(a, b)
20
21use crate::arm_semantics::{ArmSemantics, ArmState};
22use crate::solver::{CheckOutcome, new_solver};
23use crate::term::BV;
24use crate::wasm_semantics::WasmSemantics;
25use synth_core::WasmOp;
26use synth_synthesis::{ArmOp, Reg, SynthesisRule};
27use thiserror::Error;
28
29/// Whether a div/rem ARM lowering carries its trap guard: synth guards a
30/// divide with a `Cmp`/branch/`Udf` sequence, so the presence of a `Udf`
31/// (the `undefined`/trap instruction) is the structural signal that the
32/// divide-by-zero (and, for signed, overflow) trap is still enforced. Its
33/// absence means the guard was dropped — the #633/#666/#642 shape.
34/// (VCR-VER-002, #166.)
35fn arm_sequence_has_trap_guard(arm_ops: &[ArmOp]) -> bool {
36    arm_ops.iter().any(|op| matches!(op, ArmOp::Udf { .. }))
37}
38
39/// Verification error types
40#[derive(Debug, Error)]
41pub enum VerificationError {
42    #[error("Translation is incorrect: counterexample found")]
43    CounterexampleFound {
44        wasm_result: String,
45        arm_result: String,
46        inputs: Vec<String>,
47    },
48
49    #[error("Verification timeout after {0}ms")]
50    Timeout(u64),
51
52    #[error("Unsupported operation: {0}")]
53    UnsupportedOperation(String),
54
55    #[error("SMT solver error: {0}")]
56    SolverError(String),
57
58    #[error("Invalid synthesis rule: {0}")]
59    InvalidRule(String),
60}
61
62/// Result of translation validation
63#[derive(Debug, Clone, PartialEq)]
64pub enum ValidationResult {
65    /// Translation is provably correct
66    Verified,
67
68    /// Counterexample found - translation is incorrect
69    Invalid { counterexample: Vec<(String, i64)> },
70
71    /// Verification inconclusive (timeout or unsupported operations)
72    Unknown { reason: String },
73}
74
75/// Translation validator over the configured SMT engine (see
76/// [`crate::solver::new_solver`]: ordeal by default, optionally
77/// cross-checked against Z3 when `SYNTH_SOLVER_DIFF=1`).
78pub struct TranslationValidator {
79    wasm_encoder: WasmSemantics,
80    arm_encoder: ArmSemantics,
81    timeout_ms: u64,
82}
83
84impl Default for TranslationValidator {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl TranslationValidator {
91    /// Create a new translation validator
92    pub fn new() -> Self {
93        Self {
94            wasm_encoder: WasmSemantics::new(),
95            arm_encoder: ArmSemantics::new(),
96            timeout_ms: 30000, // 30 seconds default
97        }
98    }
99
100    /// Set verification timeout in milliseconds
101    pub fn set_timeout(&mut self, timeout_ms: u64) {
102        self.timeout_ms = timeout_ms;
103    }
104
105    /// Verify a synthesis rule
106    ///
107    /// Proves that the ARM code generated by the rule has equivalent semantics
108    /// to the WASM code matched by the pattern.
109    pub fn verify_rule(&self, rule: &SynthesisRule) -> Result<ValidationResult, VerificationError> {
110        // Extract WASM operation from pattern
111        let wasm_op = match &rule.pattern {
112            synth_synthesis::Pattern::WasmInstr(op) => op,
113            _ => {
114                return Err(VerificationError::UnsupportedOperation(
115                    "Only single WASM instruction patterns are supported".to_string(),
116                ));
117            }
118        };
119
120        // Extract ARM operations from replacement
121        let arm_ops = match &rule.replacement {
122            synth_synthesis::Replacement::ArmInstr(op) => vec![op.clone()],
123            synth_synthesis::Replacement::ArmSequence(ops) => ops.clone(),
124            _ => {
125                return Err(VerificationError::UnsupportedOperation(
126                    "Only ARM instruction replacements are supported".to_string(),
127                ));
128            }
129        };
130
131        self.verify_equivalence(wasm_op, &arm_ops)
132    }
133
134    /// Verify equivalence between a WASM operation and ARM operations
135    pub fn verify_equivalence(
136        &self,
137        wasm_op: &WasmOp,
138        arm_ops: &[ArmOp],
139    ) -> Result<ValidationResult, VerificationError> {
140        self.verify_equivalence_parameterized(wasm_op, arm_ops, &[])
141    }
142
143    /// Verify equivalence with concrete parameter values
144    pub fn verify_equivalence_parameterized(
145        &self,
146        wasm_op: &WasmOp,
147        arm_ops: &[ArmOp],
148        concrete_params: &[(usize, i64)],
149    ) -> Result<ValidationResult, VerificationError> {
150        let mut solver = new_solver();
151
152        // Create inputs - some symbolic, some concrete
153        let num_inputs = self.get_num_inputs(wasm_op);
154        let mut inputs: Vec<BV> = Vec::new();
155
156        for i in 0..num_inputs {
157            let input = if let Some((_, value)) = concrete_params.iter().find(|(idx, _)| *idx == i)
158            {
159                // Concrete value
160                BV::from_i64(*value, 32)
161            } else {
162                // Symbolic value
163                BV::new_const(format!("input_{}", i), 32)
164            };
165            inputs.push(input);
166        }
167
168        // Encode WASM semantics
169        let wasm_result = self.wasm_encoder.encode_op(wasm_op, &inputs);
170
171        // Encode ARM semantics
172        let arm_result = self.encode_arm_sequence(arm_ops, &inputs)?;
173
174        // Assert that results are NOT equal
175        // If this is UNSAT, then the results are always equal (proven correct)
176        // If this is SAT, we found a counterexample
177        solver.assert(&wasm_result.eq(&arm_result).not());
178
179        match solver.check() {
180            CheckOutcome::Unsat => {
181                // Proven correct - no inputs exist where results differ
182                Ok(ValidationResult::Verified)
183            }
184
185            CheckOutcome::Sat => {
186                // Found counterexample: read the differing inputs back from
187                // the model (symbolic inputs only — concrete params have no
188                // model entry). Values are reported unsigned, as before.
189                let mut counterexample = Vec::new();
190                for (i, input) in inputs.iter().enumerate() {
191                    if let Some(value) = solver.value(input)
192                        && let Ok(int_val) = i64::try_from(value)
193                    {
194                        counterexample.push((format!("input_{}", i), int_val));
195                    }
196                }
197
198                Ok(ValidationResult::Invalid { counterexample })
199            }
200
201            CheckOutcome::Unknown(reason) => {
202                // Verification inconclusive
203                Ok(ValidationResult::Unknown {
204                    reason: format!("SMT solver returned unknown: {reason}"),
205                })
206            }
207        }
208    }
209
210    /// Encode a sequence of ARM operations
211    fn encode_arm_sequence(
212        &self,
213        arm_ops: &[ArmOp],
214        inputs: &[BV],
215    ) -> Result<BV, VerificationError> {
216        let mut state = ArmState::new_symbolic();
217
218        // Initialize input registers
219        for (i, input) in inputs.iter().enumerate() {
220            let reg = match i {
221                0 => Reg::R0,
222                1 => Reg::R1,
223                2 => Reg::R2,
224                _ => {
225                    return Err(VerificationError::UnsupportedOperation(format!(
226                        "Too many inputs: {}",
227                        inputs.len()
228                    )));
229                }
230            };
231            state.set_reg(&reg, input.clone());
232        }
233
234        // Execute ARM operations
235        for arm_op in arm_ops {
236            self.arm_encoder.encode_op(arm_op, &mut state);
237        }
238
239        // Extract result from R0 (ARM calling convention)
240        Ok(self.arm_encoder.extract_result(&state, &Reg::R0))
241    }
242
243    /// Verify operation for all parameter values in a range
244    pub fn verify_parameterized_range<F>(
245        &self,
246        wasm_op: &WasmOp,
247        create_arm_ops: F,
248        param_index: usize,
249        range: std::ops::Range<i64>,
250    ) -> Result<ValidationResult, VerificationError>
251    where
252        F: Fn(i64) -> Vec<ArmOp>,
253    {
254        for value in range {
255            let arm_ops = create_arm_ops(value);
256            let result =
257                self.verify_equivalence_parameterized(wasm_op, &arm_ops, &[(param_index, value)])?;
258
259            match result {
260                ValidationResult::Verified => continue,
261                ValidationResult::Invalid { counterexample } => {
262                    return Ok(ValidationResult::Invalid {
263                        counterexample: counterexample
264                            .into_iter()
265                            .map(|(k, v)| (format!("{} (param={})", k, value), v))
266                            .collect(),
267                    });
268                }
269                ValidationResult::Unknown { reason } => {
270                    return Ok(ValidationResult::Unknown {
271                        reason: format!("Failed at param={}: {}", value, reason),
272                    });
273                }
274            }
275        }
276
277        Ok(ValidationResult::Verified)
278    }
279
280    /// VCR-VER-002 (#166): mandatory **trap-preservation** obligation for a
281    /// div/rem lowering — that the ARM sequence preserves the WASM op's trap
282    /// (`÷0`, plus `INT_MIN/-1` for the signed ops) *and* its value, discharged
283    /// by [`crate::trap::prove_trap_equivalence`].
284    ///
285    /// The WASM trap condition is derivable from the operands. The ARM
286    /// lowering's trap condition is derived **structurally** from `arm_ops`:
287    /// synth guards a divide with a `Cmp`/branch/`Udf` sequence (see
288    /// `synth_synthesis::contracts::division`), so a `Udf` in the sequence ⇒
289    /// the guard is present and the lowering traps on the same condition; its
290    /// absence ⇒ the guard was dropped (the #633/#666/#642 shape) and the
291    /// lowering never traps — which this gate reports `Invalid`.
292    ///
293    /// # Soundness scope
294    ///
295    /// Sound in the **reject** direction: a div/rem lowering with no `Udf` is
296    /// reported `Invalid`, catching the whole trap-drop class. Presence of a
297    /// `Udf` is a *necessary* structural signal but does not by itself prove the
298    /// guard fires on *exactly* `÷0 ∨ overflow`.
299    ///
300    // VCR-VER-002 follow-on: fully AUTO-deriving `opt.may_trap` from the shipped
301    // lowering (rather than the structural `Udf`-presence proxy) needs a
302    // `may_trap` flag threaded through the `ArmState` exec model so the
303    // encoder's `Cmp`/`Bne`/`Udf` expansion produces a derived trap term — or,
304    // equivalently, decoding the emitted guard bytes in `expansion_validator`.
305    // Until then the other partial-op classes (load/store, call_indirect,
306    // unreachable) have no ARM trap term on this value-only path and remain
307    // gated at the unit level (`tests/trap_preservation.rs`), not here.
308    pub fn verify_div_rem_trap_preservation(
309        &self,
310        wasm_op: &WasmOp,
311        arm_ops: &[ArmOp],
312    ) -> Result<ValidationResult, VerificationError> {
313        let Some(div_op) = crate::trap::div_op(wasm_op) else {
314            return Err(VerificationError::UnsupportedOperation(format!(
315                "trap-preservation gate applies to div/rem only, got {wasm_op:?}"
316            )));
317        };
318        // This method models 32-bit operands only; `div_op` also maps the i64
319        // variants (VCR-VER-002 follow-on: i64 needs 64-bit operand terms +
320        // the register-pair ARM value model).
321        if !matches!(
322            wasm_op,
323            WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU
324        ) {
325            return Err(VerificationError::UnsupportedOperation(format!(
326                "trap-preservation gate currently supports i32 div/rem only, got {wasm_op:?}"
327            )));
328        }
329
330        // Symbolic operands, matching `verify_equivalence_parameterized`'s
331        // naming: dividend = input_0 (R0), divisor = input_1 (R1).
332        let dividend = BV::new_const("input_0", 32);
333        let divisor = BV::new_const("input_1", 32);
334        let inputs = vec![dividend.clone(), divisor.clone()];
335
336        let wasm_value = self.wasm_encoder.encode_op(wasm_op, &inputs);
337        let arm_value = self.encode_arm_sequence(arm_ops, &inputs)?;
338
339        let orig = crate::trap::DefineOrTrap {
340            value: wasm_value,
341            may_trap: crate::trap::trap_div(div_op, &dividend, &divisor),
342        };
343        let arm_may_trap = if arm_sequence_has_trap_guard(arm_ops) {
344            crate::trap::trap_div(div_op, &dividend, &divisor)
345        } else {
346            crate::term::Bool::from_bool(false)
347        };
348        let opt = crate::trap::DefineOrTrap {
349            value: arm_value,
350            may_trap: arm_may_trap,
351        };
352
353        Ok(match crate::trap::prove_trap_equivalence(&orig, &opt) {
354            crate::trap::TrapVerdict::Preserved => ValidationResult::Verified,
355            crate::trap::TrapVerdict::Dropped(model) => ValidationResult::Invalid {
356                counterexample: model
357                    .into_iter()
358                    .filter_map(|(n, v)| i64::try_from(v).ok().map(|x| (n, x)))
359                    .collect(),
360            },
361            crate::trap::TrapVerdict::Unknown => ValidationResult::Unknown {
362                reason: "trap-preservation VC returned Unknown (conservative reject)".to_string(),
363            },
364        })
365    }
366
367    /// Get number of inputs required for a WASM operation
368    fn get_num_inputs(&self, wasm_op: &WasmOp) -> usize {
369        use WasmOp::*;
370        match wasm_op {
371            // Binary operations
372            I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or
373            | I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I32Eq | I32Ne | I32LtS
374            | I32LtU | I32LeS | I32LeU | I32GtS | I32GtU | I32GeS | I32GeU => 2,
375
376            // Unary operations
377            I32Clz | I32Ctz | I32Popcnt | I32Eqz => 1,
378
379            // Constants
380            I32Const(_) => 0,
381
382            // Memory operations
383            I32Load { .. } => 1,  // address
384            I32Store { .. } => 2, // address + value
385
386            // Control flow
387            LocalGet(_) | GlobalGet(_) => 0,
388            LocalSet(_) | GlobalSet(_) | LocalTee(_) => 1,
389            Br(_) | BrIf(_) | Return => 0,
390
391            // Other operations
392            Drop => 1,
393            Select => 3, // condition + two values
394            Nop | Unreachable | Block | Loop | If | Else | End => 0,
395
396            // Default for unknown
397            _ => 0,
398        }
399    }
400
401    /// Batch verify multiple synthesis rules
402    pub fn verify_rules(
403        &self,
404        rules: &[SynthesisRule],
405    ) -> Vec<(String, Result<ValidationResult, VerificationError>)> {
406        rules
407            .iter()
408            .map(|rule| {
409                let result = self.verify_rule(rule);
410                (rule.name.clone(), result)
411            })
412            .collect()
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::with_verification_context;
420    use synth_synthesis::{Cost, Operand2, Pattern, Replacement};
421
422    fn create_test_rule(wasm_op: WasmOp, arm_op: ArmOp) -> SynthesisRule {
423        SynthesisRule {
424            name: format!("{:?}", wasm_op),
425            priority: 0,
426            pattern: Pattern::WasmInstr(wasm_op),
427            replacement: Replacement::ArmInstr(arm_op),
428            cost: Cost {
429                cycles: 1,
430                code_size: 4,
431                registers: 2,
432            },
433        }
434    }
435
436    // --- VCR-VER-002 (#166): div/rem trap-preservation wired into the validator ---
437
438    #[test]
439    fn div_lowering_without_guard_is_rejected_as_trap_drop() {
440        with_verification_context(|| {
441            let validator = TranslationValidator::new();
442            // Bare UDIV — the value is right but the ÷0 guard is missing
443            // (the #633/#666 shape). The trap-preservation gate must reject it.
444            let arm_ops = [ArmOp::Udiv {
445                rd: Reg::R0,
446                rn: Reg::R0,
447                rm: Reg::R1,
448            }];
449            let result = validator
450                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
451                .unwrap();
452            match result {
453                ValidationResult::Invalid { counterexample } => {
454                    // The counterexample must exhibit the dropped trap: divisor 0.
455                    let divisor = counterexample.iter().find(|(n, _)| n == "input_1");
456                    assert_eq!(
457                        divisor.map(|(_, v)| *v),
458                        Some(0),
459                        "trap-drop counterexample must set the divisor to 0"
460                    );
461                }
462                other => panic!("unguarded div must be Invalid, got {other:?}"),
463            }
464        });
465    }
466
467    #[test]
468    fn div_lowering_with_guard_preserves_the_trap() {
469        with_verification_context(|| {
470            let validator = TranslationValidator::new();
471            // Guarded divide: CMP divisor,#0 ; UDF (trap) ; UDIV. The structural
472            // Udf ⇒ the ÷0 trap is enforced; value matches WASM ⇒ Verified.
473            let arm_ops = [
474                ArmOp::Cmp {
475                    rn: Reg::R1,
476                    op2: Operand2::Imm(0),
477                },
478                ArmOp::Udf { imm: 0 },
479                ArmOp::Udiv {
480                    rd: Reg::R0,
481                    rn: Reg::R0,
482                    rm: Reg::R1,
483                },
484            ];
485            let result = validator
486                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
487                .unwrap();
488            assert_eq!(result, ValidationResult::Verified);
489        });
490    }
491
492    #[test]
493    fn signed_div_guard_preserves_both_zero_and_overflow_traps() {
494        with_verification_context(|| {
495            let validator = TranslationValidator::new();
496            let arm_ops = [
497                ArmOp::Udf { imm: 0 }, // structural guard present
498                ArmOp::Sdiv {
499                    rd: Reg::R0,
500                    rn: Reg::R0,
501                    rm: Reg::R1,
502                },
503            ];
504            let result = validator
505                .verify_div_rem_trap_preservation(&WasmOp::I32DivS, &arm_ops)
506                .unwrap();
507            assert_eq!(result, ValidationResult::Verified);
508        });
509    }
510
511    #[test]
512    fn trap_preservation_gate_rejects_non_div_ops() {
513        with_verification_context(|| {
514            let validator = TranslationValidator::new();
515            let err = validator
516                .verify_div_rem_trap_preservation(&WasmOp::I32Add, &[])
517                .unwrap_err();
518            assert!(matches!(err, VerificationError::UnsupportedOperation(_)));
519            // i64 div/rem is a div op but this method models 32-bit only —
520            // it must Err rather than build wrong-width terms.
521            let err64 = validator
522                .verify_div_rem_trap_preservation(&WasmOp::I64DivU, &[])
523                .unwrap_err();
524            assert!(matches!(err64, VerificationError::UnsupportedOperation(_)));
525        });
526    }
527
528    #[test]
529    fn test_verify_add_correct() {
530        with_verification_context(|| {
531            let validator = TranslationValidator::new();
532
533            let rule = create_test_rule(
534                WasmOp::I32Add,
535                ArmOp::Add {
536                    rd: Reg::R0,
537                    rn: Reg::R0,
538                    op2: Operand2::Reg(Reg::R1),
539                },
540            );
541
542            let result = validator.verify_rule(&rule).unwrap();
543            assert_eq!(result, ValidationResult::Verified);
544        });
545    }
546
547    #[test]
548    fn test_verify_sub_correct() {
549        with_verification_context(|| {
550            let validator = TranslationValidator::new();
551
552            let rule = create_test_rule(
553                WasmOp::I32Sub,
554                ArmOp::Sub {
555                    rd: Reg::R0,
556                    rn: Reg::R0,
557                    op2: Operand2::Reg(Reg::R1),
558                },
559            );
560
561            let result = validator.verify_rule(&rule).unwrap();
562            assert_eq!(result, ValidationResult::Verified);
563        });
564    }
565
566    #[test]
567    fn test_verify_mul_correct() {
568        with_verification_context(|| {
569            let validator = TranslationValidator::new();
570
571            let rule = create_test_rule(
572                WasmOp::I32Mul,
573                ArmOp::Mul {
574                    rd: Reg::R0,
575                    rn: Reg::R0,
576                    rm: Reg::R1,
577                },
578            );
579
580            let result = validator.verify_rule(&rule).unwrap();
581            assert_eq!(result, ValidationResult::Verified);
582        });
583    }
584
585    #[test]
586    fn test_verify_and_correct() {
587        with_verification_context(|| {
588            let validator = TranslationValidator::new();
589
590            let rule = create_test_rule(
591                WasmOp::I32And,
592                ArmOp::And {
593                    rd: Reg::R0,
594                    rn: Reg::R0,
595                    op2: Operand2::Reg(Reg::R1),
596                },
597            );
598
599            let result = validator.verify_rule(&rule).unwrap();
600            assert_eq!(result, ValidationResult::Verified);
601        });
602    }
603
604    #[test]
605    fn test_verify_incorrect_rule() {
606        with_verification_context(|| {
607            let validator = TranslationValidator::new();
608
609            // INCORRECT rule: WASM i32.add -> ARM SUB (should find counterexample)
610            let rule = create_test_rule(
611                WasmOp::I32Add,
612                ArmOp::Sub {
613                    rd: Reg::R0,
614                    rn: Reg::R0,
615                    op2: Operand2::Reg(Reg::R1),
616                },
617            );
618
619            let result = validator.verify_rule(&rule).unwrap();
620
621            match result {
622                ValidationResult::Invalid { counterexample } => {
623                    assert!(!counterexample.is_empty());
624                }
625                _ => panic!("Expected counterexample but got: {:?}", result),
626            }
627        });
628    }
629
630    #[test]
631    fn test_verify_bitwise_ops() {
632        with_verification_context(|| {
633            let validator = TranslationValidator::new();
634
635            // Test OR
636            let or_rule = create_test_rule(
637                WasmOp::I32Or,
638                ArmOp::Orr {
639                    rd: Reg::R0,
640                    rn: Reg::R0,
641                    op2: Operand2::Reg(Reg::R1),
642                },
643            );
644            assert_eq!(
645                validator.verify_rule(&or_rule).unwrap(),
646                ValidationResult::Verified
647            );
648
649            // Test XOR
650            let xor_rule = create_test_rule(
651                WasmOp::I32Xor,
652                ArmOp::Eor {
653                    rd: Reg::R0,
654                    rn: Reg::R0,
655                    op2: Operand2::Reg(Reg::R1),
656                },
657            );
658            assert_eq!(
659                validator.verify_rule(&xor_rule).unwrap(),
660                ValidationResult::Verified
661            );
662        });
663    }
664
665    #[test]
666    fn test_verify_shift_ops() {
667        // Note: Shift operations require concrete immediate values in ARM
668        // but use register operands in WASM. Verification requires
669        // modeling the shift amount modulo operation.
670        // TODO: Implement shift verification with proper modulo handling
671    }
672}