Skip to content

Commit 85c1ee3

Browse files
committed
[compiler] Translate legacy FunctionSignature into new AliasingEffects
To help bootstrap the new inference model, this PR adds a helper that takes a legacy FunctionSignature and converts into a list of (new) AliasingEffects. This conversion tries to make explicit all the implicit handling of InferReferenceEffects and previous FunctionSignature. For example, the signature for Array.proto.pop has a calleeEffect of `Store`. Nowhere does it say that the receiver flows into the result! There's an implicit behavior that the receiver flows into the result. The new function makes this explicit by emitting a `Capture receiver -> lvalue` effect. So far I confirmed that this works for Array.proto.push() if i hard code the inference to ignore new-style aliasing signatures. I'll continue to refine it going forward as I start running the new inference on more fixtures. ghstack-source-id: ba2fec0 Pull Request resolved: #33371
1 parent 5cc872c commit 85c1ee3

File tree

1 file changed

+173
-3
lines changed

1 file changed

+173
-3
lines changed

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

Lines changed: 173 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
printPlace,
4646
printSourceLocation,
4747
} from '../HIR/PrintHIR';
48+
import {FunctionSignature} from '../HIR/ObjectShape';
4849

4950
export function inferMutationAliasingEffects(
5051
fn: HIRFunction,
@@ -233,6 +234,18 @@ function applySignature(
233234
instruction: Instruction,
234235
effectInstructionValueCache: Map<AliasingEffect, InstructionValue>,
235236
): Array<AliasingEffect> | null {
237+
/*
238+
* TODO: this should look for FunctionExpression instructions, and check for any
239+
* obviously invalid effects. for example, a known mutation of props is always
240+
* invalid even if the function might not be called
241+
*/
242+
243+
/*
244+
* Track which values we've already aliased once, so that we can switch to
245+
* appendAlias() for subsequent aliases into the same value
246+
*/
247+
const aliased = new Set<IdentifierId>();
248+
236249
const effects: Array<AliasingEffect> = [];
237250
for (const effect of signature.effects) {
238251
switch (effect.kind) {
@@ -407,7 +420,12 @@ function applySignature(
407420
break;
408421
}
409422
default: {
410-
state.alias(effect.into, effect.from);
423+
if (aliased.has(effect.into.identifier.id)) {
424+
state.appendAlias(effect.into, effect.from);
425+
} else {
426+
aliased.add(effect.into.identifier.id);
427+
state.alias(effect.into, effect.from);
428+
}
411429
effects.push(effect);
412430
break;
413431
}
@@ -564,6 +582,21 @@ class InferenceState {
564582
this.#variables.set(place.identifier.id, new Set(values));
565583
}
566584

585+
appendAlias(place: Place, value: Place): void {
586+
const values = this.#variables.get(value.identifier.id);
587+
CompilerError.invariant(values != null, {
588+
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
589+
description: `${printIdentifier(value.identifier)}`,
590+
loc: value.loc,
591+
suggestions: null,
592+
});
593+
const prevValues = this.values(place);
594+
this.#variables.set(
595+
place.identifier.id,
596+
new Set([...prevValues, ...values]),
597+
);
598+
}
599+
567600
// Defines (initializing or updating) a variable with a specific kind of value.
568601
define(place: Place, value: InstructionValue): void {
569602
CompilerError.invariant(this.#values.has(value), {
@@ -650,10 +683,13 @@ class InferenceState {
650683
case 'MutateTransitive': {
651684
switch (kind) {
652685
case ValueKind.Mutable:
653-
case ValueKind.Primitive:
654686
case ValueKind.Context: {
655687
return true;
656688
}
689+
case ValueKind.Primitive: {
690+
// technically an error, but it's not React specific
691+
return false;
692+
}
657693
default: {
658694
// TODO this is an error!
659695
return false;
@@ -952,6 +988,15 @@ function computeSignatureForInstruction(
952988
: null;
953989
if (signatureEffects != null && signature?.aliasing != null) {
954990
effects.push(...signatureEffects);
991+
} else if (signature != null) {
992+
effects.push(
993+
...computeEffectsForLegacySignature(
994+
signature,
995+
lvalue,
996+
receiver,
997+
value.args,
998+
),
999+
);
9551000
} else {
9561001
effects.push({kind: 'Create', into: lvalue, value: ValueKind.Mutable});
9571002
/**
@@ -1054,10 +1099,13 @@ function computeSignatureForInstruction(
10541099
*
10551100
* ```
10561101
* const a = {};
1057-
* // We don't want to consider a as mutating here either, this just declares the function
1102+
*
1103+
* // We don't want to consider a as mutating here, this just declares the function
10581104
* const f = () => { maybeMutate(a) };
1105+
*
10591106
* // We don't want to consider a as mutating here either, it can't possibly call f yet
10601107
* const x = [f];
1108+
*
10611109
* // Here we have to assume that f can be called (transitively), and have to consider a
10621110
* // as mutating
10631111
* callAllFunctionInArray(x);
@@ -1068,6 +1116,12 @@ function computeSignatureForInstruction(
10681116
* that those operands are also considered mutated. If the function is never called,
10691117
* they won't be!
10701118
*
1119+
* This relies on the rule that:
1120+
* Capture a -> b and MutateTransitive(b) => Mutate(a)
1121+
*
1122+
* Substituting:
1123+
* Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar)
1124+
*
10711125
* Note that if the type of the context variables are frozen, global, or primitive, the
10721126
* Capture will either get pruned or downgraded to an ImmutableCapture.
10731127
*/
@@ -1259,6 +1313,122 @@ function computeSignatureForInstruction(
12591313
};
12601314
}
12611315

1316+
/**
1317+
* Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the
1318+
* old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments
1319+
* in the body for details.
1320+
*
1321+
* The goal of this method is to make it easier to migrate incrementally to the new system,
1322+
* so we don't have to immediately write new signatures for all the methods to get expected
1323+
* compilation output.
1324+
*/
1325+
function computeEffectsForLegacySignature(
1326+
signature: FunctionSignature,
1327+
lvalue: Place,
1328+
receiver: Place,
1329+
args: Array<Place | SpreadPattern>,
1330+
): Array<AliasingEffect> {
1331+
const effects: Array<AliasingEffect> = [];
1332+
effects.push({
1333+
kind: 'Create',
1334+
into: lvalue,
1335+
value: signature.returnValueKind,
1336+
});
1337+
/*
1338+
* InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver
1339+
* is captured into the return value. Consider for example the signature for Array.proto.pop:
1340+
* the calleeEffect is Store, since it's a known mutation but non-transitive. But the return
1341+
* of the pop() captures from the receiver! This isn't specified explicitly. So we add this
1342+
* here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if
1343+
* the type doesn't actually need to be captured based on the input and return type.
1344+
*/
1345+
effects.push({
1346+
kind: 'Capture',
1347+
from: receiver,
1348+
into: lvalue,
1349+
});
1350+
const stores: Array<Place> = [];
1351+
const captures: Array<Place> = [];
1352+
const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
1353+
function visit(place: Place, effect: Effect): void {
1354+
switch (effect) {
1355+
case Effect.Store: {
1356+
effects.push({
1357+
kind: 'Mutate',
1358+
value: place,
1359+
});
1360+
stores.push(place);
1361+
break;
1362+
}
1363+
case Effect.Capture: {
1364+
captures.push(place);
1365+
break;
1366+
}
1367+
case Effect.ConditionallyMutate: {
1368+
effects.push({
1369+
kind: 'MutateTransitiveConditionally',
1370+
value: place,
1371+
});
1372+
break;
1373+
}
1374+
case Effect.ConditionallyMutateIterator: {
1375+
// We don't currently use this effect in any signatures
1376+
CompilerError.throwTodo({
1377+
reason: `Unexpected ${effect} effect in a legacy function signature`,
1378+
loc: place.loc,
1379+
});
1380+
}
1381+
case Effect.Freeze: {
1382+
effects.push({
1383+
kind: 'Freeze',
1384+
value: place,
1385+
reason: returnValueReason,
1386+
});
1387+
break;
1388+
}
1389+
case Effect.Mutate: {
1390+
effects.push({kind: 'MutateTransitive', value: place});
1391+
break;
1392+
}
1393+
case Effect.Read: {
1394+
effects.push({
1395+
kind: 'ImmutableCapture',
1396+
from: place,
1397+
into: lvalue,
1398+
});
1399+
break;
1400+
}
1401+
}
1402+
}
1403+
1404+
visit(receiver, signature.calleeEffect);
1405+
for (let i = 0; i < args.length; i++) {
1406+
const arg = args[i];
1407+
const place = arg.kind === 'Identifier' ? arg : arg.place;
1408+
const effect =
1409+
arg.kind === 'Identifier' && i < signature.positionalParams.length
1410+
? signature.positionalParams[i]!
1411+
: (signature.restParam ?? Effect.ConditionallyMutate);
1412+
visit(place, effect);
1413+
}
1414+
if (captures.length !== 0) {
1415+
if (stores.length === 0) {
1416+
// If no stores, then capture into the return value
1417+
for (const capture of captures) {
1418+
effects.push({kind: 'Capture', from: capture, into: lvalue});
1419+
}
1420+
} else {
1421+
// Else capture into the stores
1422+
for (const capture of captures) {
1423+
for (const store of stores) {
1424+
effects.push({kind: 'Capture', from: capture, into: store});
1425+
}
1426+
}
1427+
}
1428+
}
1429+
return effects;
1430+
}
1431+
12621432
function computeEffectsForSignature(
12631433
signature: AliasingSignature,
12641434
lvalue: Place,

0 commit comments

Comments
 (0)