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,