@samitouri / QOS-React / commits / 10e11c4a88

[HIR] Assert program blocks and scopes are properly nested

ghstack-source-id: e3fe9b5fb8d1314e16e562581f40ca2b2a555b97 Pull Request resolved: https://github.com/facebook/react-forget/pull/2867

Mofei Zhang committed Apr 23, 2024 at 10:18 UTC 10e11c4a88b53f22f481ea92115ee62c05332469
13 files changed +380 -55
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+3
@@ -14,6 +14,7 @@ import {
14 ReactiveFunction,
15 assertConsistentIdentifiers,
16 assertTerminalSuccessorsExist,
17 + assertValidBlockNesting,
18 assertValidMutableRanges,
19 lower,
20 mergeConsecutiveBlocks,
@@ -250,6 +251,8 @@ function* runWithEnvironment(
251 name: "AlignReactiveScopesToBlockScopesHIR",
252 value: hir,
253 });
254 +
255 + assertValidBlockNesting(hir);
256 }
257
258 const reactiveFunction = buildReactiveFunction(hir);
compiler/packages/babel-plugin-react-forget/src/HIR/AssertValidBlockNesting.ts new
+140
@@ -0,0 +1,140 @@
1 +import { CompilerError } from "..";
2 +import {
3 + BlockId,
4 + GeneratedSource,
5 + HIRFunction,
6 + MutableRange,
7 + Place,
8 + ReactiveScope,
9 + ScopeId,
10 +} from "./HIR";
11 +import {
12 + eachInstructionLValue,
13 + eachInstructionOperand,
14 + eachTerminalOperand,
15 + terminalFallthrough,
16 +} from "./visitors";
17 +
18 +/**
19 + * This pass asserts that program blocks and scopes properly form a tree hierarchy
20 + * with respect to block and scope ranges. In other words, two ranges must either
21 + * disjoint or nested.
22 + *
23 + * ProgramBlockSubtree = subtree of basic blocks between a terminal and its fallthrough
24 + * (e.g. continuation in the source AST). This spans every instruction contained within
25 + * the source AST subtree representing the terminal.
26 + *
27 + * In this example, there is a single ProgramBlockSubtree, which spans instructions 1:5
28 + * ```js
29 + * function Foo() {
30 + * [0] a;
31 + * [1] if (cond) {
32 + * [2] b;
33 + * [3] } else {
34 + * [4] c;
35 + * }
36 + * [5] d;
37 + * }
38 + * ```
39 + *
40 + * Scope = reactive scope whose range has been correctly aligned and merged.
41 + */
42 +type Block =
43 + | ({
44 + kind: "ProgramBlockSubtree";
45 + id: BlockId;
46 + } & MutableRange)
47 + | ({
48 + kind: "Scope";
49 + id: ScopeId;
50 + } & MutableRange);
51 +
52 +function getScopes(fn: HIRFunction): Set<ReactiveScope> {
53 + const scopes: Set<ReactiveScope> = new Set();
54 + function visitPlace(place: Place): void {
55 + const scope = place.identifier.scope;
56 + if (scope != null) {
57 + if (scope.range.start !== scope.range.end) {
58 + scopes.add(scope);
59 + }
60 + }
61 + }
62 +
63 + for (const [, block] of fn.body.blocks) {
64 + for (const instr of block.instructions) {
65 + for (const operand of eachInstructionLValue(instr)) {
66 + visitPlace(operand);
67 + }
68 +
69 + for (const operand of eachInstructionOperand(instr)) {
70 + visitPlace(operand);
71 + }
72 + }
73 +
74 + for (const operand of eachTerminalOperand(block.terminal)) {
75 + visitPlace(operand);
76 + }
77 + }
78 +
79 + return scopes;
80 +}
81 +
82 +/**
83 + * Sort range in ascending order of start instruction, breaking ties
84 + * with descending order of end instructions. For overlapping ranges, this
85 + * always orders nested inner range after outer ranges which is identical
86 + * to the ordering of a pre-order tree traversal.
87 + * e.g. we order the following ranges to [0, 4], [0, 2], [5, 8]
88 + * 0 ⌝ ⌝
89 + * 1 ⌟ |
90 + * 2 |
91 + * 3 ⌟
92 + * 4
93 + * 5 ⌝
94 + * 6 |
95 + * 7 ⌟
96 + */
97 +function nestedRangeComparator(a: MutableRange, b: MutableRange): number {
98 + const startDiff = a.start - b.start;
99 + if (startDiff !== 0) return startDiff;
100 + return b.end - a.end;
101 +}
102 +
103 +export function assertValidBlockNesting(fn: HIRFunction): void {
104 + const scopes = getScopes(fn);
105 +
106 + const blocks: Array<Block> = [...scopes].map((scope) => ({
107 + kind: "Scope",
108 + id: scope.id,
109 + ...scope.range,
110 + })) as Array<Block>;
111 + for (const [, block] of fn.body.blocks) {
112 + const fallthroughId = terminalFallthrough(block.terminal);
113 + if (fallthroughId != null) {
114 + const fallthrough = fn.body.blocks.get(fallthroughId)!;
115 + const end = fallthrough.instructions[0]?.id ?? fallthrough.terminal.id;
116 + blocks.push({
117 + kind: "ProgramBlockSubtree",
118 + id: block.id,
119 + start: block.terminal.id,
120 + end,
121 + });
122 + }
123 + }
124 +
125 + blocks.sort(nestedRangeComparator);
126 +
127 + for (let i = 1; i < blocks.length; i++) {
128 + const last = blocks[i - 1];
129 + const curr = blocks[i];
130 +
131 + const blocksDisjoint = curr.start >= last.end;
132 + const blocksNested = curr.end <= last.end;
133 +
134 + CompilerError.invariant(blocksDisjoint || blocksNested, {
135 + reason: "Invalid nesting in program blocks or scopes",
136 + description: `Blocks overlap but are not nested: ${last.kind}@${last.id}(${last.start}:${last.end}) ${curr.kind}@${curr.id}(${curr.start}:${curr.end})`,
137 + loc: GeneratedSource,
138 + });
139 + }
140 +}
compiler/packages/babel-plugin-react-forget/src/HIR/index.ts
+1
@@ -7,6 +7,7 @@
7
8 export { assertConsistentIdentifiers } from "./AssertConsistentIdentifiers";
9 export { assertTerminalSuccessorsExist } from "./AssertTerminalSuccessorsExist";
10 +export { assertValidBlockNesting } from "./AssertValidBlockNesting";
11 export { assertValidMutableRanges } from "./AssertValidMutableRanges";
12 export { lower } from "./BuildHIR";
13 export { computeDominatorTree, computePostDominatorTree } from "./Dominator";
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts
+18
@@ -251,6 +251,24 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
251 );
252 }
253 });
254 + /**
255 + * Join scopes that begin and end at the same instructions
256 + */
257 + {
258 + const allScopes = [...new Set(placeScopes.values())].sort(
259 + (a, b) => a.range.start - b.range.start
260 + );
261 + for (let i = 1; i < allScopes.length; i++) {
262 + const prev = allScopes[i - 1];
263 + const curr = allScopes[i];
264 + if (
265 + prev.range.start === curr.range.start &&
266 + prev.range.end === curr.range.end
267 + ) {
268 + joinedScopes.union([prev, curr]);
269 + }
270 + }
271 + }
272 for (const [place, originalScope] of placeScopes) {
273 const nextScope = joinedScopes.find(originalScope);
274 if (nextScope !== null && nextScope !== originalScope) {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.expect.md new
+29
@@ -0,0 +1,29 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { getNull } from "shared-runtime";
6 +
7 +function Component(props) {
8 + const items = (() => {
9 + return getNull() ?? [];
10 + })();
11 + items.push(props.a);
12 + return items;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{ a: {} }],
18 +};
19 +
20 +```
21 +
22 +
23 +## Error
24 +
25 +```
26 +Invariant: Invalid nesting in program blocks or scopes. Blocks overlap but are not nested: ProgramBlockSubtree@0(2:15) Scope@0(3:21)
27 +```
28 +
29 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.js renamed
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useFoo({ cond }) {
6 + let items: any = {};
7 + b0: {
8 + if (cond) {
9 + // Mutable range of `items` begins here, but its reactive scope block
10 + // should be aligned to above the if-branch
11 + items = [];
12 + } else {
13 + break b0;
14 + }
15 + items.push(2);
16 + }
17 + return items;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ cond: true }],
23 + sequentialRenders: [
24 + { cond: true },
25 + { cond: true },
26 + { cond: false },
27 + { cond: false },
28 + { cond: true },
29 + ],
30 +};
31 +
32 +```
33 +
34 +
35 +## Error
36 +
37 +```
38 +Invariant: Invalid nesting in program blocks or scopes. Blocks overlap but are not nested: ProgramBlockSubtree@2(6:11) Scope@1(7:15)
39 +```
40 +
41 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.ts new
+26
@@ -0,0 +1,26 @@
1 +function useFoo({ cond }) {
2 + let items: any = {};
3 + b0: {
4 + if (cond) {
5 + // Mutable range of `items` begins here, but its reactive scope block
6 + // should be aligned to above the if-branch
7 + items = [];
8 + } else {
9 + break b0;
10 + }
11 + items.push(2);
12 + }
13 + return items;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ cond: true }],
19 + sequentialRenders: [
20 + { cond: true },
21 + { cond: true },
22 + { cond: false },
23 + { cond: false },
24 + { cond: true },
25 + ],
26 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush } from "shared-runtime";
6 +
7 +function useFoo({ cond, value }) {
8 + let items;
9 + label: {
10 + items = [];
11 + // Mutable range of `items` begins here, but its reactive scope block
12 + // should be aligned to above the label-block
13 + if (cond) break label;
14 + arrayPush(items, value);
15 + }
16 + arrayPush(items, value);
17 + return items;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [{ cond: true, value: 2 }],
23 + sequentialRenders: [
24 + { cond: true, value: 2 },
25 + { cond: true, value: 2 },
26 + { cond: true, value: 3 },
27 + { cond: false, value: 3 },
28 + ],
29 +};
30 +
31 +```
32 +
33 +
34 +## Error
35 +
36 +```
37 +Invariant: Invalid nesting in program blocks or scopes. Blocks overlap but are not nested: ProgramBlockSubtree@0(3:14) Scope@0(4:18)
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.ts new
+25
@@ -0,0 +1,25 @@
1 +import { arrayPush } from "shared-runtime";
2 +
3 +function useFoo({ cond, value }) {
4 + let items;
5 + label: {
6 + items = [];
7 + // Mutable range of `items` begins here, but its reactive scope block
8 + // should be aligned to above the label-block
9 + if (cond) break label;
10 + arrayPush(items, value);
11 + }
12 + arrayPush(items, value);
13 + return items;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [{ cond: true, value: 2 }],
19 + sequentialRenders: [
20 + { cond: true, value: 2 },
21 + { cond: true, value: 2 },
22 + { cond: true, value: 3 },
23 + { cond: false, value: 3 },
24 + ],
25 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.expect.md new
+36
@@ -0,0 +1,36 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { arrayPush } from "shared-runtime";
6 +
7 +function useFoo({ value }) {
8 + let items = null;
9 + try {
10 + // Mutable range of `items` begins here, but its reactive scope block
11 + // should be aligned to above the try-block
12 + items = [];
13 + arrayPush(items, value);
14 + } catch {
15 + // ignore
16 + }
17 + mutate(items);
18 + return items;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: useFoo,
23 + params: [{ value: 2 }],
24 + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
25 +};
26 +
27 +```
28 +
29 +
30 +## Error
31 +
32 +```
33 +Invariant: Invalid nesting in program blocks or scopes. Blocks overlap but are not nested: ProgramBlockSubtree@0(4:19) Scope@0(5:22)
34 +```
35 +
36 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.ts new
+21
@@ -0,0 +1,21 @@
1 +import { arrayPush } from "shared-runtime";
2 +
3 +function useFoo({ value }) {
4 + let items = null;
5 + try {
6 + // Mutable range of `items` begins here, but its reactive scope block
7 + // should be aligned to above the try-block
8 + items = [];
9 + arrayPush(items, value);
10 + } catch {
11 + // ignore
12 + }
13 + mutate(items);
14 + return items;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [{ value: 2 }],
20 + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }],
21 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/iife-return-modified-later-logical.expect.md deleted
-55
@@ -1,55 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import { getNull } from "shared-runtime";
6 -
7 -function Component(props) {
8 - const items = (() => {
9 - return getNull() ?? [];
10 - })();
11 - items.push(props.a);
12 - return items;
13 -}
14 -
15 -export const FIXTURE_ENTRYPOINT = {
16 - fn: Component,
17 - params: [{ a: {} }],
18 -};
19 -
20 -```
21 -
22 -## Code
23 -
24 -```javascript
25 -import { unstable_useMemoCache as useMemoCache } from "react";
26 -import { getNull } from "shared-runtime";
27 -
28 -function Component(props) {
29 - const $ = useMemoCache(3);
30 - let t0;
31 - let items;
32 - if ($[0] !== props.a) {
33 - t0 = getNull() ?? [];
34 - items = t0;
35 -
36 - items.push(props.a);
37 - $[0] = props.a;
38 - $[1] = items;
39 - $[2] = t0;
40 - } else {
41 - items = $[1];
42 - t0 = $[2];
43 - }
44 - return items;
45 -}
46 -
47 -export const FIXTURE_ENTRYPOINT = {
48 - fn: Component,
49 - params: [{ a: {} }],
50 -};
51 -
52 -```
53 -
54 -### Eval output
55 -(kind: ok) [{}]
\ No newline at end of file