@samitouri / QOS-React / commits / 7b98a168fd

[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';