13
BlockId,
14
DependencyPathEntry,
15
GeneratedSource,
16
+ getHookKind,
17
HIRFunction,
18
Identifier,
19
IdentifierId,
20
InstructionId,
21
InstructionValue,
22
+ LoweredFunction,
23
PropertyLiteral,
24
ReactiveScopeDependency,
25
ScopeId,
114
hoistableFromOptionals,
115
registry,
116
nestedFnImmutableContext: null,
117
+ assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
118
+ ? new Set()
119
+ : getAssumedInvokedFunctions(fn),
120
});
121
}
122
132
* but are currently kept separate for readability.
133
*/
134
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
135
+ /**
136
+ * Functions which are assumed to be eventually called (as opposed to ones which might
137
+ * not be called, e.g. the 0th argument of Array.map)
138
+ */
139
+ assumedInvokedFns: ReadonlySet<LoweredFunction>;
140
};
141
function collectHoistablePropertyLoadsImpl(
142
fn: HIRFunction,
348
context.registry.getOrCreateIdentifier(identifier),
349
);
350
}
341
- const nodes = new Map<BlockId, BlockInfo>();
351
+ const nodes = new Map<
352
+ BlockId,
353
+ {
354
+ block: BasicBlock;
355
+ assumedNonNullObjects: Set<PropertyPathNode>;
356
+ }
357
+ >();
358
for (const [_, block] of fn.body.blocks) {
359
const assumedNonNullObjects = new Set<PropertyPathNode>(
360
knownNonNullIdentifiers,
374
) {
375
assumedNonNullObjects.add(maybeNonNull);
376
}
361
- if (
362
- (instr.value.kind === 'FunctionExpression' ||
363
- instr.value.kind === 'ObjectMethod') &&
364
- !fn.env.config.enableTreatFunctionDepsAsConditional
365
- ) {
377
+ if (instr.value.kind === 'FunctionExpression') {
378
const innerFn = instr.value.loweredFunc;
367
- const innerHoistableMap = collectHoistablePropertyLoadsImpl(
368
- innerFn.func,
369
- {
370
- ...context,
371
- nestedFnImmutableContext:
372
- context.nestedFnImmutableContext ??
373
- new Set(
374
- innerFn.func.context
375
- .filter(place =>
376
- isImmutableAtInstr(place.identifier, instr.id, context),
377
- )
378
- .map(place => place.identifier.id),
379
- ),
380
- },
381
- );
382
- const innerHoistables = assertNonNull(
383
- innerHoistableMap.get(innerFn.func.body.entry),
384
- );
385
- for (const entry of innerHoistables.assumedNonNullObjects) {
386
- assumedNonNullObjects.add(entry);
379
+ if (context.assumedInvokedFns.has(innerFn)) {
380
+ const innerHoistableMap = collectHoistablePropertyLoadsImpl(
381
+ innerFn.func,
382
+ {
383
+ ...context,
384
+ nestedFnImmutableContext:
385
+ context.nestedFnImmutableContext ??
386
+ new Set(
387
+ innerFn.func.context
388
+ .filter(place =>
389
+ isImmutableAtInstr(place.identifier, instr.id, context),
390
+ )
391
+ .map(place => place.identifier.id),
392
+ ),
393
+ },
394
+ );
395
+ const innerHoistables = assertNonNull(
396
+ innerHoistableMap.get(innerFn.func.body.entry),
397
+ );
398
+ for (const entry of innerHoistables.assumedNonNullObjects) {
399
+ assumedNonNullObjects.add(entry);
400
+ }
401
}
402
}
403
}
605
}
606
} while (changed);
607
}
608
+
609
+function getAssumedInvokedFunctions(
610
+ fn: HIRFunction,
611
+ temporaries: Map<
612
+ IdentifierId,
613
+ {fn: LoweredFunction; mayInvoke: Set<LoweredFunction>}
614
+ > = new Map(),
615
+): ReadonlySet<LoweredFunction> {
616
+ const hoistableFunctions = new Set<LoweredFunction>();
617
+ /**
618
+ * Step 1: Conservatively collect identifier to function expression mappings
619
+ */
620
+ for (const block of fn.body.blocks.values()) {
621
+ for (const {lvalue, value} of block.instructions) {
622
+ /**
623
+ * Conservatively only match function expressions which can have guaranteed ssa.
624
+ * ObjectMethods and ObjectProperties do not.
625
+ */
626
+ if (value.kind === 'FunctionExpression') {
627
+ temporaries.set(lvalue.identifier.id, {
628
+ fn: value.loweredFunc,
629
+ mayInvoke: new Set(),
630
+ });
631
+ } else if (value.kind === 'StoreLocal') {
632
+ const lvalue = value.lvalue.place.identifier;
633
+ const maybeLoweredFunc = temporaries.get(value.value.identifier.id);
634
+ if (maybeLoweredFunc != null) {
635
+ temporaries.set(lvalue.id, maybeLoweredFunc);
636
+ }
637
+ } else if (value.kind === 'LoadLocal') {
638
+ const maybeLoweredFunc = temporaries.get(value.place.identifier.id);
639
+ if (maybeLoweredFunc != null) {
640
+ temporaries.set(lvalue.identifier.id, maybeLoweredFunc);
641
+ }
642
+ }
643
+ }
644
+ }
645
+ /**
646
+ * Step 2: Forward pass to do analysis of assumed function calls. Note that
647
+ * this is conservative and does not count indirect references through
648
+ * containers (e.g. `return {cb: () => {...}})`).
649
+ */
650
+ for (const block of fn.body.blocks.values()) {
651
+ for (const {lvalue, value} of block.instructions) {
652
+ if (value.kind === 'CallExpression') {
653
+ const callee = value.callee;
654
+ const maybeHook = getHookKind(fn.env, callee.identifier);
655
+ const maybeLoweredFunc = temporaries.get(callee.identifier.id);
656
+ if (maybeLoweredFunc != null) {
657
+ // Direct calls
658
+ hoistableFunctions.add(maybeLoweredFunc.fn);
659
+ } else if (maybeHook != null) {
660
+ /**
661
+ * Assume arguments to all hooks are safe to invoke
662
+ */
663
+ for (const arg of value.args) {
664
+ if (arg.kind === 'Identifier') {
665
+ const maybeLoweredFunc = temporaries.get(arg.identifier.id);
666
+ if (maybeLoweredFunc != null) {
667
+ hoistableFunctions.add(maybeLoweredFunc.fn);
668
+ }
669
+ }
670
+ }
671
+ }
672
+ } else if (value.kind === 'JsxExpression') {
673
+ /**
674
+ * Assume JSX attributes and children are safe to invoke
675
+ */
676
+ for (const attr of value.props) {
677
+ if (attr.kind === 'JsxSpreadAttribute') {
678
+ continue;
679
+ }
680
+ const maybeLoweredFunc = temporaries.get(attr.place.identifier.id);
681
+ if (maybeLoweredFunc != null) {
682
+ hoistableFunctions.add(maybeLoweredFunc.fn);
683
+ }
684
+ }
685
+ for (const child of value.children ?? []) {
686
+ const maybeLoweredFunc = temporaries.get(child.identifier.id);
687
+ if (maybeLoweredFunc != null) {
688
+ hoistableFunctions.add(maybeLoweredFunc.fn);
689
+ }
690
+ }
691
+ } else if (value.kind === 'FunctionExpression') {
692
+ /**
693
+ * Recursively traverse into other function expressions which may invoke
694
+ * or pass already declared functions to react (e.g. as JSXAttributes).
695
+ *
696
+ * If lambda A calls lambda B, we assume lambda B is safe to invoke if
697
+ * lambda A is -- even if lambda B is conditionally called. (see
698
+ * `conditional-call-chain` fixture for example).
699
+ */
700
+ const loweredFunc = value.loweredFunc.func;
701
+ const lambdasCalled = getAssumedInvokedFunctions(
702
+ loweredFunc,
703
+ temporaries,
704
+ );
705
+ const maybeLoweredFunc = temporaries.get(lvalue.identifier.id);
706
+ if (maybeLoweredFunc != null) {
707
+ for (const called of lambdasCalled) {
708
+ maybeLoweredFunc.mayInvoke.add(called);
709
+ }
710
+ }
711
+ }
712
+ }
713
+ if (block.terminal.kind === 'return') {
714
+ /**
715
+ * Assume directly returned functions are safe to call
716
+ */
717
+ const maybeLoweredFunc = temporaries.get(
718
+ block.terminal.value.identifier.id,
719
+ );
720
+ if (maybeLoweredFunc != null) {
721
+ hoistableFunctions.add(maybeLoweredFunc.fn);
722
+ }
723
+ }
724
+ }
725
+
726
+ for (const [_, {fn, mayInvoke}] of temporaries) {
727
+ if (hoistableFunctions.has(fn)) {
728
+ for (const called of mayInvoke) {
729
+ hoistableFunctions.add(called);
730
+ }
731
+ }
732
+ }
733
+ return hoistableFunctions;
734
+}