Skip to content

Commit 26f2c21

Browse files
committed
Add tests for arithmetic overflow and deeply nested expressions in WASM codegen
- Implemented arithmetic overflow tests for i32, i64, and u32 types, verifying correct wrapping behavior for boundary cases. - Added execution tests to validate the generated WASM modules against expected outputs. - Created a new test file for deeply nested expressions, ensuring the compiler handles complex expression trees correctly. - Included regeneration scripts for updating expected WASM and WAT files based on current compiler output.
1 parent da3e07b commit 26f2c21

File tree

13 files changed

+935
-15
lines changed

13 files changed

+935
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Execution functions use target's release optimization so proofs cover actual deployed code
2525
- `OptLevel` is currently metadata only; optimization passes planned for future
2626
- Add validation guards in `codegen()`: reject proof mode with non-Wasm32 targets, reject Soroban with non-det operations ([#97])
27+
- Upgrade shadowing detection from `debug_assert!` to `assert!` in `pre_scan_locals` — fires in release builds for parameter, constant, and variable name collisions in `locals_map`
28+
- Add `Statement::Loop` body recursion to `pre_scan_locals()` — locals inside loop bodies will be pre-registered when loop lowering is implemented
29+
- Replace silent `if let ArgumentType::Argument` skip with exhaustive `match` covering `SelfReference`, `IgnoreArgument`, and `Type` variants, each with an explicit `todo!()`
2730
- Add assignment statement lowering to WebAssembly codegen ([#146])
2831
- `mut` keyword support in AST: `is_mut: bool` field on `VariableDefinitionStatement`
2932
- Mutability enforcement in type-checker: `AssignToImmutable` error for assignment to non-`mut` variables
@@ -80,6 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8083

8184
### Testing
8285

86+
- Add execution test for `numeric_literals` verifying MIN/MAX boundary values for all 8 integer types (i8, i16, i32, i64, u8, u16, u32, u64) via Wasmtime
87+
- Add `arith_overflow` test module with 8 functions covering two's-complement wrapping arithmetic: i32/i64/u32 overflow and underflow, multiplication overflow, and negation of MIN (8 Wasmtime execution assertions)
88+
- Add `expr_deep_nesting` test module with 5 functions verifying 8+ level expression nesting: left-associative addition chain, mixed arithmetic in nested groups, boolean connectives over nested comparisons, function calls embedded in expressions, and chained unary negation (6 Wasmtime execution assertions)
8389
- Add 2 assignment test fixtures with 10 Wasmtime execution assertions ([#146])
8490
- `assign.inf`: 10 functions covering simple i32/i64 assignment, expression RHS, parameter assignment, multiple reassignment, function call RHS, bool assignment, assignment inside conditional, mutable parameter assignment
8591
- `assign_nondet.inf`: assignment inside `forall` non-det block with uzumaki RHS

book/arithmetic-overflow-in-wasm-codegen.md

Lines changed: 309 additions & 0 deletions
Large diffs are not rendered by default.

core/wasm-codegen/src/compiler.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -242,19 +242,30 @@ impl Compiler {
242242

243243
if let Some(arguments) = &function_definition.arguments {
244244
for arg_type in arguments {
245-
if let ArgumentType::Argument(arg) = arg_type {
246-
cov_mark::hit!(wasm_codegen_emit_function_params);
247-
let vt = Self::val_type_from_type(&arg.ty)
248-
.expect("Function parameter type must not be unit");
249-
params.push(vt);
250-
let prev = locals_map.insert(arg.name(), (local_idx, vt));
251-
debug_assert!(
252-
prev.is_none(),
253-
"parameter `{}` collides with an existing entry in locals_map; \
254-
the type-checker should have rejected duplicate parameter names",
255-
arg.name(),
256-
);
257-
local_idx += 1;
245+
match arg_type {
246+
ArgumentType::Argument(arg) => {
247+
cov_mark::hit!(wasm_codegen_emit_function_params);
248+
let vt = Self::val_type_from_type(&arg.ty)
249+
.expect("Function parameter type must not be unit");
250+
params.push(vt);
251+
let prev = locals_map.insert(arg.name(), (local_idx, vt));
252+
assert!(
253+
prev.is_none(),
254+
"parameter `{}` collides with an existing entry in locals_map; \
255+
the type-checker should have rejected duplicate parameter names",
256+
arg.name(),
257+
);
258+
local_idx += 1;
259+
}
260+
ArgumentType::SelfReference(_) => {
261+
todo!("Self-reference parameters are not yet supported in WASM codegen")
262+
}
263+
ArgumentType::IgnoreArgument(_) => {
264+
todo!("Ignore arguments are not yet supported in WASM codegen")
265+
}
266+
ArgumentType::Type(_) => {
267+
todo!("Type arguments are not yet supported in WASM codegen")
268+
}
258269
}
259270
}
260271
}
@@ -363,7 +374,7 @@ impl Compiler {
363374
};
364375
let prev =
365376
locals_map.insert(constant_definition.name(), (*local_idx, val_type));
366-
debug_assert!(
377+
assert!(
367378
prev.is_none(),
368379
"local `{}` collides with an existing entry in locals_map; \
369380
the type-checker should have rejected shadowing",
@@ -382,7 +393,7 @@ impl Compiler {
382393
};
383394
let prev =
384395
locals_map.insert(variable_definition.name(), (*local_idx, val_type));
385-
debug_assert!(
396+
assert!(
386397
prev.is_none(),
387398
"local `{}` collides with an existing entry in locals_map; \
388399
the type-checker should have rejected shadowing",
@@ -399,6 +410,9 @@ impl Compiler {
399410
Self::pre_scan_locals(else_arm, ctx, locals_map, local_idx);
400411
}
401412
}
413+
Statement::Loop(loop_statement) => {
414+
Self::pre_scan_locals(&loop_statement.body, ctx, locals_map, local_idx);
415+
}
402416
_ => {}
403417
}
404418
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Arithmetic overflow/wrapping behavior tests.
2+
//
3+
// WASM integers use two's complement wrapping arithmetic with no overflow traps.
4+
// These tests verify that boundary arithmetic (max+1, min-1, negating min, etc.)
5+
// wraps correctly for i32, i64, and u32 types.
6+
//
7+
// Expected wrapping behavior:
8+
// i32_max_plus_one: 2147483647 + 1 = -2147483648 (wraps to i32::MIN)
9+
// i32_min_minus_one: -2147483648 - 1 = 2147483647 (wraps to i32::MAX)
10+
// i64_max_plus_one: i64::MAX + 1 = i64::MIN (wraps to i64::MIN)
11+
// i64_min_minus_one: i64::MIN - 1 = i64::MAX (wraps to i64::MAX)
12+
// u32_max_plus_one: 4294967295 + 1 = 0 (wraps to zero)
13+
// i32_mul_overflow: 2147483647 * 2 = -2 (truncated to i32)
14+
// i32_neg_min: -(-2147483648) = -2147483648 (negating MIN wraps to MIN)
15+
// i64_neg_min: -(i64::MIN) = i64::MIN (negating MIN wraps to MIN)
16+
//
17+
// Total: 6 binary expressions, 2 prefix unary (neg), 11 constant definitions.
18+
19+
#[cfg(test)]
20+
mod arith_overflow_tests {
21+
use crate::utils::{
22+
assert_wasms_modules_equivalence, assert_wat_equivalence, get_test_file_path,
23+
get_test_wasm_path, wasm_codegen,
24+
};
25+
26+
#[test]
27+
fn arith_overflow_test() {
28+
cov_mark::check_count!(wasm_codegen_emit_binary_expression, 6);
29+
cov_mark::check_count!(wasm_codegen_emit_prefix_unary_expression, 2);
30+
cov_mark::check_count!(wasm_codegen_emit_unary_neg, 2);
31+
cov_mark::check_count!(wasm_codegen_emit_constant_definition, 11);
32+
let test_name = "arith_overflow";
33+
let test_file_path = get_test_file_path(module_path!(), test_name);
34+
let source_code = std::fs::read_to_string(&test_file_path)
35+
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
36+
let actual = wasm_codegen(&source_code);
37+
inf_wasmparser::validate(&actual)
38+
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
39+
let expected = get_test_wasm_path(module_path!(), test_name);
40+
let expected = std::fs::read(&expected)
41+
.unwrap_or_else(|_| panic!("Failed to read expected wasm file for test: {test_name}"));
42+
assert_wasms_modules_equivalence(&expected, &actual);
43+
assert_wat_equivalence(&actual, module_path!(), test_name);
44+
}
45+
46+
#[test]
47+
fn arith_overflow_execution_test() {
48+
use wasmtime::{Engine, Module, Store, TypedFunc};
49+
50+
let test_name = "arith_overflow";
51+
let test_file_path = get_test_file_path(module_path!(), test_name);
52+
let source_code = std::fs::read_to_string(&test_file_path)
53+
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
54+
let wasm_bytes = wasm_codegen(&source_code);
55+
56+
let engine = Engine::default();
57+
let module = Module::new(&engine, &wasm_bytes)
58+
.unwrap_or_else(|e| panic!("Failed to create Wasm module: {e}"));
59+
let mut store = Store::new(&engine, ());
60+
let instance = wasmtime::Instance::new(&mut store, &module, &[])
61+
.unwrap_or_else(|e| panic!("Failed to instantiate Wasm module: {e}"));
62+
63+
macro_rules! call {
64+
($name:expr, $ty:ty, $args:expr, $expected:expr) => {{
65+
let f: TypedFunc<_, $ty> = instance
66+
.get_typed_func(&mut store, $name)
67+
.unwrap_or_else(|e| panic!("Failed to get '{}': {e}", $name));
68+
let result = f
69+
.call(&mut store, $args)
70+
.unwrap_or_else(|e| panic!("Call to '{}' failed: {e}", $name));
71+
assert_eq!(result, $expected, "{}({:?}) expected {:?}", $name, $args, $expected);
72+
}};
73+
}
74+
75+
// --- i32 wrapping ---
76+
77+
// i32::MAX + 1 wraps to i32::MIN
78+
call!("i32_max_plus_one", i32, (), i32::MIN);
79+
// i32::MIN - 1 wraps to i32::MAX
80+
call!("i32_min_minus_one", i32, (), i32::MAX);
81+
82+
// --- i64 wrapping ---
83+
84+
// i64::MAX + 1 wraps to i64::MIN
85+
call!("i64_max_plus_one", i64, (), i64::MIN);
86+
// i64::MIN - 1 wraps to i64::MAX
87+
call!("i64_min_minus_one", i64, (), i64::MAX);
88+
89+
// --- u32 wrapping (returned as i32 in WASM) ---
90+
91+
// u32::MAX + 1 wraps to 0
92+
call!("u32_max_plus_one", i32, (), 0_i32);
93+
94+
// --- Multiplication overflow ---
95+
96+
// 2147483647 * 2 = 4294967294 = -2 as i32
97+
call!("i32_mul_overflow", i32, (), -2_i32);
98+
99+
// --- Negation of MIN wraps back to MIN ---
100+
101+
// -i32::MIN = i32::MIN (two's complement: no positive representation)
102+
call!("i32_neg_min", i32, (), i32::MIN);
103+
// -i64::MIN = i64::MIN
104+
call!("i64_neg_min", i64, (), i64::MIN);
105+
}
106+
}
107+
108+
/// Test data regeneration helper.
109+
///
110+
/// Regenerates the expected `.wasm` and `.wat` golden files from the current compiler output.
111+
/// Run with `--ignored` flag:
112+
///
113+
/// ```bash
114+
/// cargo test -p inference-tests codegen::wasm::arith_overflow::regenerate -- --ignored
115+
/// ```
116+
#[cfg(test)]
117+
mod regenerate {
118+
use crate::utils::{get_test_data_path, regenerate_wat, wasm_codegen};
119+
120+
fn test_dir() -> std::path::PathBuf {
121+
get_test_data_path()
122+
.join("codegen")
123+
.join("wasm")
124+
.join("arith_overflow")
125+
}
126+
127+
#[test]
128+
#[ignore]
129+
fn regenerate_arith_overflow_wasm() {
130+
let dir = test_dir();
131+
let source_code = std::fs::read_to_string(dir.join("arith_overflow.inf"))
132+
.expect("Failed to read arith_overflow.inf");
133+
let actual = wasm_codegen(&source_code);
134+
inf_wasmparser::validate(&actual)
135+
.unwrap_or_else(|e| panic!("Generated Wasm module is invalid: {}", e));
136+
let wasm_path = dir.join("arith_overflow.wasm");
137+
std::fs::write(&wasm_path, &actual)
138+
.unwrap_or_else(|e| panic!("Failed to write {}: {e}", wasm_path.display()));
139+
println!(
140+
"Regenerated: {} ({} bytes)",
141+
wasm_path.display(),
142+
actual.len()
143+
);
144+
regenerate_wat(&actual, &dir, "arith_overflow");
145+
}
146+
}

tests/src/codegen/wasm/base.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,50 @@ mod base_codegen_tests {
229229
assert_wat_equivalence(&actual, module_path!(), test_name);
230230
}
231231

232+
#[test]
233+
fn numeric_literals_execution_test() {
234+
use wasmtime::{Engine, Module, Store, TypedFunc};
235+
236+
let test_name = "numeric_literals";
237+
let test_file_path = get_test_file_path(module_path!(), test_name);
238+
let source_code = std::fs::read_to_string(&test_file_path)
239+
.unwrap_or_else(|_| panic!("Failed to read test file: {test_file_path:?}"));
240+
let wasm_bytes = wasm_codegen(&source_code);
241+
242+
let engine = Engine::default();
243+
let module = Module::new(&engine, &wasm_bytes)
244+
.unwrap_or_else(|e| panic!("Failed to create Wasm module: {e}"));
245+
let mut store = Store::new(&engine, ());
246+
let instance = wasmtime::Instance::new(&mut store, &module, &[])
247+
.unwrap_or_else(|e| panic!("Failed to instantiate Wasm module: {e}"));
248+
249+
macro_rules! call {
250+
($name:expr, $ty:ty, $args:expr, $expected:expr) => {{
251+
let f: TypedFunc<_, $ty> = instance
252+
.get_typed_func(&mut store, $name)
253+
.unwrap_or_else(|e| panic!("Failed to get '{}': {e}", $name));
254+
let result = f
255+
.call(&mut store, $args)
256+
.unwrap_or_else(|e| panic!("Call to '{}' failed: {e}", $name));
257+
assert_eq!(result, $expected, "{}({:?}) expected {:?}", $name, $args, $expected);
258+
}};
259+
}
260+
261+
// Signed types return as i32 (sub-i32 types promoted)
262+
call!("signed_i8", i32, (), -128_i32);
263+
call!("signed_i16", i32, (), -32768_i32);
264+
call!("signed_i32", i32, (), i32::MIN);
265+
call!("signed_i64", i64, (), i64::MIN);
266+
267+
// Unsigned types: sub-i32 promoted to i32, u32/u64 bit-reinterpreted
268+
call!("unsigned_u8", i32, (), 255_i32);
269+
call!("unsigned_u16", i32, (), 65535_i32);
270+
// u32::MAX (4294967295) is bit-reinterpreted as i32(-1)
271+
call!("unsigned_u32", i32, (), -1_i32);
272+
// u64::MAX is bit-reinterpreted as i64(-1)
273+
call!("unsigned_u64", i64, (), -1_i64);
274+
}
275+
232276
#[test]
233277
fn local_variables_test() {
234278
cov_mark::check_count!(wasm_codegen_emit_variable_definition, 14);

0 commit comments

Comments
 (0)