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_module silently 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 optimize from 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.
Refs #196, #238, #239, #331
Found while writing coverage tests for #331 — a test calling
optimize::optimize_moduleon a 724-function fixture recorded zero verification attempts in 0.01s, whileloom optimizeon 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: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-901calls 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_functionsappears nowhere in the CLI. The guard is applied inoptimize_moduleand incomponent_optimizer.rs(two sites); the binary's core-module path has no equivalent.Measured
loom-core/tests/fixtures/issue254-records-fused.wasm— 724 functions, andwasm-tools objdumpreports 4 element segments:optimize::optimize_moduleOk(())immediately, module unchanged, 0 verification attemptsloom optimize(same file)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-GCasserts 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 inoptimize_moduleis 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:
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
optimize_moduledoc 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:
loom optimizefrom touching any module with a table.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.Refs #196, #238, #239, #331