7
8
import {
9
HIRFunction,
10
- Identifier,
10
IdentifierId,
11
InstructionValue,
12
makeInstructionId,
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/)
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(
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
+}