Skip to content

Commit 7bf2cc9

Browse files
committed
[compiler] Propagate CreateFunction effects for functions that return functions
If you have a local helper function that itself returns a function (`() => () => { ... }`), we currently infer the return effect of the outer function as `Create mutable`. We correctly track the aliasing, but we lose some precision because we don't understand that a function specifically is being returned. Here, we do some extra analysis of which values are returned in InferMutationAliasingRanges, and if the sole return value is a function we infer a `CreateFunction` effect. We also infer an `Assign` (instead of a Create) if the sole return value was one of the context variables or parameters.
1 parent 7398334 commit 7bf2cc9

File tree

3 files changed

+168
-35
lines changed

3 files changed

+168
-35
lines changed

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2509,10 +2509,47 @@ function computeEffectsForSignature(
25092509
break;
25102510
}
25112511
case 'CreateFunction': {
2512-
CompilerError.throwTodo({
2513-
reason: `Support CreateFrom effects in signatures`,
2514-
loc: receiver.loc,
2512+
const applyInto = substitutions.get(effect.into.identifier.id);
2513+
if (applyInto == null || applyInto.length !== 1) {
2514+
return null;
2515+
}
2516+
const captures: Array<Place> = [];
2517+
for (let i = 0; i < effect.captures.length; i++) {
2518+
const substitution = substitutions.get(
2519+
effect.captures[i].identifier.id,
2520+
);
2521+
if (substitution == null || substitution.length !== 1) {
2522+
return null;
2523+
}
2524+
captures.push(substitution[0]);
2525+
}
2526+
const context: Array<Place> = [];
2527+
const originalContext = effect.function.loweredFunc.func.context;
2528+
for (let i = 0; i < originalContext.length; i++) {
2529+
const substitution = substitutions.get(
2530+
originalContext[i].identifier.id,
2531+
);
2532+
if (substitution == null || substitution.length !== 1) {
2533+
return null;
2534+
}
2535+
context.push(substitution[0]);
2536+
}
2537+
effects.push({
2538+
kind: 'CreateFunction',
2539+
into: applyInto[0],
2540+
function: {
2541+
...effect.function,
2542+
loweredFunc: {
2543+
...effect.function.loweredFunc,
2544+
func: {
2545+
...effect.function.loweredFunc.func,
2546+
context,
2547+
},
2548+
},
2549+
},
2550+
captures,
25152551
});
2552+
break;
25162553
}
25172554
default: {
25182555
assertExhaustive(

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

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function inferMutationAliasingRanges(
140140
} else if (effect.kind === 'CreateFunction') {
141141
state.create(effect.into, {
142142
kind: 'Function',
143-
function: effect.function.loweredFunc.func,
143+
effect,
144144
});
145145
} else if (effect.kind === 'CreateFrom') {
146146
state.createFrom(index++, effect.from, effect.into);
@@ -155,7 +155,7 @@ export function inferMutationAliasingRanges(
155155
* invariant here.
156156
*/
157157
if (!state.nodes.has(effect.into.identifier)) {
158-
state.create(effect.into, {kind: 'Object'});
158+
state.create(effect.into, {kind: 'Assign'});
159159
}
160160
state.assign(index++, effect.from, effect.into);
161161
} else if (effect.kind === 'Alias') {
@@ -469,35 +469,112 @@ export function inferMutationAliasingRanges(
469469
}
470470
}
471471

472+
const tracked: Array<Place> = [];
473+
for (const param of [...fn.params, ...fn.context, fn.returns]) {
474+
const place = param.kind === 'Identifier' ? param : param.place;
475+
tracked.push(place);
476+
}
477+
478+
const returned: Set<Node> = new Set();
479+
const queue: Array<Node> = [state.nodes.get(fn.returns.identifier)!];
480+
const seen: Set<Node> = new Set();
481+
while (queue.length !== 0) {
482+
const node = queue.pop()!;
483+
if (seen.has(node)) {
484+
continue;
485+
}
486+
seen.add(node);
487+
for (const id of node.aliases.keys()) {
488+
queue.push(state.nodes.get(id)!);
489+
}
490+
for (const id of node.createdFrom.keys()) {
491+
queue.push(state.nodes.get(id)!);
492+
}
493+
if (node.id.id === fn.returns.identifier.id) {
494+
continue;
495+
}
496+
switch (node.value.kind) {
497+
case 'Assign':
498+
case 'CreateFrom': {
499+
break;
500+
}
501+
case 'Phi':
502+
case 'Object':
503+
case 'Function': {
504+
returned.add(node);
505+
break;
506+
}
507+
default: {
508+
assertExhaustive(
509+
node.value,
510+
`Unexpected node value kind '${(node.value as any).kind}'`,
511+
);
512+
}
513+
}
514+
}
515+
const returnedValues = [...returned];
516+
if (
517+
returnedValues.length === 1 &&
518+
returnedValues[0].value.kind === 'Object' &&
519+
tracked.some(place => place.identifier.id === returnedValues[0].id.id)
520+
) {
521+
const from = tracked.find(
522+
place => place.identifier.id === returnedValues[0].id.id,
523+
)!;
524+
functionEffects.push({
525+
kind: 'Assign',
526+
from,
527+
into: fn.returns,
528+
});
529+
} else if (
530+
returnedValues.length === 1 &&
531+
returnedValues[0].value.kind === 'Function'
532+
) {
533+
const outerContext = new Set(fn.context.map(p => p.identifier.id));
534+
const effect = returnedValues[0].value.effect;
535+
functionEffects.push({
536+
kind: 'CreateFunction',
537+
function: {
538+
...effect.function,
539+
loweredFunc: {
540+
func: {
541+
...effect.function.loweredFunc.func,
542+
context: effect.function.loweredFunc.func.context.filter(p =>
543+
outerContext.has(p.identifier.id),
544+
),
545+
},
546+
},
547+
},
548+
captures: effect.captures.filter(p => outerContext.has(p.identifier.id)),
549+
into: fn.returns,
550+
});
551+
} else {
552+
const returns = fn.returns.identifier;
553+
functionEffects.push({
554+
kind: 'Create',
555+
into: fn.returns,
556+
value: isPrimitiveType(returns)
557+
? ValueKind.Primitive
558+
: isJsxType(returns.type)
559+
? ValueKind.Frozen
560+
: ValueKind.Mutable,
561+
reason: ValueReason.KnownReturnSignature,
562+
});
563+
}
564+
472565
/**
473566
* Part 3
474567
* Finish populating the externally visible effects. Above we bubble-up the side effects
475568
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
476569
* Here we populate an effect to create the return value as well as populating alias/capture
477570
* effects for how data flows between the params, context vars, and return.
478571
*/
479-
const returns = fn.returns.identifier;
480-
functionEffects.push({
481-
kind: 'Create',
482-
into: fn.returns,
483-
value: isPrimitiveType(returns)
484-
? ValueKind.Primitive
485-
: isJsxType(returns.type)
486-
? ValueKind.Frozen
487-
: ValueKind.Mutable,
488-
reason: ValueReason.KnownReturnSignature,
489-
});
490572
/**
491573
* Determine precise data-flow effects by simulating transitive mutations of the params/
492574
* captures and seeing what other params/context variables are affected. Anything that
493575
* would be transitively mutated needs a capture relationship.
494576
*/
495-
const tracked: Array<Place> = [];
496577
const ignoredErrors = new CompilerError();
497-
for (const param of [...fn.params, ...fn.context, fn.returns]) {
498-
const place = param.kind === 'Identifier' ? param : param.place;
499-
tracked.push(place);
500-
}
501578
for (const into of tracked) {
502579
const mutationIndex = index++;
503580
state.mutate(
@@ -581,9 +658,14 @@ type Node = {
581658
local: {kind: MutationKind; loc: SourceLocation} | null;
582659
lastMutated: number;
583660
value:
661+
| {kind: 'Assign'}
662+
| {kind: 'CreateFrom'}
584663
| {kind: 'Object'}
585664
| {kind: 'Phi'}
586-
| {kind: 'Function'; function: HIRFunction};
665+
| {
666+
kind: 'Function';
667+
effect: Extract<AliasingEffect, {kind: 'CreateFunction'}>;
668+
};
587669
};
588670
class AliasingState {
589671
nodes: Map<Identifier, Node> = new Map();
@@ -604,7 +686,7 @@ class AliasingState {
604686
}
605687

606688
createFrom(index: number, from: Place, into: Place): void {
607-
this.create(into, {kind: 'Object'});
689+
this.create(into, {kind: 'CreateFrom'});
608690
const fromNode = this.nodes.get(from.identifier);
609691
const toNode = this.nodes.get(into.identifier);
610692
if (fromNode == null || toNode == null) {
@@ -666,7 +748,10 @@ class AliasingState {
666748
continue;
667749
}
668750
if (node.value.kind === 'Function') {
669-
appendFunctionErrors(errors, node.value.function);
751+
appendFunctionErrors(
752+
errors,
753+
node.value.effect.function.loweredFunc.func,
754+
);
670755
}
671756
for (const [alias, when] of node.createdFrom) {
672757
if (when >= index) {
@@ -728,7 +813,10 @@ class AliasingState {
728813
node.transitive == null &&
729814
node.local == null
730815
) {
731-
appendFunctionErrors(errors, node.value.function);
816+
appendFunctionErrors(
817+
errors,
818+
node.value.effect.function.loweredFunc.func,
819+
);
732820
}
733821
if (transitive) {
734822
if (node.transitive == null || node.transitive.kind < kind) {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { makeArray, Stringify, useIdentity } from "shared-runtime";
5555
*/
5656
function Foo(t0) {
5757
"use memo";
58-
const $ = _c(3);
58+
const $ = _c(5);
5959
const { b } = t0;
6060

6161
const fnFactory = () => () => {
@@ -66,18 +66,26 @@ function Foo(t0) {
6666
useIdentity();
6767

6868
const fn = fnFactory();
69-
const arr = makeArray(b);
70-
fn(arr);
7169
let t1;
72-
if ($[0] !== arr || $[1] !== myVar) {
73-
t1 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
74-
$[0] = arr;
75-
$[1] = myVar;
76-
$[2] = t1;
70+
if ($[0] !== b) {
71+
t1 = makeArray(b);
72+
$[0] = b;
73+
$[1] = t1;
74+
} else {
75+
t1 = $[1];
76+
}
77+
const arr = t1;
78+
fn(arr);
79+
let t2;
80+
if ($[2] !== arr || $[3] !== myVar) {
81+
t2 = <Stringify cb={myVar} value={arr} shouldInvokeFns={true} />;
82+
$[2] = arr;
83+
$[3] = myVar;
84+
$[4] = t2;
7785
} else {
78-
t1 = $[2];
86+
t2 = $[4];
7987
}
80-
return t1;
88+
return t2;
8189
}
8290
function _temp2() {
8391
return console.log("b");

0 commit comments

Comments
 (0)