[compiler][rewrite] Patch logic for aligning scopes to non-value blocks

Our previous logic for aligning scopes to block scopes constructs a tree of block and scope nodes. We ensured that blocks always mapped to the same node as their fallthroughs. e.g.
```js
// source
a();
if (...) {
  b();
}
c();

// HIR
bb0:
a()
if test=... consequent=bb1 fallthrough=bb2

bb1:
b()
goto bb2

bb2:
c()

// AlignReactiveScopesToBlockScopesHIR nodes
Root node (maps to both bb0 and bb2)
  |- bb1
  |- ...
```

There are two issues with the existing implementation:
1. Only scopes that overlap with the beginning of a block are aligned correctly. This is because the traversal does not store information about the block-fallthrough pair for scopes that begin *within* the block-fallthrough range.
```
\# This case gets handled correctly
         ┌──────────────┐
         │              │
         block start    block end

scope start     scope end
│               │
└───────────────┘

\# But not this one!
┌──────────────┐
│              │
block start    block end

          scope start     scope end
          │               │
          └───────────────┘
```
2. Only scopes that are directly used by a block is considered. See the `align-scopes-nested-block-structure` fixture for details.

ghstack-source-id: 327dec5019
Pull Request resolved: https://github.com/facebook/react/pull/29891
This commit is contained in:
Mofei Zhang
2024-06-25 17:00:13 -04:00
parent ca0b3aeff1
commit aa61d7259d
29 changed files with 841 additions and 411 deletions
@@ -22,8 +22,10 @@ import {
mapTerminalSuccessors,
terminalFallthrough,
} from "../HIR/visitors";
import { retainWhere_Set } from "../Utils/utils";
import { getPlaceScope } from "./BuildReactiveBlocks";
type InstructionRange = MutableRange;
/*
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
* reactive scopes (independently memoizeable units of code):
@@ -66,18 +68,20 @@ import { getPlaceScope } from "./BuildReactiveBlocks";
* will be the updated end for that scope).
*/
export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
const blockNodes = new Map<BlockId, BlockNode>();
const rootNode: BlockNode = {
kind: "node",
valueRange: null,
children: [],
id: makeInstructionId(0),
};
blockNodes.set(fn.body.entry, rootNode);
const activeBlockFallthroughRanges: Array<{
range: InstructionRange;
fallthrough: BlockId;
}> = [];
const activeScopes = new Set<ReactiveScope>();
const seen = new Set<ReactiveScope>();
const valueBlockNodes = new Map<BlockId, ValueBlockNode>();
const placeScopes = new Map<Place, ReactiveScope>();
function recordPlace(id: InstructionId, place: Place, node: BlockNode): void {
function recordPlace(
id: InstructionId,
place: Place,
node: ValueBlockNode | null
): void {
if (place.identifier.scope !== null) {
placeScopes.set(place, place.identifier.scope);
}
@@ -86,13 +90,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
if (scope == null) {
return;
}
node.children.push({ kind: "scope", scope, id });
activeScopes.add(scope);
node?.children.push({ kind: "scope", scope, id });
if (seen.has(scope)) {
return;
}
seen.add(scope);
if (node.valueRange !== null) {
if (node != null && node.valueRange !== null) {
scope.range.start = makeInstructionId(
Math.min(node.valueRange.start, scope.range.start)
);
@@ -103,16 +108,25 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
}
for (const [, block] of fn.body.blocks) {
const { instructions, terminal } = block;
const node = blockNodes.get(block.id);
if (node === undefined) {
CompilerError.invariant(false, {
reason: `Expected a node to be initialized for block`,
loc: instructions[0]?.loc ?? terminal.loc,
description: `No node for block bb${block.id} (${block.kind})`,
});
const startingId = block.instructions[0]?.id ?? block.terminal.id;
retainWhere_Set(activeScopes, (scope) => scope.range.end > startingId);
const top = activeBlockFallthroughRanges.at(-1);
if (top?.fallthrough === block.id) {
activeBlockFallthroughRanges.pop();
/*
* All active scopes must have either started before or within the last
* block-fallthrough range. In either case, they overlap this block-
* fallthrough range and can have their ranges extended.
*/
for (const scope of activeScopes) {
scope.range.start = makeInstructionId(
Math.min(scope.range.start, top.range.start)
);
}
}
const { instructions, terminal } = block;
const node = valueBlockNodes.get(block.id) ?? null;
for (const instr of instructions) {
for (const lvalue of eachInstructionLValue(instr)) {
recordPlace(instr.id, lvalue, node);
@@ -125,36 +139,42 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
recordPlace(terminal.id, operand, node);
}
// Save the current node for the fallback block, where this block scope continues
const fallthrough = terminalFallthrough(terminal);
if (fallthrough !== null && !blockNodes.has(fallthrough)) {
if (fallthrough !== null) {
/*
* Any scopes that carried over across a terminal->fallback need their range extended
* to at least the first instruction of the fallback
*
* Note that it's possible for a terminal such as an if or switch to have a null fallback,
* indicating that all control-flow paths diverge instead of reaching the fallthrough.
* In this case there isn't an instruction id in the program that we can point to for the
* updated range. Since the output is correct in this case we leave it, but it would be
* more correct to find the maximum instuction id in the whole program and set the range.end
* to one greater. Alternatively, we could leave in an unreachable fallthrough (with a new
* "unreachable" terminal variant, perhaps) and use that instruction id.
* Any currently active scopes that overlaps the block-fallthrough range
* need their range extended to at least the first instruction of the
* fallthrough
*/
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
const nextId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
for (const child of node.children) {
if (child.kind !== "scope") {
continue;
}
const scope = child.scope;
for (const scope of activeScopes) {
if (scope.range.end > terminal.id) {
scope.range.end = makeInstructionId(
Math.max(scope.range.end, nextId)
);
}
}
blockNodes.set(fallthrough, node);
/**
* We also record the block-fallthrough range for future scopes that begin
* within the range (and overlap with the range end).
*/
activeBlockFallthroughRanges.push({
fallthrough,
range: {
start: terminal.id,
end: nextId,
},
});
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
reason: "Expect hir blocks to have unique fallthroughs",
loc: terminal.loc,
});
if (node != null) {
valueBlockNodes.set(fallthrough, node);
}
}
/*
@@ -166,48 +186,35 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
* just those that are direct successors for normal control-flow ordering.
*/
mapTerminalSuccessors(terminal, (successor) => {
if (blockNodes.has(successor)) {
if (valueBlockNodes.has(successor)) {
return successor;
}
const successorBlock = fn.body.blocks.get(successor)!;
/*
* we need the block kind check here because the do..while terminal's successor
* is a block, and try's successor is a catch block
*/
if (successorBlock.kind === "block" || successorBlock.kind === "catch") {
const childNode: BlockNode = {
kind: "node",
id: terminal.id,
children: [],
valueRange: null,
};
node.children.push(childNode);
blockNodes.set(successor, childNode);
/*
* we need the block kind check here because the do..while terminal's
* successor is a block, and try's successor is a catch block
*/
} else if (
node.valueRange === null ||
node == null ||
terminal.kind === "ternary" ||
terminal.kind === "logical" ||
terminal.kind === "optional"
) {
/**
* Create a new scope node whenever we transition from block scope -> value scope.
* Create a new node whenever we transition from non-value -> value block.
*
* For compatibility with the previous ReactiveFunction-based scope merging logic,
* we also create new scope nodes for ternary, logical, and optional terminals.
* However, inside value blocks we always store a range (valueRange) that is the
* Inside value blocks we always store a range (valueRange) that is the
* start/end instruction ids at the nearest parent block scope level, so that
* scopes inside the value blocks can be extended to align with block scope
* instructions.
*/
const childNode = {
kind: "node",
id: terminal.id,
children: [],
valueRange: null,
} as BlockNode;
if (node.valueRange === null) {
// Transition from block->value scope, derive the outer block scope range
let valueRange: MutableRange;
if (node == null) {
// Transition from block->value block, derive the outer block range
CompilerError.invariant(fallthrough !== null, {
reason: `Expected a fallthrough for value block`,
loc: terminal.loc,
@@ -216,32 +223,36 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
const nextId =
fallthroughBlock.instructions[0]?.id ??
fallthroughBlock.terminal.id;
childNode.valueRange = {
valueRange = {
start: terminal.id,
end: nextId,
};
} else {
// else value->value transition, reuse the range
childNode.valueRange = node.valueRange;
valueRange = node.valueRange;
}
node.children.push(childNode);
blockNodes.set(successor, childNode);
const childNode: ValueBlockNode = {
kind: "node",
id: terminal.id,
children: [],
valueRange,
};
node?.children.push(childNode);
valueBlockNodes.set(successor, childNode);
} else {
// this is a value -> value block transition, reuse the node
blockNodes.set(successor, node);
valueBlockNodes.set(successor, node);
}
return successor;
});
}
// console.log(_debug(rootNode));
}
type BlockNode = {
type ValueBlockNode = {
kind: "node";
id: InstructionId;
valueRange: MutableRange | null;
children: Array<BlockNode | ReactiveScopeNode>;
valueRange: MutableRange;
children: Array<ValueBlockNode | ReactiveScopeNode>;
};
type ReactiveScopeNode = {
kind: "scope";
@@ -249,13 +260,13 @@ type ReactiveScopeNode = {
scope: ReactiveScope;
};
function _debug(node: BlockNode): string {
function _debug(node: ValueBlockNode): string {
const buf: Array<string> = [];
_printNode(node, buf, 0);
return buf.join("\n");
}
function _printNode(
node: BlockNode | ReactiveScopeNode,
node: ValueBlockNode | ReactiveScopeNode,
out: Array<string>,
depth: number = 0
): void {
@@ -265,10 +276,7 @@ function _printNode(
`${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]`
);
} else {
let range =
node.valueRange !== null
? ` [${node.valueRange.start}:${node.valueRange.end}]`
: "";
let range = ` (range=[${node.valueRange.start}:${node.valueRange.end}])`;
out.push(`${prefix}[${node.id}] node${range} [`);
for (const child of node.children) {
_printNode(child, out, depth + 1);
@@ -45,6 +45,17 @@ export function retainWhere<T>(
array.length = writeIndex;
}
export function retainWhere_Set<T>(
items: Set<T>,
predicate: (item: T) => boolean
): void {
for (const item of items) {
if (!predicate(item)) {
items.delete(item);
}
}
}
export function getOrInsertWith<U, V>(
m: Map<U, V>,
key: U,
@@ -0,0 +1,76 @@
## Input
```javascript
import { mutate } from "shared-runtime";
/**
* Similar fixture to `align-scopes-nested-block-structure`, but
* a simpler case.
*/
function useFoo(cond) {
let s = null;
if (cond) {
s = {};
} else {
return null;
}
mutate(s);
return s;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [true],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutate } from "shared-runtime";
/**
* Similar fixture to `align-scopes-nested-block-structure`, but
* a simpler case.
*/
function useFoo(cond) {
const $ = _c(3);
let s;
let t0;
if ($[0] !== cond) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
if (cond) {
s = {};
} else {
t0 = null;
break bb0;
}
mutate(s);
}
$[0] = cond;
$[1] = t0;
$[2] = s;
} else {
t0 = $[1];
s = $[2];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
}
return s;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [true],
};
```
### Eval output
(kind: ok) {"wat0":"joe"}
@@ -0,0 +1,21 @@
import { mutate } from "shared-runtime";
/**
* Similar fixture to `align-scopes-nested-block-structure`, but
* a simpler case.
*/
function useFoo(cond) {
let s = null;
if (cond) {
s = {};
} else {
return null;
}
mutate(s);
return s;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [true],
};
@@ -0,0 +1,55 @@
## Input
```javascript
import { getNull } from "shared-runtime";
function Component(props) {
const items = (() => {
return getNull() ?? [];
})();
items.push(props.a);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: {} }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { getNull } from "shared-runtime";
function Component(props) {
const $ = _c(3);
let t0;
let items;
if ($[0] !== props.a) {
t0 = getNull() ?? [];
items = t0;
items.push(props.a);
$[0] = props.a;
$[1] = items;
$[2] = t0;
} else {
items = $[1];
t0 = $[2];
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: {} }],
};
```
### Eval output
(kind: ok) [{}]
@@ -0,0 +1,169 @@
## Input
```javascript
import { mutate } from "shared-runtime";
/**
* Fixture showing that it's not sufficient to only align direct scoped
* accesses of a block-fallthrough pair.
* Below is a simplified view of HIR blocks in this fixture.
* Note that here, s is mutated in both bb1 and bb4. However, neither
* bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
*
* This means that we need to recursively visit all scopes accessed between
* a block and its fallthrough and extend the range of those scopes which overlap
* with an active block/fallthrough pair,
*
* bb0
* ┌──────────────┐
* │let s = null │
* │test cond1 │
* │ <fallthr=bb3>│
* └┬─────────────┘
* │ bb1
* ├─►┌───────┐
* │ │s = {} ├────┐
* │ └───────┘ │
* │ bb2 │
* └─►┌───────┐ │
* │return;│ │
* └───────┘ │
* bb3 │
* ┌──────────────┐◄┘
* │test cond2 │
* │ <fallthr=bb5>│
* └┬─────────────┘
* │ bb4
* ├─►┌─────────┐
* │ │mutate(s)├─┐
* ▼ └─────────┘ │
* bb5 │
* ┌───────────┐ │
* │return s; │◄──┘
* └───────────┘
*/
function useFoo({ cond1, cond2 }) {
let s = null;
if (cond1) {
s = {};
} else {
return null;
}
if (cond2) {
mutate(s);
}
return s;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond1: true, cond2: false }],
sequentialRenders: [
{ cond1: true, cond2: false },
{ cond1: true, cond2: false },
{ cond1: true, cond2: true },
{ cond1: true, cond2: true },
{ cond1: false, cond2: true },
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutate } from "shared-runtime";
/**
* Fixture showing that it's not sufficient to only align direct scoped
* accesses of a block-fallthrough pair.
* Below is a simplified view of HIR blocks in this fixture.
* Note that here, s is mutated in both bb1 and bb4. However, neither
* bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
*
* This means that we need to recursively visit all scopes accessed between
* a block and its fallthrough and extend the range of those scopes which overlap
* with an active block/fallthrough pair,
*
* bb0
* ┌──────────────┐
* │let s = null │
* │test cond1 │
* │ <fallthr=bb3>│
* └┬─────────────┘
* │ bb1
* ├─►┌───────┐
* │ │s = {} ├────┐
* │ └───────┘ │
* │ bb2 │
* └─►┌───────┐ │
* │return;│ │
* └───────┘ │
* bb3 │
* ┌──────────────┐◄┘
* │test cond2 │
* │ <fallthr=bb5>│
* └┬─────────────┘
* │ bb4
* ├─►┌─────────┐
* │ │mutate(s)├─┐
* ▼ └─────────┘ │
* bb5 │
* ┌───────────┐ │
* │return s; │◄──┘
* └───────────┘
*/
function useFoo(t0) {
const $ = _c(4);
const { cond1, cond2 } = t0;
let s;
let t1;
if ($[0] !== cond1 || $[1] !== cond2) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: {
if (cond1) {
s = {};
} else {
t1 = null;
break bb0;
}
if (cond2) {
mutate(s);
}
}
$[0] = cond1;
$[1] = cond2;
$[2] = t1;
$[3] = s;
} else {
t1 = $[2];
s = $[3];
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
}
return s;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond1: true, cond2: false }],
sequentialRenders: [
{ cond1: true, cond2: false },
{ cond1: true, cond2: false },
{ cond1: true, cond2: true },
{ cond1: true, cond2: true },
{ cond1: false, cond2: true },
],
};
```
### Eval output
(kind: ok) {}
{}
{"wat0":"joe"}
{"wat0":"joe"}
null
@@ -1,7 +1,4 @@
## Input
```javascript
import { mutate } from "shared-runtime";
/**
* Fixture showing that it's not sufficient to only align direct scoped
* accesses of a block-fallthrough pair.
@@ -41,7 +38,7 @@
* │return s; │◄──┘
* └───────────┘
*/
function useFoo(cond1, cond2) {
function useFoo({ cond1, cond2 }) {
let s = null;
if (cond1) {
s = {};
@@ -56,13 +53,14 @@ function useFoo(cond1, cond2) {
return s;
}
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:15)
```
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond1: true, cond2: false }],
sequentialRenders: [
{ cond1: true, cond2: false },
{ cond1: true, cond2: false },
{ cond1: true, cond2: true },
{ cond1: true, cond2: true },
{ cond1: false, cond2: true },
],
};
@@ -0,0 +1,84 @@
## Input
```javascript
function useFoo({ cond }) {
let items: any = {};
b0: {
if (cond) {
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the if-branch
items = [];
} else {
break b0;
}
items.push(2);
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true }],
sequentialRenders: [
{ cond: true },
{ cond: true },
{ cond: false },
{ cond: false },
{ cond: true },
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function useFoo(t0) {
const $ = _c(3);
const { cond } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {};
$[0] = t1;
} else {
t1 = $[0];
}
let items = t1;
bb0: if ($[1] !== cond) {
if (cond) {
items = [];
} else {
break bb0;
}
items.push(2);
$[1] = cond;
$[2] = items;
} else {
items = $[2];
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true }],
sequentialRenders: [
{ cond: true },
{ cond: true },
{ cond: false },
{ cond: false },
{ cond: true },
],
};
```
### Eval output
(kind: ok) [2]
[2]
{}
{}
[2]
@@ -0,0 +1,79 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
function useFoo({ cond, value }) {
let items;
label: {
items = [];
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the label-block
if (cond) break label;
arrayPush(items, value);
}
arrayPush(items, value);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true, value: 2 }],
sequentialRenders: [
{ cond: true, value: 2 },
{ cond: true, value: 2 },
{ cond: true, value: 3 },
{ cond: false, value: 3 },
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
function useFoo(t0) {
const $ = _c(3);
const { cond, value } = t0;
let items;
if ($[0] !== cond || $[1] !== value) {
bb0: {
items = [];
if (cond) {
break bb0;
}
arrayPush(items, value);
}
arrayPush(items, value);
$[0] = cond;
$[1] = value;
$[2] = items;
} else {
items = $[2];
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true, value: 2 }],
sequentialRenders: [
{ cond: true, value: 2 },
{ cond: true, value: 2 },
{ cond: true, value: 3 },
{ cond: false, value: 3 },
],
};
```
### Eval output
(kind: ok) [2]
[2]
[3]
[3,3]
@@ -0,0 +1,65 @@
## Input
```javascript
import { arrayPush, mutate } from "shared-runtime";
function useFoo({ value }) {
let items = null;
try {
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the try-block
items = [];
arrayPush(items, value);
} catch {
// ignore
}
mutate(items);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, mutate } from "shared-runtime";
function useFoo(t0) {
const $ = _c(2);
const { value } = t0;
let items;
if ($[0] !== value) {
try {
items = [];
arrayPush(items, value);
} catch {}
mutate(items);
$[0] = value;
$[1] = items;
} else {
items = $[1];
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
};
```
### Eval output
(kind: ok) [2]
[2]
[3]
@@ -1,4 +1,4 @@
import { arrayPush } from "shared-runtime";
import { arrayPush, mutate } from "shared-runtime";
function useFoo({ value }) {
let items = null;
@@ -0,0 +1,61 @@
## Input
```javascript
import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
function Foo() {
try {
let thing = null;
if (cond) {
thing = makeObject_Primitives();
}
if (CONST_TRUE) {
mutate(thing);
}
return thing;
} catch {}
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
function Foo() {
const $ = _c(1);
try {
let thing;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
thing = null;
if (cond) {
thing = makeObject_Primitives();
}
if (CONST_TRUE) {
mutate(thing);
}
$[0] = thing;
} else {
thing = $[0];
}
return thing;
} catch {}
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
```
### Eval output
(kind: ok)
@@ -0,0 +1,19 @@
import { CONST_TRUE, makeObject_Primitives } from "shared-runtime";
function Foo() {
try {
let thing = null;
if (cond) {
thing = makeObject_Primitives();
}
if (CONST_TRUE) {
mutate(thing);
}
return thing;
} catch {}
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}],
};
@@ -1,26 +0,0 @@
## Input
```javascript
function Foo() {
try {
let thing = null;
if (cond) {
thing = makeObject();
}
if (otherCond) {
mutate(thing);
}
} catch {}
}
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:24(18:26)
```
@@ -1,11 +0,0 @@
function Foo() {
try {
let thing = null;
if (cond) {
thing = makeObject();
}
if (otherCond) {
mutate(thing);
}
} catch {}
}
@@ -1,27 +0,0 @@
## Input
```javascript
function Foo(props, ref) {
const value = {};
if (cond1) {
mutate(value);
return <Child ref={ref} />;
}
mutate(value);
if (cond2) {
return <Child ref={identity(ref)} />;
}
return value;
}
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 1:21(16:23)
```
@@ -1,12 +0,0 @@
function Foo(props, ref) {
const value = {};
if (cond1) {
mutate(value);
return <Child ref={ref} />;
}
mutate(value);
if (cond2) {
return <Child ref={identity(ref)} />;
}
return value;
}
@@ -1,29 +0,0 @@
## Input
```javascript
/**
* Similar fixture to `error.todo-align-scopes-nested-block-structure`, but
* a simpler case.
*/
function useFoo(cond) {
let s = null;
if (cond) {
s = {};
} else {
return null;
}
mutate(s);
return s;
}
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:13)
```
@@ -1,14 +0,0 @@
/**
* Similar fixture to `error.todo-align-scopes-nested-block-structure`, but
* a simpler case.
*/
function useFoo(cond) {
let s = null;
if (cond) {
s = {};
} else {
return null;
}
mutate(s);
return s;
}
@@ -1,53 +0,0 @@
/**
* Fixture showing that it's not sufficient to only align direct scoped
* accesses of a block-fallthrough pair.
* Below is a simplified view of HIR blocks in this fixture.
* Note that here, s is mutated in both bb1 and bb4. However, neither
* bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves.
*
* This means that we need to recursively visit all scopes accessed between
* a block and its fallthrough and extend the range of those scopes which overlap
* with an active block/fallthrough pair,
*
* bb0
* ┌──────────────┐
* │let s = null │
* │test cond1 │
* │ <fallthr=bb3>│
* └┬─────────────┘
* │ bb1
* ├─►┌───────┐
* │ │s = {} ├────┐
* │ └───────┘ │
* │ bb2 │
* └─►┌───────┐ │
* │return;│ │
* └───────┘ │
* bb3 │
* ┌──────────────┐◄┘
* │test cond2 │
* │ <fallthr=bb5>│
* └┬─────────────┘
* │ bb4
* ├─►┌─────────┐
* │ │mutate(s)├─┐
* ▼ └─────────┘ │
* bb5 │
* ┌───────────┐ │
* │return s; │◄──┘
* └───────────┘
*/
function useFoo(cond1, cond2) {
let s = null;
if (cond1) {
s = {};
} else {
return null;
}
if (cond2) {
mutate(s);
}
return s;
}
@@ -1,29 +0,0 @@
## Input
```javascript
import { getNull } from "shared-runtime";
function Component(props) {
const items = (() => {
return getNull() ?? [];
})();
items.push(props.a);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ a: {} }],
};
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:15(3:21)
```
@@ -1,41 +0,0 @@
## Input
```javascript
function useFoo({ cond }) {
let items: any = {};
b0: {
if (cond) {
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the if-branch
items = [];
} else {
break b0;
}
items.push(2);
}
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true }],
sequentialRenders: [
{ cond: true },
{ cond: true },
{ cond: false },
{ cond: false },
{ cond: true },
],
};
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 6:11(7:15)
```
@@ -1,40 +0,0 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
function useFoo({ cond, value }) {
let items;
label: {
items = [];
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the label-block
if (cond) break label;
arrayPush(items, value);
}
arrayPush(items, value);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ cond: true, value: 2 }],
sequentialRenders: [
{ cond: true, value: 2 },
{ cond: true, value: 2 },
{ cond: true, value: 3 },
{ cond: false, value: 3 },
],
};
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 3:14(4:18)
```
@@ -1,36 +0,0 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
function useFoo({ value }) {
let items = null;
try {
// Mutable range of `items` begins here, but its reactive scope block
// should be aligned to above the try-block
items = [];
arrayPush(items, value);
} catch {
// ignore
}
mutate(items);
return items;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ value: 2 }],
sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
};
```
## Error
```
Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:19(5:22)
```
@@ -0,0 +1,83 @@
## Input
```javascript
import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
function Foo(props, ref) {
const value = {};
if (CONST_TRUE) {
mutate(value);
return <Stringify ref={ref} />;
}
mutate(value);
if (CONST_TRUE) {
return <Stringify ref={identity(ref)} />;
}
return value;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}, { current: "fake-ref-object" }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
function Foo(props, ref) {
const $ = _c(5);
let value;
let t0;
if ($[0] !== ref) {
t0 = Symbol.for("react.early_return_sentinel");
bb0: {
value = {};
if (CONST_TRUE) {
mutate(value);
t0 = <Stringify ref={ref} />;
break bb0;
}
mutate(value);
if (CONST_TRUE) {
const t1 = identity(ref);
let t2;
if ($[3] !== t1) {
t2 = <Stringify ref={t1} />;
$[3] = t1;
$[4] = t2;
} else {
t2 = $[4];
}
t0 = t2;
break bb0;
}
}
$[0] = ref;
$[1] = value;
$[2] = t0;
} else {
value = $[1];
t0 = $[2];
}
if (t0 !== Symbol.for("react.early_return_sentinel")) {
return t0;
}
return value;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}, { current: "fake-ref-object" }],
};
```
### Eval output
(kind: ok) <div>{"ref":{"current":"fake-ref-object"}}</div>
@@ -0,0 +1,19 @@
import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
function Foo(props, ref) {
const value = {};
if (CONST_TRUE) {
mutate(value);
return <Stringify ref={ref} />;
}
mutate(value);
if (CONST_TRUE) {
return <Stringify ref={identity(ref)} />;
}
return value;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{}, { current: "fake-ref-object" }],
};