Skip to content

Commit 7399427

Browse files
committed
[compiler] Fix fbt for the ∞th time
We now do a single pass over the HIR, building up two data structures: * One tracks values that are known macro tags or macro calls. * One tracks operands of macro-related instructions so that we can later group them. After building up these data structures, we do a pass over the latter structure. For each macro call instruction, we recursively traverse its operands to ensure they're in the same scope. Thus, something like `fbt('hello' + fbt.param(foo(), "..."))` will correctly merge the fbt call, the `+` binary expression, the `fbt.param()` call, and `foo()` into a single scope.
1 parent 903366b commit 7399427

9 files changed

+342
-165
lines changed

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

Lines changed: 110 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77

88
import {
99
HIRFunction,
10+
Identifier,
1011
IdentifierId,
12+
InstructionValue,
1113
makeInstructionId,
1214
MutableRange,
1315
Place,
14-
ReactiveValue,
16+
ReactiveScope,
1517
} from '../HIR';
1618
import {Macro, MacroMethod} from '../HIR/Environment';
17-
import {eachReactiveValueOperand} from './visitors';
19+
import {printIdentifier} from '../HIR/PrintHIR';
20+
import {eachInstructionValueOperand} from '../HIR/visitors';
21+
import {Iterable_some} from '../Utils/utils';
1822

1923
/**
2024
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
@@ -48,24 +52,49 @@ export function memoizeFbtAndMacroOperandsInSameScope(
4852
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
4953
...(fn.env.config.customMacros ?? []),
5054
]);
51-
const fbtValues: Set<IdentifierId> = new Set();
55+
/**
56+
* Set of all identifiers that load fbt or other macro functions or their nested
57+
* properties, as well as values known to be the results of invoking macros
58+
*/
59+
const macroTagsCalls: Set<IdentifierId> = new Set();
60+
/**
61+
* Mapping of lvalue => list of operands for all expressions where either
62+
* the lvalue is a known fbt/macro call and/or the operands transitively
63+
* contain fbt/macro calls.
64+
*
65+
* This is the key data structure that powers the scope merging: we start
66+
* at the lvalues and merge operands into the lvalue's scope.
67+
*/
68+
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
69+
// Tracks methods loaded from macros, like fbt.param or idx.foo
5270
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
53-
while (true) {
54-
let vsize = fbtValues.size;
55-
let msize = macroMethods.size;
56-
visit(fn, fbtMacroTags, fbtValues, macroMethods);
57-
if (vsize === fbtValues.size && msize === macroMethods.size) {
58-
break;
71+
72+
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
73+
74+
for (const root of macroValues.keys()) {
75+
const scope = root.scope;
76+
if (scope == null) {
77+
continue;
78+
}
79+
// Merge the operands into the same scope if this is a known macro invocation
80+
if (!macroTagsCalls.has(root.id)) {
81+
continue;
5982
}
83+
mergeScopes(root, scope, macroValues, macroTagsCalls);
6084
}
61-
return fbtValues;
85+
86+
return macroTagsCalls;
6287
}
6388

6489
export const FBT_TAGS: Set<string> = new Set([
6590
'fbt',
6691
'fbt:param',
92+
'fbt:enum',
93+
'fbt:plural',
6794
'fbs',
6895
'fbs:param',
96+
'fbs:enum',
97+
'fbs:plural',
6998
]);
7099
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
71100
'fbt:param',
@@ -75,10 +104,22 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
75104
function visit(
76105
fn: HIRFunction,
77106
fbtMacroTags: Set<Macro>,
78-
fbtValues: Set<IdentifierId>,
107+
macroTagsCalls: Set<IdentifierId>,
79108
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
109+
macroValues: Map<Identifier, Array<Identifier>>,
80110
): void {
81111
for (const [, block] of fn.body.blocks) {
112+
for (const phi of block.phis) {
113+
const macroOperands: Array<Identifier> = [];
114+
for (const operand of phi.operands.values()) {
115+
if (macroValues.has(operand.identifier)) {
116+
macroOperands.push(operand.identifier);
117+
}
118+
}
119+
if (macroOperands.length !== 0) {
120+
macroValues.set(phi.place.identifier, macroOperands);
121+
}
122+
}
82123
for (const instruction of block.instructions) {
83124
const {lvalue, value} = instruction;
84125
if (lvalue === null) {
@@ -93,13 +134,13 @@ function visit(
93134
* We don't distinguish between tag names and strings, so record
94135
* all `fbt` string literals in case they are used as a jsx tag.
95136
*/
96-
fbtValues.add(lvalue.identifier.id);
137+
macroTagsCalls.add(lvalue.identifier.id);
97138
} else if (
98139
value.kind === 'LoadGlobal' &&
99140
matchesExactTag(value.binding.name, fbtMacroTags)
100141
) {
101142
// Record references to `fbt` as a global
102-
fbtValues.add(lvalue.identifier.id);
143+
macroTagsCalls.add(lvalue.identifier.id);
103144
} else if (
104145
value.kind === 'LoadGlobal' &&
105146
matchTagRoot(value.binding.name, fbtMacroTags) !== null
@@ -121,84 +162,66 @@ function visit(
121162
if (method.length > 1) {
122163
newMethods.push(method.slice(1));
123164
} else {
124-
fbtValues.add(lvalue.identifier.id);
165+
macroTagsCalls.add(lvalue.identifier.id);
125166
}
126167
}
127168
}
128169
if (newMethods.length > 0) {
129170
macroMethods.set(lvalue.identifier.id, newMethods);
130171
}
131-
} else if (isFbtCallExpression(fbtValues, value)) {
132-
const fbtScope = lvalue.identifier.scope;
133-
if (fbtScope === null) {
134-
continue;
135-
}
136-
137-
/*
138-
* if the JSX element's tag was `fbt`, mark all its operands
139-
* to ensure that they end up in the same scope as the jsx element
140-
* itself.
141-
*/
142-
for (const operand of eachReactiveValueOperand(value)) {
143-
operand.identifier.scope = fbtScope;
144-
145-
// Expand the jsx element's range to account for its operands
146-
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
147-
fbtValues.add(operand.identifier.id);
148-
}
149172
} else if (
150-
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
151-
isFbtJsxChild(fbtValues, lvalue, value)
173+
value.kind === 'PropertyLoad' &&
174+
macroTagsCalls.has(value.object.identifier.id)
152175
) {
153-
const fbtScope = lvalue.identifier.scope;
154-
if (fbtScope === null) {
155-
continue;
156-
}
157-
158-
/*
159-
* if the JSX element's tag was `fbt`, mark all its operands
160-
* to ensure that they end up in the same scope as the jsx element
161-
* itself.
162-
*/
163-
for (const operand of eachReactiveValueOperand(value)) {
164-
operand.identifier.scope = fbtScope;
165-
166-
// Expand the jsx element's range to account for its operands
167-
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
168-
169-
/*
170-
* NOTE: we add the operands as fbt values so that they are also
171-
* grouped with this expression
172-
*/
173-
fbtValues.add(operand.identifier.id);
174-
}
175-
} else if (fbtValues.has(lvalue.identifier.id)) {
176-
const fbtScope = lvalue.identifier.scope;
177-
if (fbtScope === null) {
178-
return;
179-
}
180-
181-
for (const operand of eachReactiveValueOperand(value)) {
182-
if (
183-
operand.identifier.name !== null &&
184-
operand.identifier.name.kind === 'named'
185-
) {
186-
/*
187-
* named identifiers were already locals, we only have to force temporaries
188-
* into the same scope
189-
*/
190-
continue;
176+
macroTagsCalls.add(lvalue.identifier.id);
177+
} else if (
178+
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
179+
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
180+
isFbtCallExpression(macroTagsCalls, value)
181+
) {
182+
macroTagsCalls.add(lvalue.identifier.id);
183+
macroValues.set(
184+
lvalue.identifier,
185+
Array.from(
186+
eachInstructionValueOperand(value),
187+
operand => operand.identifier,
188+
),
189+
);
190+
} else if (
191+
Iterable_some(eachInstructionValueOperand(value), operand =>
192+
macroValues.has(operand.identifier),
193+
)
194+
) {
195+
const macroOperands: Array<Identifier> = [];
196+
for (const operand of eachInstructionValueOperand(value)) {
197+
if (macroValues.has(operand.identifier)) {
198+
macroOperands.push(operand.identifier);
191199
}
192-
operand.identifier.scope = fbtScope;
193-
194-
// Expand the jsx element's range to account for its operands
195-
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
196200
}
201+
macroValues.set(lvalue.identifier, macroOperands);
197202
}
198203
}
199204
}
200205
}
201206

207+
function mergeScopes(
208+
root: Identifier,
209+
scope: ReactiveScope,
210+
macroValues: Map<Identifier, Array<Identifier>>,
211+
macroTagsCalls: Set<IdentifierId>,
212+
): void {
213+
const operands = macroValues.get(root);
214+
if (operands == null) {
215+
return;
216+
}
217+
for (const operand of operands) {
218+
operand.scope = scope;
219+
expandFbtScopeRange(scope.range, operand.mutableRange);
220+
macroTagsCalls.add(operand.id);
221+
mergeScopes(operand, scope, macroValues, macroTagsCalls);
222+
}
223+
}
224+
202225
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
203226
return Array.from(tags).some(macro =>
204227
typeof macro === 'string'
@@ -229,39 +252,40 @@ function matchTagRoot(
229252
}
230253

231254
function isFbtCallExpression(
232-
fbtValues: Set<IdentifierId>,
233-
value: ReactiveValue,
255+
macroTagsCalls: Set<IdentifierId>,
256+
value: InstructionValue,
234257
): boolean {
235258
return (
236259
(value.kind === 'CallExpression' &&
237-
fbtValues.has(value.callee.identifier.id)) ||
238-
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
260+
macroTagsCalls.has(value.callee.identifier.id)) ||
261+
(value.kind === 'MethodCall' &&
262+
macroTagsCalls.has(value.property.identifier.id))
239263
);
240264
}
241265

242266
function isFbtJsxExpression(
243267
fbtMacroTags: Set<Macro>,
244-
fbtValues: Set<IdentifierId>,
245-
value: ReactiveValue,
268+
macroTagsCalls: Set<IdentifierId>,
269+
value: InstructionValue,
246270
): boolean {
247271
return (
248272
value.kind === 'JsxExpression' &&
249273
((value.tag.kind === 'Identifier' &&
250-
fbtValues.has(value.tag.identifier.id)) ||
274+
macroTagsCalls.has(value.tag.identifier.id)) ||
251275
(value.tag.kind === 'BuiltinTag' &&
252276
matchesExactTag(value.tag.name, fbtMacroTags)))
253277
);
254278
}
255279

256280
function isFbtJsxChild(
257-
fbtValues: Set<IdentifierId>,
281+
macroTagsCalls: Set<IdentifierId>,
258282
lvalue: Place | null,
259-
value: ReactiveValue,
283+
value: InstructionValue,
260284
): boolean {
261285
return (
262286
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
263287
lvalue !== null &&
264-
fbtValues.has(lvalue.identifier.id)
288+
macroTagsCalls.has(lvalue.identifier.id)
265289
);
266290
}
267291

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/error.todo-fbt-param-nested-fbt.expect.md

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

0 commit comments

Comments
 (0)