Skip to content

Commit 7185c40

Browse files
committed
[compiler][wip] Infer alias effects for function expressions
This is a stab at addressing a pattern that mofeiz and I have both stumbled across. Today, FunctionExpression's context list describes values from the outer context that are accessed in the function, and with what effect they were accessed. This allows us to describe the fact that a value from the outer context is known to be mutated inside a function expression, or is known to be captured (aliased) into some other value in the function expression. However, the basic `Effect` kind is insufficient to describe the full semantics. Notably, it doesn't let us describe more complex aliasing relationships. From an example mofeiz added: ```js const x = {}; const y = {}; const f = () => { const a = [y]; const b = x; // this sets y.x = x a[0].x = b; } f(); mutate(y.x); // which means this mutates x! ``` Here, the Effect on the context operands are `[mutate y, read x]`. The `mutate y` is bc of the array push. But the `read x` is surprising — `x` is captured into `y`, but there is no subsequent mutation of y or x, so we consider this a read. But as the comments indicate, the final line mutates x! We need to reflect the fact that even though x isn't mutated inside the function, it is aliased into y, such that if y is subsequently mutated that this should count as a mutation of x too. The idea of this PR is to extend the FunctionEffect type with a CaptureEffect variant which lists out the aliasing groups that occur inside the function expression. This allows us to bubble up the results of alias analysis from inside a function. The idea is to: * Return the alias sets from InferMutableRanges * Augment them with capturing of the form above, handling cases such as the `a[0].x = b` * For each alias group, record a CaptureEffect for any group that contains 2+ context operands * Extend the alias sets in the _outer_ function with the CaptureEffect sets from FunctionExpression/ObjectMethod instructions. This isn't quite right yet, just sharing early hacking. ghstack-source-id: 0056007 Pull Request resolved: #33151
1 parent 6b34659 commit 7185c40

13 files changed

+261
-188
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ export type FunctionEffect =
300300
places: ReadonlySet<Place>;
301301
effect: Effect;
302302
loc: SourceLocation;
303+
}
304+
| {
305+
kind: 'CaptureEffect';
306+
places: ReadonlySet<Place>;
303307
};
304308

305309
/*

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,12 +546,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
546546
const effects =
547547
instrValue.loweredFunc.func.effects
548548
?.map(effect => {
549-
if (effect.kind === 'ContextMutation') {
550-
return `ContextMutation places=[${[...effect.places]
551-
.map(place => printPlace(place))
552-
.join(', ')}] effect=${effect.effect}`;
553-
} else {
554-
return `GlobalMutation`;
549+
switch (effect.kind) {
550+
case 'ContextMutation': {
551+
return `ContextMutation places=[${[...effect.places]
552+
.map(place => printPlace(place))
553+
.join(', ')}] effect=${effect.effect}`;
554+
}
555+
case 'GlobalMutation': {
556+
return 'GlobalMutation';
557+
}
558+
case 'ReactMutation': {
559+
return 'ReactMutation';
560+
}
561+
case 'CaptureEffect': {
562+
return `CaptureEffect places=[${[...effect.places]
563+
.map(place => printPlace(place))
564+
.join(', ')}]`;
565+
}
555566
}
556567
})
557568
.join(', ') ?? '';

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
HIRFunction,
1212
Identifier,
1313
LoweredFunction,
14+
Place,
1415
isRefOrRefValue,
1516
makeInstructionId,
1617
} from '../HIR';
@@ -19,15 +20,17 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
1920
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
2021
import {inferMutableRanges} from './InferMutableRanges';
2122
import inferReferenceEffects from './InferReferenceEffects';
23+
import DisjointSet from '../Utils/DisjointSet';
24+
import {eachInstructionValueOperand} from '../HIR/visitors';
2225

2326
export default function analyseFunctions(func: HIRFunction): void {
2427
for (const [_, block] of func.body.blocks) {
2528
for (const instr of block.instructions) {
2629
switch (instr.value.kind) {
2730
case 'ObjectMethod':
2831
case 'FunctionExpression': {
29-
lower(instr.value.loweredFunc.func);
30-
infer(instr.value.loweredFunc);
32+
const aliases = lower(instr.value.loweredFunc.func);
33+
infer(instr.value.loweredFunc, aliases);
3134

3235
/**
3336
* Reset mutable range for outer inferReferenceEffects
@@ -44,21 +47,61 @@ export default function analyseFunctions(func: HIRFunction): void {
4447
}
4548
}
4649

47-
function lower(func: HIRFunction): void {
50+
function lower(func: HIRFunction): DisjointSet<Identifier> {
4851
analyseFunctions(func);
4952
inferReferenceEffects(func, {isFunctionExpression: true});
5053
deadCodeElimination(func);
51-
inferMutableRanges(func);
54+
const aliases = inferMutableRanges(func);
5255
rewriteInstructionKindsBasedOnReassignment(func);
5356
inferReactiveScopeVariables(func);
5457
func.env.logger?.debugLogIRs?.({
5558
kind: 'hir',
5659
name: 'AnalyseFunction (inner)',
5760
value: func,
5861
});
62+
inferAliasesForCapturing(func, aliases);
63+
return aliases;
5964
}
6065

61-
function infer(loweredFunc: LoweredFunction): void {
66+
/**
67+
* The alias sets returned by InferMutableRanges() accounts only for aliases that
68+
* are known to mutate together. Notably this skips cases where a value is captured
69+
* into some other value, but neither is subsequently mutated. An example is pushing
70+
* a mutable value onto an array, where neither the array or value are subsequently
71+
* mutated.
72+
*
73+
* This function extends the aliases sets to account for such capturing, so that we
74+
* can detect cases where one of the values in a set is mutated later (in an outer function)
75+
* we can correctly infer them as mutating together.
76+
*/
77+
function inferAliasesForCapturing(
78+
fn: HIRFunction,
79+
aliases: DisjointSet<Identifier>,
80+
): void {
81+
for (const block of fn.body.blocks.values()) {
82+
for (const instr of block.instructions) {
83+
let operands: Array<Identifier> | null = null;
84+
for (const operand of eachInstructionValueOperand(instr.value)) {
85+
if (
86+
operand.effect === Effect.Mutate ||
87+
operand.effect === Effect.Store ||
88+
operand.effect === Effect.Capture
89+
) {
90+
operands ??= [];
91+
operands.push(operand.identifier);
92+
}
93+
}
94+
if (operands != null && operands.length > 1) {
95+
aliases.union(operands);
96+
}
97+
}
98+
}
99+
}
100+
101+
function infer(
102+
loweredFunc: LoweredFunction,
103+
aliases: DisjointSet<Identifier>,
104+
): void {
62105
for (const operand of loweredFunc.func.context) {
63106
const identifier = operand.identifier;
64107
CompilerError.invariant(operand.effect === Effect.Unknown, {
@@ -85,6 +128,23 @@ function infer(loweredFunc: LoweredFunction): void {
85128
operand.effect = Effect.Read;
86129
}
87130
}
131+
const contextIdentifiers = new Map(
132+
loweredFunc.func.context.map(place => [place.identifier, place]),
133+
);
134+
for (const set of aliases.buildSets()) {
135+
const contextOperands: Set<Place> = new Set(
136+
[...set]
137+
.map(identifier => contextIdentifiers.get(identifier))
138+
.filter(place => place != null) as Array<Place>,
139+
);
140+
if (contextOperands.size > 1) {
141+
loweredFunc.func.effects ??= [];
142+
loweredFunc.func.effects?.push({
143+
kind: 'CaptureEffect',
144+
places: contextOperands,
145+
});
146+
}
147+
}
88148
}
89149

90150
function isMutatedOrReassigned(id: Identifier): boolean {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ function inferInstr(
6060
alias = instrValue.value;
6161
break;
6262
}
63+
case 'ObjectMethod':
64+
case 'FunctionExpression': {
65+
for (const effect of instrValue.loweredFunc.func.effects ?? []) {
66+
if (effect.kind !== 'CaptureEffect') {
67+
continue;
68+
}
69+
aliases.union([...effect.places].map(place => place.identifier));
70+
}
71+
return;
72+
}
6373
default:
6474
return;
6575
}

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

Lines changed: 74 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -95,45 +95,58 @@ function inheritFunctionEffects(
9595

9696
return effects
9797
.flatMap(effect => {
98-
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
99-
return [effect];
100-
} else {
101-
const effects: Array<FunctionEffect | null> = [];
102-
CompilerError.invariant(effect.kind === 'ContextMutation', {
103-
reason: 'Expected ContextMutation',
104-
loc: null,
105-
});
106-
/**
107-
* Contextual effects need to be replayed against the current inference
108-
* state, which may know more about the value to which the effect applied.
109-
* The main cases are:
110-
* 1. The mutated context value is _still_ a context value in the current scope,
111-
* so we have to continue propagating the original context mutation.
112-
* 2. The mutated context value is a mutable value in the current scope,
113-
* so the context mutation was fine and we can skip propagating the effect.
114-
* 3. The mutated context value is an immutable value in the current scope,
115-
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
116-
* more detailed effect to the current function context.
117-
*/
118-
for (const place of effect.places) {
119-
if (state.isDefined(place)) {
120-
const replayedEffect = inferOperandEffect(state, {
121-
...place,
122-
loc: effect.loc,
123-
effect: effect.effect,
124-
});
125-
if (replayedEffect != null) {
126-
if (replayedEffect.kind === 'ContextMutation') {
127-
// Case 1, still a context value so propagate the original effect
128-
effects.push(effect);
129-
} else {
130-
// Case 3, immutable value so propagate the more precise effect
131-
effects.push(replayedEffect);
132-
}
133-
} // else case 2, local mutable value so this effect was fine
98+
switch (effect.kind) {
99+
case 'GlobalMutation':
100+
case 'ReactMutation': {
101+
return [effect];
102+
}
103+
case 'ContextMutation': {
104+
const effects: Array<FunctionEffect | null> = [];
105+
CompilerError.invariant(effect.kind === 'ContextMutation', {
106+
reason: 'Expected ContextMutation',
107+
loc: null,
108+
});
109+
/**
110+
* Contextual effects need to be replayed against the current inference
111+
* state, which may know more about the value to which the effect applied.
112+
* The main cases are:
113+
* 1. The mutated context value is _still_ a context value in the current scope,
114+
* so we have to continue propagating the original context mutation.
115+
* 2. The mutated context value is a mutable value in the current scope,
116+
* so the context mutation was fine and we can skip propagating the effect.
117+
* 3. The mutated context value is an immutable value in the current scope,
118+
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
119+
* more detailed effect to the current function context.
120+
*/
121+
for (const place of effect.places) {
122+
if (state.isDefined(place)) {
123+
const replayedEffect = inferOperandEffect(state, {
124+
...place,
125+
loc: effect.loc,
126+
effect: effect.effect,
127+
});
128+
if (replayedEffect != null) {
129+
if (replayedEffect.kind === 'ContextMutation') {
130+
// Case 1, still a context value so propagate the original effect
131+
effects.push(effect);
132+
} else {
133+
// Case 3, immutable value so propagate the more precise effect
134+
effects.push(replayedEffect);
135+
}
136+
} // else case 2, local mutable value so this effect was fine
137+
}
134138
}
139+
return effects;
140+
}
141+
case 'CaptureEffect': {
142+
return [];
143+
}
144+
default: {
145+
assertExhaustive(
146+
effect,
147+
`Unexpected effect kind '${(effect as any).kind}'`,
148+
);
135149
}
136-
return effects;
137150
}
138151
})
139152
.filter((effect): effect is FunctionEffect => effect != null);
@@ -298,26 +311,31 @@ export function inferTerminalFunctionEffects(
298311
export function transformFunctionEffectErrors(
299312
functionEffects: Array<FunctionEffect>,
300313
): Array<CompilerErrorDetailOptions> {
301-
return functionEffects.map(eff => {
302-
switch (eff.kind) {
303-
case 'ReactMutation':
304-
case 'GlobalMutation': {
305-
return eff.error;
306-
}
307-
case 'ContextMutation': {
308-
return {
309-
severity: ErrorSeverity.Invariant,
310-
reason: `Unexpected ContextMutation in top-level function effects`,
311-
loc: eff.loc,
312-
};
314+
return functionEffects
315+
.map(eff => {
316+
switch (eff.kind) {
317+
case 'ReactMutation':
318+
case 'GlobalMutation': {
319+
return eff.error;
320+
}
321+
case 'ContextMutation': {
322+
return {
323+
severity: ErrorSeverity.Invariant,
324+
reason: `Unexpected ContextMutation in top-level function effects`,
325+
loc: eff.loc,
326+
};
327+
}
328+
case 'CaptureEffect': {
329+
return null;
330+
}
331+
default:
332+
assertExhaustive(
333+
eff,
334+
`Unexpected function effect kind \`${(eff as any).kind}\``,
335+
);
313336
}
314-
default:
315-
assertExhaustive(
316-
eff,
317-
`Unexpected function effect kind \`${(eff as any).kind}\``,
318-
);
319-
}
320-
});
337+
})
338+
.filter(eff => eff != null) as Array<CompilerErrorDetailOptions>;
321339
}
322340

323341
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import {HIRFunction, Identifier} from '../HIR/HIR';
9+
import DisjointSet from '../Utils/DisjointSet';
910
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
1011
import {inferAliases} from './InferAlias';
1112
import {inferAliasForPhis} from './InferAliasForPhis';
@@ -14,7 +15,7 @@ import {inferMutableLifetimes} from './InferMutableLifetimes';
1415
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
1516
import {inferTryCatchAliases} from './InferTryCatchAliases';
1617

17-
export function inferMutableRanges(ir: HIRFunction): void {
18+
export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
1819
// Infer mutable ranges for non fields
1920
inferMutableLifetimes(ir, false);
2021

@@ -84,6 +85,8 @@ export function inferMutableRanges(ir: HIRFunction): void {
8485
}
8586
prevAliases = nextAliases;
8687
}
88+
89+
return aliases;
8790
}
8891

8992
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {

0 commit comments

Comments
 (0)