Skip to content

Use effects for indirect call expressions#8625

Open
stevenfontanella wants to merge 9 commits into
mainfrom
expression-effects
Open

Use effects for indirect call expressions#8625
stevenfontanella wants to merge 9 commits into
mainfrom
expression-effects

Conversation

@stevenfontanella
Copy link
Copy Markdown
Member

@stevenfontanella stevenfontanella commented Apr 19, 2026

Part of #8615. After #8609, we compute effects for indirect call expressions, but only reflect this in the call-site via the effects of the Function that contains the indirect call. That let us reason about effects only one layer of indirection away, for example in the following module:

(func $a
  (call_ref $t (...))
)

(func $b
  (call $a)
)

If we know that an indirect call to $t can't possibly have any effects (e.g. its only potential target is a nop), we'd be able to optimize away (call $a) but not the (call_ref) itself, since the effects only got stored in the effects of $a.

This PR lets us reason about indirect call effects at the expression level within function bodies by adding a map from HeapType to effects typeEffects in wasm::Module. As a result we can completely optimize out the call_ref in the above example.

Drive-by fixes:

  • Set an unconditional trap effect on call_indirect when the call type doesn't match the target table.
  • Correctly set branchesOut for return_call on call.without.effects. Previously this would not have a branchesOut effect which may have allowed incorrect reorderings (we shouldn't move an effectful expression above a return_call but we would have allowed this). Will follow up in return_call with call.without.effects optimizes incorrectly #8693.

Comment thread src/ir/effects.h Outdated
@stevenfontanella
Copy link
Copy Markdown
Member Author

Seems like JJ + Github don't play well when I'm on a branch based on another branch that had a merge commit. Will fix this after merging the other branch.

stevenfontanella added a commit that referenced this pull request Apr 24, 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](https://github.com/WebAssembly/binaryen/blob/29b2d42e8a748fbe1095696d58a52b7bf83e2253/src/passes/GlobalEffects.cpp#L83-L87)
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 (#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.
Base automatically changed from indirect-effects-scc to main April 24, 2026 21:36
@stevenfontanella stevenfontanella force-pushed the expression-effects branch 3 times, most recently from 30a31e1 to 60665f3 Compare May 7, 2026 20:33
Gemini WIP

Try changing call effects
@stevenfontanella stevenfontanella marked this pull request as ready for review May 7, 2026 21:33
@stevenfontanella stevenfontanella requested a review from a team as a code owner May 7, 2026 21:33
@stevenfontanella stevenfontanella requested review from aheejin and removed request for a team May 7, 2026 21:33
Comment thread test/lit/passes/global-effects-closed-world-tnh.wast Outdated
Comment thread test/lit/passes/global-effects-closed-world.wast
Comment thread src/ir/effects.h Outdated
Comment thread src/ir/effects.h
Comment thread src/ir/effects.h Outdated
@stevenfontanella stevenfontanella requested a review from aheejin May 12, 2026 20:45
Comment thread src/ir/effects.h Outdated
Comment thread src/ir/effects.h
Comment thread src/wasm.h
Comment thread src/ir/effects.h Outdated
Comment thread src/ir/effects.h
Comment thread src/ir/effects.h
Comment thread src/support/utilities.h Outdated
Comment thread src/ir/effects.h
Comment thread src/ir/effects.h Outdated
Comment thread src/ir/effects.h Outdated
Comment thread src/wasm.h
Comment on lines +2730 to +2734
// When types are rewritten globally, the target type inherits the effects of
// source type (see type-updating.cpp). If the type of just one function is
// rewritten, we don't update this, because such a rewrite is only valid
// if the function is not the target of an indirect call (otherwise the
// indirect call would have to be rewritten too).
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 paragraph can maybe move to type-updating.cpp?

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.

Alon suggested documenting it here: #8625 (comment)

There is a similar comment in type-updating.cpp:

  // Update indirect call effects per type.
  // When A is rewritten to B, B inherits the effects of A and A loses its
  // effects.

The part about individual functions being rewritten isn't relevant there since type-updating.cpp is for global type updates. Let me know if I should add anything else to make it more clear.

Comment thread src/ir/effects.h
Comment on lines 1311 to 1326
@@ -1320,16 +1325,18 @@ class EffectAnalyzer {
}
}
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.

Can't we just blindly merge funcEffects here and deal with exception and tryDepth and isReturn stuff at the end of addCallEffects once and for all? (In this case we may not need addCallEffectsFromGlobalEffects as a separate function after all)

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.

That's how the code was written before but I found it hard to follow with the two different cases interleaved. I prefer to expand it into the two cases where we have and don't have global effects. If you want to compare:

binaryen/src/ir/effects.h

Lines 718 to 771 in 2f1f55a

void visitCall(Call* curr) {
// call.without.effects has no effects.
if (Intrinsics(parent.module).isCallWithoutEffects(curr)) {
return;
}
// Get the target's effects, if they exist. Note that we must handle the
// case of the function not yet existing (we may be executed in the middle
// of a pass, which may have built up calls but not the targets of those
// calls; in such a case, we do not find the targets and therefore assume
// we know nothing about the effects, which is safe).
const EffectAnalyzer* targetEffects = nullptr;
if (auto* target = parent.module.getFunctionOrNull(curr->target)) {
targetEffects = target->effects.get();
}
if (curr->isReturn) {
parent.branchesOut = true;
// When EH is enabled, any call can throw.
if (parent.features.hasExceptionHandling() &&
(!targetEffects || targetEffects->throws())) {
parent.hasReturnCallThrow = true;
}
}
if (targetEffects) {
// We have effect information for this call target, and can just use
// that. The one change we may want to make is to remove throws_, if the
// target function throws and we know that will be caught anyhow, the
// same as the code below for the general path. We can always filter out
// throws for return calls because they are already more precisely
// captured by `branchesOut`, which models the return, and
// `hasReturnCallThrow`, which models the throw that will happen after
// the return.
if (targetEffects->throws_ && (parent.tryDepth > 0 || curr->isReturn)) {
auto filteredEffects = *targetEffects;
filteredEffects.throws_ = false;
parent.mergeIn(filteredEffects);
} else {
// Just merge in all the effects.
parent.mergeIn(*targetEffects);
}
return;
}
parent.calls = true;
// When EH is enabled, any call can throw. Skip this for return calls
// because the throw is already more precisely captured by the combination
// of `hasReturnCallThrow` and `branchesOut`.
if (parent.features.hasExceptionHandling() && parent.tryDepth == 0 &&
!curr->isReturn) {
parent.throws_ = true;
}
}

Comment thread src/ir/effects.h Outdated
Comment thread src/ir/effects.h
@stevenfontanella
Copy link
Copy Markdown
Member Author

Will run the fuzzer for a few hours.

@stevenfontanella
Copy link
Copy Markdown
Member Author

Ran 3900 iterations with no issues.

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.

4 participants