Skip to content

[DAE] Refine the results of never-returning functions to bottom - #9169

Open
vouillon wants to merge 1 commit into
WebAssembly:mainfrom
vouillon:dea-refine
Open

vouillon wants to merge 1 commit into
WebAssembly:mainfrom
vouillon:dea-refine

Conversation

@vouillon

Copy link
Copy Markdown
Contributor

When no value can flow out of a function, DAE leaves the result type alone. Such functions are common in compiled code (e.g. functions that just throw an exception).

For example, with --dae:

  (func $fail (param $0 (ref eq)) (result (ref eq))
    (throw $exn (local.get $0)))
  (func $f (param $0 i32) (result (ref eq))
    (if (local.get $0)
      (then (return (ref.i31 (i32.const 0)))))
    (return_call $fail (global.get $msg)))

$f stayed at (ref eq) although it can only return an i31.

When the result is a single reference type and no value is noted, we refine it to the non-nullable bottom type of its hierarchy (e.g. (ref none)), under the same conditions as other result refinements (all calls are known). $fail now returns (ref none) and $f (ref i31).

@vouillon
vouillon requested a review from a team as a code owner September 28, 2026 21:17
@vouillon
vouillon requested review from tlively and removed request for a team September 28, 2026 21:17
When no value can flow out of a function (its body is unreachable, it
has no return with a value, and it does not tail-call a function with a
possible result), `LUB::getResultsLUB` notes nothing and DAE left the
result type alone. Such functions are common in compiled code (e.g.
wrappers that just throw an exception), and since `getResultsLUB` notes
the declared result type of `return_call` targets, every function that
tail-calls them also kept its imprecise result type.

For example, with `--dae`:

  (func $fail (param $0 (ref eq)) (result (ref eq))
    (throw $exn (local.get $0)))
  (func $f (param $0 i32) (result (ref eq))
    (if (local.get $0)
      (then (return (ref.i31 (i32.const 0)))))
    (return_call $fail (global.get $msg)))

`$f` stayed at `(ref eq)` although it can only return an i31.

When the result is a single reference type and no value is noted,
refine it to the non-nullable bottom type of its hierarchy (e.g. `(ref
none)`), under the same conditions as other result refinements (all
calls are known). `$fail` now returns `(ref none)` and `$f` `(ref i31)`
(chains of tail calls are refined over the DAE iterations). The callers
see an uninhabitable call result, so with dae-optimizing the code after
the call becomes unreachable, and a call whose result is dropped gets
an `unreachable` after it when the result is later removed.

Multivalue results are left alone.

Some existing tests relied on never-returning helper functions keeping
their declared result types; export those helpers to preserve what the
tests check.

@tlively tlively left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Can you run the fuzzer for a few thousand iterations to see if it finds any issues? (scripts/fuzz_opt.py or scripts/monitor_fuzz.py -j --max-iters=5000)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants