1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import { CompilerError } from "..";
9
+import {
10
+ BasicBlock,
11
+ Environment,
12
+ GeneratedSource,
13
+ HIRFunction,
14
+ IdentifierId,
15
+ Instruction,
16
+ isExpressionBlockKind,
17
+ markInstructionIds,
18
+} from "../HIR";
19
+import { printInstruction } from "../HIR/PrintHIR";
20
+import {
21
+ eachInstructionValueLValue,
22
+ eachInstructionValueOperand,
23
+ eachTerminalOperand,
24
+} from "../HIR/visitors";
25
+import { mayAllocate } from "../ReactiveScopes/InferReactiveScopeVariables";
26
+import { getOrInsertWith } from "../Utils/utils";
27
+
28
+/**
29
+ * This pass implements conservative instruction reordering to move instructions closer to
30
+ * to where their produced values are consumed. The goal is to group instructions in a way that
31
+ * is more optimal for future optimizations. Notably, MergeReactiveScopesThatAlwaysInvalidateTogether
32
+ * can only merge two candidate scopes if there are no intervenining instructions that are used by
33
+ * some later code: instruction reordering can move those intervening instructions later in many cases,
34
+ * thereby allowing more scopes to merge together.
35
+ *
36
+ * The high-level approach is to build a dependency graph where nodes correspond either to
37
+ * instructions OR to a particular lvalue assignment of another instruction. So
38
+ * `Destructure [x, y] = z` creates 3 nodes: one for the instruction, and one each for x and y.
39
+ * The lvalue nodes depend on the instruction node that assigns them.
40
+ *
41
+ * Dependency edges are added for all the lvalues and rvalues of each instruction, so for example
42
+ * the node for `t$2 = CallExpression t$0 ( t$1 )` will take dependencies on the nodes for t$0 and t$1.
43
+ *
44
+ * Individual instructions are grouped into two categories:
45
+ * - "Reorderable" instructions include a safe set of instructions that we know are fine to reorder.
46
+ * This includes JSX elements/fragments/text, primitives, template literals, and globals.
47
+ * These instructions are never emitted until they are referenced, and can even be moved across
48
+ * basic blocks until they are used.
49
+ * - All other instructions are non-reorderable, and take an explicit dependency on the last such
50
+ * non-reorderable instruction in their block. This largely ensures that mutations are serialized,
51
+ * since all potentially mutating instructions are in this category.
52
+ *
53
+ * The only remaining mutation not handled by the above is variable reassignment. To ensure that all
54
+ * reads/writes of a variable access the correct version, all references (lvalues and rvalues) to
55
+ * each named variable are serialized. Thus `x = 1; y = x; x = 2; z = x` will establish a chain
56
+ * of dependencies and retain the correct ordering.
57
+ *
58
+ * The algorithm proceeds one basic block at a time, first building up the dependnecy graph and then
59
+ * reordering.
60
+ *
61
+ * The reordering weights nodes according to their transitive dependencies, and whether a particular node
62
+ * needs memoization or not. Larger dependencies go first, followed by smaller dependencies, which in
63
+ * testing seems to allow scopes to merge more effectively. Over time we can likely continue to improve
64
+ * the reordering heuristic.
65
+ *
66
+ * An obvious area for improvement is to allow reordering of LoadLocals that occur after the last write
67
+ * of the named variable. We can add this in a follow-up.
68
+ */
69
+export function instructionReordering(fn: HIRFunction): void {
70
+ // Shared nodes are emitted when they are first used
71
+ const shared: Nodes = new Map();
72
+ for (const [, block] of fn.body.blocks) {
73
+ reorderBlock(fn.env, block, shared);
74
+ }
75
+ CompilerError.invariant(shared.size === 0, {
76
+ reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
77
+ loc:
78
+ [...shared.values()]
79
+ .map((node) => node.instruction?.loc)
80
+ .filter((loc) => loc != null)[0] ?? GeneratedSource,
81
+ });
82
+ markInstructionIds(fn.body);
83
+}
84
+
85
+const DEBUG = false;
86
+
87
+type Nodes = Map<IdentifierId, Node>;
88
+type Node = {
89
+ instruction: Instruction | null;
90
+ dependencies: Set<IdentifierId>;
91
+ depth: number | null;
92
+};
93
+
94
+function reorderBlock(
95
+ env: Environment,
96
+ block: BasicBlock,
97
+ shared: Nodes
98
+): void {
99
+ const locals: Nodes = new Map();
100
+ const named: Map<string, IdentifierId> = new Map();
101
+ let previous: IdentifierId | null = null;
102
+ for (const instr of block.instructions) {
103
+ const { lvalue, value } = instr;
104
+ // Get or create a node for this lvalue
105
+ const node = getOrInsertWith(
106
+ locals,
107
+ lvalue.identifier.id,
108
+ () =>
109
+ ({
110
+ instruction: instr,
111
+ dependencies: new Set(),
112
+ depth: null,
113
+ }) as Node
114
+ );
115
+ /**
116
+ * Ensure non-reoderable instructions have their order retained by
117
+ * adding explicit dependencies to the previous such instruction.
118
+ */
119
+ if (getReoderability(instr) === Reorderability.Nonreorderable) {
120
+ if (previous !== null) {
121
+ node.dependencies.add(previous);
122
+ }
123
+ previous = lvalue.identifier.id;
124
+ }
125
+ /**
126
+ * Establish dependencies on operands
127
+ */
128
+ for (const operand of eachInstructionValueOperand(value)) {
129
+ const { name, id } = operand.identifier;
130
+ if (name !== null && name.kind === "named") {
131
+ // Serialize all accesses to named variables
132
+ const previous = named.get(name.value);
133
+ if (previous !== undefined) {
134
+ node.dependencies.add(previous);
135
+ }
136
+ named.set(name.value, lvalue.identifier.id);
137
+ } else if (locals.has(id) || shared.has(id)) {
138
+ node.dependencies.add(id);
139
+ }
140
+ }
141
+ /**
142
+ * Establish nodes for lvalues, with dependencies on the node
143
+ * for the instruction itself. This ensures that any consumers
144
+ * of the lvalue will take a dependency through to the original
145
+ * instruction.
146
+ */
147
+ for (const lvalueOperand of eachInstructionValueLValue(value)) {
148
+ const lvalueNode = getOrInsertWith(
149
+ locals,
150
+ lvalueOperand.identifier.id,
151
+ () =>
152
+ ({
153
+ instruction: null,
154
+ dependencies: new Set(),
155
+ depth: null,
156
+ }) as Node
157
+ );
158
+ lvalueNode.dependencies.add(lvalue.identifier.id);
159
+ const name = lvalueOperand.identifier.name;
160
+ if (name !== null && name.kind === "named") {
161
+ const previous = named.get(name.value);
162
+ if (previous !== undefined) {
163
+ node.dependencies.add(previous);
164
+ }
165
+ named.set(name.value, lvalue.identifier.id);
166
+ }
167
+ }
168
+ }
169
+
170
+ const nextInstructions: Array<Instruction> = [];
171
+ const seen = new Set<IdentifierId>();
172
+
173
+ DEBUG && console.log(`bb${block.id}`);
174
+
175
+ // First emit everything that can't be reordered
176
+ if (previous !== null) {
177
+ DEBUG && console.log(`(last non-reorderable instruction)`);
178
+ DEBUG && print(env, locals, shared, seen, previous);
179
+ emit(env, locals, shared, nextInstructions, previous);
180
+ }
181
+ /*
182
+ * For "value" blocks the final instruction represents its value, so we have to be
183
+ * careful to not change the ordering. Emit the last instruction explicitly.
184
+ * Any non-reorderable instructions will get emitted first, and any unused
185
+ * reorderable instructions can be deferred to the shared node list.
186
+ */
187
+ if (isExpressionBlockKind(block.kind) && block.instructions.length !== 0) {
188
+ DEBUG && console.log(`(block value)`);
189
+ DEBUG &&
190
+ print(
191
+ env,
192
+ locals,
193
+ shared,
194
+ seen,
195
+ block.instructions.at(-1)!.lvalue.identifier.id
196
+ );
197
+ emit(
198
+ env,
199
+ locals,
200
+ shared,
201
+ nextInstructions,
202
+ block.instructions.at(-1)!.lvalue.identifier.id
203
+ );
204
+ }
205
+ /*
206
+ * Then emit the dependencies of the terminal operand. In many cases they will have
207
+ * already been emitted in the previous step and this is a no-op.
208
+ * TODO: sort the dependencies based on weight, like we do for other nodes. Not a big
209
+ * deal though since most terminals have a single operand
210
+ */
211
+ for (const operand of eachTerminalOperand(block.terminal)) {
212
+ DEBUG && console.log(`(terminal operand)`);
213
+ DEBUG && print(env, locals, shared, seen, operand.identifier.id);
214
+ emit(env, locals, shared, nextInstructions, operand.identifier.id);
215
+ }
216
+ // Anything not emitted yet is globally reorderable
217
+ for (const [id, node] of locals) {
218
+ if (node.instruction == null) {
219
+ continue;
220
+ }
221
+ CompilerError.invariant(
222
+ node.instruction != null &&
223
+ getReoderability(node.instruction) === Reorderability.Reorderable,
224
+ {
225
+ reason: `Expected all remaining instructions to be reorderable`,
226
+ loc: node.instruction?.loc ?? block.terminal.loc,
227
+ description:
228
+ node.instruction != null
229
+ ? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`
230
+ : `Lvalue $${id} was not emitted yet but is not reorderable`,
231
+ }
232
+ );
233
+ DEBUG && console.log(`save shared: $${id}`);
234
+ shared.set(id, node);
235
+ }
236
+
237
+ block.instructions = nextInstructions;
238
+ DEBUG && console.log();
239
+}
240
+
241
+function getDepth(env: Environment, nodes: Nodes, id: IdentifierId): number {
242
+ const node = nodes.get(id)!;
243
+ if (node == null) {
244
+ return 0;
245
+ }
246
+ if (node.depth != null) {
247
+ return node.depth;
248
+ }
249
+ node.depth = 0; // in case of cycles
250
+ let depth =
251
+ node.instruction != null && mayAllocate(env, node.instruction) ? 1 : 0;
252
+ for (const dep of node.dependencies) {
253
+ depth += getDepth(env, nodes, dep);
254
+ }
255
+ node.depth = depth;
256
+ return depth;
257
+}
258
+
259
+function print(
260
+ env: Environment,
261
+ locals: Nodes,
262
+ shared: Nodes,
263
+ seen: Set<IdentifierId>,
264
+ id: IdentifierId,
265
+ depth: number = 0
266
+): void {
267
+ if (seen.has(id)) {
268
+ console.log(`${"| ".repeat(depth)}$${id} <skipped>`);
269
+ return;
270
+ }
271
+ seen.add(id);
272
+ const node = locals.get(id) ?? shared.get(id);
273
+ if (node == null) {
274
+ return;
275
+ }
276
+ const deps = [...node.dependencies];
277
+ deps.sort((a, b) => {
278
+ const aDepth = getDepth(env, locals, a);
279
+ const bDepth = getDepth(env, locals, b);
280
+ return bDepth - aDepth;
281
+ });
282
+ for (const dep of deps) {
283
+ print(env, locals, shared, seen, dep, depth + 1);
284
+ }
285
+ console.log(
286
+ `${"| ".repeat(depth)}$${id} ${printNode(node)} deps=[${deps.map((x) => `$${x}`).join(", ")}]`
287
+ );
288
+}
289
+
290
+function printNode(node: Node): string {
291
+ const { instruction } = node;
292
+ if (instruction === null) {
293
+ return "<lvalue-only>";
294
+ }
295
+ switch (instruction.value.kind) {
296
+ case "FunctionExpression":
297
+ case "ObjectMethod": {
298
+ return `[${instruction.id}] ${instruction.value.kind}`;
299
+ }
300
+ default: {
301
+ return printInstruction(instruction);
302
+ }
303
+ }
304
+}
305
+
306
+function emit(
307
+ env: Environment,
308
+ locals: Nodes,
309
+ shared: Nodes,
310
+ instructions: Array<Instruction>,
311
+ id: IdentifierId
312
+): void {
313
+ const node = locals.get(id) ?? shared.get(id);
314
+ if (node == null) {
315
+ return;
316
+ }
317
+ locals.delete(id);
318
+ shared.delete(id);
319
+ const deps = [...node.dependencies];
320
+ deps.sort((a, b) => {
321
+ const aDepth = getDepth(env, locals, a);
322
+ const bDepth = getDepth(env, locals, b);
323
+ return bDepth - aDepth;
324
+ });
325
+ for (const dep of deps) {
326
+ emit(env, locals, shared, instructions, dep);
327
+ }
328
+ if (node.instruction !== null) {
329
+ instructions.push(node.instruction);
330
+ }
331
+}
332
+
333
+enum Reorderability {
334
+ Reorderable,
335
+ Nonreorderable,
336
+}
337
+function getReoderability(instr: Instruction): Reorderability {
338
+ switch (instr.value.kind) {
339
+ case "JsxExpression":
340
+ case "JsxFragment":
341
+ case "JSXText":
342
+ case "LoadGlobal":
343
+ case "Primitive":
344
+ case "TemplateLiteral":
345
+ case "BinaryExpression":
346
+ case "UnaryExpression": {
347
+ return Reorderability.Reorderable;
348
+ }
349
+ default: {
350
+ return Reorderability.Nonreorderable;
351
+ }
352
+ }
353
+}