Skip to content

Commit 1895bec

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 1895bec

9 files changed

+341
-165
lines changed

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

Lines changed: 109 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
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 {eachInstructionValueOperand} from '../HIR/visitors';
20+
import {Iterable_some} from '../Utils/utils';
1821

1922
/**
2023
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
@@ -48,24 +51,49 @@ export function memoizeFbtAndMacroOperandsInSameScope(
4851
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
4952
...(fn.env.config.customMacros ?? []),
5053
]);
51-
const fbtValues: Set<IdentifierId> = new Set();
54+
/**
55+
* Set of all identifiers that load fbt or other macro functions or their nested
56+
* properties, as well as values known to be the results of invoking macros
57+
*/
58+
const macroTagsCalls: Set<IdentifierId> = new Set();
59+
/**
60+
* Mapping of lvalue => list of operands for all expressions where either
61+
* the lvalue is a known fbt/macro call and/or the operands transitively
62+
* contain fbt/macro calls.
63+
*
64+
* This is the key data structure that powers the scope merging: we start
65+
* at the lvalues and merge operands into the lvalue's scope.
66+
*/
67+
const macroValues: Map<Identifier, Array<Identifier>> = new Map();
68+
// Tracks methods loaded from macros, like fbt.param or idx.foo
5269
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;
70+
71+
visit(fn, fbtMacroTags, macroTagsCalls, macroMethods, macroValues);
72+
73+
for (const root of macroValues.keys()) {
74+
const scope = root.scope;
75+
if (scope == null) {
76+
continue;
77+
}
78+
// Merge the operands into the same scope if this is a known macro invocation
79+
if (!macroTagsCalls.has(root.id)) {
80+
continue;
5981
}
82+
mergeScopes(root, scope, macroValues, macroTagsCalls);
6083
}
61-
return fbtValues;
84+
85+
return macroTagsCalls;
6286
}
6387

6488
export const FBT_TAGS: Set<string> = new Set([
6589
'fbt',
6690
'fbt:param',
91+
'fbt:enum',
92+
'fbt:plural',
6793
'fbs',
6894
'fbs:param',
95+
'fbs:enum',
96+
'fbs:plural',
6997
]);
7098
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
7199
'fbt:param',
@@ -75,10 +103,22 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
75103
function visit(
76104
fn: HIRFunction,
77105
fbtMacroTags: Set<Macro>,
78-
fbtValues: Set<IdentifierId>,
106+
macroTagsCalls: Set<IdentifierId>,
79107
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
108+
macroValues: Map<Identifier, Array<Identifier>>,
80109
): void {
81110
for (const [, block] of fn.body.blocks) {
111+
for (const phi of block.phis) {
112+
const macroOperands: Array<Identifier> = [];
113+
for (const operand of phi.operands.values()) {
114+
if (macroValues.has(operand.identifier)) {
115+
macroOperands.push(operand.identifier);
116+
}
117+
}
118+
if (macroOperands.length !== 0) {
119+
macroValues.set(phi.place.identifier, macroOperands);
120+
}
121+
}
82122
for (const instruction of block.instructions) {
83123
const {lvalue, value} = instruction;
84124
if (lvalue === null) {
@@ -93,13 +133,13 @@ function visit(
93133
* We don't distinguish between tag names and strings, so record
94134
* all `fbt` string literals in case they are used as a jsx tag.
95135
*/
96-
fbtValues.add(lvalue.identifier.id);
136+
macroTagsCalls.add(lvalue.identifier.id);
97137
} else if (
98138
value.kind === 'LoadGlobal' &&
99139
matchesExactTag(value.binding.name, fbtMacroTags)
100140
) {
101141
// Record references to `fbt` as a global
102-
fbtValues.add(lvalue.identifier.id);
142+
macroTagsCalls.add(lvalue.identifier.id);
103143
} else if (
104144
value.kind === 'LoadGlobal' &&
105145
matchTagRoot(value.binding.name, fbtMacroTags) !== null
@@ -121,84 +161,66 @@ function visit(
121161
if (method.length > 1) {
122162
newMethods.push(method.slice(1));
123163
} else {
124-
fbtValues.add(lvalue.identifier.id);
164+
macroTagsCalls.add(lvalue.identifier.id);
125165
}
126166
}
127167
}
128168
if (newMethods.length > 0) {
129169
macroMethods.set(lvalue.identifier.id, newMethods);
130170
}
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-
}
149171
} else if (
150-
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
151-
isFbtJsxChild(fbtValues, lvalue, value)
172+
value.kind === 'PropertyLoad' &&
173+
macroTagsCalls.has(value.object.identifier.id)
152174
) {
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;
175+
macroTagsCalls.add(lvalue.identifier.id);
176+
} else if (
177+
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
178+
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
179+
isFbtCallExpression(macroTagsCalls, value)
180+
) {
181+
macroTagsCalls.add(lvalue.identifier.id);
182+
macroValues.set(
183+
lvalue.identifier,
184+
Array.from(
185+
eachInstructionValueOperand(value),
186+
operand => operand.identifier,
187+
),
188+
);
189+
} else if (
190+
Iterable_some(eachInstructionValueOperand(value), operand =>
191+
macroValues.has(operand.identifier),
192+
)
193+
) {
194+
const macroOperands: Array<Identifier> = [];
195+
for (const operand of eachInstructionValueOperand(value)) {
196+
if (macroValues.has(operand.identifier)) {
197+
macroOperands.push(operand.identifier);
191198
}
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);
196199
}
200+
macroValues.set(lvalue.identifier, macroOperands);
197201
}
198202
}
199203
}
200204
}
201205

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

231253
function isFbtCallExpression(
232-
fbtValues: Set<IdentifierId>,
233-
value: ReactiveValue,
254+
macroTagsCalls: Set<IdentifierId>,
255+
value: InstructionValue,
234256
): boolean {
235257
return (
236258
(value.kind === 'CallExpression' &&
237-
fbtValues.has(value.callee.identifier.id)) ||
238-
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
259+
macroTagsCalls.has(value.callee.identifier.id)) ||
260+
(value.kind === 'MethodCall' &&
261+
macroTagsCalls.has(value.property.identifier.id))
239262
);
240263
}
241264

242265
function isFbtJsxExpression(
243266
fbtMacroTags: Set<Macro>,
244-
fbtValues: Set<IdentifierId>,
245-
value: ReactiveValue,
267+
macroTagsCalls: Set<IdentifierId>,
268+
value: InstructionValue,
246269
): boolean {
247270
return (
248271
value.kind === 'JsxExpression' &&
249272
((value.tag.kind === 'Identifier' &&
250-
fbtValues.has(value.tag.identifier.id)) ||
273+
macroTagsCalls.has(value.tag.identifier.id)) ||
251274
(value.tag.kind === 'BuiltinTag' &&
252275
matchesExactTag(value.tag.name, fbtMacroTags)))
253276
);
254277
}
255278

256279
function isFbtJsxChild(
257-
fbtValues: Set<IdentifierId>,
280+
macroTagsCalls: Set<IdentifierId>,
258281
lvalue: Place | null,
259-
value: ReactiveValue,
282+
value: InstructionValue,
260283
): boolean {
261284
return (
262285
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
263286
lvalue !== null &&
264-
fbtValues.has(lvalue.identifier.id)
287+
macroTagsCalls.has(lvalue.identifier.id)
265288
);
266289
}
267290

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)