4
import {
5
BasicBlock,
6
BlockId,
7
+ DependencyPathEntry,
8
GeneratedSource,
9
HIRFunction,
10
Identifier,
67
fn: HIRFunction,
68
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
69
): ReadonlyMap<ScopeId, BlockInfo> {
69
- const nodes = collectNonNullsInBlocks(fn, temporaries);
70
+ const registry = new PropertyPathRegistry();
71
+
72
+ const nodes = collectNonNullsInBlocks(fn, temporaries, registry);
73
propagateNonNull(fn, nodes);
74
75
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
87
88
export type BlockInfo = {
89
block: BasicBlock;
87
- assumedNonNullObjects: ReadonlySet<PropertyLoadNode>;
90
+ assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
91
};
92
93
/**
91
- * Tree data structure to dedupe property loads (e.g. a.b.c)
94
+ * PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
95
* and make computing sets intersections simpler.
96
*/
97
type RootNode = {
95
- properties: Map<string, PropertyLoadNode>;
98
+ properties: Map<string, PropertyPathNode>;
99
parent: null;
100
// Recorded to make later computations simpler
101
fullPath: ReactiveScopeDependency;
102
root: IdentifierId;
103
};
104
102
-type PropertyLoadNode =
105
+type PropertyPathNode =
106
| {
104
- properties: Map<string, PropertyLoadNode>;
105
- parent: PropertyLoadNode;
107
+ properties: Map<string, PropertyPathNode>;
108
+ parent: PropertyPathNode;
109
fullPath: ReactiveScopeDependency;
110
}
111
| RootNode;
112
110
-class Tree {
113
+class PropertyPathRegistry {
114
roots: Map<IdentifierId, RootNode> = new Map();
115
113
- getOrCreateRoot(identifier: Identifier): PropertyLoadNode {
116
+ getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
117
/**
118
* Reads from a statically scoped variable are always safe in JS,
119
* with the exception of TDZ (not addressed by this pass).
135
return rootNode;
136
}
137
135
- static #getOrCreateProperty(
136
- node: PropertyLoadNode,
137
- property: string,
138
- ): PropertyLoadNode {
139
- let child = node.properties.get(property);
138
+ static getOrCreatePropertyEntry(
139
+ parent: PropertyPathNode,
140
+ entry: DependencyPathEntry,
141
+ ): PropertyPathNode {
142
+ if (entry.optional) {
143
+ CompilerError.throwTodo({
144
+ reason: 'handle optional nodes',
145
+ loc: GeneratedSource,
146
+ });
147
+ }
148
+ let child = parent.properties.get(entry.property);
149
if (child == null) {
150
child = {
151
properties: new Map(),
143
- parent: node,
152
+ parent: parent,
153
fullPath: {
145
- identifier: node.fullPath.identifier,
146
- path: node.fullPath.path.concat([{property, optional: false}]),
154
+ identifier: parent.fullPath.identifier,
155
+ path: parent.fullPath.path.concat(entry),
156
},
157
};
149
- node.properties.set(property, child);
158
+ parent.properties.set(entry.property, child);
159
}
160
return child;
161
}
162
154
- getPropertyLoadNode(n: ReactiveScopeDependency): PropertyLoadNode {
163
+ getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
164
/**
165
* We add ReactiveScopeDependencies according to instruction ordering,
166
* so all subpaths of a PropertyLoad should already exist
167
* (e.g. a.b is added before a.b.c),
168
*/
160
- let currNode = this.getOrCreateRoot(n.identifier);
169
+ let currNode = this.getOrCreateIdentifier(n.identifier);
170
if (n.path.length === 0) {
171
return currNode;
172
}
173
for (let i = 0; i < n.path.length - 1; i++) {
165
- currNode = assertNonNull(currNode.properties.get(n.path[i].property));
174
+ currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
175
+ currNode,
176
+ n.path[i],
177
+ );
178
}
179
168
- return Tree.#getOrCreateProperty(currNode, n.path.at(-1)!.property);
180
+ return PropertyPathRegistry.getOrCreatePropertyEntry(
181
+ currNode,
182
+ n.path.at(-1)!,
183
+ );
184
}
185
}
186
172
-function pushPropertyLoadNode(
173
- loadSource: Identifier,
174
- loadSourceNode: PropertyLoadNode,
187
+function addNonNullPropertyPath(
188
+ source: Identifier,
189
+ sourceNode: PropertyPathNode,
190
instrId: InstructionId,
191
knownImmutableIdentifiers: Set<IdentifierId>,
177
- result: Set<PropertyLoadNode>,
192
+ result: Set<PropertyPathNode>,
193
): void {
194
/**
195
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
202
* See comment at top of function for why we track known immutable identifiers.
203
*/
204
const isMutableAtInstr =
190
- loadSource.mutableRange.end > loadSource.mutableRange.start + 1 &&
191
- loadSource.scope != null &&
192
- inRange({id: instrId}, loadSource.scope.range);
205
+ source.mutableRange.end > source.mutableRange.start + 1 &&
206
+ source.scope != null &&
207
+ inRange({id: instrId}, source.scope.range);
208
if (
209
!isMutableAtInstr ||
195
- knownImmutableIdentifiers.has(loadSourceNode.fullPath.identifier.id)
210
+ knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id)
211
) {
197
- let curr: PropertyLoadNode | null = loadSourceNode;
198
- while (curr != null) {
199
- result.add(curr);
200
- curr = curr.parent;
201
- }
212
+ result.add(sourceNode);
213
}
214
}
215
216
function collectNonNullsInBlocks(
217
fn: HIRFunction,
218
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
219
+ registry: PropertyPathRegistry,
220
): ReadonlyMap<BlockId, BlockInfo> {
209
- const tree = new Tree();
221
/**
222
* Due to current limitations of mutable range inference, there are edge cases in
223
* which we infer known-immutable values (e.g. props or hook params) to have a
238
* Known non-null objects such as functional component props can be safely
239
* read from any block.
240
*/
230
- const knownNonNullIdentifiers = new Set<PropertyLoadNode>();
241
+ const knownNonNullIdentifiers = new Set<PropertyPathNode>();
242
if (
243
fn.fnType === 'Component' &&
244
fn.params.length > 0 &&
245
fn.params[0].kind === 'Identifier'
246
) {
247
const identifier = fn.params[0].identifier;
237
- knownNonNullIdentifiers.add(tree.getOrCreateRoot(identifier));
248
+ knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
249
}
250
const nodes = new Map<BlockId, BlockInfo>();
251
for (const [_, block] of fn.body.blocks) {
241
- const assumedNonNullObjects = new Set<PropertyLoadNode>(
252
+ const assumedNonNullObjects = new Set<PropertyPathNode>(
253
knownNonNullIdentifiers,
254
);
255
for (const instr of block.instructions) {
258
identifier: instr.value.object.identifier,
259
path: [],
260
};
250
- pushPropertyLoadNode(
261
+ addNonNullPropertyPath(
262
instr.value.object.identifier,
252
- tree.getPropertyLoadNode(source),
263
+ registry.getOrCreateProperty(source),
264
instr.id,
265
knownImmutableIdentifiers,
266
assumedNonNullObjects,
269
const source = instr.value.value.identifier.id;
270
const sourceNode = temporaries.get(source);
271
if (sourceNode != null) {
261
- pushPropertyLoadNode(
272
+ addNonNullPropertyPath(
273
instr.value.value.identifier,
263
- tree.getPropertyLoadNode(sourceNode),
274
+ registry.getOrCreateProperty(sourceNode),
275
instr.id,
276
knownImmutableIdentifiers,
277
assumedNonNullObjects,
281
const source = instr.value.object.identifier.id;
282
const sourceNode = temporaries.get(source);
283
if (sourceNode != null) {
273
- pushPropertyLoadNode(
284
+ addNonNullPropertyPath(
285
instr.value.object.identifier,
275
- tree.getPropertyLoadNode(sourceNode),
286
+ registry.getOrCreateProperty(sourceNode),
287
instr.id,
288
knownImmutableIdentifiers,
289
assumedNonNullObjects,
325
nodeId: BlockId,
326
direction: 'forward' | 'backward',
327
traversalState: Map<BlockId, 'active' | 'done'>,
317
- nonNullObjectsByBlock: Map<BlockId, ReadonlySet<PropertyLoadNode>>,
328
): boolean {
329
/**
330
* Avoid re-visiting computed or currently active nodes, which can
355
pred,
356
direction,
357
traversalState,
348
- nonNullObjectsByBlock,
358
);
359
changed ||= neighborChanged;
360
}
383
const neighborAccesses = Set_intersect(
384
Array.from(neighbors)
385
.filter(n => traversalState.get(n) === 'done')
377
- .map(n => assertNonNull(nonNullObjectsByBlock.get(n))),
386
+ .map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
387
);
388
380
- const prevObjects = assertNonNull(nonNullObjectsByBlock.get(nodeId));
381
- const newObjects = Set_union(prevObjects, neighborAccesses);
389
+ const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
390
+ const mergedObjects = Set_union(prevObjects, neighborAccesses);
391
383
- nonNullObjectsByBlock.set(nodeId, newObjects);
392
+ assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
393
traversalState.set(nodeId, 'done');
385
- changed ||= prevObjects.size !== newObjects.size;
394
+ changed ||= prevObjects.size !== mergedObjects.size;
395
return changed;
396
}
388
- const fromEntry = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
389
- const fromExit = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
390
- for (const [blockId, blockInfo] of nodes) {
391
- fromEntry.set(blockId, blockInfo.assumedNonNullObjects);
392
- fromExit.set(blockId, blockInfo.assumedNonNullObjects);
393
- }
397
const traversalState = new Map<BlockId, 'done' | 'active'>();
398
const reversedBlocks = [...fn.body.blocks];
399
reversedBlocks.reverse();
400
398
- let i = 0;
401
let changed;
402
+ let i = 0;
403
do {
401
- i++;
404
+ CompilerError.invariant(i++ < 100, {
405
+ reason:
406
+ '[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
407
+ loc: GeneratedSource,
408
+ });
409
+
410
changed = false;
411
for (const [blockId] of fn.body.blocks) {
412
const forwardChanged = recursivelyPropagateNonNull(
413
blockId,
414
'forward',
415
traversalState,
408
- fromEntry,
416
);
417
changed ||= forwardChanged;
418
}
422
blockId,
423
'backward',
424
traversalState,
418
- fromExit,
425
);
426
changed ||= backwardChanged;
427
}
428
traversalState.clear();
429
} while (changed);
424
-
425
- /**
426
- * TODO: validate against meta internal code, then remove in future PR.
427
- * Currently cannot come up with a case that requires fixed-point iteration.
428
- */
429
- CompilerError.invariant(i <= 2, {
430
- reason: 'require fixed-point iteration',
431
- description: `#iterations = ${i}`,
432
- loc: GeneratedSource,
433
- });
434
-
435
- CompilerError.invariant(
436
- fromEntry.size === fromExit.size && fromEntry.size === nodes.size,
437
- {
438
- reason:
439
- 'bad sizes after calculating fromEntry + fromExit ' +
440
- `${fromEntry.size} ${fromExit.size} ${nodes.size}`,
441
- loc: GeneratedSource,
442
- },
443
- );
444
-
445
- for (const [id, node] of nodes) {
446
- const assumedNonNullObjects = Set_union(
447
- assertNonNull(fromEntry.get(id)),
448
- assertNonNull(fromExit.get(id)),
449
- );
450
- node.assumedNonNullObjects = assumedNonNullObjects;
451
- }
430
}
431
454
-function assertNonNull<T extends NonNullable<U>, U>(
432
+export function assertNonNull<T extends NonNullable<U>, U>(
433
value: T | null | undefined,
434
source?: string,
435
): T {