@samitouri / QOS-React / commits / adbc32de32

[compiler] More fbt compatibility (#34887)

In my previous PR I fixed some cases but broke others. So, new approach. Two phase algorithm: * First pass is forward data flow to determine all usages of macros. This is necessary because many of Meta's macros have variants that can be accessed via properties, eg you can do `macro(...)` but also `macro.variant(...)`. * Second pass is backwards data flow to find macro invocations (JSX and calls) and then merge their operands into the same scope as the macro call. Note that this required updating PromoteUsedTemporaries to avoid promoting macro calls that have interposing instructions between their creation and usage. Macro calls in general are pure so it should be safe to reorder them. In addition, we're now more precise about `<fb:plural>`, `<fbt:param>`, `fbt.plural()` and `fbt.param()`, which don't actually require all their arguments to be inlined. The whole point is that the plural/param value is an arbitrary value (along with a string name). So we no longer transitively inline the arguments, we just make sure that they don't get inadvertently promoted to named variables. One caveat: we actually don't do anything to treat macro functions as non-mutating, so `fbt.plural()` and friends (function form) may still sometimes group arguments just due to mutability inference. In a follow-up, i'll work to infer the types of nested macro functions as non-mutating. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34887). * #34900 * __->__ #34887

Joseph Savona committed Oct 17, 2025 at 11:37 UTC adbc32de32bc52f9014cedb5ff5a502be35aff51
16 files changed +640 -294
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+1 -11
@@ -83,21 +83,11 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
83 export const USE_FIRE_FUNCTION_NAME = 'useFire';
84 export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
85
86 -export const MacroMethodSchema = z.union([
87 - z.object({type: z.literal('wildcard')}),
88 - z.object({type: z.literal('name'), name: z.string()}),
89 -]);
90 -
91 -// Would like to change this to drop the string option, but breaks compatibility with existing configs
92 -export const MacroSchema = z.union([
93 - z.string(),
94 - z.tuple([z.string(), z.array(MacroMethodSchema)]),
95 -]);
86 +export const MacroSchema = z.string();
87
88 export type CompilerMode = 'all_features' | 'no_inferred_memo';
89
90 export type Macro = z.infer<typeof MacroSchema>;
100 -export type MacroMethod = z.infer<typeof MacroMethodSchema>;
91
92 const HookSchema = z.object({
93 /*
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts
+218 -214
@@ -7,7 +7,6 @@
7
8 import {
9 HIRFunction,
10 - Identifier,
10 IdentifierId,
11 InstructionValue,
12 makeInstructionId,
@@ -15,9 +14,35 @@ import {
14 Place,
15 ReactiveScope,
16 } from '../HIR';
18 -import {Macro, MacroMethod} from '../HIR/Environment';
17 +import {Macro} from '../HIR/Environment';
18 import {eachInstructionValueOperand} from '../HIR/visitors';
20 -import {Iterable_some} from '../Utils/utils';
19 +
20 +/**
21 + * Whether a macro requires its arguments to be transitively inlined (eg fbt)
22 + * or just avoid having the top-level values be converted to variables (eg fbt.param)
23 + */
24 +enum InlineLevel {
25 + Transitive = 'Transitive',
26 + Shallow = 'Shallow',
27 +}
28 +type MacroDefinition = {
29 + level: InlineLevel;
30 + properties: Map<string, MacroDefinition> | null;
31 +};
32 +
33 +const SHALLOW_MACRO: MacroDefinition = {
34 + level: InlineLevel.Shallow,
35 + properties: null,
36 +};
37 +const TRANSITIVE_MACRO: MacroDefinition = {
38 + level: InlineLevel.Transitive,
39 + properties: null,
40 +};
41 +const FBT_MACRO: MacroDefinition = {
42 + level: InlineLevel.Transitive,
43 + properties: new Map([['*', SHALLOW_MACRO]]),
44 +};
45 +FBT_MACRO.properties!.set('enum', FBT_MACRO);
46
47 /**
48 * This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
@@ -42,250 +67,210 @@ import {Iterable_some} from '../Utils/utils';
67 * ## User-defined macro-like function
68 *
69 * Users can also specify their own functions to be treated similarly to fbt via the
45 - * `customMacros` environment configuration.
70 + * `customMacros` environment configuration. By default, user-supplied custom macros
71 + * have their arguments transitively inlined.
72 */
73 export function memoizeFbtAndMacroOperandsInSameScope(
74 fn: HIRFunction,
75 ): Set<IdentifierId> {
50 - const fbtMacroTags = new Set<Macro>([
51 - ...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
52 - ...(fn.env.config.customMacros ?? []),
76 + const macroKinds = new Map<Macro, MacroDefinition>([
77 + ...Array.from(FBT_TAGS.entries()),
78 + ...(fn.env.config.customMacros ?? []).map(
79 + name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
80 + ),
81 ]);
82 /**
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
83 + * Forward data-flow analysis to identify all macro tags, including
84 + * things like `fbt.foo.bar(...)`
85 */
58 - const macroTagsCalls: Set<IdentifierId> = new Set();
86 + const macroTags = populateMacroTags(fn, macroKinds);
87 +
88 /**
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.
89 + * Reverse data-flow analysis to merge arguments to macro *invocations*
90 + * based on the kind of the macro
91 */
67 - const macroValues: Map<Identifier, Array<Identifier>> = new Map();
68 - // Tracks methods loaded from macros, like fbt.param or idx.foo
69 - const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
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;
81 - }
82 - mergeScopes(root, scope, macroValues, macroTagsCalls);
83 - }
92 + const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
93
85 - return macroTagsCalls;
94 + return macroValues;
95 }
96
88 -export const FBT_TAGS: Set<string> = new Set([
89 - 'fbt',
90 - 'fbt:param',
91 - 'fbt:enum',
92 - 'fbt:plural',
93 - 'fbs',
94 - 'fbs:param',
95 - 'fbs:enum',
96 - 'fbs:plural',
97 +const FBT_TAGS: Map<string, MacroDefinition> = new Map([
98 + ['fbt', FBT_MACRO],
99 + ['fbt:param', SHALLOW_MACRO],
100 + ['fbt:enum', FBT_MACRO],
101 + ['fbt:plural', SHALLOW_MACRO],
102 + ['fbs', FBT_MACRO],
103 + ['fbs:param', SHALLOW_MACRO],
104 + ['fbs:enum', FBT_MACRO],
105 + ['fbs:plural', SHALLOW_MACRO],
106 ]);
107 export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
108 'fbt:param',
109 'fbs:param',
110 ]);
111
103 -function visit(
112 +function populateMacroTags(
113 fn: HIRFunction,
105 - fbtMacroTags: Set<Macro>,
106 - macroTagsCalls: Set<IdentifierId>,
107 - macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
108 - macroValues: Map<Identifier, Array<Identifier>>,
109 -): void {
110 - 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 - }
122 - for (const instruction of block.instructions) {
123 - const {lvalue, value} = instruction;
124 - if (lvalue === null) {
125 - continue;
126 - }
127 - if (
128 - value.kind === 'Primitive' &&
129 - typeof value.value === 'string' &&
130 - matchesExactTag(value.value, fbtMacroTags)
131 - ) {
132 - /*
133 - * We don't distinguish between tag names and strings, so record
134 - * all `fbt` string literals in case they are used as a jsx tag.
135 - */
136 - macroTagsCalls.add(lvalue.identifier.id);
137 - } else if (
138 - value.kind === 'LoadGlobal' &&
139 - matchesExactTag(value.binding.name, fbtMacroTags)
140 - ) {
141 - // Record references to `fbt` as a global
142 - macroTagsCalls.add(lvalue.identifier.id);
143 - } else if (
144 - value.kind === 'LoadGlobal' &&
145 - matchTagRoot(value.binding.name, fbtMacroTags) !== null
146 - ) {
147 - const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
148 - macroMethods.set(lvalue.identifier.id, methods);
149 - } else if (
150 - value.kind === 'PropertyLoad' &&
151 - macroMethods.has(value.object.identifier.id)
152 - ) {
153 - const methods = macroMethods.get(value.object.identifier.id)!;
154 - const newMethods = [];
155 - for (const method of methods) {
156 - if (
157 - method.length > 0 &&
158 - (method[0].type === 'wildcard' ||
159 - (method[0].type === 'name' && method[0].name === value.property))
160 - ) {
161 - if (method.length > 1) {
162 - newMethods.push(method.slice(1));
163 - } else {
164 - macroTagsCalls.add(lvalue.identifier.id);
114 + macroKinds: Map<Macro, MacroDefinition>,
115 +): Map<IdentifierId, MacroDefinition> {
116 + const macroTags = new Map<IdentifierId, MacroDefinition>();
117 + for (const block of fn.body.blocks.values()) {
118 + for (const instr of block.instructions) {
119 + const {lvalue, value} = instr;
120 + switch (value.kind) {
121 + case 'Primitive': {
122 + if (typeof value.value === 'string') {
123 + const macroDefinition = macroKinds.get(value.value);
124 + if (macroDefinition != null) {
125 + /*
126 + * We don't distinguish between tag names and strings, so record
127 + * all `fbt` string literals in case they are used as a jsx tag.
128 + */
129 + macroTags.set(lvalue.identifier.id, macroDefinition);
130 }
131 }
132 + break;
133 }
168 - if (newMethods.length > 0) {
169 - macroMethods.set(lvalue.identifier.id, newMethods);
134 + case 'LoadGlobal': {
135 + let macroDefinition = macroKinds.get(value.binding.name);
136 + if (macroDefinition != null) {
137 + macroTags.set(lvalue.identifier.id, macroDefinition);
138 + }
139 + break;
140 }
171 - } else if (
172 - value.kind === 'PropertyLoad' &&
173 - macroTagsCalls.has(value.object.identifier.id)
174 - ) {
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);
141 + case 'PropertyLoad': {
142 + if (typeof value.property === 'string') {
143 + const macroDefinition = macroTags.get(value.object.identifier.id);
144 + if (macroDefinition != null) {
145 + const propertyDefinition =
146 + macroDefinition.properties != null
147 + ? (macroDefinition.properties.get(value.property) ??
148 + macroDefinition.properties.get('*'))
149 + : null;
150 + const propertyMacro = propertyDefinition ?? macroDefinition;
151 + macroTags.set(lvalue.identifier.id, propertyMacro);
152 + }
153 }
154 + break;
155 }
200 - macroValues.set(lvalue.identifier, macroOperands);
156 }
157 }
158 }
159 + return macroTags;
160 }
161
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 -
224 -function matchesExactTag(s: string, tags: Set<Macro>): boolean {
225 - return Array.from(tags).some(macro =>
226 - typeof macro === 'string'
227 - ? s === macro
228 - : macro[1].length === 0 && macro[0] === s,
229 - );
230 -}
231 -
232 -function matchTagRoot(
233 - s: string,
234 - tags: Set<Macro>,
235 -): Array<Array<MacroMethod>> | null {
236 - const methods: Array<Array<MacroMethod>> = [];
237 - for (const macro of tags) {
238 - if (typeof macro === 'string') {
239 - continue;
162 +function mergeMacroArguments(
163 + fn: HIRFunction,
164 + macroTags: Map<IdentifierId, MacroDefinition>,
165 + macroKinds: Map<Macro, MacroDefinition>,
166 +): Set<IdentifierId> {
167 + const macroValues = new Set<IdentifierId>(macroTags.keys());
168 + for (const block of Array.from(fn.body.blocks.values()).reverse()) {
169 + for (let i = block.instructions.length - 1; i >= 0; i--) {
170 + const instr = block.instructions[i]!;
171 + const {lvalue, value} = instr;
172 + switch (value.kind) {
173 + case 'DeclareContext':
174 + case 'DeclareLocal':
175 + case 'Destructure':
176 + case 'LoadContext':
177 + case 'LoadLocal':
178 + case 'PostfixUpdate':
179 + case 'PrefixUpdate':
180 + case 'StoreContext':
181 + case 'StoreLocal': {
182 + // Instructions that never need to be merged
183 + break;
184 + }
185 + case 'CallExpression':
186 + case 'MethodCall': {
187 + const scope = lvalue.identifier.scope;
188 + if (scope == null) {
189 + continue;
190 + }
191 + const callee =
192 + value.kind === 'CallExpression' ? value.callee : value.property;
193 + const macroDefinition =
194 + macroTags.get(callee.identifier.id) ??
195 + macroTags.get(lvalue.identifier.id);
196 + if (macroDefinition != null) {
197 + visitOperands(
198 + macroDefinition,
199 + scope,
200 + lvalue,
201 + value,
202 + macroValues,
203 + macroTags,
204 + );
205 + }
206 + break;
207 + }
208 + case 'JsxExpression': {
209 + const scope = lvalue.identifier.scope;
210 + if (scope == null) {
211 + continue;
212 + }
213 + let macroDefinition;
214 + if (value.tag.kind === 'Identifier') {
215 + macroDefinition = macroTags.get(value.tag.identifier.id);
216 + } else {
217 + macroDefinition = macroKinds.get(value.tag.name);
218 + }
219 + macroDefinition ??= macroTags.get(lvalue.identifier.id);
220 + if (macroDefinition != null) {
221 + visitOperands(
222 + macroDefinition,
223 + scope,
224 + lvalue,
225 + value,
226 + macroValues,
227 + macroTags,
228 + );
229 + }
230 + break;
231 + }
232 + default: {
233 + const scope = lvalue.identifier.scope;
234 + if (scope == null) {
235 + continue;
236 + }
237 + const macroDefinition = macroTags.get(lvalue.identifier.id);
238 + if (macroDefinition != null) {
239 + visitOperands(
240 + macroDefinition,
241 + scope,
242 + lvalue,
243 + value,
244 + macroValues,
245 + macroTags,
246 + );
247 + }
248 + break;
249 + }
250 + }
251 }
241 - const [tag, rest] = macro;
242 - if (tag === s && rest.length > 0) {
243 - methods.push(rest);
252 + for (const phi of block.phis) {
253 + const scope = phi.place.identifier.scope;
254 + if (scope == null) {
255 + continue;
256 + }
257 + const macroDefinition = macroTags.get(phi.place.identifier.id);
258 + if (
259 + macroDefinition == null ||
260 + macroDefinition.level === InlineLevel.Shallow
261 + ) {
262 + continue;
263 + }
264 + macroValues.add(phi.place.identifier.id);
265 + for (const operand of phi.operands.values()) {
266 + operand.identifier.scope = scope;
267 + expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
268 + macroTags.set(operand.identifier.id, macroDefinition);
269 + macroValues.add(operand.identifier.id);
270 + }
271 }
272 }
246 - if (methods.length > 0) {
247 - return methods;
248 - } else {
249 - return null;
250 - }
251 -}
252 -
253 -function isFbtCallExpression(
254 - macroTagsCalls: Set<IdentifierId>,
255 - value: InstructionValue,
256 -): boolean {
257 - return (
258 - (value.kind === 'CallExpression' &&
259 - macroTagsCalls.has(value.callee.identifier.id)) ||
260 - (value.kind === 'MethodCall' &&
261 - macroTagsCalls.has(value.property.identifier.id))
262 - );
263 -}
264 -
265 -function isFbtJsxExpression(
266 - fbtMacroTags: Set<Macro>,
267 - macroTagsCalls: Set<IdentifierId>,
268 - value: InstructionValue,
269 -): boolean {
270 - return (
271 - value.kind === 'JsxExpression' &&
272 - ((value.tag.kind === 'Identifier' &&
273 - macroTagsCalls.has(value.tag.identifier.id)) ||
274 - (value.tag.kind === 'BuiltinTag' &&
275 - matchesExactTag(value.tag.name, fbtMacroTags)))
276 - );
277 -}
278 -
279 -function isFbtJsxChild(
280 - macroTagsCalls: Set<IdentifierId>,
281 - lvalue: Place | null,
282 - value: InstructionValue,
283 -): boolean {
284 - return (
285 - (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
286 - lvalue !== null &&
287 - macroTagsCalls.has(lvalue.identifier.id)
288 - );
273 + return macroValues;
274 }
275
276 function expandFbtScopeRange(
@@ -298,3 +283,22 @@ function expandFbtScopeRange(
283 );
284 }
285 }
286 +
287 +function visitOperands(
288 + macroDefinition: MacroDefinition,
289 + scope: ReactiveScope,
290 + lvalue: Place,
291 + value: InstructionValue,
292 + macroValues: Set<IdentifierId>,
293 + macroTags: Map<IdentifierId, MacroDefinition>,
294 +): void {
295 + macroValues.add(lvalue.identifier.id);
296 + for (const operand of eachInstructionValueOperand(value)) {
297 + if (macroDefinition.level === InlineLevel.Transitive) {
298 + operand.identifier.scope = scope;
299 + expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
300 + macroTags.set(operand.identifier.id, macroDefinition);
301 + }
302 + macroValues.add(operand.identifier.id);
303 + }
304 +}
compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts
+1 -10
@@ -135,16 +135,7 @@ function parseConfigPragmaEnvironmentForTest(
135 } else if (val) {
136 const parsedVal = tryParseTestPragmaValue(val).unwrap();
137 if (key === 'customMacros' && typeof parsedVal === 'string') {
138 - const valSplit = parsedVal.split('.');
139 - const props = [];
140 - for (const elt of valSplit.slice(1)) {
141 - if (elt === '*') {
142 - props.push({type: 'wildcard'});
143 - } else if (elt.length > 0) {
144 - props.push({type: 'name', name: elt});
145 - }
146 - }
147 - maybeConfig[key] = [[valSplit[0], props]];
138 + maybeConfig[key] = [parsedVal.split('.')[0]];
139 continue;
140 }
141 maybeConfig[key] = parsedVal;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-param-with-leading-whitespace.expect.md
+10 -2
@@ -44,15 +44,23 @@ import fbt from "fbt";
44 import { identity } from "shared-runtime";
45
46 function Component(props) {
47 - const $ = _c(3);
47 + const $ = _c(5);
48 let t0;
49 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 + }
58 t0 = (
59 <span>
60 {fbt._(
61 { "*": "{count} votes for {option}", _1: "1 vote for {option}" },
62 [
55 - fbt._plural(identity(props.count), "count"),
63 + fbt._plural(t1, "count"),
64 fbt._param(
65 "option",
66
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-param-with-trailing-whitespace.expect.md
+10 -2
@@ -44,15 +44,23 @@ import fbt from "fbt";
44 import { identity } from "shared-runtime";
45
46 function Component(props) {
47 - const $ = _c(3);
47 + const $ = _c(5);
48 let t0;
49 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 + }
58 t0 = (
59 <span>
60 {fbt._(
61 { "*": "{count} votes for {option}", _1: "1 vote for {option}" },
62 [
55 - fbt._plural(identity(props.count), "count"),
63 + fbt._plural(t1, "count"),
64 fbt._param(
65 "option",
66
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-preserve-whitespace-two-subtrees.expect.md
+25 -10
@@ -37,7 +37,7 @@ import { c as _c } from "react/compiler-runtime";
37 import fbt from "fbt";
38
39 function Foo(t0) {
40 - const $ = _c(7);
40 + const $ = _c(13);
41 const { name1, name2 } = t0;
42 let t1;
43 if ($[0] !== name1 || $[1] !== name2) {
@@ -50,19 +50,34 @@ function Foo(t0) {
50 t2 = $[4];
51 }
52 let t3;
53 - if ($[5] !== name2) {
54 - t3 = <b>{name2}</b>;
55 - $[5] = name2;
56 - $[6] = t3;
53 + if ($[5] !== name1 || $[6] !== t2) {
54 + t3 = <span key={name1}>{t2}</span>;
55 + $[5] = name1;
56 + $[6] = t2;
57 + $[7] = t3;
58 } else {
58 - t3 = $[6];
59 + t3 = $[7];
60 + }
61 + let t4;
62 + if ($[8] !== name2) {
63 + t4 = <b>{name2}</b>;
64 + $[8] = name2;
65 + $[9] = t4;
66 + } else {
67 + t4 = $[9];
68 + }
69 + let t5;
70 + if ($[10] !== name2 || $[11] !== t4) {
71 + t5 = <span key={name2}>{t4}</span>;
72 + $[10] = name2;
73 + $[11] = t4;
74 + $[12] = t5;
75 + } else {
76 + t5 = $[12];
77 }
78 t1 = fbt._(
79 "{user1} and {user2} accepted your PR!",
62 - [
63 - fbt._param("user1", <span key={name1}>{t2}</span>),
64 - fbt._param("user2", <span key={name2}>{t3}</span>),
65 - ],
80 + [fbt._param("user1", t3), fbt._param("user2", t5)],
81 { hk: "2PxMie" },
82 );
83 $[0] = name1;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbtparam-with-jsx-element-content.expect.md
+10 -6
@@ -29,20 +29,24 @@ import { c as _c } from "react/compiler-runtime";
29 import fbt from "fbt";
30
31 function Component(t0) {
32 - const $ = _c(4);
32 + const $ = _c(6);
33 const { name, data, icon } = t0;
34 let t1;
35 if ($[0] !== data || $[1] !== icon || $[2] !== name) {
36 + let t2;
37 + if ($[4] !== name) {
38 + t2 = <Text type="h4">{name}</Text>;
39 + $[4] = name;
40 + $[5] = t2;
41 + } else {
42 + t2 = $[5];
43 + }
44 t1 = (
45 <Text type="body4">
46 {fbt._(
47 "{item author}{icon}{=m2}",
48 [
41 - fbt._param(
42 - "item author",
43 -
44 - <Text type="h4">{name}</Text>,
45 - ),
49 + fbt._param("item author", t2),
50 fbt._param(
51 "icon",
52
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbtparam-with-jsx-fragment-value.expect.md
+11 -6
@@ -27,16 +27,21 @@ import fbt from "fbt";
27 import { identity } from "shared-runtime";
28
29 function Component(props) {
30 - const $ = _c(2);
30 + const $ = _c(4);
31 let t0;
32 if ($[0] !== props.text) {
33 + const t1 = identity(props.text);
34 + let t2;
35 + if ($[2] !== t1) {
36 + t2 = <>{t1}</>;
37 + $[2] = t1;
38 + $[3] = t2;
39 + } else {
40 + t2 = $[3];
41 + }
42 t0 = (
43 <Foo
35 - value={fbt._(
36 - "{value}%",
37 - [fbt._param("value", <>{identity(props.text)}</>)],
38 - { hk: "10F5Cc" },
39 - )}
44 + value={fbt._("{value}%", [fbt._param("value", t2)], { hk: "10F5Cc" })}
45 />
46 );
47 $[0] = props.text;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/recursively-merge-scopes-jsx.expect.md new
+109
@@ -0,0 +1,109 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow
6 +import {fbt} from 'fbt';
7 +
8 +function Example({x}) {
9 + // "Inner Text" needs to be visible to fbt: the <Bar> element cannot
10 + // be memoized separately
11 + return (
12 + <fbt desc="Description">
13 + Outer Text
14 + <Foo key="b" x={x}>
15 + <Bar key="a">Inner Text</Bar>
16 + </Foo>
17 + </fbt>
18 + );
19 +}
20 +
21 +function Foo({x, children}) {
22 + 'use no memo';
23 + return (
24 + <>
25 + <div>{x}</div>
26 + <span>{children}</span>
27 + </>
28 + );
29 +}
30 +
31 +function Bar({children}) {
32 + 'use no memo';
33 + return children;
34 +}
35 +
36 +export const FIXTURE_ENTRYPOINT = {
37 + fn: Example,
38 + params: [{x: 'Hello'}],
39 +};
40 +
41 +```
42 +
43 +## Code
44 +
45 +```javascript
46 +import { c as _c } from "react/compiler-runtime";
47 +import { fbt } from "fbt";
48 +
49 +function Example(t0) {
50 + const $ = _c(2);
51 + const { x } = t0;
52 + let t1;
53 + if ($[0] !== x) {
54 + t1 = fbt._(
55 + "Outer Text {=m1}",
56 + [
57 + fbt._implicitParam(
58 + "=m1",
59 +
60 + <Foo key="b" x={x}>
61 + {fbt._(
62 + "{=m1}",
63 + [
64 + fbt._implicitParam(
65 + "=m1",
66 + <Bar key="a">
67 + {fbt._("Inner Text", null, { hk: "32YB0l" })}
68 + </Bar>,
69 + ),
70 + ],
71 + { hk: "23dJsI" },
72 + )}
73 + </Foo>,
74 + ),
75 + ],
76 + { hk: "2RVA7V" },
77 + );
78 + $[0] = x;
79 + $[1] = t1;
80 + } else {
81 + t1 = $[1];
82 + }
83 + return t1;
84 +}
85 +
86 +function Foo({ x, children }) {
87 + "use no memo";
88 + return (
89 + <>
90 + <div>{x}</div>
91 + <span>{children}</span>
92 + </>
93 + );
94 +}
95 +
96 +function Bar({ children }) {
97 + "use no memo";
98 + return children;
99 +}
100 +
101 +export const FIXTURE_ENTRYPOINT = {
102 + fn: Example,
103 + params: [{ x: "Hello" }],
104 +};
105 +
106 +```
107 +
108 +### Eval output
109 +(kind: ok) Outer Text <div>Hello</div><span>Inner Text</span>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/recursively-merge-scopes-jsx.js new
+35
@@ -0,0 +1,35 @@
1 +// @flow
2 +import {fbt} from 'fbt';
3 +
4 +function Example({x}) {
5 + // "Inner Text" needs to be visible to fbt: the <Bar> element cannot
6 + // be memoized separately
7 + return (
8 + <fbt desc="Description">
9 + Outer Text
10 + <Foo key="b" x={x}>
11 + <Bar key="a">Inner Text</Bar>
12 + </Foo>
13 + </fbt>
14 + );
15 +}
16 +
17 +function Foo({x, children}) {
18 + 'use no memo';
19 + return (
20 + <>
21 + <div>{x}</div>
22 + <span>{children}</span>
23 + </>
24 + );
25 +}
26 +
27 +function Bar({children}) {
28 + 'use no memo';
29 + return children;
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: Example,
34 + params: [{x: 'Hello'}],
35 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/repro-fbt-param-nested-fbt-jsx.expect.md new
+128
@@ -0,0 +1,128 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import fbt from 'fbt';
6 +import {Stringify, identity} from 'shared-runtime';
7 +
8 +/**
9 + * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
10 + * Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
11 + * /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
12 + * ---
13 + * t3
14 + * ---
15 + */
16 +function Component({firstname, lastname}) {
17 + 'use memo';
18 + return (
19 + <div>
20 + {fbt(
21 + [
22 + 'Name: ',
23 + fbt.param('firstname', <Stringify key={0} name={firstname} />),
24 + ', ',
25 + fbt.param(
26 + 'lastname',
27 + identity(
28 + fbt(
29 + '(inner)' +
30 + fbt.param('lastname', <Stringify key={1} name={lastname} />),
31 + 'Inner fbt value'
32 + )
33 + )
34 + ),
35 + ],
36 + 'Name'
37 + )}
38 + </div>
39 + );
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Component,
44 + params: [{firstname: 'first', lastname: 'last'}],
45 + sequentialRenders: [{firstname: 'first', lastname: 'last'}],
46 +};
47 +
48 +```
49 +
50 +## Code
51 +
52 +```javascript
53 +import { c as _c } from "react/compiler-runtime";
54 +import fbt from "fbt";
55 +import { Stringify, identity } from "shared-runtime";
56 +
57 +/**
58 + * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
59 + * Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
60 + * /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
61 + * ---
62 + * t3
63 + * ---
64 + */
65 +function Component(t0) {
66 + "use memo";
67 + const $ = _c(9);
68 + const { firstname, lastname } = t0;
69 + let t1;
70 + if ($[0] !== firstname || $[1] !== lastname) {
71 + let t2;
72 + if ($[3] !== firstname) {
73 + t2 = <Stringify key={0} name={firstname} />;
74 + $[3] = firstname;
75 + $[4] = t2;
76 + } else {
77 + t2 = $[4];
78 + }
79 + let t3;
80 + if ($[5] !== lastname) {
81 + t3 = <Stringify key={1} name={lastname} />;
82 + $[5] = lastname;
83 + $[6] = t3;
84 + } else {
85 + t3 = $[6];
86 + }
87 + t1 = fbt._(
88 + "Name: {firstname}, {lastname}",
89 + [
90 + fbt._param("firstname", t2),
91 + fbt._param(
92 + "lastname",
93 + identity(
94 + fbt._("(inner){lastname}", [fbt._param("lastname", t3)], {
95 + hk: "1Kdxyo",
96 + }),
97 + ),
98 + ),
99 + ],
100 + { hk: "3AiIf8" },
101 + );
102 + $[0] = firstname;
103 + $[1] = lastname;
104 + $[2] = t1;
105 + } else {
106 + t1 = $[2];
107 + }
108 + let t2;
109 + if ($[7] !== t1) {
110 + t2 = <div>{t1}</div>;
111 + $[7] = t1;
112 + $[8] = t2;
113 + } else {
114 + t2 = $[8];
115 + }
116 + return t2;
117 +}
118 +
119 +export const FIXTURE_ENTRYPOINT = {
120 + fn: Component,
121 + params: [{ firstname: "first", lastname: "last" }],
122 + sequentialRenders: [{ firstname: "first", lastname: "last" }],
123 +};
124 +
125 +```
126 +
127 +### Eval output
128 +(kind: ok) <div>Name: <div>{"name":"first"}</div>, (inner)<div>{"name":"last"}</div></div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/repro-fbt-param-nested-fbt-jsx.js new
+42
@@ -0,0 +1,42 @@
1 +import fbt from 'fbt';
2 +import {Stringify, identity} from 'shared-runtime';
3 +
4 +/**
5 + * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
6 + * Expected fixture `fbt-param-call-arguments` to succeed but it failed with error:
7 + * /fbt-param-call-arguments.ts: Line 19 Column 11: fbt: unsupported babel node: Identifier
8 + * ---
9 + * t3
10 + * ---
11 + */
12 +function Component({firstname, lastname}) {
13 + 'use memo';
14 + return (
15 + <div>
16 + {fbt(
17 + [
18 + 'Name: ',
19 + fbt.param('firstname', <Stringify key={0} name={firstname} />),
20 + ', ',
21 + fbt.param(
22 + 'lastname',
23 + identity(
24 + fbt(
25 + '(inner)' +
26 + fbt.param('lastname', <Stringify key={1} name={lastname} />),
27 + 'Inner fbt value'
28 + )
29 + )
30 + ),
31 + ],
32 + 'Name'
33 + )}
34 + </div>
35 + );
36 +}
37 +
38 +export const FIXTURE_ENTRYPOINT = {
39 + fn: Component,
40 + params: [{firstname: 'first', lastname: 'last'}],
41 + sequentialRenders: [{firstname: 'first', lastname: 'last'}],
42 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/repro-fbt-param-nested-fbt.expect.md
+27 -14
@@ -3,7 +3,7 @@
3
4 ```javascript
5 import fbt from 'fbt';
6 -import {Stringify} from 'shared-runtime';
6 +import {identity} from 'shared-runtime';
7
8 /**
9 * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -16,22 +16,25 @@ import {Stringify} from 'shared-runtime';
16 function Component({firstname, lastname}) {
17 'use memo';
18 return (
19 - <Stringify>
19 + <div>
20 {fbt(
21 [
22 'Name: ',
23 - fbt.param('firstname', <Stringify key={0} name={firstname} />),
23 + fbt.param('firstname', identity(firstname)),
24 ', ',
25 fbt.param(
26 'lastname',
27 - <Stringify key={0} name={lastname}>
28 - {fbt('(inner fbt)', 'Inner fbt value')}
29 - </Stringify>
27 + identity(
28 + fbt(
29 + '(inner)' + fbt.param('lastname', identity(lastname)),
30 + 'Inner fbt value'
31 + )
32 + )
33 ),
34 ],
35 'Name'
36 )}
34 - </Stringify>
37 + </div>
38 );
39 }
40
@@ -48,7 +51,7 @@ export const FIXTURE_ENTRYPOINT = {
51 ```javascript
52 import { c as _c } from "react/compiler-runtime";
53 import fbt from "fbt";
51 -import { Stringify } from "shared-runtime";
54 +import { identity } from "shared-runtime";
55
56 /**
57 * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -70,14 +73,24 @@ function Component(t0) {
73 fbt._param(
74 "firstname",
75
73 - <Stringify key={0} name={firstname} />,
76 + identity(firstname),
77 ),
78 fbt._param(
79 "lastname",
80
78 - <Stringify key={0} name={lastname}>
79 - {fbt._("(inner fbt)", null, { hk: "36qNwF" })}
80 - </Stringify>,
81 + identity(
82 + fbt._(
83 + "(inner){lastname}",
84 + [
85 + fbt._param(
86 + "lastname",
87 +
88 + identity(lastname),
89 + ),
90 + ],
91 + { hk: "1Kdxyo" },
92 + ),
93 + ),
94 ),
95 ],
96 { hk: "3AiIf8" },
@@ -90,7 +103,7 @@ function Component(t0) {
103 }
104 let t2;
105 if ($[3] !== t1) {
93 - t2 = <Stringify>{t1}</Stringify>;
106 + t2 = <div>{t1}</div>;
107 $[3] = t1;
108 $[4] = t2;
109 } else {
@@ -108,4 +121,4 @@ export const FIXTURE_ENTRYPOINT = {
121 ```
122
123 ### Eval output
111 -(kind: ok) <div>{"children":"Name: , "}</div>
\ No newline at end of file
124 +(kind: ok) <div>Name: first, (inner)last</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/repro-fbt-param-nested-fbt.js
+10 -7
@@ -1,5 +1,5 @@
1 import fbt from 'fbt';
2 -import {Stringify} from 'shared-runtime';
2 +import {identity} from 'shared-runtime';
3
4 /**
5 * MemoizeFbtAndMacroOperands needs to account for nested fbt calls.
@@ -12,22 +12,25 @@ import {Stringify} from 'shared-runtime';
12 function Component({firstname, lastname}) {
13 'use memo';
14 return (
15 - <Stringify>
15 + <div>
16 {fbt(
17 [
18 'Name: ',
19 - fbt.param('firstname', <Stringify key={0} name={firstname} />),
19 + fbt.param('firstname', identity(firstname)),
20 ', ',
21 fbt.param(
22 'lastname',
23 - <Stringify key={0} name={lastname}>
24 - {fbt('(inner fbt)', 'Inner fbt value')}
25 - </Stringify>
23 + identity(
24 + fbt(
25 + '(inner)' + fbt.param('lastname', identity(lastname)),
26 + 'Inner fbt value'
27 + )
28 + )
29 ),
30 ],
31 'Name'
32 )}
30 - </Stringify>
33 + </div>
34 );
35 }
36
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-method-no-outlining-wildcard.expect.md
+2 -8
@@ -37,7 +37,7 @@ function Component(props) {
37 const $ = _c(16);
38 let t0;
39 if ($[0] !== props) {
40 - t0 = idx(props, _temp);
40 + t0 = idx(props, (_) => _.group.label);
41 $[0] = props;
42 $[1] = t0;
43 } else {
@@ -46,7 +46,7 @@ function Component(props) {
46 const groupName1 = t0;
47 let t1;
48 if ($[2] !== props) {
49 - t1 = idx.a(props, _temp2);
49 + t1 = idx.a(props, (__0) => __0.group.label);
50 $[2] = props;
51 $[3] = t1;
52 } else {
@@ -108,12 +108,6 @@ function Component(props) {
108 }
109 return t5;
110 }
111 -function _temp2(__0) {
112 - return __0.group.label;
113 -}
114 -function _temp(_) {
115 - return _.group.label;
116 -}
111
112 ```
113
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/idx-method-no-outlining.expect.md
+1 -4
@@ -31,7 +31,7 @@ function Component(props) {
31 const $ = _c(10);
32 let t0;
33 if ($[0] !== props) {
34 - t0 = idx(props, _temp);
34 + t0 = idx(props, (_) => _.group.label);
35 $[0] = props;
36 $[1] = t0;
37 } else {
@@ -74,9 +74,6 @@ function Component(props) {
74 }
75 return t3;
76 }
77 -function _temp(_) {
78 - return _.group.label;
79 -}
77
78 ```
79
\ No newline at end of file