Skip to content

Commit 2de3094

Browse files
committed
[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: 4b9b1d5 Pull Request resolved: #30573
1 parent 98eb874 commit 2de3094

29 files changed

+1194
-674
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWit
7777
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
7878
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
7979
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
80-
import {eliminateRedundantPhi, enterSSA, leaveSSA} from '../SSA';
80+
import {
81+
eliminateRedundantPhi,
82+
enterSSA,
83+
rewriteInstructionKindsBasedOnReassignment,
84+
} from '../SSA';
8185
import {inferTypes} from '../TypeInference';
8286
import {
8387
logCodegenFunction,
@@ -98,6 +102,7 @@ import {
98102
} from '../Validation';
99103
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
100104
import {outlineFunctions} from '../Optimization/OutlineFunctions';
105+
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
101106

102107
export type CompilerPipelineValue =
103108
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -237,8 +242,19 @@ function* runWithEnvironment(
237242
inferReactivePlaces(hir);
238243
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
239244

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+
});
242258

243259
inferReactiveScopeVariables(hir);
244260
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,9 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
12481248

12491249
/**
12501250
* 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.
12511254
*/
12521255
export function promoteTemporary(identifier: Identifier): void {
12531256
CompilerError.invariant(identifier.name === null, {
@@ -1258,7 +1261,7 @@ export function promoteTemporary(identifier: Identifier): void {
12581261
});
12591262
identifier.name = {
12601263
kind: 'promoted',
1261-
value: `#t${identifier.id}`,
1264+
value: `#t${identifier.declarationId}`,
12621265
};
12631266
}
12641267

@@ -1269,6 +1272,9 @@ export function isPromotedTemporary(name: string): boolean {
12691272
/**
12701273
* Given an unnamed identifier, promote it to a named identifier, distinguishing
12711274
* 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.
12721278
*/
12731279
export function promoteTemporaryJsxTag(identifier: Identifier): void {
12741280
CompilerError.invariant(identifier.name === null, {
@@ -1279,7 +1285,7 @@ export function promoteTemporaryJsxTag(identifier: Identifier): void {
12791285
});
12801286
identifier.name = {
12811287
kind: 'promoted',
1282-
value: `#T${identifier.id}`,
1288+
value: `#T${identifier.declarationId}`,
12831289
};
12841290
}
12851291

compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,3 +908,16 @@ export function createTemporaryPlace(
908908
loc: GeneratedSource,
909909
};
910910
}
911+
912+
/**
913+
* Clones an existing Place, returning a new temporary Place that shares the
914+
* same metadata properties as the original place (effect, reactive flag, type)
915+
* but has a new, temporary Identifier.
916+
*/
917+
export function clonePlaceToTemporary(env: Environment, place: Place): Place {
918+
const temp = createTemporaryPlace(env, place.loc);
919+
temp.effect = place.effect;
920+
temp.identifier.type = place.identifier.type;
921+
temp.reactive = place.reactive;
922+
return temp;
923+
}

compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '../HIR';
2121
import {deadCodeElimination} from '../Optimization';
2222
import {inferReactiveScopeVariables} from '../ReactiveScopes';
23-
import {leaveSSA} from '../SSA';
23+
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
2424
import {logHIRFunction} from '../Utils/logger';
2525
import {inferMutableContextVariables} from './InferMutableContextVariables';
2626
import {inferMutableRanges} from './InferMutableRanges';
@@ -108,7 +108,7 @@ function lower(func: HIRFunction): void {
108108
inferReferenceEffects(func, {isFunctionExpression: true});
109109
deadCodeElimination(func);
110110
inferMutableRanges(func);
111-
leaveSSA(func);
111+
rewriteInstructionKindsBasedOnReassignment(func);
112112
inferReactiveScopeVariables(func);
113113
inferMutableContextVariables(func);
114114
logHIRFunction('AnalyseFunction (inner)', func);

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {Environment, EnvironmentConfig, ExternalFunction} from '../HIR';
1818
import {
1919
ArrayPattern,
2020
BlockId,
21+
DeclarationId,
2122
GeneratedSource,
2223
Identifier,
2324
IdentifierId,
@@ -309,9 +310,9 @@ function codegenReactiveFunction(
309310
): Result<CodegenFunction, CompilerError> {
310311
for (const param of fn.params) {
311312
if (param.kind === 'Identifier') {
312-
cx.temp.set(param.identifier.id, null);
313+
cx.temp.set(param.identifier.declarationId, null);
313314
} else {
314-
cx.temp.set(param.place.identifier.id, null);
315+
cx.temp.set(param.place.identifier.declarationId, null);
315316
}
316317
}
317318

@@ -392,7 +393,11 @@ class Context {
392393
env: Environment;
393394
fnName: string;
394395
#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();
396401
temp: Temporaries;
397402
errors: CompilerError = new CompilerError();
398403
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
@@ -418,11 +423,11 @@ class Context {
418423
}
419424

420425
declare(identifier: Identifier): void {
421-
this.#declarations.add(identifier.id);
426+
this.#declarations.add(identifier.declarationId);
422427
}
423428

424429
hasDeclared(identifier: Identifier): boolean {
425-
return this.#declarations.has(identifier.id);
430+
return this.#declarations.has(identifier.declarationId);
426431
}
427432

428433
synthesizeName(name: string): ValidIdentifierName {
@@ -1147,7 +1152,7 @@ function codegenTerminal(
11471152
let catchParam = null;
11481153
if (terminal.handlerBinding !== null) {
11491154
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
1150-
cx.temp.set(terminal.handlerBinding.identifier.id, null);
1155+
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
11511156
}
11521157
return t.tryStatement(
11531158
codegenBlock(cx, terminal.block),
@@ -1205,7 +1210,7 @@ function codegenInstructionNullable(
12051210
kind !== InstructionKind.Reassign &&
12061211
place.identifier.name === null
12071212
) {
1208-
cx.temp.set(place.identifier.id, null);
1213+
cx.temp.set(place.identifier.declarationId, null);
12091214
}
12101215
const isDeclared = cx.hasDeclared(place.identifier);
12111216
hasReasign ||= isDeclared;
@@ -1261,7 +1266,7 @@ function codegenInstructionNullable(
12611266
);
12621267
if (instr.lvalue !== null) {
12631268
if (instr.value.kind !== 'StoreContext') {
1264-
cx.temp.set(instr.lvalue.identifier.id, expr);
1269+
cx.temp.set(instr.lvalue.identifier.declarationId, expr);
12651270
return null;
12661271
} else {
12671272
// Handle chained reassignments for context variables
@@ -1530,7 +1535,7 @@ function createCallExpression(
15301535
}
15311536
}
15321537

1533-
type Temporaries = Map<IdentifierId, t.Expression | t.JSXText | null>;
1538+
type Temporaries = Map<DeclarationId, t.Expression | t.JSXText | null>;
15341539

15351540
function codegenLabel(id: BlockId): string {
15361541
return `bb${id}`;
@@ -1549,7 +1554,7 @@ function codegenInstruction(
15491554
}
15501555
if (instr.lvalue.identifier.name === null) {
15511556
// temporary
1552-
cx.temp.set(instr.lvalue.identifier.id, value);
1557+
cx.temp.set(instr.lvalue.identifier.declarationId, value);
15531558
return t.emptyStatement();
15541559
} else {
15551560
const expressionValue = convertValueToExpression(value);
@@ -2498,7 +2503,7 @@ function codegenPlaceToExpression(cx: Context, place: Place): t.Expression {
24982503
}
24992504

25002505
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);
25022507
if (tmp != null) {
25032508
return tmp;
25042509
}

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import {
9+
DeclarationId,
910
Destructure,
1011
Environment,
1112
IdentifierId,
@@ -17,6 +18,7 @@ import {
1718
ReactiveStatement,
1819
promoteTemporary,
1920
} from '../HIR';
21+
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
2022
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
2123
import {
2224
ReactiveFunctionTransform,
@@ -82,7 +84,11 @@ export function extractScopeDeclarationsFromDestructuring(
8284

8385
class State {
8486
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();
8692

8793
constructor(env: Environment) {
8894
this.env = env;
@@ -92,7 +98,7 @@ class State {
9298
class Visitor extends ReactiveFunctionTransform<State> {
9399
override visitScope(scope: ReactiveScopeBlock, state: State): void {
94100
for (const [, declaration] of scope.scope.declarations) {
95-
state.declared.add(declaration.identifier.id);
101+
state.declared.add(declaration.identifier.declarationId);
96102
}
97103
this.traverseScope(scope, state);
98104
}
@@ -131,7 +137,7 @@ function transformDestructuring(
131137
let reassigned: Set<IdentifierId> = new Set();
132138
let hasDeclaration = false;
133139
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);
135141
if (isDeclared) {
136142
reassigned.add(place.identifier.id);
137143
}
@@ -150,15 +156,7 @@ function transformDestructuring(
150156
if (!reassigned.has(place.identifier.id)) {
151157
return place;
152158
}
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);
162160
promoteTemporary(temporary.identifier);
163161
renamed.set(place, temporary);
164162
return temporary;

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import {CompilerError, SourceLocation} from '..';
99
import {Environment} from '../HIR';
1010
import {
11+
DeclarationId,
1112
GeneratedSource,
1213
HIRFunction,
1314
Identifier,
@@ -257,21 +258,34 @@ export function findDisjointMutableValues(
257258
fn: HIRFunction,
258259
): DisjointSet<Identifier> {
259260
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+
260269
for (const [_, block] of fn.body.blocks) {
261270
/*
262271
* If a phi is mutated after creation, then we need to alias all of its operands such that they
263272
* are assigned to the same scope.
264273
*/
265274
for (const phi of block.phis) {
266275
if (
267-
// The phi was reset because it was not mutated after creation
268276
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
269277
phi.id.mutableRange.end >
270278
(block.instructions.at(0)?.id ?? block.terminal.id)
271279
) {
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);
274287
}
288+
scopeIdentifiers.union(operands);
275289
} else if (fn.env.config.enableForest) {
276290
for (const [, phiId] of phi.operands) {
277291
scopeIdentifiers.union([phi.id, phiId]);
@@ -286,9 +300,15 @@ export function findDisjointMutableValues(
286300
operands.push(instr.lvalue!.identifier);
287301
}
288302
if (
303+
instr.value.kind === 'DeclareLocal' ||
304+
instr.value.kind === 'DeclareContext'
305+
) {
306+
declareIdentifier(instr.value.lvalue.place);
307+
} else if (
289308
instr.value.kind === 'StoreLocal' ||
290309
instr.value.kind === 'StoreContext'
291310
) {
311+
declareIdentifier(instr.value.lvalue.place);
292312
if (
293313
instr.value.lvalue.place.identifier.mutableRange.end >
294314
instr.value.lvalue.place.identifier.mutableRange.start + 1
@@ -303,6 +323,7 @@ export function findDisjointMutableValues(
303323
}
304324
} else if (instr.value.kind === 'Destructure') {
305325
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
326+
declareIdentifier(place);
306327
if (
307328
place.identifier.mutableRange.end >
308329
place.identifier.mutableRange.start + 1

0 commit comments

Comments
 (0)