Skip to content

Commit 24a0995

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: 8a45891 Pull Request resolved: #30446
1 parent e9ea912 commit 24a0995

File tree

4 files changed

+137
-28
lines changed

4 files changed

+137
-28
lines changed

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

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,100 @@ 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+
);
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+
// safety: we synthesized it above, no need to check again
257+
const insertedFuncExpr = insertedCompiledFnVarDecl
258+
.get('declarations')[0]!
259+
.get('init')!;
260+
CompilerError.invariant(
261+
insertedFuncExpr.isArrowFunctionExpression() ||
262+
insertedFuncExpr.isFunctionExpression(),
263+
{
264+
reason: 'Expected inserted (arrow) function expression',
265+
description: `Got: ${insertedFuncExpr}`,
266+
loc: null,
267+
},
268+
);
269+
return insertedFuncExpr;
270+
}
271+
default: {
272+
assertExhaustive(
273+
originalFn,
274+
`Inserting unhandled function: ${originalFn}`,
275+
);
276+
}
277+
}
278+
}
279+
193280
/*
194281
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
195282
* consistently respect the `skip()` function to avoid revisiting a node within
@@ -407,9 +494,7 @@ export function compileProgram(
407494
reason: 'Unexpected nested outlined functions',
408495
loc: outlined.fn.loc,
409496
});
410-
const fn = current.fn.insertAfter(
411-
createNewFunctionNode(current.fn, outlined.fn),
412-
)[0]!;
497+
const fn = insertNewFunctionNode(current.fn, outlined.fn);
413498
fn.skip();
414499
ALREADY_COMPILED.add(fn.node);
415500
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.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
```
16+
17+
## Code
18+
19+
```javascript
20+
import { c as _c } from "react/compiler-runtime";
21+
const Component2 = (props) => {
22+
const $ = _c(4);
23+
let t0;
24+
if ($[0] !== props.items) {
25+
t0 = props.items.map(_temp);
26+
$[0] = props.items;
27+
$[1] = t0;
28+
} else {
29+
t0 = $[1];
30+
}
31+
let t1;
32+
if ($[2] !== t0) {
33+
t1 = <ul>{t0}</ul>;
34+
$[2] = t0;
35+
$[3] = t1;
36+
} else {
37+
t1 = $[3];
38+
}
39+
return t1;
40+
};
41+
const _temp = (item) => {
42+
return <li key={item.id}>{item.name}</li>;
43+
};
44+
45+
```
46+
47+
### Eval output
48+
(kind: exception) Fixture not implemented

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

File renamed without changes.

0 commit comments

Comments
 (0)