Merge remote-tracking branch 'origin/main' into gcrotr
diff --git a/src/ir/properties.cpp b/src/ir/properties.cpp
index b5d2df9..d52445b 100644
--- a/src/ir/properties.cpp
+++ b/src/ir/properties.cpp
@@ -36,4 +36,36 @@
return scanner.generative;
}
+Expression* getSingleDescendantWithEffects(Expression* curr, const PassOptions& options, Module& module) {
+ struct Scanner : public PostWalker<Scanner, UnifiedExpressionVisitor<Scanner>> {
+ const PassOptions& options;
+ Module& module;
+
+ Scanner(const PassOptions& options, Module& module) : options(options), module(module) {}
+
+ Expression* withEffects = nullptr;
+ bool multiple = false;
+
+ void visitExpression(Expression* curr) {
+ if (multiple) {
+ return;
+ }
+ // This handles simple patterns, but cannot ignore nested things that can
+ // be ignored as a whole, which the SimplifyGlobals readOnlyToWrite code
+ // might be able to.
+ ShallowEffectAnalyzer effects(options, module, curr);
+ if (effects.hasSideEffects() || effects.readsMutableGlobalState()) {
+ if (withEffects) {
+ multiple = true;
+ withEffects = nullptr;
+ } else {
+ withEffects = curr;
+ }
+ }
+ }
+ } scanner(options, module);
+ scanner.walk(curr);
+ return scanner.withEffects;
+}
+
} // namespace wasm::Properties
diff --git a/src/ir/properties.h b/src/ir/properties.h
index d2c5aff..f58be83 100644
--- a/src/ir/properties.h
+++ b/src/ir/properties.h
@@ -414,6 +414,8 @@
//
bool isGenerative(Expression* curr, FeatureSet features);
+Expression* getSingleDescendantWithEffects(Expression* curr, const PassOptions& options, Module& module);
+
} // namespace wasm::Properties
#endif // wasm_ir_properties_h
diff --git a/src/ir/struct-utils.h b/src/ir/struct-utils.h
index f686923..8470ecf 100644
--- a/src/ir/struct-utils.h
+++ b/src/ir/struct-utils.h
@@ -106,7 +106,7 @@
//
// * Note an expression written into a field.
//
-// void noteExpression(Expression* expr, HeapType type, Index index, T& info);
+// void noteWrite(Expression* expr, HeapType type, Index index, T& info);
//
// * Note a default value written during creation.
//
@@ -135,6 +135,11 @@
: functionNewInfos(functionNewInfos),
functionSetGetInfos(functionSetGetInfos) {}
+ // We queue reads here. Some of them may be ignored later if they are part of
+ // a copy or a readOnlyToWrite pattern (in which case we only emit that
+ // particular pattern for the entire thing).
+ std::unordered_set<StructGet*> reads;
+
void visitStructNew(StructNew* curr) {
auto type = curr->type;
if (type == Type::unreachable) {
@@ -150,7 +155,7 @@
static_cast<SubType*>(this)->noteDefault(
fields[i].type, heapType, i, infos[i]);
} else {
- noteExpressionOrCopy(curr->operands[i], heapType, i, infos[i]);
+ noteWriteOrCopy(curr->operands[i], heapType, i, infos[i]);
}
}
}
@@ -162,7 +167,7 @@
}
// Note a write to this field of the struct.
- noteExpressionOrCopy(curr->value,
+ noteWriteOrCopy(curr->value,
type.getHeapType(),
curr->index,
functionSetGetInfos[this->getFunction()]
@@ -175,16 +180,25 @@
return;
}
- auto heapType = type.getHeapType();
- auto index = curr->index;
- static_cast<SubType*>(this)->noteRead(
- heapType,
- index,
- functionSetGetInfos[this->getFunction()][heapType][index]);
+ reads.insert(curr);
+ }
+
+ void visitFunction(Function* curr) {
+ // "Flush" any reads that have not been removed (which they would have had
+ // they been part of a copy of readOnlyToWrite pattern).
+ for (auto* read : reads) {
+ auto type = read->ref->type;
+ auto heapType = type.getHeapType();
+ auto index = read->index;
+ static_cast<SubType*>(this)->noteRead(
+ heapType,
+ index,
+ functionSetGetInfos[this->getFunction()][heapType][index]);
+ }
}
void
- noteExpressionOrCopy(Expression* expr, HeapType type, Index index, T& info) {
+ noteWriteOrCopy(Expression* expr, HeapType type, Index index, T& info) {
// Look at the value falling through, if it has the exact same type
// (otherwise, we'd need to consider both the type actually written and the
// type of the fallthrough, somehow).
@@ -193,14 +207,48 @@
if (fallthrough->type == expr->type) {
expr = fallthrough;
}
- if (auto* get = expr->dynCast<StructGet>()) {
- if (get->index == index && get->ref->type != Type::unreachable &&
- get->ref->type.getHeapType() == type) {
- static_cast<SubType*>(this)->noteCopy(type, index, info);
+
+ // Check if it is a direct copy (a write of a read from the same field).
+ auto readsSameField = [&](Expression* curr) -> StructGet* {
+ if (auto* get = curr->dynCast<StructGet>()) {
+ if (get->index == index && get->ref->type != Type::unreachable &&
+ get->ref->type.getHeapType() == type) {
+ return get;
+ }
+ }
+ return nullptr;
+ };
+ if (auto* get = readsSameField(expr)) {
+ assert(reads.count(get));
+ reads.erase(get);
+ static_cast<SubType*>(this)->noteCopy(type, index, info);
+ return;
+ }
+
+ // Check if it is a read-only-to-write situation, that is, where we read the
+ // field, process that value, then write that result. This is similar to a
+ // copy that was already handled, but the value may not be identical; still,
+ // it has the property that the read value does not "escape" anywhere but
+ // back into this field. (See SimplifyGlobals for the readOnlyToWrite logic
+ // there that is similar to this. Perhaps we can share some code somehow
+ // eventually TODO For now, handle simple cases like incrementing a field
+ // that we see in practice in j2wasm output.)
+ if (auto* single = Properties::getSingleDescendantWithEffects(expr, this->getPassOptions(), *this->getModule())) {
+ if (auto* get = readsSameField(single)) {
+ assert(reads.count(get));
+ reads.erase(get);
+ static_cast<SubType*>(this)->noteReadOnlyToWrite(expr, type, index, info);
return;
}
}
- static_cast<SubType*>(this)->noteExpression(expr, type, index, info);
+
+ // Otherwise, note a general write.
+ static_cast<SubType*>(this)->noteWrite(expr, type, index, info);
+ }
+
+ void noteReadOnlyToWrite(Expression* expr, HeapType type, Index index, T& info) {
+ // By default call the same code as a general write.
+ static_cast<SubType*>(this)->noteWrite(expr, type, index, info);
}
FunctionStructValuesMap<T>& functionNewInfos;
diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp
index 340b3b9..a381dba 100644
--- a/src/passes/ConstantFieldPropagation.cpp
+++ b/src/passes/ConstantFieldPropagation.cpp
@@ -256,7 +256,7 @@
: StructUtils::StructScanner<PossibleConstantValues, PCVScanner>(
functionNewInfos, functionSetInfos) {}
- void noteExpression(Expression* expr,
+ void noteWrite(Expression* expr,
HeapType type,
Index index,
PossibleConstantValues& info) {
diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp
index e8cee2b..c2e368b 100644
--- a/src/passes/GlobalTypeOptimization.cpp
+++ b/src/passes/GlobalTypeOptimization.cpp
@@ -43,19 +43,26 @@
// Information about usage of a field.
struct FieldInfo {
bool hasWrite = false;
- bool hasRead = false;
+ // Reads noted here are *escaping* reads, since a read that is part of a
+ // copy of readOnlyToWrite pattern would not be recorded.
+ bool hasReads = false;
void noteWrite() { hasWrite = true; }
- void noteRead() { hasRead = true; }
-
+ void noteRead() {
+ hasReads = true;
+ }
+ void noteReadOnlyToWrite() {
+ // Do *not* mark either a read or a write. We can remove this field if all
+ // it has are readOnlyToWrite (or copy) operations.
+ }
bool combine(const FieldInfo& other) {
bool changed = false;
if (!hasWrite && other.hasWrite) {
hasWrite = true;
changed = true;
}
- if (!hasRead && other.hasRead) {
- hasRead = true;
+ if (!hasReads && other.hasReads) {
+ hasReads = true;
changed = true;
}
return changed;
@@ -74,7 +81,7 @@
: StructUtils::StructScanner<FieldInfo, FieldInfoScanner>(
functionNewInfos, functionSetGetInfos) {}
- void noteExpression(Expression* expr,
+ void noteWrite(Expression* expr,
HeapType type,
Index index,
FieldInfo& info) {
@@ -87,7 +94,14 @@
}
void noteCopy(HeapType type, Index index, FieldInfo& info) {
- info.noteWrite();
+ info.noteReadOnlyToWrite();
+ }
+
+ void noteReadOnlyToWrite(Expression* expr,
+ HeapType type,
+ Index index,
+ FieldInfo& info) {
+ info.noteReadOnlyToWrite();
}
void noteRead(HeapType type, Index index, FieldInfo& info) {
@@ -178,13 +192,13 @@
// Process removability. First, see if we can remove anything before we
// start to allocate info for that.
if (std::any_of(infos.begin(), infos.end(), [&](const FieldInfo& info) {
- return !info.hasRead;
+ return !info.hasReads;
})) {
auto& indexesAfterRemoval = indexesAfterRemovals[type];
indexesAfterRemoval.resize(fields.size());
Index skip = 0;
for (Index i = 0; i < fields.size(); i++) {
- if (infos[i].hasRead) {
+ if (infos[i].hasReads) {
indexesAfterRemoval[i] = i - skip;
} else {
indexesAfterRemoval[i] = RemovedField;
@@ -346,8 +360,26 @@
return;
}
- auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index);
- if (newIndex != RemovedField) {
+ auto heapType = curr->ref->type.getHeapType();
+ auto newIndex = getNewIndex(heapType, curr->index);
+
+ // Map to the new index if this is a struct.set that should remain, that
+ // is, if the field remains present, and if we are not making it
+ // immutable (if a struct.set exists and we are in fact making this
+ // field immutable, that means that all writes to it are just copies or
+ // read-only-to-write things, that is, the writes will not be observed
+ // and can be dropped).
+ bool keepSet = newIndex != RemovedField;
+ if (keepSet) {
+ auto immIter = parent.canBecomeImmutable.find(heapType);
+ if (immIter != parent.canBecomeImmutable.end()) {
+ auto& immutableVec = immIter->second;
+ if (curr->index < immutableVec.size() && immutableVec[curr->index]) {
+ keepSet = false;
+ }
+ }
+ }
+ if (keepSet) {
// Map to the new index.
curr->index = newIndex;
} else {
@@ -364,8 +396,16 @@
}
auto newIndex = getNewIndex(curr->ref->type.getHeapType(), curr->index);
- // We must not remove a field that is read from.
- assert(newIndex != RemovedField);
+ if (newIndex == RemovedField) {
+ // This is a read of the field, but the field has no escaping reads so
+ // we removed it. That means that the field is read only to be written
+ // back into the field, which means we can ignore the read and
+ // replace it with something.
+ Builder builder(*getModule());
+ // TODO: traps
+ replaceCurrent(builder.replaceWithIdenticalType(curr));
+ }
+
curr->index = newIndex;
}
diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp
index 74b23bb..b84fe09 100644
--- a/src/passes/TypeRefining.cpp
+++ b/src/passes/TypeRefining.cpp
@@ -50,7 +50,7 @@
: StructUtils::StructScanner<FieldInfo, FieldInfoScanner>(
functionNewInfos, functionSetGetInfos) {}
- void noteExpression(Expression* expr,
+ void noteWrite(Expression* expr,
HeapType type,
Index index,
FieldInfo& info) {
diff --git a/test/lit/passes/gto-removals.wast b/test/lit/passes/gto-removals.wast
index fc5ed90..da84dda 100644
--- a/test/lit/passes/gto-removals.wast
+++ b/test/lit/passes/gto-removals.wast
@@ -115,6 +115,66 @@
)
(module
+ ;; A copy does *not* keep a field from being removed, since the value does not
+ ;; escape and is therefore not observable.
+
+ ;; CHECK: (type $ref|$struct|_=>_none (func_subtype (param (ref $struct)) func))
+
+ ;; CHECK: (type $struct (struct_subtype data))
+ (type $struct (struct_subtype (field (mut funcref)) data))
+
+ ;; CHECK: (func $func (type $ref|$struct|_=>_none) (param $x (ref $struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.null func)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $func (param $x (ref $struct))
+ (struct.set $struct 0
+ (local.get $x)
+ (struct.get $struct 0
+ (local.get $x)
+ )
+ )
+ )
+)
+
+(module
+ ;; A read-only-to-write operation like an increment also does not keep a field
+ ;; from being removed.
+
+ ;; CHECK: (type $ref|$struct|_=>_none (func_subtype (param (ref $struct)) func))
+
+ ;; CHECK: (type $struct (struct_subtype data))
+ (type $struct (struct_subtype (field (mut i32)) data))
+
+ ;; CHECK: (func $func (type $ref|$struct|_=>_none) (param $x (ref $struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $func (param $x (ref $struct))
+ (struct.set $struct 0
+ (local.get $x)
+ (i32.add
+ (struct.get $struct 0
+ (local.get $x)
+ )
+ (i32.const 1)
+ )
+ )
+ )
+)
+
+(module
;; Different struct types with different situations: some fields are read,
;; some written, and some both. (Note that this also tests the interaction
;; of removing with the immutability inference that --gto does.)