diff --git a/src/passes/DeadArgumentElimination.cpp b/src/passes/DeadArgumentElimination.cpp index 605b69eeb86..b31588e2e80 100644 --- a/src/passes/DeadArgumentElimination.cpp +++ b/src/passes/DeadArgumentElimination.cpp @@ -50,6 +50,7 @@ #include "passes/opt-utils.h" #include "support/sorted_vector.h" #include "wasm-builder.h" +#include "wasm-type.h" #include "wasm.h" namespace wasm { @@ -76,12 +77,9 @@ struct DAEFunctionInfo { // removed as well. bool hasTailCalls = false; std::unordered_set tailCallees; - // 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; + // 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; // Clears all data, which marks us as stale and in need of recomputation. void clear() { *this = DAEFunctionInfo(); } @@ -143,12 +141,9 @@ 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->hasUnseenCalls.insert(curr->func); + currInfo->hasRef.insert(curr->func); } // main entry point @@ -248,7 +243,7 @@ struct DAE : public Pass { std::vector> allCalls(numFunctions); std::vector tailCallees(numFunctions); - std::vector hasUnseenCalls(numFunctions); + std::vector hasRef(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. @@ -267,14 +262,17 @@ struct DAE : public Pass { for (auto& [call, dropp] : info.droppedCalls) { allDroppedCalls[call] = dropp; } - for (auto& name : info.hasUnseenCalls) { - hasUnseenCalls[indexes[name]] = true; + for (auto& name : info.hasRef) { + hasRef[indexes[name]] = true; } } - // Exports are considered unseen calls. + + // 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); for (auto& curr : module->exports) { if (curr->kind == ExternalKind::Function) { - hasUnseenCalls[indexes[*curr->getInternalName()]] = true; + isExported[indexes[*curr->getInternalName()]] = true; } } @@ -318,38 +316,48 @@ struct DAE : public Pass { if (func->imported()) { continue; } - // We can only optimize if we see all the calls and can modify them. - if (hasUnseenCalls[index]) { + // References prevent optimization. + if (hasRef[index]) { continue; } auto& calls = allCalls[index]; - if (calls.empty()) { + if (calls.empty() && !isExported[index]) { // 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; - if (refineArgumentTypes(func, calls, module, infoMap[name])) { - worthOptimizing.insert(func); - markStale(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); + } } - // Refine return types as well. - if (refineReturnTypes(func, calls, module)) { + // 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])) { refinedReturnTypes = true; markStale(name); markCallersStale(calls); } - 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); + // 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); + } } } if (refinedReturnTypes) { @@ -364,7 +372,7 @@ struct DAE : public Pass { if (func->imported()) { continue; } - if (hasUnseenCalls[index]) { + if (hasRef[index] || isExported[index]) { continue; } auto numParams = func->getNumParams(); @@ -401,7 +409,7 @@ struct DAE : public Pass { if (func->getResults() == Type::none) { continue; } - if (hasUnseenCalls[index]) { + if (hasRef[index] || isExported[index]) { continue; } auto name = func->name; @@ -570,22 +578,59 @@ struct DAE : public Pass { // the middle, etc. bool refineReturnTypes(Function* func, const std::vector& calls, - Module* module) { + 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; + } + auto lub = LUB::getResultsLUB(func, *module); if (!lub.noted()) { return false; } auto newType = lub.getLUB(); - if (newType != func->getResults()) { + 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) { func->setResults(newType); - for (auto* call : calls) { - if (call->type != Type::unreachable) { - call->type = 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; } - return true; } - return false; + + return true; } }; diff --git a/src/passes/SignatureRefining.cpp b/src/passes/SignatureRefining.cpp index 67f45aed08d..270035ef680 100644 --- a/src/passes/SignatureRefining.cpp +++ b/src/passes/SignatureRefining.cpp @@ -26,7 +26,6 @@ // type, and all call_refs using it). // -#include "ir/export-utils.h" #include "ir/find_all.h" #include "ir/lubs.h" #include "ir/module-utils.h" diff --git a/test/lit/passes/dae-gc-no-cd.wast b/test/lit/passes/dae-gc-no-cd.wast new file mode 100644 index 00000000000..f5c4064f0f2 --- /dev/null +++ b/test/lit/passes/dae-gc-no-cd.wast @@ -0,0 +1,38 @@ +;; 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 6d014d0f8b4..9381968cfe0 100644 --- a/test/lit/passes/dae-gc.wast +++ b/test/lit/passes/dae-gc.wast @@ -1,7 +1,10 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items 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)) @@ -68,6 +71,14 @@ ;; 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 @@ -167,6 +178,8 @@ ;; Test that string constants can be applied. (module + ;; CHECK: (type $0 (func)) + ;; CHECK: (func $0 (type $0) ;; CHECK-NEXT: (call $1) ;; CHECK-NEXT: ) @@ -195,3 +208,135 @@ (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) + ) +)