[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
+};