Skip to content

[compiler] Infer alias effects for function expressions #33151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: gh/josephsavona/81/base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd2537c
[compiler][wip] Infer alias effects for function expressions
josephsavona May 8, 2025
c3a733f
Update on "[compiler][wip] Infer alias effects for function expressions"
josephsavona May 8, 2025
97442d7
Update on "[compiler][wip] Infer alias effects for function expressions"
josephsavona May 8, 2025
1fec32e
Update on "[compiler][wip] Infer alias effects for function expressions"
josephsavona May 8, 2025
25ed1a1
Update on "[compiler][wip] Infer alias effects for function expressions"
josephsavona May 9, 2025
6719631
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 9, 2025
dd3625b
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 9, 2025
0111754
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 9, 2025
ba46290
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 9, 2025
ed8a0b4
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 12, 2025
49a8425
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 13, 2025
8bb9368
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 22, 2025
ad38437
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 23, 2025
d07e256
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 27, 2025
a7c677f
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 27, 2025
4e50377
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 28, 2025
5c0d2af
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 28, 2025
2ad9c84
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 29, 2025
d48e6f4
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 29, 2025
f699c8a
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 30, 2025
532ced2
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 30, 2025
0d853f5
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 30, 2025
b33fc8b
Update on "[compiler] Infer alias effects for function expressions"
josephsavona May 30, 2025
1cc66c5
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 2, 2025
ddd2a2a
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 3, 2025
0f323b0
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 3, 2025
75c18bd
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 3, 2025
07a674d
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 4, 2025
8775a7a
Update on "[compiler] Infer alias effects for function expressions"
josephsavona Jun 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ export type FunctionEffect =
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
}
| {
kind: 'CaptureEffect';
places: ReadonlySet<Place>;
};

/*
Expand Down
23 changes: 17 additions & 6 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,12 +546,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
switch (effect.kind) {
case 'ContextMutation': {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
}
case 'GlobalMutation': {
return 'GlobalMutation';
}
case 'ReactMutation': {
return 'ReactMutation';
}
case 'CaptureEffect': {
return `CaptureEffect places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}]`;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactoring to a switch statement, handling ReactMutation case (prev printed incorrectly as "GlobalMutation") and handling the new effect type

}
})
.join(', ') ?? '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
HIRFunction,
Identifier,
LoweredFunction,
Place,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
Expand All @@ -19,15 +20,23 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import DisjointSet from '../Utils/DisjointSet';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import prettyFormat from 'pretty-format';
import {printIdentifier} from '../HIR/PrintHIR';
import {Iterable_some} from '../Utils/utils';

export default function analyseFunctions(func: HIRFunction): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
const aliases = lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc, aliases);

/**
* Reset mutable range for outer inferReferenceEffects
Expand All @@ -44,21 +53,82 @@ export default function analyseFunctions(func: HIRFunction): void {
}
}

function lower(func: HIRFunction): void {
function lower(func: HIRFunction): DisjointSet<Identifier> {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
const aliases = inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
func.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: func,
});
inferAliasesForCapturing(func, aliases);
return aliases;
}

export function debugAliases(aliases: DisjointSet<Identifier>): void {
console.log(
prettyFormat(
aliases
.buildSets()
.map(set => [...set].map(ident => printIdentifier(ident))),
),
);
}

/**
* The alias sets returned by InferMutableRanges() accounts only for aliases that
* are known to mutate together. Notably this skips cases where a value is captured
* into some other value, but neither is subsequently mutated. An example is pushing
* a mutable value onto an array, where neither the array or value are subsequently
* mutated.
*
* This function extends the aliases sets to account for such capturing, so that we
* can detect cases where one of the values in a set is mutated later (in an outer function)
* we can correctly infer them as mutating together.
*/
function inferAliasesForCapturing(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
const hasStore =
lvalue.effect === Effect.Store ||
Iterable_some(
eachInstructionValueOperand(value),
operand => operand.effect === Effect.Store,
);
if (!hasStore) {
continue;
}
const operands: Array<Identifier> = [];
for (const lvalue of eachInstructionLValue(instr)) {
operands.push(lvalue.identifier);
}
for (const operand of eachInstructionValueOperand(instr.value)) {
if (
operand.effect === Effect.Store ||
operand.effect === Effect.Capture
) {
operands.push(operand.identifier);
}
}
if (operands.length > 1) {
aliases.union(operands);
}
}
}
}

function infer(loweredFunc: LoweredFunction): void {
function infer(
loweredFunc: LoweredFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
Expand All @@ -85,6 +155,23 @@ function infer(loweredFunc: LoweredFunction): void {
operand.effect = Effect.Read;
}
}
const contextIdentifiers = new Map(
loweredFunc.func.context.map(place => [place.identifier, place]),
);
for (const set of aliases.buildSets()) {
const contextOperands: Set<Place> = new Set(
[...set]
.map(identifier => contextIdentifiers.get(identifier))
.filter(place => place != null) as Array<Place>,
);
if (contextOperands.size !== 0) {
loweredFunc.func.effects ??= [];
loweredFunc.func.effects?.push({
kind: 'CaptureEffect',
places: contextOperands,
});
}
}
}

function isMutatedOrReassigned(id: Identifier): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function inferInstr(
alias = instrValue.value;
break;
}
case 'IteratorNext': {
alias = instrValue.collection;
break;
}
Comment on lines +63 to +66
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was a missing alias, oops

default:
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {HIRFunction, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';

export function inferAliasForFunctionCaptureEffects(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
const {value} = instr;
if (
value.kind !== 'FunctionExpression' &&
value.kind !== 'ObjectMethod'
) {
continue;
}
const loweredFunction = value.loweredFunc.func;
for (const effect of loweredFunction.effects ?? []) {
if (effect.kind !== 'CaptureEffect') {
continue;
}
aliases.union([...effect.places].map(place => place.identifier));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,45 +95,58 @@ function inheritFunctionEffects(

return effects
.flatMap(effect => {
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
return [effect];
} else {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
switch (effect.kind) {
case 'GlobalMutation':
case 'ReactMutation': {
return [effect];
}
case 'ContextMutation': {
const effects: Array<FunctionEffect | null> = [];
CompilerError.invariant(effect.kind === 'ContextMutation', {
reason: 'Expected ContextMutation',
loc: null,
});
/**
* Contextual effects need to be replayed against the current inference
* state, which may know more about the value to which the effect applied.
* The main cases are:
* 1. The mutated context value is _still_ a context value in the current scope,
* so we have to continue propagating the original context mutation.
* 2. The mutated context value is a mutable value in the current scope,
* so the context mutation was fine and we can skip propagating the effect.
* 3. The mutated context value is an immutable value in the current scope,
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
* more detailed effect to the current function context.
*/
for (const place of effect.places) {
if (state.isDefined(place)) {
const replayedEffect = inferOperandEffect(state, {
...place,
loc: effect.loc,
effect: effect.effect,
});
if (replayedEffect != null) {
if (replayedEffect.kind === 'ContextMutation') {
// Case 1, still a context value so propagate the original effect
effects.push(effect);
} else {
// Case 3, immutable value so propagate the more precise effect
effects.push(replayedEffect);
}
} // else case 2, local mutable value so this effect was fine
}
}
return effects;
}
case 'CaptureEffect': {
return [];
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
Comment on lines +141 to 149
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactoring to a switch (no changes to the existing cases) and adding the new case

return effects;
}
})
.filter((effect): effect is FunctionEffect => effect != null);
Expand Down Expand Up @@ -298,26 +311,31 @@ export function inferTerminalFunctionEffects(
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
return functionEffects
.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
};
}
case 'CaptureEffect': {
return null;
}
Comment on lines +328 to +330
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding the new case

default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
default:
assertExhaustive(
eff,
`Unexpected function effect kind \`${(eff as any).kind}\``,
);
}
});
})
.filter(eff => eff != null) as Array<CompilerErrorDetailOptions>;
}

function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
Expand Down
Loading
Loading