Generalize helper for reorderable expressions

Adds a new helper method that we can use when processing expressions whose 
evaluation ordering may not be preserved. This was previously the case only for 
switch test case values, but we can use this for AssignmentPattern 
(destructuring default values) as well.
This commit is contained in:
Joe Savona
2023-03-27 10:34:11 -07:00
parent 80bdab7447
commit 6d434cc777
6 changed files with 95 additions and 55 deletions
+86 -46
View File
@@ -514,51 +514,7 @@ function lowerStatement(
});
let test: Place | null = null;
if (testExpr.node != null) {
switch (testExpr.node.type) {
case "Identifier":
case "StringLiteral":
case "NumericLiteral":
case "NullLiteral":
case "BooleanLiteral":
case "BigIntLiteral": {
// ok
break;
}
case "MemberExpression": {
// A common pattern is switch statements where the case test values are properties of a global,
// eg `case ProductOptions.Option: { ... }`
// We therefore allow expressions where the innermost object is a global identifier, and reject
// all other member expressions (for now).
const test = testExpr as NodePath<t.MemberExpression>;
let innerObject: NodePath<t.Expression> = test;
while (innerObject.isMemberExpression()) {
innerObject = innerObject.get("object");
}
if (
innerObject.isIdentifier() &&
builder.resolveIdentifier(innerObject) === null // null means global
) {
// This is a property/computed load from a global, that's safe to evaluate as a test expression
break;
}
builder.errors.push({
reason:
"(BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported",
severity: ErrorSeverity.Todo,
nodePath: testExpr,
});
break;
}
default: {
builder.errors.push({
reason:
"(BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported",
severity: ErrorSeverity.Todo,
nodePath: testExpr,
});
}
}
test = lowerExpressionToTemporary(
test = lowerReorderableExpression(
builder,
testExpr as NodePath<t.Expression>
);
@@ -1830,6 +1786,88 @@ function lowerExpression(
}
}
/**
* There are a few places where we do not preserve original evaluation ordering, such as switch case test values
* and default values in destructuring (assignment patterns). In these cases we allow simple expressions whose
* evaluation cannot be observed: primitives and arrays/objects whose values are also safely reorderable.
*/
function lowerReorderableExpression(
builder: HIRBuilder,
expr: NodePath<t.Expression>
): Place {
if (isReorderableExpression(builder, expr)) {
return lowerExpressionToTemporary(builder, expr);
} else {
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type '${expr.type}' cannot be safely reordered`,
severity: ErrorSeverity.Todo,
nodePath: expr,
});
return buildTemporaryPlace(builder, expr.node.loc ?? GeneratedSource);
}
}
function isReorderableExpression(
builder: HIRBuilder,
expr: NodePath<t.Expression>
): boolean {
switch (expr.node.type) {
case "Identifier":
case "RegExpLiteral":
case "StringLiteral":
case "NumericLiteral":
case "NullLiteral":
case "BooleanLiteral":
case "BigIntLiteral": {
return true;
}
case "ArrayExpression": {
return (expr as NodePath<t.ArrayExpression>)
.get("elements")
.every(
(element) =>
element.isExpression() && isReorderableExpression(builder, element)
);
}
case "ObjectExpression": {
return (expr as NodePath<t.ObjectExpression>)
.get("properties")
.every((property) => {
if (!property.isObjectProperty() || property.node.computed) {
return false;
}
const value = property.get("value");
return (
value.isExpression() && isReorderableExpression(builder, value)
);
});
}
case "MemberExpression": {
// A common pattern is switch statements where the case test values are properties of a global,
// eg `case ProductOptions.Option: { ... }`
// We therefore allow expressions where the innermost object is a global identifier, and reject
// all other member expressions (for now).
const test = expr as NodePath<t.MemberExpression>;
let innerObject: NodePath<t.Expression> = test;
while (innerObject.isMemberExpression()) {
innerObject = innerObject.get("object");
}
if (
innerObject.isIdentifier() &&
builder.resolveIdentifier(innerObject) === null // null means global
) {
// This is a property/computed load from a global, that's safe to reorder
return true;
} else {
return false;
}
}
default: {
return false;
}
}
}
function lowerArguments(
builder: HIRBuilder,
expr: Array<
@@ -2455,7 +2493,9 @@ function lowerAssignment(
const continuationBlock = builder.reserve(builder.currentBlockKind());
const consequent = builder.enter("value", () => {
const defaultValue = lowerExpressionToTemporary(
// Because we reorder evaluation, we restrict the allowed default values to those where
// evaluation order is unobservable
const defaultValue = lowerReorderableExpression(
builder,
lvalue.get("right")
);
@@ -3,7 +3,7 @@
```javascript
function Component(props) {
const [[x] = [foo()]] = props.y;
const [[x] = ["default"]] = props.y;
return x;
}
@@ -18,7 +18,7 @@ function Component(props) {
const c_0 = $[0] !== t0;
let t1;
if (c_0) {
t1 = t0 === undefined ? [foo()] : t0;
t1 = t0 === undefined ? ["default"] : t0;
$[0] = t0;
$[1] = t1;
} else {
@@ -1,4 +1,4 @@
function Component(props) {
const [[x] = [foo()]] = props.y;
const [[x] = ["default"]] = props.y;
return x;
}
@@ -5,7 +5,7 @@
function Component(props) {
let x;
if (props.cond) {
[[x] = [foo()]] = props.y;
[[x] = ["default"]] = props.y;
} else {
x = props.fallback;
}
@@ -25,7 +25,7 @@ function Component(props) {
const c_0 = $[0] !== t0;
let t1;
if (c_0) {
t1 = t0 === undefined ? [foo()] : t0;
t1 = t0 === undefined ? ["default"] : t0;
$[0] = t0;
$[1] = t1;
} else {
@@ -1,7 +1,7 @@
function Component(props) {
let x;
if (props.cond) {
[[x] = [foo()]] = props.y;
[[x] = ["default"]] = props.y;
} else {
x = props.fallback;
}
@@ -264,7 +264,7 @@ let moduleLocal = false;
48 | switch (i) {
49 | case 1 + 1: {
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
[ReactForget] TodoError: (BuildHIR::node.lowerReorderableExpression) Expression type 'MemberExpression' cannot be safely reordered
51 | case foo(): {
52 | }
> 53 | case x.y: {
@@ -273,7 +273,7 @@ let moduleLocal = false;
55 | default: {
56 | }
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
[ReactForget] TodoError: (BuildHIR::node.lowerReorderableExpression) Expression type 'CallExpression' cannot be safely reordered
49 | case 1 + 1: {
50 | }
> 51 | case foo(): {
@@ -282,7 +282,7 @@ let moduleLocal = false;
53 | case x.y: {
54 | }
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
[ReactForget] TodoError: (BuildHIR::node.lowerReorderableExpression) Expression type 'BinaryExpression' cannot be safely reordered
47 |
48 | switch (i) {
> 49 | case 1 + 1: {