main
ts 848 lines 25.7 KB
Raw
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 {
9 ScopeId,
10 HIRFunction,
11 Place,
12 Instruction,
13 ReactiveScopeDependency,
14 Identifier,
15 ReactiveScope,
16 isObjectMethodType,
17 isRefValueType,
18 isUseRefType,
19 makeInstructionId,
20 InstructionId,
21 InstructionKind,
22 GeneratedSource,
23 DeclarationId,
24 areEqualPaths,
25 IdentifierId,
26 Terminal,
27 InstructionValue,
28 LoadContext,
29 TInstruction,
30 FunctionExpression,
31 ObjectMethod,
32 PropertyLiteral,
33 convertHoistedLValueKind,
34 SourceLocation,
35 } from './HIR';
36 import {
37 collectHoistablePropertyLoads,
38 keyByScopeId,
39 } from './CollectHoistablePropertyLoads';
40 import {
41 ScopeBlockTraversal,
42 eachInstructionOperand,
43 eachInstructionValueOperand,
44 eachPatternOperand,
45 eachTerminalOperand,
46 } from './visitors';
47 import {Stack, empty} from '../Utils/Stack';
48 import {CompilerError} from '../CompilerError';
49 import {Iterable_some} from '../Utils/utils';
50 import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
51 import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
52
53 export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
54 const usedOutsideDeclaringScope =
55 findTemporariesUsedOutsideDeclaringScope(fn);
56 const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
57 const {
58 temporariesReadInOptional,
59 processedInstrsInOptional,
60 hoistableObjects,
61 } = collectOptionalChainSidemap(fn);
62
63 const hoistablePropertyLoads = keyByScopeId(
64 fn,
65 collectHoistablePropertyLoads(fn, temporaries, hoistableObjects),
66 );
67
68 const scopeDeps = collectDependencies(
69 fn,
70 usedOutsideDeclaringScope,
71 new Map([...temporaries, ...temporariesReadInOptional]),
72 processedInstrsInOptional,
73 );
74
75 /**
76 * Derive the minimal set of hoistable dependencies for each scope.
77 */
78 for (const [scope, deps] of scopeDeps) {
79 if (deps.length === 0) {
80 continue;
81 }
82
83 /**
84 * Step 1: Find hoistable accesses, given the basic block in which the scope
85 * begins.
86 */
87 const hoistables = hoistablePropertyLoads.get(scope.id);
88 CompilerError.invariant(hoistables != null, {
89 reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
90 loc: GeneratedSource,
91 });
92 /**
93 * Step 2: Calculate hoistable dependencies.
94 */
95 const tree = new ReactiveScopeDependencyTreeHIR(
96 [...hoistables.assumedNonNullObjects].map(o => o.fullPath),
97 );
98 for (const dep of deps) {
99 tree.addDependency({...dep});
100 }
101
102 /**
103 * Step 3: Reduce dependencies to a minimal set.
104 */
105 const candidates = tree.deriveMinimalDependencies();
106 for (const candidateDep of candidates) {
107 if (
108 !Iterable_some(
109 scope.dependencies,
110 existingDep =>
111 existingDep.identifier.declarationId ===
112 candidateDep.identifier.declarationId &&
113 areEqualPaths(existingDep.path, candidateDep.path),
114 )
115 )
116 scope.dependencies.add(candidateDep);
117 }
118 }
119 }
120
121 export function findTemporariesUsedOutsideDeclaringScope(
122 fn: HIRFunction,
123 ): ReadonlySet<DeclarationId> {
124 /*
125 * tracks all relevant LoadLocal and PropertyLoad lvalues
126 * and the scope where they are defined
127 */
128 const declarations = new Map<DeclarationId, ScopeId>();
129 const prunedScopes = new Set<ScopeId>();
130 const scopeTraversal = new ScopeBlockTraversal();
131 const usedOutsideDeclaringScope = new Set<DeclarationId>();
132
133 function handlePlace(place: Place): void {
134 const declaringScope = declarations.get(place.identifier.declarationId);
135 if (
136 declaringScope != null &&
137 !scopeTraversal.isScopeActive(declaringScope) &&
138 !prunedScopes.has(declaringScope)
139 ) {
140 // Declaring scope is not active === used outside declaring scope
141 usedOutsideDeclaringScope.add(place.identifier.declarationId);
142 }
143 }
144
145 function handleInstruction(instr: Instruction): void {
146 const scope = scopeTraversal.currentScope;
147 if (scope == null || prunedScopes.has(scope)) {
148 return;
149 }
150 switch (instr.value.kind) {
151 case 'LoadLocal':
152 case 'LoadContext':
153 case 'PropertyLoad': {
154 declarations.set(instr.lvalue.identifier.declarationId, scope);
155 break;
156 }
157 default: {
158 break;
159 }
160 }
161 }
162
163 for (const [blockId, block] of fn.body.blocks) {
164 scopeTraversal.recordScopes(block);
165 const scopeStartInfo = scopeTraversal.blockInfos.get(blockId);
166 if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) {
167 prunedScopes.add(scopeStartInfo.scope.id);
168 }
169 for (const instr of block.instructions) {
170 for (const place of eachInstructionOperand(instr)) {
171 handlePlace(place);
172 }
173 handleInstruction(instr);
174 }
175
176 for (const place of eachTerminalOperand(block.terminal)) {
177 handlePlace(place);
178 }
179 }
180 return usedOutsideDeclaringScope;
181 }
182
183 /**
184 * @returns mapping of LoadLocal and PropertyLoad to the source of the load.
185 * ```js
186 * // source
187 * foo(a.b);
188 *
189 * // HIR: a potential sidemap is {0: a, 1: a.b, 2: foo}
190 * $0 = LoadLocal 'a'
191 * $1 = PropertyLoad $0, 'b'
192 * $2 = LoadLocal 'foo'
193 * $3 = CallExpression $2($1)
194 * ```
195 * @param usedOutsideDeclaringScope is used to check the correctness of
196 * reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
197 * PropertyLoad in the returned temporaries map if reordering the read (from the
198 * time-of-load to time-of-use) is valid.
199 *
200 * If a LoadLocal or PropertyLoad instruction is within the reactive scope range
201 * (a proxy for mutable range) of the load source, later instructions may
202 * reassign / mutate the source value. Since it's incorrect to reorder these
203 * load instructions to after their scope ranges, we also do not store them in
204 * identifier sidemaps.
205 *
206 * Take this example (from fixture
207 * `evaluation-order-mutate-call-after-dependency-load`)
208 * ```js
209 * // source
210 * function useFoo(arg) {
211 * const arr = [1, 2, 3, ...arg];
212 * return [
213 * arr.length,
214 * arr.push(0)
215 * ];
216 * }
217 *
218 * // IR pseudocode
219 * scope @0 {
220 * $0 = arr = ArrayExpression [1, 2, 3, ...arg]
221 * $1 = arr.length
222 * $2 = arr.push(0)
223 * }
224 * scope @1 {
225 * $3 = ArrayExpression [$1, $2]
226 * }
227 * ```
228 * Here, it's invalid for scope@1 to take `arr.length` as a dependency instead
229 * of $1, as the evaluation of `arr.length` changes between instructions $1 and
230 * $3. We do not track $1 -> arr.length in this case.
231 */
232 export function collectTemporariesSidemap(
233 fn: HIRFunction,
234 usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
235 ): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
236 const temporaries = new Map();
237 collectTemporariesSidemapImpl(
238 fn,
239 usedOutsideDeclaringScope,
240 temporaries,
241 null,
242 );
243 return temporaries;
244 }
245
246 function isLoadContextMutable(
247 instrValue: InstructionValue,
248 id: InstructionId,
249 ): instrValue is LoadContext {
250 if (instrValue.kind === 'LoadContext') {
251 /**
252 * Not all context variables currently have scopes due to limitations of
253 * mutability analysis for function expressions.
254 *
255 * Currently, many function expressions references are inferred to be
256 * 'Read' | 'Freeze' effects which don't replay mutable effects of captured
257 * context.
258 */
259 return (
260 instrValue.place.identifier.scope != null &&
261 id >= instrValue.place.identifier.scope.range.end
262 );
263 }
264 return false;
265 }
266 /**
267 * Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
268 * function and all nested functions.
269 *
270 * Note that IdentifierIds are currently unique, so we can use a single
271 * Map<IdentifierId, ...> across all nested functions.
272 */
273 function collectTemporariesSidemapImpl(
274 fn: HIRFunction,
275 usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
276 temporaries: Map<IdentifierId, ReactiveScopeDependency>,
277 innerFnContext: {instrId: InstructionId} | null,
278 ): void {
279 for (const [_, block] of fn.body.blocks) {
280 for (const {value, lvalue, id: origInstrId} of block.instructions) {
281 const instrId =
282 innerFnContext != null ? innerFnContext.instrId : origInstrId;
283 const usedOutside = usedOutsideDeclaringScope.has(
284 lvalue.identifier.declarationId,
285 );
286
287 if (value.kind === 'PropertyLoad' && !usedOutside) {
288 if (
289 innerFnContext == null ||
290 temporaries.has(value.object.identifier.id)
291 ) {
292 /**
293 * All dependencies of a inner / nested function must have a base
294 * identifier from the outermost component / hook. This is because the
295 * compiler cannot break an inner function into multiple granular
296 * scopes.
297 */
298 const property = getProperty(
299 value.object,
300 value.property,
301 false,
302 value.loc,
303 temporaries,
304 );
305 temporaries.set(lvalue.identifier.id, property);
306 }
307 } else if (
308 (value.kind === 'LoadLocal' || isLoadContextMutable(value, instrId)) &&
309 lvalue.identifier.name == null &&
310 value.place.identifier.name !== null &&
311 !usedOutside
312 ) {
313 if (
314 innerFnContext == null ||
315 fn.context.some(
316 context => context.identifier.id === value.place.identifier.id,
317 )
318 ) {
319 temporaries.set(lvalue.identifier.id, {
320 identifier: value.place.identifier,
321 reactive: value.place.reactive,
322 path: [],
323 loc: value.loc,
324 });
325 }
326 } else if (
327 value.kind === 'FunctionExpression' ||
328 value.kind === 'ObjectMethod'
329 ) {
330 collectTemporariesSidemapImpl(
331 value.loweredFunc.func,
332 usedOutsideDeclaringScope,
333 temporaries,
334 innerFnContext ?? {instrId},
335 );
336 }
337 }
338 }
339 }
340
341 function getProperty(
342 object: Place,
343 propertyName: PropertyLiteral,
344 optional: boolean,
345 loc: SourceLocation,
346 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
347 ): ReactiveScopeDependency {
348 /*
349 * (1) Get the base object either from the temporary sidemap (e.g. a LoadLocal)
350 * or a deep copy of an existing property dependency.
351 * Example 1:
352 * $0 = LoadLocal x
353 * $1 = PropertyLoad $0.y
354 * getProperty($0, ...) -> resolvedObject = x, resolvedDependency = null
355 *
356 * Example 2:
357 * $0 = LoadLocal x
358 * $1 = PropertyLoad $0.y
359 * $2 = PropertyLoad $1.z
360 * getProperty($1, ...) -> resolvedObject = null, resolvedDependency = x.y
361 *
362 * Example 3:
363 * $0 = Call(...)
364 * $1 = PropertyLoad $0.y
365 * getProperty($0, ...) -> resolvedObject = null, resolvedDependency = null
366 */
367 const resolvedDependency = temporaries.get(object.identifier.id);
368
369 /**
370 * (2) Push the last PropertyLoad
371 * TODO(mofeiZ): understand optional chaining
372 */
373 let property: ReactiveScopeDependency;
374 if (resolvedDependency == null) {
375 property = {
376 identifier: object.identifier,
377 reactive: object.reactive,
378 path: [{property: propertyName, optional, loc}],
379 loc,
380 };
381 } else {
382 property = {
383 identifier: resolvedDependency.identifier,
384 reactive: resolvedDependency.reactive,
385 path: [
386 ...resolvedDependency.path,
387 {property: propertyName, optional, loc},
388 ],
389 loc,
390 };
391 }
392 return property;
393 }
394
395 type Decl = {
396 id: InstructionId;
397 scope: Stack<ReactiveScope>;
398 };
399
400 export class DependencyCollectionContext {
401 #declarations: Map<DeclarationId, Decl> = new Map();
402 #reassignments: Map<Identifier, Decl> = new Map();
403
404 #scopes: Stack<ReactiveScope> = empty();
405 // Reactive dependencies used in the current reactive scope.
406 #dependencies: Stack<Array<ReactiveScopeDependency>> = empty();
407 deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
408
409 #temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
410 #temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
411 #processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
412
413 /**
414 * Tracks the traversal state. See Context.declare for explanation of why this
415 * is needed.
416 */
417 #innerFnContext: {outerInstrId: InstructionId} | null = null;
418
419 constructor(
420 temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
421 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
422 processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
423 ) {
424 this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
425 this.#temporaries = temporaries;
426 this.#processedInstrsInOptional = processedInstrsInOptional;
427 }
428
429 enterScope(scope: ReactiveScope): void {
430 // Set context for new scope
431 this.#dependencies = this.#dependencies.push([]);
432 this.#scopes = this.#scopes.push(scope);
433 }
434
435 exitScope(scope: ReactiveScope, pruned: boolean): void {
436 // Save dependencies we collected from the exiting scope
437 const scopedDependencies = this.#dependencies.value;
438 CompilerError.invariant(scopedDependencies != null, {
439 reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
440 loc: scope.loc,
441 });
442
443 // Restore context of previous scope
444 this.#scopes = this.#scopes.pop();
445 this.#dependencies = this.#dependencies.pop();
446
447 /*
448 * Collect dependencies we recorded for the exiting scope and propagate
449 * them upward using the same rules as normal dependency collection.
450 * Child scopes may have dependencies on values created within the outer
451 * scope, which necessarily cannot be dependencies of the outer scope.
452 */
453 for (const dep of scopedDependencies) {
454 if (this.#checkValidDependency(dep)) {
455 this.#dependencies.value?.push(dep);
456 }
457 }
458
459 if (!pruned) {
460 this.deps.set(scope, scopedDependencies);
461 }
462 }
463
464 isUsedOutsideDeclaringScope(place: Place): boolean {
465 return this.#temporariesUsedOutsideScope.has(
466 place.identifier.declarationId,
467 );
468 }
469
470 /*
471 * Records where a value was declared, and optionally, the scope where the
472 * value originated from. This is later used to determine if a dependency
473 * should be added to a scope; if the current scope we are visiting is the
474 * same scope where the value originates, it can't be a dependency on itself.
475 *
476 * Note that we do not track declarations or reassignments within inner
477 * functions for the following reasons:
478 * - inner functions cannot be split by scope boundaries and are guaranteed
479 * to consume their own declarations
480 * - reassignments within inner functions are tracked as context variables,
481 * which already have extended mutable ranges to account for reassignments
482 * - *most importantly* it's currently simply incorrect to compare inner
483 * function instruction ids (tracked by `decl`) with outer ones (as stored
484 * by root identifier mutable ranges).
485 */
486 declare(identifier: Identifier, decl: Decl): void {
487 if (this.#innerFnContext != null) return;
488 if (!this.#declarations.has(identifier.declarationId)) {
489 this.#declarations.set(identifier.declarationId, decl);
490 }
491 this.#reassignments.set(identifier, decl);
492 }
493 hasDeclared(identifier: Identifier): boolean {
494 return this.#declarations.has(identifier.declarationId);
495 }
496
497 // Checks if identifier is a valid dependency in the current scope
498 #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
499 // ref value is not a valid dep
500 if (isRefValueType(maybeDependency.identifier)) {
501 return false;
502 }
503
504 /*
505 * object methods are not deps because they will be codegen'ed back in to
506 * the object literal.
507 */
508 if (isObjectMethodType(maybeDependency.identifier)) {
509 return false;
510 }
511
512 const identifier = maybeDependency.identifier;
513 /*
514 * If this operand is used in a scope, has a dynamic value, and was defined
515 * before this scope, then its a dependency of the scope.
516 */
517 const currentDeclaration =
518 this.#reassignments.get(identifier) ??
519 this.#declarations.get(identifier.declarationId);
520 const currentScope = this.currentScope.value;
521 return (
522 currentScope != null &&
523 currentDeclaration !== undefined &&
524 currentDeclaration.id < currentScope.range.start
525 );
526 }
527
528 #isScopeActive(scope: ReactiveScope): boolean {
529 if (this.#scopes === null) {
530 return false;
531 }
532 return this.#scopes.find(state => state === scope);
533 }
534
535 get currentScope(): Stack<ReactiveScope> {
536 return this.#scopes;
537 }
538
539 visitOperand(place: Place): void {
540 /*
541 * if this operand is a temporary created for a property load, try to resolve it to
542 * the expanded Place. Fall back to using the operand as-is.
543 */
544 this.visitDependency(
545 this.#temporaries.get(place.identifier.id) ?? {
546 identifier: place.identifier,
547 reactive: place.reactive,
548 path: [],
549 loc: place.loc,
550 },
551 );
552 }
553
554 visitProperty(
555 object: Place,
556 property: PropertyLiteral,
557 optional: boolean,
558 loc: SourceLocation,
559 ): void {
560 const nextDependency = getProperty(
561 object,
562 property,
563 optional,
564 loc,
565 this.#temporaries,
566 );
567 this.visitDependency(nextDependency);
568 }
569
570 visitDependency(maybeDependency: ReactiveScopeDependency): void {
571 /*
572 * Any value used after its originally defining scope has concluded must be added as an
573 * output of its defining scope. Regardless of whether its a const or not,
574 * some later code needs access to the value. If the current
575 * scope we are visiting is the same scope where the value originates, it can't be a dependency
576 * on itself.
577 */
578
579 /*
580 * if originalDeclaration is undefined here, then this is not a local var
581 * (all decls e.g. `let x;` should be initialized in BuildHIR)
582 */
583 const originalDeclaration = this.#declarations.get(
584 maybeDependency.identifier.declarationId,
585 );
586 if (
587 originalDeclaration !== undefined &&
588 originalDeclaration.scope.value !== null
589 ) {
590 originalDeclaration.scope.each(scope => {
591 if (
592 !this.#isScopeActive(scope) &&
593 !Iterable_some(
594 scope.declarations.values(),
595 decl =>
596 decl.identifier.declarationId ===
597 maybeDependency.identifier.declarationId,
598 )
599 ) {
600 scope.declarations.set(maybeDependency.identifier.id, {
601 identifier: maybeDependency.identifier,
602 scope: originalDeclaration.scope.value!,
603 });
604 }
605 });
606 }
607
608 // ref.current access is not a valid dep
609 if (
610 isUseRefType(maybeDependency.identifier) &&
611 maybeDependency.path.at(0)?.property === 'current'
612 ) {
613 maybeDependency = {
614 identifier: maybeDependency.identifier,
615 reactive: maybeDependency.reactive,
616 path: [],
617 loc: maybeDependency.loc,
618 };
619 }
620 if (this.#checkValidDependency(maybeDependency)) {
621 this.#dependencies.value!.push(maybeDependency);
622 }
623 }
624
625 /*
626 * Record a variable that is declared in some other scope and that is being reassigned in the
627 * current one as a {@link ReactiveScope.reassignments}
628 */
629 visitReassignment(place: Place): void {
630 const currentScope = this.currentScope.value;
631 if (
632 currentScope != null &&
633 !Iterable_some(
634 currentScope.reassignments,
635 identifier =>
636 identifier.declarationId === place.identifier.declarationId,
637 ) &&
638 this.#checkValidDependency({
639 identifier: place.identifier,
640 reactive: place.reactive,
641 path: [],
642 loc: place.loc,
643 })
644 ) {
645 currentScope.reassignments.add(place.identifier);
646 }
647 }
648 enterInnerFn<T>(
649 innerFn: TInstruction<FunctionExpression> | TInstruction<ObjectMethod>,
650 cb: () => T,
651 ): T {
652 const prevContext = this.#innerFnContext;
653 this.#innerFnContext = this.#innerFnContext ?? {outerInstrId: innerFn.id};
654 const result = cb();
655 this.#innerFnContext = prevContext;
656 return result;
657 }
658
659 /**
660 * Skip dependencies that are subexpressions of other dependencies. e.g. if a
661 * dependency is tracked in the temporaries sidemap, it can be added at
662 * site-of-use
663 */
664 isDeferredDependency(
665 instr:
666 | {kind: HIRValue.Instruction; value: Instruction}
667 | {kind: HIRValue.Terminal; value: Terminal},
668 ): boolean {
669 return (
670 this.#processedInstrsInOptional.has(instr.value) ||
671 (instr.kind === HIRValue.Instruction &&
672 this.#temporaries.has(instr.value.lvalue.identifier.id))
673 );
674 }
675 }
676 enum HIRValue {
677 Instruction = 1,
678 Terminal,
679 }
680
681 export function handleInstruction(
682 instr: Instruction,
683 context: DependencyCollectionContext,
684 ): void {
685 const {id, value, lvalue} = instr;
686 context.declare(lvalue.identifier, {
687 id,
688 scope: context.currentScope,
689 });
690 if (
691 context.isDeferredDependency({kind: HIRValue.Instruction, value: instr})
692 ) {
693 return;
694 }
695 if (value.kind === 'PropertyLoad') {
696 context.visitProperty(value.object, value.property, false, value.loc);
697 } else if (value.kind === 'StoreLocal') {
698 context.visitOperand(value.value);
699 if (value.lvalue.kind === InstructionKind.Reassign) {
700 context.visitReassignment(value.lvalue.place);
701 }
702 context.declare(value.lvalue.place.identifier, {
703 id,
704 scope: context.currentScope,
705 });
706 } else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
707 /*
708 * Some variables may be declared and never initialized. We need to retain
709 * (and hoist) these declarations if they are included in a reactive scope.
710 * One approach is to simply add all `DeclareLocal`s as scope declarations.
711 *
712 * Context variables with hoisted declarations only become live after their
713 * first assignment. We only declare real DeclareLocal / DeclareContext
714 * instructions (not hoisted ones) to avoid generating dependencies on
715 * hoisted declarations.
716 */
717 if (convertHoistedLValueKind(value.lvalue.kind) === null) {
718 context.declare(value.lvalue.place.identifier, {
719 id,
720 scope: context.currentScope,
721 });
722 }
723 } else if (value.kind === 'Destructure') {
724 context.visitOperand(value.value);
725 for (const place of eachPatternOperand(value.lvalue.pattern)) {
726 if (value.lvalue.kind === InstructionKind.Reassign) {
727 context.visitReassignment(place);
728 }
729 context.declare(place.identifier, {
730 id,
731 scope: context.currentScope,
732 });
733 }
734 } else if (value.kind === 'StoreContext') {
735 /**
736 * Some StoreContext variables have hoisted declarations. If we're storing
737 * to a context variable that hasn't yet been declared, the StoreContext is
738 * the declaration.
739 * (see corresponding logic in PruneHoistedContext)
740 */
741 if (
742 !context.hasDeclared(value.lvalue.place.identifier) ||
743 value.lvalue.kind !== InstructionKind.Reassign
744 ) {
745 context.declare(value.lvalue.place.identifier, {
746 id,
747 scope: context.currentScope,
748 });
749 }
750
751 for (const operand of eachInstructionValueOperand(value)) {
752 context.visitOperand(operand);
753 }
754 } else {
755 for (const operand of eachInstructionValueOperand(value)) {
756 context.visitOperand(operand);
757 }
758 }
759 }
760
761 function collectDependencies(
762 fn: HIRFunction,
763 usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
764 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
765 processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
766 ): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
767 const context = new DependencyCollectionContext(
768 usedOutsideDeclaringScope,
769 temporaries,
770 processedInstrsInOptional,
771 );
772
773 for (const param of fn.params) {
774 if (param.kind === 'Identifier') {
775 context.declare(param.identifier, {
776 id: makeInstructionId(0),
777 scope: empty(),
778 });
779 } else {
780 context.declare(param.place.identifier, {
781 id: makeInstructionId(0),
782 scope: empty(),
783 });
784 }
785 }
786
787 const scopeTraversal = new ScopeBlockTraversal();
788
789 const handleFunction = (fn: HIRFunction): void => {
790 for (const [blockId, block] of fn.body.blocks) {
791 scopeTraversal.recordScopes(block);
792 const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
793 if (scopeBlockInfo?.kind === 'begin') {
794 context.enterScope(scopeBlockInfo.scope);
795 } else if (scopeBlockInfo?.kind === 'end') {
796 context.exitScope(scopeBlockInfo.scope, scopeBlockInfo.pruned);
797 }
798 // Record referenced optional chains in phis
799 for (const phi of block.phis) {
800 for (const operand of phi.operands) {
801 const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
802 if (maybeOptionalChain) {
803 context.visitDependency(maybeOptionalChain);
804 }
805 }
806 }
807 for (const instr of block.instructions) {
808 if (
809 instr.value.kind === 'FunctionExpression' ||
810 instr.value.kind === 'ObjectMethod'
811 ) {
812 context.declare(instr.lvalue.identifier, {
813 id: instr.id,
814 scope: context.currentScope,
815 });
816 /**
817 * Recursively visit the inner function to extract dependencies there
818 */
819 const innerFn = instr.value.loweredFunc.func;
820 context.enterInnerFn(
821 instr as
822 | TInstruction<FunctionExpression>
823 | TInstruction<ObjectMethod>,
824 () => {
825 handleFunction(innerFn);
826 },
827 );
828 } else {
829 handleInstruction(instr, context);
830 }
831 }
832
833 if (
834 !context.isDeferredDependency({
835 kind: HIRValue.Terminal,
836 value: block.terminal,
837 })
838 ) {
839 for (const place of eachTerminalOperand(block.terminal)) {
840 context.visitOperand(place);
841 }
842 }
843 }
844 };
845
846 handleFunction(fn);
847 return context.deps;
848 }