Skip to content

Commit 92a8f7a

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 92a8f7a

11 files changed

+359
-169
lines changed

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

Lines changed: 107 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,47 @@ 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',
6792
'fbs',
6893
'fbs:param',
94+
'fbs:enum',
6995
]);
7096
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
7197
'fbt:param',
@@ -75,10 +101,22 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
75101
function visit(
76102
fn: HIRFunction,
77103
fbtMacroTags: Set<Macro>,
78-
fbtValues: Set<IdentifierId>,
104+
macroTagsCalls: Set<IdentifierId>,
79105
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
106+
macroValues: Map<Identifier, Array<Identifier>>,
80107
): void {
81108
for (const [, block] of fn.body.blocks) {
109+
for (const phi of block.phis) {
110+
const fbtOperands: Array<Identifier> = [];
111+
for (const operand of phi.operands.values()) {
112+
if (macroValues.has(operand.identifier)) {
113+
fbtOperands.push(operand.identifier);
114+
}
115+
}
116+
if (fbtOperands.length !== 0) {
117+
macroValues.set(phi.place.identifier, fbtOperands);
118+
}
119+
}
82120
for (const instruction of block.instructions) {
83121
const {lvalue, value} = instruction;
84122
if (lvalue === null) {
@@ -93,13 +131,13 @@ function visit(
93131
* We don't distinguish between tag names and strings, so record
94132
* all `fbt` string literals in case they are used as a jsx tag.
95133
*/
96-
fbtValues.add(lvalue.identifier.id);
134+
macroTagsCalls.add(lvalue.identifier.id);
97135
} else if (
98136
value.kind === 'LoadGlobal' &&
99137
matchesExactTag(value.binding.name, fbtMacroTags)
100138
) {
101139
// Record references to `fbt` as a global
102-
fbtValues.add(lvalue.identifier.id);
140+
macroTagsCalls.add(lvalue.identifier.id);
103141
} else if (
104142
value.kind === 'LoadGlobal' &&
105143
matchTagRoot(value.binding.name, fbtMacroTags) !== null
@@ -121,84 +159,66 @@ function visit(
121159
if (method.length > 1) {
122160
newMethods.push(method.slice(1));
123161
} else {
124-
fbtValues.add(lvalue.identifier.id);
162+
macroTagsCalls.add(lvalue.identifier.id);
125163
}
126164
}
127165
}
128166
if (newMethods.length > 0) {
129167
macroMethods.set(lvalue.identifier.id, newMethods);
130168
}
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-
}
149169
} else if (
150-
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
151-
isFbtJsxChild(fbtValues, lvalue, value)
170+
value.kind === 'PropertyLoad' &&
171+
macroTagsCalls.has(value.object.identifier.id)
152172
) {
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;
173+
macroTagsCalls.add(lvalue.identifier.id);
174+
} else if (
175+
isFbtJsxExpression(fbtMacroTags, macroTagsCalls, value) ||
176+
isFbtJsxChild(macroTagsCalls, lvalue, value) ||
177+
isFbtCallExpression(macroTagsCalls, value)
178+
) {
179+
macroTagsCalls.add(lvalue.identifier.id);
180+
macroValues.set(
181+
lvalue.identifier,
182+
Array.from(
183+
eachInstructionValueOperand(value),
184+
operand => operand.identifier,
185+
),
186+
);
187+
} else if (
188+
Iterable_some(eachInstructionValueOperand(value), operand =>
189+
macroValues.has(operand.identifier),
190+
)
191+
) {
192+
const fbtOperands: Array<Identifier> = [];
193+
for (const operand of eachInstructionValueOperand(value)) {
194+
if (macroValues.has(operand.identifier)) {
195+
fbtOperands.push(operand.identifier);
191196
}
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);
196197
}
198+
macroValues.set(lvalue.identifier, fbtOperands);
197199
}
198200
}
199201
}
200202
}
201203

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

231251
function isFbtCallExpression(
232-
fbtValues: Set<IdentifierId>,
233-
value: ReactiveValue,
252+
macroTagsCalls: Set<IdentifierId>,
253+
value: InstructionValue,
234254
): boolean {
235255
return (
236256
(value.kind === 'CallExpression' &&
237-
fbtValues.has(value.callee.identifier.id)) ||
238-
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
257+
macroTagsCalls.has(value.callee.identifier.id)) ||
258+
(value.kind === 'MethodCall' &&
259+
macroTagsCalls.has(value.property.identifier.id))
239260
);
240261
}
241262

242263
function isFbtJsxExpression(
243264
fbtMacroTags: Set<Macro>,
244-
fbtValues: Set<IdentifierId>,
245-
value: ReactiveValue,
265+
macroTagsCalls: Set<IdentifierId>,
266+
value: InstructionValue,
246267
): boolean {
247268
return (
248269
value.kind === 'JsxExpression' &&
249270
((value.tag.kind === 'Identifier' &&
250-
fbtValues.has(value.tag.identifier.id)) ||
271+
macroTagsCalls.has(value.tag.identifier.id)) ||
251272
(value.tag.kind === 'BuiltinTag' &&
252273
matchesExactTag(value.tag.name, fbtMacroTags)))
253274
);
254275
}
255276

256277
function isFbtJsxChild(
257-
fbtValues: Set<IdentifierId>,
278+
macroTagsCalls: Set<IdentifierId>,
258279
lvalue: Place | null,
259-
value: ReactiveValue,
280+
value: InstructionValue,
260281
): boolean {
261282
return (
262283
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
263284
lvalue !== null &&
264-
fbtValues.has(lvalue.identifier.id)
285+
macroTagsCalls.has(lvalue.identifier.id)
265286
);
266287
}
267288

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.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-param-with-leading-whitespace.expect.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,23 @@ import fbt from "fbt";
4444
import { identity } from "shared-runtime";
4545

4646
function Component(props) {
47-
const $ = _c(3);
47+
const $ = _c(5);
4848
let t0;
4949
if ($[0] !== props.count || $[1] !== props.option) {
50+
let t1;
51+
if ($[3] !== props.count) {
52+
t1 = identity(props.count);
53+
$[3] = props.count;
54+
$[4] = t1;
55+
} else {
56+
t1 = $[4];
57+
}
5058
t0 = (
5159
<span>
5260
{fbt._(
5361
{ "*": "{count} votes for {option}", _1: "1 vote for {option}" },
5462
[
55-
fbt._plural(identity(props.count), "count"),
63+
fbt._plural(t1, "count"),
5664
fbt._param(
5765
"option",
5866

0 commit comments

Comments
 (0)