-
Notifications
You must be signed in to change notification settings - Fork 0
optimize_module applies the #196 element-section guard; the CLI pipeline does not — the library and the binary optimize differently #345
Description
Found while writing coverage tests for #331 — a test calling optimize::optimize_module on a 724-function fixture recorded zero verification attempts in 0.01s, while loom optimize on the same file recorded 7575. Not a bug in either, but they are not the same optimizer, and nothing says so.
The divergence
optimize::optimize_module (loom-core/src/lib.rs:7103) opens with the #196 fail-safe:
if super::fused_optimizer::element_section_references_functions(module) { return Ok(()); }
A module with a function-referencing element segment is skipped entirely — the documented rationale being that structural verification cannot detect a scrambled or stale function-pointer table, the v1.1.11 silent miscompile.
The CLI does not go through that function. loom-cli/src/main.rs:711-901 calls each pass directly — directize, inline_functions, forward_carrier_locals, precompute, constant_folding, canonicalize, egraph_optimize, CSE, simplify_branches, DCE, merge_blocks, vacuum, simplify_locals, dead-stores, dead-locals. element_section_references_functions appears nowhere in the CLI. The guard is applied in optimize_module and in component_optimizer.rs (two sites); the binary's core-module path has no equivalent.
Measured
loom-core/tests/fixtures/issue254-records-fused.wasm — 724 functions, and wasm-tools objdump reports 4 element segments:
| entry point | behaviour |
|---|---|
optimize::optimize_module |
returns Ok(()) immediately, module unchanged, 0 verification attempts |
loom optimize (same file) |
full pipeline, 7575 verification attempts, module changed |
What I am not claiming
I am not claiming a miscompile, and I checked before writing this. Per-pass #196 protections do exist and are tested — TEST-239B-COMPONENT-REACHABILITY-GC asserts that a function reachable only through an element segment is preserved and its module comes through byte-identical, and the element section is remapped rather than frozen. It is entirely possible the module-level bail in optimize_module is a legacy stopgap that the per-pass guards have since superseded, and that the CLI path is the intended one.
The actual problem
Whichever is intended, both cannot be. Today:
- a library consumer calling
optimize_modulesilently gets no optimization at all on any module with an indirect-call table — a large fraction of real modules — with no diagnostic saying why; - the binary optimizes those same modules fully;
- nothing in either doc comment mentions the other's behaviour.
The optimize_module doc comment says "For backwards compatibility, this function applies the core optimizations. The full optimization pipeline is in loom-cli/src/main.rs", which describes a difference in pass coverage — not a difference in whether the module is processed at all.
Ask
Decide which contract is right and make the other match:
- If the module-level bail is still load-bearing, the CLI needs it too — and that is a behaviour change worth measuring, since it would stop
loom optimizefrom touching any module with a table. - If the per-pass guards have superseded it, remove the bail from
optimize_module(or reduce it to a documented, diagnosed skip) so the library and the binary agree.
Either way the silent return Ok(()) should say something: a module that comes back unoptimized with no explanation is indistinguishable from one that had nothing to optimize — the same "no silent failures" tension as #331 and #332.