Skip to content

Commit a6b7e43

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: df13e3b Pull Request resolved: #30446
1 parent 91e4f07 commit a6b7e43

File tree

5 files changed

+170
-37
lines changed

5 files changed

+170
-37
lines changed

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

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,88 @@ 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: funcExpr.loc ?? null,
227+
},
228+
);
229+
CompilerError.invariant(compiledFn.id != null, {
230+
reason: 'Expected compiled function to have an identifier',
231+
loc: compiledFn.loc,
232+
});
233+
CompilerError.invariant(
234+
originalFn.parentPath.isVariableDeclarator() &&
235+
originalFn.parentPath.parentPath.isVariableDeclaration(),
236+
{
237+
reason: 'Expected a variable declarator parent',
238+
loc: originalFn.node.loc ?? null,
239+
},
240+
);
241+
const varDecl = originalFn.parentPath.parentPath;
242+
varDecl.insertAfter(
243+
t.variableDeclaration('const', [
244+
t.variableDeclarator(compiledFn.id, funcExpr),
245+
]),
246+
);
247+
const insertedFuncExpr = varDecl.get('declarations')[0]!.get('init')!;
248+
CompilerError.invariant(
249+
insertedFuncExpr.isArrowFunctionExpression() ||
250+
insertedFuncExpr.isFunctionExpression(),
251+
{
252+
reason: 'Expected inserted (arrow) function expression',
253+
description: `Got: ${insertedFuncExpr}`,
254+
loc: insertedFuncExpr.node?.loc ?? null,
255+
},
256+
);
257+
return insertedFuncExpr;
258+
}
259+
default: {
260+
assertExhaustive(
261+
originalFn,
262+
`Inserting unhandled function: ${originalFn}`,
263+
);
264+
}
265+
}
266+
}
267+
193268
/*
194269
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
195270
* consistently respect the `skip()` function to avoid revisiting a node within
@@ -407,9 +482,7 @@ export function compileProgram(
407482
reason: 'Unexpected nested outlined functions',
408483
loc: outlined.fn.loc,
409484
});
410-
const fn = current.fn.insertAfter(
411-
createNewFunctionNode(current.fn, outlined.fn),
412-
)[0]!;
485+
const fn = insertNewFunctionNode(current.fn, outlined.fn);
413486
fn.skip();
414487
ALREADY_COMPILED.add(fn.node);
415488
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.

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

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
2+
## Input
3+
4+
```javascript
5+
const Component2 = props => {
6+
return (
7+
<ul>
8+
{props.items.map(item => (
9+
<li key={item.id}>{item.name}</li>
10+
))}
11+
</ul>
12+
);
13+
};
14+
15+
export const FIXTURE_ENTRYPOINT = {
16+
fn: Component2,
17+
params: [
18+
{
19+
items: [
20+
{id: 2, name: 'foo'},
21+
{id: 3, name: 'bar'},
22+
],
23+
},
24+
],
25+
};
26+
27+
```
28+
29+
## Code
30+
31+
```javascript
32+
import { c as _c } from "react/compiler-runtime";
33+
const Component2 = (props) => {
34+
const $ = _c(4);
35+
let t0;
36+
if ($[0] !== props.items) {
37+
t0 = props.items.map(_temp);
38+
$[0] = props.items;
39+
$[1] = t0;
40+
} else {
41+
t0 = $[1];
42+
}
43+
let t1;
44+
if ($[2] !== t0) {
45+
t1 = <ul>{t0}</ul>;
46+
$[2] = t0;
47+
$[3] = t1;
48+
} else {
49+
t1 = $[3];
50+
}
51+
return t1;
52+
};
53+
const _temp = (item) => {
54+
return <li key={item.id}>{item.name}</li>;
55+
};
56+
57+
export const FIXTURE_ENTRYPOINT = {
58+
fn: Component2,
59+
params: [
60+
{
61+
items: [
62+
{ id: 2, name: "foo" },
63+
{ id: 3, name: "bar" },
64+
],
65+
},
66+
],
67+
};
68+
69+
```
70+
71+
### Eval output
72+
(kind: ok) <ul><li>foo</li><li>bar</li></ul>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const Component2 = props => {
2+
return (
3+
<ul>
4+
{props.items.map(item => (
5+
<li key={item.id}>{item.name}</li>
6+
))}
7+
</ul>
8+
);
9+
};
10+
11+
export const FIXTURE_ENTRYPOINT = {
12+
fn: Component2,
13+
params: [
14+
{
15+
items: [
16+
{id: 2, name: 'foo'},
17+
{id: 3, name: 'bar'},
18+
],
19+
},
20+
],
21+
};

0 commit comments

Comments
 (0)