[compiler][cleanup] Delete now-unused reactive scope fork
Followup to #30891 ghstack-source-id: 6b42055b5d28da39d99a235bcd86a82eb7c270f4 Pull Request resolved: https://github.com/facebook/react/pull/30892
Mofei Zhang committed
Sep 5, 2024 at 20:14 UTC
7b98a168fdebb57b3a0b965cb0b5efa16c9cf9e0
11 files changed
+20
-929
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+16
-1
@@ -1492,6 +1492,7 @@ export type ReactiveScopeDeclaration = {
1492
scope: ReactiveScope; // the scope in which the variable was originally declared
1493
};
1494
1495
+export type DependencyPath = Array<{property: string; optional: boolean}>;
1496
export type ReactiveScopeDependency = {
1497
identifier: Identifier;
1498
path: DependencyPath;
@@ -1506,7 +1507,21 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
1507
)
1508
);
1509
}
1509
-export type DependencyPath = Array<{property: string; optional: boolean}>;
1510
+
1511
+export function getPlaceScope(
1512
+ id: InstructionId,
1513
+ place: Place,
1514
+): ReactiveScope | null {
1515
+ const scope = place.identifier.scope;
1516
+ if (scope !== null && isScopeActive(scope, id)) {
1517
+ return scope;
1518
+ }
1519
+ return null;
1520
+}
1521
+
1522
+function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
1523
+ return id >= scope.range.start && id < scope.range.end;
1524
+}
1525
1526
/*
1527
* Simulated opaque type for BlockIds to prevent using normal numbers as block ids
compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts
+1
-1
@@ -5,7 +5,7 @@ import {
5
ReactiveScope,
6
makeInstructionId,
7
} from '.';
8
-import {getPlaceScope} from '../ReactiveScopes/BuildReactiveBlocks';
8
+import {getPlaceScope} from '../HIR/HIR';
9
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
10
import DisjointSet from '../Utils/DisjointSet';
11
import {getOrInsertDefault} from '../Utils/utils';
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts
deleted
-187
@@ -1,187 +0,0 @@
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
- InstructionId,
10
- Place,
11
- ReactiveBlock,
12
- ReactiveFunction,
13
- ReactiveInstruction,
14
- ReactiveScope,
15
- ScopeId,
16
- makeInstructionId,
17
-} from '../HIR/HIR';
18
-import {getPlaceScope} from './BuildReactiveBlocks';
19
-import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
20
-
21
-/*
22
- * Note: this is the 2nd of 4 passes that determine how to break a function into discrete
23
- * reactive scopes (independently memoizeable units of code):
24
- * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
25
- * them a unique reactive scope.
26
- * 2. AlignReactiveScopesToBlockScopes (this pass, on ReactiveFunction) aligns reactive scopes
27
- * to block scopes.
28
- * 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
29
- * overlap, merging any such scopes.
30
- * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
31
- * a ReactiveScopeBlock.
32
- *
33
- * Prior inference passes assign a reactive scope to each operand, but the ranges of these
34
- * scopes are based on specific instructions at arbitrary points in the control-flow graph.
35
- * However, to codegen blocks around the instructions in each scope, the scopes must be
36
- * aligned to block-scope boundaries - we can't memoize half of a loop!
37
- *
38
- * This pass updates reactive scope boundaries to align to control flow boundaries, for
39
- * example:
40
- *
41
- * ```javascript
42
- * function foo(cond, a) {
43
- * ⌵ original scope
44
- * ⌵ expanded scope
45
- * const x = []; ⌝ ⌝
46
- * if (cond) { ⎮ ⎮
47
- * ... ⎮ ⎮
48
- * x.push(a); ⌟ ⎮
49
- * ... ⎮
50
- * } ⌟
51
- * }
52
- * ```
53
- *
54
- * Here the original scope for `x` ended partway through the if consequent, but we can't
55
- * memoize part of that block. This pass would align the scope to the end of the consequent.
56
- *
57
- * The more general rule is that a reactive scope may only end at the same block scope as it
58
- * began: this pass therefore finds, for each scope, the block where that scope started and
59
- * finds the first instruction after the scope's mutable range in that same block scope (which
60
- * will be the updated end for that scope).
61
- */
62
-
63
-export function alignReactiveScopesToBlockScopes(fn: ReactiveFunction): void {
64
- const context = new Context();
65
- visitReactiveFunction(fn, new Visitor(), context);
66
-}
67
-
68
-class Visitor extends ReactiveFunctionVisitor<Context> {
69
- override visitID(id: InstructionId, state: Context): void {
70
- state.visitId(id);
71
- }
72
- override visitPlace(id: InstructionId, place: Place, state: Context): void {
73
- const scope = getPlaceScope(id, place);
74
- if (scope !== null) {
75
- state.visitScope(scope);
76
- }
77
- }
78
- override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
79
- const scope = getPlaceScope(id, lvalue);
80
- if (scope !== null) {
81
- state.visitScope(scope);
82
- }
83
- }
84
-
85
- override visitInstruction(instr: ReactiveInstruction, state: Context): void {
86
- switch (instr.value.kind) {
87
- case 'OptionalExpression':
88
- case 'SequenceExpression':
89
- case 'ConditionalExpression':
90
- case 'LogicalExpression': {
91
- const prevScopeCount = state.currentScopes().length;
92
- this.traverseInstruction(instr, state);
93
-
94
- /**
95
- * These compound value types can have nested sequences of instructions
96
- * with scopes that start "partway" through a block-level instruction.
97
- * This would cause the start of the scope to not align with any block-level
98
- * instruction and get skipped by the later BuildReactiveBlocks pass.
99
- *
100
- * Here we detect scopes created within compound instructions and align the
101
- * start of these scopes to the outer instruction id to ensure the scopes
102
- * aren't skipped.
103
- */
104
- const scopes = state.currentScopes();
105
- for (let i = prevScopeCount; i < scopes.length; i++) {
106
- const scope = scopes[i];
107
- scope.scope.range.start = makeInstructionId(
108
- Math.min(instr.id, scope.scope.range.start),
109
- );
110
- }
111
- break;
112
- }
113
- default: {
114
- this.traverseInstruction(instr, state);
115
- }
116
- }
117
- }
118
-
119
- override visitBlock(block: ReactiveBlock, state: Context): void {
120
- state.enter(() => {
121
- this.traverseBlock(block, state);
122
- });
123
- }
124
-}
125
-
126
-type PendingReactiveScope = {active: boolean; scope: ReactiveScope};
127
-
128
-class Context {
129
- /*
130
- * For each block scope (outer array) stores a list of ReactiveScopes that start
131
- * in that block scope.
132
- */
133
- #blockScopes: Array<Array<PendingReactiveScope>> = [];
134
-
135
- /*
136
- * ReactiveScopes whose declaring block scope has ended but may still need to
137
- * be "closed" (ie have their range.end be updated). A given scope can be in
138
- * blockScopes OR this array but not both.
139
- */
140
- #unclosedScopes: Array<PendingReactiveScope> = [];
141
-
142
- /*
143
- * Set of all scope ids that have been seen so far, regardless of which of
144
- * the above data structures they're in, to avoid tracking the same scope twice.
145
- */
146
- #seenScopes: Set<ScopeId> = new Set();
147
-
148
- currentScopes(): Array<PendingReactiveScope> {
149
- return this.#blockScopes.at(-1) ?? [];
150
- }
151
-
152
- enter(fn: () => void): void {
153
- this.#blockScopes.push([]);
154
- fn();
155
- const lastScope = this.#blockScopes.pop()!;
156
- for (const scope of lastScope) {
157
- if (scope.active) {
158
- this.#unclosedScopes.push(scope);
159
- }
160
- }
161
- }
162
-
163
- visitId(id: InstructionId): void {
164
- const currentScopes = this.#blockScopes.at(-1)!;
165
- const scopes = [...currentScopes, ...this.#unclosedScopes];
166
- for (const pending of scopes) {
167
- if (!pending.active) {
168
- continue;
169
- }
170
- if (id >= pending.scope.range.end) {
171
- pending.active = false;
172
- pending.scope.range.end = id;
173
- }
174
- }
175
- }
176
-
177
- visitScope(scope: ReactiveScope): void {
178
- if (!this.#seenScopes.has(scope.id)) {
179
- const currentScopes = this.#blockScopes.at(-1)!;
180
- this.#seenScopes.add(scope.id);
181
- currentScopes.push({
182
- active: true,
183
- scope,
184
- });
185
- }
186
- }
187
-}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts
+1
-1
@@ -13,6 +13,7 @@ import {
13
MutableRange,
14
Place,
15
ReactiveScope,
16
+ getPlaceScope,
17
makeInstructionId,
18
} from '../HIR/HIR';
19
import {
@@ -23,7 +24,6 @@ import {
24
terminalFallthrough,
25
} from '../HIR/visitors';
26
import {retainWhere_Set} from '../Utils/utils';
26
-import {getPlaceScope} from './BuildReactiveBlocks';
27
28
type InstructionRange = MutableRange;
29
/*
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts
+1
-1
@@ -14,7 +14,7 @@ import {
14
ReactiveScopeBlock,
15
ScopeId,
16
} from '../HIR';
17
-import {getPlaceScope} from './BuildReactiveBlocks';
17
+import {getPlaceScope} from '../HIR/HIR';
18
import {ReactiveFunctionVisitor} from './visitors';
19
20
/*
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveBlocks.ts
deleted
-244
@@ -1,244 +0,0 @@
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 '../CompilerError';
9
-import {
10
- BlockId,
11
- InstructionId,
12
- Place,
13
- ReactiveBlock,
14
- ReactiveFunction,
15
- ReactiveInstruction,
16
- ReactiveScope,
17
- ReactiveScopeBlock,
18
- ReactiveStatement,
19
- ScopeId,
20
-} from '../HIR';
21
-import {eachInstructionLValue} from '../HIR/visitors';
22
-import {assertExhaustive} from '../Utils/utils';
23
-import {eachReactiveValueOperand, mapTerminalBlocks} from './visitors';
24
-
25
-/*
26
- * Note: this is the 4th of 4 passes that determine how to break a function into discrete
27
- * reactive scopes (independently memoizeable units of code):
28
- * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
29
- * them a unique reactive scope.
30
- * 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
31
- * to block scopes.
32
- * 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
33
- * scopes do not overlap, merging any such scopes.
34
- * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
35
- * a ReactiveScopeBlock.
36
- *
37
- * Given a function where the reactive scopes have been correctly aligned and merged,
38
- * this pass groups the instructions for each reactive scope into ReactiveBlocks.
39
- */
40
-export function buildReactiveBlocks(fn: ReactiveFunction): void {
41
- const context = new Context();
42
- fn.body = context.enter(() => {
43
- visitBlock(context, fn.body);
44
- });
45
-}
46
-
47
-class Context {
48
- #builders: Array<Builder> = [];
49
- #scopes: Set<ScopeId> = new Set();
50
-
51
- visitId(id: InstructionId): void {
52
- const builder = this.#builders.at(-1)!;
53
- builder.visitId(id);
54
- }
55
-
56
- visitScope(scope: ReactiveScope): void {
57
- if (this.#scopes.has(scope.id)) {
58
- return;
59
- }
60
- this.#scopes.add(scope.id);
61
- this.#builders.at(-1)!.startScope(scope);
62
- }
63
-
64
- append(
65
- stmt: ReactiveStatement,
66
- label: {id: BlockId; implicit: boolean} | null,
67
- ): void {
68
- this.#builders.at(-1)!.append(stmt, label);
69
- }
70
-
71
- enter(fn: () => void): ReactiveBlock {
72
- const builder = new Builder();
73
- this.#builders.push(builder);
74
- fn();
75
- const popped = this.#builders.pop();
76
- CompilerError.invariant(popped === builder, {
77
- reason: 'Expected push/pop to be called 1:1',
78
- description: null,
79
- loc: null,
80
- suggestions: null,
81
- });
82
- return builder.complete();
83
- }
84
-}
85
-
86
-class Builder {
87
- #instructions: ReactiveBlock;
88
- #stack: Array<
89
- | {kind: 'scope'; block: ReactiveScopeBlock}
90
- | {kind: 'block'; block: ReactiveBlock}
91
- >;
92
-
93
- constructor() {
94
- const block: ReactiveBlock = [];
95
- this.#instructions = block;
96
- this.#stack = [{kind: 'block', block}];
97
- }
98
-
99
- append(
100
- item: ReactiveStatement,
101
- label: {id: BlockId; implicit: boolean} | null,
102
- ): void {
103
- if (label !== null) {
104
- CompilerError.invariant(item.kind === 'terminal', {
105
- reason: 'Only terminals may have a label',
106
- description: null,
107
- loc: null,
108
- suggestions: null,
109
- });
110
- item.label = label;
111
- }
112
- this.#instructions.push(item);
113
- }
114
-
115
- startScope(scope: ReactiveScope): void {
116
- const block: ReactiveScopeBlock = {
117
- kind: 'scope',
118
- scope,
119
- instructions: [],
120
- };
121
- this.append(block, null);
122
- this.#instructions = block.instructions;
123
- this.#stack.push({kind: 'scope', block});
124
- }
125
-
126
- visitId(id: InstructionId): void {
127
- for (let i = 0; i < this.#stack.length; i++) {
128
- const entry = this.#stack[i]!;
129
- if (entry.kind === 'scope' && id >= entry.block.scope.range.end) {
130
- this.#stack.length = i;
131
- break;
132
- }
133
- }
134
- const last = this.#stack[this.#stack.length - 1]!;
135
- if (last.kind === 'block') {
136
- this.#instructions = last.block;
137
- } else {
138
- this.#instructions = last.block.instructions;
139
- }
140
- }
141
-
142
- complete(): ReactiveBlock {
143
- /*
144
- * TODO: @josephsavona debug violations of this invariant
145
- * invariant(
146
- * this.#stack.length === 1,
147
- * "Expected all scopes to be closed when exiting a block"
148
- * );
149
- */
150
- const first = this.#stack[0]!;
151
- CompilerError.invariant(first.kind === 'block', {
152
- reason: 'Expected first stack item to be a basic block',
153
- description: null,
154
- loc: null,
155
- suggestions: null,
156
- });
157
- return first.block;
158
- }
159
-}
160
-
161
-function visitBlock(context: Context, block: ReactiveBlock): void {
162
- for (const stmt of block) {
163
- switch (stmt.kind) {
164
- case 'instruction': {
165
- context.visitId(stmt.instruction.id);
166
- const scope = getInstructionScope(stmt.instruction);
167
- if (scope !== null) {
168
- context.visitScope(scope);
169
- }
170
- context.append(stmt, null);
171
- break;
172
- }
173
- case 'terminal': {
174
- const id = stmt.terminal.id;
175
- if (id !== null) {
176
- context.visitId(id);
177
- }
178
- mapTerminalBlocks(stmt.terminal, block => {
179
- return context.enter(() => {
180
- visitBlock(context, block);
181
- });
182
- });
183
- context.append(stmt, stmt.label);
184
- break;
185
- }
186
- case 'pruned-scope':
187
- case 'scope': {
188
- CompilerError.invariant(false, {
189
- reason: 'Expected the function to not have scopes already assigned',
190
- description: null,
191
- loc: null,
192
- suggestions: null,
193
- });
194
- }
195
- default: {
196
- assertExhaustive(
197
- stmt,
198
- `Unexpected statement kind \`${(stmt as any).kind}\``,
199
- );
200
- }
201
- }
202
- }
203
-}
204
-
205
-export function getInstructionScope(
206
- instr: ReactiveInstruction,
207
-): ReactiveScope | null {
208
- CompilerError.invariant(instr.lvalue !== null, {
209
- reason:
210
- 'Expected lvalues to not be null when assigning scopes. ' +
211
- 'Pruning lvalues too early can result in missing scope information.',
212
- description: null,
213
- loc: instr.loc,
214
- suggestions: null,
215
- });
216
- for (const operand of eachInstructionLValue(instr)) {
217
- const operandScope = getPlaceScope(instr.id, operand);
218
- if (operandScope !== null) {
219
- return operandScope;
220
- }
221
- }
222
- for (const operand of eachReactiveValueOperand(instr.value)) {
223
- const operandScope = getPlaceScope(instr.id, operand);
224
- if (operandScope !== null) {
225
- return operandScope;
226
- }
227
- }
228
- return null;
229
-}
230
-
231
-export function getPlaceScope(
232
- id: InstructionId,
233
- place: Place,
234
-): ReactiveScope | null {
235
- const scope = place.identifier.scope;
236
- if (scope !== null && isScopeActive(scope, id)) {
237
- return scope;
238
- }
239
- return null;
240
-}
241
-
242
-function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
243
- return id >= scope.range.start && id < scope.range.end;
244
-}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenReactiveLoops.ts
deleted
-84
@@ -1,84 +0,0 @@
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
- ReactiveFunction,
10
- ReactiveScopeBlock,
11
- ReactiveStatement,
12
- ReactiveTerminal,
13
- ReactiveTerminalStatement,
14
-} from '../HIR/HIR';
15
-import {assertExhaustive} from '../Utils/utils';
16
-import {
17
- ReactiveFunctionTransform,
18
- Transformed,
19
- visitReactiveFunction,
20
-} from './visitors';
21
-
22
-/*
23
- * Given a reactive function, flattens any scopes contained within a loop construct.
24
- * We won't initially support memoization within loops though this is possible in the future.
25
- */
26
-export function flattenReactiveLoops(fn: ReactiveFunction): void {
27
- visitReactiveFunction(fn, new Transform(), false);
28
-}
29
-
30
-class Transform extends ReactiveFunctionTransform<boolean> {
31
- override transformScope(
32
- scope: ReactiveScopeBlock,
33
- isWithinLoop: boolean,
34
- ): Transformed<ReactiveStatement> {
35
- this.visitScope(scope, isWithinLoop);
36
- if (isWithinLoop) {
37
- return {
38
- kind: 'replace',
39
- value: {
40
- kind: 'pruned-scope',
41
- scope: scope.scope,
42
- instructions: scope.instructions,
43
- },
44
- };
45
- } else {
46
- return {kind: 'keep'};
47
- }
48
- }
49
-
50
- override visitTerminal(
51
- stmt: ReactiveTerminalStatement<ReactiveTerminal>,
52
- isWithinLoop: boolean,
53
- ): void {
54
- switch (stmt.terminal.kind) {
55
- // Loop terminals flatten nested scopes
56
- case 'do-while':
57
- case 'while':
58
- case 'for':
59
- case 'for-of':
60
- case 'for-in': {
61
- this.traverseTerminal(stmt, true);
62
- break;
63
- }
64
- // Non-loop terminals passthrough is contextual, inherits the parent isWithinScope
65
- case 'try':
66
- case 'label':
67
- case 'break':
68
- case 'continue':
69
- case 'if':
70
- case 'return':
71
- case 'switch':
72
- case 'throw': {
73
- this.traverseTerminal(stmt, isWithinLoop);
74
- break;
75
- }
76
- default: {
77
- assertExhaustive(
78
- stmt.terminal,
79
- `Unexpected terminal kind \`${(stmt.terminal as any).kind}\``,
80
- );
81
- }
82
- }
83
- }
84
-}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts
deleted
-123
@@ -1,123 +0,0 @@
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
- Environment,
10
- InstructionId,
11
- ReactiveFunction,
12
- ReactiveScopeBlock,
13
- ReactiveStatement,
14
- ReactiveValue,
15
- getHookKind,
16
- isUseOperator,
17
-} from '../HIR';
18
-import {
19
- ReactiveFunctionTransform,
20
- Transformed,
21
- visitReactiveFunction,
22
-} from './visitors';
23
-
24
-/**
25
- * For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
26
- * from regular functions in two key ways:
27
- * - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
28
- * - They cannot be called conditionally
29
- *
30
- * The `use` operator is similar:
31
- * - It can access context, and therefore introduce reactivity
32
- * - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
33
- * React uses the fact that use was called to remember that the component needs the value, and that changes to the
34
- * input should invalidate the component itself.
35
- *
36
- * This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
37
- * different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
38
- * called conditionally in the output.
39
- *
40
- * The pass finds and removes any scopes that transitively contain a hook or use call. By running all
41
- * the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
42
- * describe the set of values which "construct together", and remove _all_ that memoization in order
43
- * to ensure the hook call does not inadvertently become conditional.
44
- */
45
-export function flattenScopesWithHooksOrUse(fn: ReactiveFunction): void {
46
- visitReactiveFunction(fn, new Transform(), {
47
- env: fn.env,
48
- hasHook: false,
49
- });
50
-}
51
-
52
-type State = {
53
- env: Environment;
54
- hasHook: boolean;
55
-};
56
-
57
-class Transform extends ReactiveFunctionTransform<State> {
58
- override transformScope(
59
- scope: ReactiveScopeBlock,
60
- outerState: State,
61
- ): Transformed<ReactiveStatement> {
62
- const innerState: State = {
63
- env: outerState.env,
64
- hasHook: false,
65
- };
66
- this.visitScope(scope, innerState);
67
- outerState.hasHook ||= innerState.hasHook;
68
- if (innerState.hasHook) {
69
- if (scope.instructions.length === 1) {
70
- /*
71
- * This was a scope just for a hook call, which doesn't need memoization.
72
- * flatten it away
73
- */
74
- return {
75
- kind: 'replace-many',
76
- value: scope.instructions,
77
- };
78
- }
79
- /*
80
- * else this scope had multiple instructions and produced some other value:
81
- * mark it as pruned
82
- */
83
- return {
84
- kind: 'replace',
85
- value: {
86
- kind: 'pruned-scope',
87
- scope: scope.scope,
88
- instructions: scope.instructions,
89
- },
90
- };
91
- } else {
92
- return {kind: 'keep'};
93
- }
94
- }
95
-
96
- override visitValue(
97
- id: InstructionId,
98
- value: ReactiveValue,
99
- state: State,
100
- ): void {
101
- this.traverseValue(id, value, state);
102
- switch (value.kind) {
103
- case 'CallExpression': {
104
- if (
105
- getHookKind(state.env, value.callee.identifier) != null ||
106
- isUseOperator(value.callee.identifier)
107
- ) {
108
- state.hasHook = true;
109
- }
110
- break;
111
- }
112
- case 'MethodCall': {
113
- if (
114
- getHookKind(state.env, value.property.identifier) != null ||
115
- isUseOperator(value.property.identifier)
116
- ) {
117
- state.hasHook = true;
118
- }
119
- break;
120
- }
121
- }
122
- }
123
-}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts
deleted
-281
@@ -1,281 +0,0 @@
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
- InstructionId,
10
- makeInstructionId,
11
- Place,
12
- ReactiveBlock,
13
- ReactiveFunction,
14
- ReactiveInstruction,
15
- ReactiveScope,
16
- ScopeId,
17
-} from '../HIR';
18
-import DisjointSet from '../Utils/DisjointSet';
19
-import {retainWhere} from '../Utils/utils';
20
-import {getPlaceScope} from './BuildReactiveBlocks';
21
-import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
22
-
23
-/*
24
- * Note: this is the 3rd of 4 passes that determine how to break a function into discrete
25
- * reactive scopes (independently memoizeable units of code):
26
- * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
27
- * them a unique reactive scope.
28
- * 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
29
- * to block scopes.
30
- * 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
31
- * scopes do not overlap, merging any such scopes.
32
- * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
33
- * a ReactiveScopeBlock.
34
- *
35
- * Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
36
- * the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
37
- * for performance reasons (side effects are less of a concern bc components are required to be
38
- * idempotent), so we cannot simply repeat the instruction once for each scope. Instead, the only
39
- * option is to combine the two scopes into one. This is an area where an eventual Forget IDE
40
- * could provide real-time feedback to the developer that two computations are accidentally merged.
41
- *
42
- * ## Detailed Walkthrough
43
- *
44
- * Two scopes overlap if there is one or more instruction that is inside the range
45
- * of both scopes. In general, overlapping scopes are merged togther. The only
46
- * exception to this is when one scope *shadows* another scope. For example:
47
- *
48
- * ```javascript
49
- * function foo(cond, a) {
50
- * ⌵ scope for x
51
- * let x = []; ⌝
52
- * if (cond) { ⎮
53
- * ⌵ scope for y ⎮
54
- * let y = []; ⌝ ⎮
55
- * if (b) { ⎮ ⎮
56
- * y.push(b); ⌟ ⎮
57
- * } ⎮
58
- * x.push(<div>{y}</div>); ⎮
59
- * } ⌟
60
- * }
61
- * ```
62
- *
63
- * In this example the two scopes overlap, but mutation of the two scopes is not
64
- * interleaved. Specifically within the y scope there are no instructions that
65
- * modify any other scope: the inner scope "shadows" the outer one. This category
66
- * of overlap does *NOT* merge the scopes together.
67
- *
68
- * The implementation is inspired by the Rust notion of "stacked borrows". We traverse
69
- * the control-flow graph in tree form, at each point keeping track of which scopes are
70
- * active. So initially we see
71
- *
72
- * `let x = []`
73
- * active scopes: [x]
74
- *
75
- * and mark the x scope as active.
76
- *
77
- * Then we later encounter
78
- *
79
- * `let y = [];`
80
- * active scopes: [x, y]
81
- *
82
- * Here we first check to see if 'y' is already in the list of active scopes. It isn't,
83
- * so we push it to the stop of the stack.
84
- *
85
- * Then
86
- *
87
- * `y.push(b)`
88
- * active scopes: [x, y]
89
- *
90
- * Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
91
- *
92
- * If instead we saw eg
93
- *
94
- * `x.push(b)`
95
- * active scopes: [x, y]
96
- *
97
- * Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
98
- * to be merged.
99
- */
100
-export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {
101
- const context = new Context();
102
- visitReactiveFunction(fn, new Visitor(), context);
103
- context.complete();
104
-}
105
-
106
-class Visitor extends ReactiveFunctionVisitor<Context> {
107
- override visitID(id: InstructionId, state: Context): void {
108
- state.visitId(id);
109
- }
110
- override visitPlace(id: InstructionId, place: Place, state: Context): void {
111
- state.visitPlace(id, place);
112
- }
113
- override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
114
- state.visitPlace(id, lvalue);
115
- }
116
- override visitBlock(block: ReactiveBlock, state: Context): void {
117
- state.enter(() => {
118
- this.traverseBlock(block, state);
119
- });
120
- }
121
- override visitInstruction(
122
- instruction: ReactiveInstruction,
123
- state: Context,
124
- ): void {
125
- if (
126
- instruction.value.kind === 'ConditionalExpression' ||
127
- instruction.value.kind === 'LogicalExpression' ||
128
- instruction.value.kind === 'OptionalExpression'
129
- ) {
130
- state.enter(() => {
131
- super.visitInstruction(instruction, state);
132
- });
133
- } else {
134
- super.visitInstruction(instruction, state);
135
- }
136
- }
137
-}
138
-
139
-class BlockScope {
140
- seen: Set<ScopeId> = new Set();
141
- scopes: Array<ShadowableReactiveScope> = [];
142
-}
143
-
144
-type ShadowableReactiveScope = {
145
- scope: ReactiveScope;
146
- shadowedBy: ReactiveScope | null;
147
-};
148
-
149
-class Context {
150
- scopes: Array<BlockScope> = [];
151
- seenScopes: Set<ScopeId> = new Set();
152
- joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
153
- operandScopes: Map<Place, ReactiveScope> = new Map();
154
-
155
- visitId(id: InstructionId): void {
156
- const currentBlock = this.scopes[this.scopes.length - 1]!;
157
- retainWhere(currentBlock.scopes, pending => {
158
- if (pending.scope.range.end > id) {
159
- return true;
160
- } else {
161
- currentBlock.seen.delete(pending.scope.id);
162
- return false;
163
- }
164
- });
165
- }
166
-
167
- visitPlace(id: InstructionId, place: Place): void {
168
- const scope = getPlaceScope(id, place);
169
- if (scope === null) {
170
- return;
171
- }
172
- this.operandScopes.set(place, scope);
173
- const currentBlock = this.scopes[this.scopes.length - 1]!;
174
- // Fast-path for the first time we see a new scope
175
- if (!this.seenScopes.has(scope.id)) {
176
- this.seenScopes.add(scope.id);
177
- currentBlock.seen.add(scope.id);
178
- currentBlock.scopes.push({shadowedBy: null, scope});
179
- return;
180
- }
181
- // Scope has already been seen, find it in the current block or a parent
182
- let index = this.scopes.length - 1;
183
- let nextBlock = currentBlock;
184
- while (!nextBlock.seen.has(scope.id)) {
185
- /*
186
- * scopes that cross control-flow boundaries are merged with overlapping
187
- * scopes
188
- */
189
- this.joinedScopes.union([scope, ...nextBlock.scopes.map(s => s.scope)]);
190
- index--;
191
- if (index < 0) {
192
- /*
193
- * TODO: handle reassignments in multiple branches. these create new identifiers that
194
- * add an entry to this.seenScopes but which are then removed when their blocks exit.
195
- * this is also wrong for codegen, different versions of an identifier could be cached
196
- * differently and so a reassigned version of a variable needs a separate declaration.
197
- * console.log(`scope ${scope.id} not found`);
198
- */
199
-
200
- /*
201
- * for (let i = this.scopes.length - 1; i > index; i--) {
202
- * const s = this.scopes[i];
203
- * console.log(
204
- * JSON.stringify(
205
- * {
206
- * seen: Array.from(s.seen),
207
- * scopes: s.scopes,
208
- * },
209
- * null,
210
- * 2
211
- * )
212
- * );
213
- * }
214
- */
215
- currentBlock.seen.add(scope.id);
216
- currentBlock.scopes.push({shadowedBy: null, scope});
217
- return;
218
- }
219
- nextBlock = this.scopes[index]!;
220
- }
221
-
222
- // Handle interleaving within a given block scope
223
- let found = false;
224
- for (let i = 0; i < nextBlock.scopes.length; i++) {
225
- const current = nextBlock.scopes[i]!;
226
- if (current.scope.id === scope.id) {
227
- found = true;
228
- if (current.shadowedBy !== null) {
229
- this.joinedScopes.union([current.shadowedBy, current.scope]);
230
- }
231
- } else if (found && current.shadowedBy === null) {
232
- // `scope` is shadowing `current` and may interleave
233
- current.shadowedBy = scope;
234
- if (current.scope.range.end > scope.range.end) {
235
- /*
236
- * Current is shadowed by `scope`, and we know that `current` will mutate
237
- * again (per its range), so the scopes are already known to interleave.
238
- *
239
- * Eagerly extend the ranges of the scopes so that we don't prematurely end
240
- * a scope relative to its eventual post-merge mutable range
241
- */
242
- const end = makeInstructionId(
243
- Math.max(current.scope.range.end, scope.range.end),
244
- );
245
- current.scope.range.end = end;
246
- scope.range.end = end;
247
- this.joinedScopes.union([current.scope, scope]);
248
- }
249
- }
250
- }
251
- if (!currentBlock.seen.has(scope.id)) {
252
- currentBlock.seen.add(scope.id);
253
- currentBlock.scopes.push({shadowedBy: null, scope});
254
- }
255
- }
256
-
257
- enter(fn: () => void): void {
258
- this.scopes.push(new BlockScope());
259
- fn();
260
- this.scopes.pop();
261
- }
262
-
263
- complete(): void {
264
- this.joinedScopes.forEach((scope, groupScope) => {
265
- if (scope !== groupScope) {
266
- groupScope.range.start = makeInstructionId(
267
- Math.min(groupScope.range.start, scope.range.start),
268
- );
269
- groupScope.range.end = makeInstructionId(
270
- Math.max(groupScope.range.end, scope.range.end),
271
- );
272
- }
273
- });
274
- for (const [operand, originalScope] of this.operandScopes) {
275
- const mergedScope = this.joinedScopes.find(originalScope);
276
- if (mergedScope !== null) {
277
- operand.identifier.scope = mergedScope;
278
- }
279
- }
280
- }
281
-}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
+1
-1
@@ -26,7 +26,7 @@ import {
26
} from '../HIR';
27
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
28
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
29
-import {getPlaceScope} from './BuildReactiveBlocks';
29
+import {getPlaceScope} from '../HIR/HIR';
30
import {
31
ReactiveFunctionTransform,
32
ReactiveFunctionVisitor,
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts
-5
@@ -6,18 +6,13 @@
6
*/
7
8
export {alignObjectMethodScopes} from './AlignObjectMethodScopes';
9
-export {alignReactiveScopesToBlockScopes} from './AlignReactiveScopesToBlockScopes';
9
export {assertScopeInstructionsWithinScopes} from './AssertScopeInstructionsWithinScope';
10
export {assertWellFormedBreakTargets} from './AssertWellFormedBreakTargets';
12
-export {buildReactiveBlocks} from './BuildReactiveBlocks';
11
export {buildReactiveFunction} from './BuildReactiveFunction';
12
export {codegenFunction, type CodegenFunction} from './CodegenReactiveFunction';
13
export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarationsFromDestructuring';
16
-export {flattenReactiveLoops} from './FlattenReactiveLoops';
17
-export {flattenScopesWithHooksOrUse} from './FlattenScopesWithHooksOrUse';
14
export {inferReactiveScopeVariables} from './InferReactiveScopeVariables';
15
export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
20
-export {mergeOverlappingReactiveScopes} from './MergeOverlappingReactiveScopes';
16
export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether';
17
export {printReactiveFunction} from './PrintReactiveFunction';
18
export {promoteUsedTemporaries} from './PromoteUsedTemporaries';