Fork HIRTreeVisitor to specialize it for ReactiveFunction conversion
Joe Savona committed
Jan 12, 2023 at 09:11 UTC
9e549e9417698a8a12fb04917461e7a20b0b3000
2 files changed
+963
-1
compiler/forget/src/HIR/ReactiveFunctionVisitor.ts
new
+958
@@ -0,0 +1,958 @@
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/BuildReactiveFunctionWithoutScopes.ts
+5
-1
@@ -19,7 +19,11 @@ import {
19
ReactiveTerminal,
20
ReactiveValueBlock,
21
} from "../HIR/HIR";
22
-import { BlockTerminal, Visitor, visitTree } from "../HIR/HIRTreeVisitor";
22
+import {
23
+ BlockTerminal,
24
+ Visitor,
25
+ visitTreeForReactiveFunction as visitTree,
26
+} from "../HIR/ReactiveFunctionVisitor";
27
import { assertExhaustive } from "../Utils/utils";
28
29
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {