Skip to content

Commit cc68dc5

Browse files
committed
Avoid newly exposing prototypes in GTO
When GTO removes fields or makes them immutable, it may introduce an immutable externref first field on the descriptor of a JS-exposed type where there was none before. That means that the described type could now have a JS-observable prototype where it did not before optimization, which makes this a misoptimization. Fix the problem by inserting an i8 placeholder first field wherever we would otherwise start exposing a prototype where there was none before. This is expected to be exceptionally rare in practice, so the extra memory use is not expected to be a real problem. Instead of adding a placeholder field, we could have inhibited optimization of the existing first field, but that would be more likely than an unaccessed placeholder field to have adverse effects in later passes.
1 parent 9d6a093 commit cc68dc5

3 files changed

Lines changed: 1082 additions & 53 deletions

File tree

src/ir/js-utils.h

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323

2424
namespace wasm::JSUtils {
2525

26+
// Whether a field is immutable and a reference to a subtype of externref that
27+
// could hold a JS prototype.
28+
inline bool isPossibleJSPrototypeField(const Field& field) {
29+
if (field.mutable_ != Immutable) {
30+
return false;
31+
}
32+
if (!field.type.isRef()) {
33+
return false;
34+
}
35+
return field.type.getHeapType().isMaybeShared(HeapType::ext);
36+
}
37+
2638
// Whether this is a descriptor struct type whose first field is immutable and a
2739
// subtype of externref.
2840
inline bool hasPossibleJSPrototypeField(HeapType type) {
@@ -34,13 +46,7 @@ inline bool hasPossibleJSPrototypeField(HeapType type) {
3446
if (fields.empty()) {
3547
return false;
3648
}
37-
if (fields[0].mutable_ == Mutable) {
38-
return false;
39-
}
40-
if (!fields[0].type.isRef()) {
41-
return false;
42-
}
43-
return fields[0].type.getHeapType().isMaybeShared(HeapType::ext);
49+
return isPossibleJSPrototypeField(fields[0]);
4450
}
4551

4652
// Calls flowIn and flowOut on all types that may flow in from or out to JS.

src/passes/GlobalTypeOptimization.cpp

Lines changed: 144 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,20 @@ struct FieldInfo {
7070

7171
struct FieldInfoScanner
7272
: public StructUtils::StructScanner<FieldInfo, FieldInfoScanner> {
73+
std::unordered_map<Function*, std::vector<Type>>& jsExposedTypes;
74+
7375
std::unique_ptr<Pass> create() override {
74-
return std::make_unique<FieldInfoScanner>(functionNewInfos,
75-
functionSetGetInfos);
76+
return std::make_unique<FieldInfoScanner>(
77+
functionNewInfos, functionSetGetInfos, jsExposedTypes);
7678
}
7779

7880
FieldInfoScanner(
7981
StructUtils::FunctionStructValuesMap<FieldInfo>& functionNewInfos,
80-
StructUtils::FunctionStructValuesMap<FieldInfo>& functionSetGetInfos)
82+
StructUtils::FunctionStructValuesMap<FieldInfo>& functionSetGetInfos,
83+
std::unordered_map<Function*, std::vector<Type>>& jsExposedTypes)
8184
: StructUtils::StructScanner<FieldInfo, FieldInfoScanner>(
82-
functionNewInfos, functionSetGetInfos) {}
85+
functionNewInfos, functionSetGetInfos),
86+
jsExposedTypes(jsExposedTypes) {}
8387

8488
void noteExpression(Expression* expr,
8589
HeapType type,
@@ -116,16 +120,8 @@ struct FieldInfoScanner
116120
// Converting a reference to externref makes the prototype field on its
117121
// descriptor available to be read by JS, if such a field exists.
118122
void visitRefAs(RefAs* curr) {
119-
if (curr->op != ExternConvertAny) {
120-
return;
121-
}
122-
if (!curr->value->type.isRef()) {
123-
return;
124-
}
125-
if (auto desc = curr->value->type.getHeapType().getDescriptorType();
126-
desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
127-
auto exact = curr->value->type.getExactness();
128-
functionSetGetInfos[getFunction()][{*desc, exact}][0].noteRead();
123+
if (curr->op == ExternConvertAny && curr->value->type.isRef()) {
124+
jsExposedTypes.at(getFunction()).push_back(curr->value->type);
129125
}
130126
}
131127
};
@@ -139,6 +135,11 @@ struct GlobalTypeOptimization : public Pass {
139135
// rare).
140136
std::unordered_map<HeapType, std::vector<bool>> canBecomeImmutable;
141137

138+
// Descriptor types that are exposed to JS but do _not_ configure prototypes
139+
// for their described types. We must avoid optimizing these types such that
140+
// they start configuring prototypes.
141+
std::unordered_set<HeapType> exposedNoProtoDescs;
142+
142143
// Maps each field to its new index after field removals. That is, this
143144
// takes into account that fields before this one may have been removed,
144145
// which would then reduce this field's index. If a field itself is removed,
@@ -149,6 +150,28 @@ struct GlobalTypeOptimization : public Pass {
149150
static const Index RemovedField = Index(-1);
150151
std::unordered_map<HeapType, std::vector<Index>> indexesAfterRemovals;
151152

153+
struct IndexAnalysis {
154+
Index newSize = 0;
155+
bool hasPlaceholder = false;
156+
157+
IndexAnalysis(const std::vector<Index>& indexes) {
158+
Index maxIndex = 0;
159+
bool hasKept = false;
160+
bool hasIndexZero = false;
161+
for (auto idx : indexes) {
162+
if (idx != RemovedField) {
163+
hasKept = true;
164+
maxIndex = std::max(maxIndex, idx);
165+
if (idx == 0) {
166+
hasIndexZero = true;
167+
}
168+
}
169+
}
170+
newSize = hasKept ? maxIndex + 1 : 0;
171+
hasPlaceholder = hasKept && !hasIndexZero;
172+
}
173+
};
174+
152175
void run(Module* module) override {
153176
if (!module->features.hasGC()) {
154177
return;
@@ -157,21 +180,32 @@ struct GlobalTypeOptimization : public Pass {
157180
Fatal() << "GTO requires --closed-world";
158181
}
159182

183+
std::unordered_map<Function*, std::vector<Type>> jsExposedTypesByFunction;
184+
jsExposedTypesByFunction[nullptr];
185+
for (auto& func : module->functions) {
186+
jsExposedTypesByFunction[func.get()];
187+
}
188+
160189
// Find and analyze struct operations inside each function.
161190
StructUtils::FunctionStructValuesMap<FieldInfo> functionNewInfos(*module),
162191
functionSetGetInfos(*module);
163-
FieldInfoScanner scanner(functionNewInfos, functionSetGetInfos);
192+
FieldInfoScanner scanner(
193+
functionNewInfos, functionSetGetInfos, jsExposedTypesByFunction);
164194
scanner.run(getPassRunner(), module);
165195
scanner.runOnModuleCode(getPassRunner(), module);
166196

167197
// Combine the data from the functions.
168198
functionSetGetInfos.combineInto(combinedSetGetInfos);
199+
std::vector<Type> jsExposedTypes;
200+
for (auto& [_, types] : jsExposedTypesByFunction) {
201+
jsExposedTypes.insert(jsExposedTypes.end(), types.begin(), types.end());
202+
}
169203

170204
SubTypes subTypes(*module);
171205

172206
// Analyze the JS interface to find fields holding configured prototypes
173207
// that cannot be removed.
174-
analyzeJSInterface(*module, subTypes);
208+
analyzeJSInterface(*module, subTypes, jsExposedTypes);
175209

176210
// Propagate information to super and subtypes on set/get infos:
177211
//
@@ -291,15 +325,16 @@ struct GlobalTypeOptimization : public Pass {
291325
}
292326

293327
// We need to compute the new set of indexes if we are removing fields, or
294-
// if our parent removed fields. In the latter case, our parent may have
295-
// reordered fields even if we ourselves are not removing anything, and we
296-
// must update to match the parent's order.
328+
// if our parent removed fields, or if we might need a placeholder. If we
329+
// have a parent, it may have reordered fields even if we ourselves are
330+
// not removing anything, and we must update to match the parent's order.
297331
auto super = type.getDeclaredSuperType();
298332
auto superHasUpdates = super && indexesAfterRemovals.contains(*super);
299-
if (!removableIndexes.empty() || superHasUpdates) {
300-
// We are removing fields. Reorder them to allow that, as in the general
301-
// case we can only remove fields from the end, so that if our subtypes
302-
// still need the fields they can append them. For example:
333+
bool isExposedNoProto = exposedNoProtoDescs.contains(type);
334+
if (!removableIndexes.empty() || superHasUpdates || isExposedNoProto) {
335+
// We might be removing fields. Reorder them to allow that, as in the
336+
// general case we can only remove fields from the end, so that if our
337+
// subtypes still need the fields they can append them. For example:
303338
//
304339
// type A = { x: i32, y: f64 };
305340
// type B : A = { x: 132, y: f64, z: v128 };
@@ -392,6 +427,37 @@ struct GlobalTypeOptimization : public Pass {
392427
}
393428
}
394429

430+
// If the type has no supertype (or its supertype has no fields), check
431+
// if its first field becomes prototype-exposing. If so, add a
432+
// placeholder at index 0 and shift all computed indices.
433+
if (isExposedNoProto && (!super || super->getStruct().fields.empty())) {
434+
// Find the field that will become field 0.
435+
Index i = 0;
436+
for (; i < fields.size(); ++i) {
437+
if (indexesAfterRemoval[i] == 0) {
438+
break;
439+
}
440+
}
441+
// Check whether that field would expose a prototype.
442+
if (i < fields.size()) {
443+
Field optimizedField = fields[i];
444+
if (auto it = canBecomeImmutable.find(type);
445+
it != canBecomeImmutable.end() && i < it->second.size() &&
446+
it->second[i]) {
447+
optimizedField.mutable_ = Immutable;
448+
}
449+
if (JSUtils::isPossibleJSPrototypeField(optimizedField)) {
450+
// The field exposes a prototype. Increment all field indices to
451+
// make room for a placeholder first field.
452+
for (auto& idx : indexesAfterRemoval) {
453+
if (idx != RemovedField) {
454+
++idx;
455+
}
456+
}
457+
}
458+
}
459+
}
460+
395461
// Only store the new indexes we computed if we found something
396462
// interesting. We might not, if e.g. our parent removes fields and we
397463
// add them back in the exact order we started with. In such cases,
@@ -416,7 +482,9 @@ struct GlobalTypeOptimization : public Pass {
416482
}
417483
}
418484

419-
void analyzeJSInterface(Module& wasm, const SubTypes& subTypes) {
485+
void analyzeJSInterface(Module& wasm,
486+
const SubTypes& subTypes,
487+
const std::vector<Type>& jsExposedTypes) {
420488
if (!wasm.features.hasCustomDescriptors()) {
421489
return;
422490
}
@@ -426,10 +494,16 @@ struct GlobalTypeOptimization : public Pass {
426494
// Mark the relevant prototype field as read and return true iff we newly
427495
// know we have to propagate the exposure to subtypes.
428496
auto noteExposed = [&](HeapType type, Exactness exact = Inexact) -> bool {
429-
if (auto desc = type.getDescriptorType();
430-
desc && JSUtils::hasPossibleJSPrototypeField(*desc)) {
431-
// This field holds a JS-visible prototype. Do not remove it.
432-
combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead();
497+
if (auto desc = type.getDescriptorType()) {
498+
if (JSUtils::hasPossibleJSPrototypeField(*desc)) {
499+
// This descriptor configures a JS-visible prototype. Do not remove
500+
// it.
501+
combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead();
502+
} else {
503+
// This descriptor does _not_ configure a JS prototype. Do not add
504+
// one.
505+
exposedNoProtoDescs.insert(*desc);
506+
}
433507
}
434508
if (exact == Inexact) {
435509
return subtypesExposed.insert(type).second;
@@ -449,6 +523,12 @@ struct GlobalTypeOptimization : public Pass {
449523

450524
JSUtils::iterJSInterface(wasm, flowIn, flowOut);
451525

526+
for (auto type : jsExposedTypes) {
527+
if (type.isRef()) {
528+
noteExposed(type.getHeapType(), type.getExactness());
529+
}
530+
}
531+
452532
// Any type that is a subtype of an exposed type is also exposed. Propagate
453533
// from supertypes to subtypes.
454534
std::vector<HeapType> work(subtypesExposed.begin(), subtypesExposed.end());
@@ -471,6 +551,18 @@ struct GlobalTypeOptimization : public Pass {
471551
}
472552
}
473553
}
554+
555+
// Also propagate exposed descriptors to supertypes so that descriptor
556+
// hierarchies have consistent layouts.
557+
for (auto type : subTypes.types) {
558+
if (exposedNoProtoDescs.contains(type)) {
559+
auto curr = type.getDeclaredSuperType();
560+
while (curr) {
561+
exposedNoProtoDescs.insert(*curr);
562+
curr = curr->getDeclaredSuperType();
563+
}
564+
}
565+
}
474566
}
475567

476568
void updateTypes(Module& wasm) {
@@ -499,17 +591,19 @@ struct GlobalTypeOptimization : public Pass {
499591
auto remIter = parent.indexesAfterRemovals.find(oldStructType);
500592
if (remIter != parent.indexesAfterRemovals.end()) {
501593
auto& indexesAfterRemoval = remIter->second;
502-
Index removed = 0;
594+
IndexAnalysis analysis(indexesAfterRemoval);
503595
auto copy = newFields;
504-
for (Index i = 0; i < newFields.size(); i++) {
596+
newFields.resize(analysis.newSize);
597+
if (analysis.hasPlaceholder) {
598+
newFields[0] = Field(Field::i8, Immutable);
599+
}
600+
for (Index i = 0; i < copy.size(); i++) {
505601
auto newIndex = indexesAfterRemoval[i];
506602
if (newIndex != RemovedField) {
603+
assert(newIndex < newFields.size());
507604
newFields[newIndex] = copy[i];
508-
} else {
509-
removed++;
510605
}
511606
}
512-
newFields.resize(newFields.size() - removed);
513607

514608
// Update field names as well. The Type Rewriter cannot do this for
515609
// us, as it does not know which old fields map to which new ones (it
@@ -595,26 +689,30 @@ struct GlobalTypeOptimization : public Pass {
595689
auto& operands = curr->operands;
596690
assert(indexesAfterRemoval.size() == operands.size());
597691

598-
Index removed = 0;
692+
IndexAnalysis analysis(indexesAfterRemoval);
599693
std::vector<Expression*> old(operands.begin(), operands.end());
600694
for (Index i = 0; i < operands.size(); ++i) {
601-
auto newIndex = indexesAfterRemoval[i];
602-
if (newIndex != RemovedField) {
603-
assert(newIndex < operands.size());
604-
operands[newIndex] = old[i];
605-
} else {
606-
++removed;
695+
if (indexesAfterRemoval[i] == RemovedField) {
607696
if (!func &&
608697
EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) {
609698
removedTrappingInits.push_back(old[i]);
610699
}
611700
}
612701
}
613-
if (removed) {
614-
operands.resize(operands.size() - removed);
615-
} else {
616-
// If we didn't remove anything then we must have reordered (or else
617-
// we have done pointless work).
702+
operands.resize(analysis.newSize);
703+
if (analysis.hasPlaceholder) {
704+
operands[0] = Builder(*getModule()).makeConst(Literal(int32_t(0)));
705+
}
706+
for (Index i = 0; i < old.size(); ++i) {
707+
auto newIndex = indexesAfterRemoval[i];
708+
if (newIndex != RemovedField) {
709+
assert(newIndex < operands.size());
710+
operands[newIndex] = old[i];
711+
}
712+
}
713+
if (analysis.newSize == old.size() && !analysis.hasPlaceholder) {
714+
// If we didn't remove or insert anything then we must have reordered
715+
// (or else we have done pointless work).
618716
assert(indexesAfterRemoval !=
619717
makeIdentity(indexesAfterRemoval.size()));
620718
}
@@ -697,7 +795,7 @@ struct GlobalTypeOptimization : public Pass {
697795
}
698796
auto& indexesAfterRemoval = iter->second;
699797
auto newIndex = indexesAfterRemoval[index];
700-
assert(newIndex < indexesAfterRemoval.size() ||
798+
assert(newIndex < IndexAnalysis(indexesAfterRemoval).newSize ||
701799
newIndex == RemovedField);
702800
return newIndex;
703801
}

0 commit comments

Comments
 (0)