Skip to content

Commit b56072b

Browse files
authored
DeadArgumentElimination: Refine result types of exported functions (#7935)
Exports prevent most optimizations in this pass (we can't remove a parameter, for example), but we can refine results: the export will just return more-refined things, which is fine. This also helps the fuzzer, in that it allows us to generate more interesting testcases - we can modify the types of exports after the fact.
1 parent f1614a6 commit b56072b

File tree

4 files changed

+274
-47
lines changed

4 files changed

+274
-47
lines changed

src/passes/DeadArgumentElimination.cpp

Lines changed: 90 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
#include "passes/opt-utils.h"
5151
#include "support/sorted_vector.h"
5252
#include "wasm-builder.h"
53+
#include "wasm-type.h"
5354
#include "wasm.h"
5455

5556
namespace wasm {
@@ -76,12 +77,9 @@ struct DAEFunctionInfo {
7677
// removed as well.
7778
bool hasTailCalls = false;
7879
std::unordered_set<Name> tailCallees;
79-
// The set of functions that have calls from places that limit what we can do.
80-
// For now, any call we don't see inhibits our optimizations, but TODO: an
81-
// export could be worked around by exporting a thunk that adds the parameter.
82-
//
83-
// This is built up in parallel in each function, and combined at the end.
84-
std::unordered_set<Name> hasUnseenCalls;
80+
// The set of functions that have their reference taken (which means there may
81+
// be non-direct calls, limiting what we can do).
82+
std::unordered_set<Name> hasRef;
8583

8684
// Clears all data, which marks us as stale and in need of recomputation.
8785
void clear() { *this = DAEFunctionInfo(); }
@@ -143,12 +141,9 @@ struct DAEScanner
143141
// the infoMap).
144142
auto* currInfo = info ? info : &(*infoMap)[Name()];
145143

146-
// Treat a ref.func as an unseen call, preventing us from changing the
147-
// function's type. If we did change it, it could be an observable
148-
// difference from the outside, if the reference escapes, for example.
149144
// TODO: look for actual escaping?
150145
// TODO: create a thunk for external uses that allow internal optimizations
151-
currInfo->hasUnseenCalls.insert(curr->func);
146+
currInfo->hasRef.insert(curr->func);
152147
}
153148

154149
// main entry point
@@ -248,7 +243,7 @@ struct DAE : public Pass {
248243

249244
std::vector<std::vector<Call*>> allCalls(numFunctions);
250245
std::vector<bool> tailCallees(numFunctions);
251-
std::vector<bool> hasUnseenCalls(numFunctions);
246+
std::vector<bool> hasRef(numFunctions);
252247

253248
// Track the function in which relevant expressions exist. When we modify
254249
// those expressions we will need to mark the function's info as stale.
@@ -267,14 +262,17 @@ struct DAE : public Pass {
267262
for (auto& [call, dropp] : info.droppedCalls) {
268263
allDroppedCalls[call] = dropp;
269264
}
270-
for (auto& name : info.hasUnseenCalls) {
271-
hasUnseenCalls[indexes[name]] = true;
265+
for (auto& name : info.hasRef) {
266+
hasRef[indexes[name]] = true;
272267
}
273268
}
274-
// Exports are considered unseen calls.
269+
270+
// Exports limit some optimizations, for example, we cannot remove or refine
271+
// parameters. TODO: we could export a thunk that drops the parameter etc.
272+
std::vector<bool> isExported(numFunctions);
275273
for (auto& curr : module->exports) {
276274
if (curr->kind == ExternalKind::Function) {
277-
hasUnseenCalls[indexes[*curr->getInternalName()]] = true;
275+
isExported[indexes[*curr->getInternalName()]] = true;
278276
}
279277
}
280278

@@ -318,38 +316,48 @@ struct DAE : public Pass {
318316
if (func->imported()) {
319317
continue;
320318
}
321-
// We can only optimize if we see all the calls and can modify them.
322-
if (hasUnseenCalls[index]) {
319+
// References prevent optimization.
320+
if (hasRef[index]) {
323321
continue;
324322
}
325323
auto& calls = allCalls[index];
326-
if (calls.empty()) {
324+
if (calls.empty() && !isExported[index]) {
327325
// Nothing calls this, so it is not worth optimizing.
328326
continue;
329327
}
330-
// Refine argument types before doing anything else. This does not
331-
// affect whether an argument is used or not, it just refines the type
332-
// where possible.
333328
auto name = func->name;
334-
if (refineArgumentTypes(func, calls, module, infoMap[name])) {
335-
worthOptimizing.insert(func);
336-
markStale(func->name);
329+
// Exports prevent refining of argument types, as we don't see all the
330+
// calls.
331+
if (!isExported[index]) {
332+
// Refine argument types before doing anything else. This does not
333+
// affect whether an argument is used or not, it just refines the type
334+
// where possible.
335+
if (refineArgumentTypes(func, calls, module, infoMap[name])) {
336+
worthOptimizing.insert(func);
337+
markStale(func->name);
338+
}
337339
}
338-
// Refine return types as well.
339-
if (refineReturnTypes(func, calls, module)) {
340+
// Refine return types as well. Note that exports do *not* prevent this!
341+
// It is valid to export a function that returns something even more
342+
// refined.
343+
if (refineReturnTypes(func, calls, module, isExported[index])) {
340344
refinedReturnTypes = true;
341345
markStale(name);
342346
markCallersStale(calls);
343347
}
344-
auto optimizedIndexes =
345-
ParamUtils::applyConstantValues({func}, calls, {}, module);
346-
for (auto i : optimizedIndexes) {
347-
// Mark it as unused, which we know it now is (no point to re-scan just
348-
// for that).
349-
infoMap[name].unusedParams.insert(i);
350-
}
351-
if (!optimizedIndexes.empty()) {
352-
markStale(func->name);
348+
// Exports prevent applying of constant values, as we don't see all the
349+
// calls.
350+
if (!isExported[index]) {
351+
auto optimizedIndexes =
352+
ParamUtils::applyConstantValues({func}, calls, {}, module);
353+
for (auto i : optimizedIndexes) {
354+
// Mark it as unused, which we know it now is (no point to re-scan
355+
// just for that).
356+
infoMap[name].unusedParams.insert(i);
357+
}
358+
if (!optimizedIndexes.empty()) {
359+
markStale(func->name);
360+
}
353361
}
354362
}
355363
if (refinedReturnTypes) {
@@ -364,7 +372,7 @@ struct DAE : public Pass {
364372
if (func->imported()) {
365373
continue;
366374
}
367-
if (hasUnseenCalls[index]) {
375+
if (hasRef[index] || isExported[index]) {
368376
continue;
369377
}
370378
auto numParams = func->getNumParams();
@@ -401,7 +409,7 @@ struct DAE : public Pass {
401409
if (func->getResults() == Type::none) {
402410
continue;
403411
}
404-
if (hasUnseenCalls[index]) {
412+
if (hasRef[index] || isExported[index]) {
405413
continue;
406414
}
407415
auto name = func->name;
@@ -570,22 +578,59 @@ struct DAE : public Pass {
570578
// the middle, etc.
571579
bool refineReturnTypes(Function* func,
572580
const std::vector<Call*>& calls,
573-
Module* module) {
581+
Module* module,
582+
bool isExported) {
583+
if (isExported && !func->type.isOpen()) {
584+
// We must subtype the current type, so that imports of it work, but it is
585+
// closed.
586+
return false;
587+
}
588+
574589
auto lub = LUB::getResultsLUB(func, *module);
575590
if (!lub.noted()) {
576591
return false;
577592
}
578593
auto newType = lub.getLUB();
579-
if (newType != func->getResults()) {
594+
if (newType == func->getResults()) {
595+
return false;
596+
}
597+
598+
// If this is exported, we cannot refine to an exact type without the
599+
// custom descriptors feature being enabled.
600+
if (isExported && !module->features.hasCustomDescriptors()) {
601+
// Remove exactness.
602+
std::vector<Type> inexact;
603+
for (auto t : newType) {
604+
inexact.push_back(t.isRef() ? t.with(Inexact) : t);
605+
}
606+
newType = Type(inexact);
607+
if (newType == func->getResults()) {
608+
return false;
609+
}
610+
}
611+
612+
if (!isExported) {
580613
func->setResults(newType);
581-
for (auto* call : calls) {
582-
if (call->type != Type::unreachable) {
583-
call->type = newType;
584-
}
614+
} else {
615+
// We must explicitly subtype the old type.
616+
TypeBuilder builder(1);
617+
builder[0] = Signature(func->getParams(), newType);
618+
builder[0].subTypeOf(func->type);
619+
// Make this subtype open like the super. This is not necessary, but might
620+
// allow more work later after other changes, in theory.
621+
builder[0].setOpen();
622+
auto result = builder.build();
623+
assert(!result.getError());
624+
func->type = (*result)[0];
625+
}
626+
627+
for (auto* call : calls) {
628+
if (call->type != Type::unreachable) {
629+
call->type = newType;
585630
}
586-
return true;
587631
}
588-
return false;
632+
633+
return true;
589634
}
590635
};
591636

src/passes/SignatureRefining.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
// type, and all call_refs using it).
2727
//
2828

29-
#include "ir/export-utils.h"
3029
#include "ir/find_all.h"
3130
#include "ir/lubs.h"
3231
#include "ir/module-utils.h"

test/lit/passes/dae-gc-no-cd.wast

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
2+
;; RUN: foreach %s %t wasm-opt -all --disable-custom-descriptors --dae -S -o - | filecheck %s
3+
4+
;; We cannot refine to an exact type in an export, as CD is disabled.
5+
(module
6+
;; CHECK: (type $A (struct))
7+
8+
;; CHECK: (type $func (sub (func (result anyref))))
9+
(type $func (sub (func (result anyref))))
10+
11+
;; CHECK: (type $func2 (sub (func (result anyref i32))))
12+
(type $func2 (sub (func (result anyref i32))))
13+
14+
(type $A (struct))
15+
16+
;; CHECK: (func $export (type $3) (result (ref $A))
17+
;; CHECK-NEXT: (struct.new_default $A)
18+
;; CHECK-NEXT: )
19+
(func $export (export "export") (type $func) (result anyref)
20+
;; We can refine to (ref $A), but not an exact one.
21+
(struct.new $A)
22+
)
23+
24+
;; CHECK: (func $export-tuple (type $4) (result (ref $A) i32)
25+
;; CHECK-NEXT: (tuple.make 2
26+
;; CHECK-NEXT: (struct.new_default $A)
27+
;; CHECK-NEXT: (i32.const 42)
28+
;; CHECK-NEXT: )
29+
;; CHECK-NEXT: )
30+
(func $export-tuple (export "export-tuple") (type $func2) (result anyref i32)
31+
;; Ditto, but the ref is in a tuple.
32+
(tuple.make 2
33+
(struct.new $A)
34+
(i32.const 42)
35+
)
36+
)
37+
)
38+

0 commit comments

Comments
 (0)