diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rwxr-xr-x | scripts/fuzz_opt.py | 3 | ||||
-rw-r--r-- | src/pass.h | 8 | ||||
-rw-r--r-- | src/passes/RemoveUnusedModuleElements.cpp | 31 | ||||
-rw-r--r-- | src/passes/pass.cpp | 18 | ||||
-rw-r--r-- | src/tools/optimization-options.h | 4 | ||||
-rw-r--r-- | test/lit/passes/gto_and_cfp_in_O.wast | 28 | ||||
-rw-r--r-- | test/lit/passes/remove-unused-module-elements-refs.wast | 184 | ||||
-rw-r--r-- | test/passes/Oz_fuzz-exec_all-features.txt | 18 | ||||
-rw-r--r-- | test/unit/test_passes.py | 49 |
10 files changed, 307 insertions, 38 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe30bdff..6c7f6f2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ full changeset diff at the end of each section. Current Trunk ------------- +- Add `--closed-world` flag. This enables more optimizations in GC mode as it + lets us assume that we can change types inside the module. - The isorecursive WasmGC type system (i.e. --hybrid) is now the default to match the spec and the old default equirecursive (i.e. --structural) system has been removed. diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 2f4fad5c2..98695580d 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1378,6 +1378,9 @@ def randomize_opt_flags(): # wasm limitation on function body size which is 128K) if random.random() < 0.5: ret += ['-fimfs=99999999'] + # test both closed and open world + if random.random() < 0.5: + ret += ['--closed-world'] assert ret.count('--flatten') <= 1 return ret diff --git a/src/pass.h b/src/pass.h index 120ed2629..02cea7879 100644 --- a/src/pass.h +++ b/src/pass.h @@ -184,6 +184,14 @@ struct PassOptions { // creates it and we know it is all zeros right before the active segments are // applied.) bool zeroFilledMemory = false; + // Assume code outside of the module does not inspect or interact with GC and + // function references, even if they are passed out. The outside may hold on + // to them and pass them back in, but not inspect their contents or call them. + // By default we do not make that assumption, and assume anything that escapes + // to the outside may be inspected in detail, which prevents us from e.g. + // changing a type that escapes (so we can't remove or refine fields on an + // escaping struct type, for example). + bool closedWorld = false; // Whether to try to preserve debug info through, which are special calls. bool debugInfo = false; // Arbitrary string arguments from the commandline, which we forward to diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 7feda6f05..bf5f84736 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -43,6 +43,8 @@ using ModuleElement = std::pair<ModuleElementKind, Name>; struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { Module* module; + bool closedWorld; + std::vector<ModuleElement> queue; std::set<ModuleElement> reachable; bool usesMemory = false; @@ -63,13 +65,15 @@ struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { // we'll have that type in calledSignatures, and so this contains only // RefFuncs that we have not seen a call for yet, hence "uncalledRefFuncMap." // - // TODO: We assume a closed world in the GC space atm, but eventually should - // have a flag for that, and when the world is not closed we'd need to - // check for RefFuncs that flow out to exports or imports + // We can only do this when assuming a closed world. TODO: In an open world we + // could carefully track which types actually escape out to exports or + // imports. std::unordered_map<HeapType, std::unordered_set<Name>> uncalledRefFuncMap; - ReachabilityAnalyzer(Module* module, const std::vector<ModuleElement>& roots) - : module(module) { + ReachabilityAnalyzer(Module* module, + const std::vector<ModuleElement>& roots, + bool closedWorld) + : module(module), closedWorld(closedWorld) { queue = roots; // Globals used in memory/table init expressions are also roots for (auto& segment : module->dataSegments) { @@ -205,6 +209,12 @@ struct ReachabilityAnalyzer : public PostWalker<ReachabilityAnalyzer> { void visitMemorySize(MemorySize* curr) { usesMemory = true; } void visitMemoryGrow(MemoryGrow* curr) { usesMemory = true; } void visitRefFunc(RefFunc* curr) { + if (!closedWorld) { + // The world is open, so assume the worst and something (inside or outside + // of the module) can call this. + maybeAdd(ModuleElement(ModuleElementKind::Function, curr->func)); + return; + } auto type = curr->type.getHeapType(); if (calledSignatures.count(type)) { // We must not have a type in both calledSignatures and @@ -313,12 +323,20 @@ struct RemoveUnusedModuleElements : public Pass { roots.emplace_back(ModuleElementKind::Function, name); }); // Compute reachability starting from the root set. - ReachabilityAnalyzer analyzer(module, roots); + auto closedWorld = getPassOptions().closedWorld; + ReachabilityAnalyzer analyzer(module, roots, closedWorld); // RefFuncs that are never called are a special case: We cannot remove the // function, since then (ref.func $foo) would not validate. But if we know // it is never called, at least the contents do not matter, so we can // empty it out. + // + // We can only do this in a closed world, as otherwise function references + // may be called outside of the module (if they escape, which we could in + // principle track, see the TODO earlier in this file). So in the case of an + // open world we should not have noted anything in uncalledRefFuncMap + // earlier and not do any related optimizations there. + assert(closedWorld || analyzer.uncalledRefFuncMap.empty()); std::unordered_set<Name> uncalledRefFuncs; for (auto& [type, targets] : analyzer.uncalledRefFuncMap) { for (auto target : targets) { @@ -334,7 +352,6 @@ struct RemoveUnusedModuleElements : public Pass { assert(analyzer.uncalledRefFuncMap.count(type) == 0); } #endif - // Remove unreachable elements. module->removeFunctions([&](Function* curr) { if (analyzer.reachable.count( diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 9daf8e1d3..ee4e2ed21 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -594,18 +594,24 @@ void PassRunner::addDefaultGlobalOptimizationPrePasses() { addIfNoDWARFIssues("once-reduction"); } if (wasm->features.hasGC() && options.optimizeLevel >= 2) { - addIfNoDWARFIssues("type-refining"); - addIfNoDWARFIssues("signature-pruning"); - addIfNoDWARFIssues("signature-refining"); + if (options.closedWorld) { + addIfNoDWARFIssues("type-refining"); + addIfNoDWARFIssues("signature-pruning"); + addIfNoDWARFIssues("signature-refining"); + } addIfNoDWARFIssues("global-refining"); // Global type optimization can remove fields that are not needed, which can // remove ref.funcs that were once assigned to vtables but are no longer // needed, which can allow more code to be removed globally. After those, // constant field propagation can be more effective. - addIfNoDWARFIssues("gto"); + if (options.closedWorld) { + addIfNoDWARFIssues("gto"); + } addIfNoDWARFIssues("remove-unused-module-elements"); - addIfNoDWARFIssues("cfp"); - addIfNoDWARFIssues("gsi"); + if (options.closedWorld) { + addIfNoDWARFIssues("cfp"); + addIfNoDWARFIssues("gsi"); + } } // TODO: generate-global-effects here, right before function passes, then // discard in addDefaultGlobalOptimizationPostPasses? the benefit seems diff --git a/src/tools/optimization-options.h b/src/tools/optimization-options.h index a1a21b596..ff9bb973d 100644 --- a/src/tools/optimization-options.h +++ b/src/tools/optimization-options.h @@ -250,8 +250,8 @@ struct OptimizationOptions : public ToolOptions { "contents or call them.", OptimizationOptionsCategory, Options::Arguments::Zero, - [](Options*, const std::string&) { - // TODO: Implement this. + [this](Options*, const std::string&) { + passOptions.closedWorld = true; }) .add("--zero-filled-memory", "-uim", diff --git a/test/lit/passes/gto_and_cfp_in_O.wast b/test/lit/passes/gto_and_cfp_in_O.wast index a36bbc4ea..9d385d0f9 100644 --- a/test/lit/passes/gto_and_cfp_in_O.wast +++ b/test/lit/passes/gto_and_cfp_in_O.wast @@ -1,18 +1,37 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: foreach %s %t wasm-opt -O -all --nominal -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -O -all --nominal --closed-world -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -O -all --nominal -S -o - | filecheck %s --check-prefix OPEN_WORLD ;; Test that -O, with nominal typing + GC enabled, will run global type -;; optimization in conjunction with constant field propagation etc. +;; optimization in conjunction with constant field propagation etc. But, in an +;; open world we do not run them. (module + ;; OPEN_WORLD: (type $struct (struct (field (mut funcref)) (field (mut i32)))) (type $struct (struct_subtype (field (mut funcref)) (field (mut i32)) data)) + ;; OPEN_WORLD: (type $none_=>_none (func)) + + ;; OPEN_WORLD: (type $none_=>_i32 (func (result i32))) + + ;; OPEN_WORLD: (global $glob (ref $struct) (struct.new $struct + ;; OPEN_WORLD-NEXT: (ref.func $by-ref) + ;; OPEN_WORLD-NEXT: (i32.const 100) + ;; OPEN_WORLD-NEXT: )) (global $glob (ref $struct) (struct.new $struct (ref.func $by-ref) (i32.const 100) )) + ;; OPEN_WORLD: (export "main" (func $main)) + + ;; OPEN_WORLD: (func $by-ref (type $none_=>_none) (; has Stack IR ;) + ;; OPEN_WORLD-NEXT: (struct.set $struct 1 + ;; OPEN_WORLD-NEXT: (global.get $glob) + ;; OPEN_WORLD-NEXT: (i32.const 200) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $by-ref ;; This function is kept alive by the reference in $glob. After we remove ;; the field that the funcref is written to, we remove the funcref, which @@ -33,6 +52,11 @@ ;; CHECK: (func $main (type $none_=>_i32) (; has Stack IR ;) (result i32) ;; CHECK-NEXT: (i32.const 100) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $main (type $none_=>_i32) (; has Stack IR ;) (result i32) + ;; OPEN_WORLD-NEXT: (struct.get $struct 1 + ;; OPEN_WORLD-NEXT: (global.get $glob) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $main (export "main") (result i32) ;; After all the above optimizations, we can infer that $main should simply ;; return 100. diff --git a/test/lit/passes/remove-unused-module-elements-refs.wast b/test/lit/passes/remove-unused-module-elements-refs.wast index 1e23099b9..8e4ed70a7 100644 --- a/test/lit/passes/remove-unused-module-elements-refs.wast +++ b/test/lit/passes/remove-unused-module-elements-refs.wast @@ -1,12 +1,22 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements --nominal -all -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements --closed-world --nominal -all -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt --remove-unused-module-elements --nominal -all -S -o - | filecheck %s --check-prefix OPEN_WORLD + +;; Test both open world (default) and closed world. In a closed world we can do +;; more with function refs, as we assume nothing calls them on the outside, so +;; if no calls exist to the right type, the function is not reached. (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) + ;; CHECK: (type $ref?|$A|_=>_none (func (param (ref null $A)))) ;; CHECK: (type $B (func)) + ;; OPEN_WORLD: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + + ;; OPEN_WORLD: (type $B (func)) (type $B (func)) ;; CHECK: (elem declare func $target-A $target-B) @@ -30,6 +40,27 @@ ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $target-A $target-B) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-B) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (block ;; (replaces something unreachable we can't emit) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (unreachable) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (unreachable) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $foo (export "foo") (param $A (ref null $A)) ;; This export has two RefFuncs, and one CallRef. (drop @@ -51,6 +82,9 @@ ;; CHECK: (func $target-A (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A (type $A) ;; This function is reachable from the export "foo": there is a RefFunc and ;; a CallRef for it there. @@ -64,6 +98,9 @@ ;; CHECK: (func $target-B (type $B) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-B (type $B) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-B (type $B) ;; This function is not reachable. We have a RefFunc in "foo" but no ;; suitable CallRef. @@ -71,33 +108,51 @@ ;; Note that we cannot remove the function, as the RefFunc must refer to ;; something in order to validate. But we can clear out the body of this ;; function with an unreachable. + ;; + ;; As mentioned above, in an open world we cannot optimize here, so the + ;; function body will remain empty as a nop, and not turn into an + ;; unreachable. ) ) ;; As above, but reverse the order inside $foo, so we see the CallRef first. (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) (type $B (func)) + ;; CHECK: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + ;; CHECK: (elem declare func $target-A) ;; CHECK: (export "foo" (func $foo)) - ;; CHECK: (func $foo (type $A) - ;; CHECK-NEXT: (block ;; (replaces something unreachable we can't emit) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.null nofunc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (unreachable) + ;; CHECK: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; CHECK-NEXT: (call_ref $A + ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.func $target-A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $foo (export "foo") + ;; OPEN_WORLD: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + + ;; OPEN_WORLD: (elem declare func $target-A) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) + (func $foo (export "foo") (param $A (ref null $A)) (call_ref $A - (ref.null $A) + (local.get $A) ) (drop (ref.func $target-A) @@ -105,8 +160,11 @@ ) ;; CHECK: (func $target-A (type $A) - ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A (type $A) ;; This function is reachable. ) @@ -119,6 +177,7 @@ ;; As above, but interleave CallRefs with RefFuncs. (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) (type $B (func)) @@ -142,6 +201,26 @@ ;; CHECK-NEXT: (ref.func $target-A-2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + + ;; OPEN_WORLD: (elem declare func $target-A-1 $target-A-2) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A-1) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A-2) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $foo (export "foo") (param $A (ref null $A)) (call_ref $A (local.get $A) @@ -160,6 +239,9 @@ ;; CHECK: (func $target-A-1 (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A-1 (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A-1 (type $A) ;; This function is reachable. ) @@ -167,6 +249,9 @@ ;; CHECK: (func $target-A-2 (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A-2 (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A-2 (type $A) ;; This function is reachable. ) @@ -180,6 +265,7 @@ ;; same. (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) (type $B (func)) @@ -203,6 +289,26 @@ ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + + ;; OPEN_WORLD: (elem declare func $target-A-1 $target-A-2) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A-1) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-A-2) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call_ref $A + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $foo (export "foo") (param $A (ref null $A)) (drop (ref.func $target-A-1) @@ -221,6 +327,9 @@ ;; CHECK: (func $target-A-1 (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A-1 (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A-1 (type $A) ;; This function is reachable. ) @@ -228,6 +337,9 @@ ;; CHECK: (func $target-A-2 (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-A-2 (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-A-2 (type $A) ;; This function is reachable. ) @@ -241,15 +353,20 @@ ;; but for now other imports do not (until we add a flag for closed-world). (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) ;; CHECK: (type $funcref_=>_none (func (param funcref))) ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) + ;; OPEN_WORLD: (type $funcref_=>_none (func (param funcref))) + + ;; OPEN_WORLD: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) ;; CHECK: (import "other" "import" (func $other-import (param funcref))) + ;; OPEN_WORLD: (import "other" "import" (func $other-import (param funcref))) (import "other" "import" (func $other-import (param funcref))) @@ -265,6 +382,18 @@ ;; CHECK-NEXT: (ref.func $target-drop) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $target-drop $target-keep) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $A) + ;; OPEN_WORLD-NEXT: (call $call-without-effects + ;; OPEN_WORLD-NEXT: (ref.func $target-keep) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call $other-import + ;; OPEN_WORLD-NEXT: (ref.func $target-drop) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $foo (export "foo") ;; Calling the intrinsic with a reference is considered a call of the ;; reference, so we will not remove $target-keep. @@ -280,13 +409,20 @@ ;; CHECK: (func $target-keep (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-keep (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-keep (type $A) ) ;; CHECK: (func $target-drop (type $A) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-drop (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-drop (type $A) + ;; In a closed world we can turn this body into unreachable. ) ) @@ -294,6 +430,7 @@ ;; function being called. (module ;; CHECK: (type $A (func)) + ;; OPEN_WORLD: (type $A (func)) (type $A (func)) ;; CHECK: (type $funcref_=>_none (func (param funcref))) @@ -301,10 +438,16 @@ ;; CHECK: (type $ref?|$A|_=>_none (func (param (ref null $A)))) ;; CHECK: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) + ;; OPEN_WORLD: (type $funcref_=>_none (func (param funcref))) + + ;; OPEN_WORLD: (type $ref?|$A|_=>_none (func (param (ref null $A)))) + + ;; OPEN_WORLD: (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) (import "binaryen-intrinsics" "call.without.effects" (func $call-without-effects (param funcref))) ;; CHECK: (import "other" "import" (func $other-import (param funcref))) + ;; OPEN_WORLD: (import "other" "import" (func $other-import (param funcref))) (import "other" "import" (func $other-import (param funcref))) @@ -323,6 +466,21 @@ ;; CHECK-NEXT: (ref.func $target-keep-2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (elem declare func $target-keep $target-keep-2) + + ;; OPEN_WORLD: (export "foo" (func $foo)) + + ;; OPEN_WORLD: (func $foo (type $ref?|$A|_=>_none) (param $A (ref null $A)) + ;; OPEN_WORLD-NEXT: (call $call-without-effects + ;; OPEN_WORLD-NEXT: (local.get $A) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (drop + ;; OPEN_WORLD-NEXT: (ref.func $target-keep) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: (call $other-import + ;; OPEN_WORLD-NEXT: (ref.func $target-keep-2) + ;; OPEN_WORLD-NEXT: ) + ;; OPEN_WORLD-NEXT: ) (func $foo (export "foo") (param $A (ref null $A)) ;; Call the intrinsic without a RefFunc. All we infer here is the type, ;; which means we must assume anything with type $A (and a reference) can be @@ -341,12 +499,18 @@ ;; CHECK: (func $target-keep (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-keep (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-keep (type $A) ) ;; CHECK: (func $target-keep-2 (type $A) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + ;; OPEN_WORLD: (func $target-keep-2 (type $A) + ;; OPEN_WORLD-NEXT: (nop) + ;; OPEN_WORLD-NEXT: ) (func $target-keep-2 (type $A) ) ) diff --git a/test/passes/Oz_fuzz-exec_all-features.txt b/test/passes/Oz_fuzz-exec_all-features.txt index aa8d3d6c9..f67fc31c3 100644 --- a/test/passes/Oz_fuzz-exec_all-features.txt +++ b/test/passes/Oz_fuzz-exec_all-features.txt @@ -69,17 +69,13 @@ [fuzz-exec] calling static-br_on_cast_fail [LoggingExternalInterface logging -2] (module - (rec - (type $bytes (array (mut i8))) - (type $struct (struct (field (mut i32)))) - (type $void_func (func)) - (type $extendedstruct (struct )) - (type $int_func (func (result i32))) - (type $i32_=>_none (func (param i32))) - (type $none_=>_ref|$struct| (func (result (ref $struct)))) - (type $anyref_=>_none (func (param anyref))) - (type $none_=>_none (func)) - ) + (type $bytes (array (mut i8))) + (type $void_func (func)) + (type $struct (struct (field (mut i32)))) + (type $i32_=>_none (func (param i32))) + (type $extendedstruct (struct (field (mut i32)) (field f64))) + (type $anyref_=>_none (func (param anyref))) + (type $int_func (func (result i32))) (import "fuzzing-support" "log-i32" (func $log (param i32))) (export "structs" (func $0)) (export "arrays" (func $1)) diff --git a/test/unit/test_passes.py b/test/unit/test_passes.py new file mode 100644 index 000000000..2fe7f6263 --- /dev/null +++ b/test/unit/test_passes.py @@ -0,0 +1,49 @@ +import os +import re +import subprocess +from scripts.test import shared +from . import utils + + +class PassesTest(utils.BinaryenTestCase): + # Given some arguments, return the passes that were run. + def get_passes_run(self, args): + os.environ['BINARYEN_PASS_DEBUG'] = '1' + try: + hello_wat = self.input_path('hello_world.wat') + log = shared.run_process(shared.WASM_OPT + [hello_wat] + args, + stderr=subprocess.PIPE).stderr + print(log) + passes = re.findall(r'running pass: ([\w-]+)\.\.\.', log) + return passes + finally: + del os.environ['BINARYEN_PASS_DEBUG'] + + def test_O2(self): + args = ['-O2', '-all'] + for nominal in ['--nominal', False]: + for closed_world in ['--closed-world', False]: + curr_args = args[:] + if nominal: + curr_args.append(nominal) + if closed_world: + curr_args.append(closed_world) + passes = self.get_passes_run(curr_args) + + # dce always runs + self.assertIn('dce', passes) + + # some passes only run in closed world + CLOSED_WORLD_PASSES = [ + 'type-refining', + 'signature-pruning', + 'signature-refining', + 'gto', + 'cfp', + 'gsi', + ] + for pass_ in CLOSED_WORLD_PASSES: + if closed_world: + self.assertIn(pass_, passes) + else: + self.assertNotIn(pass_, passes) |