Skip to content

Commit 74aa17f

Browse files
committed
[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: 7ebf5ea Pull Request resolved: #30922
1 parent 0d850f2 commit 74aa17f

16 files changed

Lines changed: 289 additions & 117 deletions

compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,19 @@ function lowerStatement(
420420
// Already hoisted
421421
continue;
422422
}
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()) {
424436
builder.errors.push({
425437
severity: ErrorSeverity.Todo,
426438
reason: 'Unsupported declaration type for hoisting',
@@ -429,11 +441,7 @@ function lowerStatement(
429441
loc: id.parentPath.node.loc ?? GeneratedSource,
430442
});
431443
continue;
432-
} else if (
433-
binding.kind !== 'const' &&
434-
binding.kind !== 'var' &&
435-
binding.kind !== 'let'
436-
) {
444+
} else {
437445
builder.errors.push({
438446
severity: ErrorSeverity.Todo,
439447
reason: 'Handle non-const declarations for hoisting',
@@ -443,6 +451,7 @@ function lowerStatement(
443451
});
444452
continue;
445453
}
454+
446455
const identifier = builder.resolveIdentifier(id);
447456
CompilerError.invariant(identifier.kind === 'Identifier', {
448457
reason:
@@ -456,13 +465,6 @@ function lowerStatement(
456465
reactive: false,
457466
loc: id.node.loc ?? GeneratedSource,
458467
};
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');
466468
lowerValueToTemporary(builder, {
467469
kind: 'DeclareContext',
468470
lvalue: {
@@ -999,7 +1001,7 @@ function lowerStatement(
9991001
lowerAssignment(
10001002
builder,
10011003
stmt.node.loc ?? GeneratedSource,
1002-
InstructionKind.Let,
1004+
InstructionKind.Function,
10031005
id,
10041006
fn,
10051007
'Assignment',

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,9 @@ export enum InstructionKind {
746746

747747
// hoisted const declarations
748748
HoistedLet = 'HoistedLet',
749+
750+
HoistedFunction = 'HoistedFunction',
751+
Function = 'Function',
749752
}
750753

751754
function _staticInvariantInstructionValueHasLocation(
@@ -865,7 +868,8 @@ export type InstructionValue =
865868
kind:
866869
| InstructionKind.Let
867870
| InstructionKind.HoistedConst
868-
| InstructionKind.HoistedLet;
871+
| InstructionKind.HoistedLet
872+
| InstructionKind.HoistedFunction;
869873
place: Place;
870874
};
871875
loc: SourceLocation;

compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,12 @@ export function printLValue(lval: LValue): string {
765765
case InstructionKind.HoistedLet: {
766766
return `HoistedLet ${lvalue}$`;
767767
}
768+
case InstructionKind.Function: {
769+
return `Function ${lvalue}$`;
770+
}
771+
case InstructionKind.HoistedFunction: {
772+
return `HoistedFunction ${lvalue}$`;
773+
}
768774
default: {
769775
assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
770776
}

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,20 @@ function codegenTerminal(
10011001
loc: iterableItem.loc,
10021002
suggestions: null,
10031003
});
1004+
case InstructionKind.HoistedFunction:
1005+
CompilerError.invariant(false, {
1006+
reason: 'Unexpected HoistedFunction variable in for..in collection',
1007+
description: null,
1008+
loc: iterableItem.loc,
1009+
suggestions: null,
1010+
});
1011+
case InstructionKind.Function:
1012+
CompilerError.invariant(false, {
1013+
reason: 'Unexpected Function variable in for..in collection',
1014+
description: null,
1015+
loc: iterableItem.loc,
1016+
suggestions: null,
1017+
});
10041018
default:
10051019
assertExhaustive(
10061020
iterableItem.value.lvalue.kind,
@@ -1103,6 +1117,20 @@ function codegenTerminal(
11031117
loc: iterableItem.loc,
11041118
suggestions: null,
11051119
});
1120+
case InstructionKind.HoistedFunction:
1121+
CompilerError.invariant(false, {
1122+
reason: 'Unexpected HoistedFunction variable in for..of collection',
1123+
description: null,
1124+
loc: iterableItem.loc,
1125+
suggestions: null,
1126+
});
1127+
case InstructionKind.Function:
1128+
CompilerError.invariant(false, {
1129+
reason: 'Unexpected Function variable in for..of collection',
1130+
description: null,
1131+
loc: iterableItem.loc,
1132+
suggestions: null,
1133+
});
11061134
default:
11071135
assertExhaustive(
11081136
iterableItem.value.lvalue.kind,
@@ -1261,6 +1289,35 @@ function codegenInstructionNullable(
12611289
t.variableDeclarator(codegenLValue(cx, lvalue), value),
12621290
]);
12631291
}
1292+
case InstructionKind.Function: {
1293+
CompilerError.invariant(instr.lvalue === null, {
1294+
reason: `Function declaration cannot be referenced as an expression`,
1295+
description: null,
1296+
loc: instr.value.loc,
1297+
suggestions: null,
1298+
});
1299+
const genLvalue = codegenLValue(cx, lvalue);
1300+
CompilerError.invariant(genLvalue.type === 'Identifier', {
1301+
reason: 'Expected an identifier as a function declaration lvalue',
1302+
description: null,
1303+
loc: instr.value.loc,
1304+
suggestions: null,
1305+
});
1306+
CompilerError.invariant(value?.type === 'FunctionExpression', {
1307+
reason: 'Expected a function as a function declaration value',
1308+
description: null,
1309+
loc: instr.value.loc,
1310+
suggestions: null,
1311+
});
1312+
return createFunctionDeclaration(
1313+
instr.loc,
1314+
genLvalue,
1315+
value.params,
1316+
value.body,
1317+
value.generator,
1318+
value.async,
1319+
);
1320+
}
12641321
case InstructionKind.Let: {
12651322
CompilerError.invariant(instr.lvalue === null, {
12661323
reason: `Const declaration cannot be referenced as an expression`,
@@ -1321,6 +1378,15 @@ function codegenInstructionNullable(
13211378
suggestions: null,
13221379
});
13231380
}
1381+
case InstructionKind.HoistedFunction: {
1382+
CompilerError.invariant(false, {
1383+
reason:
1384+
'Expected HoistedFunction to have been pruned in PruneHoistedContexts',
1385+
description: null,
1386+
loc: instr.loc,
1387+
suggestions: null,
1388+
});
1389+
}
13241390
default: {
13251391
assertExhaustive(kind, `Unexpected instruction kind \`${kind}\``);
13261392
}
@@ -1486,6 +1552,7 @@ const createBinaryExpression = withLoc(t.binaryExpression);
14861552
const createExpressionStatement = withLoc(t.expressionStatement);
14871553
const _createLabelledStatement = withLoc(t.labeledStatement);
14881554
const createVariableDeclaration = withLoc(t.variableDeclaration);
1555+
const createFunctionDeclaration = withLoc(t.functionDeclaration);
14891556
const _createWhileStatement = withLoc(t.whileStatement);
14901557
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
14911558
const createLogicalExpression = withLoc(t.logicalExpression);

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
5757
return {kind: 'remove'};
5858
}
5959

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+
6071
if (
6172
instruction.value.kind === 'StoreContext' &&
6273
state.has(instruction.value.lvalue.place.identifier.declarationId)

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.expect.md

Lines changed: 0 additions & 29 deletions
This file was deleted.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisted-function-declaration.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ export const FIXTURE_ENTRYPOINT = {
2424
## Error
2525

2626
```
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 |
3434
```
3535
3636

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-hoist-function-decls.expect.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ function Component() {
1616

1717
```
1818
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 |
2428
```
2529
2630

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-recursive-function-expression.expect.md

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)