Skip to content

Commit d49c707

Browse files
committed
[compiler] New inference repros/fixes
Adds a mechanism where if we can't determine a precise set of aliasing (data flow) effects for a function signature, to fall back to treating function expressions as general-purpose mutable values. Concretely: * `InferMutationAliasingEffects` has an optimization where if you're `Apply`-ing a specific, known function expression, that we use the inferred signature for that function. Now we only use this mode _if_ we could infer a precise signature. * `InferFunctionExpressionAliasingSignature` now bails out if there is a phi with a backedge, since we weren't handling this case. See the repro for this case: i confirmed that it was previously incorrect. Also adds a repro for a case of an invalid array length, which exposes the fact that InferFunctionExpressionAliasingSignatures is accumulating aliases w/o reducing them in any way. A complex enough function expression can lead to the accumulated array of effects overflowing the max array size. Fix in the next PR.
1 parent 2bee348 commit d49c707

9 files changed

+264
-108
lines changed

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

Lines changed: 90 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,10 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
2020
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
2121
import {inferMutableRanges} from './InferMutableRanges';
2222
import inferReferenceEffects from './InferReferenceEffects';
23-
import {assertExhaustive, retainWhere} from '../Utils/utils';
23+
import {assertExhaustive} from '../Utils/utils';
2424
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
2525
import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature';
2626
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
27-
import {hashEffect} from './AliasingEffects';
2827

2928
export default function analyseFunctions(func: HIRFunction): void {
3029
for (const [_, block] of func.body.blocks) {
@@ -69,89 +68,103 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
6968
analyseFunctions(fn);
7069
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
7170
deadCodeElimination(fn);
72-
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
71+
const mutationEffects = inferMutationAliasingRanges(fn, {
72+
isFunctionExpression: true,
73+
}).unwrap();
7374
rewriteInstructionKindsBasedOnReassignment(fn);
7475
inferReactiveScopeVariables(fn);
75-
const effects = inferFunctionExpressionAliasingEffectsSignature(fn);
76-
fn.env.logger?.debugLogIRs?.({
77-
kind: 'hir',
78-
name: 'AnalyseFunction (inner)',
79-
value: fn,
80-
});
81-
if (effects != null) {
82-
fn.aliasingEffects ??= [];
83-
fn.aliasingEffects?.push(...effects);
84-
}
85-
if (fn.aliasingEffects != null) {
86-
const seen = new Set<string>();
87-
retainWhere(fn.aliasingEffects, effect => {
88-
const hash = hashEffect(effect);
89-
if (seen.has(hash)) {
90-
return false;
91-
}
92-
seen.add(hash);
93-
return true;
94-
});
95-
}
76+
const aliasingEffects = inferFunctionExpressionAliasingEffectsSignature(fn);
77+
if (aliasingEffects == null) {
78+
/**
79+
* If the data flow within the function expression is too complex to resolve,
80+
* we null out the function expressions' alisingEffects. This ensures that
81+
* InferMutationAliasingEffects at the outer level will fall back to a more
82+
* conservative of the function as a plain mutable value — see the handling of
83+
* 'Apply' in applyEffect(). As part of this we also set all context variables
84+
* to Capture, which will downgrade if they turn out to be primitives, frozen,
85+
* globals, etc.
86+
*/
87+
fn.aliasingEffects = null;
88+
for (const operand of fn.context) {
89+
operand.effect = Effect.Capture;
90+
}
91+
} else {
92+
/**
93+
* We were able to determine a precise set of mutation and aliasing (data flow)
94+
* effects for the function. Annotate which context variables are Capture based
95+
* on which are mutated and/or captured into some other value. Capture is used
96+
* as a signal to InferMutationAliasingEffects (at the outer function level) that
97+
* the function itself has to be treated as mutable. See 'CreateFunction' handling
98+
* in applyEffect().
99+
*/
100+
const functionEffects = [...mutationEffects, ...aliasingEffects];
101+
fn.aliasingEffects = functionEffects;
96102

97-
/**
98-
* Phase 2: populate the Effect of each context variable to use in inferring
99-
* the outer function. For example, InferMutationAliasingEffects uses context variable
100-
* effects to decide if the function may be mutable or not.
101-
*/
102-
const capturedOrMutated = new Set<IdentifierId>();
103-
for (const effect of effects ?? []) {
104-
switch (effect.kind) {
105-
case 'Assign':
106-
case 'Alias':
107-
case 'Capture':
108-
case 'CreateFrom': {
109-
capturedOrMutated.add(effect.from.identifier.id);
110-
break;
111-
}
112-
case 'Apply': {
113-
CompilerError.invariant(false, {
114-
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
115-
loc: effect.function.loc,
116-
});
117-
}
118-
case 'Mutate':
119-
case 'MutateConditionally':
120-
case 'MutateTransitive':
121-
case 'MutateTransitiveConditionally': {
122-
capturedOrMutated.add(effect.value.identifier.id);
123-
break;
124-
}
125-
case 'Impure':
126-
case 'Render':
127-
case 'MutateFrozen':
128-
case 'MutateGlobal':
129-
case 'CreateFunction':
130-
case 'Create':
131-
case 'Freeze':
132-
case 'ImmutableCapture': {
133-
// no-op
134-
break;
135-
}
136-
default: {
137-
assertExhaustive(
138-
effect,
139-
`Unexpected effect kind ${(effect as any).kind}`,
140-
);
103+
/**
104+
* Phase 2: populate the Effect of each context variable to use in inferring
105+
* the outer function. For example, InferMutationAliasingEffects uses context variable
106+
* effects to decide if the function may be mutable or not.
107+
*/
108+
const capturedOrMutated = new Set<IdentifierId>();
109+
for (const effect of functionEffects) {
110+
switch (effect.kind) {
111+
case 'Assign':
112+
case 'Alias':
113+
case 'Capture':
114+
case 'CreateFrom': {
115+
capturedOrMutated.add(effect.from.identifier.id);
116+
break;
117+
}
118+
case 'Apply': {
119+
CompilerError.invariant(false, {
120+
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
121+
loc: effect.function.loc,
122+
});
123+
}
124+
case 'Mutate':
125+
case 'MutateConditionally':
126+
case 'MutateTransitive':
127+
case 'MutateTransitiveConditionally': {
128+
capturedOrMutated.add(effect.value.identifier.id);
129+
break;
130+
}
131+
case 'Impure':
132+
case 'Render':
133+
case 'MutateFrozen':
134+
case 'MutateGlobal':
135+
case 'CreateFunction':
136+
case 'Create':
137+
case 'Freeze':
138+
case 'ImmutableCapture': {
139+
// no-op
140+
break;
141+
}
142+
default: {
143+
assertExhaustive(
144+
effect,
145+
`Unexpected effect kind ${(effect as any).kind}`,
146+
);
147+
}
141148
}
142149
}
143-
}
144150

145-
for (const operand of fn.context) {
146-
if (
147-
capturedOrMutated.has(operand.identifier.id) ||
148-
operand.effect === Effect.Capture
149-
) {
150-
operand.effect = Effect.Capture;
151-
} else {
152-
operand.effect = Effect.Read;
151+
for (const operand of fn.context) {
152+
if (
153+
capturedOrMutated.has(operand.identifier.id) ||
154+
operand.effect === Effect.Capture
155+
) {
156+
operand.effect = Effect.Capture;
157+
} else {
158+
operand.effect = Effect.Read;
159+
}
153160
}
154161
}
162+
163+
fn.env.logger?.debugLogIRs?.({
164+
kind: 'hir',
165+
name: 'AnalyseFunction (inner)',
166+
value: fn,
167+
});
155168
}
156169

157170
function lower(func: HIRFunction): void {

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
9-
import {getOrInsertDefault} from '../Utils/utils';
10-
import {AliasingEffect} from './AliasingEffects';
8+
import {
9+
BlockId,
10+
HIRFunction,
11+
IdentifierId,
12+
Place,
13+
ValueKind,
14+
ValueReason,
15+
} from '../HIR';
16+
import {getOrInsertDefault, retainWhere} from '../Utils/utils';
17+
import {AliasingEffect, hashEffect} from './AliasingEffects';
1118

1219
/**
1320
* This function tracks data flow within an inner function expression in order to
@@ -77,11 +84,14 @@ export function inferFunctionExpressionAliasingEffectsSignature(
7784
);
7885
}
7986

80-
// todo: fixpoint
87+
const seenBlocks = new Set<BlockId>();
8188
for (const block of fn.body.blocks.values()) {
8289
for (const phi of block.phis) {
8390
const operands: Array<AliasedIdentifier> = [];
84-
for (const operand of phi.operands.values()) {
91+
for (const [pred, operand] of phi.operands) {
92+
if (!seenBlocks.has(pred)) {
93+
return null;
94+
}
8595
const inputs = lookup(operand, 'Alias');
8696
if (inputs != null) {
8797
operands.push(...inputs);
@@ -91,6 +101,7 @@ export function inferFunctionExpressionAliasingEffectsSignature(
91101
dataFlow.set(phi.place.identifier.id, operands);
92102
}
93103
}
104+
seenBlocks.add(block.id);
94105
for (const instr of block.instructions) {
95106
if (instr.effects == null) continue;
96107
for (const effect of instr.effects) {
@@ -182,15 +193,18 @@ export function inferFunctionExpressionAliasingEffectsSignature(
182193
});
183194
}
184195

196+
const seen = new Set<string>();
197+
retainWhere(effects, effect => {
198+
const hash = hashEffect(effect);
199+
if (seen.has(hash)) {
200+
return false;
201+
}
202+
seen.add(hash);
203+
return true;
204+
});
185205
return effects;
186206
}
187207

188-
export enum MutationKind {
189-
None = 0,
190-
Conditional = 1,
191-
Definite = 2,
192-
}
193-
194208
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
195209
function joinEffects(
196210
effect1: AliasingKind,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,8 @@ function applyEffect(
822822
const functionValues = state.values(effect.function);
823823
if (
824824
functionValues.length === 1 &&
825-
functionValues[0].kind === 'FunctionExpression'
825+
functionValues[0].kind === 'FunctionExpression' &&
826+
functionValues[0].loweredFunc.func.aliasingEffects != null
826827
) {
827828
/*
828829
* We're calling a locally declared function, we already know it's effects!

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
eachTerminalOperand,
2323
} from '../HIR/visitors';
2424
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
25-
import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature';
26-
import {Result} from '../Utils/Result';
25+
import {Err, Ok, Result} from '../Utils/Result';
26+
import {AliasingEffect} from './AliasingEffects';
2727

2828
/**
2929
* Infers mutable ranges for all values in the program, using previously inferred
@@ -49,7 +49,7 @@ import {Result} from '../Utils/Result';
4949
export function inferMutationAliasingRanges(
5050
fn: HIRFunction,
5151
{isFunctionExpression}: {isFunctionExpression: boolean},
52-
): Result<void, CompilerError> {
52+
): Result<Array<AliasingEffect>, CompilerError> {
5353
/**
5454
* Part 1: Infer mutable ranges for values. We build an abstract model of
5555
* values, the alias/capture edges between them, and the set of mutations.
@@ -215,7 +215,7 @@ export function inferMutationAliasingRanges(
215215
for (const render of renders) {
216216
state.render(render.index, render.place.identifier, errors);
217217
}
218-
fn.aliasingEffects ??= [];
218+
const functionEffects: Array<AliasingEffect> = [];
219219
for (const param of [...fn.context, ...fn.params]) {
220220
const place = param.kind === 'Identifier' ? param : param.place;
221221
const node = state.nodes.get(place.identifier);
@@ -226,13 +226,13 @@ export function inferMutationAliasingRanges(
226226
if (node.local != null) {
227227
if (node.local.kind === MutationKind.Conditional) {
228228
mutated = true;
229-
fn.aliasingEffects.push({
229+
functionEffects.push({
230230
kind: 'MutateConditionally',
231231
value: {...place, loc: node.local.loc},
232232
});
233233
} else if (node.local.kind === MutationKind.Definite) {
234234
mutated = true;
235-
fn.aliasingEffects.push({
235+
functionEffects.push({
236236
kind: 'Mutate',
237237
value: {...place, loc: node.local.loc},
238238
});
@@ -241,13 +241,13 @@ export function inferMutationAliasingRanges(
241241
if (node.transitive != null) {
242242
if (node.transitive.kind === MutationKind.Conditional) {
243243
mutated = true;
244-
fn.aliasingEffects.push({
244+
functionEffects.push({
245245
kind: 'MutateTransitiveConditionally',
246246
value: {...place, loc: node.transitive.loc},
247247
});
248248
} else if (node.transitive.kind === MutationKind.Definite) {
249249
mutated = true;
250-
fn.aliasingEffects.push({
250+
functionEffects.push({
251251
kind: 'MutateTransitive',
252252
value: {...place, loc: node.transitive.loc},
253253
});
@@ -436,7 +436,9 @@ export function inferMutationAliasingRanges(
436436
}
437437
}
438438

439-
return errors.asResult();
439+
return errors.hasErrors() && !isFunctionExpression
440+
? Err(errors)
441+
: Ok(functionEffects);
440442
}
441443

442444
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
@@ -452,6 +454,12 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
452454
}
453455
}
454456

457+
export enum MutationKind {
458+
None = 0,
459+
Conditional = 1,
460+
Definite = 2,
461+
}
462+
455463
type Node = {
456464
id: Identifier;
457465
createdFrom: Map<Identifier, number>;

compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -514,9 +514,9 @@ Intuition: these effects are inverses of each other (capturing into an object, e
514514
Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
515515

516516
```js
517-
const y = [x]; // capture
518-
const z = y[0]; // createfrom
519-
mutate(z); // this clearly can mutate x, so the result must be one of Assign/Alias/CreateFrom
517+
const b = [a]; // capture
518+
const c = b[0]; // createfrom
519+
mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
520520
```
521521

522522
We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
@@ -528,17 +528,17 @@ CreateFrom c <- b
528528
Alias c <- a
529529
```
530530

531-
Meanwhile the opposite direction preservers the capture, because the result is not the same as the source:
531+
Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
532532

533533
```js
534-
const y = x[0]; // createfrom
535-
const z = [y]; // capture
536-
mutate(z); // does not mutate x, so the result must be Capture
534+
const b = a[0]; // createfrom
535+
const c = [b]; // capture
536+
mutate(c); // does not mutate a, so the result must be Capture
537537
```
538538

539539
```
540-
Capture b <- a
541-
CreateFrom c <- b
540+
CreateFrom b <- a
541+
Capture c <- b
542542
=>
543-
Capture b <- a
543+
Capture c <- a
544544
```

0 commit comments

Comments
 (0)