Fixes, identical output in new version
Joe Savona committed
Jan 12, 2023 at 09:43 UTC
a40ade1e6148b2a13e1179c477033324ea90debd
5 files changed
+206
-57
compiler/forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts
+3
-1
@@ -19,7 +19,7 @@ import { getInstructionScope, getPlaceScope } from "./BuildReactiveBlocks";
19
import { eachTerminalBlock, eachTerminalOperand } from "./visitors";
20
21
/**
22
- * Note: this is the 2nd of 3 passes that determine how to break a function into discrete
22
+ * Note: this is the 2nd of 4 passes that determine how to break a function into discrete
23
* reactive scopes (independently memoizeable units of code):
24
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
25
* them a unique reactive scope.
@@ -27,6 +27,8 @@ import { eachTerminalBlock, eachTerminalOperand } from "./visitors";
27
* to block scopes.
28
* 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
29
* overlap, merging any such scopes.
30
+ * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
31
+ * a ReactiveScopeBlock.
32
*
33
* Prior inference passes assign a reactive scope to each operand, but the ranges of these
34
* scopes are based on specific instructions at arbitrary points in the control-flow graph.
compiler/forget/src/ReactiveScopes/BuildReactiveBlocks.ts
+159
-42
@@ -7,73 +7,168 @@
7
8
import invariant from "invariant";
9
import {
10
+ BlockId,
11
InstructionId,
11
- makeInstructionId,
12
Place,
13
ReactiveBlock,
14
ReactiveFunction,
15
ReactiveInstruction,
16
ReactiveScope,
17
ReactiveScopeBlock,
18
+ ReactiveStatement,
19
+ ReactiveValueBlock,
20
+ ScopeId,
21
} from "../HIR";
22
import { eachInstructionValueOperand } from "../HIR/visitors";
20
-import { mapTerminalBlocks } from "./visitors";
23
+import { assertExhaustive } from "../Utils/utils";
24
+import { eachTerminalBlock, mapTerminalBlocks } from "./visitors";
25
26
/**
27
+ * Note: this is the 4th of 4 passes that determine how to break a function into discrete
28
+ * reactive scopes (independently memoizeable units of code):
29
+ * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
30
+ * them a unique reactive scope.
31
+ * 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
32
+ * to block scopes.
33
+ * 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
34
+ * scopes do not overlap, merging any such scopes.
35
+ * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
36
+ * a ReactiveScopeBlock.
37
+ *
38
* Given a function where the reactive scopes have been correctly aligned and merged,
39
* this pass groups the instructions for each reactive scope into ReactiveBlocks.
40
*/
41
export function buildReactiveBlocks(fn: ReactiveFunction): void {
27
- fn.body = visitBlock(fn.body);
42
+ const context = new Context();
43
+ fn.body = context.enter(() => {
44
+ visitBlock(context, fn.body);
45
+ });
46
+}
47
+
48
+class Context {
49
+ #builders: Array<Builder> = [];
50
+ #scopes: Set<ScopeId> = new Set();
51
+
52
+ visitId(id: InstructionId): void {
53
+ const builder = this.#builders.at(-1)!;
54
+ builder.visitId(id);
55
+ }
56
+
57
+ visitScope(scope: ReactiveScope): void {
58
+ if (this.#scopes.has(scope.id)) {
59
+ return;
60
+ }
61
+ this.#scopes.add(scope.id);
62
+ this.#builders.at(-1)!.startScope(scope);
63
+ }
64
+
65
+ append(stmt: ReactiveStatement, label: BlockId | null): void {
66
+ this.#builders.at(-1)!.append(stmt, label);
67
+ }
68
+
69
+ enter(fn: () => void): ReactiveBlock {
70
+ const builder = new Builder();
71
+ this.#builders.push(builder);
72
+ fn();
73
+ const popped = this.#builders.pop();
74
+ invariant(popped === builder, "Expected push/pop to be called 1:1");
75
+ return builder.complete();
76
+ }
77
}
78
30
-type Entry =
31
- | ReactiveScopeBlock
32
- | { kind: "block"; instructions: ReactiveBlock };
79
+class Builder {
80
+ #instructions: ReactiveBlock;
81
+ #stack: Array<
82
+ | { kind: "scope"; block: ReactiveScopeBlock }
83
+ | { kind: "block"; block: ReactiveBlock }
84
+ >;
85
34
-function visitBlock(block: ReactiveBlock): ReactiveBlock {
35
- let current: Entry = { kind: "block", instructions: [] };
36
- const stack: Array<Entry> = [current];
37
- let lastId: InstructionId = makeInstructionId(0);
86
+ constructor() {
87
+ const block: ReactiveBlock = [];
88
+ this.#instructions = block;
89
+ this.#stack = [{ kind: "block", block }];
90
+ }
91
+
92
+ append(item: ReactiveStatement, label: BlockId | null): void {
93
+ if (label !== null) {
94
+ invariant(item.kind === "terminal", "Only terminals may have a label");
95
+ item.label = label;
96
+ }
97
+ this.#instructions.push(item);
98
+ }
99
+
100
+ startScope(scope: ReactiveScope): void {
101
+ const block: ReactiveScopeBlock = {
102
+ kind: "scope",
103
+ scope,
104
+ instructions: [],
105
+ };
106
+ this.append(block, null);
107
+ this.#instructions = block.instructions;
108
+ this.#stack.push({ kind: "scope", block });
109
+ }
110
+
111
+ visitId(id: InstructionId): void {
112
+ for (let i = 0; i < this.#stack.length; i++) {
113
+ const entry = this.#stack[i]!;
114
+ if (entry.kind === "scope" && id >= entry.block.scope.range.end) {
115
+ this.#stack.length = i;
116
+ break;
117
+ }
118
+ }
119
+ const last = this.#stack[this.#stack.length - 1]!;
120
+ if (last.kind === "block") {
121
+ this.#instructions = last.block;
122
+ } else {
123
+ this.#instructions = last.block.instructions;
124
+ }
125
+ }
126
+
127
+ complete(): ReactiveBlock {
128
+ // TODO: @josephsavona debug violations of this invariant
129
+ // invariant(
130
+ // this.#stack.length === 1,
131
+ // "Expected all scopes to be closed when exiting a block"
132
+ // );
133
+ const first = this.#stack[0]!;
134
+ invariant(
135
+ first.kind === "block",
136
+ "Expected first stack item to be a basic block"
137
+ );
138
+ return first.block;
139
+ }
140
+}
141
+
142
+function visitBlock(context: Context, block: ReactiveBlock): void {
143
for (const stmt of block) {
144
switch (stmt.kind) {
145
case "instruction": {
41
- lastId = stmt.instruction.id;
42
- while (current.kind === "scope" && lastId >= current.scope.range.end) {
43
- current = stack.pop()!;
44
- }
146
+ context.visitId(stmt.instruction.id);
147
const scope = getInstructionScope(stmt.instruction);
46
-
47
- if (
48
- scope !== null &&
49
- (current.kind !== "scope" || current.scope.id !== scope.id)
50
- ) {
51
- const reactiveScope: ReactiveScopeBlock = {
52
- kind: "scope",
53
- scope,
54
- instructions: [],
55
- };
56
- current.instructions.push(reactiveScope);
57
- stack.push(current);
58
- current = reactiveScope;
148
+ if (scope !== null) {
149
+ context.visitScope(scope);
150
}
60
-
61
- current.instructions.push(stmt);
151
+ context.append(stmt, null);
152
break;
153
}
154
case "terminal": {
155
const id = stmt.terminal.id;
156
if (id !== null) {
67
- lastId = id;
68
- while (
69
- current.kind === "scope" &&
70
- lastId >= current.scope.range.end
71
- ) {
72
- current = stack.pop()!;
73
- }
157
+ context.visitId(id);
158
}
75
- mapTerminalBlocks(stmt.terminal, visitBlock);
76
- current.instructions.push(stmt);
159
+ mapTerminalBlocks(stmt.terminal, (block) => {
160
+ return context.enter(() => {
161
+ visitBlock(context, block);
162
+ });
163
+ });
164
+ eachTerminalBlock(
165
+ stmt.terminal,
166
+ (_) => {},
167
+ (valueBlock) => {
168
+ visitValueBlock(context, valueBlock);
169
+ }
170
+ );
171
+ context.append(stmt, stmt.label);
172
break;
173
}
174
case "scope": {
@@ -82,13 +177,35 @@ function visitBlock(block: ReactiveBlock): ReactiveBlock {
177
"Expected the function to not have scopes already assigned"
178
);
179
}
180
+ default: {
181
+ assertExhaustive(
182
+ stmt,
183
+ `Unexpected statement kind '${(stmt as any).kind}'`
184
+ );
185
+ }
186
+ }
187
+ }
188
+}
189
+
190
+function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
191
+ for (const stmt of block.instructions) {
192
+ switch (stmt.kind) {
193
+ case "instruction": {
194
+ context.visitId(stmt.instruction.id);
195
+ const scope = getInstructionScope(stmt.instruction);
196
+ if (scope !== null) {
197
+ context.visitScope(scope);
198
+ }
199
+ break;
200
+ }
201
+ default: {
202
+ invariant(false, "Unexpected terminal or scope in value block");
203
+ }
204
}
205
}
87
- while (current.kind === "scope") {
88
- // invariant(current.scope.range.end === lastId + 1, "Scope ended too soon");
89
- current = stack.pop()!;
206
+ if (block.last !== null) {
207
+ context.visitId(block.last.id);
208
}
91
- return current.instructions;
209
}
210
211
export function getInstructionScope({
compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+11
@@ -22,6 +22,17 @@ import DisjointSet from "../Utils/DisjointSet";
22
import { assertExhaustive } from "../Utils/utils";
23
24
/**
25
+ * Note: this is the 1st of 4 passes that determine how to break a function into discrete
26
+ * reactive scopes (independently memoizeable units of code):
27
+ * 1. InferReactiveScopeVariables (this pass, on HIR) determines operands that mutate
28
+ * together and assigns them a unique reactive scope.
29
+ * 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
30
+ * to block scopes.
31
+ * 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive
32
+ * scopes do not overlap, merging any such scopes.
33
+ * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
34
+ * a ReactiveScopeBlock.
35
+ *
36
* For each mutable variable, infers a reactive scope which will construct that
37
* variable. Variables that co-mutate are assigned to the same reactive scope.
38
* This pass does *not* infer the set of instructions necessary to compute each
compiler/forget/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts
+15
-2
@@ -24,7 +24,7 @@ import { getPlaceScope } from "./BuildReactiveBlocks";
24
import { eachTerminalBlock, eachTerminalOperand } from "./visitors";
25
26
/**
27
- * Note: this is the 3rd of 3 passes that determine how to break a function into discrete
27
+ * Note: this is the 3rd of 4 passes that determine how to break a function into discrete
28
* reactive scopes (independently memoizeable units of code):
29
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
30
* them a unique reactive scope.
@@ -32,6 +32,8 @@ import { eachTerminalBlock, eachTerminalOperand } from "./visitors";
32
* to block scopes.
33
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
34
* scopes do not overlap, merging any such scopes.
35
+ * 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
36
+ * a ReactiveScopeBlock.
37
*
38
* Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
39
* the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
@@ -144,7 +146,18 @@ function visitBlock(context: Context, block: ReactiveBlock): void {
146
}
147
148
function visitValueBlock(context: Context, block: ReactiveValueBlock): void {
147
- visitBlock(context, block.instructions);
149
+ for (const stmt of block.instructions) {
150
+ switch (stmt.kind) {
151
+ case "instruction": {
152
+ visitValue(context, stmt.instruction.id, stmt.instruction.value);
153
+ visitInstruction(context, stmt.instruction);
154
+ break;
155
+ }
156
+ default: {
157
+ invariant(false, "Unexpected terminal or scope in value block");
158
+ }
159
+ }
160
+ }
161
if (block.last !== null) {
162
context.visitId(block.last.id);
163
if (block.last.value.kind === "Identifier") {
compiler/forget/src/ReactiveScopes/PrintReactiveFunction.ts
+18
-12
@@ -5,7 +5,6 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import invariant from "invariant";
8
import {
9
ReactiveFunction,
10
ReactiveScopeBlock,
@@ -20,6 +19,7 @@ import {
19
printInstructionValue,
20
printPlace,
21
} from "../HIR/PrintHIR";
22
+import { invariant } from "../Utils/CompilerError";
23
import { assertExhaustive } from "../Utils/utils";
24
25
export function printReactiveFunction(fn: ReactiveFunction): string {
@@ -113,23 +113,25 @@ function printValueBlock(writer: Writer, block: ReactiveValueBlock): void {
113
function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
114
switch (terminal.kind) {
115
case "break": {
116
+ const id = terminal.id !== null ? `[${terminal.id}]` : [];
117
if (terminal.label !== null) {
117
- writer.writeLine(`break bb${terminal.label}`);
118
+ writer.writeLine(`${id} break bb${terminal.label}`);
119
} else {
119
- writer.writeLine(`break`);
120
+ writer.writeLine(`${id} break`);
121
}
122
break;
123
}
124
case "continue": {
125
+ const id = `[${terminal.id}]`;
126
if (terminal.label !== null) {
125
- writer.writeLine(`continue bb${terminal.label}`);
127
+ writer.writeLine(`${id} continue bb${terminal.label}`);
128
} else {
127
- writer.writeLine(`continue`);
129
+ writer.writeLine(`${id} continue`);
130
}
131
break;
132
}
133
case "while": {
132
- writer.writeLine(`while (`);
134
+ writer.writeLine(`[${terminal.id}] while (`);
135
printValueBlock(writer, terminal.test);
136
writer.writeLine(") {");
137
printReactiveInstructions(writer, terminal.loop);
@@ -138,7 +140,7 @@ function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
140
}
141
case "if": {
142
const { test, consequent, alternate } = terminal;
141
- writer.writeLine(`if (${printPlace(test)}) {`);
143
+ writer.writeLine(`[${terminal.id}] if (${printPlace(test)}) {`);
144
printReactiveInstructions(writer, consequent);
145
if (alternate !== null) {
146
writer.writeLine("} else {");
@@ -148,7 +150,9 @@ function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
150
break;
151
}
152
case "switch": {
151
- writer.writeLine(`switch (${printPlace(terminal.test)}) {`);
153
+ writer.writeLine(
154
+ `[${terminal.id}] switch (${printPlace(terminal.test)}) {`
155
+ );
156
writer.indented(() => {
157
for (const case_ of terminal.cases) {
158
let prefix =
@@ -166,7 +170,7 @@ function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
170
break;
171
}
172
case "for": {
169
- writer.writeLine("for (");
173
+ writer.writeLine("[${terminal.id}] for (");
174
printValueBlock(writer, terminal.init);
175
writer.writeLine(";");
176
printValueBlock(writer, terminal.test);
@@ -178,14 +182,16 @@ function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
182
break;
183
}
184
case "throw": {
181
- writer.writeLine(`throw ${printPlace(terminal.value)}`);
185
+ writer.writeLine(`[${terminal.id}] throw ${printPlace(terminal.value)}`);
186
break;
187
}
188
case "return": {
189
if (terminal.value !== null) {
186
- writer.writeLine(`return ${printPlace(terminal.value)}`);
190
+ writer.writeLine(
191
+ `[${terminal.id}] return ${printPlace(terminal.value)}`
192
+ );
193
} else {
188
- writer.writeLine("return");
194
+ writer.writeLine(`[${terminal.id}] return`);
195
}
196
break;
197
}