@samitouri / QOS-React-1 / commits / d7167c3505

[compiler] Implement support for hoisted and recursive functions

Summary: Introduces a new binding kind for functions that allows them to be hoisted. Also has the result of causing all nested function declarations to be outputted as function declarations, not as let bindings. ghstack-source-id: fa40d4909fb3d30c23691e36510ebb3c3cc41053 Pull Request resolved: https://github.com/facebook/react/pull/30922

Mike Vitousek committed Sep 16, 2024 at 11:12 UTC d7167c35059bc6a0ad84eb34e65b3b66328d5dd8
14 files changed +234 -139
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+16 -14
@@ -420,7 +420,19 @@ function lowerStatement(
420 // Already hoisted
421 continue;
422 }
423 - if (!binding.path.isVariableDeclarator()) {
423 +
424 + let kind:
425 + | InstructionKind.Let
426 + | InstructionKind.HoistedConst
427 + | InstructionKind.HoistedLet
428 + | InstructionKind.HoistedFunction;
429 + if (binding.kind === 'const' || binding.kind === 'var') {
430 + kind = InstructionKind.HoistedConst;
431 + } else if (binding.kind === 'let') {
432 + kind = InstructionKind.HoistedLet;
433 + } else if (binding.path.isFunctionDeclaration()) {
434 + kind = InstructionKind.HoistedFunction;
435 + } else if (!binding.path.isVariableDeclarator()) {
436 builder.errors.push({
437 severity: ErrorSeverity.Todo,
438 reason: 'Unsupported declaration type for hoisting',
@@ -429,11 +441,7 @@ function lowerStatement(
441 loc: id.parentPath.node.loc ?? GeneratedSource,
442 });
443 continue;
432 - } else if (
433 - binding.kind !== 'const' &&
434 - binding.kind !== 'var' &&
435 - binding.kind !== 'let'
436 - ) {
444 + } else {
445 builder.errors.push({
446 severity: ErrorSeverity.Todo,
447 reason: 'Handle non-const declarations for hoisting',
@@ -443,6 +451,7 @@ function lowerStatement(
451 });
452 continue;
453 }
454 +
455 const identifier = builder.resolveIdentifier(id);
456 CompilerError.invariant(identifier.kind === 'Identifier', {
457 reason:
@@ -456,13 +465,6 @@ function lowerStatement(
465 reactive: false,
466 loc: id.node.loc ?? GeneratedSource,
467 };
459 - const kind =
460 - // Avoid double errors on var declarations, which we do not plan to support anyways
461 - binding.kind === 'const' || binding.kind === 'var'
462 - ? InstructionKind.HoistedConst
463 - : binding.kind === 'let'
464 - ? InstructionKind.HoistedLet
465 - : assertExhaustive(binding.kind, 'Unexpected binding kind');
468 lowerValueToTemporary(builder, {
469 kind: 'DeclareContext',
470 lvalue: {
@@ -999,7 +1001,7 @@ function lowerStatement(
1001 lowerAssignment(
1002 builder,
1003 stmt.node.loc ?? GeneratedSource,
1002 - InstructionKind.Let,
1004 + InstructionKind.Function,
1005 id,
1006 fn,
1007 'Assignment',
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+5 -1
@@ -746,6 +746,9 @@ export enum InstructionKind {
746
747 // hoisted const declarations
748 HoistedLet = 'HoistedLet',
749 +
750 + HoistedFunction = 'HoistedFunction',
751 + Function = 'Function',
752 }
753
754 function _staticInvariantInstructionValueHasLocation(
@@ -865,7 +868,8 @@ export type InstructionValue =
868 kind:
869 | InstructionKind.Let
870 | InstructionKind.HoistedConst
868 - | InstructionKind.HoistedLet;
871 + | InstructionKind.HoistedLet
872 + | InstructionKind.HoistedFunction;
873 place: Place;
874 };
875 loc: SourceLocation;
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+6
@@ -765,6 +765,12 @@ export function printLValue(lval: LValue): string {
765 case InstructionKind.HoistedLet: {
766 return `HoistedLet ${lvalue}$`;
767 }
768 + case InstructionKind.Function: {
769 + return `Function ${lvalue}$`;
770 + }
771 + case InstructionKind.HoistedFunction: {
772 + return `HoistedFunction ${lvalue}$`;
773 + }
774 default: {
775 assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
776 }
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+40 -45
@@ -981,22 +981,12 @@ function codegenTerminal(
981 suggestions: null,
982 });
983 case InstructionKind.Catch:
984 - CompilerError.invariant(false, {
985 - reason: 'Unexpected catch variable as for..in collection',
986 - description: null,
987 - loc: iterableItem.loc,
988 - suggestions: null,
989 - });
984 case InstructionKind.HoistedConst:
991 - CompilerError.invariant(false, {
992 - reason: 'Unexpected HoistedConst variable in for..in collection',
993 - description: null,
994 - loc: iterableItem.loc,
995 - suggestions: null,
996 - });
985 case InstructionKind.HoistedLet:
986 + case InstructionKind.HoistedFunction:
987 + case InstructionKind.Function:
988 CompilerError.invariant(false, {
999 - reason: 'Unexpected HoistedLet variable in for..in collection',
989 + reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
990 description: null,
991 loc: iterableItem.loc,
992 suggestions: null,
@@ -1075,30 +1065,13 @@ function codegenTerminal(
1065 varDeclKind = 'let' as const;
1066 break;
1067 case InstructionKind.Reassign:
1078 - CompilerError.invariant(false, {
1079 - reason:
1080 - 'Destructure should never be Reassign as it would be an Object/ArrayPattern',
1081 - description: null,
1082 - loc: iterableItem.loc,
1083 - suggestions: null,
1084 - });
1068 case InstructionKind.Catch:
1086 - CompilerError.invariant(false, {
1087 - reason: 'Unexpected catch variable as for..of collection',
1088 - description: null,
1089 - loc: iterableItem.loc,
1090 - suggestions: null,
1091 - });
1069 case InstructionKind.HoistedConst:
1093 - CompilerError.invariant(false, {
1094 - reason: 'Unexpected HoistedConst variable in for..of collection',
1095 - description: null,
1096 - loc: iterableItem.loc,
1097 - suggestions: null,
1098 - });
1070 case InstructionKind.HoistedLet:
1071 + case InstructionKind.HoistedFunction:
1072 + case InstructionKind.Function:
1073 CompilerError.invariant(false, {
1101 - reason: 'Unexpected HoistedLet variable in for..of collection',
1074 + reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
1075 description: null,
1076 loc: iterableItem.loc,
1077 suggestions: null,
@@ -1261,6 +1234,35 @@ function codegenInstructionNullable(
1234 t.variableDeclarator(codegenLValue(cx, lvalue), value),
1235 ]);
1236 }
1237 + case InstructionKind.Function: {
1238 + CompilerError.invariant(instr.lvalue === null, {
1239 + reason: `Function declaration cannot be referenced as an expression`,
1240 + description: null,
1241 + loc: instr.value.loc,
1242 + suggestions: null,
1243 + });
1244 + const genLvalue = codegenLValue(cx, lvalue);
1245 + CompilerError.invariant(genLvalue.type === 'Identifier', {
1246 + reason: 'Expected an identifier as a function declaration lvalue',
1247 + description: null,
1248 + loc: instr.value.loc,
1249 + suggestions: null,
1250 + });
1251 + CompilerError.invariant(value?.type === 'FunctionExpression', {
1252 + reason: 'Expected a function as a function declaration value',
1253 + description: null,
1254 + loc: instr.value.loc,
1255 + suggestions: null,
1256 + });
1257 + return createFunctionDeclaration(
1258 + instr.loc,
1259 + genLvalue,
1260 + value.params,
1261 + value.body,
1262 + value.generator,
1263 + value.async,
1264 + );
1265 + }
1266 case InstructionKind.Let: {
1267 CompilerError.invariant(instr.lvalue === null, {
1268 reason: `Const declaration cannot be referenced as an expression`,
@@ -1303,19 +1305,11 @@ function codegenInstructionNullable(
1305 case InstructionKind.Catch: {
1306 return t.emptyStatement();
1307 }
1306 - case InstructionKind.HoistedLet: {
1307 - CompilerError.invariant(false, {
1308 - reason:
1309 - 'Expected HoistedLet to have been pruned in PruneHoistedContexts',
1310 - description: null,
1311 - loc: instr.loc,
1312 - suggestions: null,
1313 - });
1314 - }
1315 - case InstructionKind.HoistedConst: {
1308 + case InstructionKind.HoistedLet:
1309 + case InstructionKind.HoistedConst:
1310 + case InstructionKind.HoistedFunction: {
1311 CompilerError.invariant(false, {
1317 - reason:
1318 - 'Expected HoistedConsts to have been pruned in PruneHoistedContexts',
1312 + reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
1313 description: null,
1314 loc: instr.loc,
1315 suggestions: null,
@@ -1486,6 +1480,7 @@ const createBinaryExpression = withLoc(t.binaryExpression);
1480 const createExpressionStatement = withLoc(t.expressionStatement);
1481 const _createLabelledStatement = withLoc(t.labeledStatement);
1482 const createVariableDeclaration = withLoc(t.variableDeclaration);
1483 +const createFunctionDeclaration = withLoc(t.functionDeclaration);
1484 const _createWhileStatement = withLoc(t.whileStatement);
1485 const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
1486 const createLogicalExpression = withLoc(t.logicalExpression);
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts
+11
@@ -57,6 +57,17 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
57 return {kind: 'remove'};
58 }
59
60 + if (
61 + instruction.value.kind === 'DeclareContext' &&
62 + instruction.value.lvalue.kind === 'HoistedFunction'
63 + ) {
64 + state.set(
65 + instruction.value.lvalue.place.identifier.declarationId,
66 + InstructionKind.Function,
67 + );
68 + return {kind: 'remove'};
69 + }
70 +
71 if (
72 instruction.value.kind === 'StoreContext' &&
73 state.has(instruction.value.lvalue.place.identifier.declarationId)
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md deleted
-29
@@ -1,29 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function component(a) {
6 - let t = {a};
7 - x(t); // hoisted call
8 - function x(p) {
9 - p.foo();
10 - }
11 - return t;
12 -}
13 -
14 -```
15 -
16 -
17 -## Error
18 -
19 -```
20 - 1 | function component(a) {
21 - 2 | let t = {a};
22 -> 3 | x(t); // hoisted call
23 - | ^^^^ Todo: Unsupported declaration type for hoisting. variable "x" declared with FunctionDeclaration (3:3)
24 - 4 | function x(p) {
25 - 5 | p.foo();
26 - 6 | }
27 -```
28 -
29 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js deleted
-8
@@ -1,8 +0,0 @@
1 -function component(a) {
2 - let t = {a};
3 - x(t); // hoisted call
4 - function x(p) {
5 - p.foo();
6 - }
7 - return t;
8 -}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md
+7 -7
@@ -24,13 +24,13 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Error
25
26 ```
27 - 3 | return x;
28 - 4 | }
29 -> 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
30 - | ^^^^^ Todo: Unsupported declaration type for hoisting. variable "baz" declared with FunctionDeclaration (5:5)
31 - 6 | function baz() {
32 - 7 | return bar();
33 - 8 | }
27 + 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting
28 + 6 | function baz() {
29 +> 7 | return bar();
30 + | ^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (7:7)
31 + 8 | }
32 + 9 | }
33 + 10 |
34 ```
35
36
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md
+9 -5
@@ -16,11 +16,15 @@ function Component() {
16
17 ```
18 1 | function Component() {
19 -> 2 | return get2();
20 - | ^^^^^^ Todo: Unsupported declaration type for hoisting. variable "get2" declared with FunctionDeclaration (2:2)
21 - 3 | function get2() {
22 - 4 | return 2;
23 - 5 | }
19 + 2 | return get2();
20 +> 3 | function get2() {
21 + | ^^^^^^^^^^^^^^^^^
22 +> 4 | return 2;
23 + | ^^^^^^^^^^^^^
24 +> 5 | }
25 + | ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (3:5)
26 + 6 | }
27 + 7 |
28 ```
29
30
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md deleted
-30
@@ -1,30 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component() {
6 - function callback(x) {
7 - if (x == 0) {
8 - return null;
9 - }
10 - return callback(x - 1);
11 - }
12 - return callback(10);
13 -}
14 -
15 -```
16 -
17 -
18 -## Error
19 -
20 -```
21 - 4 | return null;
22 - 5 | }
23 -> 6 | return callback(x - 1);
24 - | ^^^^^^^^^^^^^^^ Todo: Unsupported declaration type for hoisting. variable "callback" declared with FunctionDeclaration (6:6)
25 - 7 | }
26 - 8 | return callback(10);
27 - 9 | }
28 -```
29 -
30 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.expect.md new
+63
@@ -0,0 +1,63 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function component(a) {
6 + let t = {a};
7 + x(t); // hoisted call
8 + function x(p) {
9 + p.a.foo();
10 + }
11 + return t;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: component,
16 + params: [
17 + {
18 + foo: () => {
19 + console.log(42);
20 + },
21 + },
22 + ],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +function component(a) {
32 + const $ = _c(2);
33 + let t;
34 + if ($[0] !== a) {
35 + t = { a };
36 + x(t);
37 + function x(p) {
38 + p.a.foo();
39 + }
40 + $[0] = a;
41 + $[1] = t;
42 + } else {
43 + t = $[1];
44 + }
45 + return t;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: component,
50 + params: [
51 + {
52 + foo: () => {
53 + console.log(42);
54 + },
55 + },
56 + ],
57 +};
58 +
59 +```
60 +
61 +### Eval output
62 +(kind: ok) {"a":{"foo":"[[ function params=0 ]]"}}
63 +logs: [42]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-function-declaration.js new
+19
@@ -0,0 +1,19 @@
1 +function component(a) {
2 + let t = {a};
3 + x(t); // hoisted call
4 + function x(p) {
5 + p.a.foo();
6 + }
7 + return t;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: component,
12 + params: [
13 + {
14 + foo: () => {
15 + console.log(42);
16 + },
17 + },
18 + ],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.expect.md new
+53
@@ -0,0 +1,53 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + function callback(x) {
7 + if (x == 0) {
8 + return null;
9 + }
10 + return callback(x - 1);
11 + }
12 + return callback(10);
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime";
26 +function Component() {
27 + const $ = _c(1);
28 + let t0;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + function callback(x) {
31 + if (x == 0) {
32 + return null;
33 + }
34 + return callback(x - 1);
35 + }
36 +
37 + t0 = callback(10);
38 + $[0] = t0;
39 + } else {
40 + t0 = $[0];
41 + }
42 + return t0;
43 +}
44 +
45 +export const FIXTURE_ENTRYPOINT = {
46 + fn: Component,
47 + params: [],
48 +};
49 +
50 +```
51 +
52 +### Eval output
53 +(kind: ok) null
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function-expression.js renamed
+5
@@ -7,3 +7,8 @@ function Component() {
7 }
8 return callback(10);
9 }
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [],
14 +};