16
DeclarationId,
17
areEqualPaths,
18
IdentifierId,
19
+ Terminal,
20
} from './HIR';
21
import {
22
collectHoistablePropertyLoads,
177
* $2 = LoadLocal 'foo'
178
* $3 = CallExpression $2($1)
179
* ```
179
- * Only map LoadLocal and PropertyLoad lvalues to their source if we know that
180
- * reordering the read (from the time-of-load to time-of-use) is valid.
180
+ * @param usedOutsideDeclaringScope is used to check the correctness of
181
+ * reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
182
+ * PropertyLoad in the returned temporaries map if reordering the read (from the
183
+ * time-of-load to time-of-use) is valid.
184
*
185
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
186
* (a proxy for mutable range) of the load source, later instructions may
218
fn: HIRFunction,
219
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
220
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
218
- const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
221
+ const temporaries = new Map();
222
+ collectTemporariesSidemapImpl(
223
+ fn,
224
+ usedOutsideDeclaringScope,
225
+ temporaries,
226
+ false,
227
+ );
228
+ return temporaries;
229
+}
230
+
231
+/**
232
+ * Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
233
+ * function and all nested functions.
234
+ *
235
+ * Note that IdentifierIds are currently unique, so we can use a single
236
+ * Map<IdentifierId, ...> across all nested functions.
237
+ */
238
+function collectTemporariesSidemapImpl(
239
+ fn: HIRFunction,
240
+ usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
241
+ temporaries: Map<IdentifierId, ReactiveScopeDependency>,
242
+ isInnerFn: boolean,
243
+): void {
244
for (const [_, block] of fn.body.blocks) {
245
for (const instr of block.instructions) {
246
const {value, lvalue} = instr;
249
);
250
251
if (value.kind === 'PropertyLoad' && !usedOutside) {
227
- const property = getProperty(
228
- value.object,
229
- value.property,
230
- false,
231
- temporaries,
232
- );
233
- temporaries.set(lvalue.identifier.id, property);
252
+ if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
253
+ /**
254
+ * All dependencies of a inner / nested function must have a base
255
+ * identifier from the outermost component / hook. This is because the
256
+ * compiler cannot break an inner function into multiple granular
257
+ * scopes.
258
+ */
259
+ const property = getProperty(
260
+ value.object,
261
+ value.property,
262
+ false,
263
+ temporaries,
264
+ );
265
+ temporaries.set(lvalue.identifier.id, property);
266
+ }
267
} else if (
268
value.kind === 'LoadLocal' &&
269
lvalue.identifier.name == null &&
270
value.place.identifier.name !== null &&
271
!usedOutside
272
) {
240
- temporaries.set(lvalue.identifier.id, {
241
- identifier: value.place.identifier,
242
- path: [],
243
- });
273
+ if (
274
+ !isInnerFn ||
275
+ fn.context.some(
276
+ context => context.identifier.id === value.place.identifier.id,
277
+ )
278
+ ) {
279
+ temporaries.set(lvalue.identifier.id, {
280
+ identifier: value.place.identifier,
281
+ path: [],
282
+ });
283
+ }
284
+ } else if (
285
+ value.kind === 'FunctionExpression' ||
286
+ value.kind === 'ObjectMethod'
287
+ ) {
288
+ collectTemporariesSidemapImpl(
289
+ value.loweredFunc.func,
290
+ usedOutsideDeclaringScope,
291
+ temporaries,
292
+ true,
293
+ );
294
}
295
}
296
}
247
- return temporaries;
297
}
298
299
function getProperty(
359
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
360
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
361
362
+ /**
363
+ * Tracks the traversal state. See Context.declare for explanation of why this
364
+ * is needed.
365
+ */
366
+ inInnerFn: boolean = false;
367
+
368
constructor(
369
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
370
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
415
}
416
417
/*
363
- * Records where a value was declared, and optionally, the scope where the value originated from.
364
- * This is later used to determine if a dependency should be added to a scope; if the current
365
- * scope we are visiting is the same scope where the value originates, it can't be a dependency
366
- * on itself.
418
+ * Records where a value was declared, and optionally, the scope where the
419
+ * value originated from. This is later used to determine if a dependency
420
+ * should be added to a scope; if the current scope we are visiting is the
421
+ * same scope where the value originates, it can't be a dependency on itself.
422
+ *
423
+ * Note that we do not track declarations or reassignments within inner
424
+ * functions for the following reasons:
425
+ * - inner functions cannot be split by scope boundaries and are guaranteed
426
+ * to consume their own declarations
427
+ * - reassignments within inner functions are tracked as context variables,
428
+ * which already have extended mutable ranges to account for reassignments
429
+ * - *most importantly* it's currently simply incorrect to compare inner
430
+ * function instruction ids (tracked by `decl`) with outer ones (as stored
431
+ * by root identifier mutable ranges).
432
*/
433
declare(identifier: Identifier, decl: Decl): void {
434
+ if (this.inInnerFn) return;
435
if (!this.#declarations.has(identifier.declarationId)) {
436
this.#declarations.set(identifier.declarationId, decl);
437
}
641
fn: HIRFunction,
642
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
643
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
578
- processedInstrsInOptional: ReadonlySet<InstructionId>,
644
+ processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
645
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
646
const context = new Context(usedOutsideDeclaringScope, temporaries);
647
680
}
681
}
682
for (const instr of block.instructions) {
617
- if (!processedInstrsInOptional.has(instr.id)) {
683
+ if (!processedInstrsInOptional.has(instr)) {
684
handleInstruction(instr, context);
685
}
686
}
687
622
- if (!processedInstrsInOptional.has(block.terminal.id)) {
688
+ if (!processedInstrsInOptional.has(block.terminal)) {
689
for (const place of eachTerminalOperand(block.terminal)) {
690
context.visitOperand(place);
691
}