7
8
import {
9
HIRFunction,
10
+ Identifier,
11
IdentifierId,
12
+ InstructionValue,
13
makeInstructionId,
14
MutableRange,
15
Place,
14
- ReactiveValue,
16
+ ReactiveScope,
17
} from '../HIR';
18
import {Macro, MacroMethod} from '../HIR/Environment';
17
-import {eachReactiveValueOperand} from './visitors';
19
+import {eachInstructionValueOperand} from '../HIR/visitors';
20
+import {Iterable_some} from '../Utils/utils';
21
22
/**
23
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
51
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
52
...(fn.env.config.customMacros ?? []),
53
]);
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
69
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;
81
}
82
+ mergeScopes(root, scope, macroValues, macroTagsCalls);
83
}
61
- return fbtValues;
84
+
85
+ return macroTagsCalls;
86
}
87
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
]);
98
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
99
'fbt:param',
103
function visit(
104
fn: HIRFunction,
105
fbtMacroTags: Set<Macro>,
78
- fbtValues: Set<IdentifierId>,
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) {
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
*/
96
- fbtValues.add(lvalue.identifier.id);
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
102
- fbtValues.add(lvalue.identifier.id);
142
+ macroTagsCalls.add(lvalue.identifier.id);
143
} else if (
144
value.kind === 'LoadGlobal' &&
145
matchTagRoot(value.binding.name, fbtMacroTags) !== null
161
if (method.length > 1) {
162
newMethods.push(method.slice(1));
163
} else {
124
- fbtValues.add(lvalue.identifier.id);
164
+ macroTagsCalls.add(lvalue.identifier.id);
165
}
166
}
167
}
168
if (newMethods.length > 0) {
169
macroMethods.set(lvalue.identifier.id, newMethods);
170
}
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
- }
171
} else if (
150
- isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
151
- isFbtJsxChild(fbtValues, lvalue, value)
172
+ value.kind === 'PropertyLoad' &&
173
+ macroTagsCalls.has(value.object.identifier.id)
174
) {
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);
198
}
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);
199
}
200
+ macroValues.set(lvalue.identifier, macroOperands);
201
}
202
}
203
}
204
}
205
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'
251
}
252
253
function isFbtCallExpression(
232
- fbtValues: Set<IdentifierId>,
233
- value: ReactiveValue,
254
+ macroTagsCalls: Set<IdentifierId>,
255
+ value: InstructionValue,
256
): boolean {
257
return (
258
(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))
262
);
263
}
264
265
function isFbtJsxExpression(
266
fbtMacroTags: Set<Macro>,
244
- fbtValues: Set<IdentifierId>,
245
- value: ReactiveValue,
267
+ macroTagsCalls: Set<IdentifierId>,
268
+ value: InstructionValue,
269
): boolean {
270
return (
271
value.kind === 'JsxExpression' &&
272
((value.tag.kind === 'Identifier' &&
250
- fbtValues.has(value.tag.identifier.id)) ||
273
+ macroTagsCalls.has(value.tag.identifier.id)) ||
274
(value.tag.kind === 'BuiltinTag' &&
275
matchesExactTag(value.tag.name, fbtMacroTags)))
276
);
277
}
278
279
function isFbtJsxChild(
257
- fbtValues: Set<IdentifierId>,
280
+ macroTagsCalls: Set<IdentifierId>,
281
lvalue: Place | null,
259
- value: ReactiveValue,
282
+ value: InstructionValue,
283
): boolean {
284
return (
285
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
286
lvalue !== null &&
264
- fbtValues.has(lvalue.identifier.id)
287
+ macroTagsCalls.has(lvalue.identifier.id)
288
);
289
}
290