Skip to content

Commit bd2e866

Browse files
authored
[Wasm GC] Fix precompute on GC data (#3810)
This fixes precomputation on GC after #3803 was too optimistic. The issue is subtle. Precompute will repeatedly evaluate expressions and propagate their values, flowing them around, and it ignores side effects when doing so. For example: (block ..side effect.. (i32.const 1) ) When we evaluate that we see there are side effects, but regardless of them we know the value flowing out is 1. So we can propagate that value, if it is assigned to a local and read elsewhere. This is not valid for GC because struct.new and array.new have a "side effect" that is noticeable in the result. Each time we call struct.new we get a new struct with a new address, which ref.eq can distinguish. So when this pass evaluates the same thing multiple times it will get a different result. Also, we can't precompute a struct.get even if we know the struct, not unless we know the reference has not escaped (where a call could modify it). To avoid all that, do not precompute references, aside from the trivially safe ones like nulls and function references (simple constants that are the same each time we evaluate the expression emitting them). precomputeExpression() had a minor bug which this fixes. It checked the type of the expression to see if we can create a constant for it, but really it should check the value - since (separate from this PR) we have no way to emit a "constant" for a struct etc. Also that only matters if replaceExpression is true, that is, if we are replacing with a constant; if we just want the value internally, we have no limit on that. Also add Literal support for comparing GC refs, which is used by ref.eq. Without that tiny fix the tests here crash. This adds a bunch of tests, many for corner cases that we don't handle (since the PR makes us not propagate GC references). But they should be helpful if/when we do, to avoid the mistakes in #3803
1 parent ac2a49b commit bd2e866

File tree

5 files changed

+531
-30
lines changed

5 files changed

+531
-30
lines changed

src/passes/Precompute.cpp

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ class PrecomputingExpressionRunner
8787
return ConstantExpressionRunner<
8888
PrecomputingExpressionRunner>::visitLocalGet(curr);
8989
}
90+
91+
// Heap data may be modified in ways we do not see. We would need escape
92+
// analysis to avoid that risk. For now, disallow all heap operations.
93+
// TODO: immutability might also be good enough
94+
Flow visitStructNew(StructNew* curr) { return Flow(NONCONSTANT_FLOW); }
95+
Flow visitStructGet(StructGet* curr) { return Flow(NONCONSTANT_FLOW); }
96+
Flow visitArrayNew(ArrayNew* curr) { return Flow(NONCONSTANT_FLOW); }
97+
Flow visitArrayGet(ArrayGet* curr) { return Flow(NONCONSTANT_FLOW); }
98+
Flow visitArrayLen(ArrayLen* curr) { return Flow(NONCONSTANT_FLOW); }
9099
};
91100

92101
struct Precompute
@@ -226,16 +235,21 @@ struct Precompute
226235
// Precompute an expression, returning a flow, which may be a constant
227236
// (that we can replace the expression with if replaceExpression is set).
228237
Flow precomputeExpression(Expression* curr, bool replaceExpression = true) {
229-
if (!canEmitConstantFor(curr->type)) {
230-
return Flow(NONCONSTANT_FLOW);
231-
}
238+
Flow flow;
232239
try {
233-
return PrecomputingExpressionRunner(
234-
getModule(), getValues, replaceExpression)
235-
.visit(curr);
240+
flow =
241+
PrecomputingExpressionRunner(getModule(), getValues, replaceExpression)
242+
.visit(curr);
236243
} catch (PrecomputingExpressionRunner::NonconstantException&) {
237244
return Flow(NONCONSTANT_FLOW);
238245
}
246+
// If we are replacing the expression, then the resulting value must be of
247+
// a type we can emit a constant for.
248+
if (!flow.breaking() && replaceExpression &&
249+
!canEmitConstantFor(flow.values)) {
250+
return Flow(NONCONSTANT_FLOW);
251+
}
252+
return flow;
239253
}
240254

241255
// Precomputes the value of an expression, as opposed to the expression
@@ -286,6 +300,17 @@ struct Precompute
286300
if (setValues[set].isConcrete()) {
287301
continue; // already known constant
288302
}
303+
// Precompute the value. Note that this executes the code from scratch
304+
// each time we reach this point, and so we need to be careful about
305+
// repeating side effects if those side effects are expressed *in the
306+
// value*. A case where that can happen is GC data (each struct.new
307+
// creates a new, unique struct, even if the data is equal), and so
308+
// PrecomputingExpressionRunner will return a nonconstant flow for all
309+
// GC heap operations.
310+
// (Other side effects are fine; if an expression does a call and we
311+
// somehow know the entire expression precomputes to a 42, then we can
312+
// propagate that 42 along to the users, regardless of whatever the call
313+
// did globally.)
289314
auto values = setValues[set] =
290315
precomputeValue(Properties::getFallthrough(
291316
set->value, getPassOptions(), getModule()->features));
@@ -360,19 +385,22 @@ struct Precompute
360385
if (value.isNull()) {
361386
return true;
362387
}
388+
return canEmitConstantFor(value.type);
389+
}
390+
391+
bool canEmitConstantFor(Type type) {
363392
// A function is fine to emit a constant for - we'll emit a RefFunc, which
364393
// is compact and immutable, so there can't be a problem.
365-
if (value.type.isFunction()) {
394+
if (type.isFunction()) {
366395
return true;
367396
}
368-
// All other reference types cannot be precomputed.
369-
if (value.type.isRef()) {
397+
// All other reference types cannot be precomputed. Even an immutable GC
398+
// reference is not currently something this pass can handle, as it will
399+
// evaluate and reevaluate code multiple times in e.g. optimizeLocals, see
400+
// the comment above.
401+
if (type.isRef()) {
370402
return false;
371403
}
372-
return canEmitConstantFor(value.type);
373-
}
374-
375-
bool canEmitConstantFor(Type type) {
376404
// For now, don't try to precompute an Rtt. TODO figure out when that would
377405
// be safe and useful.
378406
return !type.isRtt();

src/wasm/literal.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ bool Literal::operator==(const Literal& other) const {
350350
assert(func.is() && other.func.is());
351351
return func == other.func;
352352
}
353+
if (type.isData()) {
354+
return gcData == other.gcData;
355+
}
353356
// other non-null reference type literals cannot represent concrete values,
354357
// i.e. there is no concrete externref, anyref or eqref other than null.
355358
WASM_UNREACHABLE("unexpected type");

0 commit comments

Comments
 (0)