Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions src/passes/GlobalEffects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//

#include "ir/effects.h"
#include "ir/element-utils.h"
#include "ir/module-utils.h"
#include "pass.h"
#include "support/graph_traversal.h"
Expand All @@ -44,6 +45,57 @@ struct FuncInfo {
std::unordered_set<HeapType> indirectCalledTypes;
};

/*
Comment thread
stevenfontanella marked this conversation as resolved.
Outdated
Only funcs that are 'addressed' may be the target of an indirect call. A
function is addressed if:
- It appears in a ref.func expression
- It appears in an `elem` segment (note that we already ignore `elem declare`
statements in our IR, but we check separately for funcs that appear in
`ref.func`).
- It's exported, because it may flow back to us as a reference.
- It's imported, which implies it is `elem declare`d.
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.

Maybe it's only me but 'it is elem declared' sounds a little confusing (because it is not, even though I get what you mean). How about just saying imported functions can be passed a reference (as you did with the exported functions)?

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.

Rephrased the comment


If a function doesn't meet any of these criteria, it can't be the target of
an indirect call and we don't need to include its effects in indirect calls.
*/
std::unordered_set<Function*> getAddressedFuncs(Module& module) {
struct AddressedFuncsWalker : WalkerPass<PostWalker<AddressedFuncsWalker>> {
std::unordered_set<Function*>& addressedFuncs;

AddressedFuncsWalker(std::unordered_set<Function*>& addressedFuncs)
: addressedFuncs(addressedFuncs) {}

void visitRefFunc(RefFunc* refFunc) {
addressedFuncs.insert(getModule()->getFunction(refFunc->func));
}
};

std::unordered_set<Function*> addressedFuncs;
AddressedFuncsWalker walker(addressedFuncs);
walker.walkModuleCode(&module);
walker.walkModule(&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 isn't going to traverse the module in parallel. You need to call .run() on the pass or explicitly use a PassRunner if you want it to run in parallel. However, having all the threads updating the single addressedFuncs set in parallel would not be safe. Safe options include collecting a separate set for each function in parallel (possibly using ParallelFunctionAnalysis) and then combining them, or using a std::unordered_map<Name, std::atomic<bool>> that is pre-filled with an entry for each function so that the threads do not race to grow the map.

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.

Done. Included it in the existing pass to avoid having to create a ParallelFunctionAnalysis with the same logic.


ModuleUtils::iterImportedFunctions(
module, [&addressedFuncs, &module](Function* import) {
addressedFuncs.insert(module.getFunction(import->name));
});

for (const auto& export_ : module.exports) {
if (export_->kind != ExternalKind::Function) {
continue;
}

addressedFuncs.insert(module.getFunction(*export_->getInternalName()));
}

ElementUtils::iterAllElementFunctionNames(
&module, [&addressedFuncs, &module](Name func) {
addressedFuncs.insert(module.getFunction(func));
});
Comment thread
stevenfontanella marked this conversation as resolved.
Outdated

return addressedFuncs;
}

std::map<Function*, FuncInfo> analyzeFuncs(Module& module,
const PassOptions& passOptions) {
ModuleUtils::ParallelFunctionAnalysis<FuncInfo> analysis(
Expand Down Expand Up @@ -144,6 +196,7 @@ using CallGraph =

CallGraph buildCallGraph(const Module& module,
const std::map<Function*, FuncInfo>& funcInfos,
const std::unordered_set<Function*>& addressedFuncs,
WorldMode worldMode) {
CallGraph callGraph;
if (worldMode == WorldMode::Open) {
Expand Down Expand Up @@ -179,16 +232,18 @@ CallGraph buildCallGraph(const Module& module,
}

// Type -> Function
callGraph[caller->type.getHeapType()].insert(caller);
if (addressedFuncs.contains(caller)) {
callGraph[caller->type.getHeapType()].insert(caller);
}
}

// Type -> Type
// Do a DFS up the type heirarchy for all function implementations.
// Do a DFS up the type hierarchy 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) {
[&callGraph](const auto& push, HeapType t) {
// Not needed except that during lookup we expect the
// key to exist.
callGraph[t];
Expand Down Expand Up @@ -350,8 +405,10 @@ struct GenerateGlobalEffects : public Pass {
std::map<Function*, FuncInfo> funcInfos =
analyzeFuncs(*module, getPassOptions());

auto callGraph =
buildCallGraph(*module, funcInfos, getPassOptions().worldMode);
auto addressedFuncs = getAddressedFuncs(*module);

auto callGraph = buildCallGraph(
*module, funcInfos, addressedFuncs, getPassOptions().worldMode);

propagateEffects(*module,
getPassOptions(),
Expand Down
246 changes: 229 additions & 17 deletions test/lit/passes/global-effects-closed-world-simplify-locals.wast
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
;; Tests for aggregating effects from indirect calls in GlobalEffects when
;; --closed-world is true. Continued from global-effects-closed-world.wast.

;; Test that effects are aggregated from both $indirect-type-super and
;; $indirect-type-sub for indirect calls to $indirect-type-super.
(module
;; CHECK: (type $indirect-type-super (sub (func (param i32))))
(type $indirect-type-super (sub (func (param i32))))

;; CHECK: (type $1 (func (param (ref $indirect-type-super))))

;; CHECK: (type $indirect-type-sub (sub $indirect-type-super (func (param i32))))
(type $indirect-type-sub (sub $indirect-type-super (func (param i32))))

;; CHECK: (type $2 (func (param (ref $indirect-type-super))))

;; CHECK: (type $3 (func (param (ref $indirect-type-sub))))

;; CHECK: (global $g1 (mut i32) (i32.const 0))
(global $g1 (mut i32) (i32.const 0))
;; CHECK: (global $g2 (mut i32) (i32.const 0))
Expand Down Expand Up @@ -42,18 +46,7 @@
(global.set $g2 (local.get $i32))
)

;; CHECK: (func $caller (type $1) (param $ref (ref $indirect-type-super))
;; CHECK-NEXT: (call_ref $indirect-type-super
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $caller (param $ref (ref $indirect-type-super))
;; This inherits effects from $impl1 and $impl2, so may mutate $g1 and $g2.
(call_ref $indirect-type-super (i32.const 1) (local.get $ref))
)

;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type-super))
;; CHECK: (func $merges-effects-from-super-and-sub (type $2) (param $ref (ref $indirect-type-super))
;; CHECK-NEXT: (local $x i32)
;; CHECK-NEXT: (local $y i32)
;; CHECK-NEXT: (local $z i32)
Expand All @@ -64,7 +57,8 @@
;; CHECK-NEXT: (global.get $g2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: (call $caller
;; CHECK-NEXT: (call_ref $indirect-type-super
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
Expand All @@ -77,7 +71,7 @@
;; CHECK-NEXT: (global.get $g3)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $merges-multiple-effects (param $ref (ref $indirect-type-super))
(func $merges-effects-from-super-and-sub (param $ref (ref $indirect-type-super))
(local $x i32)
(local $y i32)
(local $z i32)
Expand All @@ -89,10 +83,228 @@
;; This acts as a barrier for $x and $y, but not $z because
;; $ref may write to $g1 (via $impl1) or $g2 (via $impl2) but not $g3.
;; $z is optimized out and $x and $y are left alone.
(call $caller (local.get $ref))
(call_ref $indirect-type-super (i32.const 1) (local.get $ref))

(drop (local.get $x))
(drop (local.get $y))
(drop (local.get $z))
)
;; CHECK: (func $merges-effects-from-sub-only (type $3) (param $ref (ref $indirect-type-sub))
;; CHECK-NEXT: (local $x i32)
;; CHECK-NEXT: (local $y i32)
;; CHECK-NEXT: (local $z i32)
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (global.get $g2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: (call_ref $indirect-type-sub
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (global.get $g1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $y)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (global.get $g3)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $merges-effects-from-sub-only (param $ref (ref $indirect-type-sub))
(local $x i32)
(local $y i32)
(local $z i32)

(local.set $x (global.get $g1))
(local.set $y (global.get $g2))
(local.set $z (global.get $g3))

;; Similar to above but here it's impossible to reach $impl1
;; (the supertype), so $x can safely be optimized out.
(call_ref $indirect-type-sub (i32.const 1) (local.get $ref))

(drop (local.get $x))
(drop (local.get $y))
(drop (local.get $z))
)
)

;; Test different ways of referencing functions to ensure that they're included
;; in indirect effects analysis. A function is considered 'addressed' if it's:
;; - imported (tested in the next test)
;; - exported
;; - referenced in a ref.func
;; - contained in an `elem` segment
;; Imported functions are tested in the next module to avoid
;; confounding this test because imports are assumed to have all possible
;; effects.
(module
;; CHECK: (type $indirect-type (func (param i32)))
(type $indirect-type (func (param i32)))

;; CHECK: (type $1 (func))

;; CHECK: (type $2 (func (param (ref $indirect-type))))

;; CHECK: (global $g1 (mut i32) (i32.const 0))
(global $g1 (mut i32) (i32.const 0))
;; CHECK: (global $g2 (mut i32) (i32.const 0))
(global $g2 (mut i32) (i32.const 0))
;; CHECK: (global $g3 (mut i32) (i32.const 0))
(global $g3 (mut i32) (i32.const 0))
;; CHECK: (global $g4 (mut i32) (i32.const 0))
(global $g4 (mut i32) (i32.const 0))

(table 1 1 funcref)

;; CHECK: (table $0 1 1 funcref)

;; CHECK: (elem $0 (i32.const 0) $f3)

;; CHECK: (elem declare func $f2)

;; CHECK: (export "f1" (func $f1))

;; CHECK: (func $f1 (type $indirect-type) (param $i32 i32)
;; CHECK-NEXT: (global.set $g1
;; CHECK-NEXT: (local.get $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $f1 (export "f1") (type $indirect-type) (param $i32 i32)
(global.set $g1 (local.get $i32))
)

;; CHECK: (func $f2 (type $indirect-type) (param $i32 i32)
;; CHECK-NEXT: (global.set $g2
;; CHECK-NEXT: (local.get $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $f2 (type $indirect-type) (param $i32 i32)
(global.set $g2 (local.get $i32))
)
;; CHECK: (func $reference-f2 (type $1)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (ref.func $f2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $reference-f2
(drop (ref.func $f2))
)

;; CHECK: (func $f3 (type $indirect-type) (param $i32 i32)
;; CHECK-NEXT: (global.set $g3
;; CHECK-NEXT: (local.get $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $f3 (type $indirect-type) (param $i32 i32)
(global.set $g3 (local.get $i32))
)
(elem (i32.const 0) $f3)

;; CHECK: (func $merges-multiple-effects (type $2) (param $ref (ref $indirect-type))
;; CHECK-NEXT: (local $l1 i32)
;; CHECK-NEXT: (local $l2 i32)
;; CHECK-NEXT: (local $l3 i32)
;; CHECK-NEXT: (local $l4 i32)
;; CHECK-NEXT: (local.set $l1
;; CHECK-NEXT: (global.get $g1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $l2
;; CHECK-NEXT: (global.get $g2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $l3
;; CHECK-NEXT: (global.get $g3)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: (call_ref $indirect-type
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $l1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $l2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $l3)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (global.get $g4)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $merges-multiple-effects (param $ref (ref $indirect-type))
(local $l1 i32)
(local $l2 i32)
(local $l3 i32)
(local $l4 i32)

(local.set $l1 (global.get $g1))
(local.set $l2 (global.get $g2))
(local.set $l3 (global.get $g3))
(local.set $l4 (global.get $g4))

;; This acts as a barrier for $l1, $l2, and $l3 but not $l4.
;; $ref may write to $g1 via $f1, or $g2 via $f2, $g3 via $f3 but not $g4.
;; $l4 is optimized out and the others are left alone.
(call_ref $indirect-type (i32.const 1) (local.get $ref))

(drop (local.get $l1))
(drop (local.get $l2))
(drop (local.get $l3))
(drop (local.get $l4))
)
)

(module
;; CHECK: (type $indirect-type (func (param i32)))

;; CHECK: (type $1 (func (param (ref $indirect-type))))

;; CHECK: (import "" "" (func $imported-func (type $indirect-type) (param i32)))
(import "" "" (func $imported-func (type $indirect-type)))

(type $indirect-type (func (param i32)))

;; CHECK: (global $g1 (mut i32) (i32.const 0))
(global $g1 (mut i32) (i32.const 0))
;; CHECK: (global $g2 (mut i32) (i32.const 0))
(global $g2 (mut i32) (i32.const 0))

;; CHECK: (func $merges-multiple-effects (type $1) (param $ref (ref $indirect-type))
;; CHECK-NEXT: (local $l1 i32)
;; CHECK-NEXT: (local $l2 i32)
;; CHECK-NEXT: (local.set $l1
;; CHECK-NEXT: (global.get $g1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $l2
;; CHECK-NEXT: (global.get $g2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (call_ref $indirect-type
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (local.get $ref)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $l1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $l2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $merges-multiple-effects (param $ref (ref $indirect-type))
(local $l1 i32)
(local $l2 i32)

(local.set $l1 (global.get $g1))
(local.set $l2 (global.get $g2))

;; This can flow to an import, so we have to assume that $g1 and $g2 could
;; be mutated, and nothing can be optimized.
(call_ref $indirect-type (i32.const 1) (local.get $ref))
Comment thread
stevenfontanella marked this conversation as resolved.
Outdated

(drop (local.get $l1))
(drop (local.get $l2))
)
)
Loading