7
Set_union,
8
getOrInsertDefault,
9
} from '../Utils/utils';
10
+import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
11
import {
12
BasicBlock,
13
BlockId,
16
HIRFunction,
17
Identifier,
18
IdentifierId,
19
+ InstructionId,
20
InstructionValue,
21
ReactiveScopeDependency,
22
ScopeId,
23
} from './HIR';
24
+import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
25
26
/**
27
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
86
fn: HIRFunction,
87
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
88
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
86
-): ReadonlyMap<ScopeId, BlockInfo> {
89
+ nestedFnImmutableContext: ReadonlySet<IdentifierId> | null,
90
+): ReadonlyMap<BlockId, BlockInfo> {
91
const registry = new PropertyPathRegistry();
92
89
- const nodes = collectNonNullsInBlocks(
90
- fn,
91
- temporaries,
93
+ const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
94
+ const actuallyEvaluatedTemporaries = new Map(
95
+ [...temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
96
+ );
97
+
98
+ /**
99
+ * Due to current limitations of mutable range inference, there are edge cases in
100
+ * which we infer known-immutable values (e.g. props or hook params) to have a
101
+ * mutable range and scope.
102
+ * (see `destructure-array-declaration-to-context-var` fixture)
103
+ * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
104
+ * is being rewritten to HIR).
105
+ */
106
+ const knownImmutableIdentifiers = new Set<IdentifierId>();
107
+ if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
108
+ for (const p of fn.params) {
109
+ if (p.kind === 'Identifier') {
110
+ knownImmutableIdentifiers.add(p.identifier.id);
111
+ }
112
+ }
113
+ }
114
+ const nodes = collectNonNullsInBlocks(fn, {
115
+ temporaries: actuallyEvaluatedTemporaries,
116
+ knownImmutableIdentifiers,
117
hoistableFromOptionals,
118
registry,
94
- );
119
+ nestedFnImmutableContext,
120
+ });
121
propagateNonNull(fn, nodes, registry);
122
97
- const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
123
+ return nodes;
124
+}
125
+
126
+export function keyByScopeId<T>(
127
+ fn: HIRFunction,
128
+ source: ReadonlyMap<BlockId, T>,
129
+): ReadonlyMap<ScopeId, T> {
130
+ const keyedByScopeId = new Map<ScopeId, T>();
131
for (const [_, block] of fn.body.blocks) {
132
if (block.terminal.kind === 'scope') {
100
- nodesKeyedByScopeId.set(
133
+ keyedByScopeId.set(
134
block.terminal.scope.id,
102
- nodes.get(block.terminal.block)!,
135
+ source.get(block.terminal.block)!,
136
);
137
}
138
}
106
-
107
- return nodesKeyedByScopeId;
139
+ return keyedByScopeId;
140
}
141
142
export type BlockInfo = {
243
244
function getMaybeNonNullInInstruction(
245
instr: InstructionValue,
214
- temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
215
- registry: PropertyPathRegistry,
246
+ context: CollectNonNullsInBlocksContext,
247
): PropertyPathNode | null {
248
let path = null;
249
if (instr.kind === 'PropertyLoad') {
219
- path = temporaries.get(instr.object.identifier.id) ?? {
250
+ path = context.temporaries.get(instr.object.identifier.id) ?? {
251
identifier: instr.object.identifier,
252
path: [],
253
};
254
} else if (instr.kind === 'Destructure') {
224
- path = temporaries.get(instr.value.identifier.id) ?? null;
255
+ path = context.temporaries.get(instr.value.identifier.id) ?? null;
256
} else if (instr.kind === 'ComputedLoad') {
226
- path = temporaries.get(instr.object.identifier.id) ?? null;
257
+ path = context.temporaries.get(instr.object.identifier.id) ?? null;
258
+ }
259
+ return path != null ? context.registry.getOrCreateProperty(path) : null;
260
+}
261
+
262
+function isImmutableAtInstr(
263
+ identifier: Identifier,
264
+ instr: InstructionId,
265
+ context: CollectNonNullsInBlocksContext,
266
+): boolean {
267
+ if (context.nestedFnImmutableContext != null) {
268
+ /**
269
+ * Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
270
+ */
271
+ return context.nestedFnImmutableContext.has(identifier.id);
272
+ } else {
273
+ /**
274
+ * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
275
+ * are not valid with respect to current instruction id numbering.
276
+ * We use attached reactive scope ranges as a proxy for mutable range, but this
277
+ * is an overestimate as (1) scope ranges merge and align to form valid program
278
+ * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
279
+ * non-mutable identifiers.
280
+ *
281
+ * See comment in exported function for why we track known immutable identifiers.
282
+ */
283
+ const mutableAtInstr =
284
+ identifier.mutableRange.end > identifier.mutableRange.start + 1 &&
285
+ identifier.scope != null &&
286
+ inRange(
287
+ {
288
+ id: instr,
289
+ },
290
+ identifier.scope.range,
291
+ );
292
+ return (
293
+ !mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id)
294
+ );
295
}
228
- return path != null ? registry.getOrCreateProperty(path) : null;
296
}
297
298
+type CollectNonNullsInBlocksContext = {
299
+ temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
300
+ knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
301
+ hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
302
+ registry: PropertyPathRegistry;
303
+ /**
304
+ * (For nested / inner function declarations)
305
+ * Context variables (i.e. captured from an outer scope) that are immutable.
306
+ * Note that this technically could be merged into `knownImmutableIdentifiers`,
307
+ * but are currently kept separate for readability.
308
+ */
309
+ nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
310
+};
311
function collectNonNullsInBlocks(
312
fn: HIRFunction,
233
- temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
234
- hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
235
- registry: PropertyPathRegistry,
313
+ context: CollectNonNullsInBlocksContext,
314
): ReadonlyMap<BlockId, BlockInfo> {
237
- /**
238
- * Due to current limitations of mutable range inference, there are edge cases in
239
- * which we infer known-immutable values (e.g. props or hook params) to have a
240
- * mutable range and scope.
241
- * (see `destructure-array-declaration-to-context-var` fixture)
242
- * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
243
- * is being rewritten to HIR).
244
- */
245
- const knownImmutableIdentifiers = new Set<IdentifierId>();
246
- if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
247
- for (const p of fn.params) {
248
- if (p.kind === 'Identifier') {
249
- knownImmutableIdentifiers.add(p.identifier.id);
250
- }
251
- }
252
- }
315
/**
316
* Known non-null objects such as functional component props can be safely
317
* read from any block.
323
fn.params[0].kind === 'Identifier'
324
) {
325
const identifier = fn.params[0].identifier;
264
- knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
326
+ knownNonNullIdentifiers.add(
327
+ context.registry.getOrCreateIdentifier(identifier),
328
+ );
329
}
330
const nodes = new Map<BlockId, BlockInfo>();
331
for (const [_, block] of fn.body.blocks) {
333
knownNonNullIdentifiers,
334
);
335
272
- const maybeOptionalChain = hoistableFromOptionals.get(block.id);
336
+ const maybeOptionalChain = context.hoistableFromOptionals.get(block.id);
337
if (maybeOptionalChain != null) {
338
assumedNonNullObjects.add(
275
- registry.getOrCreateProperty(maybeOptionalChain),
339
+ context.registry.getOrCreateProperty(maybeOptionalChain),
340
);
341
}
342
for (const instr of block.instructions) {
279
- const maybeNonNull = getMaybeNonNullInInstruction(
280
- instr.value,
281
- temporaries,
282
- registry,
283
- );
284
- if (maybeNonNull != null) {
285
- const baseIdentifier = maybeNonNull.fullPath.identifier;
286
- /**
287
- * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
288
- * are not valid with respect to current instruction id numbering.
289
- * We use attached reactive scope ranges as a proxy for mutable range, but this
290
- * is an overestimate as (1) scope ranges merge and align to form valid program
291
- * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
292
- * non-mutable identifiers.
293
- *
294
- * See comment at top of function for why we track known immutable identifiers.
295
- */
296
- const isMutableAtInstr =
297
- baseIdentifier.mutableRange.end >
298
- baseIdentifier.mutableRange.start + 1 &&
299
- baseIdentifier.scope != null &&
300
- inRange(
301
- {
302
- id: instr.id,
303
- },
304
- baseIdentifier.scope.range,
305
- );
306
- if (
307
- !isMutableAtInstr ||
308
- knownImmutableIdentifiers.has(baseIdentifier.id)
309
- ) {
310
- assumedNonNullObjects.add(maybeNonNull);
343
+ const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
344
+ if (
345
+ maybeNonNull != null &&
346
+ isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context)
347
+ ) {
348
+ assumedNonNullObjects.add(maybeNonNull);
349
+ }
350
+ if (
351
+ instr.value.kind === 'FunctionExpression' &&
352
+ !fn.env.config.enableTreatFunctionDepsAsConditional
353
+ ) {
354
+ const innerFn = instr.value.loweredFunc;
355
+ const innerTemporaries = collectTemporariesSidemap(
356
+ innerFn.func,
357
+ new Set(),
358
+ );
359
+ const innerOptionals = collectOptionalChainSidemap(innerFn.func);
360
+ const innerHoistableMap = collectHoistablePropertyLoads(
361
+ innerFn.func,
362
+ innerTemporaries,
363
+ innerOptionals.hoistableObjects,
364
+ context.nestedFnImmutableContext ??
365
+ new Set(
366
+ innerFn.func.context
367
+ .filter(place =>
368
+ isImmutableAtInstr(place.identifier, instr.id, context),
369
+ )
370
+ .map(place => place.identifier.id),
371
+ ),
372
+ );
373
+ const innerHoistables = assertNonNull(
374
+ innerHoistableMap.get(innerFn.func.body.entry),
375
+ );
376
+ for (const entry of innerHoistables.assumedNonNullObjects) {
377
+ assumedNonNullObjects.add(entry);
378
}
379
}
380
}
582
}
583
} while (changed);
584
}
585
+
586
+function collectFunctionExpressionFakeLoads(
587
+ fn: HIRFunction,
588
+): Set<IdentifierId> {
589
+ const sources = new Map<IdentifierId, IdentifierId>();
590
+ const functionExpressionReferences = new Set<IdentifierId>();
591
+
592
+ for (const [_, block] of fn.body.blocks) {
593
+ for (const {lvalue, value} of block.instructions) {
594
+ if (value.kind === 'FunctionExpression') {
595
+ for (const reference of value.loweredFunc.dependencies) {
596
+ let curr: IdentifierId | undefined = reference.identifier.id;
597
+ while (curr != null) {
598
+ functionExpressionReferences.add(curr);
599
+ curr = sources.get(curr);
600
+ }
601
+ }
602
+ } else if (value.kind === 'PropertyLoad') {
603
+ sources.set(lvalue.identifier.id, value.object.identifier.id);
604
+ }
605
+ }
606
+ }
607
+ return functionExpressionReferences;
608
+}