main
ts 110 lines 3.86 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 {CompilerError} from '..';
9 import {
10 BlockId,
11 HIRFunction,
12 LabelTerminal,
13 PrunedScopeTerminal,
14 getHookKind,
15 isUseOperator,
16 } from '../HIR';
17 import {retainWhere} from '../Utils/utils';
18
19 /**
20 * For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
21 * from regular functions in two key ways:
22 * - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
23 * - They cannot be called conditionally
24 *
25 * The `use` operator is similar:
26 * - It can access context, and therefore introduce reactivity
27 * - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
28 * React uses the fact that use was called to remember that the component needs the value, and that changes to the
29 * input should invalidate the component itself.
30 *
31 * This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
32 * different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
33 * called conditionally in the output.
34 *
35 * The pass finds and removes any scopes that transitively contain a hook or use call. By running all
36 * the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
37 * describe the set of values which "construct together", and remove _all_ that memoization in order
38 * to ensure the hook call does not inadvertently become conditional.
39 */
40 export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
41 const activeScopes: Array<{block: BlockId; fallthrough: BlockId}> = [];
42 const prune: Array<BlockId> = [];
43
44 for (const [, block] of fn.body.blocks) {
45 retainWhere(activeScopes, current => current.fallthrough !== block.id);
46
47 for (const instr of block.instructions) {
48 const {value} = instr;
49 switch (value.kind) {
50 case 'MethodCall':
51 case 'CallExpression': {
52 const callee =
53 value.kind === 'MethodCall' ? value.property : value.callee;
54 if (
55 getHookKind(fn.env, callee.identifier) != null ||
56 isUseOperator(callee.identifier)
57 ) {
58 prune.push(...activeScopes.map(entry => entry.block));
59 activeScopes.length = 0;
60 }
61 }
62 }
63 }
64 if (block.terminal.kind === 'scope') {
65 activeScopes.push({
66 block: block.id,
67 fallthrough: block.terminal.fallthrough,
68 });
69 }
70 }
71
72 for (const id of prune) {
73 const block = fn.body.blocks.get(id)!;
74 const terminal = block.terminal;
75 CompilerError.invariant(terminal.kind === 'scope', {
76 reason: `Expected block to have a scope terminal`,
77 description: `Expected block bb${block.id} to end in a scope terminal`,
78 loc: terminal.loc,
79 });
80 const body = fn.body.blocks.get(terminal.block)!;
81 if (
82 body.instructions.length === 1 &&
83 body.terminal.kind === 'goto' &&
84 body.terminal.block === terminal.fallthrough
85 ) {
86 /*
87 * This was a scope just for a hook call, which doesn't need memoization.
88 * flatten it away. We rely on the PrunedUnusedLabel step to do the actual
89 * flattening
90 */
91 block.terminal = {
92 kind: 'label',
93 block: terminal.block,
94 fallthrough: terminal.fallthrough,
95 id: terminal.id,
96 loc: terminal.loc,
97 } as LabelTerminal;
98 continue;
99 }
100
101 block.terminal = {
102 kind: 'pruned-scope',
103 block: terminal.block,
104 fallthrough: terminal.fallthrough,
105 id: terminal.id,
106 loc: terminal.loc,
107 scope: terminal.scope,
108 } as PrunedScopeTerminal;
109 }
110 }