@samitouri / QOS-React-2 / commits / a40ade1e61

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 }