Skip to content

[compiler][newinference] ensure fixpoint converges for loops w backedges #33449

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
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export function inferMutationAliasingEffects(
}

class Context {
internedEffects: Map<string, AliasingEffect> = new Map();
instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
new Map();
Expand All @@ -200,6 +201,17 @@ class Context {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
}

internEffect(effect: AliasingEffect): AliasingEffect {
const hash = hashEffect(effect);
let interned = this.internedEffects.get(hash);
if (interned == null) {
console.log(`intern: ${hash}`);
this.internedEffects.set(hash, effect);
interned = effect;
}
return interned;
}
}

function inferParam(
Expand Down Expand Up @@ -388,10 +400,11 @@ function applySignature(
function applyEffect(
context: Context,
state: InferenceState,
effect: AliasingEffect,
_effect: AliasingEffect,
aliased: Set<IdentifierId>,
effects: Array<AliasingEffect>,
): void {
const effect = context.internEffect(_effect);
if (DEBUG) {
console.log(printAliasingEffect(effect));
}
Expand Down Expand Up @@ -465,15 +478,12 @@ function applyEffect(
break;
}
default: {
// Even if the source is non-primitive, the destination may be. skip primitives.
if (!isPrimitiveType(effect.into.identifier)) {
effects.push({
// OK: recording information flow
kind: 'CreateFrom', // prev Alias
from: effect.from,
into: effect.into,
});
}
effects.push({
// OK: recording information flow
kind: 'CreateFrom', // prev Alias
from: effect.from,
into: effect.into,
});
}
}
break;
Expand Down Expand Up @@ -1363,11 +1373,19 @@ function computeSignatureForInstruction(
}
case 'PropertyLoad':
case 'ComputedLoad': {
effects.push({
kind: 'CreateFrom',
from: value.object,
into: lvalue,
});
if (isPrimitiveType(lvalue.identifier)) {
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
});
} else {
effects.push({
kind: 'CreateFrom',
from: value.object,
into: lvalue,
});
}
break;
}
case 'PropertyStore':
Expand All @@ -1378,8 +1396,11 @@ function computeSignatureForInstruction(
from: value.value,
into: value.object,
});
// OK: lvalues are assigned to
effects.push({kind: 'Assign', from: value.value, into: lvalue});
effects.push({
kind: 'Create',
into: lvalue,
value: ValueKind.Primitive,
});
break;
}
case 'ObjectMethod':
Expand Down Expand Up @@ -2207,7 +2228,23 @@ export type AliasedPlace = {place: Place; kind: 'alias' | 'capture'};

export type AliasingEffect =
/**
* Marks the given value, its aliases, and indirect captures, as frozen.
* Marks the given value and its direct aliases as frozen.
*
* Captured values are *not* considered frozen, because we cannot be sure that a previously
* captured value will still be captured at the point of the freeze.
*
* For example:
* const x = {};
* const y = [x];
* y.pop(); // y dosn't contain x anymore!
* freeze(y);
* mutate(x); // safe to mutate!
*
* The exception to this is FunctionExpressions - since it is impossible to change which
* value a function closes over[1] we can transitively freeze functions and their captures.
*
* [1] Except for `let` values that are reassigned and closed over by a function, but we
* handle this explicitly with StoreContext/LoadContext.
*/
| {kind: 'Freeze'; value: Place; reason: ValueReason}
/**
Expand Down Expand Up @@ -2305,6 +2342,67 @@ export type AliasingEffect =
error: CompilerErrorDetailOptions;
};

function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
return [
effect.kind,
effect.receiver.identifier.id,
effect.function.identifier.id,
effect.mutatesFunction,
effect.args
.map(a => {
if (a.kind === 'Hole') {
return '';
} else if (a.kind === 'Identifier') {
return a.identifier.id;
} else {
return `...${a.place.identifier.id}`;
}
})
.join(','),
effect.into.identifier.id,
].join(':');
}
case 'CreateFrom':
case 'ImmutableCapture':
case 'Assign':
case 'Alias':
case 'Capture': {
return [
effect.kind,
effect.from.identifier.id,
effect.into.identifier.id,
].join(':');
}
case 'Create': {
return [effect.kind, effect.into.identifier.id].join(':');
}
case 'Freeze': {
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
}
case 'MutateFrozen':
case 'MutateGlobal': {
return [effect.kind, effect.place.identifier.id].join(':');
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return [effect.kind, effect.value.identifier.id].join(':');
}
case 'CreateFunction': {
return [
effect.kind,
effect.into.identifier.id,
// return places are a unique way to identify functions themselves
effect.function.loweredFunc.func.returns.identifier.id,
effect.captures.map(p => p.identifier.id).join(','),
].join(':');
}
}
}

export type AliasingSignatureEffect = AliasingEffect;

export type AliasingSignature = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,6 @@ export function inferMutationAliasingFunctionEffects(
});
}

// console.log(pretty(tracked));
// console.log(pretty(dataFlow));
// console.log('FunctionEffects', effects.map(printAliasingEffect).join('\n'));
return effects;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

## Input

```javascript
// @enableNewMutationAliasingModel
import {arrayPush, Stringify} from 'shared-runtime';

function Component({prop1, prop2}) {
'use memo';

// we'll ultimately extract the item from this array as z, and mutate later
let x = [{value: prop1}];
let z;
while (x.length < 2) {
// there's a phi here for x (value before the loop and the reassignment later)

// this mutation occurs before the reassigned value
arrayPush(x, {value: prop2});

// this condition will never be true, so x doesn't get reassigned
if (x[0].value === null) {
x = [{value: prop2}];
const y = x;
z = y[0];
}
}
// the code is set up so that z will always be the value from the original x
z.other = true;
return <Stringify z={z} />;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{prop1: 0, prop2: 0}],
sequentialRenders: [
{prop1: 0, prop2: 0},
{prop1: 1, prop2: 0},
{prop1: 1, prop2: 1},
{prop1: 0, prop2: 1},
{prop1: 0, prop2: 0},
],
};

```

## Code

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

function Component(t0) {
"use memo";
const $ = _c(5);
const { prop1, prop2 } = t0;
let z;
if ($[0] !== prop1 || $[1] !== prop2) {
let x = [{ value: prop1 }];
while (x.length < 2) {
arrayPush(x, { value: prop2 });
if (x[0].value === null) {
x = [{ value: prop2 }];
const y = x;
z = y[0];
}
}

z.other = true;
$[0] = prop1;
$[1] = prop2;
$[2] = z;
} else {
z = $[2];
}
let t1;
if ($[3] !== z) {
t1 = <Stringify z={z} />;
$[3] = z;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ prop1: 0, prop2: 0 }],
sequentialRenders: [
{ prop1: 0, prop2: 0 },
{ prop1: 1, prop2: 0 },
{ prop1: 1, prop2: 1 },
{ prop1: 0, prop2: 1 },
{ prop1: 0, prop2: 0 },
],
};

```

### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
[[ (exception in render) TypeError: Cannot set properties of undefined (setting 'other') ]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @enableNewMutationAliasingModel
import {arrayPush, Stringify} from 'shared-runtime';

function Component({prop1, prop2}) {
'use memo';

let x = [{value: prop1}];
let z;
while (x.length < 2) {
// there's a phi here for x (value before the loop and the reassignment later)

// this mutation occurs before the reassigned value
arrayPush(x, {value: prop2});

if (x[0].value === prop1) {
x = [{value: prop2}];
const y = x;
z = y[0];
}
}
z.other = true;
return <Stringify z={z} />;
}

// export const FIXTURE_ENTRYPOINT = {
// fn: Component,
// params: [{prop1: 0, prop2: 0}],
// sequentialRenders: [
// {prop1: 0, prop2: 0},
// {prop1: 1, prop2: 0},
// {prop1: 1, prop2: 1},
// {prop1: 0, prop2: 1},
// {prop1: 0, prop2: 0},
// ],
// };
Loading