Skip to content

optimize_module applies the #196 element-section guard; the CLI pipeline does not — the library and the binary optimize differently #345

Description

@avrabe

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:

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions