@samitouri / QOS-React-2 / commits / 87d70b9129

BuildReactiveBlocks to construct scopes in ReactiveFunction

The primary goal of this stack is to change HIRTreeVisitor to make it easier to handle value blocks. That's complicated by the fact that the visitor is a general-purpose visitor, used in several analysis passes including BuildReactiveFunction (which translates HIR->ReactiveFunction while also grouping instructions into scopes) and InferReactiveScopes (which is actually two passes, one to align scopes to block boundaries, one to merge overlapping scopes). The long-term goal then is as follows: 1. Make BuildReactiveFunction transform HIR->ReactiveFunction but _without_ reactive scopes. 2. Align scopes to block boundaries, but rewritten to operate on ReactiveFunction 3. Merge overlapping scopes, again rewritten to operate on ReactiveFunction 4. Group statements within ReactiveFunction into ReactiveScopeBlocks (today this occurs when constructing the ReactiveFunction). This PR implements 1 and 4. Because the implementation is incomplete this would break the whole compiler, so for now both versions are still around. By default compilation uses the old pipeline, but if a feature flag is enabled we use the new version. The plan is to incrementally fix up the new version of the passes in this stack, and then cutover: removing the flag and the old version of the passes.

Joe Savona committed Jan 11, 2023 at 14:04 UTC 87d70b9129e91f85bbcc6945a4416d9d7ffe22a6
12 files changed +617 -34
compiler/forget/src/CompilerFlags.ts new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +type Flags = {
9 + enableNewReactiveFunctionBuilder: boolean;
10 +};
11 +
12 +export const flags: Flags = {
13 + enableNewReactiveFunctionBuilder: false,
14 +};
compiler/forget/src/CompilerPipeline.ts
+45 -9
@@ -6,6 +6,7 @@
6 */
7 import { NodePath } from "@babel/traverse";
8 import * as t from "@babel/types";
9 +import { flags } from "./CompilerFlags";
10 import {
11 Environment,
12 HIRFunction,
@@ -16,11 +17,15 @@ import {
17 import { inferMutableRanges, inferReferenceEffects } from "./Inference";
18 import { constantPropagation } from "./Optimization";
19 import {
20 + alignReactiveScopesToBlockScopes,
21 + buildReactiveBlocks,
22 buildReactiveFunction,
23 + buildReactiveFunctionWithoutScopes,
24 codegenReactiveFunction,
25 flattenReactiveLoops,
26 inferReactiveScopes,
27 inferReactiveScopeVariables,
28 + mergeOverlappingReactiveScopes,
29 propagateScopeDependencies,
30 pruneUnusedLabels,
31 pruneUnusedLValues,
@@ -72,15 +77,46 @@ export function* run(
77 inferReactiveScopeVariables(hir);
78 yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir });
79
75 - inferReactiveScopes(hir);
76 - yield log({ kind: "hir", name: "InferReactiveScopes", value: hir });
77 -
78 - const reactiveFunction = buildReactiveFunction(hir);
79 - yield log({
80 - kind: "reactive",
81 - name: "BuildReactiveFunction",
82 - value: reactiveFunction,
83 - });
80 + let reactiveFunction: ReactiveFunction;
81 + if (!flags.enableNewReactiveFunctionBuilder) {
82 + inferReactiveScopes(hir);
83 + yield log({ kind: "hir", name: "InferReactiveScopes", value: hir });
84 +
85 + reactiveFunction = buildReactiveFunction(hir);
86 + yield log({
87 + kind: "reactive",
88 + name: "BuildReactiveFunction",
89 + value: reactiveFunction,
90 + });
91 + } else {
92 + reactiveFunction = buildReactiveFunctionWithoutScopes(hir);
93 + yield log({
94 + kind: "reactive",
95 + name: "BuildReactiveFunction",
96 + value: reactiveFunction,
97 + });
98 +
99 + alignReactiveScopesToBlockScopes(reactiveFunction);
100 + yield log({
101 + kind: "reactive",
102 + name: "AlignReactiveScopesToBlockScopes",
103 + value: reactiveFunction,
104 + });
105 +
106 + mergeOverlappingReactiveScopes(reactiveFunction);
107 + yield log({
108 + kind: "reactive",
109 + name: "MergeOverlappingReactiveScopes",
110 + value: reactiveFunction,
111 + });
112 +
113 + buildReactiveBlocks(reactiveFunction);
114 + yield log({
115 + kind: "reactive",
116 + name: "BuildReactiveBlocks",
117 + value: reactiveFunction,
118 + });
119 + }
120
121 pruneUnusedLabels(reactiveFunction);
122 yield log({
compiler/forget/src/HIR/HIR.ts
+13 -5
@@ -77,10 +77,10 @@ export type ReactiveInstruction = {
77 };
78
79 export type ReactiveTerminal =
80 - | { kind: "break"; label: BlockId | null }
81 - | { kind: "continue"; label: BlockId | null }
82 - | { kind: "return"; value: Place | null }
83 - | { kind: "throw"; value: Place }
80 + | { kind: "break"; label: BlockId | null; id: InstructionId | null }
81 + | { kind: "continue"; label: BlockId | null; id: InstructionId }
82 + | { kind: "return"; value: Place | null; id: InstructionId }
83 + | { kind: "throw"; value: Place; id: InstructionId }
84 | {
85 kind: "switch";
86 test: Place;
@@ -88,20 +88,28 @@ export type ReactiveTerminal =
88 test: Place | null;
89 block: ReactiveBlock | void;
90 }>;
91 + id: InstructionId;
92 + }
93 + | {
94 + kind: "while";
95 + test: ReactiveValueBlock;
96 + loop: ReactiveBlock;
97 + id: InstructionId;
98 }
92 - | { kind: "while"; test: ReactiveValueBlock; loop: ReactiveBlock }
99 | {
100 kind: "for";
101 init: ReactiveValueBlock;
102 test: ReactiveValueBlock;
103 update: ReactiveValueBlock;
104 loop: ReactiveBlock;
105 + id: InstructionId;
106 }
107 | {
108 kind: "if";
109 test: Place;
110 consequent: ReactiveBlock;
111 alternate: ReactiveBlock | null;
112 + id: InstructionId;
113 };
114
115 /**
compiler/forget/src/HIR/HIRTreeVisitor.ts
+36 -15
@@ -120,6 +120,7 @@ class Driver<
120 kind: "return",
121 loc: terminal.loc,
122 value,
123 + id: terminal.id,
124 })
125 );
126 break;
@@ -132,6 +133,7 @@ class Driver<
133 this.visitor.visitTerminal({
134 kind: "throw",
135 value,
136 + id: terminal.id,
137 })
138 );
139 break;
@@ -156,7 +158,7 @@ class Driver<
158 this.visitor.visitTerminalId(terminal.id);
159 let consequent: TBlock | null = null;
160 if (this.cx.isScheduled(terminal.consequent)) {
159 - const break_ = this.visitBreak(terminal.consequent);
161 + const break_ = this.visitBreak(terminal.consequent, null);
162 if (break_ !== null) {
163 const builder = this.visitor.enterBlock();
164 this.visitor.appendBlock(builder, break_);
@@ -171,7 +173,7 @@ class Driver<
173 let alternate: TBlock | null = null;
174 if (alternateId !== null) {
175 if (this.cx.isScheduled(alternateId)) {
174 - const break_ = this.visitBreak(alternateId);
176 + const break_ = this.visitBreak(alternateId, null);
177 if (break_ !== null) {
178 const builder = this.visitor.enterBlock();
179 this.visitor.appendBlock(builder, break_);
@@ -191,6 +193,7 @@ class Driver<
193 test,
194 consequent: consequent ?? this.emptyBlock(),
195 alternate: alternate,
196 + id: terminal.id,
197 }),
198 fallthroughId
199 );
@@ -203,6 +206,7 @@ class Driver<
206 test,
207 consequent: consequent ?? this.emptyBlock(),
208 alternate: alternate,
209 + id: terminal.id,
210 })
211 );
212 }
@@ -234,7 +238,7 @@ class Driver<
238 // that are already scheduled. emit as follows:
239 // - if the block is for another case branch, don't emit a break and fall-through
240 // - else, emit an explicit break.
237 - const break_ = this.visitBreak(case_.block);
241 + const break_ = this.visitBreak(case_.block, null);
242 if (
243 index === 0 &&
244 break_ === null &&
@@ -270,6 +274,7 @@ class Driver<
274 kind: "switch",
275 test,
276 cases,
277 + id: terminal.id,
278 }),
279 fallthroughId
280 );
@@ -281,6 +286,7 @@ class Driver<
286 kind: "switch",
287 test,
288 cases,
289 + id: terminal.id,
290 })
291 );
292 }
@@ -319,7 +325,7 @@ class Driver<
325 if (loopId) {
326 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
327 } else {
322 - const break_ = this.visitBreak(terminal.loop);
328 + const break_ = this.visitBreak(terminal.loop, null);
329 invariant(
330 break_ !== null,
331 "If loop body is already scheduled it must be a break"
@@ -338,6 +344,7 @@ class Driver<
344 loc: terminal.loc,
345 test: testValue,
346 loop: loopBody,
347 + id: terminal.id,
348 }),
349 fallthroughId
350 );
@@ -350,6 +357,7 @@ class Driver<
357 loc: terminal.loc,
358 test: testValue,
359 loop: loopBody,
360 + id: terminal.id,
361 })
362 );
363 }
@@ -408,7 +416,7 @@ class Driver<
416 if (loopId) {
417 loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
418 } else {
411 - const break_ = this.visitBreak(terminal.loop);
419 + const break_ = this.visitBreak(terminal.loop, null);
420 invariant(
421 break_ !== null,
422 "If loop body is already scheduled it must be a break"
@@ -428,6 +436,7 @@ class Driver<
436 test: testValue,
437 update: updateValue,
438 loop: loopBody,
439 + id: terminal.id,
440 }),
441 fallthroughId
442 );
@@ -441,6 +450,7 @@ class Driver<
450 test: testValue,
451 update: updateValue,
452 loop: loopBody,
453 + id: terminal.id,
454 })
455 );
456 }
@@ -453,14 +463,14 @@ class Driver<
463 this.visitor.visitTerminalId(terminal.id);
464 switch (terminal.variant) {
465 case GotoVariant.Break: {
456 - const break_ = this.visitBreak(terminal.block);
466 + const break_ = this.visitBreak(terminal.block, terminal.id);
467 if (break_ !== null) {
468 this.visitor.appendBlock(blockValue, break_);
469 }
470 break;
471 }
472 case GotoVariant.Continue: {
463 - const continue_ = this.visitContinue(terminal.block);
473 + const continue_ = this.visitContinue(terminal.block, terminal.id);
474 if (continue_ !== null) {
475 this.visitor.appendBlock(blockValue, continue_);
476 }
@@ -519,7 +529,7 @@ class Driver<
529 return this.visitor.leaveBlock(block);
530 }
531
522 - visitBreak(block: BlockId): TStatement | null {
532 + visitBreak(block: BlockId, id: InstructionId | null): TStatement | null {
533 const target = this.cx.getBreakTarget(block);
534 if (target === null) {
535 // TODO: we should always have a target
@@ -530,18 +540,19 @@ class Driver<
540 return this.visitor.visitImplicitTerminal();
541 }
542 case "unlabeled": {
533 - return this.visitor.visitTerminal({ kind: "break", label: null });
543 + return this.visitor.visitTerminal({ kind: "break", label: null, id });
544 }
545 case "labeled": {
546 return this.visitor.visitTerminal({
547 kind: "break",
548 label: target.block,
549 + id,
550 });
551 }
552 }
553 }
554
544 - visitContinue(block: BlockId): TStatement | null {
555 + visitContinue(block: BlockId, id: InstructionId): TStatement | null {
556 const target = this.cx.getContinueTarget(block);
557 invariant(
558 target !== null,
@@ -552,12 +563,14 @@ class Driver<
563 return this.visitor.visitTerminal({
564 kind: "continue",
565 label: target.block,
566 + id,
567 });
568 }
569 case "unlabeled": {
570 return this.visitor.visitTerminal({
571 kind: "continue",
572 label: null,
573 + id,
574 });
575 }
576 case "implicit": {
@@ -911,20 +924,27 @@ export interface Visitor<
924 }
925
926 export type BlockTerminal<TInit, TValue, TBlock, TCase> =
914 - | { kind: "return"; loc: SourceLocation; value: TValue | null }
915 - | { kind: "throw"; value: TValue }
927 + | {
928 + kind: "return";
929 + loc: SourceLocation;
930 + value: TValue | null;
931 + id: InstructionId;
932 + }
933 + | { kind: "throw"; value: TValue; id: InstructionId }
934 | {
935 kind: "if";
936 test: TValue;
937 consequent: TBlock;
938 alternate: TBlock | null;
939 + id: InstructionId;
940 }
922 - | { kind: "switch"; test: TValue; cases: Array<TCase> }
941 + | { kind: "switch"; test: TValue; cases: Array<TCase>; id: InstructionId }
942 | {
943 kind: "while";
944 loc: SourceLocation;
945 test: TValue;
946 loop: TBlock;
947 + id: InstructionId;
948 }
949 | {
950 kind: "for";
@@ -932,6 +952,7 @@ export type BlockTerminal<TInit, TValue, TBlock, TCase> =
952 test: TValue;
953 update: TValue;
954 loop: TBlock;
955 + id: InstructionId;
956 }
936 - | { kind: "break"; label: BlockId | null }
937 - | { kind: "continue"; label: BlockId | null };
957 + | { kind: "break"; label: BlockId | null; id: InstructionId | null }
958 + | { kind: "continue"; label: BlockId | null; id: InstructionId };
compiler/forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts new
+50
@@ -0,0 +1,50 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { ReactiveFunction } from "../HIR/HIR";
9 +
10 +/**
11 + * Note: this is the 2nd of 3 passes that determine how to break a function into discrete
12 + * reactive scopes (independently memoizeable units of code):
13 + * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
14 + * them a unique reactive scope.
15 + * 2. AlignReactiveScopesToBlockScopes (this pass, on ReactiveFunction) aligns reactive scopes
16 + * to block scopes.
17 + * 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
18 + * overlap, merging any such scopes.
19 + *
20 + * Prior inference passes assign a reactive scope to each operand, but the ranges of these
21 + * scopes are based on specific instructions at arbitrary points in the control-flow graph.
22 + * However, to codegen blocks around the instructions in each scope, the scopes must be
23 + * aligned to block-scope boundaries - we can't memoize half of a loop!
24 + *
25 + * This pass updates reactive scope boundaries to align to control flow boundaries, for
26 + * example:
27 + *
28 + * ```javascript
29 + * function foo(cond, a) {
30 + * ⌵ original scope
31 + * ⌵ expanded scope
32 + * const x = []; ⌝ ⌝
33 + * if (cond) { ⎮ ⎮
34 + * ... ⎮ ⎮
35 + * x.push(a); ⌟ ⎮
36 + * ... ⎮
37 + * } ⌟
38 + * }
39 + * ```
40 + *
41 + * Here the original scope for `x` ended partway through the if consequent, but we can't
42 + * memoize part of that block. This pass would align the scope to the end of the consequent.
43 + *
44 + * The more general rule is that a reactive scope may only end at the same block scope as it
45 + * began: this pass therefore finds, for each scope, the block where that scope started and
46 + * finds the first instruction after the scope's mutable range in that same block scope (which
47 + * will be the updated end for that scope).
48 + */
49 +
50 +export function alignReactiveScopesToBlockScopes(fn: ReactiveFunction): void {}
compiler/forget/src/ReactiveScopes/BuildReactiveBlocks.ts new
+123
@@ -0,0 +1,123 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import invariant from "invariant";
9 +import {
10 + InstructionId,
11 + makeInstructionId,
12 + ReactiveBlock,
13 + ReactiveFunction,
14 + ReactiveInstruction,
15 + ReactiveScope,
16 + ReactiveScopeBlock,
17 +} from "../HIR";
18 +import { eachInstructionValueOperand } from "../HIR/visitors";
19 +import { mapTerminalBlocks } from "./visitors";
20 +
21 +/**
22 + * Given a function where the reactive scopes have been correctly aligned and merged,
23 + * this pass groups the instructions for each reactive scope into ReactiveBlocks.
24 + */
25 +export function buildReactiveBlocks(fn: ReactiveFunction): void {
26 + fn.body = visitBlock(fn.body);
27 +}
28 +
29 +type Entry =
30 + | ReactiveScopeBlock
31 + | { kind: "block"; instructions: ReactiveBlock };
32 +
33 +function visitBlock(block: ReactiveBlock): ReactiveBlock {
34 + let current: Entry = { kind: "block", instructions: [] };
35 + const stack: Array<Entry> = [current];
36 + let lastId: InstructionId = makeInstructionId(0);
37 + for (const stmt of block) {
38 + switch (stmt.kind) {
39 + case "instruction": {
40 + lastId = stmt.instruction.id;
41 + while (current.kind === "scope" && lastId >= current.scope.range.end) {
42 + current = stack.pop()!;
43 + }
44 + const scope = getInstructionScope(stmt.instruction);
45 +
46 + if (
47 + scope !== null &&
48 + (current.kind !== "scope" || current.scope.id !== scope.id)
49 + ) {
50 + const reactiveScope: ReactiveScopeBlock = {
51 + kind: "scope",
52 + scope,
53 + instructions: [],
54 + };
55 + current.instructions.push(reactiveScope);
56 + stack.push(current);
57 + current = reactiveScope;
58 + }
59 +
60 + current.instructions.push(stmt);
61 + break;
62 + }
63 + case "terminal": {
64 + const id = stmt.terminal.id;
65 + if (id !== null) {
66 + lastId = id;
67 + while (
68 + current.kind === "scope" &&
69 + lastId >= current.scope.range.end
70 + ) {
71 + current = stack.pop()!;
72 + }
73 + }
74 + mapTerminalBlocks(stmt.terminal, visitBlock);
75 + current.instructions.push(stmt);
76 + break;
77 + }
78 + case "scope": {
79 + invariant(
80 + false,
81 + "Expected the function to not have scopes already assigned"
82 + );
83 + }
84 + }
85 + }
86 + while (current.kind === "scope") {
87 + invariant(current.scope.range.end === lastId + 1, "Scope ended too soon");
88 + current = stack.pop()!;
89 + }
90 + return current.instructions;
91 +}
92 +
93 +function getInstructionScope({
94 + id,
95 + lvalue,
96 + value,
97 +}: ReactiveInstruction): ReactiveScope | null {
98 + invariant(
99 + lvalue !== null,
100 + "Expected lvalues to not be null when assigning scopes. " +
101 + "Pruning lvalues too early can result in missing scope information."
102 + );
103 + if (
104 + lvalue.place.identifier.scope !== null &&
105 + isScopeActive(lvalue.place.identifier.scope, id)
106 + ) {
107 + return lvalue.place.identifier.scope;
108 + } else {
109 + for (const operand of eachInstructionValueOperand(value)) {
110 + if (
111 + operand.identifier.scope !== null &&
112 + isScopeActive(operand.identifier.scope, id)
113 + ) {
114 + return operand.identifier.scope;
115 + }
116 + }
117 + }
118 + return null;
119 +}
120 +
121 +function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
122 + return id >= scope.range.start && id < scope.range.end;
123 +}
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+12 -4
@@ -218,11 +218,11 @@ class ReactiveFunctionBuilder
218 let result: ReactiveTerminal;
219 switch (terminal.kind) {
220 case "break": {
221 - result = { kind: "break", label: terminal.label };
221 + result = { kind: "break", label: terminal.label, id: terminal.id };
222 break;
223 }
224 case "continue": {
225 - result = { kind: "continue", label: terminal.label };
225 + result = { kind: "continue", label: terminal.label, id: terminal.id };
226 break;
227 }
228 case "for": {
@@ -233,6 +233,7 @@ class ReactiveFunctionBuilder
233 test: terminal.test as ReactiveValueBlock,
234 update: terminal.update as ReactiveValueBlock,
235 loop: terminal.loop,
236 + id: terminal.id,
237 };
238 break;
239 }
@@ -242,6 +243,7 @@ class ReactiveFunctionBuilder
243 test: terminal.test as Place,
244 consequent: terminal.consequent,
245 alternate: terminal.alternate,
246 + id: terminal.id,
247 };
248 break;
249 }
@@ -250,7 +252,7 @@ class ReactiveFunctionBuilder
252 if (value !== null && value.kind !== "Identifier") {
253 invariant(false, "Expected return to be a Place");
254 }
253 - result = { kind: "return", value };
255 + result = { kind: "return", value, id: terminal.id };
256 break;
257 }
258 case "switch": {
@@ -261,11 +263,16 @@ class ReactiveFunctionBuilder
263 test: Place | null;
264 block: ReactiveBlock | void;
265 }>,
266 + id: terminal.id,
267 };
268 break;
269 }
270 case "throw": {
268 - result = { kind: "throw", value: terminal.value as Place };
271 + result = {
272 + kind: "throw",
273 + value: terminal.value as Place,
274 + id: terminal.id,
275 + };
276 break;
277 }
278 case "while": {
@@ -273,6 +280,7 @@ class ReactiveFunctionBuilder
280 kind: "while",
281 test: terminal.test as ReactiveValueBlock,
282 loop: terminal.loop,
283 + id: terminal.id,
284 };
285 break;
286 }
compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts new
+230
@@ -0,0 +1,230 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import invariant from "invariant";
9 +import {
10 + BlockId,
11 + HIRFunction,
12 + Instruction,
13 + InstructionId,
14 + InstructionValue,
15 + Place,
16 + ReactiveBlock,
17 + ReactiveFunction,
18 + ReactiveStatement,
19 + ReactiveTerminal,
20 + ReactiveValueBlock,
21 +} from "../HIR/HIR";
22 +import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
23 +import { assertExhaustive } from "../Utils/utils";
24 +
25 +export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
26 + const builder = new ReactiveFunctionBuilder();
27 + const body = visitTree(fn, builder);
28 + invariant(body != null, "Expected a root block");
29 + return {
30 + loc: fn.loc,
31 + id: fn.id,
32 + params: fn.params,
33 + generator: fn.generator,
34 + async: fn.async,
35 + body,
36 + };
37 +}
38 +
39 +class Builder {
40 + #instructions: ReactiveBlock = [];
41 +
42 + append(item: ReactiveStatement, label: BlockId | undefined): void {
43 + if (label !== undefined) {
44 + invariant(item.kind === "terminal", "Only terminals may have a label");
45 + item.label = label;
46 + }
47 + this.#instructions.push(item);
48 + }
49 +
50 + complete(): ReactiveBlock {
51 + return this.#instructions;
52 + }
53 +}
54 +
55 +class ReactiveFunctionBuilder
56 + implements
57 + Visitor<
58 + Builder,
59 + ReactiveBlock,
60 + ReactiveValueBlock,
61 + ReactiveValueBlock,
62 + InstructionValue | ReactiveValueBlock,
63 + ReactiveStatement,
64 + { test: InstructionValue | null; block: ReactiveBlock }
65 + >
66 +{
67 + enterBlock(): Builder {
68 + return new Builder();
69 + }
70 + appendBlock(
71 + block: Builder,
72 + item: ReactiveStatement,
73 + label?: BlockId | undefined
74 + ): void {
75 + block.append(item, label);
76 + }
77 + leaveBlock(block: Builder): ReactiveBlock {
78 + return block.complete();
79 + }
80 +
81 + enterValueBlock(block: Builder): ReactiveValueBlock {
82 + return {
83 + kind: "value-block",
84 + instructions: [],
85 + value: null,
86 + };
87 + }
88 + appendValueBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
89 + block.instructions.push(item);
90 + }
91 + leaveValueBlock(
92 + block: ReactiveValueBlock,
93 + value: InstructionValue | ReactiveValueBlock | null
94 + ): InstructionValue | ReactiveValueBlock {
95 + if (value !== null) {
96 + invariant(
97 + value.kind !== "value-block",
98 + "Expected value block to end in a value"
99 + );
100 + block.value = value;
101 + }
102 + return block;
103 + }
104 +
105 + enterInitBlock(block: Builder): ReactiveValueBlock {
106 + return this.enterValueBlock(block);
107 + }
108 + appendInitBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
109 + this.appendValueBlock(block, item);
110 + }
111 + leaveInitBlock(block: ReactiveValueBlock): ReactiveValueBlock {
112 + return block;
113 + }
114 +
115 + visitValue(
116 + value: InstructionValue,
117 + id: InstructionId
118 + ): InstructionValue | ReactiveValueBlock {
119 + return value;
120 + }
121 + visitInstruction(
122 + instruction: Instruction,
123 + value: InstructionValue | ReactiveValueBlock
124 + ): ReactiveStatement {
125 + return { kind: "instruction", instruction };
126 + }
127 + visitTerminalId(id: InstructionId): void {}
128 + visitImplicitTerminal(): ReactiveStatement | null {
129 + return null;
130 + }
131 + visitTerminal(
132 + terminal: BlockTerminal<
133 + ReactiveValueBlock,
134 + InstructionValue | ReactiveValueBlock,
135 + ReactiveBlock,
136 + { test: InstructionValue | null; block: ReactiveBlock }
137 + >
138 + ): ReactiveStatement {
139 + let result: ReactiveTerminal;
140 + switch (terminal.kind) {
141 + case "break": {
142 + result = { kind: "break", label: terminal.label, id: terminal.id };
143 + break;
144 + }
145 + case "continue": {
146 + result = { kind: "continue", label: terminal.label, id: terminal.id };
147 + break;
148 + }
149 + case "for": {
150 + const { test, update } = terminal;
151 + result = {
152 + kind: "for",
153 + init: terminal.init,
154 + test: terminal.test as ReactiveValueBlock,
155 + update: terminal.update as ReactiveValueBlock,
156 + loop: terminal.loop,
157 + id: terminal.id,
158 + };
159 + break;
160 + }
161 + case "if": {
162 + result = {
163 + kind: "if",
164 + test: terminal.test as Place,
165 + consequent: terminal.consequent,
166 + alternate: terminal.alternate,
167 + id: terminal.id,
168 + };
169 + break;
170 + }
171 + case "return": {
172 + const value = terminal.value;
173 + if (value !== null && value.kind !== "Identifier") {
174 + invariant(false, "Expected return to be a Place");
175 + }
176 + result = { kind: "return", value, id: terminal.id };
177 + break;
178 + }
179 + case "switch": {
180 + result = {
181 + kind: "switch",
182 + test: terminal.test as Place,
183 + cases: terminal.cases as Array<{
184 + test: Place | null;
185 + block: ReactiveBlock | void;
186 + }>,
187 + id: terminal.id,
188 + };
189 + break;
190 + }
191 + case "throw": {
192 + result = {
193 + kind: "throw",
194 + value: terminal.value as Place,
195 + id: terminal.id,
196 + };
197 + break;
198 + }
199 + case "while": {
200 + result = {
201 + kind: "while",
202 + test: terminal.test as ReactiveValueBlock,
203 + loop: terminal.loop,
204 + id: terminal.id,
205 + };
206 + break;
207 + }
208 + default: {
209 + assertExhaustive(
210 + terminal,
211 + `Unexpected terminal kind '${(terminal as any).kind}'`
212 + );
213 + }
214 + }
215 + return {
216 + kind: "terminal",
217 + terminal: result,
218 + label: null,
219 + };
220 + }
221 + visitCase(
222 + test: InstructionValue | ReactiveValueBlock | null,
223 + block: ReactiveBlock
224 + ): { test: InstructionValue | null; block: ReactiveBlock } {
225 + if (test !== null && test.kind !== "Identifier") {
226 + invariant(false, "Expected a Place");
227 + }
228 + return { test, block };
229 + }
230 +}
compiler/forget/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts new
+85
@@ -0,0 +1,85 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { ReactiveFunction } from "../HIR";
9 +
10 +/**
11 + * Note: this is the 3rd of 3 passes that determine how to break a function into discrete
12 + * reactive scopes (independently memoizeable units of code):
13 + * 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
14 + * them a unique reactive scope.
15 + * 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
16 + * to block scopes.
17 + * 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
18 + * scopes do not overlap, merging any such scopes.
19 + *
20 + * Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
21 + * the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
22 + * for performance reasons (side effects are less of a concern bc components are required to be
23 + * idempotent), so we cannot simply repeat the instruction once for each scope. Instead, the only
24 + * option is to combine the two scopes into one. This is an area where an eventual Forget IDE
25 + * could provide real-time feedback to the developer that two computations are accidentally merged.
26 + *
27 + * ## Detailed Walkthrough
28 + *
29 + * Two scopes overlap if there is one or more instruction that is inside the range
30 + * of both scopes. In general, overlapping scopes are merged togther. The only
31 + * exception to this is when one scope *shadows* another scope. For example:
32 + *
33 + * ```javascript
34 + * function foo(cond, a) {
35 + * ⌵ scope for x
36 + * let x = []; ⌝
37 + * if (cond) { ⎮
38 + * ⌵ scope for y ⎮
39 + * let y = []; ⌝ ⎮
40 + * if (b) { ⎮ ⎮
41 + * y.push(b); ⌟ ⎮
42 + * } ⎮
43 + * x.push(<div>{y}</div>); ⎮
44 + * } ⌟
45 + * }
46 + * ```
47 + *
48 + * In this example the two scopes overlap, but mutation of the two scopes is not
49 + * interleaved. Specifically within the y scope there are no instructions that
50 + * modify any other scope: the inner scope "shadows" the outer one. This category
51 + * of overlap does *NOT* merge the scopes together.
52 + *
53 + * The implementation is inspired by the Rust notion of "stacked borrows". We traverse
54 + * the control-flow graph in tree form, at each point keeping track of which scopes are
55 + * active. So initially we see
56 + *
57 + * `let x = []`
58 + * active scopes: [x]
59 + *
60 + * and mark the x scope as active.
61 + *
62 + * Then we later encounter
63 + *
64 + * `let y = [];`
65 + * active scopes: [x, y]
66 + *
67 + * Here we first check to see if 'y' is already in the list of active scopes. It isn't,
68 + * so we push it to the stop of the stack.
69 + *
70 + * Then
71 + *
72 + * `y.push(b)`
73 + * active scopes: [x, y]
74 + *
75 + * Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
76 + *
77 + * If instead we saw eg
78 + *
79 + * `x.push(b)`
80 + * active scopes: [x, y]
81 + *
82 + * Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
83 + * to be merged.
84 + */
85 +export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {}
compiler/forget/src/ReactiveScopes/index.ts
+4
@@ -5,11 +5,15 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +export { alignReactiveScopesToBlockScopes } from "./AlignReactiveScopesToBlockScopes";
9 +export { buildReactiveBlocks } from "./BuildReactiveBlocks";
10 export { buildReactiveFunction } from "./BuildReactiveFunction";
11 +export { buildReactiveFunction as buildReactiveFunctionWithoutScopes } from "./BuildReactiveFunctionWithoutScopes";
12 export { codegenReactiveFunction } from "./CodegenReactiveFunction";
13 export { flattenReactiveLoops } from "./FlattenReactiveLoops";
14 export { inferReactiveScopes } from "./InferReactiveScopes";
15 export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
16 +export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
17 export { printReactiveFunction } from "./PrintReactiveFunction";
18 export { propagateScopeDependencies } from "./PropagateScopeDependencies";
19 export { pruneTemporaryLValues as pruneUnusedLValues } from "./PruneTemporaryLValues";
compiler/forget/src/__tests__/hir-test.ts
+4
@@ -13,6 +13,7 @@ import traverse from "@babel/traverse";
13 import { wasmFolder } from "@hpcc-js/wasm";
14 import path from "path";
15 import prettier from "prettier";
16 +import { flags } from "../CompilerFlags";
17 import { compile } from "../CompilerPipeline";
18 import { toggleLogging } from "../Utils/logger";
19 import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
@@ -30,6 +31,9 @@ wasmFolder(
31 const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
32
33 describe("React Forget (HIR version)", () => {
34 + flags.enableNewReactiveFunctionBuilder =
35 + String(process.env["ENABLE_NEW_BUILDER"]) === "1";
36 +
37 generateTestsFromFixtures(
38 path.join(__dirname, "fixtures", "hir"),
39 (input, file, options) => {
compiler/forget/src/index.ts
+1 -1
@@ -20,7 +20,7 @@ import codegen from "./HIR/Codegen";
20 import { Environment } from "./HIR/HIRBuilder";
21 import printHIR, { printFunction } from "./HIR/PrintHIR";
22 import { inferMutableRanges, inferReferenceEffects } from "./Inference";
23 -import { buildReactiveFunction } from "./ReactiveScopes/BuildReactiveFunction";
23 +import { buildReactiveFunction } from "./ReactiveScopes/BuildReactiveFunctionWithoutScopes";
24 import { codegenReactiveFunction } from "./ReactiveScopes/CodegenReactiveFunction";
25 import { flattenReactiveLoops } from "./ReactiveScopes/FlattenReactiveLoops";
26 import { inferReactiveScopes } from "./ReactiveScopes/InferReactiveScopes";