Skip to content
Merged
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ All notable changes to LOOM will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.11] - 2026-06-10

**Bug fix: fused pass no longer invalidates core modules with element segments (#172).**
The fused component-optimization pass produced an invalid core module after
removing a dead function, so loom fail-safe-fell-back to the original bytes —
leaving the largest module of a real Component Model artifact (falcon-flight)
at ~0% optimization. Fixed; that component now goes 0.2% → 3.9% (its hot core
module 0: 96.5 KB → 92.6 KB), output validates.

### Fixed

- **Element-section segment-count prefix stripped during function-index remap**
(`fused_optimizer::remap_element_section_refs`). After dead-function removal,
the pass rebuilds the element section with `wasm_encoder` and extracts the
raw payload to store in `element_section_bytes`. `wasm_encoder`'s
`Encode::encode` writes a **length-prefixed body** (`<LEB128 len><payload>`),
with no section-id byte — but the extraction assumed `id + len + payload` and
skipped a phantom id byte, stripping the leading `<segment-count>` off the
payload. The count-less segment then re-validated as `section size mismatch:
unexpected data at the end of the section`. Fix: strip only the leading
LEB128 length, from offset 0.

### Validation

- New `test_remap_element_section_preserves_segment_count` (asserts the
rebuilt payload still parses as exactly one segment with the original func
indices, consuming every byte). 393 lib + 85 integration tests pass.
- gale's exact artifact (`falcon-flight-v1.34.wasm`, sha256 `3213a135…`) now
optimizes all 4 core modules (was 3/4); `wasm-tools validate` passes.

## [1.1.10] - 2026-06-02

**Optimization release: inline callees with integer division (#163) — full
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [
]

[workspace.package]
version = "1.1.10"
version = "1.1.11"
authors = ["PulseEngine <https://github.com/pulseengine>"]
edition = "2024"
license = "Apache-2.0"
Expand Down
89 changes: 77 additions & 12 deletions loom-core/src/fused_optimizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2455,21 +2455,28 @@ fn remap_element_section_refs(module: &mut Module, remap: &HashMap<u32, u32>) ->
}
}

// Encode the new section and extract just the section data
// (skip section ID and LEB128 length prefix)
// Extract the section *payload* (`<segment-count><segments>`) to store in
// `element_section_bytes`; encode_wasm prepends the id and a freshly-derived
// length when emitting.
//
// #172: `wasm_encoder`'s `Encode::encode` for a section builder writes the
// length-prefixed BODY (`<LEB128 len><payload>`) — it does NOT emit a
// section-id byte (the id is supplied separately via the `Section` trait).
// The previous code assumed `id + len + payload` and skipped a phantom id
// byte first, which stripped the `<segment-count>` prefix off the payload —
// leaving a count-less segment that re-validates as "section size mismatch:
// unexpected data at the end of the section". Strip ONLY the leading LEB128
// length, from offset 0.
use wasm_encoder::Encode;
let mut encoded = Vec::new();
new_section.encode(&mut encoded);
// ElementSection::encode writes: section_id (1 byte) + LEB128 length + data
if encoded.len() > 1 {
let mut pos = 1; // Skip section ID byte
while pos < encoded.len() && encoded[pos] & 0x80 != 0 {
pos += 1; // Skip LEB128 length bytes
}
pos += 1; // Skip last byte of LEB128
if pos < encoded.len() {
module.element_section_bytes = Some(encoded[pos..].to_vec());
}
let mut pos = 0;
while pos < encoded.len() && encoded[pos] & 0x80 != 0 {
pos += 1; // LEB128 length continuation bytes
}
pos += 1; // final LEB128 length byte
if pos <= encoded.len() {
module.element_section_bytes = Some(encoded[pos..].to_vec());
}

Ok(())
Expand Down Expand Up @@ -5910,4 +5917,62 @@ mod tests {
stats.function_bodies_deduplicated
);
}

/// #172 regression: rebuilding the element section after a function-index
/// remap must preserve the leading `<segment-count>` prefix of the section
/// payload. The bug stripped it (the extraction skipped a phantom section-id
/// byte that `wasm_encoder`'s length-prefixed body never emits), leaving a
/// count-less segment that re-validates as "section size mismatch:
/// unexpected data at the end of the section" — which made loom fall back to
/// the original bytes on falcon-flight's core module 0 (~0% optimization).
#[test]
fn test_remap_element_section_preserves_segment_count() {
use std::collections::HashMap;
let mut module = empty_module();

// One active element segment (table 0, offset i32.const 0) listing func
// indices [0, 1, 2]. Payload = <segment-count=1><flags=0><offset:
// i32.const 0, end><n-funcs=3><0,1,2>.
let payload: Vec<u8> = vec![
0x01, // segment count = 1
0x00, // flags: active, table 0, func-index list
0x41, 0x00, 0x0b, // offset const expr: i32.const 0, end
0x03, // number of functions
0x00, 0x01, 0x02, // function indices
];
module.element_section_bytes = Some(payload);

// Identity remap — the bug is in payload reconstruction, not in the
// index values, so identity is enough to exercise it.
let remap: HashMap<u32, u32> = (0u32..3).map(|i| (i, i)).collect();
remap_element_section_refs(&mut module, &remap).expect("remap must succeed");

let rebuilt = module
.element_section_bytes
.as_ref()
.expect("element section must remain present");

// The payload must still parse as exactly ONE segment with the original
// func indices — and consume every byte (no trailing data, which is the
// exact symptom of a stripped count prefix).
let reader =
wasmparser::ElementSectionReader::new(wasmparser::BinaryReader::new(rebuilt, 0))
.expect("rebuilt element section payload must parse");
assert_eq!(
reader.count(),
1,
"#172: segment-count prefix must survive the remap (got {} segments)",
reader.count()
);
for elem in reader {
let elem = elem.expect("element segment must parse");
match elem.items {
wasmparser::ElementItems::Functions(funcs) => {
let idxs: Vec<u32> = funcs.into_iter().map(|f| f.unwrap()).collect();
assert_eq!(idxs, vec![0, 1, 2], "func indices must be preserved");
}
_ => panic!("expected a function-index element list"),
}
}
}
}
Loading