Repository navigation
Missed devirtualization of call_indirect with constant index under full optimization pipeline #8866
Description
Missed devirtualization of call_indirect with constant index under full optimization pipeline
System Info
- Binaryen Version: Commit
3c25487214600a965e6d5134252efc55b7980000(from Dart SDKthird_party/binaryen/src).
Description
We are seeing a case where wasm-opt fails to devirtualize a call_indirect instruction even though the index has been successfully resolved to a constant by gufa and the target function is statically known from the table initialization.
Specifically, in our "before" module:
- The
call_indirectindex is resolved to962(constant). - Table
0is immutable (notable.setor other write instructions). - An active
elemsegment initializes index962to$BoxedInt.toDouble:
(elem $42 (table $0) (i32.const 962) func $BoxedInt.toDouble $BoxedDouble.toDouble) - The signatures match.
However, the call_indirect remains in the optimized output.
If we run a focused pipeline containing only --gufa --directize, the call is successfully devirtualized:
wasm-opt --all-features --closed-world --gufa --directize before_unopt.wasm -S -o success.wat
# call_indirect is replaced by direct call to $BoxedInt.toDoubleBut if we run the full pipeline (which runs -Os before and after --gufa), the devirtualization fails:
wasm-opt --all-features --closed-world --traps-never-happen --type-unfinalizing -Os --type-ssa --gufa -Os --type-merging -Os --type-finalizing --minimize-rec-groups before_unopt.wasm -S -o fail.wat
# call_indirect remains as call_indirect to index 962In the "after" version of our module (where class IDs shifted and the element segment became contiguous/larger), the devirtualization succeeds even under the full pipeline.
This suggests that some pass in the initial -Os suite is modifying the IR in a way that prevents the subsequent Directize pass from resolving the target, even though the index itself is successfully folded to a constant.
Relevant WAT Snippets
Before (Failing Case)
The element segment at offset 962 (size 2):
(elem $42 (table $0) (i32.const 962) func $BoxedInt.toDouble $BoxedDouble.toDouble)The optimized loop body still performing call_indirect even though the index is constant 962:
(loop $label
;; ...
(local.set $3
(f64.add
(local.tee $5 (local.get $3))
(call_indirect $0 (type $97)
(global.get $C655)
(i32.const 962) ;; constant index
)
)
)
;; ...
)After (Succeeding Case)
The element segment at offset 1081 (size 13), containing the target $BoxedInt.toDouble at index 1082:
(elem $32 (table $0) (i32.const 1081) func $BoxedDouble.toDouble $BoxedInt.toDouble $BoxedDouble.isNegative ...)The optimized loop body successfully directized:
(loop $label
;; ...
(local.set $3
(f64.add
(local.tee $5 (local.get $3))
(call $BoxedInt.toDouble
(global.get $C661)
)
)
)
;; ...
)Reproduction Steps
- Download
before_unopt.wasm(attached/provided). - Run the focused pipeline (succeeds):
Verify that
wasm-opt --all-features --closed-world --gufa --directize before_unopt.wasm -S -o success.wat
success.watcontainscall $BoxedInt.toDoubleinstead ofcall_indirectat the target call site. - Run the full pipeline (fails):
Verify that
wasm-opt --all-features --closed-world --traps-never-happen --type-unfinalizing -Os --type-ssa --gufa -Os --type-merging -Os --type-finalizing --minimize-rec-groups before_unopt.wasm -S -o fail.wat
fail.watstill contains(call_indirect $0 (type $97) ... (i32.const 962))in the loop.
Activity
kevmoo commentedon Jun 25, 2026
After investigation, it seems that the wasm-opt threshold for table.fill makes sense. Going to see about optimizing our compiler initialization strategy!