Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"Bash(git apply:*)",
"Bash(Z3_SYS_Z3_HEADER=/opt/homebrew/opt/z3/include/z3.h git add -A)",
"Bash(Z3_SYS_Z3_HEADER=/opt/homebrew/opt/z3/include/z3.h git commit -m \"fix: skip advanced_math.wat in CI validation (known issue #33)\n\nThe function inlining pass produces invalid WASM for advanced_math.wat,\nleaving values on the stack. This is tracked in issue #33.\nSkip this fixture in CI until the bug is fixed.\")",
"Bash(git push:*)"
"Bash(git push:*)",
"Bash(Z3_SYS_Z3_HEADER=/opt/homebrew/opt/z3/include/z3.h git commit -m \"$(cat <<''EOF''\nfix: disable buggy eliminate_redundant_sets optimization\n\n## Problem\nThe `eliminate_redundant_sets` optimization in `simplify-locals` was\nincorrectly removing stack-producing instructions, causing invalid WASM\noutput with \"values remaining on stack\" errors.\n\n## Root Cause\nThe optimization assumed that the value for a `local.set` was always\nproduced by the immediately preceding instruction (`result.len() - 1`).\nThis is incorrect in stack-based WASM where values can come from complex\nexpression trees spanning multiple instructions.\n\nExample of buggy behavior:\n```wat\nInput:\n local.get $base\n i32.const 3\n i32.shl # ← Produces value for local.set\n local.set $temp1\n\nOutput (broken):\n local.get $base\n i32.const 3\n # i32.shl REMOVED! ← Bug: incorrectly removed\n local.set $temp1\n```\n\n## Fix\nDisabled the `eliminate_redundant_sets` optimization by commenting it out\nin `simplify_locals` (loom-core/src/lib.rs:4155).\n\nThe optimization needs to be rewritten to properly understand the stack\ndiscipline and track which instructions actually produce values for sets.\n\n## Changes\n1. **loom-core/src/lib.rs:4155** - Commented out eliminate_redundant_sets call\n2. **loom-core/src/lib.rs:4369** - Added `#[allow(dead_code)]` to function\n3. **loom-core/tests/optimization_tests.rs** - Marked RSE tests as `#[ignore]`\n4. **.github/workflows/ci.yml** - Removed skips for advanced_math.wat and crypto_utils.wat\n\n## Testing\n- ✅ Both advanced_math.wat and crypto_utils.wat now validate correctly\n- ✅ All other tests pass\n- ✅ Idempotence test still passes\n\n## Impact\nThe `simplify-locals` pass still performs other optimizations (local variable\nequivalence, redundant get/set elimination). Only the buggy redundant set\nelimination is disabled.\nEOF\n)\")"
],
"deny": [],
"ask": []
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@ jobs:
- name: Optimize test fixtures
run: |
for fixture in tests/fixtures/*.wat; do
# Skip fixtures with known inlining bugs (issue #33)
if [[ "$fixture" == *"advanced_math.wat"* ]] || [[ "$fixture" == *"crypto_utils.wat"* ]]; then
echo "⏭️ Skipping $fixture (known issue #33)"
continue
fi
echo "Optimizing $fixture"
./target/release/loom optimize "$fixture" -o "/tmp/$(basename $fixture .wat).wasm"
wasm-tools validate "/tmp/$(basename $fixture .wat).wasm"
Expand Down
104 changes: 58 additions & 46 deletions loom-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,12 +680,9 @@ pub mod parse {
element_section_bytes = Some(bytes[range.start..range.end].to_vec());
}
Payload::CustomSection(reader) => {
// Store raw custom section bytes to pass through unchanged
let range = reader.range();
custom_sections.push((
reader.name().to_string(),
bytes[range.start..range.end].to_vec(),
));
// Store custom section data (without the name field)
// The encoder will add the name field when re-encoding
custom_sections.push((reader.name().to_string(), reader.data().to_vec()));
}
_ => {
// Ignore other payloads (e.g., End, Version, etc.)
Expand Down Expand Up @@ -4151,8 +4148,11 @@ pub mod optimize {
func.instructions =
simplify_instructions(&func.instructions, &usage, &equivalences, &mut changed);

// Eliminate redundant sets (new optimization)
func.instructions = eliminate_redundant_sets(&func.instructions, &mut changed);
// TODO: Fix eliminate_redundant_sets - it incorrectly removes stack-producing instructions
// The bug is that it assumes value_idx is always result.len() - 1, but in stack-based
// WASM, the value could be produced by an instruction multiple positions back.
// Disabled until properly fixed.
// func.instructions = eliminate_redundant_sets(&func.instructions, &mut changed);
}
}
Ok(())
Expand Down Expand Up @@ -4366,6 +4366,7 @@ pub mod optimize {
/// - 2-5% binary size reduction on typical code
/// - Reduces unnecessary local variable writes
/// - Enables further optimizations (dead code elimination)
#[allow(dead_code)] // Currently disabled due to bugs - see comment in simplify_locals
fn eliminate_redundant_sets(
instructions: &[Instruction],
changed: &mut bool,
Expand Down Expand Up @@ -6806,53 +6807,64 @@ pub mod optimize {
pub fn inline_functions(module: &mut Module) -> Result<()> {
use std::collections::HashMap;

// Phase 1: Build call graph and analyze functions
let mut call_counts: HashMap<u32, usize> = HashMap::new();
let mut function_sizes: HashMap<u32, usize> = HashMap::new();
// Run inlining to fixed point to ensure idempotence
// Keep inlining until no more candidates are found
loop {
// Phase 1: Build call graph and analyze functions
let mut call_counts: HashMap<u32, usize> = HashMap::new();
let mut function_sizes: HashMap<u32, usize> = HashMap::new();

// Calculate function sizes (instruction count)
for (idx, func) in module.functions.iter().enumerate() {
let size = count_instructions_recursive(&func.instructions);
function_sizes.insert(idx as u32, size);
}
// Calculate function sizes (instruction count)
for (idx, func) in module.functions.iter().enumerate() {
let size = count_instructions_recursive(&func.instructions);
function_sizes.insert(idx as u32, size);
}

// Count call sites for each function
for func in &module.functions {
count_calls_recursive(&func.instructions, &mut call_counts);
}
// Count call sites for each function
for func in &module.functions {
count_calls_recursive(&func.instructions, &mut call_counts);
}

// Phase 2: Identify inlining candidates
let mut inline_candidates = Vec::new();
for (func_idx, &call_count) in &call_counts {
let size = function_sizes.get(func_idx).copied().unwrap_or(0);
// Phase 2: Identify inlining candidates
let mut inline_candidates = Vec::new();
for (func_idx, &call_count) in &call_counts {
let size = function_sizes.get(func_idx).copied().unwrap_or(0);

// Heuristic: inline if:
// 1. Single call site, OR
// 2. Small function (< 10 instructions)
if call_count == 1 || size < 10 {
// Don't inline large functions even if single call site
if size < 50 {
inline_candidates.push(*func_idx);
// Heuristic: inline if:
// 1. Single call site, OR
// 2. Small function (< 10 instructions)
if call_count == 1 || size < 10 {
// Don't inline large functions even if single call site
if size < 50 {
inline_candidates.push(*func_idx);
}
}
}
}

// Phase 3: Perform inlining
// For each function, inline calls to candidate functions
let inline_set: std::collections::HashSet<u32> =
inline_candidates.iter().copied().collect();
// Exit loop if no more inlining opportunities - this ensures idempotence
if inline_candidates.is_empty() {
break;
}

// Clone functions to avoid borrow checker issues
let all_functions = module.functions.clone();
// Phase 3: Perform inlining
// For each function, inline calls to candidate functions
let inline_set: std::collections::HashSet<u32> =
inline_candidates.iter().copied().collect();

for func in &mut module.functions {
func.instructions = inline_calls_in_block(
&func.instructions,
&inline_set,
&all_functions,
func.signature.params.len() as u32,
&mut func.locals,
);
// Clone functions to avoid borrow checker issues
let all_functions = module.functions.clone();

for func in &mut module.functions {
func.instructions = inline_calls_in_block(
&func.instructions,
&inline_set,
&all_functions,
func.signature.params.len() as u32,
&mut func.locals,
);
}

// Continue to next iteration to check for more inlining opportunities
}

Ok(())
Expand Down
9 changes: 8 additions & 1 deletion loom-core/tests/optimization_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ fn test_self_ne_i64() {
// ============================================================================

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_simple_redundant_set() {
let input = r#"
(module
Expand Down Expand Up @@ -923,6 +924,7 @@ fn test_rse_simple_redundant_set() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_with_intervening_get() {
let input = r#"
(module
Expand Down Expand Up @@ -952,6 +954,7 @@ fn test_rse_with_intervening_get() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_multiple_redundant_sets() {
let input = r#"
(module
Expand Down Expand Up @@ -979,6 +982,7 @@ fn test_rse_multiple_redundant_sets() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_different_locals() {
let input = r#"
(module
Expand Down Expand Up @@ -1018,6 +1022,7 @@ fn test_rse_different_locals() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_with_tee() {
let input = r#"
(module
Expand Down Expand Up @@ -1050,6 +1055,7 @@ fn test_rse_with_tee() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_in_block() {
let input = r#"
(module
Expand Down Expand Up @@ -1078,6 +1084,7 @@ fn test_rse_in_block() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_conservative_in_if() {
let input = r#"
(module
Expand Down Expand Up @@ -1637,6 +1644,7 @@ fn test_merge_constant_adds_i64() {
}

#[test]
#[ignore] // eliminate_redundant_sets disabled - has bugs
fn test_rse_no_false_positive_with_call() {
let input = r#"
(module
Expand Down Expand Up @@ -1796,7 +1804,6 @@ fn test_inline_stack_discipline_with_locals() {
}

#[test]
#[ignore] // TODO: Fix idempotence issue - see GitHub issue #33
fn test_inline_stack_discipline_idempotence() {
let input = r#"
(module
Expand Down
Loading