Skip to content

Commit d8333f0

Browse files
committed
[compiler] Correctly insert (Arrow)FunctionExpressions
Previously we would insert new (Arrow)FunctionExpressions as a sibling of the original function. However this would break in the outlining case as it would cause the original function expression's parent to become a SequenceExpression, breaking a bunch of assumptions in the babel plugin. To get around this, we synthesize a new VariableDeclaration to contain the newly inserted function expression and therefore insert it as a true sibling to the original function. Yeah, it's kinda gross ghstack-source-id: aefc2cd Pull Request resolved: #30446
1 parent 4bb7bdb commit d8333f0

File tree

4 files changed

+138
-28
lines changed

4 files changed

+138
-28
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,99 @@ export function createNewFunctionNode(
183183
transformedFn = fn;
184184
break;
185185
}
186+
default: {
187+
assertExhaustive(
188+
originalFn.node,
189+
`Creating unhandled function: ${originalFn.node}`,
190+
);
191+
}
186192
}
187-
188193
// Avoid visiting the new transformed version
189194
ALREADY_COMPILED.add(transformedFn);
190195
return transformedFn;
191196
}
192197

198+
function insertNewFunctionNode(
199+
originalFn: BabelFn,
200+
compiledFn: CodegenFunction,
201+
): NodePath<t.Function> {
202+
switch (originalFn.type) {
203+
case 'FunctionDeclaration': {
204+
return originalFn.insertAfter(
205+
createNewFunctionNode(originalFn, compiledFn),
206+
)[0]!;
207+
}
208+
/**
209+
* This is pretty gross but we can't just append an (Arrow)FunctionExpression as a sibling of
210+
* the original function. If the original function was itself an (Arrow)FunctionExpression,
211+
* this would cause its parent to become a SequenceExpression instead which breaks a bunch of
212+
* assumptions elsewhere in the plugin.
213+
*
214+
* To get around this, we synthesize a new VariableDeclaration holding the compiled function
215+
* expression and insert it as a true sibling (ie within the Program's block statements).
216+
*/
217+
case 'ArrowFunctionExpression':
218+
case 'FunctionExpression': {
219+
const funcExpr = createNewFunctionNode(originalFn, compiledFn);
220+
CompilerError.invariant(
221+
t.isArrowFunctionExpression(funcExpr) ||
222+
t.isFunctionExpression(funcExpr),
223+
{
224+
reason: 'Expected an (arrow) function expression to be created',
225+
description: `Got: ${funcExpr.type}`,
226+
loc: null,
227+
},
228+
);
229+
CompilerError.invariant(compiledFn.id != null, {
230+
reason: 'Expected compiled function to have an identifier',
231+
loc: null,
232+
});
233+
CompilerError.invariant(originalFn.parentPath.isVariableDeclarator(), {
234+
reason: 'Expected a variable declarator parent',
235+
loc: null,
236+
});
237+
const varDecl = originalFn.parentPath.parentPath;
238+
varDecl.insertAfter(
239+
t.variableDeclaration('const', [
240+
t.variableDeclarator(compiledFn.id, funcExpr),
241+
]),
242+
)[0]!;
243+
CompilerError.invariant(typeof varDecl.key === 'number', {
244+
reason:
245+
'Just Babel things: expected the VariableDeclaration containing the compiled function expression to have a number key',
246+
loc: null,
247+
});
248+
const insertedCompiledFnVarDecl = varDecl.getSibling(varDecl.key + 1);
249+
CompilerError.invariant(
250+
insertedCompiledFnVarDecl.isVariableDeclaration(),
251+
{
252+
reason: 'Expected inserted sibling to be a VariableDeclaration',
253+
loc: null,
254+
},
255+
);
256+
const insertedFuncExpr = insertedCompiledFnVarDecl
257+
.get('declarations')[0]!
258+
.get('init')!;
259+
CompilerError.invariant(
260+
insertedFuncExpr.isArrowFunctionExpression() ||
261+
insertedFuncExpr.isFunctionExpression(),
262+
{
263+
reason: 'Expected inserted (arrow) function expression',
264+
description: `Got: ${insertedFuncExpr}`,
265+
loc: null,
266+
},
267+
);
268+
return insertedFuncExpr;
269+
}
270+
default: {
271+
assertExhaustive(
272+
originalFn,
273+
`Inserting unhandled function: ${originalFn}`,
274+
);
275+
}
276+
}
277+
}
278+
193279
/*
194280
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
195281
* consistently respect the `skip()` function to avoid revisiting a node within
@@ -407,9 +493,7 @@ export function compileProgram(
407493
reason: 'Unexpected nested outlined functions',
408494
loc: outlined.fn.loc,
409495
});
410-
const fn = current.fn.insertAfter(
411-
createNewFunctionNode(current.fn, outlined.fn),
412-
)[0]!;
496+
const fn = insertNewFunctionNode(current.fn, outlined.fn);
413497
fn.skip();
414498
ALREADY_COMPILED.add(fn.node);
415499
if (outlined.type !== null) {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-outlining-in-func-expr.expect.md

Lines changed: 0 additions & 24 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @debug
6+
const Component2 = props => {
7+
return (
8+
<ul>
9+
{props.items.map(item => (
10+
<li key={item.id}>{item.name}</li>
11+
))}
12+
</ul>
13+
);
14+
};
15+
16+
```
17+
18+
## Code
19+
20+
```javascript
21+
import { c as _c } from "react/compiler-runtime"; // @debug
22+
const Component2 = (props) => {
23+
const $ = _c(4);
24+
let t0;
25+
if ($[0] !== props.items) {
26+
t0 = props.items.map(_temp);
27+
$[0] = props.items;
28+
$[1] = t0;
29+
} else {
30+
t0 = $[1];
31+
}
32+
let t1;
33+
if ($[2] !== t0) {
34+
t1 = <ul>{t0}</ul>;
35+
$[2] = t0;
36+
$[3] = t1;
37+
} else {
38+
t1 = $[3];
39+
}
40+
return t1;
41+
};
42+
const _temp = (item) => {
43+
return <li key={item.id}>{item.name}</li>;
44+
};
45+
46+
```
47+
48+
### Eval output
49+
(kind: exception) Fixture not implemented
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @debug
12
const Component2 = props => {
23
return (
34
<ul>

0 commit comments

Comments
 (0)