Skip to content

Commit ef44cd1

Browse files
committed
[compiler][bugfix] Bail out when a memo block declares hoisted fns
(needs cleanup + more test fixtures)
1 parent 53b3678 commit ef44cd1

File tree

4 files changed

+137
-83
lines changed

4 files changed

+137
-83
lines changed

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

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {CompilerError} from '..';
89
import {
910
convertHoistedLValueKind,
11+
Environment,
1012
IdentifierId,
13+
InstructionId,
1114
InstructionKind,
15+
Place,
1216
ReactiveFunction,
1317
ReactiveInstruction,
18+
ReactiveScope,
1419
ReactiveScopeBlock,
1520
ReactiveStatement,
1621
} from '../HIR';
@@ -24,31 +29,78 @@ import {
2429
/*
2530
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
2631
* original instruction kind.
32+
*
33+
* Detects and bails out on context variables which are:
34+
* - function declarations, which are hoisted by JS engines to the nearest block scope
35+
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
36+
* - declared
2737
*/
2838
export function pruneHoistedContexts(fn: ReactiveFunction): void {
2939
visitReactiveFunction(fn, new Visitor(), {
3040
activeScopes: empty(),
41+
exceptions: new Set(),
42+
uninitialized: new Map(),
3143
});
3244
}
3345

3446
type VisitorState = {
3547
activeScopes: Stack<Set<IdentifierId>>;
48+
exceptions: Set<Place>;
49+
uninitialized: Map<
50+
IdentifierId,
51+
| {
52+
kind: 'maybefunc';
53+
}
54+
| {
55+
kind: 'func';
56+
definition: Place | null;
57+
}
58+
>;
3659
};
3760

61+
/**
62+
* Oh man what about declarations in nested scopes?? t.t
63+
* We *might* encounter the following.
64+
* scope @0
65+
*/
3866
class Visitor extends ReactiveFunctionTransform<VisitorState> {
3967
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
4068
state.activeScopes = state.activeScopes.push(
4169
new Set(scope.scope.declarations.keys()),
4270
);
71+
for (const decl of scope.scope.declarations.values()) {
72+
state.uninitialized.set(decl.identifier.id, {kind: 'maybefunc'});
73+
}
4374
this.traverseScope(scope, state);
4475
state.activeScopes.pop();
76+
77+
/**
78+
* References to hoisted functions are now "safe" as it has been assigned
79+
*/
80+
for (const id of scope.scope.declarations.keys()) {
81+
state.uninitialized.delete(id);
82+
}
83+
}
84+
override visitPlace(
85+
_id: InstructionId,
86+
place: Place,
87+
state: VisitorState,
88+
): void {
89+
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
90+
if (
91+
maybeHoistedFn?.kind === 'func' &&
92+
maybeHoistedFn.definition !== place
93+
) {
94+
CompilerError.throwTodo({
95+
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
96+
loc: place.loc,
97+
});
98+
}
4599
}
46100
override transformInstruction(
47101
instruction: ReactiveInstruction,
48102
state: VisitorState,
49103
): Transformed<ReactiveStatement> {
50-
this.visitInstruction(instruction, state);
51-
52104
/**
53105
* Remove hoisted declarations to preserve TDZ
54106
*/
@@ -57,6 +109,18 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
57109
instruction.value.lvalue.kind,
58110
);
59111
if (maybeNonHoisted != null) {
112+
if (
113+
maybeNonHoisted === InstructionKind.Function &&
114+
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
115+
) {
116+
state.uninitialized.set(
117+
instruction.value.lvalue.place.identifier.id,
118+
{
119+
kind: 'func',
120+
definition: null,
121+
},
122+
);
123+
}
60124
return {kind: 'remove'};
61125
}
62126
}
@@ -65,18 +129,44 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
65129
instruction.value.lvalue.kind !== InstructionKind.Reassign
66130
) {
67131
/**
68-
* Rewrite StoreContexts let/const/functions that will be pre-declared in
132+
* Rewrite StoreContexts let/const that will be pre-declared in
69133
* codegen to reassignments.
70134
*/
71135
const lvalueId = instruction.value.lvalue.place.identifier.id;
72136
const isDeclaredByScope = state.activeScopes.find(scope =>
73137
scope.has(lvalueId),
74138
);
75139
if (isDeclaredByScope) {
76-
instruction.value.lvalue.kind = InstructionKind.Reassign;
140+
if (
141+
instruction.value.lvalue.kind === InstructionKind.Let ||
142+
instruction.value.lvalue.kind === InstructionKind.Const
143+
) {
144+
instruction.value.lvalue.kind = InstructionKind.Reassign;
145+
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
146+
state.exceptions.add(instruction.value.lvalue.place);
147+
const maybeHoistedFn = state.uninitialized.get(lvalueId);
148+
if (maybeHoistedFn != null) {
149+
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
150+
reason: '[PruneHoistedContexts] Unexpected hoisted function',
151+
loc: instruction.loc,
152+
});
153+
maybeHoistedFn.definition = instruction.value.lvalue.place;
154+
}
155+
} else {
156+
CompilerError.throwTodo({
157+
reason: '[PruneHoistedContexts] Unexpected kind ',
158+
description: `(${instruction.value.lvalue.kind})`,
159+
loc: instruction.loc,
160+
});
161+
}
77162
}
78163
}
79164

165+
this.visitInstruction(instruction, state);
80166
return {kind: 'keep'};
81167
}
82168
}
169+
170+
/**
171+
* For functions whose declarations span block boundaries,
172+
*/

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-functiondecl-hoisting.expect.md

Lines changed: 0 additions & 79 deletions
This file was deleted.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {Stringify} from 'shared-runtime';
6+
7+
/**
8+
* Fixture currently fails with
9+
* Found differences in evaluator results
10+
* Non-forget (expected):
11+
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
12+
* Forget:
13+
* (kind: exception) bar is not a function
14+
*/
15+
function Foo({value}) {
16+
const result = bar();
17+
function bar() {
18+
return {value};
19+
}
20+
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
21+
}
22+
23+
export const FIXTURE_ENTRYPOINT = {
24+
fn: Foo,
25+
params: [{value: 2}],
26+
};
27+
28+
```
29+
30+
31+
## Error
32+
33+
```
34+
10 | */
35+
11 | function Foo({value}) {
36+
> 12 | const result = bar();
37+
| ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (12:12)
38+
13 | function bar() {
39+
14 | return {value};
40+
15 | }
41+
```
42+
43+

0 commit comments

Comments
 (0)