Skip to main content

synth_verify/
properties.rs

1//! Property-Based Testing for Compiler Correctness
2//!
3//! This module defines formal properties that the compiler must satisfy
4//! and uses proptest to automatically generate test cases to verify these properties.
5//!
6//! # Properties
7//!
8//! 1. **Type Preservation**: Compilation preserves type safety
9//! 2. **Semantic Preservation**: Compiled code has same behavior as source
10//! 3. **Memory Safety**: Generated code respects memory bounds
11//! 4. **Control Flow Correctness**: Branch targets are valid
12//! 5. **Optimization Soundness**: Optimizations don't change semantics
13
14use proptest::prelude::*;
15use synth_core::WasmOp;
16#[cfg(feature = "arm")]
17use synth_synthesis::{ArmOp, Operand2, Reg};
18
19/// Compiler properties to verify
20pub struct CompilerProperties;
21
22impl CompilerProperties {
23    /// Property: Arithmetic operations don't overflow without detection
24    ///
25    /// For any WASM arithmetic operation, if it overflows, the ARM code
26    /// must also overflow in the same way (wrapping semantics).
27    pub fn arithmetic_overflow_consistency() -> impl Strategy<Value = (i32, i32)> {
28        (any::<i32>(), any::<i32>())
29    }
30
31    /// Property: Bitwise operations are bit-precise
32    ///
33    /// For any WASM bitwise operation, the ARM code must produce
34    /// identical bit patterns for all possible inputs.
35    pub fn bitwise_bit_precision() -> impl Strategy<Value = (u32, u32)> {
36        (any::<u32>(), any::<u32>())
37    }
38
39    /// Property: Comparison operations produce correct boolean results
40    pub fn comparison_correctness() -> impl Strategy<Value = (i32, i32)> {
41        (any::<i32>(), any::<i32>())
42    }
43
44    /// Property: Shift operations handle edge cases correctly
45    ///
46    /// Shifts by >= 32 bits should behave according to WASM spec
47    /// (result is the original value shifted by (amount % 32))
48    pub fn shift_edge_cases() -> impl Strategy<Value = (i32, u32)> {
49        (any::<i32>(), any::<u32>())
50    }
51
52    /// Property: Division by zero handling
53    ///
54    /// WASM traps on division by zero, ARM behavior may differ
55    pub fn division_by_zero() -> impl Strategy<Value = i32> {
56        any::<i32>()
57    }
58
59    /// Property: Optimization preserves semantics
60    ///
61    /// For any sequence of operations, optimized and unoptimized
62    /// versions must produce identical results.
63    pub fn optimization_soundness() -> impl Strategy<Value = Vec<WasmOp>> {
64        // Generate sequences of WASM operations
65        prop::collection::vec(Self::arbitrary_wasm_op(), 1..10)
66    }
67
68    /// Generate arbitrary WASM operations for testing
69    fn arbitrary_wasm_op() -> impl Strategy<Value = WasmOp> {
70        prop_oneof![
71            Just(WasmOp::I32Add),
72            Just(WasmOp::I32Sub),
73            Just(WasmOp::I32Mul),
74            Just(WasmOp::I32And),
75            Just(WasmOp::I32Or),
76            Just(WasmOp::I32Xor),
77            Just(WasmOp::I32Shl),
78            Just(WasmOp::I32ShrU),
79            Just(WasmOp::I32ShrS),
80            any::<i32>().prop_map(WasmOp::I32Const),
81        ]
82    }
83
84    /// Generate arbitrary ARM operations for testing
85    #[cfg(feature = "arm")]
86    #[allow(dead_code)]
87    fn arbitrary_arm_op() -> impl Strategy<Value = ArmOp> {
88        let reg_strategy = prop_oneof![Just(Reg::R0), Just(Reg::R1), Just(Reg::R2), Just(Reg::R3),];
89
90        prop_oneof![
91            (
92                reg_strategy.clone(),
93                reg_strategy.clone(),
94                reg_strategy.clone()
95            )
96                .prop_map(|(rd, rn, rm)| ArmOp::Add {
97                    rd,
98                    rn,
99                    op2: Operand2::Reg(rm),
100                }),
101            (
102                reg_strategy.clone(),
103                reg_strategy.clone(),
104                reg_strategy.clone()
105            )
106                .prop_map(|(rd, rn, rm)| ArmOp::Sub {
107                    rd,
108                    rn,
109                    op2: Operand2::Reg(rm),
110                }),
111            (
112                reg_strategy.clone(),
113                reg_strategy.clone(),
114                reg_strategy.clone()
115            )
116                .prop_map(|(rd, rn, rm)| ArmOp::Mul { rd, rn, rm }),
117            (
118                reg_strategy.clone(),
119                reg_strategy.clone(),
120                reg_strategy.clone()
121            )
122                .prop_map(|(rd, rn, rm)| ArmOp::And {
123                    rd,
124                    rn,
125                    op2: Operand2::Reg(rm),
126                }),
127        ]
128    }
129}
130
131/// Test that WASM and ARM arithmetic have same overflow behavior
132#[cfg(test)]
133mod arithmetic_tests {
134    use super::*;
135
136    proptest! {
137        #[test]
138        fn test_add_overflow_consistency(a in any::<i32>(), b in any::<i32>()) {
139            // WASM i32.add has wrapping semantics
140            let wasm_result = a.wrapping_add(b);
141
142            // ARM ADD also has wrapping semantics
143            let arm_result = a.wrapping_add(b);
144
145            assert_eq!(wasm_result, arm_result,
146                "ADD overflow behavior differs: WASM={}, ARM={}", wasm_result, arm_result);
147        }
148
149        #[test]
150        fn test_sub_overflow_consistency(a in any::<i32>(), b in any::<i32>()) {
151            let wasm_result = a.wrapping_sub(b);
152            let arm_result = a.wrapping_sub(b);
153
154            assert_eq!(wasm_result, arm_result,
155                "SUB overflow behavior differs: WASM={}, ARM={}", wasm_result, arm_result);
156        }
157
158        #[test]
159        fn test_mul_overflow_consistency(a in any::<i32>(), b in any::<i32>()) {
160            let wasm_result = a.wrapping_mul(b);
161            let arm_result = a.wrapping_mul(b);
162
163            assert_eq!(wasm_result, arm_result,
164                "MUL overflow behavior differs: WASM={}, ARM={}", wasm_result, arm_result);
165        }
166    }
167}
168
169/// Test that bitwise operations are bit-precise
170#[cfg(test)]
171mod bitwise_tests {
172    use super::*;
173
174    proptest! {
175        #[test]
176        fn test_and_bit_precision(a in any::<u32>(), b in any::<u32>()) {
177            let wasm_result = a & b;
178            let arm_result = a & b;
179
180            assert_eq!(wasm_result, arm_result,
181                "AND bit precision differs: WASM={:032b}, ARM={:032b}", wasm_result, arm_result);
182        }
183
184        #[test]
185        fn test_or_bit_precision(a in any::<u32>(), b in any::<u32>()) {
186            let wasm_result = a | b;
187            let arm_result = a | b;
188
189            assert_eq!(wasm_result, arm_result);
190        }
191
192        #[test]
193        fn test_xor_bit_precision(a in any::<u32>(), b in any::<u32>()) {
194            let wasm_result = a ^ b;
195            let arm_result = a ^ b;
196
197            assert_eq!(wasm_result, arm_result);
198        }
199    }
200}
201
202/// Test that shift operations handle edge cases correctly
203#[cfg(test)]
204mod shift_tests {
205    use super::*;
206
207    proptest! {
208        #[test]
209        fn test_shl_edge_cases(value in any::<i32>(), shift in any::<u32>()) {
210            // WASM shifts by (shift % 32)
211            let wasm_shift = shift % 32;
212            let wasm_result = value.wrapping_shl(wasm_shift);
213
214            // ARM LSL also shifts by (shift % 32) for immediate
215            let arm_result = value.wrapping_shl(wasm_shift);
216
217            assert_eq!(wasm_result, arm_result,
218                "SHL edge case: value={}, shift={}, WASM={}, ARM={}",
219                value, shift, wasm_result, arm_result);
220        }
221
222        #[test]
223        fn test_shr_logical_edge_cases(value in any::<u32>(), shift in any::<u32>()) {
224            let wasm_shift = shift % 32;
225            let wasm_result = value.wrapping_shr(wasm_shift);
226            let arm_result = value.wrapping_shr(wasm_shift);
227
228            assert_eq!(wasm_result, arm_result);
229        }
230
231        #[test]
232        fn test_shr_arithmetic_edge_cases(value in any::<i32>(), shift in any::<u32>()) {
233            let wasm_shift = shift % 32;
234            let wasm_result = value.wrapping_shr(wasm_shift);
235            let arm_result = value.wrapping_shr(wasm_shift);
236
237            assert_eq!(wasm_result, arm_result);
238        }
239    }
240}
241
242/// Test that comparison operations produce correct results
243#[cfg(test)]
244mod comparison_tests {
245    use super::*;
246
247    proptest! {
248        #[test]
249        fn test_eq_correctness(a in any::<i32>(), b in any::<i32>()) {
250            let wasm_result = if a == b { 1 } else { 0 };
251            let arm_result = if a == b { 1 } else { 0 };
252
253            assert_eq!(wasm_result, arm_result);
254        }
255
256        #[test]
257        fn test_ne_correctness(a in any::<i32>(), b in any::<i32>()) {
258            let wasm_result = if a != b { 1 } else { 0 };
259            let arm_result = if a != b { 1 } else { 0 };
260
261            assert_eq!(wasm_result, arm_result);
262        }
263
264        #[test]
265        fn test_lt_signed_correctness(a in any::<i32>(), b in any::<i32>()) {
266            let wasm_result = if a < b { 1 } else { 0 };
267            let arm_result = if a < b { 1 } else { 0 };
268
269            assert_eq!(wasm_result, arm_result);
270        }
271
272        #[test]
273        fn test_lt_unsigned_correctness(a in any::<u32>(), b in any::<u32>()) {
274            let wasm_result = if a < b { 1 } else { 0 };
275            let arm_result = if a < b { 1 } else { 0 };
276
277            assert_eq!(wasm_result, arm_result);
278        }
279
280        #[test]
281        fn test_le_signed_correctness(a in any::<i32>(), b in any::<i32>()) {
282            let wasm_result = if a <= b { 1 } else { 0 };
283            let arm_result = if a <= b { 1 } else { 0 };
284
285            assert_eq!(wasm_result, arm_result);
286        }
287
288        #[test]
289        fn test_gt_signed_correctness(a in any::<i32>(), b in any::<i32>()) {
290            let wasm_result = if a > b { 1 } else { 0 };
291            let arm_result = if a > b { 1 } else { 0 };
292
293            assert_eq!(wasm_result, arm_result);
294        }
295
296        #[test]
297        fn test_ge_signed_correctness(a in any::<i32>(), b in any::<i32>()) {
298            let wasm_result = if a >= b { 1 } else { 0 };
299            let arm_result = if a >= b { 1 } else { 0 };
300
301            assert_eq!(wasm_result, arm_result);
302        }
303    }
304}
305
306/// Test optimization soundness
307#[cfg(test)]
308mod optimization_tests {
309    use super::*;
310
311    #[test]
312    fn test_constant_folding_soundness() {
313        // Test that constant folding produces correct results
314        // Example: (5 + 3) should fold to 8
315        let _original = [WasmOp::I32Const(5), WasmOp::I32Const(3), WasmOp::I32Add];
316        let _optimized = [WasmOp::I32Const(8)];
317
318        // Both should produce the same result
319        // (This would require an interpreter to verify properly)
320        // TODO: Implement WASM interpreter for verification
321    }
322
323    #[test]
324    fn test_dead_code_elimination_soundness() {
325        // Test that dead code elimination doesn't change observable behavior
326        // Example: Removing unreachable code after return
327        let _original = [WasmOp::I32Const(42), WasmOp::Return, WasmOp::I32Const(99)];
328        let _optimized = [WasmOp::I32Const(42), WasmOp::Return];
329
330        // Both should have identical observable behavior
331        // TODO: Implement WASM interpreter for verification
332    }
333
334    proptest! {
335        #[test]
336        fn test_algebraic_simplification_soundness(a in any::<i32>()) {
337            // Test: x + 0 = x
338            let with_identity = a.wrapping_add(0);
339            assert_eq!(with_identity, a);
340
341            // Test: x * 1 = x
342            let with_mul_identity = a.wrapping_mul(1);
343            assert_eq!(with_mul_identity, a);
344
345            // Test: x * 0 = 0
346            let with_mul_zero = a.wrapping_mul(0);
347            assert_eq!(with_mul_zero, 0);
348        }
349
350        #[test]
351        fn test_strength_reduction_soundness(a in any::<i32>()) {
352            // Test: x * 2 = x << 1
353            let mul_result = a.wrapping_mul(2);
354            let shift_result = a.wrapping_shl(1);
355            assert_eq!(mul_result, shift_result);
356
357            // Test: x * 4 = x << 2
358            let mul_result = a.wrapping_mul(4);
359            let shift_result = a.wrapping_shl(2);
360            assert_eq!(mul_result, shift_result);
361
362            // Test: x * 8 = x << 3
363            let mul_result = a.wrapping_mul(8);
364            let shift_result = a.wrapping_shl(3);
365            assert_eq!(mul_result, shift_result);
366        }
367    }
368}
369
370/// Memory safety properties
371#[cfg(test)]
372mod memory_tests {
373    use super::*;
374
375    proptest! {
376        #[test]
377        fn test_memory_bounds_checking(offset in 0u32..1024, size in 1u32..64) {
378            // Test that memory accesses within bounds are valid
379            let address = offset;
380            let end = address.wrapping_add(size);
381
382            // If we stay within bounds, access should be valid
383            // This is a simplified check - real implementation would verify
384            // against actual memory size
385            assert!(address <= end || size == 0);
386        }
387
388        #[test]
389        fn test_stack_pointer_validity(sp_offset in -256i32..256) {
390            // Test that stack pointer adjustments maintain alignment
391            // ARM requires 8-byte alignment for AAPCS
392            let base_sp = 0x20010000u32; // Typical ARM stack address
393            let new_sp = (base_sp as i32).wrapping_add(sp_offset) as u32;
394
395            // Check that SP remains in valid range
396            // (This is a simplified check)
397            assert!(new_sp > 0x20000000 && new_sp < 0x20020000);
398        }
399    }
400}