[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