Skip to content

Commit 6ea7fc2

Browse files
authored
[Custom Descriptors] Update Heap2Local (#7667)
When optimizing allocations of structs with descriptors, store the descriptor in a local the same way we store normal fields. Update ref.get_desc on optimized allocations to simply get the local holding the descriptor. Lower ref.cast_desc to unreachable when the optimized allocation flows in as the descriptor because it cannot have also been the descriptor on the cast value without being considered to have escaped. When the optimized allocation is itself the cast value, compare the local containing its descriptor to the target descriptor to determine whether the cast would have succeeded.
1 parent 2578b41 commit 6ea7fc2

File tree

3 files changed

+578
-62
lines changed

3 files changed

+578
-62
lines changed

scripts/test/fuzzing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
'remove-unused-types-descriptors.wast',
122122
'unsubtyping-desc.wast',
123123
'type-merging-desc.wast',
124+
'heap2local-desc.wast',
124125
# TODO: fix split_wast() on tricky escaping situations like a string ending
125126
# in \\" (the " is not escaped - there is an escaped \ before it)
126127
'string-lifting-section.wast',

src/passes/Heap2Local.cpp

Lines changed: 113 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -395,13 +395,23 @@ struct EscapeAnalyzer {
395395
// Whether the cast succeeds or fails, it does not escape.
396396
escapes = false;
397397

398-
// If the cast fails then the allocation is fully consumed and does not
399-
// flow any further (instead, we trap).
400-
if (!Type::isSubType(allocation->type, curr->type)) {
398+
if (curr->ref == child) {
399+
// If the cast fails then the allocation is fully consumed and does
400+
// not flow any further (instead, we trap).
401+
if (!Type::isSubType(allocation->type, curr->type)) {
402+
fullyConsumes = true;
403+
}
404+
} else {
405+
assert(curr->desc == child);
401406
fullyConsumes = true;
402407
}
403408
}
404409

410+
void visitRefGetDesc(RefGetDesc* curr) {
411+
escapes = false;
412+
fullyConsumes = true;
413+
}
414+
405415
// GC operations.
406416
void visitStructSet(StructSet* curr) {
407417
// The reference does not escape (but the value is stored to memory and
@@ -598,10 +608,14 @@ struct Struct2Local : PostWalker<Struct2Local> {
598608
: allocation(allocation), analyzer(analyzer), func(func), wasm(wasm),
599609
builder(wasm), fields(allocation->type.getHeapType().getStruct().fields) {
600610

601-
// Allocate locals to store the allocation's fields in.
611+
// Allocate locals to store the allocation's fields and descriptor in.
602612
for (auto field : fields) {
603613
localIndexes.push_back(builder.addVar(func, field.type));
604614
}
615+
if (allocation->descriptor) {
616+
localIndexes.push_back(
617+
builder.addVar(func, allocation->descriptor->type));
618+
}
605619

606620
// Replace the things we need to using the visit* methods.
607621
walk(func->body);
@@ -708,61 +722,54 @@ struct Struct2Local : PostWalker<Struct2Local> {
708722
// First, assign the initial values to the new locals.
709723
std::vector<Expression*> contents;
710724

711-
if (!allocation->isWithDefault()) {
712-
// We must assign the initial values to temp indexes, then copy them
713-
// over all at once. If instead we did set them as we go, then we might
714-
// hit a problem like this:
715-
//
716-
// (local.set X (new_X))
717-
// (local.set Y (block (result ..)
718-
// (.. (local.get X) ..) ;; returns new_X, wrongly
719-
// (new_Y)
720-
// )
721-
//
722-
// Note how we assign to the local X and use it during the assignment to
723-
// the local Y - but we should still see the old value of X, not new_X.
724-
// Temp locals X', Y' can ensure that:
725-
//
726-
// (local.set X' (new_X))
727-
// (local.set Y' (block (result ..)
728-
// (.. (local.get X) ..) ;; returns the proper, old X
729-
// (new_Y)
730-
// )
731-
// ..
732-
// (local.set X (local.get X'))
733-
// (local.set Y (local.get Y'))
734-
std::vector<Index> tempIndexes;
735-
725+
// We might be in a loop, so the locals representing the struct fields might
726+
// already have values. Furthermore, the computation of the new field values
727+
// might depend on the old field values. If we naively assign the new values
728+
// to the locals as they are computed, the computation of a later field may
729+
// use the new value of an earlier field where it should have used the old
730+
// value of the earlier field. To avoid this problem, we store all the
731+
// nontrivial new values in temp locals, and only once they have fully been
732+
// computed do we copy them into the locals representing the fields.
733+
std::vector<Index> tempIndexes;
734+
Index numTemps =
735+
(curr->isWithDefault() ? 0 : fields.size()) + bool(curr->descriptor);
736+
tempIndexes.reserve(numTemps);
737+
738+
// Create the temp variables.
739+
if (!curr->isWithDefault()) {
736740
for (auto field : fields) {
737741
tempIndexes.push_back(builder.addVar(func, field.type));
738742
}
743+
}
744+
if (curr->descriptor) {
745+
tempIndexes.push_back(builder.addVar(func, curr->descriptor->type));
746+
}
739747

740-
// Store the initial values into the temp locals.
741-
for (Index i = 0; i < tempIndexes.size(); i++) {
748+
// Store the initial values into the temp locals.
749+
if (!curr->isWithDefault()) {
750+
for (Index i = 0; i < fields.size(); i++) {
742751
contents.push_back(
743-
builder.makeLocalSet(tempIndexes[i], allocation->operands[i]));
744-
}
745-
746-
// Copy them to the normal ones.
747-
for (Index i = 0; i < tempIndexes.size(); i++) {
748-
auto* value = builder.makeLocalGet(tempIndexes[i], fields[i].type);
749-
contents.push_back(builder.makeLocalSet(localIndexes[i], value));
752+
builder.makeLocalSet(tempIndexes[i], curr->operands[i]));
750753
}
754+
}
755+
if (curr->descriptor) {
756+
contents.push_back(
757+
builder.makeLocalSet(tempIndexes[numTemps - 1], curr->descriptor));
758+
}
751759

752-
// TODO Check if the nondefault case does not increase code size in some
753-
// cases. A heap allocation that implicitly sets the default values
754-
// is smaller than multiple explicit settings of locals to
755-
// defaults.
756-
} else {
757-
// Set the default values.
758-
//
759-
// Note that we must assign the defaults because we might be in a loop,
760-
// that is, there might be a previous value.
761-
for (Index i = 0; i < localIndexes.size(); i++) {
762-
contents.push_back(builder.makeLocalSet(
763-
localIndexes[i],
764-
builder.makeConstantExpression(Literal::makeZero(fields[i].type))));
765-
}
760+
// Store the values into the locals representing the fields.
761+
for (Index i = 0; i < fields.size(); ++i) {
762+
auto* val =
763+
curr->isWithDefault()
764+
? builder.makeConstantExpression(Literal::makeZero(fields[i].type))
765+
: builder.makeLocalGet(tempIndexes[i], fields[i].type);
766+
contents.push_back(builder.makeLocalSet(localIndexes[i], val));
767+
}
768+
if (curr->descriptor) {
769+
auto* val =
770+
builder.makeLocalGet(tempIndexes[numTemps - 1], curr->descriptor->type);
771+
contents.push_back(
772+
builder.makeLocalSet(localIndexes[fields.size()], val));
766773
}
767774

768775
// Replace the allocation with a null reference. This changes the type
@@ -838,25 +845,69 @@ struct Struct2Local : PostWalker<Struct2Local> {
838845
return;
839846
}
840847

841-
// We know this RefCast receives our allocation, so we can see whether it
842-
// succeeds or fails.
843-
if (Type::isSubType(allocation->type, curr->type)) {
844-
// The cast succeeds, so it is a no-op, and we can skip it, since after we
845-
// remove the allocation it will not even be needed for validation.
846-
replaceCurrent(curr->ref);
848+
if (curr->desc) {
849+
// If we are doing a ref.cast_desc of the optimized allocation, but we
850+
// know it does not have a descriptor, then we know the cast must fail. We
851+
// also know the cast must fail if the optimized allocation flows in as
852+
// the descriptor, since it cannot possibly have been used in the
853+
// allocation of the cast value without having been considered to escape.
854+
if (!allocation->descriptor || analyzer.getInteraction(curr->desc) ==
855+
ParentChildInteraction::Flows) {
856+
// The allocation does not have a descriptor, so there is no way for the
857+
// cast to succeed.
858+
replaceCurrent(builder.blockify(builder.makeDrop(curr->ref),
859+
builder.makeDrop(curr->desc),
860+
builder.makeUnreachable()));
861+
} else {
862+
// The cast succeeds iff the optimized allocation's descriptor is the
863+
// same as the given descriptor and traps otherwise.
864+
auto type = allocation->descriptor->type;
865+
replaceCurrent(builder.blockify(
866+
builder.makeDrop(curr->ref),
867+
builder.makeIf(
868+
builder.makeRefEq(
869+
curr->desc,
870+
builder.makeLocalGet(localIndexes[fields.size()], type)),
871+
builder.makeRefNull(allocation->type.getHeapType()),
872+
builder.makeUnreachable())));
873+
}
847874
} else {
848-
// The cast fails, so this must trap.
849-
replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref),
850-
builder.makeUnreachable()));
875+
// We know this RefCast receives our allocation, so we can see whether it
876+
// succeeds or fails.
877+
if (Type::isSubType(allocation->type, curr->type)) {
878+
// The cast succeeds, so it is a no-op, and we can skip it, since after
879+
// we remove the allocation it will not even be needed for validation.
880+
replaceCurrent(curr->ref);
881+
} else {
882+
// The cast fails, so this must trap.
883+
replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref),
884+
builder.makeUnreachable()));
885+
}
851886
}
852887

853-
// Either way, we need to refinalize here (we either added an unreachable,
888+
// In any case, we need to refinalize here (we either added an unreachable,
854889
// or we replaced a cast with the value being cast, which may have a less-
855890
// refined type - it will not be used after we remove the allocation, but we
856891
// must still fix that up for validation).
857892
refinalize = true;
858893
}
859894

895+
void visitRefGetDesc(RefGetDesc* curr) {
896+
if (analyzer.getInteraction(curr) == ParentChildInteraction::None) {
897+
return;
898+
}
899+
900+
auto type = allocation->descriptor->type;
901+
if (type != curr->type) {
902+
// We know exactly the allocation that flows into this expression, so we
903+
// know the exact type of the descriptor. This type may be more precise
904+
// than the static type of this expression.
905+
refinalize = true;
906+
}
907+
auto* value = builder.makeLocalGet(localIndexes[fields.size()], type);
908+
replaceCurrent(builder.blockify(builder.makeDrop(curr->ref), value));
909+
}
910+
860911
void visitStructSet(StructSet* curr) {
861912
if (analyzer.getInteraction(curr) == ParentChildInteraction::None) {
862913
return;

0 commit comments

Comments
 (0)