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
5556namespace 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
0 commit comments