[compiler][hir] Only hoist always-accessed PropertyLoads from function decls

Prior to this PR, we consider all of a nested function's accessed paths as 'hoistable' (to the basic block in which the function was defined). Now, we traverse nested functions and find all paths hoistable to their *entry block*.

Note that this only replaces the *hoisting* part of function declarations, not dependencies. This realistically only affects optional chains within functions, which always get truncated to its inner non-optional path (see [todo-infer-function-uncond-optionals-hoisted.tsx](https://github.com/facebook/react/blob/576f3c0aa898cb99da1b7bf15317756e25c13708/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.tsx))

See newly added test fixtures for details

Update: Note that toggling `enableTreatFunctionDepsAsConditional` makes a non-trivial impact on granularity of inferred deps (i.e. we find that function declarations uniquely identify some paths as hoistable). Snapshot comparison of internal code shows ~2.5% of files get worse dependencies ([internal link](https://www.internalfb.com/phabricator/paste/view/P1625792186))

ghstack-source-id: 778ab5494a
Pull Request resolved: https://github.com/facebook/react/pull/31066
This commit is contained in:
Mofei Zhang
2024-10-02 16:18:33 -04:00
parent 88341cf22c
commit c8ca7e361a
27 changed files with 1306 additions and 75 deletions
@@ -7,6 +7,7 @@ import {
Set_union,
getOrInsertDefault,
} from '../Utils/utils';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
import {
BasicBlock,
BlockId,
@@ -15,10 +16,12 @@ import {
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
InstructionValue,
ReactiveScopeDependency,
ScopeId,
} from './HIR';
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
/**
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
@@ -83,28 +86,57 @@ export function collectHoistablePropertyLoads(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
): ReadonlyMap<ScopeId, BlockInfo> {
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null,
): ReadonlyMap<BlockId, BlockInfo> {
const registry = new PropertyPathRegistry();
const nodes = collectNonNullsInBlocks(
fn,
temporaries,
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
const actuallyEvaluatedTemporaries = new Map(
[...temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
);
/**
* Due to current limitations of mutable range inference, there are edge cases in
* which we infer known-immutable values (e.g. props or hook params) to have a
* mutable range and scope.
* (see `destructure-array-declaration-to-context-var` fixture)
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
* is being rewritten to HIR).
*/
const knownImmutableIdentifiers = new Set<IdentifierId>();
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
for (const p of fn.params) {
if (p.kind === 'Identifier') {
knownImmutableIdentifiers.add(p.identifier.id);
}
}
}
const nodes = collectNonNullsInBlocks(fn, {
temporaries: actuallyEvaluatedTemporaries,
knownImmutableIdentifiers,
hoistableFromOptionals,
registry,
);
nestedFnImmutableContext,
});
propagateNonNull(fn, nodes, registry);
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
return nodes;
}
export function keyByScopeId<T>(
fn: HIRFunction,
source: ReadonlyMap<BlockId, T>,
): ReadonlyMap<ScopeId, T> {
const keyedByScopeId = new Map<ScopeId, T>();
for (const [_, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
nodesKeyedByScopeId.set(
keyedByScopeId.set(
block.terminal.scope.id,
nodes.get(block.terminal.block)!,
source.get(block.terminal.block)!,
);
}
}
return nodesKeyedByScopeId;
return keyedByScopeId;
}
export type BlockInfo = {
@@ -211,45 +243,75 @@ class PropertyPathRegistry {
function getMaybeNonNullInInstruction(
instr: InstructionValue,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
registry: PropertyPathRegistry,
context: CollectNonNullsInBlocksContext,
): PropertyPathNode | null {
let path = null;
if (instr.kind === 'PropertyLoad') {
path = temporaries.get(instr.object.identifier.id) ?? {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
path: [],
};
} else if (instr.kind === 'Destructure') {
path = temporaries.get(instr.value.identifier.id) ?? null;
path = context.temporaries.get(instr.value.identifier.id) ?? null;
} else if (instr.kind === 'ComputedLoad') {
path = temporaries.get(instr.object.identifier.id) ?? null;
path = context.temporaries.get(instr.object.identifier.id) ?? null;
}
return path != null ? registry.getOrCreateProperty(path) : null;
return path != null ? context.registry.getOrCreateProperty(path) : null;
}
function isImmutableAtInstr(
identifier: Identifier,
instr: InstructionId,
context: CollectNonNullsInBlocksContext,
): boolean {
if (context.nestedFnImmutableContext != null) {
/**
* Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
*/
return context.nestedFnImmutableContext.has(identifier.id);
} else {
/**
* 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 in exported function for why we track known immutable identifiers.
*/
const mutableAtInstr =
identifier.mutableRange.end > identifier.mutableRange.start + 1 &&
identifier.scope != null &&
inRange(
{
id: instr,
},
identifier.scope.range,
);
return (
!mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id)
);
}
}
type CollectNonNullsInBlocksContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
registry: PropertyPathRegistry;
/**
* (For nested / inner function declarations)
* Context variables (i.e. captured from an outer scope) that are immutable.
* Note that this technically could be merged into `knownImmutableIdentifiers`,
* but are currently kept separate for readability.
*/
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
};
function collectNonNullsInBlocks(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
registry: PropertyPathRegistry,
context: CollectNonNullsInBlocksContext,
): ReadonlyMap<BlockId, BlockInfo> {
/**
* Due to current limitations of mutable range inference, there are edge cases in
* which we infer known-immutable values (e.g. props or hook params) to have a
* mutable range and scope.
* (see `destructure-array-declaration-to-context-var` fixture)
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
* is being rewritten to HIR).
*/
const knownImmutableIdentifiers = new Set<IdentifierId>();
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
for (const p of fn.params) {
if (p.kind === 'Identifier') {
knownImmutableIdentifiers.add(p.identifier.id);
}
}
}
/**
* Known non-null objects such as functional component props can be safely
* read from any block.
@@ -261,7 +323,9 @@ function collectNonNullsInBlocks(
fn.params[0].kind === 'Identifier'
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<BlockId, BlockInfo>();
for (const [_, block] of fn.body.blocks) {
@@ -269,45 +333,48 @@ function collectNonNullsInBlocks(
knownNonNullIdentifiers,
);
const maybeOptionalChain = hoistableFromOptionals.get(block.id);
const maybeOptionalChain = context.hoistableFromOptionals.get(block.id);
if (maybeOptionalChain != null) {
assumedNonNullObjects.add(
registry.getOrCreateProperty(maybeOptionalChain),
context.registry.getOrCreateProperty(maybeOptionalChain),
);
}
for (const instr of block.instructions) {
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);
const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
if (
maybeNonNull != null &&
isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context)
) {
assumedNonNullObjects.add(maybeNonNull);
}
if (
instr.value.kind === 'FunctionExpression' &&
!fn.env.config.enableTreatFunctionDepsAsConditional
) {
const innerFn = instr.value.loweredFunc;
const innerTemporaries = collectTemporariesSidemap(
innerFn.func,
new Set(),
);
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
const innerHoistableMap = collectHoistablePropertyLoads(
innerFn.func,
innerTemporaries,
innerOptionals.hoistableObjects,
context.nestedFnImmutableContext ??
new Set(
innerFn.func.context
.filter(place =>
isImmutableAtInstr(place.identifier, instr.id, context),
)
.map(place => place.identifier.id),
),
);
const innerHoistables = assertNonNull(
innerHoistableMap.get(innerFn.func.body.entry),
);
for (const entry of innerHoistables.assumedNonNullObjects) {
assumedNonNullObjects.add(entry);
}
}
}
@@ -515,3 +582,27 @@ function reduceMaybeOptionalChains(
}
} while (changed);
}
function collectFunctionExpressionFakeLoads(
fn: HIRFunction,
): Set<IdentifierId> {
const sources = new Map<IdentifierId, IdentifierId>();
const functionExpressionReferences = new Set<IdentifierId>();
for (const [_, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (value.kind === 'FunctionExpression') {
for (const reference of value.loweredFunc.dependencies) {
let curr: IdentifierId | undefined = reference.identifier.id;
while (curr != null) {
functionExpressionReferences.add(curr);
curr = sources.get(curr);
}
}
} else if (value.kind === 'PropertyLoad') {
sources.set(lvalue.identifier.id, value.object.identifier.id);
}
}
}
return functionExpressionReferences;
}
@@ -17,7 +17,10 @@ import {
areEqualPaths,
IdentifierId,
} from './HIR';
import {collectHoistablePropertyLoads} from './CollectHoistablePropertyLoads';
import {
collectHoistablePropertyLoads,
keyByScopeId,
} from './CollectHoistablePropertyLoads';
import {
ScopeBlockTraversal,
eachInstructionOperand,
@@ -41,10 +44,9 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
hoistableObjects,
} = collectOptionalChainSidemap(fn);
const hoistablePropertyLoads = collectHoistablePropertyLoads(
const hoistablePropertyLoads = keyByScopeId(
fn,
temporaries,
hoistableObjects,
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects, null),
);
const scopeDeps = collectDependencies(
@@ -209,7 +211,7 @@ function findTemporariesUsedOutsideDeclaringScope(
* of $1, as the evaluation of `arr.length` changes between instructions $1 and
* $3. We do not track $1 -> arr.length in this case.
*/
function collectTemporariesSidemap(
export function collectTemporariesSidemap(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
@@ -0,0 +1,97 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {shallowCopy, mutate, Stringify} from 'shared-runtime';
function useFoo({
a,
shouldReadA,
}: {
a: {b: {c: number}; x: number};
shouldReadA: boolean;
}) {
const local = shallowCopy(a);
mutate(local);
return (
<Stringify
fn={() => {
if (shouldReadA) return local.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { shallowCopy, mutate, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, shouldReadA } = t0;
let local;
if ($[0] !== a) {
local = shallowCopy(a);
mutate(local);
$[0] = a;
$[1] = local;
} else {
local = $[1];
}
let t1;
if ($[2] !== shouldReadA || $[3] !== local) {
t1 = (
<Stringify
fn={() => {
if (shouldReadA) {
return local.b.c;
}
return null;
}}
shouldInvokeFns={true}
/>
);
$[2] = shouldReadA;
$[3] = local;
$[4] = t1;
} else {
t1 = $[4];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null, shouldReadA: true }],
sequentialRenders: [
{ a: null, shouldReadA: true },
{ a: null, shouldReadA: false },
{ a: { b: { c: 4 } }, shouldReadA: true },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
<div>{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,33 @@
// @enablePropagateDepsInHIR
import {shallowCopy, mutate, Stringify} from 'shared-runtime';
function useFoo({
a,
shouldReadA,
}: {
a: {b: {c: number}; x: number};
shouldReadA: boolean;
}) {
const local = shallowCopy(a);
mutate(local);
return (
<Stringify
fn={() => {
if (shouldReadA) return local.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
@@ -0,0 +1,80 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function Foo({a, shouldReadA}) {
return (
<Stringify
fn={() => {
if (shouldReadA) return a.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { Stringify } from "shared-runtime";
function Foo(t0) {
const $ = _c(3);
const { a, shouldReadA } = t0;
let t1;
if ($[0] !== shouldReadA || $[1] !== a) {
t1 = (
<Stringify
fn={() => {
if (shouldReadA) {
return a.b.c;
}
return null;
}}
shouldInvokeFns={true}
/>
);
$[0] = shouldReadA;
$[1] = a;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ a: null, shouldReadA: true }],
sequentialRenders: [
{ a: null, shouldReadA: true },
{ a: null, shouldReadA: false },
{ a: { b: { c: 4 } }, shouldReadA: true },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,25 @@
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function Foo({a, shouldReadA}) {
return (
<Stringify
fn={() => {
if (shouldReadA) return a.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
@@ -0,0 +1,52 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
t1 = <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
$[0] = a.b.c;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,13 @@
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
@@ -0,0 +1,92 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime';
function Foo({a, cond}) {
// Assume fn will be uncond evaluated, so we can safely evaluate {a.<any>,
// a.b.<any}
const fn = () => [a, a.b.c];
useIdentity(null);
const x = makeArray();
if (cond) {
x.push(identity(a.b.c));
}
return <Stringify fn={fn} x={x} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, cond: true}],
sequentialRenders: [
{a: null, cond: true},
{a: {b: {c: 4}}, cond: true},
{a: {b: {c: 4}}, cond: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { identity, makeArray, Stringify, useIdentity } from "shared-runtime";
function Foo(t0) {
const $ = _c(8);
const { a, cond } = t0;
let t1;
if ($[0] !== a) {
t1 = () => [a, a.b.c];
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const fn = t1;
useIdentity(null);
let x;
if ($[2] !== cond || $[3] !== a.b.c) {
x = makeArray();
if (cond) {
x.push(identity(a.b.c));
}
$[2] = cond;
$[3] = a.b.c;
$[4] = x;
} else {
x = $[4];
}
let t2;
if ($[5] !== fn || $[6] !== x) {
t2 = <Stringify fn={fn} x={x} shouldInvokeFns={true} />;
$[5] = fn;
$[6] = x;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ a: null, cond: true }],
sequentialRenders: [
{ a: null, cond: true },
{ a: { b: { c: 4 } }, cond: true },
{ a: { b: { c: 4 } }, cond: true },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[4],"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[4],"shouldInvokeFns":true}</div>
@@ -0,0 +1,25 @@
// @enablePropagateDepsInHIR
import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime';
function Foo({a, cond}) {
// Assume fn will be uncond evaluated, so we can safely evaluate {a.<any>,
// a.b.<any}
const fn = () => [a, a.b.c];
useIdentity(null);
const x = makeArray();
if (cond) {
x.push(identity(a.b.c));
}
return <Stringify fn={fn} x={x} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, cond: true}],
sequentialRenders: [
{a: null, cond: true},
{a: {b: {c: 4}}, cond: true},
{a: {b: {c: 4}}, cond: true},
],
};
@@ -0,0 +1,73 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {mutate, shallowCopy, Stringify} from 'shared-runtime';
function useFoo({a}: {a: {b: {c: number}}}) {
const local = shallowCopy(a);
mutate(local);
const fn = () => local.b.c;
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { mutate, shallowCopy, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(6);
const { a } = t0;
let local;
if ($[0] !== a) {
local = shallowCopy(a);
mutate(local);
$[0] = a;
$[1] = local;
} else {
local = $[1];
}
let t1;
if ($[2] !== local.b.c) {
t1 = () => local.b.c;
$[2] = local.b.c;
$[3] = t1;
} else {
t1 = $[3];
}
const fn = t1;
let t2;
if ($[4] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[4] = fn;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,16 @@
// @enablePropagateDepsInHIR
import {mutate, shallowCopy, Stringify} from 'shared-runtime';
function useFoo({a}: {a: {b: {c: number}}}) {
const local = shallowCopy(a);
mutate(local);
const fn = () => local.b.c;
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
@@ -0,0 +1,91 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime';
function Foo({a, cond}) {
// Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c.<any>
const fn = () => [a, a.b?.c.d];
useIdentity(null);
const arr = makeArray();
if (cond) {
arr.push(identity(a.b?.c.e));
}
return <Stringify fn={fn} arr={arr} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, cond: true}],
sequentialRenders: [
{a: null, cond: true},
{a: {b: {c: {d: 5}}}, cond: true},
{a: {b: null}, cond: false},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { identity, makeArray, Stringify, useIdentity } from "shared-runtime";
function Foo(t0) {
const $ = _c(8);
const { a, cond } = t0;
let t1;
if ($[0] !== a) {
t1 = () => [a, a.b?.c.d];
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
const fn = t1;
useIdentity(null);
let arr;
if ($[2] !== cond || $[3] !== a.b?.c.e) {
arr = makeArray();
if (cond) {
arr.push(identity(a.b?.c.e));
}
$[2] = cond;
$[3] = a.b?.c.e;
$[4] = arr;
} else {
arr = $[4];
}
let t2;
if ($[5] !== fn || $[6] !== arr) {
t2 = <Stringify fn={fn} arr={arr} shouldInvokeFns={true} />;
$[5] = fn;
$[6] = arr;
$[7] = t2;
} else {
t2 = $[7];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ a: null, cond: true }],
sequentialRenders: [
{ a: null, cond: true },
{ a: { b: { c: { d: 5 } } }, cond: true },
{ a: { b: null }, cond: false },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function","result":[{"b":{"c":{"d":5}}},5]},"arr":[null],"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":[{"b":null},null]},"arr":[],"shouldInvokeFns":true}</div>
@@ -0,0 +1,24 @@
// @enablePropagateDepsInHIR
import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime';
function Foo({a, cond}) {
// Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c.<any>
const fn = () => [a, a.b?.c.d];
useIdentity(null);
const arr = makeArray();
if (cond) {
arr.push(identity(a.b?.c.e));
}
return <Stringify fn={fn} arr={arr} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, cond: true}],
sequentialRenders: [
{a: null, cond: true},
{a: {b: {c: {d: 5}}}, cond: true},
{a: {b: null}, cond: false},
],
};
@@ -0,0 +1,73 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {shallowCopy, Stringify, mutate} from 'shared-runtime';
function useFoo({a}: {a: {b: {c: number}}}) {
const local = shallowCopy(a);
mutate(local);
const fn = () => [() => local.b.c];
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { shallowCopy, Stringify, mutate } from "shared-runtime";
function useFoo(t0) {
const $ = _c(6);
const { a } = t0;
let local;
if ($[0] !== a) {
local = shallowCopy(a);
mutate(local);
$[0] = a;
$[1] = local;
} else {
local = $[1];
}
let t1;
if ($[2] !== local.b.c) {
t1 = () => [() => local.b.c];
$[2] = local.b.c;
$[3] = t1;
} else {
t1 = $[3];
}
const fn = t1;
let t2;
if ($[4] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[4] = fn;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
<div>{"fn":{"kind":"Function","result":[{"kind":"Function","result":4}]},"shouldInvokeFns":true}</div>
@@ -0,0 +1,16 @@
// @enablePropagateDepsInHIR
import {shallowCopy, Stringify, mutate} from 'shared-runtime';
function useFoo({a}: {a: {b: {c: number}}}) {
const local = shallowCopy(a);
mutate(local);
const fn = () => [() => local.b.c];
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
@@ -0,0 +1,66 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
const fn = () => {
return () => ({
value: a.b.c,
});
};
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
t1 = () => () => ({ value: a.b.c });
$[0] = a.b.c;
$[1] = t1;
} else {
t1 = $[1];
}
const fn = t1;
let t2;
if ($[2] !== fn) {
t2 = <Stringify fn={fn} shouldInvokeFns={true} />;
$[2] = fn;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function","result":{"kind":"Function","result":{"value":4}}},"shouldInvokeFns":true}</div>
@@ -0,0 +1,18 @@
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
const fn = () => {
return () => ({
value: a.b.c,
});
};
return <Stringify fn={fn} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
@@ -0,0 +1,70 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {identity, Stringify} from 'shared-runtime';
function useFoo({a}) {
const x = {
fn() {
return identity(a.b.c);
},
};
return <Stringify x={x} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { identity, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a.b.c) {
t1 = {
fn() {
return identity(a.b.c);
},
};
$[0] = a.b.c;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let t2;
if ($[2] !== x) {
t2 = <Stringify x={x} shouldInvokeFns={true} />;
$[2] = x;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"x":{"fn":{"kind":"Function","result":4}},"shouldInvokeFns":true}</div>
@@ -0,0 +1,18 @@
// @enablePropagateDepsInHIR
import {identity, Stringify} from 'shared-runtime';
function useFoo({a}) {
const x = {
fn() {
return identity(a.b.c);
},
};
return <Stringify x={x} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
};
@@ -0,0 +1,64 @@
## Input
```javascript
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [
{a: null},
{a: {b: null}},
{a: {b: {c: {d: null}}}},
{a: {b: {c: {d: {e: 4}}}}},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
import { Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b) {
t1 = <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
$[0] = a.b;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [
{ a: null },
{ a: { b: null } },
{ a: { b: { c: { d: null } } } },
{ a: { b: { c: { d: { e: 4 } } } } },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function"},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function"},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,18 @@
// @enablePropagateDepsInHIR
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [
{a: null},
{a: {b: null}},
{a: {b: {c: {d: null}}}},
{a: {b: {c: {d: {e: 4}}}}},
],
};
@@ -0,0 +1,73 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
function Foo({a, shouldReadA}) {
return (
<Stringify
fn={() => {
if (shouldReadA) return a.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Foo(t0) {
const $ = _c(3);
const { a, shouldReadA } = t0;
let t1;
if ($[0] !== shouldReadA || $[1] !== a.b.c) {
t1 = (
<Stringify
fn={() => {
if (shouldReadA) {
return a.b.c;
}
return null;
}}
shouldInvokeFns={true}
/>
);
$[0] = shouldReadA;
$[1] = a.b.c;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ a: null, shouldReadA: true }],
sequentialRenders: [
{ a: null, shouldReadA: true },
{ a: null, shouldReadA: false },
{ a: { b: { c: 4 } }, shouldReadA: true },
],
};
```
@@ -0,0 +1,23 @@
import {Stringify} from 'shared-runtime';
function Foo({a, shouldReadA}) {
return (
<Stringify
fn={() => {
if (shouldReadA) return a.b.c;
return null;
}}
shouldInvokeFns={true}
/>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{a: null, shouldReadA: true}],
sequentialRenders: [
{a: null, shouldReadA: true},
{a: null, shouldReadA: false},
{a: {b: {c: 4}}, shouldReadA: true},
],
};
@@ -0,0 +1,61 @@
## Input
```javascript
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [
{a: null},
{a: {b: null}},
{a: {b: {c: {d: null}}}},
{a: {b: {c: {d: {e: 4}}}}},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const { a } = t0;
let t1;
if ($[0] !== a.b) {
t1 = <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
$[0] = a.b;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: null }],
sequentialRenders: [
{ a: null },
{ a: { b: null } },
{ a: { b: { c: { d: null } } } },
{ a: { b: { c: { d: { e: 4 } } } } },
],
};
```
### Eval output
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
<div>{"fn":{"kind":"Function"},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function"},"shouldInvokeFns":true}</div>
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
@@ -0,0 +1,16 @@
import {Stringify} from 'shared-runtime';
function useFoo({a}) {
return <Stringify fn={() => a.b?.c.d?.e} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: null}],
sequentialRenders: [
{a: null},
{a: {b: null}},
{a: {b: {c: {d: null}}}},
{a: {b: {c: {d: {e: 4}}}}},
],
};
@@ -479,6 +479,7 @@ const skipFilter = new Set([
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
'bug-invalid-hoisting-functionexpr',
'bug-try-catch-maybe-null-dependency',
'reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted',
'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',