compiler: merge reactive scopes across const StoreLocal (#29095)

@jbonta nerd-sniped me into making this optimization during conference
prep, posting this as a PR now that keynote is over.

Consider these two cases:

```javascript
export default function MyApp1({ count }) {
  const cb = () => count;
  return <div onclick={cb}>Hello World</div>;
}

export default function MyApp2({ count }) {
  return <div onclick={() => count}>Hello World</div>;
}
```

Previously, the former would create two reactive scopes (one for `cb`,
one for the div) while the latter would only have a single scope for the
`div` and its inline callback. The reason we created separate scopes
before is that there's a `StoreLocal 'cb' = t0` instruction in-between,
and i had conservatively implemented the merging pass to not allow
intervening StoreLocal instructions.

The observation is that intervening StoreLocals are fine _if_ the
assigned variable's last usage is the next scope. We already have a
check that the intervening lvalues are last-used at/before the next
scope, so it's trivial to extend this to support StoreLocal.

Note that we already don't merge scopes if there are intervening
terminals, so we don't have to worry about things like conditional
StoreLocal, conditional access of the resulting value, etc.
This commit is contained in:
Joseph Savona
2024-05-16 17:29:05 -07:00
committed by GitHub
parent 7a149aa162
commit 5ab54718a5
22 changed files with 304 additions and 211 deletions
@@ -9,6 +9,7 @@ import { CompilerError } from "..";
import {
IdentifierId,
InstructionId,
InstructionKind,
Place,
ReactiveBlock,
ReactiveFunction,
@@ -20,6 +21,7 @@ import {
ReactiveStatement,
makeInstructionId,
} from "../HIR";
import { eachInstructionLValue } from "../HIR/visitors";
import { assertExhaustive } from "../Utils/utils";
import { printReactiveScopeSummary } from "./PrintReactiveFunction";
import {
@@ -186,6 +188,33 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
}
break;
}
case "StoreLocal": {
/**
* It's safe to have intervening StoreLocal instructions _if_ they are const
* and the last usage of the variable is at or before the next scope. This is
* similar to the case above for simple instructions.
*
* Reassignments are *not* safe to merge since they are a side-effect that we
* don't want to make conditional.
*/
if (current !== null) {
if (
instr.instruction.value.lvalue.kind === InstructionKind.Const
) {
for (const lvalue of eachInstructionLValue(
instr.instruction
)) {
current.lvalues.add(lvalue.identifier.id);
}
} else {
log(
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`
);
reset();
}
}
break;
}
default: {
// Other instructions are known to prevent merging, so we reset the scope if present
if (current !== null) {
@@ -24,24 +24,17 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[0] = t0;
} else {
t0 = $[0];
}
const z = t0;
const $ = _c(1);
let x;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const z = [];
const y = {};
y.z = z;
x = {};
x.y = y;
$[1] = x;
$[0] = x;
} else {
x = $[1];
x = $[0];
}
return x;
}
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(3);
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
@@ -33,20 +33,13 @@ function Component(props) {
const x = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = x.map((item) => item);
const y = x.map((item) => item);
t1 = [x, y];
$[1] = t1;
} else {
t1 = $[1];
}
const y = t1;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [x, y];
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(3);
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
@@ -33,20 +33,13 @@ function Component(props) {
const x = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = x.map((item) => item);
const y = x.map((item) => item);
t1 = [x, y];
$[1] = t1;
} else {
t1 = $[1];
}
const y = t1;
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [x, y];
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {
@@ -24,30 +24,20 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(3);
const $ = _c(1);
let t0;
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = [];
t0 = x.map((item) => {
const x = [];
const y = x.map((item) => {
item.updated = true;
return item;
});
t0 = [x, y];
$[0] = t0;
$[1] = x;
} else {
t0 = $[0];
x = $[1];
}
const y = t0;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [x, y];
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -24,30 +24,20 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(3);
const $ = _c(1);
let t0;
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = [];
t0 = x.map((item) => {
const x = [];
const y = x.map((item) => {
item.updated = true;
return item;
});
t0 = [x, y];
$[0] = t0;
$[1] = x;
} else {
t0 = $[0];
x = $[1];
}
const y = t0;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [x, y];
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -18,23 +18,17 @@ function foo() {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo() {
const $ = _c(2);
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => <Child x={GLOBAL_IS_X} />;
const getJSX = () => <Child x={GLOBAL_IS_X} />;
t0 = getJSX();
$[0] = t0;
} else {
t0 = $[0];
}
const getJSX = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getJSX();
$[1] = t1;
} else {
t1 = $[1];
}
const result = t1;
const result = t0;
return result;
}
@@ -0,0 +1,70 @@
## Input
```javascript
import { Stringify, makeObject_Primitives } from "shared-runtime";
function Component(props) {
const array = [props.count];
const x = makeObject_Primitives();
const element = <div>{array}</div>;
console.log(x);
return <div>{element}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, makeObject_Primitives } from "shared-runtime";
function Component(props) {
const $ = _c(6);
let t0;
if ($[0] !== props.count) {
t0 = [props.count];
$[0] = props.count;
$[1] = t0;
} else {
t0 = $[1];
}
const array = t0;
const x = makeObject_Primitives();
let t1;
if ($[2] !== array) {
t1 = <div>{array}</div>;
$[2] = array;
$[3] = t1;
} else {
t1 = $[3];
}
const element = t1;
console.log(x);
let t2;
if ($[4] !== element) {
t2 = <div>{element}</div>;
$[4] = element;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
```
### Eval output
(kind: ok) <div><div>42</div></div>
logs: [{ a: 0, b: 'value1', c: true }]
@@ -0,0 +1,14 @@
import { Stringify, makeObject_Primitives } from "shared-runtime";
function Component(props) {
const array = [props.count];
const x = makeObject_Primitives();
const element = <div>{array}</div>;
console.log(x);
return <div>{element}</div>;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
@@ -0,0 +1,80 @@
## Input
```javascript
import { Stringify } from "shared-runtime";
function Component(props) {
let x;
const array = [props.count];
x = array;
const element = <div>{array}</div>;
return (
<div>
{element}
{x}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Component(props) {
const $ = _c(7);
let x;
let t0;
if ($[0] !== props.count) {
t0 = [props.count];
$[0] = props.count;
$[1] = t0;
} else {
t0 = $[1];
}
const array = t0;
x = array;
let t1;
if ($[2] !== array) {
t1 = <div>{array}</div>;
$[2] = array;
$[3] = t1;
} else {
t1 = $[3];
}
const element = t1;
let t2;
if ($[4] !== element || $[5] !== x) {
t2 = (
<div>
{element}
{x}
</div>
);
$[4] = element;
$[5] = x;
$[6] = t2;
} else {
t2 = $[6];
}
return t2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
```
### Eval output
(kind: ok) <div><div>42</div>42</div>
@@ -0,0 +1,19 @@
import { Stringify } from "shared-runtime";
function Component(props) {
let x;
const array = [props.count];
x = array;
const element = <div>{array}</div>;
return (
<div>
{element}
{x}
</div>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ count: 42 }],
};
@@ -27,17 +27,10 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [0, 1, 2, 3];
$[0] = t0;
} else {
t0 = $[0];
}
const x = t0;
const $ = _c(1);
let ret;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const x = [0, 1, 2, 3];
ret = [];
for (const item of x) {
if (item === 0) {
@@ -46,9 +39,9 @@ function Component() {
ret.push(item / 2);
}
$[1] = ret;
$[0] = ret;
} else {
ret = $[1];
ret = $[0];
}
return ret;
}
@@ -27,10 +27,10 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Foo(t0) {
const $ = _c(2);
const $ = _c(1);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = (val) => {
const outer = (val) => {
const fact = (x) => {
if (x <= 0) {
return 1;
@@ -39,19 +39,13 @@ function Foo(t0) {
};
return fact(val);
};
t1 = outer(3);
$[0] = t1;
} else {
t1 = $[0];
}
const outer = t1;
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = outer(3);
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {
@@ -25,28 +25,22 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(t0) {
const $ = _c(2);
const $ = _c(1);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
const outer = () => {
const inner = () => x;
const x = 3;
return inner();
};
t1 = <div>{outer()}</div>;
$[0] = t1;
} else {
t1 = $[0];
}
const outer = t1;
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <div>{outer()}</div>;
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
return t1;
}
export const FIXTURE_ENTRYPOINT = {
@@ -24,23 +24,17 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function useFoo() {
const $ = _c(2);
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => <Stringify value={4} />;
const callback = () => <Stringify value={4} />;
t0 = callback();
$[0] = t0;
} else {
t0 = $[0];
}
const callback = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = callback();
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -24,24 +24,18 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import * as SharedRuntime from "shared-runtime";
function useFoo() {
const $ = _c(2);
const $ = _c(1);
const MyLocal = SharedRuntime;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => <MyLocal.Text value={4} />;
const callback = () => <MyLocal.Text value={4} />;
t0 = callback();
$[0] = t0;
} else {
t0 = $[0];
}
const callback = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = callback();
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -32,34 +32,28 @@ import { c as _c } from "react/compiler-runtime";
const { mutate } = require("shared-runtime");
function Component(props) {
const $ = _c(4);
const $ = _c(3);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {};
const object = {};
t0 = () => {
mutate(object);
};
$[0] = t0;
} else {
t0 = $[0];
}
const object = t0;
const onClick = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => {
mutate(object);
};
$[1] = t1;
if ($[1] !== props.children) {
t1 = <Foo callback={onClick}>{props.children}</Foo>;
$[1] = props.children;
$[2] = t1;
} else {
t1 = $[1];
t1 = $[2];
}
const onClick = t1;
let t2;
if ($[2] !== props.children) {
t2 = <Foo callback={onClick}>{props.children}</Foo>;
$[2] = props.children;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
return t1;
}
function Foo(t0) {
@@ -34,12 +34,12 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime"; // @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions
let cond = true;
function Component(props) {
const $ = _c(2);
const $ = _c(1);
let a;
let b;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
const f = () => {
if (cond) {
a = {};
b = [];
@@ -51,19 +51,13 @@ function Component(props) {
a.property = true;
b.push(false);
};
t0 = <div onClick={f} />;
$[0] = t0;
} else {
t0 = $[0];
}
const f = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <div onClick={f} />;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -22,22 +22,15 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function foo() {
const $ = _c(2);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[0] = t0;
} else {
t0 = $[0];
}
const x = t0;
const $ = _c(1);
let y;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const x = [];
y = {};
y.x = x;
$[1] = y;
$[0] = y;
} else {
y = $[1];
y = $[0];
}
return y;
}
@@ -16,27 +16,17 @@ function Component() {
```javascript
import { c as _c } from "react/compiler-runtime"; // Forget should call the original x (x = foo()) to compute result
function Component() {
const $ = _c(3);
const $ = _c(1);
let t0;
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = foo();
t0 = x((x = bar()), 5);
let x = foo();
const result = x((x = bar()), 5);
t0 = [result, x];
$[0] = t0;
$[1] = x;
} else {
t0 = $[0];
x = $[1];
}
const result = t0;
let t1;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [result, x];
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
return t0;
}
```
@@ -25,29 +25,23 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => {
const callback = () => {
try {
return [];
} catch (t1) {
return;
}
};
t0 = callback();
$[0] = t0;
} else {
t0 = $[0];
}
const callback = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = callback();
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
return t0;
}
export const FIXTURE_ENTRYPOINT = {
@@ -24,26 +24,20 @@ function component() {
```javascript
import { c as _c } from "react/compiler-runtime";
function component() {
const $ = _c(2);
const $ = _c(1);
const p = makePrimitive();
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {};
$[0] = t0;
} else {
t0 = $[0];
}
const o = t0;
let x;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const o = {};
x = {};
x.t = p;
x.t = o;
$[1] = x;
$[0] = x;
} else {
x = $[1];
x = $[0];
}
const y = x.t;
return y;