Skip to content
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

[compiler][hir-rewrite] Check mutability of base identifier when hoisting #31032

Merged
merged 14 commits into from
Oct 3, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
InstructionValue,
ReactiveScopeDependency,
ScopeId,
} from './HIR';
Expand Down Expand Up @@ -209,33 +209,23 @@ class PropertyPathRegistry {
}
}

function addNonNullPropertyPath(
source: Identifier,
sourceNode: PropertyPathNode,
instrId: InstructionId,
knownImmutableIdentifiers: Set<IdentifierId>,
result: Set<PropertyPathNode>,
): void {
/**
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
* are not valid with respect to current instruction id numbering.
* We use attached reactive scope ranges as a proxy for mutable range, but this
* is an overestimate as (1) scope ranges merge and align to form valid program
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
* non-mutable identifiers.
*
* See comment at top of function for why we track known immutable identifiers.
*/
const isMutableAtInstr =
source.mutableRange.end > source.mutableRange.start + 1 &&
source.scope != null &&
inRange({id: instrId}, source.scope.range);
if (
!isMutableAtInstr ||
knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id)
) {
result.add(sourceNode);
function getMaybeNonNullInInstruction(
instr: InstructionValue,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
registry: PropertyPathRegistry,
): PropertyPathNode | null {
let path = null;
if (instr.kind === 'PropertyLoad') {
path = temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
path: [],
};
} else if (instr.kind === 'Destructure') {
path = temporaries.get(instr.value.identifier.id) ?? null;
} else if (instr.kind === 'ComputedLoad') {
path = temporaries.get(instr.object.identifier.id) ?? null;
}
return path != null ? registry.getOrCreateProperty(path) : null;
}

function collectNonNullsInBlocks(
Expand Down Expand Up @@ -286,41 +276,38 @@ function collectNonNullsInBlocks(
);
}
for (const instr of block.instructions) {
if (instr.value.kind === 'PropertyLoad') {
const source = temporaries.get(instr.value.object.identifier.id) ?? {
identifier: instr.value.object.identifier,
path: [],
};
addNonNullPropertyPath(
instr.value.object.identifier,
registry.getOrCreateProperty(source),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
);
} else if (instr.value.kind === 'Destructure') {
const source = instr.value.value.identifier.id;
const sourceNode = temporaries.get(source);
if (sourceNode != null) {
addNonNullPropertyPath(
instr.value.value.identifier,
registry.getOrCreateProperty(sourceNode),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
);
}
} else if (instr.value.kind === 'ComputedLoad') {
const source = instr.value.object.identifier.id;
const sourceNode = temporaries.get(source);
if (sourceNode != null) {
addNonNullPropertyPath(
instr.value.object.identifier,
registry.getOrCreateProperty(sourceNode),
instr.id,
knownImmutableIdentifiers,
assumedNonNullObjects,
const maybeNonNull = getMaybeNonNullInInstruction(
instr.value,
temporaries,
registry,
);
if (maybeNonNull != null) {
const baseIdentifier = maybeNonNull.fullPath.identifier;
/**
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
* are not valid with respect to current instruction id numbering.
* We use attached reactive scope ranges as a proxy for mutable range, but this
* is an overestimate as (1) scope ranges merge and align to form valid program
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
* non-mutable identifiers.
*
* See comment at top of function for why we track known immutable identifiers.
*/
const isMutableAtInstr =
baseIdentifier.mutableRange.end >
baseIdentifier.mutableRange.start + 1 &&
baseIdentifier.scope != null &&
inRange(
{
id: instr.id,
},
baseIdentifier.scope.range,
);
if (
!isMutableAtInstr ||
knownImmutableIdentifiers.has(baseIdentifier.id)
) {
assumedNonNullObjects.add(maybeNonNull);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

## Input

```javascript
import {identity} from 'shared-runtime';

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
const y = [];
try {
y.push(identity(maybeNullObject.value.inner));
} catch {
y.push('null');
}

return y;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, {value: 2}, {value: 3}, null],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
import { identity } from "shared-runtime";

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject) {
const $ = _c(2);
let y;
if ($[0] !== maybeNullObject.value.inner) {
y = [];
try {
y.push(identity(maybeNullObject.value.inner));
} catch {
y.push("null");
}
$[0] = maybeNullObject.value.inner;
$[1] = y;
} else {
y = $[1];
}
return y;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, { value: 2 }, { value: 3 }, null],
};

```

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {identity} from 'shared-runtime';

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
const y = [];
try {
y.push(identity(maybeNullObject.value.inner));
} catch {
y.push('null');
}

return y;
}
Comment on lines +3 to +16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug repro is unrelated to this fix


export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, {value: 2}, {value: 3}, null],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

## Input

```javascript
// @enablePropagateDepsInHIR
import {identity} from 'shared-runtime';

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
const y = [];
try {
y.push(identity(maybeNullObject.value.inner));
} catch {
y.push('null');
}

return y;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, {value: 2}, {value: 3}, null],
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { identity } from "shared-runtime";

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject) {
const $ = _c(4);
let y;
if ($[0] !== maybeNullObject) {
y = [];
try {
let t0;
if ($[2] !== maybeNullObject.value.inner) {
t0 = identity(maybeNullObject.value.inner);
$[2] = maybeNullObject.value.inner;
$[3] = t0;
} else {
t0 = $[3];
}
y.push(t0);
} catch {
y.push("null");
}
$[0] = maybeNullObject;
$[1] = y;
} else {
y = $[1];
}
return y;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, { value: 2 }, { value: 3 }, null],
};

```

### Eval output
(kind: ok) ["null"]
[null]
[null]
["null"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @enablePropagateDepsInHIR
import {identity} from 'shared-runtime';

/**
* Not safe to hoist read of maybeNullObject.value.inner outside of the
* try-catch block, as that might throw
*/
function useFoo(maybeNullObject: {value: {inner: number}} | null) {
const y = [];
try {
y.push(identity(maybeNullObject.value.inner));
} catch {
y.push('null');
}

return y;
}

export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [null],
sequentialRenders: [null, {value: 2}, {value: 3}, null],
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
const { throwInput } = require("shared-runtime");

function Component(props) {
const $ = _c(2);
const $ = _c(3);
let x;
if ($[0] !== props) {
if ($[0] !== props.y || $[1] !== props.e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we're now inferring props as non-mutable. Previously, props.y was not marked hoistable because the evaluation of LoadLocal props had a mutable range (due to the try-catch).

try {
const y = [];
y.push(props.y);
Expand All @@ -44,10 +44,11 @@ function Component(props) {
e.push(props.e);
x = e;
}
$[0] = props;
$[1] = x;
$[0] = props.y;
$[1] = props.e;
$[2] = x;
} else {
x = $[1];
x = $[2];
}
return x;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
const { throwInput } = require("shared-runtime");

function Component(props) {
const $ = _c(2);
const $ = _c(3);
let t0;
if ($[0] !== props) {
if ($[0] !== props.y || $[1] !== props.e) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
try {
Expand All @@ -47,10 +47,11 @@ function Component(props) {
break bb0;
}
}
$[0] = props;
$[1] = t0;
$[0] = props.y;
$[1] = props.e;
$[2] = t0;
} else {
t0 = $[1];
t0 = $[2];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
Expand Down
1 change: 1 addition & 0 deletions compiler/packages/snap/src/SproutTodoFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ const skipFilter = new Set([
'fbt/bug-fbt-plural-multiple-function-calls',
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
'bug-invalid-hoisting-functionexpr',
'bug-try-catch-maybe-null-dependency',
'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond',
'original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block',
'original-reactive-scopes-fork/bug-hoisted-declaration-with-scope',
Expand Down