@samitouri / QOS-React / commits / 1e0eb25cd0

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 }