@samitouri / QOS-React-1 / commits / 43677da7e4

Add instructions for UpdateExpression variants

Adds new instructions to accurately model UpdateExpression semantics, since `x++` is un-intuitively not the same as `x = x + 1`. There are a few different ways to model the combination of prefix/postfix and increment/decrement: * One instruction for all combinations of prefix/postfix and increment/decrement, eg 'UpdateExpression' * Instructions for Increment/Decrement, each with a property to distinguish prefix/postfix * Instructions for Prefix/Postfix, each with aproperty to distinguish increment/decrement. I chose the latter, `PrefixUpdate` and `PostfixUpdate`, because it keeps the number of new instructions minimal while keeping separate instructions for the most important distinction: whether the result of the instruction is the value before applying the operation or after. I'm open to suggestions about this though. A few quick notes: * Constant propagation is supported but only for numbers (we don't support bigint yet anyway) * LeaveSSA needs to know about these instructions since their presence requires making the original variable declaration Let, not Const. * EnterSSA mapped lvalues before rvalues, which is out of order but didn't previously matter. I just had to flip the order and everything worked.

Joe Savona committed Aug 7, 2023 at 13:50 UTC 43677da7e465d89ac33bc5603d6fba83c50f1bc1
22 files changed +348 -54
compiler/forget/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+30 -43
@@ -1753,56 +1753,43 @@ function lowerExpression(
1753 });
1754 return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
1755 }
1756 - if (expr.node.prefix) {
1757 - builder.errors.push({
1758 - reason: `(BuildHIR::lowerExpression) Handle prefix UpdateExpression`,
1759 - severity: ErrorSeverity.Todo,
1760 - loc: exprPath.node.loc ?? null,
1761 - suggestions: null,
1762 - });
1763 - return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
1764 - }
1765 - const primitiveTemp = lowerValueToTemporary(builder, {
1766 - kind: "Primitive",
1767 - value: 1,
1768 - loc: expr.node.loc ?? GeneratedSource,
1769 - });
1770 - const temp = buildTemporaryPlace(
1771 - builder,
1772 - expr.node.loc ?? GeneratedSource
1773 - );
1774 - const identifier = lowerIdentifierForAssignment(
1756 + const lvalue = lowerIdentifierForAssignment(
1757 builder,
1758 argument.node.loc ?? GeneratedSource,
1759 InstructionKind.Reassign,
1778 - argument as NodePath<t.Identifier>
1760 + argument
1761 );
1780 - if (identifier === null) {
1762 + if (lvalue === null) {
1763 + // lowerIdentifierForAssignment should have already reported an error if it returned null,
1764 + // we check here just in case
1765 + if (!builder.errors.hasErrors()) {
1766 + builder.errors.push({
1767 + reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
1768 + severity: ErrorSeverity.Invariant,
1769 + loc: exprLoc,
1770 + suggestions: null,
1771 + });
1772 + }
1773 return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
1774 }
1783 - builder.push({
1784 - id: makeInstructionId(0),
1785 - lvalue: { ...temp },
1786 - value: {
1787 - kind: "BinaryExpression",
1788 - operator: expr.node.operator === "++" ? "+" : "-",
1789 - left: { ...identifier },
1790 - right: { ...primitiveTemp },
1775 + const value = lowerIdentifier(builder, argument);
1776 + if (expr.node.prefix) {
1777 + return {
1778 + kind: "PrefixUpdate",
1779 + lvalue,
1780 + operation: expr.node.operator,
1781 + value,
1782 loc: exprLoc,
1792 - },
1793 - loc: exprLoc,
1794 - });
1795 - lowerValueToTemporary(builder, {
1796 - kind: getStoreKind(builder, argument),
1797 - lvalue: { place: { ...identifier }, kind: InstructionKind.Reassign },
1798 - value: { ...temp },
1799 - loc: exprLoc,
1800 - });
1801 - return {
1802 - kind: "LoadLocal",
1803 - place: { ...identifier },
1804 - loc: exprLoc,
1805 - };
1783 + };
1784 + } else {
1785 + return {
1786 + kind: "PostfixUpdate",
1787 + lvalue,
1788 + operation: expr.node.operator,
1789 + value,
1790 + loc: exprLoc,
1791 + };
1792 + }
1793 }
1794 case "RegExpLiteral": {
1795 let expr = exprPath as NodePath<t.RegExpLiteral>;
compiler/forget/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+20
@@ -736,6 +736,26 @@ export type InstructionValue =
736 value: Place; // the collection
737 loc: SourceLocation;
738 }
739 + // Models a prefix update expression such as --x or ++y
740 + // This instructions increments or decrements the <lvalue>
741 + // but evaluates to the value of <value> prior to the update.
742 + | {
743 + kind: "PrefixUpdate";
744 + lvalue: Place;
745 + operation: t.UpdateExpression["operator"];
746 + value: Place;
747 + loc: SourceLocation;
748 + }
749 + // Models a postfix update expression such as x-- or y++
750 + // This instructions increments or decrements the <lvalue>
751 + // and evaluates to the value after the update
752 + | {
753 + kind: "PostfixUpdate";
754 + lvalue: Place;
755 + operation: t.UpdateExpression["operator"];
756 + value: Place;
757 + loc: SourceLocation;
758 + }
759 // `debugger` statement
760 | { kind: "Debugger"; loc: SourceLocation }
761 /**
compiler/forget/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+12
@@ -513,6 +513,18 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
513 value = `Debugger`;
514 break;
515 }
516 + case "PostfixUpdate": {
517 + value = `PostfixUpdate ${printPlace(instrValue.lvalue)} = ${printPlace(
518 + instrValue.value
519 + )} ${instrValue.operation}`;
520 + break;
521 + }
522 + case "PrefixUpdate": {
523 + value = `PrefixUpdate ${printPlace(instrValue.lvalue)} = ${
524 + instrValue.operation
525 + } ${printPlace(instrValue.value)}`;
526 + break;
527 + }
528 default: {
529 assertExhaustive(
530 instrValue,
compiler/forget/packages/babel-plugin-react-forget/src/HIR/visitors.ts
+20
@@ -35,6 +35,11 @@ export function* eachInstructionLValue(
35 yield* eachPatternOperand(instr.value.lvalue.pattern);
36 break;
37 }
38 + case "PostfixUpdate":
39 + case "PrefixUpdate": {
40 + yield instr.value.lvalue;
41 + break;
42 + }
43 }
44 }
45
@@ -188,6 +193,11 @@ export function* eachInstructionValueOperand(
193 yield instrValue.value;
194 break;
195 }
196 + case "PostfixUpdate":
197 + case "PrefixUpdate": {
198 + yield instrValue.value;
199 + break;
200 + }
201 case "Debugger":
202 case "RegExpLiteral":
203 case "LoadGlobal":
@@ -305,6 +315,11 @@ export function mapInstructionLValues(
315 mapPatternOperands(instr.value.lvalue.pattern, fn);
316 break;
317 }
318 + case "PostfixUpdate":
319 + case "PrefixUpdate": {
320 + instr.value.lvalue = fn(instr.value.lvalue);
321 + break;
322 + }
323 }
324 if (instr.lvalue !== null) {
325 instr.lvalue = fn(instr.lvalue);
@@ -463,6 +478,11 @@ export function mapInstructionOperands(
478 instrValue.value = fn(instrValue.value);
479 break;
480 }
481 + case "PostfixUpdate":
482 + case "PrefixUpdate": {
483 + instrValue.value = fn(instrValue.value);
484 + break;
485 + }
486 case "Debugger":
487 case "RegExpLiteral":
488 case "LoadGlobal":
compiler/forget/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+20
@@ -933,6 +933,26 @@ function inferBlock(
933 state.define(instrValue.lvalue.place, instrValue);
934 continue;
935 }
936 + case "PostfixUpdate":
937 + case "PrefixUpdate": {
938 + const effect =
939 + state.isDefined(instrValue.lvalue) &&
940 + state.kind(instrValue.lvalue) === ValueKind.Context
941 + ? Effect.ConditionallyMutate
942 + : Effect.Capture;
943 + state.reference(instrValue.value, effect);
944 +
945 + const lvalue = instr.lvalue;
946 + state.alias(lvalue, instrValue.value);
947 + lvalue.effect = Effect.Store;
948 + state.alias(instrValue.lvalue, instrValue.value);
949 + // NOTE: *not* using state.reference since this is an assignment.
950 + // reference() checks if the effect is valid given the value kind,
951 + // but here the previous value kind doesn't matter since we are
952 + // replacing it
953 + instrValue.lvalue.effect = Effect.Store;
954 + continue;
955 + }
956 case "StoreLocal": {
957 const effect =
958 state.isDefined(instrValue.lvalue.place) &&
compiler/forget/packages/babel-plugin-react-forget/src/Optimization/ConstantPropagation.ts
+39
@@ -237,6 +237,45 @@ function evaluateInstruction(
237 }
238 return null;
239 }
240 + case "PostfixUpdate": {
241 + const previous = read(constants, value.value);
242 + if (
243 + previous !== null &&
244 + previous.kind === "Primitive" &&
245 + typeof previous.value === "number"
246 + ) {
247 + const next =
248 + value.operation === "++" ? previous.value + 1 : previous.value - 1;
249 + // Store the updated value
250 + constants.set(value.lvalue.identifier.id, {
251 + kind: "Primitive",
252 + value: next,
253 + loc: value.loc,
254 + });
255 + // But return the value prior to the update
256 + return previous;
257 + }
258 + return null;
259 + }
260 + case "PrefixUpdate": {
261 + const previous = read(constants, value.value);
262 + if (
263 + previous !== null &&
264 + previous.kind === "Primitive" &&
265 + typeof previous.value === "number"
266 + ) {
267 + const next: Primitive = {
268 + kind: "Primitive",
269 + value:
270 + value.operation === "++" ? previous.value + 1 : previous.value - 1,
271 + loc: value.loc,
272 + };
273 + // Store and return the updated value
274 + constants.set(value.lvalue.identifier.id, next);
275 + return next;
276 + }
277 + return null;
278 + }
279 case "BinaryExpression": {
280 const lhsValue = read(constants, value.left);
281 const rhsValue = read(constants, value.right);
compiler/forget/packages/babel-plugin-react-forget/src/Optimization/DeadCodeElimination.ts
+5
@@ -190,6 +190,11 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
190 }
191 return true;
192 }
193 + case "PostfixUpdate":
194 + case "PrefixUpdate": {
195 + // Updates are pruneable only if the identifier being stored to is never read later
196 + return !state.used(value.lvalue.identifier);
197 + }
198 case "Debugger": {
199 // explicitly retain debugger statements to not break debugging workflows
200 return false;
compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+16
@@ -1125,6 +1125,22 @@ function codegenInstructionValue(
1125 value = codegenPlace(cx, instrValue.value);
1126 break;
1127 }
1128 + case "PostfixUpdate": {
1129 + value = t.updateExpression(
1130 + instrValue.operation,
1131 + codegenPlace(cx, instrValue.lvalue),
1132 + false
1133 + );
1134 + break;
1135 + }
1136 + case "PrefixUpdate": {
1137 + value = t.updateExpression(
1138 + instrValue.operation,
1139 + codegenPlace(cx, instrValue.lvalue),
1140 + true
1141 + );
1142 + break;
1143 + }
1144 case "Debugger":
1145 case "DeclareLocal":
1146 case "DeclareContext":
compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+2
@@ -223,6 +223,8 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
223 case "Destructure": {
224 return doesPatternContainSpreadElement(value.lvalue.pattern);
225 }
226 + case "PostfixUpdate":
227 + case "PrefixUpdate":
228 case "Await":
229 case "DeclareLocal":
230 case "DeclareContext":
compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+2
@@ -430,6 +430,8 @@ function computeMemoizationInputs(
430 rvalues: value.children,
431 };
432 }
433 + case "PrefixUpdate":
434 + case "PostfixUpdate":
435 case "Debugger":
436 case "ComputedDelete":
437 case "PropertyDelete":
compiler/forget/packages/babel-plugin-react-forget/src/SSA/EnterSSA.ts
+1 -1
@@ -264,8 +264,8 @@ function enterSSAImpl(
264 }
265
266 for (const instr of block.instructions) {
267 - mapInstructionLValues(instr, (lvalue) => builder.definePlace(lvalue));
267 mapInstructionOperands(instr, (place) => builder.getPlace(place));
268 + mapInstructionLValues(instr, (lvalue) => builder.definePlace(lvalue));
269
270 if (
271 instr.value.kind === "FunctionExpression" &&
compiler/forget/packages/babel-plugin-react-forget/src/SSA/LeaveSSA.ts
+18
@@ -142,6 +142,24 @@ export function leaveSSA(fn: HIRFunction): void {
142 place: value.lvalue.place,
143 });
144 }
145 + } else if (
146 + value.kind === "PrefixUpdate" ||
147 + value.kind === "PostfixUpdate"
148 + ) {
149 + CompilerError.invariant(value.lvalue.identifier.name !== null, {
150 + reason: `Expected update expression to be applied to a named variable`,
151 + description: null,
152 + loc: value.lvalue.loc,
153 + suggestions: null,
154 + });
155 + const originalLVal = declarations.get(value.lvalue.identifier.name);
156 + CompilerError.invariant(originalLVal !== undefined, {
157 + reason: `Expected update expression to be applied to a previously defined variable`,
158 + description: null,
159 + loc: value.lvalue.loc,
160 + suggestions: null,
161 + });
162 + originalLVal.lvalue.kind = InstructionKind.Let;
163 } else if (value.kind === "StoreLocal") {
164 if (value.lvalue.place.identifier.name != null) {
165 const originalLVal = declarations.get(
compiler/forget/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts
+8
@@ -157,6 +157,14 @@ function* generateInstructionTypes(
157 break;
158 }
159
160 + case "PostfixUpdate":
161 + case "PrefixUpdate": {
162 + yield equation(value.value.identifier.type, { kind: "Primitive" });
163 + yield equation(value.lvalue.identifier.type, { kind: "Primitive" });
164 + yield equation(left, { kind: "Primitive" });
165 + break;
166 + }
167 +
168 case "LoadGlobal": {
169 const globalType = env.getGlobalDeclaration(value.name);
170 if (globalType) {
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/update-expressions.e2e.js new
+48
@@ -0,0 +1,48 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { render, screen, fireEvent } from "@testing-library/react";
9 +import * as React from "react";
10 +import { expectLogsAndClear, log } from "./expectLogs";
11 +
12 +function Counter(props) {
13 + let value = props.value;
14 + let a = value++;
15 + expect(a).toBe(props.value); // postfix
16 + let b = ++value;
17 + expect(b).toBe(props.value + 2); // previous postfix operation + prefix operation
18 + let c = ++value;
19 + expect(c).toBe(props.value + 3);
20 + let d = value--;
21 + expect(d).toBe(props.value + 3);
22 + let e = --value;
23 + expect(e).toBe(props.value + 1);
24 + let f = --value;
25 + expect(f).toBe(props.value);
26 + expect(value).toBe(props.value);
27 + return <span>{value}</span>;
28 +}
29 +
30 +test("use-state", async () => {
31 + const { asFragment, rerender } = render(<Counter value={0} />);
32 + expect(asFragment()).toMatchInlineSnapshot(`
33 + <DocumentFragment>
34 + <span>
35 + 0
36 + </span>
37 + </DocumentFragment>
38 + `);
39 +
40 + rerender(<Counter value={1} />);
41 + expect(asFragment()).toMatchInlineSnapshot(`
42 + <DocumentFragment>
43 + <span>
44 + 1
45 + </span>
46 + </DocumentFragment>
47 + `);
48 +});
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/dce-loop.expect.md
+1 -1
@@ -20,7 +20,7 @@ function foo(props) {
20 function foo(props) {
21 let y = 0;
22 while (y < props.max) {
23 - y = y + 1;
23 + y++;
24 }
25 return y;
26 }
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-kitchensink.expect.md
-4
@@ -109,10 +109,6 @@ let moduleLocal = false;
109
110 [ReactForget] Todo: (BuildHIR::lowerStatement) Handle ForInStatement statements (43:44)
111
112 -[ReactForget] Todo: (BuildHIR::lowerExpression) Handle prefix UpdateExpression (47:47)
113 -
114 -[ReactForget] Todo: (BuildHIR::lowerExpression) Handle prefix UpdateExpression (48:48)
115 -
112 [ReactForget] Todo: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument (49:49)
113
114 [ReactForget] Todo: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument (50:50)
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-for.expect.md
+1 -1
@@ -17,7 +17,7 @@ function foo() {
17 ```javascript
18 function foo() {
19 let x = 1;
20 - for (let i = 0; i < 10; i = i + 1, i) {
20 + for (let i = 0; i < 10; i++) {
21 x = x + 1;
22 }
23 return x;
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-constant-propagation.expect.md new
+33
@@ -0,0 +1,33 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + let a = 0;
7 + const b = a++;
8 + const c = ++a;
9 + const d = a--;
10 + const e = --a;
11 + return { a, b, c, d, e };
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { unstable_useMemoCache as useMemoCache } from "react";
20 +function Component() {
21 + const $ = useMemoCache(1);
22 + let t0;
23 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
24 + t0 = { a: 0, b: 0, c: 2, d: 2, e: 0 };
25 + $[0] = t0;
26 + } else {
27 + t0 = $[0];
28 + }
29 + return t0;
30 +}
31 +
32 +```
33 +
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-constant-propagation.js new
+8
@@ -0,0 +1,8 @@
1 +function Component() {
2 + let a = 0;
3 + const b = a++;
4 + const c = ++a;
5 + const d = a--;
6 + const e = --a;
7 + return { a, b, c, d, e };
8 +}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-in-sequence.expect.md new
+51
@@ -0,0 +1,51 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @debug
6 +function Component(props) {
7 + let a = props.x;
8 + let b;
9 + let c;
10 + let d;
11 + if (props.cond) {
12 + d = ((b = a), a++, (c = a), ++a);
13 + }
14 + return [a, b, c, d];
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
23 +function Component(props) {
24 + const $ = useMemoCache(5);
25 + let a = props.x;
26 + let b;
27 + let c;
28 + let d;
29 + if (props.cond) {
30 + d = ((b = a), a++, (c = a), ++a);
31 + }
32 + const c_0 = $[0] !== a;
33 + const c_1 = $[1] !== b;
34 + const c_2 = $[2] !== c;
35 + const c_3 = $[3] !== d;
36 + let t0;
37 + if (c_0 || c_1 || c_2 || c_3) {
38 + t0 = [a, b, c, d];
39 + $[0] = a;
40 + $[1] = b;
41 + $[2] = c;
42 + $[3] = d;
43 + $[4] = t0;
44 + } else {
45 + t0 = $[4];
46 + }
47 + return t0;
48 +}
49 +
50 +```
51 +
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-in-sequence.js new
+11
@@ -0,0 +1,11 @@
1 +// @debug
2 +function Component(props) {
3 + let a = props.x;
4 + let b;
5 + let c;
6 + let d;
7 + if (props.cond) {
8 + d = ((b = a), a++, (c = a), ++a);
9 + }
10 + return [a, b, c, d];
11 +}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression.expect.md
+2 -4
@@ -18,10 +18,8 @@ import { unstable_useMemoCache as useMemoCache } from "react";
18 function foo(props) {
19 const $ = useMemoCache(4);
20 let x = props.x;
21 - x = x + 1;
22 - const y = x;
23 - x = x - 1;
24 - const z = x;
21 + const y = x++;
22 + const z = x--;
23 const c_0 = $[0] !== x;
24 const c_1 = $[1] !== y;
25 const c_2 = $[2] !== z;