Skip to content

Compute effects for indirect calls in GlobalEffects#8609

Open
stevenfontanella wants to merge 23 commits intomainfrom
indirect-effects-scc
Open

Compute effects for indirect calls in GlobalEffects#8609
stevenfontanella wants to merge 23 commits intomainfrom
indirect-effects-scc

Conversation

@stevenfontanella
Copy link
Copy Markdown
Member

@stevenfontanella stevenfontanella commented Apr 15, 2026

When running in --closed-world, compute effects for indirect calls by unioning the effects of all potential functions of that type. In --closed-world, we assume that all references originate in our module, so the only possible functions that we don't know about are imports. Previously we gave up on effects analysis for indirect calls.

Yields a very small byte count reduction in calcworker (3799354 - 3799297 = 57 bytes). Also shows no significant difference in Binaryen runtime: (0.1346069 -> 0.13375045 = <1% improvement, probably within noise). We expect more benefits after we're able to share indirect call effects with other passes, since currently they're only seen one layer up for callers of functions that indirectly call functions (see the newly-added tests for examples).

Followups:

  • Share effect information per type with other passes besides just via Function::effects (Use effects for indirect call expressions #8625)
  • Exclude functions that don't have an address (i.e. functions that aren't the target of ref.func) from effect analysis ()
  • Compute effects more precisely for exact + nullable/non-nullable references

Part of #8615.

@stevenfontanella stevenfontanella changed the base branch from main to effects-scc April 16, 2026 16:28
@stevenfontanella stevenfontanella force-pushed the indirect-effects-scc branch 8 times, most recently from b7ce0b6 to 9fcf76b Compare April 16, 2026 20:59
@stevenfontanella stevenfontanella changed the title Indirect effects scc Compute effects for indirect calls in GlobalEffects Apr 16, 2026
@stevenfontanella stevenfontanella marked this pull request as ready for review April 16, 2026 21:43
@stevenfontanella stevenfontanella requested a review from a team as a code owner April 16, 2026 21:43
@stevenfontanella stevenfontanella requested review from kripken and removed request for a team April 16, 2026 21:43
Base automatically changed from effects-scc to main April 16, 2026 22:36
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread test/lit/passes/global-effects-closed-world.wast Outdated
Comment thread test/lit/passes/global-effects-closed-world.wast
;; There's no function with this type, so it's impossible to create a ref to
;; call this function with and there are no effects to aggregate.
;; Remove this call.
(call $calls-uninhabited (local.get $ref))
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.

This is replacing a trap with a nop, which is generally something we don't want to do, see #8387 - can we avoid this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This can't trap since the refs are non-nullable. Added a test case in the first module showing that a nullable ref won't be optimized out even if we determine that its potential function targets don't have effects. This is handled in the initial effects calculation, we only clear the calls and throws effects and replace them with the effects of function bodies, but the trap effect stays.

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.

Sounds good. Please also add a testcase with a nullable uninstantiated type - that one must trap, so (1) we should not remove it, and (2) we can add a TODO to turn it into an unreachable if we don't already (though perhaps other passes do that, I'm not sure offhand).

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.

(Specifically, I mean to add a function right after this one, with (ref null $uninhabited))

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.

It would still be good to add the TODO suggested above. Also, even in the case with non-nullable references, it's generally better to replace code that cannot possibly be executed with unreachable rather than nop because that lets us remove more code as a follow-on. So it would be good to have a TODO about that as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I put the TODO above on $calls-nullable-uninhabited, but on second thought that might be the wrong place for it. With unreachable, is there a distinction between "impossible" and "always traps"? Does the value of --traps-never-happen affect that? $calls-uninhabited is the latter case while $calls-nullable-uninhabited is the former case.

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.

Without --traps-never-happen, there's no good way to encode "impossible" in the type system1, but we can still remove all the code that comes after an unreachable. With --traps-never-happen, we can additionally remove all the code that comes before the unreachable.

Footnotes

  1. We could in principle use an uninhabitable type like (ref none) to represent impossibility, but we don't use it in that way today. Most toolchains do use --traps-never-happen, so it doesn't matter much in practice.

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.

So both $calls-nullable-uninhabited and $calls-uninhabited should have TODOs for optimizing to unreachable, but for different reasons. The former because we know it must trap, and the latter because we know it is impossible to reach.

Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp Outdated
Comment thread src/passes/GlobalEffects.cpp
Comment thread test/lit/passes/global-effects-closed-world.wast
Comment thread out.txt Outdated
Comment thread test/lit/passes/global-effects-closed-world.wast Outdated
Comment thread test/lit/passes/global-effects-closed-world.wast
Comment thread src/passes/GlobalEffects.cpp Outdated
@stevenfontanella
Copy link
Copy Markdown
Member Author

Also added some tests exercising call_indirect.

We don't need to walk up the supertype chain for type targets of
indirect calls, we already do that for function implementations which
would reach the type of the indirect call anyway
@stevenfontanella
Copy link
Copy Markdown
Member Author

Added a test showing two different effect types being aggregated into the same function (global-effects-closed-world-simplify-locals.wast).

@stevenfontanella
Copy link
Copy Markdown
Member Author

Added a test showing two different effect types being aggregated into the same function (global-effects-closed-world-simplify-locals.wast).

The test went wrong at some point, let me update.

@stevenfontanella
Copy link
Copy Markdown
Member Author

Added a test showing two different effect types being aggregated into the same function (global-effects-closed-world-simplify-locals.wast).

The test went wrong at some point, let me update.

It turns out that the effects were pessimized when changing the features from --enable-gc --enable-reference-types to -all. Changed the features of the test back and looking into why this is now.

Comment thread src/passes/GlobalEffects.cpp
Comment thread src/support/graph_traversal.h Outdated
@stevenfontanella
Copy link
Copy Markdown
Member Author

Added a test showing two different effect types being aggregated into the same function (global-effects-closed-world-simplify-locals.wast).

The test went wrong at some point, let me update.

It turns out that the effects were pessimized when changing the features from --enable-gc --enable-reference-types to -all. Changed the features of the test back and looking into why this is now.

Looks like this was due to LinearExecutionWalker (used in SimplifyLocals) not taking global effects into account and unconditionally assuming that functions may throw even if we determined that they won't. I tested with #8637 and found that the test works correctly with -all / --enable-exception-handling. Will land that PR after this one.

@stevenfontanella
Copy link
Copy Markdown
Member Author

stevenfontanella commented Apr 23, 2026

(Messed up merging in JJ, let me try again)

@stevenfontanella
Copy link
Copy Markdown
Member Author

(Messed up merging in JJ, let me try again)

Done. Merged in main with #8637 and changed the test to run with -all to ensure it's working as intended with all features in case something else changes.

Copy link
Copy Markdown
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

LGTM % final comments.

} else if (auto* callIndirect = curr->dynCast<CallIndirect>()) {
type = callIndirect->heapType;
} else {
WASM_UNREACHABLE("Unexpected call type");
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.

I think we model a few of the stack switching instructions as having the calls effect, so we might want to handle this more gracefully. Does this pass the fuzzer as-is?

Comment thread src/passes/GlobalEffects.cpp
Comment thread src/passes/GlobalEffects.cpp
;; There's no function with this type, so it's impossible to create a ref to
;; call this function with and there are no effects to aggregate.
;; Remove this call.
(call $calls-uninhabited (local.get $ref))
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.

It would still be good to add the TODO suggested above. Also, even in the case with non-nullable references, it's generally better to replace code that cannot possibly be executed with unreachable rather than nop because that lets us remove more code as a follow-on. So it would be good to have a TODO about that as well.

Comment on lines +281 to +284
;; Same as above but this time our reference is the exact supertype
;; so we know not to aggregate effects from the subtype.
;; TODO: this case doesn't optimize today. Add exact ref support in the pass.
(module
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.

This could be tested in the previous module to avoid some duplication of the setup.

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.

3 participants