main
md 231 lines 6.35 KB
Rendered Raw
1 # memoizeFbtAndMacroOperandsInSameScope
2
3 ## File
4 `src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts`
5
6 ## Purpose
7 This pass ensures that FBT (Facebook Translation) expressions and their operands are memoized within the same reactive scope. FBT is Facebook's internationalization system that requires special handling to ensure translations work correctly.
8
9 The key insight is that FBT operands must be computed and frozen together with the FBT call itself. If operands were memoized in separate scopes, the translation system could receive stale operand values when only some inputs change.
10
11 ## Input Invariants
12 - The function has been through type inference
13 - FBT calls (`fbt`, `fbt.c`, `fbt:param`, etc.) are properly identified
14 - Custom macros are configured in `fn.env.config.customMacros`
15 - Reactive scope variables have been inferred
16
17 ## Output Guarantees
18 - All operands of FBT calls are assigned to the same reactive scope as the FBT call
19 - The `fbtOperands` set is returned for use by other passes (e.g., `outlineFunctions`)
20 - Operand scope assignments use either transitive or shallow inlining based on macro definition
21
22 ## Algorithm
23
24 ### Phase 1: Collect Macro Kinds
25 ```typescript
26 const macroKinds = new Map<Macro, MacroDefinition>([
27 ...Array.from(FBT_TAGS.entries()), // Built-in fbt tags
28 ...(fn.env.config.customMacros ?? []).map(([name, def]) => [name, def]),
29 ]);
30 ```
31
32 ### Phase 2: Populate Macro Tags
33 ```typescript
34 function populateMacroTags(
35 fn: HIRFunction,
36 macroKinds: Map<Macro, MacroDefinition>,
37 ): Map<IdentifierId, MacroDefinition> {
38 const macroTags = new Map();
39
40 for (const instr of allInstructions(fn)) {
41 if (isLoadGlobal(instr) || isPropertyLoad(instr)) {
42 const name = getName(instr);
43 if (macroKinds.has(name)) {
44 macroTags.set(instr.lvalue.id, macroKinds.get(name));
45 }
46 }
47 }
48
49 return macroTags;
50 }
51 ```
52
53 ### Phase 3: Merge Macro Arguments
54 ```typescript
55 function mergeMacroArguments(
56 fn: HIRFunction,
57 macroTags: Map<IdentifierId, MacroDefinition>,
58 macroKinds: Map<Macro, MacroDefinition>,
59 ): Set<IdentifierId> {
60 const macroValues = new Set<IdentifierId>();
61
62 for (const instr of allInstructions(fn)) {
63 if (isCall(instr) || isMethodCall(instr) || isJSX(instr)) {
64 const callee = getCallee(instr);
65 const macroDef = macroTags.get(callee.id);
66
67 if (macroDef !== undefined) {
68 // Mark all operands to be in same scope
69 for (const operand of getOperands(instr)) {
70 macroValues.add(operand.id);
71
72 // Merge scope to match macro call scope
73 if (macroDef.inlineLevel === InlineLevel.Transitive) {
74 mergeScopesTransitively(operand, instr.lvalue);
75 } else {
76 mergeScopes(operand, instr.lvalue);
77 }
78 }
79 }
80 }
81 }
82
83 return macroValues;
84 }
85 ```
86
87 ### InlineLevel Types
88 ```typescript
89 enum InlineLevel {
90 Shallow, // Only merge direct operands
91 Transitive, // Merge operands and their dependencies
92 }
93 ```
94
95 ## Edge Cases
96
97 ### Nested FBT Params
98 FBT params can be nested, and all levels must be in the same scope:
99 ```javascript
100 <fbt>
101 Hello <fbt:param name="user">
102 <fbt:param name="firstName">{user.firstName}</fbt:param>
103 </fbt:param>
104 </fbt>
105 ```
106
107 ### FBT with Complex Expressions
108 Complex expressions as operands have their entire dependency chain merged:
109 ```javascript
110 <fbt>
111 Count: <fbt:param name="count">{items.length * multiplier}</fbt:param>
112 </fbt>
113 // Both items.length and multiplier expressions are merged into fbt scope
114 ```
115
116 ### Custom Macros
117 User-defined macros can specify their inlining behavior:
118 ```typescript
119 customMacros: [
120 ['myMacro', { inlineLevel: InlineLevel.Transitive }],
121 ]
122 ```
123
124 ### Method Calls on FBT
125 `fbt.param()`, `fbt.plural()`, etc. are handled as method calls:
126 ```javascript
127 fbt(
128 fbt.param('count', items.length), // MethodCall on fbt
129 'description'
130 )
131 ```
132
133 ### JSX vs Call Syntax
134 Both JSX and call syntax for FBT are handled:
135 ```javascript
136 // JSX syntax
137 <fbt desc="greeting">Hello</fbt>
138
139 // Call syntax
140 fbt('Hello', 'greeting')
141 ```
142
143 ## Built-in FBT Tags
144 The pass recognizes these FBT constructs:
145 - `fbt` / `fbt.c` - Main translation functions
146 - `fbt:param` - Parameter substitution
147 - `fbt:plural` - Plural handling
148 - `fbt:enum` - Enumeration values
149 - `fbt:name` - Name parameters
150 - `fbt:pronoun` - Pronoun handling
151 - `fbs` - Simple string translation
152
153 ## TODOs
154 None in the source file.
155
156 ## Example
157
158 ### Fixture: `fbt/fbt-call.js`
159
160 **Input:**
161 ```javascript
162 function Component(props) {
163 const text = fbt(
164 `${fbt.param('count', props.count)} items`,
165 'Number of items'
166 );
167 return <div>{text}</div>;
168 }
169 ```
170
171 **Before MemoizeFbtAndMacroOperandsInSameScope:**
172 ```
173 [1] $18 = LoadGlobal import fbt from 'fbt'
174 [2] $19 = LoadGlobal import fbt from 'fbt'
175 [3] $20_@0[3:8] = PropertyLoad $19.param
176 [4] $21 = "(key) count"
177 [5] $22 = LoadLocal props$17
178 [6] $23 = PropertyLoad $22.count
179 [7] $24_@0[3:8] = MethodCall $19.$20_@0($21, $23) // fbt.param call
180 [8] $25 = `${$24_@0} items`
181 [9] $26 = "(description) Number of items"
182 [10] $27_@1 = Call $18($25, $26) // fbt call
183 ```
184
185 **After MemoizeFbtAndMacroOperandsInSameScope:**
186 ```
187 [1] $18_@1[1:11] = LoadGlobal import fbt from 'fbt' // Merged to @1
188 [2] $19 = LoadGlobal import fbt from 'fbt'
189 [3] $20_@0[3:8] = PropertyLoad $19.param
190 [4] $21 = "(key) count"
191 [5] $22 = LoadLocal props$17
192 [6] $23 = PropertyLoad $22.count
193 [7] $24_@1[1:11] = MethodCall $19.$20_@0($21, $23) // Merged to @1
194 [8] $25_@1[1:11] = `${$24_@1} items` // Merged to @1
195 [9] $26_@1[1:11] = "(description) Number of items" // Merged to @1
196 [10] $27_@1[1:11] = Call $18_@1($25_@1, $26_@1) // Main fbt scope @1
197 ```
198
199 **Generated Code:**
200 ```javascript
201 function Component(props) {
202 const $ = _c(3);
203 let t0;
204 if ($[0] !== props.count) {
205 // All fbt operands computed in same memoization block
206 t0 = fbt(
207 `${fbt.param("count", props.count)} items`,
208 "Number of items"
209 );
210 $[0] = props.count;
211 $[1] = t0;
212 } else {
213 t0 = $[1];
214 }
215 const text = t0;
216 let t1;
217 if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
218 t1 = <div>{text}</div>;
219 $[2] = t1;
220 } else {
221 t1 = $[2];
222 }
223 return t1;
224 }
225 ```
226
227 Key observations:
228 - All FBT-related operations are in the same memoization scope `@1`
229 - `fbt.param`, template literal, and `fbt` call are memoized together
230 - This ensures the translation system receives consistent operand values
231 - The entire translation is recomputed when any operand (`props.count`) changes