diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp index b31588e2e80..605b69eeb86 100644 --- a/src/passes/DeadArgumentElimination.cpp +++ b/src/passes/DeadArgumentElimination.cpp @@ -50,7 +50,6 @@ #include "passes/opt-utils.h" #include "support/sorted_vector.h" #include "wasm-builder.h" -#include "wasm-type.h" #include "wasm.h" namespace wasm { @@ -77,9 +76,12 @@ struct DAEFunctionInfo { // removed as well. bool hasTailCalls = false; std::unordered_set tailCallees; - // The set of functions that have their reference taken (which means there may - // be non-direct calls, limiting what we can do). - std::unordered_set hasRef; + // The set of functions that have calls from places that limit what we can do. + // For now, any call we don't see inhibits our optimizations, but TODO: an + // export could be worked around by exporting a thunk that adds the parameter. + // + // This is built up in parallel in each function, and combined at the end. + std::unordered_set hasUnseenCalls; // Clears all data, which marks us as stale and in need of recomputation. void clear() { *this = DAEFunctionInfo(); } @@ -141,9 +143,12 @@ struct DAEScanner // the infoMap). auto* currInfo = info ? info : &(*infoMap)[Name()]; + // Treat a ref.func as an unseen call, preventing us from changing the + // function's type. If we did change it, it could be an observable + // difference from the outside, if the reference escapes, for example. // TODO: look for actual escaping? // TODO: create a thunk for external uses that allow internal optimizations - currInfo->hasRef.insert(curr->func); + currInfo->hasUnseenCalls.insert(curr->func); } // main entry point @@ -243,7 +248,7 @@ struct DAE : public Pass { std::vector> allCalls(numFunctions); std::vector tailCallees(numFunctions); - std::vector hasRef(numFunctions); + std::vector hasUnseenCalls(numFunctions); // Track the function in which relevant expressions exist. When we modify // those expressions we will need to mark the function's info as stale. @@ -262,17 +267,14 @@ struct DAE : public Pass { for (auto& [call, dropp] : info.droppedCalls) { allDroppedCalls[call] = dropp; } - for (auto& name : info.hasRef) { - hasRef[indexes[name]] = true; + for (auto& name : info.hasUnseenCalls) { + hasUnseenCalls[indexes[name]] = true; } } - - // Exports limit some optimizations, for example, we cannot remove or refine - // parameters. TODO: we could export a thunk that drops the parameter etc. - std::vector isExported(numFunctions); + // Exports are considered unseen calls. for (auto& curr : module->exports) { if (curr->kind == ExternalKind::Function) { - isExported[indexes[*curr->getInternalName()]] = true; + hasUnseenCalls[indexes[*curr->getInternalName()]] = true; } } @@ -316,48 +318,38 @@ struct DAE : public Pass { if (func->imported()) { continue; } - // References prevent optimization. - if (hasRef[index]) { + // We can only optimize if we see all the calls and can modify them. + if (hasUnseenCalls[index]) { continue; } auto& calls = allCalls[index]; - if (calls.empty() && !isExported[index]) { + if (calls.empty()) { // Nothing calls this, so it is not worth optimizing. continue; } + // Refine argument types before doing anything else. This does not + // affect whether an argument is used or not, it just refines the type + // where possible. auto name = func->name; - // Exports prevent refining of argument types, as we don't see all the - // calls. - if (!isExported[index]) { - // Refine argument types before doing anything else. This does not - // affect whether an argument is used or not, it just refines the type - // where possible. - if (refineArgumentTypes(func, calls, module, infoMap[name])) { - worthOptimizing.insert(func); - markStale(func->name); - } + if (refineArgumentTypes(func, calls, module, infoMap[name])) { + worthOptimizing.insert(func); + markStale(func->name); } - // Refine return types as well. Note that exports do *not* prevent this! - // It is valid to export a function that returns something even more - // refined. - if (refineReturnTypes(func, calls, module, isExported[index])) { + // Refine return types as well. + if (refineReturnTypes(func, calls, module)) { refinedReturnTypes = true; markStale(name); markCallersStale(calls); } - // Exports prevent applying of constant values, as we don't see all the - // calls. - if (!isExported[index]) { - auto optimizedIndexes = - ParamUtils::applyConstantValues({func}, calls, {}, module); - for (auto i : optimizedIndexes) { - // Mark it as unused, which we know it now is (no point to re-scan - // just for that). - infoMap[name].unusedParams.insert(i); - } - if (!optimizedIndexes.empty()) { - markStale(func->name); - } + auto optimizedIndexes = + ParamUtils::applyConstantValues({func}, calls, {}, module); + for (auto i : optimizedIndexes) { + // Mark it as unused, which we know it now is (no point to re-scan just + // for that). + infoMap[name].unusedParams.insert(i); + } + if (!optimizedIndexes.empty()) { + markStale(func->name); } } if (refinedReturnTypes) { @@ -372,7 +364,7 @@ struct DAE : public Pass { if (func->imported()) { continue; } - if (hasRef[index] || isExported[index]) { + if (hasUnseenCalls[index]) { continue; } auto numParams = func->getNumParams(); @@ -409,7 +401,7 @@ struct DAE : public Pass { if (func->getResults() == Type::none) { continue; } - if (hasRef[index] || isExported[index]) { + if (hasUnseenCalls[index]) { continue; } auto name = func->name; @@ -578,59 +570,22 @@ struct DAE : public Pass { // the middle, etc. bool refineReturnTypes(Function* func, const std::vector& calls, - Module* module, - bool isExported) { - if (isExported && !func->type.isOpen()) { - // We must subtype the current type, so that imports of it work, but it is - // closed. - return false; - } - + Module* module) { auto lub = LUB::getResultsLUB(func, *module); if (!lub.noted()) { return false; } auto newType = lub.getLUB(); - if (newType == func->getResults()) { - return false; - } - - // If this is exported, we cannot refine to an exact type without the - // custom descriptors feature being enabled. - if (isExported && !module->features.hasCustomDescriptors()) { - // Remove exactness. - std::vector inexact; - for (auto t : newType) { - inexact.push_back(t.isRef() ? t.with(Inexact) : t); - } - newType = Type(inexact); - if (newType == func->getResults()) { - return false; - } - } - - if (!isExported) { + if (newType != func->getResults()) { func->setResults(newType); - } else { - // We must explicitly subtype the old type. - TypeBuilder builder(1); - builder[0] = Signature(func->getParams(), newType); - builder[0].subTypeOf(func->type); - // Make this subtype open like the super. This is not necessary, but might - // allow more work later after other changes, in theory. - builder[0].setOpen(); - auto result = builder.build(); - assert(!result.getError()); - func->type = (*result)[0]; - } - - for (auto* call : calls) { - if (call->type != Type::unreachable) { - call->type = newType; + for (auto* call : calls) { + if (call->type != Type::unreachable) { + call->type = newType; + } } + return true; } - - return true; + return false; } }; diff --git a/test/lit/passes/dae-gc-no-cd.wast b/test/lit/passes/dae-gc-no-cd.wast deleted file mode 100644 index f5c4064f0f2..00000000000 --- a/test/lit/passes/dae-gc-no-cd.wast +++ /dev/null @@ -1,38 +0,0 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: foreach %s %t wasm-opt -all --disable-custom-descriptors --dae -S -o - | filecheck %s - -;; We cannot refine to an exact type in an export, as CD is disabled. -(module - ;; CHECK: (type $A (struct)) - - ;; CHECK: (type $func (sub (func (result anyref)))) - (type $func (sub (func (result anyref)))) - - ;; CHECK: (type $func2 (sub (func (result anyref i32)))) - (type $func2 (sub (func (result anyref i32)))) - - (type $A (struct)) - - ;; CHECK: (func $export (type $3) (result (ref $A)) - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (result anyref) - ;; We can refine to (ref $A), but not an exact one. - (struct.new $A) - ) - - ;; CHECK: (func $export-tuple (type $4) (result (ref $A) i32) - ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $export-tuple (export "export-tuple") (type $func2) (result anyref i32) - ;; Ditto, but the ref is in a tuple. - (tuple.make 2 - (struct.new $A) - (i32.const 42) - ) - ) -) - diff --git a/test/lit/passes/dae-gc.wast b/test/lit/passes/dae-gc.wast index 9381968cfe0..6d014d0f8b4 100644 --- a/test/lit/passes/dae-gc.wast +++ b/test/lit/passes/dae-gc.wast @@ -1,10 +1,7 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. - +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. ;; RUN: foreach %s %t wasm-opt -all --dae -S -o - | filecheck %s (module - ;; CHECK: (type $0 (func)) - ;; CHECK: (type $"{}" (struct)) (type $"{}" (struct)) @@ -71,14 +68,6 @@ ;; Test ref.func and ref.null optimization of constant parameter values. (module - ;; CHECK: (type $0 (func)) - - ;; CHECK: (type $1 (func (param (ref (exact $0))))) - - ;; CHECK: (type $2 (func (param i31ref))) - - ;; CHECK: (elem declare func $a $b $c) - ;; CHECK: (func $foo (type $1) (param $0 (ref (exact $0))) ;; CHECK-NEXT: (local $1 (ref (exact $0))) ;; CHECK-NEXT: (local.set $1 @@ -178,8 +167,6 @@ ;; Test that string constants can be applied. (module - ;; CHECK: (type $0 (func)) - ;; CHECK: (func $0 (type $0) ;; CHECK-NEXT: (call $1) ;; CHECK-NEXT: ) @@ -208,135 +195,3 @@ (nop) ) ) - -;; Function results can be refined, even if exported. -(module - ;; CHECK: (type $func (sub (func (param anyref) (result anyref)))) - (type $func (sub (func (param anyref) (result anyref)))) - - ;; CHECK: (type $1 (sub $func (func (param anyref) (result (ref any))))) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (export "export" (func $export)) - - ;; CHECK: (func $export (type $1) (param $x anyref) (result (ref any)) - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (param $x anyref) (result anyref) - (ref.as_non_null - (local.get $x) - ) - ) - - ;; CHECK: (func $caller (type $2) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (call $export - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $caller - ;; Send a non-null param as well, but an export's params cannot be - ;; refined. Also drop the result, and again, an export's result cannot be - ;; removed. - (drop - (call $export - (ref.as_non_null - (ref.null any) - ) - ) - ) - ) -) - -;; An export's results can be refined even without any calls to it. -(module - ;; CHECK: (type $func (sub (func (param anyref) (result anyref)))) - (type $func (sub (func (param anyref) (result anyref)))) - - ;; CHECK: (type $1 (sub $func (func (param anyref) (result (ref any))))) - - ;; CHECK: (export "export" (func $export)) - - ;; CHECK: (func $export (type $1) (param $x anyref) (result (ref any)) - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (param $x anyref) (result anyref) - (ref.as_non_null - (local.get $x) - ) - ) -) - -;; A ref.func stops an export's results from being be refined. -(module - ;; CHECK: (type $func (sub (func (param anyref) (result anyref)))) - (type $func (sub (func (param anyref) (result anyref)))) - - ;; CHECK: (elem declare func $export) - - ;; CHECK: (export "export" (func $export)) - - ;; CHECK: (func $export (type $func) (param $x anyref) (result anyref) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.func $export) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (param $x anyref) (result anyref) - (drop - (ref.func $export) - ) - (ref.as_non_null - (local.get $x) - ) - ) -) - -;; We can refine to an exact type in an export, as CD is enabled. -(module - ;; CHECK: (type $A (struct)) - (type $A (struct)) - - ;; CHECK: (type $func (sub (func (result anyref)))) - (type $func (sub (func (result anyref)))) - - ;; CHECK: (type $2 (sub $func (func (result (ref (exact $A)))))) - - ;; CHECK: (export "export" (func $export)) - - ;; CHECK: (func $export (type $2) (result (ref (exact $A))) - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (result anyref) - (struct.new $A) - ) -) - -;; We do not refine the result if the type is final/closed, as we can't -;; subtype it. -(module - ;; CHECK: (type $func (func (result anyref))) - (type $func (func (result anyref))) - - ;; CHECK: (type $A (struct)) - (type $A (struct)) - - ;; CHECK: (export "export" (func $export)) - - ;; CHECK: (func $export (type $func) (result anyref) - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - (func $export (export "export") (type $func) (result anyref) - (struct.new $A) - ) -)