@samitouri / QOS-React / commits / 3d61b9b4cd

[compiler] Stay in SSA form through entire pipeline

This PR updates to use SSA form through the entire compilation pipeline. This means that in both HIR form and ReactiveFunction form, `Identifier` instances map 1:1 to `IdentifierId` values. If two identifiers have the same IdentifierId, they are the same instance. What this means is that all our passes can use this more precise information to determine if two particular identifiers are not just the same variable, but the same SSA "version" of that variable. However, some parts of our analysis really care about program variables as opposed to SSA versions, and were relying on LeaveSSA to reset identifiers such that all Identifier instances for a particular program variable would have the same IdentifierId (though not necessarily the same Identifier instance). With LeaveSSA removed, those analysis passes can now use DeclarationId instead to uniquely identify a program variable. Note that this PR surfaces some opportunties to improve edge-cases around reassigned values being declared/reassigned/depended-upon across multiple scopes. Several passes could/should use IdentifierId to more precisely identify exactly which values are accessed - for example, a scope that reassigns `x` but doesn't use `x` prior to reassignment doesn't have to take a dependency on `x`. But today we take a dependnecy. My approach for these cases was to add a "TODO LeaveSSA" comment with notes and the name of the fixture demonstrating the difference, but to intentionally preserve the existing behavior (generally, switching to use DeclarationId when IdentifierId would have been more precise). Beyond updating passes to use DeclarationId instead of Identifier/IdentifierId, the other change here is to extract out the remaining necessary bits of LeaveSSA into a new pass that rewrites InstructionKind (const/let/reassign/etc) based on whether a value is actually const or has reassignments and should be let. ghstack-source-id: 69afdaee5fadf3fdc98ce97549da805f288218b4 Pull Request resolved: https://github.com/facebook/react/pull/30573

Joe Savona committed Aug 6, 2024 at 11:24 UTC 3d61b9b4cd4135084d1e8e3b05813b915c38764d
29 files changed +1194 -674
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+19 -3
@@ -77,7 +77,11 @@ import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWit
77 import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
78 import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
79 import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
80 -import {eliminateRedundantPhi, enterSSA, leaveSSA} from '../SSA';
80 +import {
81 + eliminateRedundantPhi,
82 + enterSSA,
83 + rewriteInstructionKindsBasedOnReassignment,
84 +} from '../SSA';
85 import {inferTypes} from '../TypeInference';
86 import {
87 logCodegenFunction,
@@ -98,6 +102,7 @@ import {
102 } from '../Validation';
103 import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
104 import {outlineFunctions} from '../Optimization/OutlineFunctions';
105 +import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
106
107 export type CompilerPipelineValue =
108 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -237,8 +242,19 @@ function* runWithEnvironment(
242 inferReactivePlaces(hir);
243 yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
244
240 - leaveSSA(hir);
241 - yield log({kind: 'hir', name: 'LeaveSSA', value: hir});
245 + rewriteInstructionKindsBasedOnReassignment(hir);
246 + yield log({
247 + kind: 'hir',
248 + name: 'RewriteInstructionKindsBasedOnReassignment',
249 + value: hir,
250 + });
251 +
252 + propagatePhiTypes(hir);
253 + yield log({
254 + kind: 'hir',
255 + name: 'PropagatePhiTypes',
256 + value: hir,
257 + });
258
259 inferReactiveScopeVariables(hir);
260 yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+8 -2
@@ -1248,6 +1248,9 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
1248
1249 /**
1250 * Given an unnamed identifier, promote it to a named identifier.
1251 + *
1252 + * Note: this uses the identifier's DeclarationId to ensure that all
1253 + * instances of the same declaration will have the same name.
1254 */
1255 export function promoteTemporary(identifier: Identifier): void {
1256 CompilerError.invariant(identifier.name === null, {
@@ -1258,7 +1261,7 @@ export function promoteTemporary(identifier: Identifier): void {
1261 });
1262 identifier.name = {
1263 kind: 'promoted',
1261 - value: `#t${identifier.id}`,
1264 + value: `#t${identifier.declarationId}`,
1265 };
1266 }
1267
@@ -1269,6 +1272,9 @@ export function isPromotedTemporary(name: string): boolean {
1272 /**
1273 * Given an unnamed identifier, promote it to a named identifier, distinguishing
1274 * it as a value that needs to be capitalized since it appears in JSX element tag position
1275 + *
1276 + * Note: this uses the identifier's DeclarationId to ensure that all
1277 + * instances of the same declaration will have the same name.
1278 */
1279 export function promoteTemporaryJsxTag(identifier: Identifier): void {
1280 CompilerError.invariant(identifier.name === null, {
@@ -1279,7 +1285,7 @@ export function promoteTemporaryJsxTag(identifier: Identifier): void {
1285 });
1286 identifier.name = {
1287 kind: 'promoted',
1282 - value: `#T${identifier.id}`,
1288 + value: `#T${identifier.declarationId}`,
1289 };
1290 }
1291
compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts
+13
@@ -899,3 +899,16 @@ export function createTemporaryPlace(
899 loc: GeneratedSource,
900 };
901 }
902 +
903 +/**
904 + * Clones an existing Place, returning a new temporary Place that shares the
905 + * same metadata properties as the original place (effect, reactive flag, type)
906 + * but has a new, temporary Identifier.
907 + */
908 +export function clonePlaceToTemporary(env: Environment, place: Place): Place {
909 + const temp = createTemporaryPlace(env, place.loc);
910 + temp.effect = place.effect;
911 + temp.identifier.type = place.identifier.type;
912 + temp.reactive = place.reactive;
913 + return temp;
914 +}
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+2 -2
@@ -20,7 +20,7 @@ import {
20 } from '../HIR';
21 import {deadCodeElimination} from '../Optimization';
22 import {inferReactiveScopeVariables} from '../ReactiveScopes';
23 -import {leaveSSA} from '../SSA';
23 +import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
24 import {logHIRFunction} from '../Utils/logger';
25 import {inferMutableContextVariables} from './InferMutableContextVariables';
26 import {inferMutableRanges} from './InferMutableRanges';
@@ -108,7 +108,7 @@ function lower(func: HIRFunction): void {
108 inferReferenceEffects(func, {isFunctionExpression: true});
109 deadCodeElimination(func);
110 inferMutableRanges(func);
111 - leaveSSA(func);
111 + rewriteInstructionKindsBasedOnReassignment(func);
112 inferReactiveScopeVariables(func);
113 inferMutableContextVariables(func);
114 logHIRFunction('AnalyseFunction (inner)', func);
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+16 -11
@@ -18,6 +18,7 @@ import {Environment, EnvironmentConfig, ExternalFunction} from '../HIR';
18 import {
19 ArrayPattern,
20 BlockId,
21 + DeclarationId,
22 GeneratedSource,
23 Identifier,
24 IdentifierId,
@@ -309,9 +310,9 @@ function codegenReactiveFunction(
310 ): Result<CodegenFunction, CompilerError> {
311 for (const param of fn.params) {
312 if (param.kind === 'Identifier') {
312 - cx.temp.set(param.identifier.id, null);
313 + cx.temp.set(param.identifier.declarationId, null);
314 } else {
314 - cx.temp.set(param.place.identifier.id, null);
315 + cx.temp.set(param.place.identifier.declarationId, null);
316 }
317 }
318
@@ -392,7 +393,11 @@ class Context {
393 env: Environment;
394 fnName: string;
395 #nextCacheIndex: number = 0;
395 - #declarations: Set<IdentifierId> = new Set();
396 + /**
397 + * Tracks which named variables have been declared to dedupe declarations,
398 + * so this uses DeclarationId instead of IdentifierId
399 + */
400 + #declarations: Set<DeclarationId> = new Set();
401 temp: Temporaries;
402 errors: CompilerError = new CompilerError();
403 objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
@@ -418,11 +423,11 @@ class Context {
423 }
424
425 declare(identifier: Identifier): void {
421 - this.#declarations.add(identifier.id);
426 + this.#declarations.add(identifier.declarationId);
427 }
428
429 hasDeclared(identifier: Identifier): boolean {
425 - return this.#declarations.has(identifier.id);
430 + return this.#declarations.has(identifier.declarationId);
431 }
432
433 synthesizeName(name: string): ValidIdentifierName {
@@ -1147,7 +1152,7 @@ function codegenTerminal(
1152 let catchParam = null;
1153 if (terminal.handlerBinding !== null) {
1154 catchParam = convertIdentifier(terminal.handlerBinding.identifier);
1150 - cx.temp.set(terminal.handlerBinding.identifier.id, null);
1155 + cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
1156 }
1157 return t.tryStatement(
1158 codegenBlock(cx, terminal.block),
@@ -1205,7 +1210,7 @@ function codegenInstructionNullable(
1210 kind !== InstructionKind.Reassign &&
1211 place.identifier.name === null
1212 ) {
1208 - cx.temp.set(place.identifier.id, null);
1213 + cx.temp.set(place.identifier.declarationId, null);
1214 }
1215 const isDeclared = cx.hasDeclared(place.identifier);
1216 hasReasign ||= isDeclared;
@@ -1261,7 +1266,7 @@ function codegenInstructionNullable(
1266 );
1267 if (instr.lvalue !== null) {
1268 if (instr.value.kind !== 'StoreContext') {
1264 - cx.temp.set(instr.lvalue.identifier.id, expr);
1269 + cx.temp.set(instr.lvalue.identifier.declarationId, expr);
1270 return null;
1271 } else {
1272 // Handle chained reassignments for context variables
@@ -1530,7 +1535,7 @@ function createCallExpression(
1535 }
1536 }
1537
1533 -type Temporaries = Map<IdentifierId, t.Expression | t.JSXText | null>;
1538 +type Temporaries = Map<DeclarationId, t.Expression | t.JSXText | null>;
1539
1540 function codegenLabel(id: BlockId): string {
1541 return `bb${id}`;
@@ -1549,7 +1554,7 @@ function codegenInstruction(
1554 }
1555 if (instr.lvalue.identifier.name === null) {
1556 // temporary
1552 - cx.temp.set(instr.lvalue.identifier.id, value);
1557 + cx.temp.set(instr.lvalue.identifier.declarationId, value);
1558 return t.emptyStatement();
1559 } else {
1560 const expressionValue = convertValueToExpression(value);
@@ -2498,7 +2503,7 @@ function codegenPlaceToExpression(cx: Context, place: Place): t.Expression {
2503 }
2504
2505 function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
2501 - let tmp = cx.temp.get(place.identifier.id);
2506 + let tmp = cx.temp.get(place.identifier.declarationId);
2507 if (tmp != null) {
2508 return tmp;
2509 }
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts
+10 -12
@@ -6,6 +6,7 @@
6 */
7
8 import {
9 + DeclarationId,
10 Destructure,
11 Environment,
12 IdentifierId,
@@ -17,6 +18,7 @@ import {
18 ReactiveStatement,
19 promoteTemporary,
20 } from '../HIR';
21 +import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
22 import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
23 import {
24 ReactiveFunctionTransform,
@@ -82,7 +84,11 @@ export function extractScopeDeclarationsFromDestructuring(
84
85 class State {
86 env: Environment;
85 - declared: Set<IdentifierId> = new Set();
87 + /**
88 + * We need to track which program variables are already declared to convert
89 + * declarations into reassignments, so we use DeclarationId
90 + */
91 + declared: Set<DeclarationId> = new Set();
92
93 constructor(env: Environment) {
94 this.env = env;
@@ -92,7 +98,7 @@ class State {
98 class Visitor extends ReactiveFunctionTransform<State> {
99 override visitScope(scope: ReactiveScopeBlock, state: State): void {
100 for (const [, declaration] of scope.scope.declarations) {
95 - state.declared.add(declaration.identifier.id);
101 + state.declared.add(declaration.identifier.declarationId);
102 }
103 this.traverseScope(scope, state);
104 }
@@ -131,7 +137,7 @@ function transformDestructuring(
137 let reassigned: Set<IdentifierId> = new Set();
138 let hasDeclaration = false;
139 for (const place of eachPatternOperand(destructure.lvalue.pattern)) {
134 - const isDeclared = state.declared.has(place.identifier.id);
140 + const isDeclared = state.declared.has(place.identifier.declarationId);
141 if (isDeclared) {
142 reassigned.add(place.identifier.id);
143 }
@@ -150,15 +156,7 @@ function transformDestructuring(
156 if (!reassigned.has(place.identifier.id)) {
157 return place;
158 }
153 - const tempId = state.env.nextIdentifierId;
154 - const temporary = {
155 - ...place,
156 - identifier: {
157 - ...place.identifier,
158 - id: tempId,
159 - name: null, // overwritten below
160 - },
161 - };
159 + const temporary = clonePlaceToTemporary(state.env, place);
160 promoteTemporary(temporary.identifier);
161 renamed.set(place, temporary);
162 return temporary;
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts
+24 -3
@@ -8,6 +8,7 @@
8 import {CompilerError, SourceLocation} from '..';
9 import {Environment} from '../HIR';
10 import {
11 + DeclarationId,
12 GeneratedSource,
13 HIRFunction,
14 Identifier,
@@ -257,6 +258,14 @@ export function findDisjointMutableValues(
258 fn: HIRFunction,
259 ): DisjointSet<Identifier> {
260 const scopeIdentifiers = new DisjointSet<Identifier>();
261 +
262 + const declarations = new Map<DeclarationId, Identifier>();
263 + function declareIdentifier(lvalue: Place): void {
264 + if (!declarations.has(lvalue.identifier.declarationId)) {
265 + declarations.set(lvalue.identifier.declarationId, lvalue.identifier);
266 + }
267 + }
268 +
269 for (const [_, block] of fn.body.blocks) {
270 /*
271 * If a phi is mutated after creation, then we need to alias all of its operands such that they
@@ -264,14 +273,19 @@ export function findDisjointMutableValues(
273 */
274 for (const phi of block.phis) {
275 if (
267 - // The phi was reset because it was not mutated after creation
276 phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
277 phi.id.mutableRange.end >
278 (block.instructions.at(0)?.id ?? block.terminal.id)
279 ) {
272 - for (const [, phiId] of phi.operands) {
273 - scopeIdentifiers.union([phi.id, phiId]);
280 + const operands = [phi.id];
281 + const declaration = declarations.get(phi.id.declarationId);
282 + if (declaration !== undefined) {
283 + operands.push(declaration);
284 + }
285 + for (const [_, phiId] of phi.operands) {
286 + operands.push(phiId);
287 }
288 + scopeIdentifiers.union(operands);
289 } else if (fn.env.config.enableForest) {
290 for (const [, phiId] of phi.operands) {
291 scopeIdentifiers.union([phi.id, phiId]);
@@ -286,9 +300,15 @@ export function findDisjointMutableValues(
300 operands.push(instr.lvalue!.identifier);
301 }
302 if (
303 + instr.value.kind === 'DeclareLocal' ||
304 + instr.value.kind === 'DeclareContext'
305 + ) {
306 + declareIdentifier(instr.value.lvalue.place);
307 + } else if (
308 instr.value.kind === 'StoreLocal' ||
309 instr.value.kind === 'StoreContext'
310 ) {
311 + declareIdentifier(instr.value.lvalue.place);
312 if (
313 instr.value.lvalue.place.identifier.mutableRange.end >
314 instr.value.lvalue.place.identifier.mutableRange.start + 1
@@ -303,6 +323,7 @@ export function findDisjointMutableValues(
323 }
324 } else if (instr.value.kind === 'Destructure') {
325 for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
326 + declareIdentifier(place);
327 if (
328 place.identifier.mutableRange.end >
329 place.identifier.mutableRange.start + 1
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts
+15 -18
@@ -9,6 +9,7 @@ import {
9 HIRFunction,
10 IdentifierId,
11 makeInstructionId,
12 + MutableRange,
13 Place,
14 ReactiveValue,
15 } from '../HIR';
@@ -110,12 +111,7 @@ function visit(
111 operand.identifier.scope = fbtScope;
112
113 // Expand the jsx element's range to account for its operands
113 - fbtScope.range.start = makeInstructionId(
114 - Math.min(
115 - fbtScope.range.start,
116 - operand.identifier.mutableRange.start,
117 - ),
118 - );
114 + expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
115 fbtValues.add(operand.identifier.id);
116 }
117 } else if (
@@ -136,12 +132,7 @@ function visit(
132 operand.identifier.scope = fbtScope;
133
134 // Expand the jsx element's range to account for its operands
139 - fbtScope.range.start = makeInstructionId(
140 - Math.min(
141 - fbtScope.range.start,
142 - operand.identifier.mutableRange.start,
143 - ),
144 - );
135 + expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
136
137 /*
138 * NOTE: we add the operands as fbt values so that they are also
@@ -169,12 +160,7 @@ function visit(
160 operand.identifier.scope = fbtScope;
161
162 // Expand the jsx element's range to account for its operands
172 - fbtScope.range.start = makeInstructionId(
173 - Math.min(
174 - fbtScope.range.start,
175 - operand.identifier.mutableRange.start,
176 - ),
177 - );
163 + expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
164 }
165 }
166 }
@@ -214,3 +200,14 @@ function isFbtJsxChild(
200 fbtValues.has(lvalue.identifier.id)
201 );
202 }
203 +
204 +function expandFbtScopeRange(
205 + fbtRange: MutableRange,
206 + extendWith: MutableRange,
207 +): void {
208 + if (extendWith.start !== 0) {
209 + fbtRange.start = makeInstructionId(
210 + Math.min(fbtRange.start, extendWith.start),
211 + );
212 + }
213 +}
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts
+33 -20
@@ -7,7 +7,7 @@
7
8 import {CompilerError} from '..';
9 import {
10 - IdentifierId,
10 + DeclarationId,
11 InstructionId,
12 InstructionKind,
13 Place,
@@ -28,7 +28,7 @@ import {
28 BuiltInObjectId,
29 } from '../HIR/ObjectShape';
30 import {eachInstructionLValue} from '../HIR/visitors';
31 -import {assertExhaustive} from '../Utils/utils';
31 +import {assertExhaustive, Iterable_some} from '../Utils/utils';
32 import {printReactiveScopeSummary} from './PrintReactiveFunction';
33 import {
34 ReactiveFunctionTransform,
@@ -97,22 +97,29 @@ function log(msg: string): void {
97 }
98
99 class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
100 - lastUsage: Map<IdentifierId, InstructionId> = new Map();
100 + /*
101 + * TODO LeaveSSA: use IdentifierId for more precise tracking
102 + * Using DeclarationId is necessary for compatible output but produces suboptimal results
103 + * in cases where a scope defines a variable, but that version is never read and always
104 + * overwritten later.
105 + * see reassignment-separate-scopes.js for example
106 + */
107 + lastUsage: Map<DeclarationId, InstructionId> = new Map();
108
109 override visitPlace(id: InstructionId, place: Place, _state: void): void {
103 - const previousUsage = this.lastUsage.get(place.identifier.id);
110 + const previousUsage = this.lastUsage.get(place.identifier.declarationId);
111 const lastUsage =
112 previousUsage !== undefined
113 ? makeInstructionId(Math.max(previousUsage, id))
114 : id;
108 - this.lastUsage.set(place.identifier.id, lastUsage);
115 + this.lastUsage.set(place.identifier.declarationId, lastUsage);
116 }
117 }
118
119 class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
113 - lastUsage: Map<IdentifierId, InstructionId>;
120 + lastUsage: Map<DeclarationId, InstructionId>;
121
115 - constructor(lastUsage: Map<IdentifierId, InstructionId>) {
122 + constructor(lastUsage: Map<DeclarationId, InstructionId>) {
123 super();
124 this.lastUsage = lastUsage;
125 }
@@ -144,7 +151,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
151 block: ReactiveScopeBlock;
152 from: number;
153 to: number;
147 - lvalues: Set<IdentifierId>;
154 + lvalues: Set<DeclarationId>;
155 };
156 let current: MergedScope | null = null;
157 const merged: Array<MergedScope> = [];
@@ -204,7 +211,9 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
211 * subsequent code wo expanding the set of declarations, which we want to avoid
212 */
213 if (current !== null && instr.instruction.lvalue !== null) {
207 - current.lvalues.add(instr.instruction.lvalue.identifier.id);
214 + current.lvalues.add(
215 + instr.instruction.lvalue.identifier.declarationId,
216 + );
217 }
218 break;
219 }
@@ -224,7 +233,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
233 for (const lvalue of eachInstructionLValue(
234 instr.instruction,
235 )) {
227 - current.lvalues.add(lvalue.identifier.id);
236 + current.lvalues.add(lvalue.identifier.declarationId);
237 }
238 } else {
239 log(
@@ -383,12 +392,12 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
392 */
393 function updateScopeDeclarations(
394 scope: ReactiveScope,
386 - lastUsage: Map<IdentifierId, InstructionId>,
395 + lastUsage: Map<DeclarationId, InstructionId>,
396 ): void {
388 - for (const [key] of scope.declarations) {
389 - const lastUsedAt = lastUsage.get(key)!;
397 + for (const [id, decl] of scope.declarations) {
398 + const lastUsedAt = lastUsage.get(decl.identifier.declarationId)!;
399 if (lastUsedAt < scope.range.end) {
391 - scope.declarations.delete(key);
400 + scope.declarations.delete(id);
401 }
402 }
403 }
@@ -400,8 +409,8 @@ function updateScopeDeclarations(
409 */
410 function areLValuesLastUsedByScope(
411 scope: ReactiveScope,
403 - lvalues: Set<IdentifierId>,
404 - lastUsage: Map<IdentifierId, InstructionId>,
412 + lvalues: Set<DeclarationId>,
413 + lastUsage: Map<DeclarationId, InstructionId>,
414 ): boolean {
415 for (const lvalue of lvalues) {
416 const lastUsedAt = lastUsage.get(lvalue)!;
@@ -454,8 +463,12 @@ function canMergeScopes(
463 (next.scope.dependencies.size !== 0 &&
464 [...next.scope.dependencies].every(
465 dep =>
457 - current.scope.declarations.has(dep.identifier.id) &&
458 - isAlwaysInvalidatingType(dep.identifier.type),
466 + isAlwaysInvalidatingType(dep.identifier.type) &&
467 + Iterable_some(
468 + current.scope.declarations.values(),
469 + decl =>
470 + decl.identifier.declarationId === dep.identifier.declarationId,
471 + ),
472 ))
473 ) {
474 log(` outputs of prev are input to current`);
@@ -492,7 +505,7 @@ function areEqualDependencies(
505 let found = false;
506 for (const bValue of b) {
507 if (
495 - aValue.identifier === bValue.identifier &&
508 + aValue.identifier.declarationId === bValue.identifier.declarationId &&
509 areEqualPaths(aValue.path, bValue.path)
510 ) {
511 found = true;
@@ -506,7 +519,7 @@ function areEqualDependencies(
519 return true;
520 }
521
509 -function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
522 +export function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
523 return a.length === b.length && a.every((item, ix) => item === b[ix]);
524 }
525
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts
+81 -12
@@ -8,12 +8,13 @@
8 import {CompilerError} from '../CompilerError';
9 import {GeneratedSource} from '../HIR';
10 import {
11 + DeclarationId,
12 Identifier,
12 - IdentifierId,
13 InstructionId,
14 Place,
15 PrunedReactiveScopeBlock,
16 ReactiveFunction,
17 + ReactiveScope,
18 ReactiveScopeBlock,
19 ReactiveValue,
20 ScopeId,
@@ -24,7 +25,6 @@ import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
25
26 class Visitor extends ReactiveFunctionVisitor<State> {
27 override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
27 - this.traverseScope(scopeBlock, state);
28 for (const dep of scopeBlock.scope.dependencies) {
29 const {identifier} = dep;
30 if (identifier.name == null) {
@@ -43,21 +43,23 @@ class Visitor extends ReactiveFunctionVisitor<State> {
43 promoteIdentifier(declaration.identifier, state);
44 }
45 }
46 + this.traverseScope(scopeBlock, state);
47 }
48
49 override visitPrunedScope(
50 scopeBlock: PrunedReactiveScopeBlock,
51 state: State,
52 ): void {
52 - this.traversePrunedScope(scopeBlock, state);
53 for (const [, declaration] of scopeBlock.scope.declarations) {
54 if (
55 declaration.identifier.name == null &&
56 - state.pruned.get(declaration.identifier.id)?.usedOutsideScope === true
56 + state.pruned.get(declaration.identifier.declarationId)
57 + ?.usedOutsideScope === true
58 ) {
59 promoteIdentifier(declaration.identifier, state);
60 }
61 }
62 + this.traversePrunedScope(scopeBlock, state);
63 }
64
65 override visitParam(place: Place, state: State): void {
@@ -93,11 +95,75 @@ class Visitor extends ReactiveFunctionVisitor<State> {
95 }
96 }
97
96 -type JsxExpressionTags = Set<IdentifierId>;
98 +class Visitor2 extends ReactiveFunctionVisitor<State> {
99 + override visitPlace(_id: InstructionId, place: Place, state: State): void {
100 + if (
101 + place.identifier.name === null &&
102 + state.promoted.has(place.identifier.declarationId)
103 + ) {
104 + promoteIdentifier(place.identifier, state);
105 + }
106 + }
107 + override visitLValue(
108 + _id: InstructionId,
109 + _lvalue: Place,
110 + _state: State,
111 + ): void {
112 + this.visitPlace(_id, _lvalue, _state);
113 + }
114 + traverseScopeIdentifiers(scope: ReactiveScope, state: State): void {
115 + for (const [, decl] of scope.declarations) {
116 + if (
117 + decl.identifier.name === null &&
118 + state.promoted.has(decl.identifier.declarationId)
119 + ) {
120 + promoteIdentifier(decl.identifier, state);
121 + }
122 + }
123 + for (const dep of scope.dependencies) {
124 + if (
125 + dep.identifier.name === null &&
126 + state.promoted.has(dep.identifier.declarationId)
127 + ) {
128 + promoteIdentifier(dep.identifier, state);
129 + }
130 + }
131 + for (const reassignment of scope.reassignments) {
132 + if (
133 + reassignment.name === null &&
134 + state.promoted.has(reassignment.declarationId)
135 + ) {
136 + promoteIdentifier(reassignment, state);
137 + }
138 + }
139 + }
140 + override visitScope(scope: ReactiveScopeBlock, state: State): void {
141 + this.traverseScope(scope, state);
142 + this.traverseScopeIdentifiers(scope.scope, state);
143 + }
144 + override visitPrunedScope(
145 + scopeBlock: PrunedReactiveScopeBlock,
146 + state: State,
147 + ): void {
148 + this.traversePrunedScope(scopeBlock, state);
149 + this.traverseScopeIdentifiers(scopeBlock.scope, state);
150 + }
151 + override visitReactiveFunctionValue(
152 + _id: InstructionId,
153 + _dependencies: Array<Place>,
154 + fn: ReactiveFunction,
155 + state: State,
156 + ): void {
157 + visitReactiveFunction(fn, this, state);
158 + }
159 +}
160 +
161 +type JsxExpressionTags = Set<DeclarationId>;
162 type State = {
163 tags: JsxExpressionTags;
164 + promoted: Set<DeclarationId>;
165 pruned: Map<
100 - IdentifierId,
166 + DeclarationId,
167 {activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
168 >; // true if referenced within another scope, false if only accessed outside of scopes
169 };
@@ -108,9 +174,9 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
174 override visitPlace(_id: InstructionId, place: Place, state: State): void {
175 if (
176 this.activeScopes.length !== 0 &&
111 - state.pruned.has(place.identifier.id)
177 + state.pruned.has(place.identifier.declarationId)
178 ) {
113 - const prunedPlace = state.pruned.get(place.identifier.id)!;
179 + const prunedPlace = state.pruned.get(place.identifier.declarationId)!;
180 if (prunedPlace.activeScopes.indexOf(this.activeScopes.at(-1)!) === -1) {
181 prunedPlace.usedOutsideScope = true;
182 }
@@ -124,7 +190,7 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
190 ): void {
191 this.traverseValue(id, value, state);
192 if (value.kind === 'JsxExpression' && value.tag.kind === 'Identifier') {
127 - state.tags.add(value.tag.identifier.id);
193 + state.tags.add(value.tag.identifier.declarationId);
194 }
195 }
196
@@ -132,8 +198,8 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
198 scopeBlock: PrunedReactiveScopeBlock,
199 state: State,
200 ): void {
135 - for (const [id] of scopeBlock.scope.declarations) {
136 - state.pruned.set(id, {
201 + for (const [_id, decl] of scopeBlock.scope.declarations) {
202 + state.pruned.set(decl.identifier.declarationId, {
203 activeScopes: [...this.activeScopes],
204 usedOutsideScope: false,
205 });
@@ -151,6 +217,7 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
217 export function promoteUsedTemporaries(fn: ReactiveFunction): void {
218 const state: State = {
219 tags: new Set(),
220 + promoted: new Set(),
221 pruned: new Map(),
222 };
223 visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
@@ -161,6 +228,7 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
228 }
229 }
230 visitReactiveFunction(fn, new Visitor(), state);
231 + visitReactiveFunction(fn, new Visitor2(), state);
232 }
233
234 function promoteIdentifier(identifier: Identifier, state: State): void {
@@ -171,9 +239,10 @@ function promoteIdentifier(identifier: Identifier, state: State): void {
239 loc: GeneratedSource,
240 suggestions: null,
241 });
174 - if (state.tags.has(identifier.id)) {
242 + if (state.tags.has(identifier.declarationId)) {
243 promoteTemporaryJsxTag(identifier);
244 } else {
245 promoteTemporary(identifier);
246 }
247 + state.promoted.add(identifier.declarationId);
248 }
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts
+69 -19
@@ -8,9 +8,9 @@
8 import {CompilerError} from '../CompilerError';
9 import {
10 BlockId,
11 + DeclarationId,
12 GeneratedSource,
13 Identifier,
13 - IdentifierId,
14 InstructionId,
15 InstructionKind,
16 isObjectMethodType,
@@ -30,11 +30,12 @@ import {
30 } from '../HIR/HIR';
31 import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors';
32 import {empty, Stack} from '../Utils/Stack';
33 -import {assertExhaustive} from '../Utils/utils';
33 +import {assertExhaustive, Iterable_some} from '../Utils/utils';
34 import {
35 ReactiveScopeDependencyTree,
36 ReactiveScopePropertyDependency,
37 } from './DeriveMinimalDependencies';
38 +import {areEqualPaths} from './MergeReactiveScopesThatInvalidateTogether';
39 import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
40
41 /*
@@ -76,9 +77,9 @@ type TemporariesUsedOutsideDefiningScope = {
77 * tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad)
78 * and the scope where they are defined
79 */
79 - declarations: Map<IdentifierId, ScopeId>;
80 + declarations: Map<DeclarationId, ScopeId>;
81 // temporaries used outside of their defining scope
81 - usedOutsideDeclaringScope: Set<IdentifierId>;
82 + usedOutsideDeclaringScope: Set<DeclarationId>;
83 };
84 class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOutsideDefiningScope> {
85 scopes: Array<ScopeId> = [];
@@ -107,7 +108,10 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
108 case 'LoadLocal':
109 case 'LoadContext':
110 case 'PropertyLoad': {
110 - state.declarations.set(instruction.lvalue.identifier.id, scope);
111 + state.declarations.set(
112 + instruction.lvalue.identifier.declarationId,
113 + scope,
114 + );
115 break;
116 }
117 default: {
@@ -121,18 +125,20 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
125 place: Place,
126 state: TemporariesUsedOutsideDefiningScope,
127 ): void {
124 - const declaringScope = state.declarations.get(place.identifier.id);
128 + const declaringScope = state.declarations.get(
129 + place.identifier.declarationId,
130 + );
131 if (declaringScope === undefined) {
132 return;
133 }
134 if (this.scopes.indexOf(declaringScope) === -1) {
135 // Declaring scope is not active === used outside declaring scope
130 - state.usedOutsideDeclaringScope.add(place.identifier.id);
136 + state.usedOutsideDeclaringScope.add(place.identifier.declarationId);
137 }
138 }
139 }
140
135 -type DeclMap = Map<IdentifierId, Decl>;
141 +type DeclMap = Map<DeclarationId, Decl>;
142 type Decl = {
143 id: InstructionId;
144 scope: Stack<ScopeTraversalState>;
@@ -280,7 +286,7 @@ class PoisonState {
286 }
287
288 class Context {
283 - #temporariesUsedOutsideScope: Set<IdentifierId>;
289 + #temporariesUsedOutsideScope: Set<DeclarationId>;
290 #declarations: DeclMap = new Map();
291 #reassignments: Map<Identifier, Decl> = new Map();
292 // Reactive dependencies used in the current reactive scope.
@@ -307,7 +313,7 @@ class Context {
313 #scopes: Stack<ScopeTraversalState> = empty();
314 poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
315
310 - constructor(temporariesUsedOutsideScope: Set<IdentifierId>) {
316 + constructor(temporariesUsedOutsideScope: Set<DeclarationId>) {
317 this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
318 }
319
@@ -377,7 +383,9 @@ class Context {
383 }
384
385 isUsedOutsideDeclaringScope(place: Place): boolean {
380 - return this.#temporariesUsedOutsideScope.has(place.identifier.id);
386 + return this.#temporariesUsedOutsideScope.has(
387 + place.identifier.declarationId,
388 + );
389 }
390
391 /*
@@ -440,8 +448,8 @@ class Context {
448 * on itself.
449 */
450 declare(identifier: Identifier, decl: Decl): void {
443 - if (!this.#declarations.has(identifier.id)) {
444 - this.#declarations.set(identifier.id, decl);
451 + if (!this.#declarations.has(identifier.declarationId)) {
452 + this.#declarations.set(identifier.declarationId, decl);
453 }
454 this.#reassignments.set(identifier, decl);
455 }
@@ -533,7 +541,7 @@ class Context {
541 */
542 const currentDeclaration =
543 this.#reassignments.get(identifier) ??
536 - this.#declarations.get(identifier.id);
544 + this.#declarations.get(identifier.declarationId);
545 const currentScope = this.currentScope.value?.value;
546 return (
547 currentScope != null &&
@@ -599,14 +607,23 @@ class Context {
607 * (all other decls e.g. `let x;` should be initialized in BuildHIR)
608 */
609 const originalDeclaration = this.#declarations.get(
602 - maybeDependency.identifier.id,
610 + maybeDependency.identifier.declarationId,
611 );
612 if (
613 originalDeclaration !== undefined &&
614 originalDeclaration.scope.value !== null
615 ) {
616 originalDeclaration.scope.each(scope => {
609 - if (!this.#isScopeActive(scope.value)) {
617 + if (
618 + !this.#isScopeActive(scope.value) &&
619 + // TODO LeaveSSA: key scope.declarations by DeclarationId
620 + !Iterable_some(
621 + scope.value.declarations.values(),
622 + decl =>
623 + decl.identifier.declarationId ===
624 + maybeDependency.identifier.declarationId,
625 + )
626 + ) {
627 scope.value.declarations.set(maybeDependency.identifier.id, {
628 identifier: maybeDependency.identifier,
629 scope: originalDeclaration.scope.value!.value,
@@ -637,11 +654,14 @@ class Context {
654 const currentScope = this.currentScope.value?.value;
655 if (
656 currentScope != null &&
640 - !Array.from(currentScope.reassignments).some(
641 - identifier => identifier.id === place.identifier.id,
657 + !Iterable_some(
658 + currentScope.reassignments,
659 + identifier =>
660 + identifier.declarationId === place.identifier.declarationId,
661 ) &&
662 this.#checkValidDependency({identifier: place.identifier, path: []})
663 ) {
664 + // TODO LeaveSSA: scope.reassignments should be keyed by declarationid
665 currentScope.reassignments.add(place.identifier);
666 }
667 }
@@ -680,7 +700,37 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
700 const scopeDependencies = context.enter(scope.scope, () => {
701 this.visitBlock(scope.instructions, context);
702 });
683 - scope.scope.dependencies = scopeDependencies;
703 + for (const candidateDep of scopeDependencies) {
704 + if (
705 + !Iterable_some(
706 + scope.scope.dependencies,
707 + existingDep =>
708 + existingDep.identifier.declarationId ===
709 + candidateDep.identifier.declarationId &&
710 + areEqualPaths(existingDep.path, candidateDep.path),
711 + )
712 + ) {
713 + scope.scope.dependencies.add(candidateDep);
714 + }
715 + }
716 + /*
717 + * TODO LeaveSSA: fix existing bug with duplicate deps and reassignments
718 + * see fixture ssa-cascading-eliminated-phis, note that we cache `x`
719 + * twice because its both a dep and a reassignment.
720 + *
721 + * for (const reassignment of scope.scope.reassignments) {
722 + * if (
723 + * Iterable_some(
724 + * scope.scope.dependencies.values(),
725 + * dep =>
726 + * dep.identifier.declarationId === reassignment.declarationId &&
727 + * dep.path.length === 0,
728 + * )
729 + * ) {
730 + * scope.scope.reassignments.delete(reassignment);
731 + * }
732 + * }
733 + */
734 }
735
736 override visitPrunedScope(
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts
+4 -4
@@ -6,7 +6,7 @@
6 */
7
8 import {
9 - Identifier,
9 + DeclarationId,
10 InstructionKind,
11 ReactiveFunction,
12 ReactiveInstruction,
@@ -27,7 +27,7 @@ export function pruneHoistedContexts(fn: ReactiveFunction): void {
27 visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
28 }
29
30 -type HoistedIdentifiers = Set<Identifier>;
30 +type HoistedIdentifiers = Set<DeclarationId>;
31
32 class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
33 override transformInstruction(
@@ -39,13 +39,13 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
39 instruction.value.kind === 'DeclareContext' &&
40 instruction.value.lvalue.kind === 'HoistedConst'
41 ) {
42 - state.add(instruction.value.lvalue.place.identifier);
42 + state.add(instruction.value.lvalue.place.identifier.declarationId);
43 return {kind: 'remove'};
44 }
45
46 if (
47 instruction.value.kind === 'StoreContext' &&
48 - state.has(instruction.value.lvalue.place.identifier)
48 + state.has(instruction.value.lvalue.place.identifier.declarationId)
49 ) {
50 return {
51 kind: 'replace',
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts
+43 -28
@@ -7,8 +7,8 @@
7
8 import {CompilerError} from '../CompilerError';
9 import {
10 + DeclarationId,
11 Environment,
11 - IdentifierId,
12 InstructionId,
13 Pattern,
14 Place,
@@ -115,9 +115,9 @@ export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
115 const state = new State(fn.env);
116 for (const param of fn.params) {
117 if (param.kind === 'Identifier') {
118 - state.declare(param.identifier.id);
118 + state.declare(param.identifier.declarationId);
119 } else {
120 - state.declare(param.place.identifier.id);
120 + state.declare(param.place.identifier.declarationId);
121 }
122 }
123 visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env), state);
@@ -193,14 +193,14 @@ function joinAliases(
193 type IdentifierNode = {
194 level: MemoizationLevel;
195 memoized: boolean;
196 - dependencies: Set<IdentifierId>;
196 + dependencies: Set<DeclarationId>;
197 scopes: Set<ScopeId>;
198 seen: boolean;
199 };
200
201 // A scope node describing its dependencies
202 type ScopeNode = {
203 - dependencies: Array<IdentifierId>;
203 + dependencies: Array<DeclarationId>;
204 seen: boolean;
205 };
206
@@ -209,20 +209,30 @@ class State {
209 env: Environment;
210 /*
211 * Maps lvalues for LoadLocal to the identifier being loaded, to resolve indirections
212 - * in subsequent lvalues/rvalues
212 + * in subsequent lvalues/rvalues.
213 + *
214 + * NOTE: this pass uses DeclarationId rather than IdentifierId because the pass is not
215 + * aware of control-flow, only data flow via mutation. Instead of precisely modeling
216 + * control flow, we analyze all values that may flow into a particular program variable,
217 + * and then whether that program variable may escape (if so, the values flowing in may
218 + * escape too). Thus we use DeclarationId to captures all values that may flow into
219 + * a particular program variable, regardless of control flow paths.
220 + *
221 + * In the future when we convert to HIR everywhere this pass can account for control
222 + * flow and use SSA ids.
223 */
214 - definitions: Map<IdentifierId, IdentifierId> = new Map();
224 + definitions: Map<DeclarationId, DeclarationId> = new Map();
225
216 - identifiers: Map<IdentifierId, IdentifierNode> = new Map();
226 + identifiers: Map<DeclarationId, IdentifierNode> = new Map();
227 scopes: Map<ScopeId, ScopeNode> = new Map();
218 - escapingValues: Set<IdentifierId> = new Set();
228 + escapingValues: Set<DeclarationId> = new Set();
229
230 constructor(env: Environment) {
231 this.env = env;
232 }
233
234 // Declare a new identifier, used for function id and params
225 - declare(id: IdentifierId): void {
235 + declare(id: DeclarationId): void {
236 this.identifiers.set(id, {
237 level: MemoizationLevel.Never,
238 memoized: false,
@@ -240,14 +250,16 @@ class State {
250 visitOperand(
251 id: InstructionId,
252 place: Place,
243 - identifier: IdentifierId,
253 + identifier: DeclarationId,
254 ): void {
255 const scope = getPlaceScope(id, place);
256 if (scope !== null) {
257 let node = this.scopes.get(scope.id);
258 if (node === undefined) {
259 node = {
250 - dependencies: [...scope.dependencies].map(dep => dep.identifier.id),
260 + dependencies: [...scope.dependencies].map(
261 + dep => dep.identifier.declarationId,
262 + ),
263 seen: false,
264 };
265 this.scopes.set(scope.id, node);
@@ -269,11 +281,11 @@ class State {
281 * to determine which other values should be memoized. Returns a set of all identifiers
282 * that should be memoized.
283 */
272 -function computeMemoizedIdentifiers(state: State): Set<IdentifierId> {
273 - const memoized = new Set<IdentifierId>();
284 +function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
285 + const memoized = new Set<DeclarationId>();
286
287 // Visit an identifier, optionally forcing it to be memoized
276 - function visit(id: IdentifierId, forceMemoize: boolean = false): boolean {
288 + function visit(id: DeclarationId, forceMemoize: boolean = false): boolean {
289 const node = state.identifiers.get(id);
290 CompilerError.invariant(node !== undefined, {
291 reason: `Expected a node for all identifiers, none found for \`${id}\``,
@@ -832,14 +844,16 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
844 // Associate all the rvalues with the instruction's scope if it has one
845 for (const operand of aliasing.rvalues) {
846 const operandId =
835 - state.definitions.get(operand.identifier.id) ?? operand.identifier.id;
847 + state.definitions.get(operand.identifier.declarationId) ??
848 + operand.identifier.declarationId;
849 state.visitOperand(instruction.id, operand, operandId);
850 }
851
852 // Add the operands as dependencies of all lvalues.
853 for (const {place: lvalue, level} of aliasing.lvalues) {
854 const lvalueId =
842 - state.definitions.get(lvalue.identifier.id) ?? lvalue.identifier.id;
855 + state.definitions.get(lvalue.identifier.declarationId) ??
856 + lvalue.identifier.declarationId;
857 let node = state.identifiers.get(lvalueId);
858 if (node === undefined) {
859 node = {
@@ -858,7 +872,8 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
872 */
873 for (const operand of aliasing.rvalues) {
874 const operandId =
861 - state.definitions.get(operand.identifier.id) ?? operand.identifier.id;
875 + state.definitions.get(operand.identifier.declarationId) ??
876 + operand.identifier.declarationId;
877 if (operandId === lvalueId) {
878 continue;
879 }
@@ -870,8 +885,8 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
885
886 if (instruction.value.kind === 'LoadLocal' && instruction.lvalue !== null) {
887 state.definitions.set(
873 - instruction.lvalue.identifier.id,
874 - instruction.value.place.identifier.id,
888 + instruction.lvalue.identifier.declarationId,
889 + instruction.value.place.identifier.declarationId,
890 );
891 } else if (
892 instruction.value.kind === 'CallExpression' ||
@@ -897,7 +912,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
912 }
913 for (const operand of instruction.value.args) {
914 const place = operand.kind === 'Spread' ? operand.place : operand;
900 - state.escapingValues.add(place.identifier.id);
915 + state.escapingValues.add(place.identifier.declarationId);
916 }
917 }
918 }
@@ -910,20 +925,20 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
925 this.traverseTerminal(stmt, state);
926
927 if (stmt.terminal.kind === 'return') {
913 - state.escapingValues.add(stmt.terminal.value.identifier.id);
928 + state.escapingValues.add(stmt.terminal.value.identifier.declarationId);
929 }
930 }
931 }
932
933 // Prune reactive scopes that do not have any memoized outputs
934 class PruneScopesTransform extends ReactiveFunctionTransform<
920 - Set<IdentifierId>
935 + Set<DeclarationId>
936 > {
937 prunedScopes: Set<ScopeId> = new Set();
938
939 override transformScope(
940 scopeBlock: ReactiveScopeBlock,
926 - state: Set<IdentifierId>,
941 + state: Set<DeclarationId>,
942 ): Transformed<ReactiveStatement> {
943 this.visitScope(scopeBlock, state);
944
@@ -945,11 +960,11 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
960 }
961
962 const hasMemoizedOutput =
948 - Array.from(scopeBlock.scope.declarations.keys()).some(id =>
949 - state.has(id),
963 + Array.from(scopeBlock.scope.declarations.values()).some(decl =>
964 + state.has(decl.identifier.declarationId),
965 ) ||
966 Array.from(scopeBlock.scope.reassignments).some(identifier =>
952 - state.has(identifier.id),
967 + state.has(identifier.declarationId),
968 );
969 if (hasMemoizedOutput) {
970 return {kind: 'keep'};
@@ -964,7 +979,7 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
979
980 override transformInstruction(
981 instruction: ReactiveInstruction,
967 - state: Set<IdentifierId>,
982 + state: Set<DeclarationId>,
983 ): Transformed<ReactiveStatement> {
984 this.traverseInstruction(instruction, state);
985
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneTemporaryLValues.ts
+14 -6
@@ -6,7 +6,7 @@
6 */
7
8 import {
9 - Identifier,
9 + DeclarationId,
10 InstructionId,
11 Place,
12 ReactiveFunction,
@@ -18,19 +18,27 @@ import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
18 * Nulls out lvalues for temporary variables that are never accessed later. This only
19 * nulls out the lvalue itself, it does not remove the corresponding instructions.
20 */
21 -export function pruneTemporaryLValues(fn: ReactiveFunction): void {
22 - const lvalues = new Map<Identifier, ReactiveInstruction>();
21 +export function pruneUnusedLValues(fn: ReactiveFunction): void {
22 + const lvalues = new Map<DeclarationId, ReactiveInstruction>();
23 visitReactiveFunction(fn, new Visitor(), lvalues);
24 for (const [, instr] of lvalues) {
25 instr.lvalue = null;
26 }
27 }
28
29 -type LValues = Map<Identifier, ReactiveInstruction>;
29 +/**
30 + * This pass uses DeclarationIds because the lvalue IdentifierId of a compound expression
31 + * (ternary, logical, optional) in ReactiveFunction may not be the same as the IdentifierId
32 + * of the phi, and which is referenced later. Keying by DeclarationId ensures we don't
33 + * delete lvalues for identifiers that are used.
34 + *
35 + * TODO LeaveSSA: once we use HIR everywhere, this can likely move back to using IdentifierId
36 + */
37 +type LValues = Map<DeclarationId, ReactiveInstruction>;
38
39 class Visitor extends ReactiveFunctionVisitor<LValues> {
40 override visitPlace(id: InstructionId, place: Place, state: LValues): void {
33 - state.delete(place.identifier);
41 + state.delete(place.identifier.declarationId);
42 }
43 override visitInstruction(
44 instruction: ReactiveInstruction,
@@ -41,7 +49,7 @@ class Visitor extends ReactiveFunctionVisitor<LValues> {
49 instruction.lvalue !== null &&
50 instruction.lvalue.identifier.name === null
51 ) {
44 - state.set(instruction.lvalue.identifier, instruction);
52 + state.set(instruction.lvalue.identifier.declarationId, instruction);
53 }
54 }
55 }
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/RenameVariables.ts
+7 -7
@@ -7,8 +7,8 @@
7
8 import {CompilerError} from '../CompilerError';
9 import {
10 + DeclarationId,
11 Identifier,
11 - IdentifierId,
12 IdentifierName,
13 InstructionId,
14 Place,
@@ -121,8 +121,8 @@ class Visitor extends ReactiveFunctionVisitor<Scopes> {
121 }
122
123 class Scopes {
124 - #seen: Map<IdentifierId, IdentifierName> = new Map();
125 - #stack: Array<Map<string, IdentifierId>> = [new Map()];
124 + #seen: Map<DeclarationId, IdentifierName> = new Map();
125 + #stack: Array<Map<string, DeclarationId>> = [new Map()];
126 #globals: Set<string>;
127 names: Set<ValidIdentifierName> = new Set();
128
@@ -135,7 +135,7 @@ class Scopes {
135 if (originalName === null) {
136 return;
137 }
138 - const mappedName = this.#seen.get(identifier.id);
138 + const mappedName = this.#seen.get(identifier.declarationId);
139 if (mappedName !== undefined) {
140 identifier.name = mappedName;
141 return;
@@ -158,12 +158,12 @@ class Scopes {
158 }
159 const identifierName = makeIdentifierName(name);
160 identifier.name = identifierName;
161 - this.#seen.set(identifier.id, identifierName);
162 - this.#stack.at(-1)!.set(identifierName.value, identifier.id);
161 + this.#seen.set(identifier.declarationId, identifierName);
162 + this.#stack.at(-1)!.set(identifierName.value, identifier.declarationId);
163 this.names.add(identifierName.value);
164 }
165
166 - #lookup(name: string): IdentifierId | null {
166 + #lookup(name: string): DeclarationId | null {
167 for (let i = this.#stack.length - 1; i >= 0; i--) {
168 const scope = this.#stack[i]!;
169 const entry = scope.get(name);
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts
+1 -1
@@ -27,7 +27,7 @@ export {pruneAllReactiveScopes} from './PruneAllReactiveScopes';
27 export {pruneHoistedContexts} from './PruneHoistedContexts';
28 export {pruneNonEscapingScopes} from './PruneNonEscapingScopes';
29 export {pruneNonReactiveDependencies} from './PruneNonReactiveDependencies';
30 -export {pruneTemporaryLValues as pruneUnusedLValues} from './PruneTemporaryLValues';
30 +export {pruneUnusedLValues} from './PruneTemporaryLValues';
31 export {pruneUnusedLabels} from './PruneUnusedLabels';
32 export {pruneUnusedScopes} from './PruneUnusedScopes';
33 export {renameVariables} from './RenameVariables';
compiler/packages/babel-plugin-react-compiler/src/SSA/LeaveSSA.ts deleted
-515
@@ -1,515 +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 - BasicBlock,
11 - BlockId,
12 - HIRFunction,
13 - Identifier,
14 - InstructionKind,
15 - LValue,
16 - LValuePattern,
17 - Phi,
18 - Place,
19 -} from '../HIR/HIR';
20 -import {printIdentifier, printPlace} from '../HIR/PrintHIR';
21 -import {
22 - eachInstructionLValue,
23 - eachInstructionValueOperand,
24 - eachPatternOperand,
25 - eachTerminalOperand,
26 - eachTerminalSuccessor,
27 - terminalFallthrough,
28 -} from '../HIR/visitors';
29 -
30 -/*
31 - * Removes SSA form by converting all phis into explicit bindings and assignments. There are two main categories
32 - * of phis:
33 - *
34 - * ## Reassignments (operands are independently memoizable)
35 - *
36 - * These are phis that occur after some high-level control flow such as an if, switch, or loop. These phis are rewritten
37 - * to add a new `let` binding for the phi id prior to the control flow node (ie prior to the if/switch),
38 - * and to add a reassignment to that let binding in each of the phi's predecessors.
39 - *
40 - * Example:
41 - *
42 - * ```javascript
43 - * // Input
44 - * let x1 = null;
45 - * if (a) {
46 - * x2 = b;
47 - * } else {
48 - * x3 = c;
49 - * }
50 - * x4 = phi(x2, x3);
51 - * return x4;
52 - *
53 - * // Output
54 - * const x1 = null;
55 - * let x4; // synthesized binding for the phi identifier
56 - * if (a) {
57 - * x2 = b;
58 - * x4 = x2;; // sythesized assignment to the phi identifier
59 - * } else {
60 - * x3 = c;
61 - * x4 = x3; // synthesized assignment
62 - * }
63 - * // phi removed
64 - * return x4;
65 - * ```
66 - *
67 - * ## Rewrites (operands are not independently memoizable)
68 - *
69 - * Phis that occur inside loop constructs cannot use the reassignment strategy, because there isn't an appropriate place
70 - * to add the new let binding. Instead, we select a single "canonical" id for these phis which is the operand that is
71 - * defined first. Then, all assignments and references for any of the phi ir and operands are rewritten to reference
72 - * the canonical id instead.
73 - *
74 - * Example:
75 - *
76 - * ```javascript
77 - * // Input
78 - * for (
79 - * let i1 = 0;
80 - * { i2 = phi(i1, i2); i2 < 10 }; // note the phi in the test block
81 - * i2 += 1
82 - * ) { ... }
83 - *
84 - * // Output
85 - * for (
86 - * let i1 = 0; // i1 is defined first, so it becomes the canonical id
87 - * i1 < 10; // rewritten to canonical id
88 - * i1 += 1 // rewritten to canonical id
89 - * )
90 - * ```
91 - */
92 -export function leaveSSA(fn: HIRFunction): void {
93 - // Maps identifier names to their original declaration.
94 - const declarations: Map<
95 - string,
96 - {lvalue: LValue | LValuePattern; place: Place}
97 - > = new Map();
98 -
99 - for (const param of fn.params) {
100 - let place: Place = param.kind === 'Identifier' ? param : param.place;
101 - if (place.identifier.name !== null) {
102 - declarations.set(place.identifier.name.value, {
103 - lvalue: {
104 - kind: InstructionKind.Let,
105 - place,
106 - },
107 - place,
108 - });
109 - }
110 - }
111 -
112 - /*
113 - * For non-memoizable phis, this maps original identifiers to the identifier they should be
114 - * *rewritten* to. The keys are the original identifiers, and the value will be _either_ the
115 - * phi id or, more typically, the operand that was defined prior to the phi.
116 - */
117 - const rewrites: Map<Identifier, Identifier> = new Map();
118 -
119 - type PhiState = {
120 - phi: Phi;
121 - block: BasicBlock;
122 - };
123 -
124 - const seen = new Set<BlockId>();
125 - const backEdgePhis = new Set<Phi>();
126 - for (const [, block] of fn.body.blocks) {
127 - for (const phi of block.phis) {
128 - for (const [pred] of phi.operands) {
129 - if (!seen.has(pred)) {
130 - backEdgePhis.add(phi);
131 - break;
132 - }
133 - }
134 - }
135 - seen.add(block.id);
136 - }
137 -
138 - for (const [, block] of fn.body.blocks) {
139 - for (const instr of block.instructions) {
140 - /*
141 - * Iterate the instructions and perform any rewrites as well as promoting SSA variables to
142 - * `let` or `reassign` where possible.
143 - */
144 - const {lvalue, value} = instr;
145 - if (value.kind === 'DeclareLocal') {
146 - const name = value.lvalue.place.identifier.name;
147 - if (name !== null) {
148 - CompilerError.invariant(!declarations.has(name.value), {
149 - reason: `Unexpected duplicate declaration`,
150 - description: `Found duplicate declaration for \`${name.value}\``,
151 - loc: value.lvalue.place.loc,
152 - suggestions: null,
153 - });
154 - declarations.set(name.value, {
155 - lvalue: value.lvalue,
156 - place: value.lvalue.place,
157 - });
158 - }
159 - } else if (
160 - value.kind === 'PrefixUpdate' ||
161 - value.kind === 'PostfixUpdate'
162 - ) {
163 - CompilerError.invariant(value.lvalue.identifier.name !== null, {
164 - reason: `Expected update expression to be applied to a named variable`,
165 - description: null,
166 - loc: value.lvalue.loc,
167 - suggestions: null,
168 - });
169 - const originalLVal = declarations.get(
170 - value.lvalue.identifier.name.value,
171 - );
172 - CompilerError.invariant(originalLVal !== undefined, {
173 - reason: `Expected update expression to be applied to a previously defined variable`,
174 - description: null,
175 - loc: value.lvalue.loc,
176 - suggestions: null,
177 - });
178 - originalLVal.lvalue.kind = InstructionKind.Let;
179 - } else if (value.kind === 'StoreLocal') {
180 - if (value.lvalue.place.identifier.name != null) {
181 - const originalLVal = declarations.get(
182 - value.lvalue.place.identifier.name.value,
183 - );
184 - if (
185 - originalLVal === undefined ||
186 - originalLVal.lvalue === value.lvalue // in case this was pre-declared for the `for` initializer
187 - ) {
188 - CompilerError.invariant(
189 - originalLVal !== undefined ||
190 - block.kind === 'block' ||
191 - block.kind === 'catch',
192 - {
193 - reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
194 - description: null,
195 - loc: value.lvalue.place.loc,
196 - suggestions: null,
197 - },
198 - );
199 - declarations.set(value.lvalue.place.identifier.name.value, {
200 - lvalue: value.lvalue,
201 - place: value.lvalue.place,
202 - });
203 - value.lvalue.kind = InstructionKind.Const;
204 - } else {
205 - /*
206 - * This is an instance of the original id, so we need to promote the original declaration
207 - * to a `let` and the current lval to a `reassign`
208 - */
209 - originalLVal.lvalue.kind = InstructionKind.Let;
210 - value.lvalue.kind = InstructionKind.Reassign;
211 - }
212 - } else if (rewrites.has(value.lvalue.place.identifier)) {
213 - value.lvalue.kind = InstructionKind.Const;
214 - }
215 - } else if (value.kind === 'Destructure') {
216 - let kind: InstructionKind | null = null;
217 - for (const place of eachPatternOperand(value.lvalue.pattern)) {
218 - if (place.identifier.name == null) {
219 - CompilerError.invariant(
220 - kind === null || kind === InstructionKind.Const,
221 - {
222 - reason: `Expected consistent kind for destructuring`,
223 - description: `other places were \`${kind}\` but '${printPlace(
224 - place,
225 - )}' is const`,
226 - loc: place.loc,
227 - suggestions: null,
228 - },
229 - );
230 - kind = InstructionKind.Const;
231 - } else {
232 - const originalLVal = declarations.get(place.identifier.name.value);
233 - if (
234 - originalLVal === undefined ||
235 - originalLVal.lvalue === value.lvalue
236 - ) {
237 - CompilerError.invariant(
238 - originalLVal !== undefined || block.kind !== 'value',
239 - {
240 - reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
241 - description: null,
242 - loc: place.loc,
243 - suggestions: null,
244 - },
245 - );
246 - declarations.set(place.identifier.name.value, {
247 - lvalue: value.lvalue,
248 - place,
249 - });
250 - CompilerError.invariant(
251 - kind === null || kind === InstructionKind.Const,
252 - {
253 - reason: `Expected consistent kind for destructuring`,
254 - description: `Other places were \`${kind}\` but '${printPlace(
255 - place,
256 - )}' is const`,
257 - loc: place.loc,
258 - suggestions: null,
259 - },
260 - );
261 - kind = InstructionKind.Const;
262 - } else {
263 - CompilerError.invariant(
264 - kind === null || kind === InstructionKind.Reassign,
265 - {
266 - reason: `Expected consistent kind for destructuring`,
267 - description: `Other places were \`${kind}\` but '${printPlace(
268 - place,
269 - )}' is reassigned`,
270 - loc: place.loc,
271 - suggestions: null,
272 - },
273 - );
274 - kind = InstructionKind.Reassign;
275 - originalLVal.lvalue.kind = InstructionKind.Let;
276 - }
277 - }
278 - }
279 - CompilerError.invariant(kind !== null, {
280 - reason: 'Expected at least one operand',
281 - description: null,
282 - loc: null,
283 - suggestions: null,
284 - });
285 - value.lvalue.kind = kind;
286 - }
287 - rewritePlace(lvalue, rewrites, declarations);
288 - for (const operand of eachInstructionLValue(instr)) {
289 - rewritePlace(operand, rewrites, declarations);
290 - }
291 - for (const operand of eachInstructionValueOperand(instr.value)) {
292 - rewritePlace(operand, rewrites, declarations);
293 - }
294 - }
295 -
296 - const terminal = block.terminal;
297 - for (const operand of eachTerminalOperand(terminal)) {
298 - rewritePlace(operand, rewrites, declarations);
299 - }
300 -
301 - /*
302 - * Find any phi nodes which need a variable declaration in the current block
303 - * This includes phis in fallthrough nodes, or blocks that form part of control flow
304 - * such as for or while (and later if/switch).
305 - */
306 - const reassignmentPhis: Array<PhiState> = [];
307 - const rewritePhis: Array<PhiState> = [];
308 - function pushPhis(phiBlock: BasicBlock): void {
309 - for (const phi of phiBlock.phis) {
310 - if (phi.id.name === null) {
311 - rewritePhis.push({phi, block: phiBlock});
312 - } else {
313 - reassignmentPhis.push({phi, block: phiBlock});
314 - }
315 - }
316 - }
317 - const fallthroughId = terminalFallthrough(terminal);
318 - if (fallthroughId !== null) {
319 - const fallthrough = fn.body.blocks.get(fallthroughId)!;
320 - pushPhis(fallthrough);
321 - }
322 - if (terminal.kind === 'while' || terminal.kind === 'for') {
323 - const test = fn.body.blocks.get(terminal.test)!;
324 - pushPhis(test);
325 -
326 - const loop = fn.body.blocks.get(terminal.loop)!;
327 - pushPhis(loop);
328 - }
329 - if (
330 - terminal.kind === 'for' ||
331 - terminal.kind === 'for-of' ||
332 - terminal.kind === 'for-in'
333 - ) {
334 - let init = fn.body.blocks.get(terminal.init)!;
335 - pushPhis(init);
336 -
337 - // The first block after the end of the init
338 - let initContinuation =
339 - terminal.kind === 'for' ? terminal.test : terminal.loop;
340 - /*
341 - * To avoid generating a let binding for the initializer prior to the loop,
342 - * check to see if the for declares an iterator variable.
343 - */
344 - const queue: Array<BlockId> = [init.id];
345 - while (queue.length !== 0) {
346 - const blockId = queue.shift()!;
347 - if (blockId === initContinuation) {
348 - break;
349 - }
350 - const block = fn.body.blocks.get(blockId)!;
351 - for (const instr of block.instructions) {
352 - if (
353 - instr.value.kind === 'StoreLocal' &&
354 - instr.value.lvalue.kind !== InstructionKind.Reassign
355 - ) {
356 - const value = instr.value;
357 - if (value.lvalue.place.identifier.name !== null) {
358 - const originalLVal = declarations.get(
359 - value.lvalue.place.identifier.name.value,
360 - );
361 - if (originalLVal === undefined) {
362 - declarations.set(value.lvalue.place.identifier.name.value, {
363 - lvalue: value.lvalue,
364 - place: value.lvalue.place,
365 - });
366 - value.lvalue.kind = InstructionKind.Const;
367 - }
368 - }
369 - }
370 - }
371 -
372 - switch (block.terminal.kind) {
373 - case 'maybe-throw': {
374 - queue.push(block.terminal.continuation);
375 - break;
376 - }
377 - case 'goto': {
378 - queue.push(block.terminal.block);
379 - break;
380 - }
381 - case 'branch':
382 - case 'logical':
383 - case 'optional':
384 - case 'ternary':
385 - case 'label': {
386 - for (const successor of eachTerminalSuccessor(block.terminal)) {
387 - queue.push(successor);
388 - }
389 - break;
390 - }
391 - default: {
392 - break;
393 - }
394 - }
395 - }
396 -
397 - if (terminal.kind === 'for' && terminal.update !== null) {
398 - const update = fn.body.blocks.get(terminal.update)!;
399 - pushPhis(update);
400 - }
401 - }
402 -
403 - for (const {phi, block: phiBlock} of reassignmentPhis) {
404 - /*
405 - * In some cases one of the phi operands can be defined *before* the let binding
406 - * we will generate. For example, a variable that is only rebound in one branch of
407 - * an if but not another. In this case we populate the let binding with this initial
408 - * value rather than generate an extra assignment.
409 - */
410 - let initOperand: Identifier | null = null;
411 - for (const [, operand] of phi.operands) {
412 - if (operand.mutableRange.start < terminal.id) {
413 - if (initOperand == null) {
414 - initOperand = operand;
415 - }
416 - }
417 - }
418 -
419 - /*
420 - * If the phi is mutated after its creation, then any values which flow into the phi
421 - * must also have their ranges extended accordingly.
422 - */
423 - const isPhiMutatedAfterCreation: boolean =
424 - phi.id.mutableRange.end >
425 - (phiBlock.instructions.at(0)?.id ?? phiBlock.terminal.id);
426 -
427 - /*
428 - * If we never saw a declaration for this phi, it may have been pruned by DCE, so synthesize
429 - * a new Let binding
430 - */
431 - CompilerError.invariant(phi.id.name != null, {
432 - reason: 'Expected reassignment phis to have a name',
433 - description: null,
434 - loc: null,
435 - suggestions: null,
436 - });
437 - const declaration = declarations.get(phi.id.name.value);
438 - CompilerError.invariant(declaration != null, {
439 - loc: null,
440 - reason: 'Expected a declaration for all variables',
441 - description: `${printIdentifier(phi.id)} in block bb${phiBlock.id}`,
442 - suggestions: null,
443 - });
444 - if (isPhiMutatedAfterCreation) {
445 - /*
446 - * The declaration is not guaranteed to flow into the phi, for example in the case of a variable
447 - * that is reassigned in all control flow paths to a given phi. The original declaration's range
448 - * has to be extended in this case (if the phi is later mutated) since we are reusing the original
449 - * declaration instead of creating a new declaration.
450 - *
451 - * NOTE: this can *only* happen if the original declaration involves an instruction that DCE does
452 - * not prune. Otherwise, the declaration would have been pruned and we'd synthesize a new one.
453 - */
454 - declaration.place.identifier.mutableRange.end = phi.id.mutableRange.end;
455 - }
456 - rewrites.set(phi.id, declaration.place.identifier);
457 - }
458 -
459 - /*
460 - * Similar logic for rewrite phis that occur in loops, except that instead of a new let binding
461 - * we pick one of the operands as the canonical id, and rewrite all references to the other
462 - * operands and the phi to reference this canonical id.
463 - */
464 - for (const {phi} of rewritePhis) {
465 - let canonicalId = rewrites.get(phi.id);
466 - if (canonicalId === undefined) {
467 - canonicalId = phi.id;
468 - for (const [, operand] of phi.operands) {
469 - let canonicalOperand = rewrites.get(operand) ?? operand;
470 - if (canonicalOperand.id < canonicalId.id) {
471 - canonicalId = canonicalOperand;
472 - }
473 - }
474 - rewrites.set(phi.id, canonicalId);
475 -
476 - if (canonicalId.name !== null) {
477 - const declaration = declarations.get(canonicalId.name.value);
478 - if (declaration !== undefined) {
479 - declaration.lvalue.kind = InstructionKind.Let;
480 - }
481 - }
482 - }
483 -
484 - // all versions of the variable need to be remapped to the canonical id
485 - for (const [, operand] of phi.operands) {
486 - rewrites.set(operand, canonicalId);
487 - }
488 - }
489 - }
490 -}
491 -
492 -/*
493 - * Rewrite @param place's identifier based on the given rewrite mapping, if the identifier
494 - * is present. Also expands the mutable range of the target identifier to include the
495 - * place's range.
496 - */
497 -function rewritePlace(
498 - place: Place,
499 - rewrites: Map<Identifier, Identifier>,
500 - declarations: Map<string, {lvalue: LValue | LValuePattern; place: Place}>,
501 -): void {
502 - const prevIdentifier = place.identifier;
503 - const nextIdentifier = rewrites.get(prevIdentifier);
504 -
505 - if (nextIdentifier !== undefined) {
506 - if (nextIdentifier === prevIdentifier) return;
507 - place.identifier = nextIdentifier;
508 - } else if (prevIdentifier.name != null) {
509 - const declaration = declarations.get(prevIdentifier.name.value);
510 - // Only rewrite identifiers that were declared within the function
511 - if (declaration === undefined) return;
512 - const originalIdentifier = declaration.place.identifier;
513 - prevIdentifier.id = originalIdentifier.id;
514 - }
515 -}
compiler/packages/babel-plugin-react-compiler/src/SSA/RewriteInstructionKindsBasedOnReassignment.ts new
+174
@@ -0,0 +1,174 @@
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 + DeclarationId,
11 + HIRFunction,
12 + InstructionKind,
13 + LValue,
14 + LValuePattern,
15 + Place,
16 +} from '../HIR/HIR';
17 +import {printPlace} from '../HIR/PrintHIR';
18 +import {eachPatternOperand} from '../HIR/visitors';
19 +
20 +/**
21 + * This pass rewrites the InstructionKind of instructions which declare/assign variables,
22 + * converting the first declaration to a Const/Let depending on whether it is subsequently
23 + * reassigned, and ensuring that subsequent reassignments are marked as a Reassign. Note
24 + * that declarations which were const in the original program cannot become `let`, but the
25 + * inverse is not true: a `let` which was reassigned in the source may be converted to a
26 + * `const` if the reassignment is not used and was removed by dead code elimination.
27 + *
28 + * NOTE: this is a subset of the operations previously performed by the LeaveSSA pass.
29 + */
30 +export function rewriteInstructionKindsBasedOnReassignment(
31 + fn: HIRFunction,
32 +): void {
33 + const declarations = new Map<DeclarationId, LValue | LValuePattern>();
34 + for (const param of fn.params) {
35 + let place: Place = param.kind === 'Identifier' ? param : param.place;
36 + if (place.identifier.name !== null) {
37 + declarations.set(place.identifier.declarationId, {
38 + kind: InstructionKind.Let,
39 + place,
40 + });
41 + }
42 + }
43 + for (const place of fn.context) {
44 + if (place.identifier.name !== null) {
45 + declarations.set(place.identifier.declarationId, {
46 + kind: InstructionKind.Let,
47 + place,
48 + });
49 + }
50 + }
51 + for (const [, block] of fn.body.blocks) {
52 + for (const instr of block.instructions) {
53 + const {value} = instr;
54 + switch (value.kind) {
55 + case 'DeclareLocal': {
56 + const lvalue = value.lvalue;
57 + CompilerError.invariant(
58 + !declarations.has(lvalue.place.identifier.declarationId),
59 + {
60 + reason: `Expected variable not to be defined prior to declaration`,
61 + description: `${printPlace(lvalue.place)} was already defined`,
62 + loc: lvalue.place.loc,
63 + },
64 + );
65 + declarations.set(lvalue.place.identifier.declarationId, lvalue);
66 + break;
67 + }
68 + case 'StoreLocal': {
69 + const lvalue = value.lvalue;
70 + if (lvalue.place.identifier.name !== null) {
71 + const declaration = declarations.get(
72 + lvalue.place.identifier.declarationId,
73 + );
74 + if (declaration === undefined) {
75 + CompilerError.invariant(
76 + !declarations.has(lvalue.place.identifier.declarationId),
77 + {
78 + reason: `Expected variable not to be defined prior to declaration`,
79 + description: `${printPlace(lvalue.place)} was already defined`,
80 + loc: lvalue.place.loc,
81 + },
82 + );
83 + declarations.set(lvalue.place.identifier.declarationId, lvalue);
84 + lvalue.kind = InstructionKind.Const;
85 + } else {
86 + declaration.kind = InstructionKind.Let;
87 + lvalue.kind = InstructionKind.Reassign;
88 + }
89 + }
90 + break;
91 + }
92 + case 'Destructure': {
93 + const lvalue = value.lvalue;
94 + let kind: InstructionKind | null = null;
95 + for (const place of eachPatternOperand(lvalue.pattern)) {
96 + if (place.identifier.name === null) {
97 + CompilerError.invariant(
98 + kind === null || kind === InstructionKind.Const,
99 + {
100 + reason: `Expected consistent kind for destructuring`,
101 + description: `other places were \`${kind}\` but '${printPlace(
102 + place,
103 + )}' is const`,
104 + loc: place.loc,
105 + suggestions: null,
106 + },
107 + );
108 + kind = InstructionKind.Const;
109 + } else {
110 + const declaration = declarations.get(
111 + place.identifier.declarationId,
112 + );
113 + if (declaration === undefined) {
114 + CompilerError.invariant(block.kind !== 'value', {
115 + reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
116 + description: null,
117 + loc: place.loc,
118 + suggestions: null,
119 + });
120 + declarations.set(place.identifier.declarationId, lvalue);
121 + CompilerError.invariant(
122 + kind === null || kind === InstructionKind.Const,
123 + {
124 + reason: `Expected consistent kind for destructuring`,
125 + description: `Other places were \`${kind}\` but '${printPlace(
126 + place,
127 + )}' is const`,
128 + loc: place.loc,
129 + suggestions: null,
130 + },
131 + );
132 + kind = InstructionKind.Const;
133 + } else {
134 + CompilerError.invariant(
135 + kind === null || kind === InstructionKind.Reassign,
136 + {
137 + reason: `Expected consistent kind for destructuring`,
138 + description: `Other places were \`${kind}\` but '${printPlace(
139 + place,
140 + )}' is reassigned`,
141 + loc: place.loc,
142 + suggestions: null,
143 + },
144 + );
145 + kind = InstructionKind.Reassign;
146 + declaration.kind = InstructionKind.Let;
147 + }
148 + }
149 + }
150 + CompilerError.invariant(kind !== null, {
151 + reason: 'Expected at least one operand',
152 + description: null,
153 + loc: null,
154 + suggestions: null,
155 + });
156 + lvalue.kind = kind;
157 + break;
158 + }
159 + case 'PostfixUpdate':
160 + case 'PrefixUpdate': {
161 + const lvalue = value.lvalue;
162 + const declaration = declarations.get(lvalue.identifier.declarationId);
163 + CompilerError.invariant(declaration !== undefined, {
164 + reason: `Expected variable to have been defined`,
165 + description: `No declaration for ${printPlace(lvalue)}`,
166 + loc: lvalue.loc,
167 + });
168 + declaration.kind = InstructionKind.Let;
169 + break;
170 + }
171 + }
172 + }
173 + }
174 +}
compiler/packages/babel-plugin-react-compiler/src/SSA/index.ts
+1 -1
@@ -7,4 +7,4 @@
7
8 export {eliminateRedundantPhi} from './EliminateRedundantPhi';
9 export {default as enterSSA} from './EnterSSA';
10 -export {leaveSSA} from './LeaveSSA';
10 +export {rewriteInstructionKindsBasedOnReassignment} from './RewriteInstructionKindsBasedOnReassignment';
compiler/packages/babel-plugin-react-compiler/src/TypeInference/PropagatePhiTypes.ts new
+108
@@ -0,0 +1,108 @@
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 {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
9 +
10 +/**
11 + * Temporary workaround for InferTypes not propagating the types of phis.
12 + * Previously, LeaveSSA would replace all the identifiers for each phi (operands and
13 + * the phi itself) with a single "canonical" identifier, generally chosen as the first
14 + * operand to flow into the phi. In case of a phi whose operand was a phi, this could
15 + * sometimes be an operand from the earlier phi.
16 + *
17 + * As a result, even though InferTypes did not propagate types for phis, LeaveSSA
18 + * could end up replacing the phi Identifier with another identifer from an operand,
19 + * which _did_ have a type inferred.
20 + *
21 + * This didn't affect the initial construction of mutable ranges because InferMutableRanges
22 + * runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
23 + * notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
24 + * will always invalidate with its input.
25 + *
26 + * The long-term correct approach is to update InferTypes to infer the types of phis,
27 + * but this is complicated because InferMutableRanges inadvertently depends on phis
28 + * never having a known type, such that a Store effect cannot occur on a phi value.
29 + * Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
30 + * to handle this case.
31 + *
32 + * As a temporary workaround, this pass propagates the type of phis and can be called
33 + * safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
34 + * type if all operands have the same type, it's its more correct.
35 + */
36 +export function propagatePhiTypes(fn: HIRFunction): void {
37 + /**
38 + * We track which SSA ids have had their types propagated to handle nested ternaries,
39 + * see the StoreLocal handling below
40 + */
41 + const propagated = new Set<IdentifierId>();
42 + for (const [, block] of fn.body.blocks) {
43 + for (const phi of block.phis) {
44 + /*
45 + * We replicate the previous LeaveSSA behavior and only propagate types for
46 + * unnamed variables. LeaveSSA would have chosen one of the operands as the
47 + * canonical id and taken its type as the type of all identifiers. We're
48 + * more conservative and only propagate if the types are the same and the
49 + * phi didn't have a type inferred.
50 + *
51 + * Note that this can change output slightly in cases such as
52 + * `cond ? <div /> : null`.
53 + *
54 + * Previously the first operand's type (BuiltInJsx) would have been propagated,
55 + * and this expression may have been merged with subsequent reactive scopes
56 + * since it appears (based on that type) to always invalidate.
57 + *
58 + * But the correct type is `BuiltInJsx | null`, which we can't express and
59 + * so leave as a generic `Type`, which does not always invalidate and therefore
60 + * does not merge with subsequent scopes.
61 + *
62 + * We also don't propagate scopes for named variables, to preserve compatibility
63 + * with previous LeaveSSA behavior.
64 + */
65 + if (phi.id.type.kind !== 'Type' || phi.id.name !== null) {
66 + continue;
67 + }
68 + let type: Type | null = null;
69 + for (const [, operand] of phi.operands) {
70 + if (type === null) {
71 + type = operand.type;
72 + } else if (!typeEquals(type, operand.type)) {
73 + type = null;
74 + break;
75 + }
76 + }
77 + if (type !== null) {
78 + phi.id.type = type;
79 + phi.type = type;
80 + propagated.add(phi.id.id);
81 + }
82 + }
83 + for (const instr of block.instructions) {
84 + const {value} = instr;
85 + switch (value.kind) {
86 + case 'StoreLocal': {
87 + /**
88 + * Nested ternaries can lower to a form with an intermediate StoreLocal where
89 + * the value.lvalue is the temporary of the outer ternary, and the value.value
90 + * is the result of the inner ternary.
91 + *
92 + * This is a common pattern in practice and easy enough to support. Again, the
93 + * long-term approach is to update InferTypes and InferMutableRanges.
94 + */
95 + const lvalue = value.lvalue.place;
96 + if (
97 + propagated.has(value.value.identifier.id) &&
98 + lvalue.identifier.type.kind === 'Type' &&
99 + lvalue.identifier.name === null
100 + ) {
101 + lvalue.identifier.type = value.value.identifier.type;
102 + propagated.add(lvalue.identifier.id);
103 + }
104 + }
105 + }
106 + }
107 + }
108 +}
compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
+12
@@ -93,6 +93,18 @@ export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
93 return union;
94 }
95
96 +export function Iterable_some<T>(
97 + iter: Iterable<T>,
98 + pred: (item: T) => boolean,
99 +): boolean {
100 + for (const item of iter) {
101 + if (pred(item)) {
102 + return true;
103 + }
104 + }
105 + return false;
106 +}
107 +
108 export function nonNull<T extends NonNullable<U>, U>(
109 value: T | null | undefined,
110 ): value is T {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts
+12 -10
@@ -7,6 +7,7 @@
7
8 import {CompilerError, Effect, ErrorSeverity} from '..';
9 import {
10 + DeclarationId,
11 GeneratedSource,
12 Identifier,
13 IdentifierId,
@@ -82,7 +83,7 @@ type ManualMemoBlockState = {
83 * } else { ... }
84 * ```
85 */
85 - decls: Set<IdentifierId>;
86 + decls: Set<DeclarationId>;
87
88 /*
89 * normalized depslist from useMemo/useCallback
@@ -204,7 +205,7 @@ function compareDeps(
205 function validateInferredDep(
206 dep: ReactiveScopeDependency,
207 temporaries: Map<IdentifierId, ManualMemoDependency>,
207 - declsWithinMemoBlock: Set<IdentifierId>,
208 + declsWithinMemoBlock: Set<DeclarationId>,
209 validDepsInMemoBlock: Array<ManualMemoDependency>,
210 errorState: CompilerError,
211 memoLocation: SourceLocation,
@@ -240,7 +241,7 @@ function validateInferredDep(
241 for (const decl of declsWithinMemoBlock) {
242 if (
243 normalizedDep.root.kind === 'NamedLocal' &&
243 - decl === normalizedDep.root.value.identifier.id
244 + decl === normalizedDep.root.value.identifier.declarationId
245 ) {
246 return;
247 }
@@ -323,7 +324,9 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
324 const dep = collectMaybeMemoDependencies(value, this.temporaries);
325 if (value.kind === 'StoreLocal' || value.kind === 'StoreContext') {
326 const storeTarget = value.lvalue.place;
326 - state.manualMemoState?.decls.add(storeTarget.identifier.id);
327 + state.manualMemoState?.decls.add(
328 + storeTarget.identifier.declarationId,
329 + );
330 if (storeTarget.identifier.name?.kind === 'named' && dep == null) {
331 const dep: ManualMemoDependency = {
332 root: {
@@ -343,15 +346,14 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
346
347 recordTemporaries(instr: ReactiveInstruction, state: VisitorState): void {
348 const temporaries = this.temporaries;
346 - const {value} = instr;
347 - const lvalId = instr.lvalue?.identifier.id;
349 + const {lvalue, value} = instr;
350 + const lvalId = lvalue?.identifier.id;
351 if (lvalId != null && temporaries.has(lvalId)) {
352 return;
353 }
351 - const isNamedLocal =
352 - lvalId != null && instr.lvalue?.identifier.name?.kind === 'named';
353 - if (isNamedLocal && state.manualMemoState != null) {
354 - state.manualMemoState.decls.add(lvalId);
354 + const isNamedLocal = lvalue?.identifier.name?.kind === 'named';
355 + if (lvalue !== null && isNamedLocal && state.manualMemoState != null) {
356 + state.manualMemoState.decls.add(lvalue.identifier.declarationId);
357 }
358
359 const maybeDep = this.recordDepsInValue(value, state);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.expect.md new
+90
@@ -0,0 +1,90 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {fbt} from 'fbt';
6 +import {useMemo} from 'react';
7 +import {ValidateMemoization} from 'shared-runtime';
8 +
9 +function Component({data}) {
10 + const el = useMemo(
11 + () => (
12 + <fbt desc="user name">
13 + <fbt:param name="name">{data.name ?? ''}</fbt:param>
14 + </fbt>
15 + ),
16 + [data.name]
17 + );
18 + return <ValidateMemoization inputs={[data.name]} output={el} />;
19 +}
20 +
21 +const props1 = {data: {name: 'Mike'}};
22 +const props2 = {data: {name: 'Mofei'}};
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [props1],
26 + sequentialRenders: [props1, props2, props2, props1, {...props1}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +import { fbt } from "fbt";
36 +import { useMemo } from "react";
37 +import { ValidateMemoization } from "shared-runtime";
38 +
39 +function Component(t0) {
40 + const $ = _c(7);
41 + const { data } = t0;
42 + let t1;
43 + let t2;
44 + if ($[0] !== data.name) {
45 + t2 = fbt._("{name}", [fbt._param("name", data.name ?? "")], {
46 + hk: "csQUH",
47 + });
48 + $[0] = data.name;
49 + $[1] = t2;
50 + } else {
51 + t2 = $[1];
52 + }
53 + t1 = t2;
54 + const el = t1;
55 + let t3;
56 + if ($[2] !== data.name) {
57 + t3 = [data.name];
58 + $[2] = data.name;
59 + $[3] = t3;
60 + } else {
61 + t3 = $[3];
62 + }
63 + let t4;
64 + if ($[4] !== t3 || $[5] !== el) {
65 + t4 = <ValidateMemoization inputs={t3} output={el} />;
66 + $[4] = t3;
67 + $[5] = el;
68 + $[6] = t4;
69 + } else {
70 + t4 = $[6];
71 + }
72 + return t4;
73 +}
74 +
75 +const props1 = { data: { name: "Mike" } };
76 +const props2 = { data: { name: "Mofei" } };
77 +export const FIXTURE_ENTRYPOINT = {
78 + fn: Component,
79 + params: [props1],
80 + sequentialRenders: [props1, props2, props2, props1, { ...props1 }],
81 +};
82 +
83 +```
84 +
85 +### Eval output
86 +(kind: ok) <div>{"inputs":["Mike"],"output":"Mike"}</div>
87 +<div>{"inputs":["Mofei"],"output":"Mofei"}</div>
88 +<div>{"inputs":["Mofei"],"output":"Mofei"}</div>
89 +<div>{"inputs":["Mike"],"output":"Mike"}</div>
90 +<div>{"inputs":["Mike"],"output":"Mike"}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.js new
+23
@@ -0,0 +1,23 @@
1 +import {fbt} from 'fbt';
2 +import {useMemo} from 'react';
3 +import {ValidateMemoization} from 'shared-runtime';
4 +
5 +function Component({data}) {
6 + const el = useMemo(
7 + () => (
8 + <fbt desc="user name">
9 + <fbt:param name="name">{data.name ?? ''}</fbt:param>
10 + </fbt>
11 + ),
12 + [data.name]
13 + );
14 + return <ValidateMemoization inputs={[data.name]} output={el} />;
15 +}
16 +
17 +const props1 = {data: {name: 'Mike'}};
18 +const props2 = {data: {name: 'Mofei'}};
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [props1],
22 + sequentialRenders: [props1, props2, props2, props1, {...props1}],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-propagate-type-of-ternary-jsx.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function V0({v1, v2}: V3<{v1: any, v2: V4}>): V12.V11 {
6 + const v5 = v1.v6?.v7;
7 + return (
8 + <Component8 c9={va} cb="apqjx">
9 + {v5 != null ? (
10 + <ComponentC cd={v5}>
11 + <ComponentE cf={v1} c10={v2} />
12 + </ComponentC>
13 + ) : (
14 + <ComponentE cf={v1} c10={v2} />
15 + )}
16 + </Component8>
17 + );
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime";
26 +function V0(t0) {
27 + const $ = _c(4);
28 + const { v1, v2 } = t0;
29 + const v5 = v1.v6?.v7;
30 + let t1;
31 + if ($[0] !== v5 || $[1] !== v1 || $[2] !== v2) {
32 + t1 = (
33 + <Component8 c9={va} cb="apqjx">
34 + {v5 != null ? (
35 + <ComponentC cd={v5}>
36 + <ComponentE cf={v1} c10={v2} />
37 + </ComponentC>
38 + ) : (
39 + <ComponentE cf={v1} c10={v2} />
40 + )}
41 + </Component8>
42 + );
43 + $[0] = v5;
44 + $[1] = v1;
45 + $[2] = v2;
46 + $[3] = t1;
47 + } else {
48 + t1 = $[3];
49 + }
50 + return t1;
51 +}
52 +
53 +```
54 +
55 +### Eval output
56 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-propagate-type-of-ternary-jsx.js new
+14
@@ -0,0 +1,14 @@
1 +function V0({v1, v2}: V3<{v1: any, v2: V4}>): V12.V11 {
2 + const v5 = v1.v6?.v7;
3 + return (
4 + <Component8 c9={va} cb="apqjx">
5 + {v5 != null ? (
6 + <ComponentC cd={v5}>
7 + <ComponentE cf={v1} c10={v2} />
8 + </ComponentC>
9 + ) : (
10 + <ComponentE cf={v1} c10={v2} />
11 + )}
12 + </Component8>
13 + );
14 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-propagate-type-of-ternary-nested.expect.md new
+72
@@ -0,0 +1,72 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function V0({v1}: V2<{v1?: V3}>): V2b.V2a {
6 + const v4 = v5(V6.v7({v8: V9.va}));
7 + const vb = (
8 + <ComponentC cd="TxqUy" ce="oh`]uc" cf="Bdbo" c10={!V9.va && v11.v12}>
9 + gmhubcw
10 + {v1 === V3.V13 ? (
11 + <c14 c15="L^]w\\T\\qrGmqrlQyrvBgf\\inuRdkEqwVPwixiriYGSZmKJf]E]RdT{N[WyVPiEJIbdFzvDohJV[BV`H[[K^xoy[HOGKDqVzUJ^h">
12 + iawyneijcgamsfgrrjyvhjrrqvzexxwenxqoknnilmfloafyvnvkqbssqnxnexqvtcpvjysaiovjxyqrorqskfph
13 + </c14>
14 + ) : v16.v17('pyorztRC]EJzVuP^e') ? (
15 + <c14 c15="CRinMqvmOknWRAKERI]RBzB_LXGKQe{SUpoN[\\gL[`bLMOhvFqDVVMNOdY">
16 + goprinbjmmjhfserfuqyluxcewpyjihektogc
17 + </c14>
18 + ) : (
19 + <c14 c15="H\\\\GAcTc\\lfGMW[yHriCpvW`w]niSIKj\\kdgFI">
20 + yejarlvudihqdrdgpvahovggdnmgnueedxpbwbkdvvkdhqwrtoiual
21 + </c14>
22 + )}
23 + hflmn
24 + </ComponentC>
25 + );
26 + return vb;
27 +}
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function V0(t0) {
36 + const $ = _c(2);
37 + const { v1 } = t0;
38 + v5(V6.v7({ v8: V9.va }));
39 + let t1;
40 + if ($[0] !== v1) {
41 + t1 = (
42 + <ComponentC cd="TxqUy" ce="oh`]uc" cf="Bdbo" c10={!V9.va && v11.v12}>
43 + gmhubcw
44 + {v1 === V3.V13 ? (
45 + <c14 c15="L^]w\\\\T\\\\qrGmqrlQyrvBgf\\\\inuRdkEqwVPwixiriYGSZmKJf]E]RdT{N[WyVPiEJIbdFzvDohJV[BV`H[[K^xoy[HOGKDqVzUJ^h">
46 + iawyneijcgamsfgrrjyvhjrrqvzexxwenxqoknnilmfloafyvnvkqbssqnxnexqvtcpvjysaiovjxyqrorqskfph
47 + </c14>
48 + ) : v16.v17("pyorztRC]EJzVuP^e") ? (
49 + <c14 c15="CRinMqvmOknWRAKERI]RBzB_LXGKQe{SUpoN[\\\\gL[`bLMOhvFqDVVMNOdY">
50 + goprinbjmmjhfserfuqyluxcewpyjihektogc
51 + </c14>
52 + ) : (
53 + <c14 c15="H\\\\\\\\GAcTc\\\\lfGMW[yHriCpvW`w]niSIKj\\\\kdgFI">
54 + yejarlvudihqdrdgpvahovggdnmgnueedxpbwbkdvvkdhqwrtoiual
55 + </c14>
56 + )}
57 + hflmn
58 + </ComponentC>
59 + );
60 + $[0] = v1;
61 + $[1] = t1;
62 + } else {
63 + t1 = $[1];
64 + }
65 + const vb = t1;
66 + return vb;
67 +}
68 +
69 +```
70 +
71 +### Eval output
72 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-propagate-type-of-ternary-nested.js new
+23
@@ -0,0 +1,23 @@
1 +function V0({v1}: V2<{v1?: V3}>): V2b.V2a {
2 + const v4 = v5(V6.v7({v8: V9.va}));
3 + const vb = (
4 + <ComponentC cd="TxqUy" ce="oh`]uc" cf="Bdbo" c10={!V9.va && v11.v12}>
5 + gmhubcw
6 + {v1 === V3.V13 ? (
7 + <c14 c15="L^]w\\T\\qrGmqrlQyrvBgf\\inuRdkEqwVPwixiriYGSZmKJf]E]RdT{N[WyVPiEJIbdFzvDohJV[BV`H[[K^xoy[HOGKDqVzUJ^h">
8 + iawyneijcgamsfgrrjyvhjrrqvzexxwenxqoknnilmfloafyvnvkqbssqnxnexqvtcpvjysaiovjxyqrorqskfph
9 + </c14>
10 + ) : v16.v17('pyorztRC]EJzVuP^e') ? (
11 + <c14 c15="CRinMqvmOknWRAKERI]RBzB_LXGKQe{SUpoN[\\gL[`bLMOhvFqDVVMNOdY">
12 + goprinbjmmjhfserfuqyluxcewpyjihektogc
13 + </c14>
14 + ) : (
15 + <c14 c15="H\\\\GAcTc\\lfGMW[yHriCpvW`w]niSIKj\\kdgFI">
16 + yejarlvudihqdrdgpvahovggdnmgnueedxpbwbkdvvkdhqwrtoiual
17 + </c14>
18 + )}
19 + hflmn
20 + </ComponentC>
21 + );
22 + return vb;
23 +}
compiler/scripts/anonymize.js new
+250
@@ -0,0 +1,250 @@
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 +'use strict';
9 +
10 +const fs = require('fs');
11 +const HermesParser = require('hermes-parser');
12 +const BabelParser = require('@babel/parser');
13 +const BabelCore = require('@babel/core');
14 +const invariant = require('invariant');
15 +const {argv, stdin} = require('process');
16 +const prettier = require('prettier');
17 +const {JSXText} = require('hermes-parser/dist/generated/ESTreeVisitorKeys');
18 +
19 +function runPlugin(text, file, language) {
20 + let ast;
21 + if (language === 'flow') {
22 + ast = HermesParser.parse(text, {
23 + babel: true,
24 + flow: 'all',
25 + sourceFilename: file,
26 + sourceType: 'module',
27 + enableExperimentalComponentSyntax: true,
28 + });
29 + } else {
30 + ast = BabelParser.parse(text, {
31 + sourceFilename: file,
32 + plugins: ['typescript', 'jsx'],
33 + sourceType: 'module',
34 + });
35 + }
36 + const result = BabelCore.transformFromAstSync(ast, text, {
37 + ast: false,
38 + filename: file,
39 + highlightCode: false,
40 + retainLines: true,
41 + plugins: [[AnonymizePlugin]],
42 + sourceType: 'module',
43 + configFile: false,
44 + babelrc: false,
45 + });
46 + invariant(
47 + result?.code != null,
48 + `Expected BabelPluginReactForget to codegen successfully, got: ${result}`
49 + );
50 + return result.code;
51 +}
52 +
53 +async function format(code, language) {
54 + return await prettier.format(code, {
55 + semi: true,
56 + parser: language === 'typescript' ? 'babel-ts' : 'flow',
57 + });
58 +}
59 +
60 +const TAG_NAMES = new Set([
61 + 'a',
62 + 'body',
63 + 'button',
64 + 'div',
65 + 'form',
66 + 'head',
67 + 'html',
68 + 'input',
69 + 'label',
70 + 'select',
71 + 'span',
72 + 'textarea',
73 +
74 + // property/attribute names
75 + 'value',
76 + 'checked',
77 + 'onClick',
78 + 'onSubmit',
79 + 'name',
80 +]);
81 +
82 +const BUILTIN_HOOKS = new Set([
83 + 'useContext',
84 + 'useEffect',
85 + 'useInsertionEffect',
86 + 'useLayoutEffect',
87 + 'useReducer',
88 + 'useState',
89 +]);
90 +
91 +const GLOBALS = new Set([
92 + 'String',
93 + 'Object',
94 + 'Function',
95 + 'Number',
96 + 'RegExp',
97 + 'Date',
98 + 'Error',
99 + 'Function',
100 + 'TypeError',
101 + 'RangeError',
102 + 'ReferenceError',
103 + 'SyntaxError',
104 + 'URIError',
105 + 'EvalError',
106 + 'Boolean',
107 + 'DataView',
108 + 'Float32Array',
109 + 'Float64Array',
110 + 'Int8Array',
111 + 'Int16Array',
112 + 'Int32Array',
113 + 'Map',
114 + 'Set',
115 + 'WeakMap',
116 + 'Uint8Array',
117 + 'Uint8ClampedArray',
118 + 'Uint16Array',
119 + 'Uint32Array',
120 + 'ArrayBuffer',
121 + 'JSON',
122 + 'parseFloat',
123 + 'parseInt',
124 + 'console',
125 + 'isNaN',
126 + 'eval',
127 + 'isFinite',
128 + 'encodeURI',
129 + 'decodeURI',
130 + 'encodeURIComponent',
131 + 'decodeURIComponent',
132 +
133 + // common method/property names of globals
134 + 'map',
135 + 'push',
136 + 'at',
137 + 'filter',
138 + 'slice',
139 + 'splice',
140 + 'add',
141 + 'get',
142 + 'set',
143 + 'has',
144 + 'size',
145 + 'length',
146 + 'toString',
147 +]);
148 +
149 +function AnonymizePlugin(_babel) {
150 + let index = 0;
151 + const identifiers = new Map();
152 + const literals = new Map();
153 + return {
154 + name: 'anonymize',
155 + visitor: {
156 + JSXNamespacedName(path) {
157 + throw error('TODO: handle JSXNamedspacedName');
158 + },
159 + JSXIdentifier(path) {
160 + const name = path.node.name;
161 + if (TAG_NAMES.has(name)) {
162 + return;
163 + }
164 + let nextName = identifiers.get(name);
165 + if (nextName == null) {
166 + const isCapitalized =
167 + name.slice(0, 1).toUpperCase() === name.slice(0, 1);
168 + nextName = isCapitalized
169 + ? `Component${(index++).toString(16).toUpperCase()}`
170 + : `c${(index++).toString(16)}`;
171 + identifiers.set(name, nextName);
172 + }
173 + path.node.name = nextName;
174 + },
175 + Identifier(path) {
176 + const name = path.node.name;
177 + if (BUILTIN_HOOKS.has(name) || GLOBALS.has(name)) {
178 + return;
179 + }
180 + let nextName = identifiers.get(name);
181 + if (nextName == null) {
182 + const isCapitalized =
183 + name.slice(0, 1).toUpperCase() === name.slice(0, 1);
184 + const prefix = isCapitalized ? 'V' : 'v';
185 + nextName = `${prefix}${(index++).toString(16)}`;
186 + if (name.startsWith('use')) {
187 + nextName =
188 + 'use' + nextName.slice(0, 1).toUpperCase() + nextName.slice(1);
189 + }
190 + identifiers.set(name, nextName);
191 + }
192 + path.node.name = nextName;
193 + },
194 + JSXText(path) {
195 + const value = path.node.value;
196 + let nextValue = literals.get(value);
197 + if (nextValue == null) {
198 + let string = '';
199 + while (string.length < value.length) {
200 + string += String.fromCharCode(Math.round(Math.random() * 25) + 97);
201 + }
202 + nextValue = string;
203 + literals.set(value, nextValue);
204 + }
205 + path.node.value = nextValue;
206 + },
207 + StringLiteral(path) {
208 + const value = path.node.value;
209 + let nextValue = literals.get(value);
210 + if (nextValue == null) {
211 + let string = '';
212 + while (string.length < value.length) {
213 + string += String.fromCharCode(Math.round(Math.random() * 58) + 65);
214 + }
215 + nextValue = string;
216 + literals.set(value, nextValue);
217 + }
218 + path.node.value = nextValue;
219 + },
220 + NumericLiteral(path) {
221 + const value = path.node.value;
222 + let nextValue = literals.get(value);
223 + if (nextValue == null) {
224 + nextValue = Number.isInteger(value)
225 + ? Math.round(Math.random() * Number.MAX_SAFE_INTEGER)
226 + : Math.random() * Number.MAX_VALUE;
227 + literals.set(value, nextValue);
228 + }
229 + path.node.value = nextValue;
230 + },
231 + },
232 + };
233 +}
234 +
235 +let file;
236 +let text;
237 +if (argv.length >= 3) {
238 + file = argv[2];
239 + text = fs.readFileSync(file, 'utf8');
240 +} else {
241 + // read from stdin
242 + file = 'stdin.js';
243 + text = fs.readFileSync(stdin.fd, 'utf8');
244 +}
245 +const language =
246 + file.endsWith('.ts') || file.endsWith('.tsx') ? 'typescript' : 'flow';
247 +const result = runPlugin(text, file, language);
248 +format(result, language).then(formatted => {
249 + console.log(formatted);
250 +});