Use sequence terminal, fix most remaining order-of-evaluation bugs
Changes the lowering for sequence expressions to use the new terminal. When converting to a ReactiveFunction, we convert these terminals into ReactiveSequenceValues, which nests the instructions and preserves order of evaluation in the output. The only catch is constant propagation — constant propagation breaks order-of-evaluation because it can effectively copy the final value of a sequence elsewhere, leaving the original sequence in the wrong place. I'll address that in a follow-up.
Joe Savona committed
Jun 4, 2023 at 21:26 UTC
1e0eb25cd07ef8a1aef6cf6acba04e2a0439e834
9 files changed
+72
-52
compiler/forget/src/HIR/BuildHIR.ts
+42
-17
@@ -1168,23 +1168,48 @@ function lowerExpression(
1168
const expr = exprPath as NodePath<t.SequenceExpression>;
1169
const exprLoc = expr.node.loc ?? GeneratedSource;
1170
1171
- let last: Place | null = null;
1172
- for (const item of expr.get("expressions")) {
1173
- last = lowerExpressionToTemporary(builder, item);
1174
- }
1175
- if (last === null) {
1176
- builder.errors.push({
1177
- reason: `(BuildHIR::lowerExpression) Expected SequenceExpression to have at least one expression`,
1178
- severity: ErrorSeverity.InvalidInput,
1179
- nodePath: expr,
1180
- });
1181
- return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
1182
- }
1183
- return {
1184
- kind: "LoadLocal", // TODO: LoadTemp
1185
- place: last,
1186
- loc: last.loc,
1187
- };
1171
+ const continuationBlock = builder.reserve(builder.currentBlockKind());
1172
+ const place = buildTemporaryPlace(builder, exprLoc);
1173
+
1174
+ const sequenceBlock = builder.enter("value", (_) => {
1175
+ let last: Place | null = null;
1176
+ for (const item of expr.get("expressions")) {
1177
+ last = lowerExpressionToTemporary(builder, item);
1178
+ }
1179
+ if (last === null) {
1180
+ builder.errors.push({
1181
+ reason: `(BuildHIR::lowerExpression) Expected SequenceExpression to have at least one expression`,
1182
+ severity: ErrorSeverity.InvalidInput,
1183
+ nodePath: expr,
1184
+ });
1185
+ } else {
1186
+ lowerValueToTemporary(builder, {
1187
+ kind: "StoreLocal",
1188
+ lvalue: { kind: InstructionKind.Const, place: { ...place } },
1189
+ value: last,
1190
+ loc: exprLoc,
1191
+ });
1192
+ }
1193
+ return {
1194
+ kind: "goto",
1195
+ id: makeInstructionId(0),
1196
+ block: continuationBlock.id,
1197
+ loc: exprLoc,
1198
+ variant: GotoVariant.Break,
1199
+ };
1200
+ });
1201
+
1202
+ builder.terminateWithContinuation(
1203
+ {
1204
+ kind: "sequence",
1205
+ block: sequenceBlock,
1206
+ fallthrough: continuationBlock.id,
1207
+ id: makeInstructionId(0),
1208
+ loc: exprLoc,
1209
+ },
1210
+ continuationBlock
1211
+ );
1212
+ return { kind: "LoadLocal", place, loc: place.loc };
1213
}
1214
case "ConditionalExpression": {
1215
const expr = exprPath as NodePath<t.ConditionalExpression>;
compiler/forget/src/HIR/MergeConsecutiveBlocks.ts
+1
-1
@@ -44,7 +44,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
44
"Expected predecessor %s to exist",
45
predecessorId
46
);
47
- if (predecessor.terminal.kind !== "goto") {
47
+ if (predecessor.terminal.kind !== "goto" || predecessor.kind !== "block") {
48
// The predecessor is not guaranteed to transfer control to this block,
49
// they aren't consecutive.
50
continue;
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+9
@@ -771,6 +771,15 @@ class Driver {
771
id: InstructionId;
772
} {
773
switch (terminal.kind) {
774
+ case "sequence": {
775
+ const block = this.visitValueBlock(terminal.block, terminal.loc);
776
+ return {
777
+ value: block.value,
778
+ place: block.place,
779
+ fallthrough: terminal.fallthrough,
780
+ id: terminal.id,
781
+ };
782
+ }
783
case "optional": {
784
const test = this.visitValueBlock(terminal.test, terminal.loc);
785
const testBlock = this.cx.ir.blocks.get(test.block)!;
compiler/forget/src/__tests__/fixtures/compiler/_bug.computed-call-evaluation-order.expect.md
+2
-5
@@ -46,11 +46,8 @@ function Component() {
46
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
47
x = { f: t1 };
48
49
- console.log("A");
50
- console.log("B");
51
- changeF(x);
52
- console.log("arg");
53
- x.f(1);
49
+ console.log("B"), "f";
50
+ (console.log("A"), x).f((changeF(x), console.log("arg"), 1));
51
$[2] = x;
52
} else {
53
x = $[2];
compiler/forget/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order-non-global.expect.md
+1
-2
@@ -36,8 +36,7 @@ function Component(props) {
36
37
T0 = Tag;
38
t1 = "\n ";
39
- Tag = props.alternateComponent;
40
- t2 = maybeMutate(maybeMutable);
39
+ t2 = ((Tag = props.alternateComponent), maybeMutate(maybeMutable));
40
$[0] = props.component;
41
$[1] = props.alternateComponent;
42
$[2] = Tag;
compiler/forget/src/__tests__/fixtures/compiler/jsx-tag-evaluation-order.expect.md
+10
-8
@@ -20,6 +20,8 @@ function Component(props) {
20
import { unstable_useMemoCache as useMemoCache } from "react";
21
function Component(props) {
22
const $ = useMemoCache(3);
23
+
24
+ const t1 = props.value;
25
let t0;
26
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
27
t0 = <HScroll />;
@@ -27,21 +29,21 @@ function Component(props) {
29
} else {
30
t0 = $[0];
31
}
30
- const c_1 = $[1] !== props.value;
31
- let t1;
32
+ const c_1 = $[1] !== t1;
33
+ let t2;
34
if (c_1) {
33
- t1 = (
35
+ t2 = (
36
<View>
35
- {props.value}
37
+ {t1}
38
{t0}
39
</View>
40
);
39
- $[1] = props.value;
40
- $[2] = t1;
41
+ $[1] = t1;
42
+ $[2] = t2;
43
} else {
42
- t1 = $[2];
44
+ t2 = $[2];
45
}
44
- return t1;
46
+ return t2;
47
}
48
49
```
compiler/forget/src/__tests__/fixtures/compiler/property-call-evaluation-order.expect.md
renamed
+1
-4
@@ -46,10 +46,7 @@ function Component() {
46
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
47
x = { f: t1 };
48
49
- console.log("A");
50
- changeF(x);
51
- console.log("arg");
52
- x.f(1);
49
+ (console.log("A"), x).f((changeF(x), console.log("arg"), 1));
50
$[2] = x;
51
} else {
52
x = $[2];
compiler/forget/src/__tests__/fixtures/compiler/property-call-evaluation-order.js
renamed
compiler/forget/src/__tests__/fixtures/compiler/sequence-expression.expect.md
+6
-15
@@ -19,25 +19,16 @@ function foo() {}
19
```javascript
20
import { unstable_useMemoCache as useMemoCache } from "react";
21
function sequence(props) {
22
- const $ = useMemoCache(2);
23
- Math.max(1, 2);
24
- let t0;
25
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26
- t0 = foo();
27
- $[0] = t0;
28
- } else {
29
- t0 = $[0];
30
- }
22
+ const $ = useMemoCache(1);
23
let x;
32
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
33
- x = t0;
24
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
+ x = (Math.max(1, 2), foo());
26
while ((foo(), true)) {
35
- foo();
36
- x = 2;
27
+ x = (foo(), 2);
28
}
38
- $[1] = x;
29
+ $[0] = x;
30
} else {
40
- x = $[1];
31
+ x = $[0];
32
}
33
return x;
34
}