Skip to content

[compiler] Move co-mutation range extension to InferMutableRanges #33157

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

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3b83903
[compiler] Move co-mutation range extension to InferMutableRanges
josephsavona May 8, 2025
ec552a5
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 9, 2025
206e7e9
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 9, 2025
13b2d32
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 9, 2025
c435b2b
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 9, 2025
cf11452
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 9, 2025
24c3f79
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 12, 2025
f642437
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 13, 2025
e7260ab
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 22, 2025
7e40e24
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 23, 2025
6da53e8
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 27, 2025
efa0e18
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 27, 2025
fcdbd46
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 28, 2025
aa9384d
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 28, 2025
79e148f
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 29, 2025
e6c4aa4
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 29, 2025
a704784
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 30, 2025
b10c1e7
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 30, 2025
c8c85f9
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 30, 2025
50fa3db
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona May 30, 2025
053de34
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 2, 2025
6536b85
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 3, 2025
a16844b
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 3, 2025
6b5f737
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 3, 2025
16eca9c
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 4, 2025
93290c3
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 4, 2025
72bc875
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 5, 2025
c71e6de
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 5, 2025
b30c58f
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 6, 2025
b8a6b08
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 6, 2025
413b8e6
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 6, 2025
619ef07
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 6, 2025
d676234
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 7, 2025
5e02551
Update on "[compiler] Move co-mutation range extension to InferMutabl…
josephsavona Jun 9, 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
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ function isMutable(range: MutableRange): boolean {
}

const DEBUG_MUTABLE_RANGES = false;
function printMutableRange(identifier: Identifier): string {
export function printMutableRange(identifier: Identifier): string {
if (DEBUG_MUTABLE_RANGES) {
// if debugging, print both the identifier and scope range if they differ
const range = identifier.mutableRange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ 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 {
Expand Down Expand Up @@ -69,16 +67,6 @@ function lower(func: HIRFunction): DisjointSet<Identifier> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import prettyFormat from 'pretty-format';
import {HIRFunction, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
Expand All @@ -14,7 +15,9 @@ import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
import {inferMutableLifetimes} from './InferMutableLifetimes';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
import {inferMutableRangesForComutation} from './InferMutableRangesForComutation';
import {inferTryCatchAliases} from './InferTryCatchAliases';
import {printIdentifier, printMutableRange} from '../HIR/PrintHIR';

export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
// Infer mutable ranges for non fields
Expand All @@ -32,11 +35,13 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
* Eagerly canonicalize so that if nothing changes we can bail out
* after a single iteration
*/
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
let prevAliases: Map<Identifier, string> = canonicalize(aliases);
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);

inferMutableRangesForComutation(ir);

// Update aliasing information of fields
inferAliasForStores(ir, aliases);

Expand All @@ -45,7 +50,7 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
// Update aliasing information of phis
inferAliasForPhis(ir, aliases);

const nextAliases = aliases.canonicalize();
const nextAliases = canonicalize(aliases);
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
Expand Down Expand Up @@ -77,12 +82,13 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
* but does not modify values that `y` "contains" such as the
* object literal or `z`.
*/
prevAliases = aliases.canonicalize();
prevAliases = canonicalize(aliases);
while (true) {
inferMutableRangesForAlias(ir, aliases);
inferMutableRangesForComutation(ir);
inferAliasForPhis(ir, aliases);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
const nextAliases = canonicalize(aliases);
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
Expand All @@ -92,7 +98,41 @@ export function inferMutableRanges(ir: HIRFunction): DisjointSet<Identifier> {
return aliases;
}

function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
export function debugAliases(aliases: DisjointSet<Identifier>): void {
console.log(
prettyFormat(
aliases
.buildSets()
.map(set =>
[...set].map(
ident => printIdentifier(ident) + printMutableRange(ident),
),
),
),
);
}

/**
* Canonicalizes the alias set and mutable range information calculated at the current time.
* The returned value maps each identifier in the program to the root identifier of its alias
* set and the the mutable range of that set.
*
* This ensures that we fixpoint the mutable ranges themselves and not just the alias sets.
*/
function canonicalize(
aliases: DisjointSet<Identifier>,
): Map<Identifier, string> {
const entries = new Map<Identifier, string>();
aliases.forEach((item, root) => {
entries.set(
item,
`${root.id}:${root.mutableRange.start}:${root.mutableRange.end}`,
);
});
return entries;
}

function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
if (a.size !== b.size) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* 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,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
import {eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';

/**
* Finds instructions with operands that co-mutate and extends all their mutable ranges
* to end at the same point (the highest `end` value of the group). Note that the
* alias sets used in InferMutableRanges are meant for values that strictly alias:
* a mutation of one value in the set would directly modify the same object as some
* other value in the set.
*
* However, co-mutation can cause an alias to one object to be stored within another object,
* for example:
*
* ```
* const a = {};
* const b = {};
* const f = () => b.c; //
* setProperty(a, 'b', b); // equiv to a.b = b
*
* a.b.c = 'c'; // this mutates b!
* ```
*
* Here, the co-mutation in `setProperty(a, 'b', b)` means that a reference to b may be stored
* in a, vice-versa, or both. We need to extend the mutable range of both a and b to reflect
* the fact the values may mutate together.
*
* Previously this was implemented in InferReactiveScopeVariables, but that is too late:
* we need this to be part of the InferMutableRanges fixpoint iteration to account for functions
* like `f` in the example, which capture a reference to a value that may change later. `f`
* cannot be independently memoized from the `setProperty()` call due to the co-mutation.
*
* See aliased-capture-mutate and aliased-capture-aliased-mutate fixtures for examples.
*/
export function inferMutableRangesForComutation(fn: HIRFunction): void {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
let operands: Array<Identifier> | null = null;
for (const operand of eachInstructionOperand(instr)) {
if (
isMutable(instr, operand) &&
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands ??= [];
operands.push(operand.identifier);
}
}
if (operands != null) {
// Find the last instruction which mutates any of the mutable operands
let lastMutatingInstructionId = makeInstructionId(0);
for (const id of operands) {
if (id.mutableRange.end > lastMutatingInstructionId) {
lastMutatingInstructionId = id.mutableRange.end;
}
}

/**
* Update all mutable operands's mutable ranges to end at the same point
*/
for (const id of operands) {
if (
id.mutableRange.end < lastMutatingInstructionId &&
!isRefOrRefValue(id)
) {
id.mutableRange.end = lastMutatingInstructionId;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

## Input

```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';

function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};

arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";

function useFoo(t0) {
const $ = _c(3);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };

arrayPush(x, y);
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};

```

### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
<div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';

function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};

arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,6 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false
import {setPropertyByKey, Stringify} from 'shared-runtime';

/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/

function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
Expand Down Expand Up @@ -46,7 +33,7 @@ import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";

function useFoo(t0) {
const $ = _c(4);
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a) {
Expand All @@ -55,15 +42,7 @@ function useFoo(t0) {

setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
Expand All @@ -84,4 +63,7 @@ export const FIXTURE_ENTRYPOINT = {
};

```


### Eval output
(kind: ok) <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
<div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
Loading
Loading