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

Switch to new BuildReactiveFunction passes

Joe Savona committed Jan 12, 2023 at 11:06 UTC da21e9c52507da43d9bce1e45e7c623722f8d664
9 files changed +34 -1826
compiler/forget/src/CompilerFlags.ts
+2 -6
@@ -5,10 +5,6 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -type Flags = {
9 - enableNewReactiveFunctionBuilder: boolean;
10 -};
8 +type Flags = {};
9
12 -export const flags: Flags = {
13 - enableNewReactiveFunctionBuilder: false,
14 -};
10 +export const flags: Flags = {};
compiler/forget/src/CompilerPipeline.ts
+27 -43
@@ -6,7 +6,6 @@
6 */
7 import { NodePath } from "@babel/traverse";
8 import * as t from "@babel/types";
9 -import { flags } from "./CompilerFlags";
9 import {
10 Environment,
11 HIRFunction,
@@ -20,10 +19,8 @@ import {
19 alignReactiveScopesToBlockScopes,
20 buildReactiveBlocks,
21 buildReactiveFunction,
23 - buildReactiveFunctionWithoutScopes,
22 codegenReactiveFunction,
23 flattenReactiveLoops,
26 - inferReactiveScopes,
24 inferReactiveScopeVariables,
25 mergeOverlappingReactiveScopes,
26 propagateScopeDependencies,
@@ -77,46 +74,33 @@ export function* run(
74 inferReactiveScopeVariables(hir);
75 yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir });
76
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 - }
77 + const reactiveFunction = buildReactiveFunction(hir);
78 + yield log({
79 + kind: "reactive",
80 + name: "BuildReactiveFunction",
81 + value: reactiveFunction,
82 + });
83 +
84 + alignReactiveScopesToBlockScopes(reactiveFunction);
85 + yield log({
86 + kind: "reactive",
87 + name: "AlignReactiveScopesToBlockScopes",
88 + value: reactiveFunction,
89 + });
90 +
91 + mergeOverlappingReactiveScopes(reactiveFunction);
92 + yield log({
93 + kind: "reactive",
94 + name: "MergeOverlappingReactiveScopes",
95 + value: reactiveFunction,
96 + });
97 +
98 + buildReactiveBlocks(reactiveFunction);
99 + yield log({
100 + kind: "reactive",
101 + name: "BuildReactiveBlocks",
102 + value: reactiveFunction,
103 + });
104
105 pruneUnusedLabels(reactiveFunction);
106 yield log({
compiler/forget/src/HIR/ReactiveFunctionVisitor.ts deleted
-958
@@ -1,958 +0,0 @@
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 todo from "../Utils/todo";
10 -import { assertExhaustive } from "../Utils/utils";
11 -import {
12 - BasicBlock,
13 - BlockId,
14 - GotoVariant,
15 - HIR,
16 - HIRFunction,
17 - Instruction,
18 - InstructionId,
19 - InstructionValue,
20 - Place,
21 - SourceLocation,
22 -} from "./HIR";
23 -
24 -/**
25 - * Function to visit HIR as a tree of high-level constructs rather than as a sequence
26 - * of lower-level basic blocks. Intended for use in codegen and reactive scope
27 - * construction which need to see the original "shape" of the code.
28 - *
29 - * See the {@link Visitor} interface for more about implementing a visitor.
30 - */
31 -export function visitTreeForReactiveFunction<
32 - TBlockBuilder,
33 - TBlock,
34 - TInit,
35 - TValueBlock,
36 - TValue,
37 - TStatement,
38 - TCase
39 ->(
40 - fn: HIRFunction,
41 - visitor: Visitor<
42 - TBlockBuilder,
43 - TBlock,
44 - TInit,
45 - TValueBlock,
46 - TValue,
47 - TStatement,
48 - TCase
49 - >
50 -): TBlock {
51 - const cx = new Context(fn.body);
52 - const driver = new Driver(cx, visitor);
53 - return driver.traverseBlock(cx.block(fn.body.entry));
54 -}
55 -
56 -class Driver<
57 - TBlockBuilder,
58 - TBlock,
59 - TInit,
60 - TValueBlock,
61 - TValue,
62 - TStatement,
63 - TCase
64 -> {
65 - cx: Context;
66 - visitor: Visitor<
67 - TBlockBuilder,
68 - TBlock,
69 - TInit,
70 - TValueBlock,
71 - TValue,
72 - TStatement,
73 - TCase
74 - >;
75 -
76 - constructor(
77 - cx: Context,
78 - visitor: Visitor<
79 - TBlockBuilder,
80 - TBlock,
81 - TInit,
82 - TValueBlock,
83 - TValue,
84 - TStatement,
85 - TCase
86 - >
87 - ) {
88 - this.cx = cx;
89 - this.visitor = visitor;
90 - }
91 -
92 - traverseBlock(block: BasicBlock): TBlock {
93 - const blockValue = this.visitor.enterBlock();
94 - this.visitBlock(block, blockValue);
95 - return this.visitor.leaveBlock(blockValue);
96 - }
97 -
98 - visitBlock(block: BasicBlock, blockValue: TBlockBuilder): void {
99 - invariant(
100 - !this.cx.emitted.has(block.id),
101 - `Cannot emit the same block twice: bb${block.id}`
102 - );
103 - this.cx.emitted.add(block.id);
104 - for (const instr of block.instructions) {
105 - this.visitInstr(instr, blockValue);
106 - }
107 -
108 - const terminal = block.terminal;
109 - const scheduleIds = [];
110 - switch (terminal.kind) {
111 - case "return": {
112 - const value =
113 - terminal.value != null
114 - ? this.visitPlace(terminal.value, terminal.id)
115 - : null;
116 - this.visitor.visitTerminalId(terminal.id);
117 - this.visitor.appendBlock(
118 - blockValue,
119 - this.visitor.visitTerminal({
120 - kind: "return",
121 - loc: terminal.loc,
122 - value,
123 - id: terminal.id,
124 - })
125 - );
126 - break;
127 - }
128 - case "throw": {
129 - const value = this.visitPlace(terminal.value, terminal.id);
130 - this.visitor.visitTerminalId(terminal.id);
131 - this.visitor.appendBlock(
132 - blockValue,
133 - this.visitor.visitTerminal({
134 - kind: "throw",
135 - value,
136 - id: terminal.id,
137 - })
138 - );
139 - break;
140 - }
141 - case "if": {
142 - const test = this.visitPlace(terminal.test, terminal.id);
143 - const fallthroughId =
144 - terminal.fallthrough !== null &&
145 - !this.cx.isScheduled(terminal.fallthrough)
146 - ? terminal.fallthrough
147 - : null;
148 - const alternateId =
149 - terminal.alternate !== terminal.fallthrough
150 - ? terminal.alternate
151 - : null;
152 -
153 - if (fallthroughId !== null) {
154 - const scheduleId = this.cx.schedule(fallthroughId, "if");
155 - scheduleIds.push(scheduleId);
156 - }
157 -
158 - this.visitor.visitTerminalId(terminal.id);
159 - let consequent: TBlock | null = null;
160 - if (this.cx.isScheduled(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_);
165 - consequent = this.visitor.leaveBlock(builder);
166 - }
167 - } else {
168 - consequent = this.traverseBlock(
169 - this.cx.ir.blocks.get(terminal.consequent)!
170 - );
171 - }
172 -
173 - let alternate: TBlock | null = null;
174 - if (alternateId !== null) {
175 - if (this.cx.isScheduled(alternateId)) {
176 - const break_ = this.visitBreak(alternateId, null);
177 - if (break_ !== null) {
178 - const builder = this.visitor.enterBlock();
179 - this.visitor.appendBlock(builder, break_);
180 - alternate = this.visitor.leaveBlock(builder);
181 - }
182 - } else {
183 - alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
184 - }
185 - }
186 -
187 - this.cx.unscheduleAll(scheduleIds);
188 - if (fallthroughId !== null) {
189 - this.visitor.appendBlock(
190 - blockValue,
191 - this.visitor.visitTerminal({
192 - kind: "if",
193 - test,
194 - consequent: consequent ?? this.emptyBlock(),
195 - alternate: alternate,
196 - id: terminal.id,
197 - }),
198 - fallthroughId
199 - );
200 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
201 - } else {
202 - this.visitor.appendBlock(
203 - blockValue,
204 - this.visitor.visitTerminal({
205 - kind: "if",
206 - test,
207 - consequent: consequent ?? this.emptyBlock(),
208 - alternate: alternate,
209 - id: terminal.id,
210 - })
211 - );
212 - }
213 - break;
214 - }
215 - case "switch": {
216 - const test = this.visitPlace(terminal.test, terminal.id);
217 - const fallthroughId =
218 - terminal.fallthrough !== null &&
219 - !this.cx.isScheduled(terminal.fallthrough)
220 - ? terminal.fallthrough
221 - : null;
222 - if (fallthroughId !== null) {
223 - const scheduleId = this.cx.schedule(fallthroughId, "switch");
224 - scheduleIds.push(scheduleId);
225 - }
226 -
227 - this.visitor.visitTerminalId(terminal.id);
228 - const cases: Array<TCase> = [];
229 - [...terminal.cases].reverse().forEach((case_, index) => {
230 - const test =
231 - case_.test !== null
232 - ? this.visitPlace(case_.test, terminal.id)
233 - : null;
234 -
235 - let consequent;
236 - if (this.cx.isScheduled(case_.block)) {
237 - // cases which are empty or contain only a `break` may point to blocks
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.
241 - const break_ = this.visitBreak(case_.block, null);
242 - if (
243 - index === 0 &&
244 - break_ === null &&
245 - case_.block === terminal.fallthrough &&
246 - case_.test === null
247 - ) {
248 - // If the last case statement (first in reverse order) is a default that
249 - // jumps to the fallthrough, then we would emit a useless `default: {}`,
250 - // so instead skip this case.
251 - return;
252 - }
253 - const block = this.visitor.enterBlock();
254 - if (break_ !== null) {
255 - this.visitor.appendBlock(block, break_);
256 - }
257 - consequent = this.visitor.leaveBlock(block);
258 - } else {
259 - consequent = this.traverseBlock(
260 - this.cx.ir.blocks.get(case_.block)!
261 - );
262 - const scheduleId = this.cx.schedule(case_.block, "case");
263 - scheduleIds.push(scheduleId);
264 - }
265 - cases.push(this.visitor.visitCase(test, consequent));
266 - });
267 - cases.reverse();
268 -
269 - this.cx.unscheduleAll(scheduleIds);
270 - if (fallthroughId !== null) {
271 - this.visitor.appendBlock(
272 - blockValue,
273 - this.visitor.visitTerminal({
274 - kind: "switch",
275 - test,
276 - cases,
277 - id: terminal.id,
278 - }),
279 - fallthroughId
280 - );
281 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
282 - } else {
283 - this.visitor.appendBlock(
284 - blockValue,
285 - this.visitor.visitTerminal({
286 - kind: "switch",
287 - test,
288 - cases,
289 - id: terminal.id,
290 - })
291 - );
292 - }
293 - break;
294 - }
295 - case "while": {
296 - const fallthroughId =
297 - terminal.fallthrough !== null &&
298 - !this.cx.isScheduled(terminal.fallthrough)
299 - ? terminal.fallthrough
300 - : null;
301 - const loopId =
302 - !this.cx.isScheduled(terminal.loop) &&
303 - terminal.loop !== terminal.fallthrough
304 - ? terminal.loop
305 - : null;
306 - const scheduleId = this.cx.scheduleLoop(
307 - terminal.fallthrough,
308 - terminal.test,
309 - terminal.loop
310 - );
311 - scheduleIds.push(scheduleId);
312 -
313 - this.visitor.visitTerminalId(terminal.id);
314 - const testBlock = this.cx.ir.blocks.get(terminal.test)!;
315 - const testTerminal = testBlock.terminal;
316 - invariant(
317 - testTerminal.kind === "if",
318 - "Expected while loop test block to end in an if"
319 - );
320 - const testValue = this.visitValueBlock(blockValue, testBlock, {
321 - value: testTerminal.test,
322 - id: testTerminal.id,
323 - });
324 - let loopBody: TBlock;
325 - if (loopId) {
326 - loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
327 - } else {
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"
332 - );
333 - const body = this.visitor.enterBlock();
334 - this.visitor.appendBlock(body, break_);
335 - loopBody = this.visitor.leaveBlock(body);
336 - }
337 -
338 - this.cx.unscheduleAll(scheduleIds);
339 - if (fallthroughId !== null) {
340 - this.visitor.appendBlock(
341 - blockValue,
342 - this.visitor.visitTerminal({
343 - kind: "while",
344 - loc: terminal.loc,
345 - test: testValue,
346 - loop: loopBody,
347 - id: terminal.id,
348 - }),
349 - fallthroughId
350 - );
351 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
352 - } else {
353 - this.visitor.appendBlock(
354 - blockValue,
355 - this.visitor.visitTerminal({
356 - kind: "while",
357 - loc: terminal.loc,
358 - test: testValue,
359 - loop: loopBody,
360 - id: terminal.id,
361 - })
362 - );
363 - }
364 - break;
365 - }
366 - case "for": {
367 - const loopId =
368 - !this.cx.isScheduled(terminal.loop) &&
369 - terminal.loop !== terminal.fallthrough
370 - ? terminal.loop
371 - : null;
372 -
373 - const fallthroughId =
374 - terminal.fallthrough !== null &&
375 - !this.cx.isScheduled(terminal.fallthrough)
376 - ? terminal.fallthrough
377 - : null;
378 -
379 - const scheduleId = this.cx.scheduleLoop(
380 - terminal.fallthrough,
381 - terminal.update,
382 - terminal.loop
383 - );
384 - scheduleIds.push(scheduleId);
385 -
386 - this.visitor.visitTerminalId(terminal.id);
387 -
388 - const initBlock = this.cx.ir.blocks.get(terminal.init)!;
389 - const initTerminal = initBlock.terminal;
390 - invariant(
391 - initTerminal.kind === "goto",
392 - "Expected for loop init block to end in a goto"
393 - );
394 - const initValue = this.visitInitBlock(blockValue, initBlock);
395 -
396 - const testBlock = this.cx.ir.blocks.get(terminal.test)!;
397 - const testTerminal = testBlock.terminal;
398 - invariant(
399 - testTerminal.kind === "if",
400 - "Expected for loop test block to end in an if"
401 - );
402 - const testValue = this.visitValueBlock(blockValue, testBlock, {
403 - value: testTerminal.test,
404 - id: testTerminal.id,
405 - });
406 -
407 - const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
408 - const updateTerminal = updateBlock.terminal;
409 - invariant(
410 - updateTerminal.kind === "goto",
411 - "Expected for loop update block to end in a goto"
412 - );
413 - const updateValue = this.visitValueBlock(blockValue, updateBlock);
414 -
415 - let loopBody: TBlock;
416 - if (loopId) {
417 - loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
418 - } else {
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"
423 - );
424 - const body = this.visitor.enterBlock();
425 - this.visitor.appendBlock(body, break_);
426 - loopBody = this.visitor.leaveBlock(body);
427 - }
428 -
429 - this.cx.unscheduleAll(scheduleIds);
430 - if (fallthroughId !== null) {
431 - this.visitor.appendBlock(
432 - blockValue,
433 - this.visitor.visitTerminal({
434 - kind: "for",
435 - init: initValue,
436 - test: testValue,
437 - update: updateValue,
438 - loop: loopBody,
439 - id: terminal.id,
440 - }),
441 - fallthroughId
442 - );
443 - this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
444 - } else {
445 - this.visitor.appendBlock(
446 - blockValue,
447 - this.visitor.visitTerminal({
448 - kind: "for",
449 - init: initValue,
450 - test: testValue,
451 - update: updateValue,
452 - loop: loopBody,
453 - id: terminal.id,
454 - })
455 - );
456 - }
457 - break;
458 - }
459 - case "logical": {
460 - todo("Implement tree visitor for logical terminal");
461 - }
462 - case "goto": {
463 - this.visitor.visitTerminalId(terminal.id);
464 - switch (terminal.variant) {
465 - case GotoVariant.Break: {
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: {
473 - const continue_ = this.visitContinue(terminal.block, terminal.id);
474 - if (continue_ !== null) {
475 - this.visitor.appendBlock(blockValue, continue_);
476 - }
477 - break;
478 - }
479 - default: {
480 - assertExhaustive(
481 - terminal.variant,
482 - `Unexpected goto variant '${terminal.variant}'`
483 - );
484 - }
485 - }
486 - break;
487 - }
488 - default: {
489 - assertExhaustive(terminal, "Unexpected terminal");
490 - }
491 - }
492 - }
493 -
494 - visitInitBlock(parent: TBlockBuilder, block: BasicBlock): TInit {
495 - const initBlock = this.visitor.enterInitBlock(parent);
496 - for (const instr of block.instructions) {
497 - const value = this.visitor.visitValue(instr.value, instr.id);
498 - const item = this.visitor.visitInstruction(instr, value);
499 - this.visitor.appendInitBlock(initBlock, item);
500 - }
501 - return this.visitor.leaveInitBlock(initBlock);
502 - }
503 -
504 - visitValueBlock(
505 - parent: TBlockBuilder,
506 - block: BasicBlock,
507 - terminalValue?: { value: InstructionValue; id: InstructionId }
508 - ): TValue {
509 - const valueBlock = this.visitor.enterValueBlock(parent);
510 - const instructions = [...block.instructions];
511 - let lastValue: { value: InstructionValue; id: InstructionId } | null = null;
512 - if (terminalValue != null) {
513 - lastValue = terminalValue;
514 - }
515 - for (const instr of instructions) {
516 - const value = this.visitor.visitValue(instr.value, instr.id);
517 - const item = this.visitor.visitInstruction(instr, value);
518 - this.visitor.appendValueBlock(valueBlock, item);
519 - }
520 - const value =
521 - lastValue !== null
522 - ? this.visitor.visitValue(lastValue.value, lastValue.id)
523 - : null;
524 - return this.visitor.leaveValueBlock(valueBlock, value);
525 - }
526 -
527 - emptyBlock(): TBlock {
528 - const block = this.visitor.enterBlock();
529 - return this.visitor.leaveBlock(block);
530 - }
531 -
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
536 - return null;
537 - }
538 - switch (target.type) {
539 - case "implicit": {
540 - return this.visitor.visitImplicitTerminal();
541 - }
542 - case "unlabeled": {
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 -
555 - visitContinue(block: BlockId, id: InstructionId): TStatement | null {
556 - const target = this.cx.getContinueTarget(block);
557 - invariant(
558 - target !== null,
559 - `Expected continue target to be scheduled for bb${block}`
560 - );
561 - switch (target.type) {
562 - case "labeled": {
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": {
577 - return this.visitor.visitImplicitTerminal();
578 - }
579 - default: {
580 - assertExhaustive(
581 - target.type,
582 - `Unexpected continue target kind '${(target as any).type}'`
583 - );
584 - }
585 - }
586 - }
587 -
588 - visitInstr(instr: Instruction, blockValue: TBlockBuilder): void {
589 - const value = this.visitor.visitValue(instr.value, instr.id);
590 - const item = this.visitor.visitInstruction(instr, value);
591 - this.visitor.appendBlock(blockValue, item);
592 - }
593 -
594 - visitPlace(place: Place, id: InstructionId): TValue {
595 - return this.visitor.visitValue(place, id);
596 - }
597 -}
598 -
599 -class Context {
600 - ir: HIR;
601 - #nextScheduleId: number = 0;
602 -
603 - /**
604 - * Used to track which blocks *have been* generated already in order to
605 - * abort if a block is generated a second time. This is an error catching
606 - * mechanism for debugging purposes, and is not used by the codegen algorithm
607 - * to drive decisions about how to emit blocks.
608 - */
609 - emitted: Set<BlockId> = new Set();
610 -
611 - /**
612 - * A set of blocks that are already scheduled to be emitted by eg a parent.
613 - * This allows child nodes to avoid re-emitting the same block and emit eg
614 - * a break instead.
615 - */
616 - #scheduled: Set<BlockId> = new Set();
617 -
618 - /**
619 - * Represents which control flow operations are currently in scope, with the innermost
620 - * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
621 - * control will implicitly transfer, such that gotos to that block can be elided. Gotos
622 - * targeting items higher up the stack may need labeled break or continue; see
623 - * getBreakTarget() and getContinueTarget() for more details.
624 - */
625 - #controlFlowStack: Array<ControlFlowTarget> = [];
626 -
627 - constructor(ir: HIR) {
628 - this.ir = ir;
629 - }
630 -
631 - block(id: BlockId): BasicBlock {
632 - return this.ir.blocks.get(id)!;
633 - }
634 -
635 - /**
636 - * Record that the given block will be emitted (eg by the codegen of a parent node)
637 - * so that child nodes can avoid re-emitting it.
638 - */
639 - schedule(block: BlockId, type: "if" | "switch" | "case"): number {
640 - const id = this.#nextScheduleId++;
641 - invariant(
642 - !this.#scheduled.has(block),
643 - `Break block is already scheduled: bb${block}`
644 - );
645 - this.#scheduled.add(block);
646 - this.#controlFlowStack.push({ block, id, type });
647 - return id;
648 - }
649 -
650 - scheduleLoop(
651 - fallthroughBlock: BlockId,
652 - continueBlock: BlockId,
653 - loopBlock: BlockId | null
654 - ): number {
655 - const id = this.#nextScheduleId++;
656 - const ownsBlock = !this.#scheduled.has(fallthroughBlock);
657 - this.#scheduled.add(fallthroughBlock);
658 - invariant(
659 - !this.#scheduled.has(continueBlock),
660 - `Continue block is already scheduled: bb${continueBlock}`
661 - );
662 - this.#scheduled.add(continueBlock);
663 - let ownsLoop = false;
664 - if (loopBlock !== null) {
665 - ownsLoop = !this.#scheduled.has(loopBlock);
666 - this.#scheduled.add(loopBlock);
667 - }
668 -
669 - this.#controlFlowStack.push({
670 - block: fallthroughBlock,
671 - ownsBlock,
672 - id,
673 - type: "loop",
674 - continueBlock,
675 - loopBlock,
676 - ownsLoop,
677 - });
678 - return id;
679 - }
680 -
681 - /**
682 - * Removes a block that was scheduled; must be called after that block is emitted.
683 - */
684 - unschedule(scheduleId: number): void {
685 - const last = this.#controlFlowStack.pop();
686 - invariant(
687 - last !== undefined && last.id === scheduleId,
688 - "Can only unschedule the last target"
689 - );
690 - if (last.type !== "loop" || last.ownsBlock !== null) {
691 - this.#scheduled.delete(last.block);
692 - }
693 - if (last.type === "loop") {
694 - this.#scheduled.delete(last.continueBlock);
695 - if (last.ownsLoop && last.loopBlock !== null) {
696 - this.#scheduled.delete(last.loopBlock);
697 - }
698 - }
699 - }
700 -
701 - /**
702 - * Helper to unschedule multiple scheduled blocks. The ids should be in
703 - * the order in which they were scheduled, ie most recently scheduled last.
704 - */
705 - unscheduleAll(scheduleIds: Array<number>): void {
706 - for (let i = scheduleIds.length - 1; i >= 0; i--) {
707 - this.unschedule(scheduleIds[i]!);
708 - }
709 - }
710 -
711 - /**
712 - * Check if the given @param block is scheduled or not.
713 - */
714 - isScheduled(block: BlockId): boolean {
715 - return this.#scheduled.has(block);
716 - }
717 -
718 - /**
719 - * Given the current control flow stack, determines how a `break` to the given @param block
720 - * must be emitted. Returns as follows:
721 - * - 'implicit' if control would implicitly transfer to that block
722 - * - 'labeled' if a labeled break is required to transfer control to that block
723 - * - 'unlabeled' if an unlabeled break would transfer to that block
724 - * - null if there is no information for this block
725 - *
726 - * The returned 'block' value should be used as the label if necessary.
727 - */
728 - getBreakTarget(
729 - block: BlockId
730 - ): { block: BlockId; type: ControlFlowKind } | null {
731 - let hasPrecedingLoop = false;
732 - for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
733 - const target = this.#controlFlowStack[i]!;
734 - if (target.block === block) {
735 - let type: ControlFlowKind;
736 - if (target.type === "loop") {
737 - // breaking out of a loop requires an explicit break,
738 - // but only requires a label if breaking past the innermost loop.
739 - type = hasPrecedingLoop ? "labeled" : "unlabeled";
740 - } else if (i === this.#controlFlowStack.length - 1) {
741 - // breaking to the last break point, which is where control will transfer
742 - // implicitly
743 - type = "implicit";
744 - } else {
745 - // breaking somewhere else requires an explicit break
746 - type = "labeled";
747 - }
748 - return {
749 - block: target.block,
750 - type,
751 - };
752 - }
753 - hasPrecedingLoop ||= target.type === "loop";
754 - }
755 - return null;
756 - }
757 -
758 - /**
759 - * Given the current control flow stack, determines how a `continue` to the given @param block
760 - * must be emitted. Returns as follows:
761 - * - 'implicit' if control would implicitly continue to that block
762 - * - 'labeled' if a labeled continue is required to continue to that block
763 - * - 'unlabeled' if an unlabeled continue would transfer to that block
764 - * - null if there is no information for this block
765 - *
766 - * The returned 'block' value should be used as the label if necessary.
767 - */
768 - getContinueTarget(
769 - block: BlockId
770 - ): { block: BlockId; type: ControlFlowKind } | null {
771 - let hasPrecedingLoop = false;
772 - for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
773 - const target = this.#controlFlowStack[i]!;
774 - if (target.type == "loop" && target.continueBlock === block) {
775 - let type: ControlFlowKind;
776 - if (hasPrecedingLoop) {
777 - // continuing to a loop that is not the innermost loop always requires
778 - // a label
779 - type = "labeled";
780 - } else if (i === this.#controlFlowStack.length - 1) {
781 - // continuing to the last break point, which is where control will
782 - // transfer to naturally
783 - type = "implicit";
784 - } else {
785 - // the continue is inside some conditional logic, requires an explicit
786 - // continue
787 - type = "unlabeled";
788 - }
789 - return {
790 - block: target.block,
791 - type,
792 - };
793 - }
794 - hasPrecedingLoop ||= target.type === "loop";
795 - }
796 - return null;
797 - }
798 -
799 - debugBreakTargets(): Array<ControlFlowTarget> {
800 - return this.#controlFlowStack.map((target) => ({ ...target }));
801 - }
802 -}
803 -
804 -type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
805 -
806 -type ControlFlowTarget =
807 - | { type: "if"; block: BlockId; id: number }
808 - | { type: "switch"; block: BlockId; id: number }
809 - | { type: "case"; block: BlockId; id: number }
810 - | {
811 - type: "loop";
812 - block: BlockId;
813 - ownsBlock: boolean;
814 - continueBlock: BlockId;
815 - loopBlock: BlockId | null;
816 - ownsLoop: boolean;
817 - id: number;
818 - };
819 -
820 -/**
821 - * An object that can receive structured callbacks to visit HIR as a tree,
822 - * and convert it to an alternate format.
823 - *
824 - * TBlock = representation of a list of statements
825 - * TValue = represenation of an InstructionValue
826 - * TStatement = representation of an Instruction
827 - * TCase = representation of a switch case
828 - */
829 -export interface Visitor<
830 - TBlockBuilder,
831 - TBlock,
832 - TInit,
833 - TValueBlock,
834 - TValue,
835 - TStatement,
836 - TCase
837 -> {
838 - /**
839 - * Must create an "empty" instance of the visitor's represenation for
840 - * the contents of a block.
841 - */
842 - enterBlock(): TBlockBuilder;
843 -
844 - /**
845 - * Appends an item onto the given block, with an optional label. The label
846 - * indicates that a break/continue will proceed to code *after* the given item.
847 - */
848 - appendBlock(block: TBlockBuilder, item: TStatement, label?: BlockId): void;
849 -
850 - /**
851 - * Converts the visitor's block representation into the representation of a
852 - * block item, simultaneously "closing" the given block. The block will no
853 - * longer be modified by the visitor driver.
854 - */
855 - leaveBlock(block: TBlockBuilder): TBlock;
856 -
857 - /**
858 - * Must create an "empty" instance of the visitor's representation for a value
859 - * block, which can generally contain only expressions but not statements. The
860 - * currently active parent block is passed as an argument.
861 - *
862 - * The exception is the for initializer, which may contain declarations but not
863 - * other statement types.
864 - */
865 - enterValueBlock(block: TBlockBuilder): TValueBlock;
866 -
867 - /**
868 - * Appends an item onto the given value block.
869 - */
870 - appendValueBlock(block: TValueBlock, item: TStatement): void;
871 -
872 - /**
873 - * Converts the visitor's value block (and final value) to the visitor's
874 - * value representation.
875 - */
876 - leaveValueBlock(block: TValueBlock, value: TValue | null): TValue;
877 -
878 - enterInitBlock(block: TBlockBuilder): TValueBlock;
879 -
880 - appendInitBlock(block: TValueBlock, item: TStatement): void;
881 -
882 - leaveInitBlock(block: TValueBlock): TInit;
883 -
884 - /**
885 - * Convert an InstructionValue into the visitor's own representation
886 - * of a value.
887 - */
888 - visitValue(value: InstructionValue, id: InstructionId): TValue;
889 -
890 - /**
891 - * Convert an Instruction into the visitor's own representation of
892 - * a block item.
893 - */
894 - visitInstruction(instruction: Instruction, value: TValue): TStatement;
895 -
896 - /**
897 - * Called when a terminal is reached, before processing any of its
898 - * possible branches.
899 - */
900 - visitTerminalId(id: InstructionId): void;
901 -
902 - /**
903 - * Converts a break/continue that is implicit — that does not strictly
904 - * have to be emitted — to the visitor's representation. The visitor
905 - * can choose to return null if this does not need to be represented.
906 - */
907 - visitImplicitTerminal(): TStatement | null;
908 -
909 - /**
910 - * Converts a terminal into the visitor's own representation of a block
911 - * item. Note that the terminal differs from HIR Terminals, because
912 - * values and block ids will have already been converted into the visitor's
913 - * own representations.
914 - */
915 - visitTerminal(
916 - terminal: BlockTerminal<TInit, TValue, TBlock, TCase>
917 - ): TStatement;
918 -
919 - /**
920 - * Visits a switch case statement, which is collected into a switch terminal
921 - * variant.
922 - */
923 - visitCase(test: TValue | null, block: TBlock): TCase;
924 -}
925 -
926 -export type BlockTerminal<TInit, TValue, TBlock, TCase> =
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 - }
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";
951 - init: TInit;
952 - test: TValue;
953 - update: TValue;
954 - loop: TBlock;
955 - id: InstructionId;
956 - }
957 - | { kind: "break"; label: BlockId | null; id: InstructionId | null }
958 - | { kind: "continue"; label: BlockId | null; id: InstructionId };
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+4 -106
@@ -15,15 +15,11 @@ import {
15 Place,
16 ReactiveBlock,
17 ReactiveFunction,
18 - ReactiveScope,
19 - ReactiveScopeBlock,
18 ReactiveStatement,
19 ReactiveTerminal,
20 ReactiveValueBlock,
23 - ScopeId,
21 } from "../HIR/HIR";
22 import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
26 -import { eachInstructionOperand } from "../HIR/visitors";
23 import { assertExhaustive } from "../Utils/utils";
24
25 export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
@@ -40,22 +36,8 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
36 };
37 }
38
43 -type BlockKind =
44 - | { kind: "block"; block: ReactiveBlock }
45 - | { kind: "scope"; block: ReactiveBlock; scope: ReactiveScope };
46 -
39 class Builder {
48 - #instructions: ReactiveBlock;
49 - #stack: Array<
50 - | { kind: "scope"; block: ReactiveScopeBlock }
51 - | { kind: "block"; block: ReactiveBlock }
52 - >;
53 -
54 - constructor() {
55 - const block: ReactiveBlock = [];
56 - this.#instructions = block;
57 - this.#stack = [{ kind: "block", block }];
58 - }
40 + #instructions: ReactiveBlock = [];
41
42 append(item: ReactiveStatement, label: BlockId | undefined): void {
43 if (label !== undefined) {
@@ -65,45 +47,8 @@ class Builder {
47 this.#instructions.push(item);
48 }
49
68 - startScope(scope: ReactiveScope): void {
69 - const block: ReactiveScopeBlock = {
70 - kind: "scope",
71 - scope,
72 - instructions: [],
73 - };
74 - this.append(block, undefined);
75 - this.#instructions = block.instructions;
76 - this.#stack.push({ kind: "scope", block });
77 - }
78 -
79 - visitId(id: InstructionId): void {
80 - for (let i = 0; i < this.#stack.length; i++) {
81 - const entry = this.#stack[i]!;
82 - if (entry.kind === "scope" && id >= entry.block.scope.range.end) {
83 - this.#stack.length = i;
84 - break;
85 - }
86 - }
87 - const last = this.#stack[this.#stack.length - 1]!;
88 - if (last.kind === "block") {
89 - this.#instructions = last.block;
90 - } else {
91 - this.#instructions = last.block.instructions;
92 - }
93 - }
94 -
50 complete(): ReactiveBlock {
96 - // TODO @josephsavona: debug two failures of this
97 - // invariant(
98 - // this.#stack.length === 1,
99 - // "Expected all scopes to be closed when exiting a block"
100 - // );
101 - const first = this.#stack[0]!;
102 - invariant(
103 - first.kind === "block",
104 - "Expected first stack item to be a basic block"
105 - );
106 - return first.block;
51 + return this.#instructions;
52 }
53 }
54
@@ -119,18 +64,8 @@ class ReactiveFunctionBuilder
64 { test: InstructionValue | null; block: ReactiveBlock }
65 >
66 {
122 - #builders: Array<Builder> = [];
123 - #scopes: Set<ScopeId> = new Set();
124 -
125 - visitId(id: InstructionId): void {
126 - const builder = this.#builders[this.#builders.length - 1]!;
127 - builder.visitId(id);
128 - }
129 -
67 enterBlock(): Builder {
131 - const builder = new Builder();
132 - this.#builders.push(builder);
133 - return builder;
68 + return new Builder();
69 }
70 appendBlock(
71 block: Builder,
@@ -140,11 +75,6 @@ class ReactiveFunctionBuilder
75 block.append(item, label);
76 }
77 leaveBlock(block: Builder): ReactiveBlock {
143 - const builder = this.#builders.pop();
144 - invariant(
145 - builder === block,
146 - "Expected enterBlock/leaveBlock to be called 1:1"
147 - );
78 return block.complete();
79 }
80
@@ -196,18 +126,9 @@ class ReactiveFunctionBuilder
126 instruction: Instruction,
127 value: InstructionValue | ReactiveValueBlock
128 ): ReactiveStatement {
199 - this.visitId(instruction.id);
200 - const scope = getInstructionScope(instruction);
201 - if (scope !== null && !this.#scopes.has(scope.id)) {
202 - this.#scopes.add(scope.id);
203 - const builder = this.#builders[this.#builders.length - 1]!;
204 - builder.startScope(scope);
205 - }
129 return { kind: "instruction", instruction };
130 }
208 - visitTerminalId(id: InstructionId): void {
209 - this.visitId(id);
210 - }
131 + visitTerminalId(id: InstructionId): void {}
132 visitImplicitTerminal(): ReactiveStatement | null {
133 return null;
134 }
@@ -311,26 +232,3 @@ class ReactiveFunctionBuilder
232 return { test, block };
233 }
234 }
314 -
315 -function getInstructionScope(instr: Instruction): ReactiveScope | null {
316 - if (
317 - instr.lvalue.place.identifier.scope !== null &&
318 - isScopeActive(instr.lvalue.place.identifier.scope, instr.id)
319 - ) {
320 - return instr.lvalue.place.identifier.scope;
321 - } else {
322 - for (const operand of eachInstructionOperand(instr)) {
323 - if (
324 - operand.identifier.scope !== null &&
325 - isScopeActive(operand.identifier.scope, instr.id)
326 - ) {
327 - return operand.identifier.scope;
328 - }
329 - }
330 - }
331 - return null;
332 -}
333 -
334 -function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
335 - return id >= scope.range.start && id < scope.range.end;
336 -}
compiler/forget/src/ReactiveScopes/BuildReactiveFunctionWithoutScopes.ts deleted
-234
@@ -1,234 +0,0 @@
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 - last: null,
86 - };
87 - }
88 - appendValueBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
89 - block.instructions.push(item);
90 - }
91 - leaveValueBlock(
92 - block: ReactiveValueBlock,
93 - last: {
94 - value: InstructionValue | ReactiveValueBlock;
95 - id: InstructionId;
96 - } | null
97 - ): InstructionValue | ReactiveValueBlock {
98 - if (last !== null) {
99 - const { id, value } = last;
100 - invariant(
101 - value.kind !== "value-block",
102 - "Expected value block to end in a value"
103 - );
104 - block.last = { id, value };
105 - }
106 - return block;
107 - }
108 -
109 - enterInitBlock(block: Builder): ReactiveValueBlock {
110 - return this.enterValueBlock(block);
111 - }
112 - appendInitBlock(block: ReactiveValueBlock, item: ReactiveStatement): void {
113 - this.appendValueBlock(block, item);
114 - }
115 - leaveInitBlock(block: ReactiveValueBlock): ReactiveValueBlock {
116 - return block;
117 - }
118 -
119 - visitValue(
120 - value: InstructionValue,
121 - id: InstructionId
122 - ): InstructionValue | ReactiveValueBlock {
123 - return value;
124 - }
125 - visitInstruction(
126 - instruction: Instruction,
127 - value: InstructionValue | ReactiveValueBlock
128 - ): ReactiveStatement {
129 - return { kind: "instruction", instruction };
130 - }
131 - visitTerminalId(id: InstructionId): void {}
132 - visitImplicitTerminal(): ReactiveStatement | null {
133 - return null;
134 - }
135 - visitTerminal(
136 - terminal: BlockTerminal<
137 - ReactiveValueBlock,
138 - InstructionValue | ReactiveValueBlock,
139 - ReactiveBlock,
140 - { test: InstructionValue | null; block: ReactiveBlock }
141 - >
142 - ): ReactiveStatement {
143 - let result: ReactiveTerminal;
144 - switch (terminal.kind) {
145 - case "break": {
146 - result = { kind: "break", label: terminal.label, id: terminal.id };
147 - break;
148 - }
149 - case "continue": {
150 - result = { kind: "continue", label: terminal.label, id: terminal.id };
151 - break;
152 - }
153 - case "for": {
154 - const { test, update } = terminal;
155 - result = {
156 - kind: "for",
157 - init: terminal.init,
158 - test: terminal.test as ReactiveValueBlock,
159 - update: terminal.update as ReactiveValueBlock,
160 - loop: terminal.loop,
161 - id: terminal.id,
162 - };
163 - break;
164 - }
165 - case "if": {
166 - result = {
167 - kind: "if",
168 - test: terminal.test as Place,
169 - consequent: terminal.consequent,
170 - alternate: terminal.alternate,
171 - id: terminal.id,
172 - };
173 - break;
174 - }
175 - case "return": {
176 - const value = terminal.value;
177 - if (value !== null && value.kind !== "Identifier") {
178 - invariant(false, "Expected return to be a Place");
179 - }
180 - result = { kind: "return", value, id: terminal.id };
181 - break;
182 - }
183 - case "switch": {
184 - result = {
185 - kind: "switch",
186 - test: terminal.test as Place,
187 - cases: terminal.cases as Array<{
188 - test: Place | null;
189 - block: ReactiveBlock | void;
190 - }>,
191 - id: terminal.id,
192 - };
193 - break;
194 - }
195 - case "throw": {
196 - result = {
197 - kind: "throw",
198 - value: terminal.value as Place,
199 - id: terminal.id,
200 - };
201 - break;
202 - }
203 - case "while": {
204 - result = {
205 - kind: "while",
206 - test: terminal.test as ReactiveValueBlock,
207 - loop: terminal.loop,
208 - id: terminal.id,
209 - };
210 - break;
211 - }
212 - default: {
213 - assertExhaustive(
214 - terminal,
215 - `Unexpected terminal kind '${(terminal as any).kind}'`
216 - );
217 - }
218 - }
219 - return {
220 - kind: "terminal",
221 - terminal: result,
222 - label: null,
223 - };
224 - }
225 - visitCase(
226 - test: InstructionValue | ReactiveValueBlock | null,
227 - block: ReactiveBlock
228 - ): { test: InstructionValue | null; block: ReactiveBlock } {
229 - if (test !== null && test.kind !== "Identifier") {
230 - invariant(false, "Expected a Place");
231 - }
232 - return { test, block };
233 - }
234 -}
compiler/forget/src/ReactiveScopes/InferReactiveScopes.ts deleted
-470
@@ -1,470 +0,0 @@
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 - makeInstructionId,
16 - MutableRange,
17 - ReactiveScope,
18 - ScopeId,
19 -} from "../HIR/HIR";
20 -import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
21 -import { printFunction } from "../HIR/PrintHIR";
22 -import {
23 - eachInstructionOperand,
24 - eachInstructionValueOperand,
25 -} from "../HIR/visitors";
26 -import DisjointSet from "../Utils/DisjointSet";
27 -import { log } from "../Utils/logger";
28 -import { retainWhere } from "../Utils/utils";
29 -
30 -/**
31 - * This is a second (final) stage of constructing reactive scopes. Prior to this pass,
32 - * InferReactiveScopeVariables infers the sets of identifiers that "construct together",
33 - * assigning each identifier in each scope the same ScopeId and same MutableRange which
34 - * describes that span.
35 - *
36 - * Note that at this point reactive scopes describe ranges based on specific instructions
37 - * at arbitrary points in the control flow graph. However, reactive scopes must align
38 - * with control-flow boundaries — we can't memoize half of a loop!
39 - *
40 - * This pass refines the reactive scopes as follows:
41 - *
42 - * ## Expanding each reactive scope to align with control-flow boundaries (DONE)
43 - *
44 - * This corresponds with the shape of the AST: a scope that extends into an if consequent
45 - * would expand across the alternate branch, A scope that extends partway into an if
46 - * would expand to cover the full loop body, etc.
47 - *
48 - * ```javascript
49 - * function foo(cond, a) {
50 - * ⌵ original scope
51 - * ⌵ expanded scope
52 - * const x = []; ⌝ ⌝
53 - * if (cond) { ⎮ ⎮
54 - * ... ⎮ ⎮
55 - * x.push(a); ⌟ ⎮
56 - * ... ⎮
57 - * } ⌟
58 - * }
59 - * ```
60 - *
61 - * ## Merging (some) overlapping reactive scopes (TODO)
62 - *
63 - * Two scopes overlap if there is one or more instruction that is inside the range
64 - * of both scopes. In general, overlapping scopes are merged togther. The only
65 - * exception to this is when one scope *shadows* another scope. For example:
66 - *
67 - * ```javascript
68 - * function foo(cond, a) {
69 - * ⌵ scope for x
70 - * let x = []; ⌝
71 - * if (cond) { ⎮
72 - * ⌵ scope for y ⎮
73 - * let y = []; ⌝ ⎮
74 - * if (b) { ⎮ ⎮
75 - * y.push(b); ⌟ ⎮
76 - * } ⎮
77 - * x.push(<div>{y}</div>); ⎮
78 - * } ⌟
79 - * }
80 - * ```
81 - *
82 - * In this example the two scopes overlap, but mutation of the two scopes is not
83 - * interleaved. Specifically within the y scope there are no instructions that
84 - * modify any other scope: the inner scope "shadows" the outer one. This category
85 - * of overlap does *NOT* merge the scopes together.
86 - *
87 - * The implementation is inspired by the Rust notion of "stacked borrows". We traverse
88 - * the control-flow graph in tree form, at each point keeping track of which scopes are
89 - * active. So initially we see
90 - *
91 - * `let x = []`
92 - * active scopes: [x]
93 - *
94 - * and mark the x scope as active.
95 - *
96 - * Then we later encounter
97 - *
98 - * `let y = [];`
99 - * active scopes: [x, y]
100 - *
101 - * Here we first check to see if 'y' is already in the list of active scopes. It isn't,
102 - * so we push it to the stop of the stack.
103 - *
104 - * Then
105 - *
106 - * `y.push(b)`
107 - * active scopes: [x, y]
108 - *
109 - * Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
110 - *
111 - * If instead we saw eg
112 - *
113 - * `x.push(b)`
114 - * active scopes: [x, y]
115 - *
116 - * Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
117 - * to be merged.
118 - */
119 -export function inferReactiveScopes(fn: HIRFunction) {
120 - // Note sure if this is strictly required: in general the only way for two scopes to have
121 - // the same range is if they were already inferred as aliasing together, and therefore
122 - // they should end up with the same range and scope anyway.
123 - mergeScopesWithIdenticalRanges(fn);
124 -
125 - visitTree(fn, new AlignReactiveScopesToBlockScopeRangeVisitor());
126 - log(
127 - () =>
128 - `AlignReactiveScopesToBlockScopeRangeVisitor:\n${printFunction(fn)}\n\n`
129 - );
130 - visitTree(fn, new MergeOverlappingReactiveScopesVisitor());
131 -}
132 -
133 -/**
134 - * Finds scopes with identical ranges and merges them
135 - */
136 -function mergeScopesWithIdenticalRanges(fn: HIRFunction) {
137 - const scopesByRange: Map<string, ReactiveScope> = new Map();
138 - for (const [_, block] of fn.body.blocks) {
139 - for (const instr of block.instructions) {
140 - const instrScope = getInstructionScope(instr);
141 - if (instrScope === null) {
142 - continue;
143 - }
144 - const rangeKey = `${instrScope.range.start}:${instrScope.range.end}`;
145 - let scope = scopesByRange.get(rangeKey);
146 - if (scope === undefined) {
147 - scope = instrScope;
148 - scopesByRange.set(rangeKey, scope);
149 - }
150 - if (scope.id !== instrScope.id) {
151 - instrScope.id = scope.id;
152 - instrScope.range = scope.range;
153 - }
154 - }
155 - }
156 -}
157 -
158 -class BlockScope {
159 - seen: Set<ScopeId> = new Set();
160 - scopes: Array<ShadowableReactiveScope> = [];
161 -}
162 -
163 -type ShadowableReactiveScope = {
164 - scope: ReactiveScope;
165 - shadowedBy: ReactiveScope | null;
166 -};
167 -
168 -class MergeOverlappingReactiveScopesVisitor
169 - implements Visitor<void, void, void, void, void, void, void>
170 -{
171 - scopes: Array<BlockScope> = [];
172 - seenScopes: Set<ScopeId> = new Set();
173 - joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
174 -
175 - /**
176 - * Determine if this scope is interleaved with any other scopes,
177 - * and if so merge them.
178 - */
179 - visitScope(scope: ReactiveScope) {
180 - const currentBlock = this.scopes[this.scopes.length - 1]!;
181 - // Fast-path for the first time we see a new scope
182 - if (!this.seenScopes.has(scope.id)) {
183 - this.seenScopes.add(scope.id);
184 - currentBlock.seen.add(scope.id);
185 - currentBlock.scopes.push({ shadowedBy: null, scope });
186 - return;
187 - }
188 - // Scope has already been seen, find it in the current block or a parent
189 - let index = this.scopes.length - 1;
190 - let nextBlock = currentBlock;
191 - while (!nextBlock.seen.has(scope.id)) {
192 - // scopes that cross control-flow boundaries are merged with overlapping
193 - // scopes
194 - this.joinedScopes.union([scope, ...nextBlock.scopes.map((s) => s.scope)]);
195 - index--;
196 - if (index < 0) {
197 - // TODO: handle reassignments in multiple branches. these create new identifiers that
198 - // add an entry to this.seenScopes but which are then removed when their blocks exit.
199 - // this is also wrong for codegen, different versions of an identifier could be cached
200 - // differently and so a reassigned version of a variable needs a separate declaration.
201 - // console.log(`scope ${scope.id} not found`);
202 -
203 - // for (let i = this.scopes.length - 1; i > index; i--) {
204 - // const s = this.scopes[i];
205 - // console.log(
206 - // JSON.stringify(
207 - // {
208 - // seen: Array.from(s.seen),
209 - // scopes: s.scopes,
210 - // },
211 - // null,
212 - // 2
213 - // )
214 - // );
215 - // }
216 - currentBlock.seen.add(scope.id);
217 - currentBlock.scopes.push({ shadowedBy: null, scope });
218 - return;
219 - }
220 - nextBlock = this.scopes[index]!;
221 - }
222 -
223 - // Handle interleaving within a given block scope
224 - let found = false;
225 - for (let i = 0; i < nextBlock.scopes.length; i++) {
226 - const current = nextBlock.scopes[i]!;
227 - if (current.scope.id === scope.id) {
228 - found = true;
229 - if (current.shadowedBy !== null) {
230 - this.joinedScopes.union([current.shadowedBy, current.scope]);
231 - }
232 - } else if (found && current.shadowedBy === null) {
233 - // `scope` is shadowing `current`, but we don't know they are interleaved yet
234 - current.shadowedBy = scope;
235 - }
236 - }
237 - if (!currentBlock.seen.has(scope.id)) {
238 - currentBlock.seen.add(scope.id);
239 - currentBlock.scopes.push({ shadowedBy: null, scope });
240 - }
241 - }
242 -
243 - /**
244 - * Prune any scopes that are out of range
245 - */
246 - visitId(id: InstructionId) {
247 - // console.log(`visitId: ${id}`);
248 - const currentBlock = this.scopes[this.scopes.length - 1]!;
249 - retainWhere(currentBlock.scopes, (pending) => {
250 - if (pending.scope.range.end > id) {
251 - return true;
252 - } else {
253 - currentBlock.seen.delete(pending.scope.id);
254 - return false;
255 - }
256 - });
257 - }
258 -
259 - enterBlock(): void {
260 - this.scopes.push(new BlockScope());
261 - }
262 - enterValueBlock(): void {
263 - this.enterBlock();
264 - }
265 - enterInitBlock(block: void): void {
266 - this.enterBlock();
267 - }
268 - leaveInitBlock(block: void): void {
269 - this.leaveBlock();
270 - }
271 - leaveValueBlock(
272 - block: void,
273 - value: { value: void; id: InstructionId } | null
274 - ): void {
275 - this.leaveBlock();
276 - }
277 - visitValue(value: InstructionValue, id: InstructionId): void {
278 - this.visitId(id);
279 - for (const operand of eachInstructionValueOperand(value)) {
280 - if (
281 - operand.identifier.scope !== null &&
282 - id >= operand.identifier.scope.range.start &&
283 - id < operand.identifier.scope.range.end
284 - ) {
285 - this.visitScope(operand.identifier.scope);
286 - }
287 - }
288 - }
289 - visitInstruction(instruction: Instruction, value: void): void {
290 - this.visitId(instruction.id);
291 - if (
292 - instruction.lvalue.place.identifier.scope !== null &&
293 - instruction.id >= instruction.lvalue.place.identifier.scope.range.start &&
294 - instruction.id < instruction.lvalue.place.identifier.scope.range.end
295 - ) {
296 - this.visitScope(instruction.lvalue.place.identifier.scope);
297 - }
298 - }
299 - visitTerminalId(id: InstructionId): void {
300 - this.visitId(id);
301 - }
302 - visitImplicitTerminal(): void | null {}
303 - visitTerminal(terminal: BlockTerminal<void, void, void, void>): void {}
304 - visitCase(test: void | null, block: void): void {}
305 - appendBlock(block: void, item: void, label?: BlockId | undefined): void {}
306 - appendValueBlock(block: void, item: void): void {}
307 - appendInitBlock(block: void, item: void): void {}
308 - leaveBlock(block: void): void {
309 - this.scopes.pop();
310 - if (this.scopes.length === 0) {
311 - this.joinedScopes.forEach((scope, groupScope) => {
312 - if (scope !== groupScope) {
313 - groupScope.range.start = makeInstructionId(
314 - Math.min(groupScope.range.start, scope.range.start)
315 - );
316 - groupScope.range.end = makeInstructionId(
317 - Math.max(groupScope.range.end, scope.range.end)
318 - );
319 - scope.range = groupScope.range;
320 - scope.id = groupScope.id;
321 - }
322 - });
323 - }
324 - }
325 -}
326 -
327 -type PendingReactiveScope = { active: boolean; scope: ReactiveScope };
328 -
329 -/**
330 - * Aligns scopes to block scope boundaries.
331 - *
332 - * TODO @josephsavona this algorithm isn't quite right. we need to ensure that
333 - * reactive scopes can only be closed (end updated) at the same block scope as they
334 - * were opened (start encountered).
335 - */
336 -class AlignReactiveScopesToBlockScopeRangeVisitor
337 - implements Visitor<void, void, void, void, void, void, void>
338 -{
339 - // For each block scope (outer array) stores a list of ReactiveScopes that start
340 - // in that block scope.
341 - blockScopes: Array<{
342 - kind: "block" | "value";
343 - scopes: Array<PendingReactiveScope>;
344 - }> = [];
345 -
346 - // ReactiveScopes whose declaring block scope has ended but may still need to
347 - // be "closed" (ie have their range.end be updated). A given scope can be in
348 - // blockScopes OR this array but not both.
349 - unclosedScopes: Array<PendingReactiveScope> = [];
350 -
351 - // Set of all scope ids that have been seen so far, regardless of which of
352 - // the above data structures they're in, to avoid tracking the same scope twice.
353 - seenScopes: Set<ScopeId> = new Set();
354 -
355 - visitId(id: InstructionId) {
356 - const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
357 - if (currentScopes.kind === "value") {
358 - return;
359 - }
360 - const scopes = [...currentScopes.scopes, ...this.unclosedScopes];
361 - for (const pending of scopes) {
362 - if (!pending.active) {
363 - continue;
364 - }
365 - if (id >= pending.scope.range.end) {
366 - pending.active = false;
367 - pending.scope.range.end = id;
368 - }
369 - }
370 - }
371 -
372 - enterBlock(): void {
373 - this.blockScopes.push({ kind: "block", scopes: [] });
374 - }
375 -
376 - appendBlock(block: void, item: void, label?: BlockId | undefined): void {}
377 -
378 - leaveBlock(block: void): void {
379 - const lastScope = this.blockScopes.pop();
380 - invariant(
381 - lastScope !== undefined && lastScope.kind === "block",
382 - "Expected enterBlock/leaveBlock to be called 1:1"
383 - );
384 - for (const scope of lastScope.scopes) {
385 - if (scope.active) {
386 - this.unclosedScopes.push(scope);
387 - }
388 - }
389 - }
390 -
391 - enterValueBlock(): void {
392 - this.blockScopes.push({ kind: "value", scopes: [] });
393 - }
394 - appendValueBlock(block: void, item: void): void {}
395 - leaveValueBlock(
396 - block: void,
397 - value: { value: void; id: InstructionId } | null
398 - ): void {
399 - const lastScope = this.blockScopes.pop();
400 - invariant(
401 - lastScope !== undefined && lastScope.kind === "value",
402 - "Expected enterValueBlock/leaveValueBlock to be called 1:1"
403 - );
404 - for (const scope of lastScope.scopes) {
405 - invariant(
406 - scope.active,
407 - "Value scopes cannot be closed separately from the parent block"
408 - );
409 - this.unclosedScopes.push(scope);
410 - }
411 - }
412 -
413 - enterInitBlock(block: void): void {
414 - this.enterValueBlock();
415 - }
416 - appendInitBlock(block: void, item: void): void {}
417 - leaveInitBlock(block: void): void {
418 - this.leaveValueBlock(block, null);
419 - }
420 -
421 - visitInstruction(instruction: Instruction, value: void): void {
422 - this.visitId(instruction.id);
423 - const scope = getInstructionScope(instruction);
424 - if (scope !== null) {
425 - if (!this.seenScopes.has(scope.id)) {
426 - const currentScopes = this.blockScopes[this.blockScopes.length - 1]!;
427 - this.seenScopes.add(scope.id);
428 - currentScopes.scopes.push({
429 - active: true,
430 - scope,
431 - });
432 - }
433 - }
434 - }
435 -
436 - visitTerminalId(id: InstructionId): void {
437 - this.visitId(id);
438 - }
439 -
440 - visitTerminal(terminal: BlockTerminal<void, void, void, void>): void {}
441 -
442 - visitImplicitTerminal(): void | null {}
443 -
444 - // no-ops
445 - visitValue(value: InstructionValue): void {}
446 - visitCase(test: void | null, block: void): void {}
447 -}
448 -
449 -function getInstructionScope(instr: Instruction): ReactiveScope | null {
450 - if (
451 - instr.lvalue.place.identifier.scope !== null &&
452 - isActive(instr, instr.lvalue.place.identifier.scope.range)
453 - ) {
454 - return instr.lvalue.place.identifier.scope;
455 - } else {
456 - for (const operand of eachInstructionOperand(instr)) {
457 - if (
458 - operand.identifier.scope !== null &&
459 - isActive(instr, operand.identifier.scope.range)
460 - ) {
461 - return operand.identifier.scope;
462 - }
463 - }
464 - }
465 - return null;
466 -}
467 -
468 -function isActive(instr: Instruction, range: MutableRange): boolean {
469 - return instr.id >= range.start && instr.id < range.end;
470 -}
compiler/forget/src/ReactiveScopes/index.ts
-2
@@ -8,10 +8,8 @@
8 export { alignReactiveScopesToBlockScopes } from "./AlignReactiveScopesToBlockScopes";
9 export { buildReactiveBlocks } from "./BuildReactiveBlocks";
10 export { buildReactiveFunction } from "./BuildReactiveFunction";
11 -export { buildReactiveFunction as buildReactiveFunctionWithoutScopes } from "./BuildReactiveFunctionWithoutScopes";
11 export { codegenReactiveFunction } from "./CodegenReactiveFunction";
12 export { flattenReactiveLoops } from "./FlattenReactiveLoops";
14 -export { inferReactiveScopes } from "./InferReactiveScopes";
13 export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
14 export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
15 export { printReactiveFunction } from "./PrintReactiveFunction";
compiler/forget/src/__tests__/hir-test.ts
-4
@@ -13,7 +13,6 @@ 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";
16 import { compile } from "../CompilerPipeline";
17 import { toggleLogging } from "../Utils/logger";
18 import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
@@ -31,9 +30,6 @@ wasmFolder(
30 const Pragma_RE = /\/\/\s*@enable\((\w+)\)$/gm;
31
32 describe("React Forget (HIR version)", () => {
34 - flags.enableNewReactiveFunctionBuilder =
35 - String(process.env["ENABLE_NEW_BUILDER"]) === "1";
36 -
33 generateTestsFromFixtures(
34 path.join(__dirname, "fixtures", "hir"),
35 (input, file, options) => {
compiler/forget/src/index.ts
+1 -3
@@ -20,10 +20,9 @@ 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/BuildReactiveFunctionWithoutScopes";
23 +import { buildReactiveFunction } from "./ReactiveScopes/BuildReactiveFunction";
24 import { codegenReactiveFunction } from "./ReactiveScopes/CodegenReactiveFunction";
25 import { flattenReactiveLoops } from "./ReactiveScopes/FlattenReactiveLoops";
26 -import { inferReactiveScopes } from "./ReactiveScopes/InferReactiveScopes";
26 import { inferReactiveScopeVariables } from "./ReactiveScopes/InferReactiveScopeVariables";
27 import { printReactiveFunction } from "./ReactiveScopes/PrintReactiveFunction";
28 import { propagateScopeDependencies } from "./ReactiveScopes/PropagateScopeDependencies";
@@ -65,7 +64,6 @@ export const HIR = {
64 Environment,
65 flattenReactiveLoops,
66 inferMutableRanges,
68 - inferReactiveScopes,
67 inferReactiveScopeVariables,
68 inferReferenceEffects,
69 inferTypes,