Summary
loom optimize (default pipeline) removes dead-result integer division/remainder operations, eliminating their mandatory divide-by-zero and INT_MIN/-1 overflow traps. When the result of an i32/i64 div_s/div_u/rem_s/rem_u is unused, the whole operation is deleted as dead code — so a module that must trap at runtime instead returns normally.
This is a trap-preservation / unsound-DCE bug. It is related to #273 but distinct and broader:
Same underlying gap as #273: value-equivalence verification does not model traps, so removing a trapping-but-dead op is accepted.
Repro
repro.wat:
(module
(func (export "f") (param $d i32) (result i32)
(local $t i32)
(local.set $t (i32.div_s (i32.const 100) (local.get $d)))
(i32.const 7)))
$ wasm-tools parse repro.wat -o repro.wasm
$ wasmtime run --invoke f repro.wasm 0
Error: wasm trap: integer divide by zero # correct: div by zero MUST trap
$ loom optimize repro.wasm -o repro.opt.wasm
$ wasmtime run --invoke f repro.opt.wasm 0
7 # WRONG: trap eliminated, returns normally
Optimized function body is reduced to i32.const 7 — the div_s is gone.
loom --version = 1.1.18. --verify reports ✓ Verification: 37/37 tests passed on the miscompiled output.
Scope (all confirmed, original traps / optimized returns 7)
| op (result dead) |
divisor |
orig |
loom-optimized |
i32.div_s(100, $d) |
runtime $d=0 |
trap |
7 |
i32.div_s(1, 0) |
const |
trap |
7 |
i32.div_u(1, 0) |
const |
trap |
7 |
i32.rem_s(1, 0) |
const |
trap |
7 |
i32.rem_u(1, 0) |
const |
trap |
7 |
i32.div_s(INT_MIN, -1) |
const |
trap |
7 |
Correctly preserved (control): dead-result i32.load at OOB address, dead-result i32.trunc_f32_s(nan), and an explicit (drop (i32.div_s 1 0)) all still trap. So the bug is specific to div/rem reached through a dead local.set in the default orchestration.
Mechanism
The removal happens in the default loom optimize orchestration (the ISLE term round-trip instructions_to_terms → simplify → terms_to_instructions, combined with eliminate_dead_locals at loom-core/src/lib.rs:11849 whose neutralize_dead_writes turns a dead LocalSet into Drop). Notably:
Expected
Per the WebAssembly spec, idiv_s/idiv_u/irem_s/irem_u trap when the divisor is 0, and idiv_s traps on INT_MIN / -1 overflow. These traps are observable side effects; an operation that may trap must not be removed just because its produced value is unused. Either treat integer div/rem as side-effecting for DCE/dead-local purposes, or keep the trap (e.g. lower a dead div to its trap-check guard only).
Summary
loom optimize(default pipeline) removes dead-result integer division/remainder operations, eliminating their mandatory divide-by-zero and INT_MIN/-1 overflow traps. When the result of ani32/i64div_s/div_u/rem_s/rem_uis unused, the whole operation is deleted as dead code — so a module that must trap at runtime instead returns normally.This is a trap-preservation / unsound-DCE bug. It is related to #273 but distinct and broader:
div_s(INT_MIN,-1)to a value (constant operands, overflow case only).div_u/rem_u/rem_stoo — which the constant folder'sr != 0guard never folds), and drops the divide-by-zero trap, not just overflow.Same underlying gap as #273: value-equivalence verification does not model traps, so removing a trapping-but-dead op is accepted.
Repro
repro.wat:Optimized function body is reduced to
i32.const 7— thediv_sis gone.loom --version= 1.1.18.--verifyreports✓ Verification: 37/37 tests passedon the miscompiled output.Scope (all confirmed, original traps / optimized returns 7)
i32.div_s(100, $d)$d=0i32.div_s(1, 0)i32.div_u(1, 0)i32.rem_s(1, 0)i32.rem_u(1, 0)i32.div_s(INT_MIN, -1)Correctly preserved (control): dead-result
i32.loadat OOB address, dead-resulti32.trunc_f32_s(nan), and an explicit(drop (i32.div_s 1 0))all still trap. So the bug is specific to div/rem reached through a deadlocal.setin the default orchestration.Mechanism
The removal happens in the default
loom optimizeorchestration (the ISLE term round-tripinstructions_to_terms→ simplify →terms_to_instructions, combined witheliminate_dead_localsatloom-core/src/lib.rs:11849whoseneutralize_dead_writesturns a deadLocalSetintoDrop). Notably:--passes <any list>, even iterated to a fixpoint (thediv_ssurvives every time) — it is a property of the default driver, not of a single named pass.eliminate_dead_locals'TranslationValidator::verify_or_revert(line 11886) does not revert, because value-equivalence does not model the trap. Same root gap as constant-folding eliminates the mandatory div_s(INT_MIN,-1) overflow trap — optimized module returns INT_MIN where the original traps (i32 + i64) #273.Expected
Per the WebAssembly spec,
idiv_s/idiv_u/irem_s/irem_utrap when the divisor is 0, andidiv_straps onINT_MIN / -1overflow. These traps are observable side effects; an operation that may trap must not be removed just because its produced value is unused. Either treat integer div/rem as side-effecting for DCE/dead-local purposes, or keep the trap (e.g. lower a dead div to its trap-check guard only).