@samitouri / QOS-React-2 / commits / 26cd435c7d

Support chained assignment expressions

Fixes codegen for chained assignment expressions. Previously each intermediate assignment would be generated independently _in addition_ to the final chained expression being emitted. We now emit a single chained expression, almost exactly matching the input except for expanding from `x += 1` into `x = x + 1`. There are two key changes: * Ensuring that assignment expressions always generate an lvalue, which is necessary for alias analysis to kick in, since it relies on the effect of the lvalue to know where to look for aliasing. * The above makes codegen think the entire assignment expression value is a temporary that can be emitted later, but that isn't true. The new PruneTemporaryLValue pass nulls out lvalues that are never read later, ensuring that codegen can eagerly emit the value instead of saving it as a temporary.

Joe Savona committed Jan 3, 2023 at 14:11 UTC 26cd435c7d45bed9075b48d6761f6f7874c7bdce
10 files changed +309 -160
compiler/forget/src/CompilerPipeline.ts
+4
@@ -23,6 +23,7 @@ import {
23 inferReactiveScopeVariables,
24 propagateScopeDependencies,
25 pruneUnusedLabels,
26 + pruneUnusedLValues,
27 pruneUnusedScopes,
28 renameVariables,
29 } from "./ReactiveScopes";
@@ -82,6 +83,9 @@ export default function (
83 pruneUnusedScopes(reactiveFunction);
84 logReactiveFunction("pruneUnusedScopes", reactiveFunction);
85
86 + pruneUnusedLValues(reactiveFunction);
87 + logReactiveFunction("pruneUnusedLValues", reactiveFunction);
88 +
89 renameVariables(reactiveFunction);
90 logReactiveFunction("renameVariables", reactiveFunction);
91
compiler/forget/src/HIR/BuildHIR.ts
+50 -118
@@ -649,13 +649,17 @@ function lowerStatement(
649 const stmt = stmtPath as NodePath<t.ExpressionStatement>;
650 const expression = stmt.get("expression");
651 const value = lowerExpression(builder, expression);
652 - if (expression.isAssignmentExpression()) {
653 - // instruction already emitted via lowerExpression()
652 + if (expression.isAssignmentExpression() && value.kind === "Identifier") {
653 + // already lowered to a place
654 return;
655 }
656 + const place = buildTemporaryPlace(
657 + builder,
658 + stmt.node.loc ?? GeneratedSource
659 + );
660 builder.push({
661 id: makeInstructionId(0),
658 - lvalue: null,
662 + lvalue: { kind: InstructionKind.Const, place },
663 value,
664 loc: stmt.node.loc ?? GeneratedSource,
665 });
@@ -885,13 +889,7 @@ function lowerExpression(
889 // tmp != null ? tmp : <right>
890 const left = lowerExpressionToPlace(builder, leftPath);
891
888 - const nullPlace: Place = {
889 - kind: "Identifier",
890 - identifier: builder.makeTemporary(),
891 - memberPath: null,
892 - effect: Effect.Unknown,
893 - loc: left.loc,
894 - };
892 + const nullPlace: Place = buildTemporaryPlace(builder, left.loc);
893 builder.push({
894 id: makeInstructionId(0),
895 value: {
@@ -903,13 +901,7 @@ function lowerExpression(
901 lvalue: { place: { ...nullPlace }, kind: InstructionKind.Const },
902 });
903
906 - const condPlace: Place = {
907 - kind: "Identifier",
908 - identifier: builder.makeTemporary(),
909 - memberPath: null,
910 - effect: Effect.Unknown,
911 - loc: left.loc,
912 - };
904 + const condPlace: Place = buildTemporaryPlace(builder, left.loc);
905 builder.push({
906 id: makeInstructionId(0),
907 lvalue: {
@@ -960,36 +952,23 @@ function lowerExpression(
952 }
953 case "MemberExpression": {
954 const leftExpr = left as NodePath<t.MemberExpression>;
963 - const object = lowerExpressionToPlace(
964 - builder,
965 - leftExpr.get("object")
966 - );
955 const property = leftExpr.get("property");
956 invariant(
957 property.isIdentifier(),
958 "Assignment expression to dynamic properties is not yet supported"
959 );
960 const right = lowerExpressionToPlace(builder, expr.get("right"));
973 - const place: Place = {
974 - kind: "Identifier",
975 - identifier: builder.makeTemporary(),
976 - memberPath: null,
977 - effect: Effect.Read,
978 - loc: exprLoc,
961 + const object = lowerExpressionToPlace(
962 + builder,
963 + leftExpr.get("object")
964 + );
965 + return {
966 + kind: "PropertyStore",
967 + object,
968 + property: property.node.name,
969 + value: right,
970 + loc: leftNode.loc ?? GeneratedSource,
971 };
980 - builder.push({
981 - id: makeInstructionId(0),
982 - lvalue: { place: { ...place }, kind: InstructionKind.Const },
983 - value: {
984 - kind: "PropertyStore",
985 - object,
986 - property: property.node.name,
987 - value: right,
988 - loc: leftNode.loc ?? GeneratedSource,
989 - },
990 - loc: exprLoc,
991 - });
992 - return place;
972 }
973 default: {
974 todoInvariant(
@@ -1056,13 +1035,10 @@ function lowerExpression(
1035 "Assignment expression to dynamic properties is not yet supported"
1036 );
1037 // Store the previous value to a temporary
1059 - const previousValuePlace: Place = {
1060 - kind: "Identifier",
1061 - identifier: builder.makeTemporary(),
1062 - memberPath: null,
1063 - effect: Effect.Read,
1064 - loc: exprLoc,
1065 - };
1038 + const previousValuePlace: Place = buildTemporaryPlace(
1039 + builder,
1040 + exprLoc
1041 + );
1042 builder.push({
1043 id: makeInstructionId(0),
1044 lvalue: {
@@ -1078,13 +1054,7 @@ function lowerExpression(
1054 loc: leftExpr.node.loc ?? GeneratedSource,
1055 });
1056 // Store the new value to a temporary
1081 - const newValuePlace: Place = {
1082 - kind: "Identifier",
1083 - identifier: builder.makeTemporary(),
1084 - memberPath: null,
1085 - effect: Effect.Read,
1086 - loc: exprLoc,
1087 - };
1057 + const newValuePlace: Place = buildTemporaryPlace(builder, exprLoc);
1058 builder.push({
1059 id: makeInstructionId(0),
1060 lvalue: {
@@ -1102,29 +1072,13 @@ function lowerExpression(
1072 });
1073
1074 // Save the result back to the property
1105 - const place: Place = {
1106 - kind: "Identifier",
1107 - identifier: builder.makeTemporary(),
1108 - memberPath: null,
1109 - effect: Effect.Read,
1110 - loc: exprLoc,
1111 - };
1112 - builder.push({
1113 - id: makeInstructionId(0),
1114 - lvalue: {
1115 - place: { ...place },
1116 - kind: InstructionKind.Const,
1117 - },
1118 - value: {
1119 - kind: "PropertyStore",
1120 - object: { ...object },
1121 - property: property.node.name,
1122 - value: { ...newValuePlace },
1123 - loc: leftExpr.node.loc ?? GeneratedSource,
1124 - },
1075 + return {
1076 + kind: "PropertyStore",
1077 + object: { ...object },
1078 + property: property.node.name,
1079 + value: { ...newValuePlace },
1080 loc: leftExpr.node.loc ?? GeneratedSource,
1126 - });
1127 - return place;
1081 + };
1082 }
1083 default: {
1084 invariant(
@@ -1149,13 +1103,7 @@ function lowerExpression(
1103 property: property.node.name,
1104 loc: exprLoc,
1105 };
1152 - const place: Place = {
1153 - kind: "Identifier",
1154 - identifier: builder.makeTemporary(),
1155 - memberPath: null,
1156 - effect: Effect.Read,
1157 - loc: exprLoc,
1158 - };
1106 + const place: Place = buildTemporaryPlace(builder, exprLoc);
1107 builder.push({
1108 id: makeInstructionId(0),
1109 lvalue: { place: { ...place }, kind: InstructionKind.Const },
@@ -1231,13 +1179,7 @@ function lowerConditional(
1179 consequent: () => InstructionValue,
1180 alternate: () => InstructionValue
1181 ): Place {
1234 - const place: Place = {
1235 - kind: "Identifier",
1236 - identifier: builder.makeTemporary(),
1237 - memberPath: null,
1238 - effect: Effect.Read,
1239 - loc,
1240 - };
1182 + const place: Place = buildTemporaryPlace(builder, loc);
1183 // Block for code following the if
1184 const continuationBlock = builder.reserve();
1185 // Block for the consequent (if the test is truthy)
@@ -1311,13 +1253,7 @@ function lowerJsxElementName(
1253 };
1254 return place;
1255 } else {
1314 - const place: Place = {
1315 - kind: "Identifier",
1316 - identifier: builder.makeTemporary(),
1317 - memberPath: null,
1318 - effect: Effect.Unknown,
1319 - loc: exprLoc,
1320 - };
1256 + const place: Place = buildTemporaryPlace(builder, exprLoc);
1257 builder.push({
1258 id: makeInstructionId(0),
1259 value: {
@@ -1351,13 +1287,7 @@ function lowerJsxElement(
1287 todoInvariant(expression.isExpression(), "handle empty expressions");
1288 return lowerExpressionToPlace(builder, expression);
1289 } else if (exprPath.isJSXText()) {
1354 - const place: Place = {
1355 - kind: "Identifier",
1356 - identifier: builder.makeTemporary(),
1357 - memberPath: null,
1358 - effect: Effect.Unknown,
1359 - loc: exprLoc,
1360 - };
1290 + const place: Place = buildTemporaryPlace(builder, exprLoc);
1291 builder.push({
1292 id: makeInstructionId(0),
1293 value: {
@@ -1374,13 +1304,7 @@ function lowerJsxElement(
1304 t.isJSXFragment(exprNode) || t.isJSXSpreadChild(exprNode),
1305 "Expected refinement to work"
1306 );
1377 - const place: Place = {
1378 - kind: "Identifier",
1379 - identifier: builder.makeTemporary(),
1380 - memberPath: null,
1381 - effect: Effect.Unknown,
1382 - loc: exprLoc,
1383 - };
1307 + const place: Place = buildTemporaryPlace(builder, exprLoc);
1308 builder.push({
1309 id: makeInstructionId(0),
1310 value: {
@@ -1404,13 +1328,7 @@ function lowerExpressionToPlace(
1328 return instr;
1329 }
1330 const exprLoc = exprPath.node.loc ?? GeneratedSource;
1407 - const place: Place = {
1408 - kind: "Identifier",
1409 - identifier: builder.makeTemporary(),
1410 - memberPath: null,
1411 - effect: Effect.Unknown,
1412 - loc: exprLoc,
1413 - };
1331 + const place: Place = buildTemporaryPlace(builder, exprLoc);
1332 builder.push({
1333 id: makeInstructionId(0),
1334 value: instr,
@@ -1496,6 +1414,20 @@ function lowerLVal(builder: HIRBuilder, exprPath: NodePath<t.LVal>): Place {
1414 }
1415 }
1416
1417 +/**
1418 + * Creates a temporary Identifier and Place referencing that identifier.
1419 + */
1420 +function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
1421 + const place: Place = {
1422 + kind: "Identifier",
1423 + identifier: builder.makeTemporary(),
1424 + memberPath: null,
1425 + effect: Effect.Unknown,
1426 + loc,
1427 + };
1428 + return place;
1429 +}
1430 +
1431 function lowerAssignment(
1432 builder: HIRBuilder,
1433 loc: SourceLocation,
compiler/forget/src/HIR/Codegen.ts
+1 -8
@@ -302,14 +302,7 @@ export function codegenInstruction(
302 if (instr.lvalue === null) {
303 return t.expressionStatement(value);
304 }
305 - if (instr.value.kind === "PropertyStore") {
306 - invariant(
307 - instr.lvalue.place.identifier.name === null,
308 - "Expected property stores to be lowered to a temporary"
309 - );
310 - temp.set(instr.lvalue.place.identifier.id, value);
311 - return t.expressionStatement(value);
312 - } else if (
305 + if (
306 instr.lvalue.place.memberPath === null &&
307 instr.lvalue.place.identifier.name === null
308 ) {
compiler/forget/src/ReactiveScopes/PruneTemporaryLValues.ts new
+41
@@ -0,0 +1,41 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 {
9 + Identifier,
10 + Instruction,
11 + InstructionKind,
12 + ReactiveFunction,
13 +} from "../HIR/HIR";
14 +import { visitFunction } from "./visitors";
15 +
16 +/**
17 + * Nulls out lvalues for temporary variables that are never accessed later. This only
18 + * nulls out the lvalue itself, it does not remove the corresponding instructions.
19 + */
20 +export function pruneTemporaryLValues(fn: ReactiveFunction): void {
21 + const lvalues = new Map<Identifier, Instruction>();
22 + visitFunction(fn, {
23 + visitInstruction: (instr) => {
24 + if (
25 + instr.lvalue !== null &&
26 + instr.lvalue.kind === InstructionKind.Const &&
27 + instr.lvalue.place.identifier.name === null
28 + ) {
29 + lvalues.set(instr.lvalue.place.identifier, instr);
30 + }
31 + },
32 + visitValue: (value) => {
33 + if (value.kind === "Identifier") {
34 + lvalues.delete(value.identifier);
35 + }
36 + },
37 + });
38 + for (const [, instr] of lvalues) {
39 + instr.lvalue = null;
40 + }
41 +}
compiler/forget/src/ReactiveScopes/index.ts
+1
@@ -12,6 +12,7 @@ export { inferReactiveScopes } from "./InferReactiveScopes";
12 export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
13 export { printReactiveFunction } from "./PrintReactiveFunction";
14 export { propagateScopeDependencies } from "./PropagateScopeDependencies";
15 +export { pruneTemporaryLValues as pruneUnusedLValues } from "./PruneTemporaryLValues";
16 export { pruneUnusedLabels } from "./PruneUnusedLabels";
17 export { pruneUnusedScopes } from "./PruneUnusedScopes";
18 export { renameVariables } from "./RenameVariables";
compiler/forget/src/ReactiveScopes/visitors.ts
+169 -1
@@ -5,9 +5,82 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { ReactiveBlock, ReactiveTerminal } from "../HIR/HIR";
8 +import {
9 + Instruction,
10 + InstructionValue,
11 + Place,
12 + ReactiveBlock,
13 + ReactiveFunction,
14 + ReactiveScope,
15 + ReactiveTerminal,
16 + ReactiveValueBlock,
17 +} from "../HIR/HIR";
18 +import { eachInstructionValueOperand } from "../HIR/visitors";
19 import { assertExhaustive } from "../Utils/utils";
20
21 +export function visitFunction(
22 + fn: ReactiveFunction,
23 + visitors: {
24 + visitValue?: (value: InstructionValue) => void;
25 + visitInstruction?: (instr: Instruction) => void;
26 + visitTerminal?: (terminal: ReactiveTerminal) => void;
27 + visitScope?: (scope: ReactiveScope) => void;
28 + }
29 +): void {
30 + const { visitValue, visitInstruction, visitTerminal, visitScope } = visitors;
31 + function visitBlock(block: ReactiveBlock): void {
32 + for (const item of block) {
33 + switch (item.kind) {
34 + case "instruction": {
35 + if (visitValue) {
36 + for (const operand of eachInstructionValueOperand(
37 + item.instruction.value
38 + )) {
39 + visitValue(operand);
40 + }
41 + }
42 + if (visitInstruction) {
43 + visitInstruction(item.instruction);
44 + }
45 + break;
46 + }
47 + case "terminal": {
48 + if (visitValue) {
49 + eachTerminalOperand(item.terminal, (operand) => {
50 + visitValue(operand);
51 + });
52 + }
53 + if (visitTerminal) {
54 + visitTerminal(item.terminal);
55 + }
56 + eachTerminalBlock(item.terminal, visitBlock, visitValueBlock);
57 + break;
58 + }
59 + case "scope": {
60 + if (visitScope) {
61 + visitScope(item.scope);
62 + }
63 + visitBlock(item.instructions);
64 + break;
65 + }
66 + default: {
67 + assertExhaustive(
68 + item,
69 + `Unexpected item kind '${(item as any).kind}'`
70 + );
71 + }
72 + }
73 + }
74 + }
75 + function visitValueBlock(block: ReactiveValueBlock): void {
76 + visitBlock(block.instructions);
77 + if (block.value !== null && visitValue) {
78 + visitValue(block.value);
79 + }
80 + }
81 + visitBlock(fn.body);
82 +}
83 +
84 export function mapTerminalBlocks(
85 terminal: ReactiveTerminal,
86 fn: (block: ReactiveBlock) => ReactiveBlock
@@ -50,3 +123,98 @@ export function mapTerminalBlocks(
123 }
124 }
125 }
126 +
127 +export function eachTerminalBlock(
128 + terminal: ReactiveTerminal,
129 + visitBlock: (block: ReactiveBlock) => void,
130 + visitValueBlock: (block: ReactiveValueBlock) => void
131 +): void {
132 + switch (terminal.kind) {
133 + case "break":
134 + case "continue":
135 + case "return":
136 + case "throw": {
137 + break;
138 + }
139 + case "for": {
140 + visitValueBlock(terminal.init);
141 + visitValueBlock(terminal.test);
142 + visitValueBlock(terminal.update);
143 + visitBlock(terminal.loop);
144 + break;
145 + }
146 + case "while": {
147 + visitValueBlock(terminal.test);
148 + visitBlock(terminal.loop);
149 + break;
150 + }
151 + case "if": {
152 + visitBlock(terminal.consequent);
153 + if (terminal.alternate !== null) {
154 + visitBlock(terminal.alternate);
155 + }
156 + break;
157 + }
158 + case "switch": {
159 + for (const case_ of terminal.cases) {
160 + if (case_.block !== undefined) {
161 + visitBlock(case_.block);
162 + }
163 + }
164 + break;
165 + }
166 + default: {
167 + assertExhaustive(
168 + terminal,
169 + `Unexpected terminal kind '${(terminal as any).kind}'`
170 + );
171 + }
172 + }
173 +}
174 +
175 +export function eachTerminalOperand(
176 + terminal: ReactiveTerminal,
177 + fn: (place: Place) => void
178 +): void {
179 + switch (terminal.kind) {
180 + case "break":
181 + case "continue": {
182 + break;
183 + }
184 + case "return": {
185 + if (terminal.value !== null) {
186 + fn(terminal.value);
187 + }
188 + break;
189 + }
190 + case "throw": {
191 + fn(terminal.value);
192 + break;
193 + }
194 + case "for": {
195 + break;
196 + }
197 + case "while": {
198 + break;
199 + }
200 + case "if": {
201 + fn(terminal.test);
202 + break;
203 + }
204 + case "switch": {
205 + fn(terminal.test);
206 + for (const case_ of terminal.cases) {
207 + if (case_.test !== null) {
208 + fn(case_.test);
209 + }
210 + }
211 + break;
212 + }
213 + default: {
214 + assertExhaustive(
215 + terminal,
216 + `Unexpected terminal kind '${(terminal as any).kind}'`
217 + );
218 + }
219 + }
220 +}
compiler/forget/src/__tests__/fixtures/hir/_bug_chained-assignment-expressions.expect.md deleted
-28
@@ -1,28 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function foo() {
6 - const x = { y: 0 };
7 - const y = { z: 0 };
8 - x.y += y.z *= 1;
9 -}
10 -
11 -```
12 -
13 -## Code
14 -
15 -```javascript
16 -function foo() {
17 - const x = {
18 - y: 0,
19 - };
20 - const y = {
21 - z: 0,
22 - };
23 - y.z = y.z * 1;
24 - x.y = x.y + (y.z = y.z * 1);
25 -}
26 -
27 -```
28 -
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/_bug_chained-assignment-expressions.js deleted
-5
@@ -1,5 +0,0 @@
1 -function foo() {
2 - const x = { y: 0 };
3 - const y = { z: 0 };
4 - x.y += y.z *= 1;
5 -}
compiler/forget/src/__tests__/fixtures/hir/chained-assignment-expressions.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo() {
6 + const x = { x: 0 };
7 + const y = { z: 0 };
8 + const z = { z: 0 };
9 + x.x += y.y *= 1;
10 + z.z += y.y *= x.x &= 3;
11 + return z;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +function foo() {
20 + const x = {
21 + x: 0,
22 + };
23 + const y = {
24 + z: 0,
25 + };
26 + const z = {
27 + z: 0,
28 + };
29 + x.x = x.x + (y.y = y.y * 1);
30 + z.z = z.z + (y.y = y.y * (x.x = x.x & 3));
31 + return z;
32 +}
33 +
34 +```
35 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/chained-assignment-expressions.js new
+8
@@ -0,0 +1,8 @@
1 +function foo() {
2 + const x = { x: 0 };
3 + const y = { z: 0 };
4 + const z = { z: 0 };
5 + x.x += y.y *= 1;
6 + z.z += y.y *= x.x &= 3;
7 + return z;
8 +}