Skip to content

Commit 87dc463

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 2de9dda commit 87dc463

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
@@ -2470,10 +2470,47 @@ function computeEffectsForSignature(
24702470
break;
24712471
}
24722472
case 'CreateFunction': {
2473-
CompilerError.throwTodo({
2474-
reason: `Support CreateFrom effects in signatures`,
2475-
loc: receiver.loc,
2473+
const applyInto = substitutions.get(effect.into.identifier.id);
2474+
if (applyInto == null || applyInto.length !== 1) {
2475+
return null;
2476+
}
2477+
const captures: Array<Place> = [];
2478+
for (let i = 0; i < effect.captures.length; i++) {
2479+
const substitution = substitutions.get(
2480+
effect.captures[i].identifier.id,
2481+
);
2482+
if (substitution == null || substitution.length !== 1) {
2483+
return null;
2484+
}
2485+
captures.push(substitution[0]);
2486+
}
2487+
const context: Array<Place> = [];
2488+
const originalContext = effect.function.loweredFunc.func.context;
2489+
for (let i = 0; i < originalContext.length; i++) {
2490+
const substitution = substitutions.get(
2491+
originalContext[i].identifier.id,
2492+
);
2493+
if (substitution == null || substitution.length !== 1) {
2494+
return null;
2495+
}
2496+
context.push(substitution[0]);
2497+
}
2498+
effects.push({
2499+
kind: 'CreateFunction',
2500+
into: applyInto[0],
2501+
function: {
2502+
...effect.function,
2503+
loweredFunc: {
2504+
...effect.function.loweredFunc,
2505+
func: {
2506+
...effect.function.loweredFunc.func,
2507+
context,
2508+
},
2509+
},
2510+
},
2511+
captures,
24762512
});
2513+
break;
24772514
}
24782515
default: {
24792516
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') {
@@ -465,35 +465,112 @@ export function inferMutationAliasingRanges(
465465
}
466466
}
467467

468+
const tracked: Array<Place> = [];
469+
for (const param of [...fn.params, ...fn.context, fn.returns]) {
470+
const place = param.kind === 'Identifier' ? param : param.place;
471+
tracked.push(place);
472+
}
473+
474+
const returned: Set<Node> = new Set();
475+
const queue: Array<Node> = [state.nodes.get(fn.returns.identifier)!];
476+
const seen: Set<Node> = new Set();
477+
while (queue.length !== 0) {
478+
const node = queue.pop()!;
479+
if (seen.has(node)) {
480+
continue;
481+
}
482+
seen.add(node);
483+
for (const id of node.aliases.keys()) {
484+
queue.push(state.nodes.get(id)!);
485+
}
486+
for (const id of node.createdFrom.keys()) {
487+
queue.push(state.nodes.get(id)!);
488+
}
489+
if (node.id.id === fn.returns.identifier.id) {
490+
continue;
491+
}
492+
switch (node.value.kind) {
493+
case 'Assign':
494+
case 'CreateFrom': {
495+
break;
496+
}
497+
case 'Phi':
498+
case 'Object':
499+
case 'Function': {
500+
returned.add(node);
501+
break;
502+
}
503+
default: {
504+
assertExhaustive(
505+
node.value,
506+
`Unexpected node value kind '${(node.value as any).kind}'`,
507+
);
508+
}
509+
}
510+
}
511+
const returnedValues = [...returned];
512+
if (
513+
returnedValues.length === 1 &&
514+
returnedValues[0].value.kind === 'Object' &&
515+
tracked.some(place => place.identifier.id === returnedValues[0].id.id)
516+
) {
517+
const from = tracked.find(
518+
place => place.identifier.id === returnedValues[0].id.id,
519+
)!;
520+
functionEffects.push({
521+
kind: 'Assign',
522+
from,
523+
into: fn.returns,
524+
});
525+
} else if (
526+
returnedValues.length === 1 &&
527+
returnedValues[0].value.kind === 'Function'
528+
) {
529+
const outerContext = new Set(fn.context.map(p => p.identifier.id));
530+
const effect = returnedValues[0].value.effect;
531+
functionEffects.push({
532+
kind: 'CreateFunction',
533+
function: {
534+
...effect.function,
535+
loweredFunc: {
536+
func: {
537+
...effect.function.loweredFunc.func,
538+
context: effect.function.loweredFunc.func.context.filter(p =>
539+
outerContext.has(p.identifier.id),
540+
),
541+
},
542+
},
543+
},
544+
captures: effect.captures.filter(p => outerContext.has(p.identifier.id)),
545+
into: fn.returns,
546+
});
547+
} else {
548+
const returns = fn.returns.identifier;
549+
functionEffects.push({
550+
kind: 'Create',
551+
into: fn.returns,
552+
value: isPrimitiveType(returns)
553+
? ValueKind.Primitive
554+
: isJsxType(returns.type)
555+
? ValueKind.Frozen
556+
: ValueKind.Mutable,
557+
reason: ValueReason.KnownReturnSignature,
558+
});
559+
}
560+
468561
/**
469562
* Part 3
470563
* Finish populating the externally visible effects. Above we bubble-up the side effects
471564
* (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
472565
* Here we populate an effect to create the return value as well as populating alias/capture
473566
* effects for how data flows between the params, context vars, and return.
474567
*/
475-
const returns = fn.returns.identifier;
476-
functionEffects.push({
477-
kind: 'Create',
478-
into: fn.returns,
479-
value: isPrimitiveType(returns)
480-
? ValueKind.Primitive
481-
: isJsxType(returns.type)
482-
? ValueKind.Frozen
483-
: ValueKind.Mutable,
484-
reason: ValueReason.KnownReturnSignature,
485-
});
486568
/**
487569
* Determine precise data-flow effects by simulating transitive mutations of the params/
488570
* captures and seeing what other params/context variables are affected. Anything that
489571
* would be transitively mutated needs a capture relationship.
490572
*/
491-
const tracked: Array<Place> = [];
492573
const ignoredErrors = new CompilerError();
493-
for (const param of [...fn.params, ...fn.context, fn.returns]) {
494-
const place = param.kind === 'Identifier' ? param : param.place;
495-
tracked.push(place);
496-
}
497574
for (const into of tracked) {
498575
const mutationIndex = index++;
499576
state.mutate(
@@ -572,9 +649,14 @@ type Node = {
572649
local: {kind: MutationKind; loc: SourceLocation} | null;
573650
lastMutated: number;
574651
value:
652+
| {kind: 'Assign'}
653+
| {kind: 'CreateFrom'}
575654
| {kind: 'Object'}
576655
| {kind: 'Phi'}
577-
| {kind: 'Function'; function: HIRFunction};
656+
| {
657+
kind: 'Function';
658+
effect: Extract<AliasingEffect, {kind: 'CreateFunction'}>;
659+
};
578660
};
579661
class AliasingState {
580662
nodes: Map<Identifier, Node> = new Map();
@@ -594,7 +676,7 @@ class AliasingState {
594676
}
595677

596678
createFrom(index: number, from: Place, into: Place): void {
597-
this.create(into, {kind: 'Object'});
679+
this.create(into, {kind: 'CreateFrom'});
598680
const fromNode = this.nodes.get(from.identifier);
599681
const toNode = this.nodes.get(into.identifier);
600682
if (fromNode == null || toNode == null) {
@@ -644,7 +726,10 @@ class AliasingState {
644726
continue;
645727
}
646728
if (node.value.kind === 'Function') {
647-
appendFunctionErrors(errors, node.value.function);
729+
appendFunctionErrors(
730+
errors,
731+
node.value.effect.function.loweredFunc.func,
732+
);
648733
}
649734
for (const [alias, when] of node.createdFrom) {
650735
if (when >= index) {
@@ -704,7 +789,10 @@ class AliasingState {
704789
node.transitive == null &&
705790
node.local == null
706791
) {
707-
appendFunctionErrors(errors, node.value.function);
792+
appendFunctionErrors(
793+
errors,
794+
node.value.effect.function.loweredFunc.func,
795+
);
708796
}
709797
if (transitive) {
710798
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)