-
Notifications
You must be signed in to change notification settings - Fork 851
Compute effects for indirect calls in GlobalEffects #8609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stevenfontanella
wants to merge
23
commits into
main
Choose a base branch
from
indirect-effects-scc
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
3a25c2e
Cherrypick SCC with indirect effects
stevenfontanella d764f59
PR updates
stevenfontanella ff72d4f
PR updates
stevenfontanella 20fd3cd
Remove dead code
stevenfontanella 6bf6f2a
Add explicit deduction guide to resolve CI error
stevenfontanella 8a0e725
PR updates
stevenfontanella ac59711
Add test for unreachable ref
stevenfontanella 4a440f9
Add comment
stevenfontanella 00407cf
PR updates
stevenfontanella 3192366
PR updates
stevenfontanella 512dad6
PR updates
stevenfontanella 0890c8f
Use type alias
stevenfontanella 4994a9d
Small fix to graph building
stevenfontanella 7e09feb
Optimize logic for walking up supertype heirarchy
stevenfontanella f5d9926
Add more tests demonstrating different effects beings aggregated
stevenfontanella 3567c0e
Try reformatting
stevenfontanella ac58ee6
Reformat again
stevenfontanella 68971e4
Lift out lambda
stevenfontanella ffb73c5
Fix test
stevenfontanella 48a9cab
Add unit test for DFS
stevenfontanella 2630eff
Fix test comment
stevenfontanella 39d2d31
Merge main
stevenfontanella 259b234
Use -all in test
stevenfontanella File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,9 +19,12 @@ | |
| // PassOptions structure; see more details there. | ||
| // | ||
|
|
||
| #include <ranges> | ||
|
|
||
| #include "ir/effects.h" | ||
| #include "ir/module-utils.h" | ||
| #include "pass.h" | ||
| #include "support/graph_traversal.h" | ||
| #include "support/strongly_connected_components.h" | ||
| #include "wasm.h" | ||
|
|
||
|
|
@@ -39,6 +42,9 @@ struct FuncInfo { | |
|
|
||
| // Directly-called functions from this function. | ||
| std::unordered_set<Name> calledFunctions; | ||
|
|
||
| // Types that are targets of indirect calls. | ||
| std::unordered_set<HeapType> indirectCalledTypes; | ||
| }; | ||
|
|
||
| std::map<Function*, FuncInfo> analyzeFuncs(Module& module, | ||
|
|
@@ -83,11 +89,21 @@ std::map<Function*, FuncInfo> analyzeFuncs(Module& module, | |
| if (auto* call = curr->dynCast<Call>()) { | ||
| // Note the direct call. | ||
| funcInfo.calledFunctions.insert(call->target); | ||
| } else if (effects.calls && options.closedWorld) { | ||
| HeapType type; | ||
| if (auto* callRef = curr->dynCast<CallRef>()) { | ||
| // call_ref on unreachable does not have a call effect, | ||
| // so this must be a HeapType. | ||
| type = callRef->target->type.getHeapType(); | ||
| } else if (auto* callIndirect = curr->dynCast<CallIndirect>()) { | ||
| type = callIndirect->heapType; | ||
| } else { | ||
| WASM_UNREACHABLE("Unexpected call type"); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
|
|
||
| funcInfo.indirectCalledTypes.insert(type); | ||
| } else if (effects.calls) { | ||
| // This is an indirect call of some sort, so we must assume the | ||
| // worst. To do so, clear the effects, which indicates nothing | ||
| // is known (so anything is possible). | ||
| // TODO: We could group effects by function type etc. | ||
| assert(!options.closedWorld); | ||
| funcInfo.effects = UnknownEffects; | ||
| } else { | ||
| // No call here, but update throwing if we see it. (Only do so, | ||
|
|
@@ -107,22 +123,86 @@ std::map<Function*, FuncInfo> analyzeFuncs(Module& module, | |
| return std::move(analysis.map); | ||
| } | ||
|
|
||
| using CallGraph = std::unordered_map<Function*, std::unordered_set<Function*>>; | ||
| using CallGraphNode = std::variant<Function*, HeapType>; | ||
|
stevenfontanella marked this conversation as resolved.
|
||
|
|
||
| /* | ||
| Call graph for indirect and direct calls. | ||
|
|
||
| key (caller) -> value (callee) | ||
| Function -> Function : direct call | ||
| Function -> HeapType : indirect call to the given HeapType | ||
| HeapType -> Function : The function `callee` has the type `caller`. The | ||
| HeapType may essentially 'call' any of its | ||
| potential implementations. | ||
| HeapType -> HeapType : `callee` is a subtype of `caller`. A call_ref | ||
| could target any subtype of the ref, so we need to | ||
| aggregate effects of subtypes of the target type. | ||
|
|
||
| If we're running in an open world, we only include Function -> Function edges, | ||
| and don't compute effects for indirect calls, conservatively assuming the | ||
| worst. | ||
| */ | ||
|
stevenfontanella marked this conversation as resolved.
|
||
| using CallGraph = | ||
| std::unordered_map<CallGraphNode, std::unordered_set<CallGraphNode>>; | ||
|
|
||
| CallGraph buildCallGraph(const Module& module, | ||
| const std::map<Function*, FuncInfo>& funcInfos) { | ||
| const std::map<Function*, FuncInfo>& funcInfos, | ||
| bool closedWorld) { | ||
| CallGraph callGraph; | ||
| for (const auto& [func, info] : funcInfos) { | ||
| if (info.calledFunctions.empty()) { | ||
| continue; | ||
| if (!closedWorld) { | ||
| for (const auto& [caller, callerInfo] : funcInfos) { | ||
|
stevenfontanella marked this conversation as resolved.
|
||
| auto& callees = callGraph[caller]; | ||
|
|
||
| // Function -> Function | ||
| for (Name calleeFunction : callerInfo.calledFunctions) { | ||
| callees.insert(module.getFunction(calleeFunction)); | ||
| } | ||
| } | ||
|
|
||
| auto& callees = callGraph[func]; | ||
| for (Name callee : info.calledFunctions) { | ||
| callees.insert(module.getFunction(callee)); | ||
| return callGraph; | ||
| } | ||
|
|
||
| std::unordered_set<HeapType> allFunctionTypes; | ||
|
stevenfontanella marked this conversation as resolved.
|
||
| for (const auto& [caller, callerInfo] : funcInfos) { | ||
| auto& callees = callGraph[caller]; | ||
|
|
||
| // Function -> Function | ||
| for (Name calleeFunction : callerInfo.calledFunctions) { | ||
| callees.insert(module.getFunction(calleeFunction)); | ||
| } | ||
|
|
||
| // Function -> Type | ||
| allFunctionTypes.insert(caller->type.getHeapType()); | ||
| for (HeapType calleeType : callerInfo.indirectCalledTypes) { | ||
| callees.insert(calleeType); | ||
|
|
||
| // Add the key to ensure the lookup doesn't fail for indirect calls to | ||
| // uninhabited types. | ||
| callGraph[calleeType]; | ||
| } | ||
|
|
||
| // Type -> Function | ||
| callGraph[caller->type.getHeapType()].insert(caller); | ||
| } | ||
|
|
||
| // Type -> Type | ||
| // Do a DFS up the type heirarchy for all function implementations. | ||
| // We are essentially walking up each supertype chain and adding edges from | ||
| // super -> subtype, but doing it via DFS to avoid repeated work. | ||
| Graph superTypeGraph(allFunctionTypes.begin(), | ||
| allFunctionTypes.end(), | ||
| [&callGraph](auto&& push, HeapType t) { | ||
| // Not needed except that during lookup we expect the | ||
| // key to exist. | ||
| callGraph[t]; | ||
|
|
||
| if (auto super = t.getDeclaredSuperType()) { | ||
| callGraph[*super].insert(t); | ||
| push(*super); | ||
| } | ||
| }); | ||
| (void)superTypeGraph.traverseDepthFirst(); | ||
|
|
||
| return callGraph; | ||
| } | ||
|
|
||
|
|
@@ -152,63 +232,60 @@ void propagateEffects(const Module& module, | |
| const PassOptions& passOptions, | ||
| std::map<Function*, FuncInfo>& funcInfos, | ||
| const CallGraph& callGraph) { | ||
| // We only care about Functions that are roots, not types. | ||
| // A type would be a root if a function exists with that type, but no-one | ||
| // indirect calls the type. | ||
| auto funcNodes = std::views::keys(callGraph) | | ||
| std::views::filter([](auto node) { | ||
| return std::holds_alternative<Function*>(node); | ||
| }) | | ||
| std::views::common; | ||
|
tlively marked this conversation as resolved.
|
||
| using funcNodesType = decltype(funcNodes); | ||
|
|
||
| struct CallGraphSCCs | ||
| : SCCs<std::vector<Function*>::const_iterator, CallGraphSCCs> { | ||
| : SCCs<std::ranges::iterator_t<funcNodesType>, CallGraphSCCs> { | ||
|
|
||
| const std::map<Function*, FuncInfo>& funcInfos; | ||
| const std::unordered_map<Function*, std::unordered_set<Function*>>& | ||
| callGraph; | ||
| const CallGraph& callGraph; | ||
| const Module& module; | ||
|
|
||
| CallGraphSCCs( | ||
| const std::vector<Function*>& funcs, | ||
| const std::map<Function*, FuncInfo>& funcInfos, | ||
| const std::unordered_map<Function*, std::unordered_set<Function*>>& | ||
| callGraph, | ||
| const Module& module) | ||
| : SCCs<std::vector<Function*>::const_iterator, CallGraphSCCs>( | ||
| funcs.begin(), funcs.end()), | ||
| CallGraphSCCs(funcNodesType&& nodes, | ||
| const std::map<Function*, FuncInfo>& funcInfos, | ||
| const CallGraph& callGraph, | ||
| const Module& module) | ||
| : SCCs<std::ranges::iterator_t<funcNodesType>, CallGraphSCCs>( | ||
| std::ranges::begin(nodes), std::ranges::end(nodes)), | ||
| funcInfos(funcInfos), callGraph(callGraph), module(module) {} | ||
|
|
||
| void pushChildren(Function* f) { | ||
| auto callees = callGraph.find(f); | ||
| if (callees == callGraph.end()) { | ||
| return; | ||
| } | ||
|
|
||
| for (auto* callee : callees->second) { | ||
| void pushChildren(CallGraphNode node) { | ||
| for (CallGraphNode callee : callGraph.at(node)) { | ||
| push(callee); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| std::vector<Function*> allFuncs; | ||
| for (auto& [func, info] : funcInfos) { | ||
| allFuncs.push_back(func); | ||
| } | ||
| CallGraphSCCs sccs(allFuncs, funcInfos, callGraph, module); | ||
| CallGraphSCCs sccs(std::move(funcNodes), funcInfos, callGraph, module); | ||
|
|
||
| std::vector<std::optional<EffectAnalyzer>> componentEffects; | ||
| // Points to an index in componentEffects | ||
| std::unordered_map<Function*, Index> funcComponents; | ||
| std::unordered_map<CallGraphNode, Index> nodeComponents; | ||
|
|
||
| for (auto ccIterator : sccs) { | ||
| std::optional<EffectAnalyzer>& ccEffects = | ||
| componentEffects.emplace_back(std::in_place, passOptions, module); | ||
| std::vector<CallGraphNode> cc(ccIterator.begin(), ccIterator.end()); | ||
|
|
||
| std::vector<Function*> ccFuncs(ccIterator.begin(), ccIterator.end()); | ||
|
|
||
| for (Function* f : ccFuncs) { | ||
| funcComponents.emplace(f, componentEffects.size() - 1); | ||
| std::vector<Function*> ccFuncs; | ||
| for (CallGraphNode node : cc) { | ||
| nodeComponents.emplace(node, componentEffects.size() - 1); | ||
| if (auto** func = std::get_if<Function*>(&node)) { | ||
| ccFuncs.push_back(*func); | ||
| } | ||
| } | ||
|
|
||
| std::unordered_set<int> calleeSccs; | ||
| for (Function* caller : ccFuncs) { | ||
| auto callees = callGraph.find(caller); | ||
| if (callees == callGraph.end()) { | ||
| continue; | ||
| } | ||
| for (auto* callee : callees->second) { | ||
| calleeSccs.insert(funcComponents.at(callee)); | ||
| for (CallGraphNode caller : cc) { | ||
| for (CallGraphNode callee : callGraph.at(caller)) { | ||
| calleeSccs.insert(nodeComponents.at(callee)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -219,11 +296,13 @@ void propagateEffects(const Module& module, | |
| } | ||
|
|
||
| // Add trap effects for potential cycles. | ||
| if (ccFuncs.size() > 1) { | ||
| if (cc.size() > 1) { | ||
| if (ccEffects != UnknownEffects) { | ||
| ccEffects->trap = true; | ||
| } | ||
| } else { | ||
| } else if (ccFuncs.size() == 1) { | ||
|
stevenfontanella marked this conversation as resolved.
|
||
| // It's possible for a CC to only contain 1 type, but that is not a | ||
| // cycle in the call graph. | ||
| auto* func = ccFuncs[0]; | ||
| if (funcInfos.at(func).calledFunctions.contains(func->name)) { | ||
| if (ccEffects != UnknownEffects) { | ||
|
|
@@ -267,7 +346,8 @@ struct GenerateGlobalEffects : public Pass { | |
| std::map<Function*, FuncInfo> funcInfos = | ||
| analyzeFuncs(*module, getPassOptions()); | ||
|
|
||
| auto callGraph = buildCallGraph(*module, funcInfos); | ||
| auto callGraph = | ||
| buildCallGraph(*module, funcInfos, getPassOptions().closedWorld); | ||
|
|
||
| propagateEffects(*module, getPassOptions(), funcInfos, callGraph); | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * Copyright 2026 WebAssembly Community Group participants | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| #include <concepts> | ||
| #include <functional> | ||
| #include <iterator> | ||
| #include <unordered_set> | ||
|
|
||
| namespace wasm { | ||
|
|
||
| // SuccessorFunction should be an invocable that takes a 'push' function (which | ||
| // is an invocable that takes a `const T&`), and a `const T&`. i.e. | ||
| // SuccessorFunction should call `push` for each neighbor of the T that it's | ||
| // called with. | ||
| // TODO: We don't have a good way to write this with concepts today. | ||
| // Something like this should do it, but we hit an ICE on dwarf symbols in debug | ||
| // builds: requires requires(const SuccessorFunction& successors, const T& t) { | ||
| // successors([](const T&) { }, t); } | ||
| template<typename T, typename SuccessorFunction> class Graph { | ||
| public: | ||
| template<std::input_iterator It, std::sentinel_for<It> Sen> | ||
| requires std::convertible_to<std::iter_reference_t<It>, T> | ||
| Graph(It rootsBegin, Sen rootsEnd, SuccessorFunction successors) | ||
| : roots(rootsBegin, rootsEnd), successors(std::move(successors)) {} | ||
|
|
||
| // Traverse the graph depth-first, calling `successors` exactly once for each | ||
| // node (unless the node appears multiple times in `roots`). Return the set of | ||
| // nodes visited. | ||
| std::unordered_set<T> traverseDepthFirst() const { | ||
| std::vector<T> stack(roots.begin(), roots.end()); | ||
| std::unordered_set<T> visited(roots.begin(), roots.end()); | ||
|
|
||
| auto maybePush = [&](const T& t) { | ||
| auto [_, inserted] = visited.insert(t); | ||
| if (inserted) { | ||
| stack.push_back(t); | ||
| } | ||
| }; | ||
|
|
||
| while (!stack.empty()) { | ||
| auto curr = std::move(stack.back()); | ||
| stack.pop_back(); | ||
|
|
||
| successors(maybePush, curr); | ||
| } | ||
|
|
||
| return visited; | ||
| } | ||
|
|
||
| private: | ||
| std::vector<T> roots; | ||
| SuccessorFunction successors; | ||
| }; | ||
|
|
||
| template<std::input_iterator It, | ||
| std::sentinel_for<It> Sen, | ||
| typename SuccessorFunction> | ||
| Graph(It, Sen, SuccessorFunction) | ||
| -> Graph<std::iter_value_t<It>, std::decay_t<SuccessorFunction>>; | ||
|
|
||
| } // namespace wasm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.