Add HIR "tree" visitor and use it for codegen, alternate printer
Refactors Codegen to extract the core "visit IR as a tree" logic separately from the code to emit JS: * `HIRTreeVisitor` is a new helper that visits the HIR as a tree. You call `visitTree(ir, yourVisitor)` and it drives visiting of the IR, tracking blocks and scopes and calling methods as appropriate. * `Codegen` is now implemented as a Visitor implementation. For example `enterBlock()` creates an empty `Array<t.Statement>`, `leaveBlock()` wraps that in a `t.BlockStatement`, etc. * `printHIRTree()` is a new IR printer (implemented as a visitor) that prints the HIR in tree form, so it retains the original shape of the code but with each block replaced with its IR equivalent.
Joe Savona committed
Nov 18, 2022 at 17:15 UTC
5f2833bd5ca70ea1fd6f72dc4ed6aaa298024c8a
7 files changed
+1001
-559
compiler/forget/src/HIR/Codegen.ts
+150
-546
@@ -9,18 +9,16 @@ import * as t from "@babel/types";
9
import { assertExhaustive } from "../Common/utils";
10
import { invariant } from "../CompilerError";
11
import {
12
- BasicBlock,
13
- BlockId,
14
- GotoVariant,
15
- HIR,
12
HIRFunction,
13
Identifier,
14
IdentifierId,
15
Instruction,
16
InstructionKind,
17
+ InstructionValue,
18
LValue,
19
Place,
20
} from "./HIR";
21
+import { BlockTerminal, Visitor, visitTree } from "./HIRTreeVisitor";
22
import { todoInvariant } from "./todo";
23
24
/**
@@ -44,9 +42,9 @@ import { todoInvariant } from "./todo";
42
* performed as an HIR optimization pass, that is left todo for the time being.
43
*/
44
export default function codegen(fn: HIRFunction): t.Function {
47
- const entry = fn.body.blocks.get(fn.body.entry)!;
48
- const cx = new Context(fn.body);
49
- const body = codegenBlock(cx, entry);
45
+ const visitor = new CodegenVisitor();
46
+ const body = visitTree(fn, visitor);
47
+ invariant(t.isBlockStatement(body), "Expected a block statement");
48
const params = fn.params.map((param) => convertIdentifier(param.identifier));
49
return t.functionDeclaration(
50
fn.id !== null ? convertIdentifier(fn.id) : null,
@@ -57,544 +55,188 @@ export default function codegen(fn: HIRFunction): t.Function {
55
);
56
}
57
60
-class Context {
61
- ir: HIR;
58
+type Temporaries = Map<IdentifierId, t.Expression>;
59
+
60
+class CodegenVisitor
61
+ implements
62
+ Visitor<
63
+ Array<t.Statement>,
64
+ t.Expression | t.Statement | t.JSXFragment,
65
+ t.Statement,
66
+ t.SwitchCase
67
+ >
68
+{
69
temp: Map<IdentifierId, t.Expression> = new Map();
63
- #nextScheduleId: number = 0;
70
65
- /**
66
- * Used to track which blocks *have been* generated already in order to
67
- * abort if a block is generated a second time. This is an error catching
68
- * mechanism for debugging purposes, and is not used by the codegen algorithm
69
- * to drive decisions about how to emit blocks.
70
- */
71
- emitted: Set<BlockId> = new Set();
72
-
73
- /**
74
- * A set of blocks that are already scheduled to be emitted by eg a parent.
75
- * This allows child nodes to avoid re-emitting the same block and emit eg
76
- * a break instead.
77
- */
78
- #scheduled: Set<BlockId> = new Set();
79
-
80
- /**
81
- * Represents which control flow operations are currently in scope, with the innermost
82
- * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
83
- * control will implicitly transfer, such that gotos to that block can be elided. Gotos
84
- * targeting items higher up the stack may need labeled break or continue; see
85
- * getBreakTarget() and getContinueTarget() for more details.
86
- */
87
- #controlFlowStack: Array<ControlFlowTarget> = [];
88
-
89
- constructor(ir: HIR) {
90
- this.ir = ir;
91
- }
92
-
93
- /**
94
- * Record that the given block will be emitted (eg by the codegen of a parent node)
95
- * so that child nodes can avoid re-emitting it.
96
- */
97
- schedule(block: BlockId, type: "if" | "switch" | "case"): number {
98
- const id = this.#nextScheduleId++;
99
- invariant(
100
- !this.#scheduled.has(block),
101
- `Break block is already scheduled: bb${block}`
102
- );
103
- this.#scheduled.add(block);
104
- this.#controlFlowStack.push({ block, id, type });
105
- return id;
71
+ enterBlock(): t.Statement[] {
72
+ return [];
73
}
107
-
108
- scheduleLoop(
109
- fallthroughBlock: BlockId,
110
- continueBlock: BlockId,
111
- loopBlock: BlockId | null
112
- ): number {
113
- const id = this.#nextScheduleId++;
114
- const ownsBlock = !this.#scheduled.has(fallthroughBlock);
115
- this.#scheduled.add(fallthroughBlock);
116
- invariant(
117
- !this.#scheduled.has(continueBlock),
118
- `Continue block is already scheduled: bb${continueBlock}`
119
- );
120
- this.#scheduled.add(continueBlock);
121
- let ownsLoop = false;
122
- if (loopBlock !== null) {
123
- ownsLoop = !this.#scheduled.has(loopBlock);
124
- this.#scheduled.add(loopBlock);
125
- }
126
-
127
- this.#controlFlowStack.push({
128
- block: fallthroughBlock,
129
- ownsBlock,
130
- id,
131
- type: "loop",
132
- continueBlock,
133
- loopBlock,
134
- ownsLoop,
135
- });
136
- return id;
74
+ visitValue(
75
+ value: InstructionValue
76
+ ): t.Expression | t.Statement | t.JSXFragment {
77
+ return codegenInstructionValue(this.temp, value);
78
}
138
-
139
- /**
140
- * Removes a block that was scheduled; must be called after that block is emitted.
141
- */
142
- unschedule(scheduleId: number): void {
143
- const last = this.#controlFlowStack.pop();
144
- invariant(
145
- last !== undefined && last.id === scheduleId,
146
- "Can only unschedule the last target"
147
- );
148
- if (last.type !== "loop" || last.ownsBlock !== null) {
149
- this.#scheduled.delete(last.block);
79
+ visitInstruction(
80
+ instr: Instruction,
81
+ value: t.Expression | t.Statement | t.JSXFragment
82
+ ): t.Statement {
83
+ if (t.isStatement(value)) {
84
+ return value;
85
}
151
- if (last.type === "loop") {
152
- this.#scheduled.delete(last.continueBlock);
153
- if (last.ownsLoop && last.loopBlock !== null) {
154
- this.#scheduled.delete(last.loopBlock);
155
- }
86
+ if (instr.lvalue === null) {
87
+ return t.expressionStatement(value);
88
}
157
- }
158
-
159
- /**
160
- * Helper to unschedule multiple scheduled blocks. The ids should be in
161
- * the order in which they were scheduled, ie most recently scheduled last.
162
- */
163
- unscheduleAll(scheduleIds: Array<number>): void {
164
- for (let i = scheduleIds.length - 1; i >= 0; i--) {
165
- this.unschedule(scheduleIds[i]!);
166
- }
167
- }
168
-
169
- /**
170
- * Check if the given @param block is scheduled or not.
171
- */
172
- isScheduled(block: BlockId): boolean {
173
- return this.#scheduled.has(block);
174
- }
175
-
176
- /**
177
- * Given the current control flow stack, determines how a `break` to the given @param block
178
- * must be emitted. Returns as follows:
179
- * - 'implicit' if control would implicitly transfer to that block
180
- * - 'labeled' if a labeled break is required to transfer control to that block
181
- * - 'unlabeled' if an unlabeled break would transfer to that block
182
- * - null if there is no information for this block
183
- *
184
- * The returned 'block' value should be used as the label if necessary.
185
- */
186
- getBreakTarget(
187
- block: BlockId
188
- ): { block: BlockId; type: ControlFlowKind } | null {
189
- let hasPrecedingLoop = false;
190
- for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
191
- const target = this.#controlFlowStack[i]!;
192
- if (target.block === block) {
193
- let type: ControlFlowKind;
194
- if (target.type === "loop") {
195
- // breaking out of a loop requires an explicit break,
196
- // but only requires a label if breaking past the innermost loop.
197
- type = hasPrecedingLoop ? "labeled" : "unlabeled";
198
- } else if (i === this.#controlFlowStack.length - 1) {
199
- // breaking to the last break point, which is where control will transfer
200
- // implicitly
201
- type = "implicit";
202
- } else {
203
- // breaking somewhere else requires an explicit break
204
- type = "labeled";
89
+ if (
90
+ instr.lvalue.place.memberPath === null &&
91
+ instr.lvalue.place.identifier.name === null
92
+ ) {
93
+ // temporary
94
+ this.temp.set(instr.lvalue.place.identifier.id, value);
95
+ return t.emptyStatement();
96
+ } else {
97
+ switch (instr.lvalue.kind) {
98
+ case InstructionKind.Const: {
99
+ return t.variableDeclaration("const", [
100
+ t.variableDeclarator(codegenLVal(instr.lvalue), value),
101
+ ]);
102
}
206
- return {
207
- block: target.block,
208
- type,
209
- };
210
- }
211
- hasPrecedingLoop ||= target.type === "loop";
212
- }
213
- return null;
214
- }
215
-
216
- /**
217
- * Given the current control flow stack, determines how a `continue` to the given @param block
218
- * must be emitted. Returns as follows:
219
- * - 'implicit' if control would implicitly continue to that block
220
- * - 'labeled' if a labeled continue is required to continue to that block
221
- * - 'unlabeled' if an unlabeled continue would transfer to that block
222
- * - null if there is no information for this block
223
- *
224
- * The returned 'block' value should be used as the label if necessary.
225
- */
226
- getContinueTarget(
227
- block: BlockId
228
- ): { block: BlockId; type: ControlFlowKind } | null {
229
- let hasPrecedingLoop = false;
230
- for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
231
- const target = this.#controlFlowStack[i]!;
232
- if (target.type == "loop" && target.continueBlock === block) {
233
- let type: ControlFlowKind;
234
- if (hasPrecedingLoop) {
235
- // continuing to a loop that is not the innermost loop always requires
236
- // a label
237
- type = "labeled";
238
- } else if (i === this.#controlFlowStack.length - 1) {
239
- // continuing to the last break point, which is where control will
240
- // transfer to naturally
241
- type = "implicit";
242
- } else {
243
- // the continue is inside some conditional logic, requires an explicit
244
- // continue
245
- type = "unlabeled";
103
+ case InstructionKind.Let: {
104
+ return t.variableDeclaration("let", [
105
+ t.variableDeclarator(codegenLVal(instr.lvalue), value),
106
+ ]);
107
+ }
108
+ case InstructionKind.Reassign: {
109
+ return t.expressionStatement(
110
+ t.assignmentExpression("=", codegenLVal(instr.lvalue), value)
111
+ );
112
+ }
113
+ default: {
114
+ assertExhaustive(
115
+ instr.lvalue.kind,
116
+ `Unexpected instruction kind '${instr.lvalue.kind}'`
117
+ );
118
}
247
- return {
248
- block: target.block,
249
- type,
250
- };
119
}
252
- hasPrecedingLoop ||= target.type === "loop";
120
}
254
- return null;
255
- }
256
-
257
- debugBreakTargets(): Array<ControlFlowTarget> {
258
- return this.#controlFlowStack.map((target) => ({ ...target }));
121
}
260
-}
261
-
262
-type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
263
-
264
-type ControlFlowTarget =
265
- | { type: "if"; block: BlockId; id: number }
266
- | { type: "switch"; block: BlockId; id: number }
267
- | { type: "case"; block: BlockId; id: number }
268
- | {
269
- type: "loop";
270
- block: BlockId;
271
- ownsBlock: boolean;
272
- continueBlock: BlockId;
273
- loopBlock: BlockId | null;
274
- ownsLoop: boolean;
275
- id: number;
276
- };
277
-
278
-function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement {
279
- const body: Array<t.Statement> = [];
280
- writeBlock(cx, block, body);
281
- return t.blockStatement(body);
282
-}
283
-
284
-function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
285
- invariant(
286
- !cx.emitted.has(block.id),
287
- `Cannot emit the same block twice: bb${block.id}`
288
- );
289
- cx.emitted.add(block.id);
290
- for (const instr of block.instructions) {
291
- writeInstr(cx, instr, body);
122
+ visitImplicitTerminal(): t.Statement | null {
123
+ return null;
124
}
293
- const terminal = block.terminal;
294
- const scheduleIds = [];
295
- switch (terminal.kind) {
296
- case "return": {
297
- const value =
298
- terminal.value != null ? codegenPlace(cx, terminal.value) : null;
299
- body.push(t.returnStatement(value));
300
- break;
301
- }
302
- case "throw": {
303
- const value = codegenPlace(cx, terminal.value);
304
- body.push(t.throwStatement(value));
305
- break;
306
- }
307
- case "if": {
308
- const test = codegenPlace(cx, terminal.test);
309
- const fallthroughId =
310
- terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
311
- ? terminal.fallthrough
312
- : null;
313
- const alternateId =
314
- terminal.alternate !== terminal.fallthrough ? terminal.alternate : null;
315
-
316
- if (fallthroughId !== null) {
317
- const scheduleId = cx.schedule(fallthroughId, "if");
318
- scheduleIds.push(scheduleId);
319
- }
320
-
321
- let consequent: t.Statement | null = null;
322
- if (cx.isScheduled(terminal.consequent)) {
323
- consequent = codegenBreak(cx, terminal.consequent);
324
- } else {
325
- consequent = codegenBlock(cx, cx.ir.blocks.get(terminal.consequent)!);
326
- }
327
-
328
- let alternate: t.Statement | null = null;
329
- if (alternateId !== null) {
330
- if (cx.isScheduled(alternateId)) {
331
- alternate = codegenBreak(cx, alternateId);
125
+ visitTerminal(
126
+ terminal: BlockTerminal<
127
+ t.Statement[],
128
+ t.Expression,
129
+ t.Statement,
130
+ t.SwitchCase
131
+ >
132
+ ): t.Statement {
133
+ switch (terminal.kind) {
134
+ case "break": {
135
+ if (terminal.label) {
136
+ return t.breakStatement(t.identifier(terminal.label));
137
} else {
333
- alternate = codegenBlock(cx, cx.ir.blocks.get(alternateId)!);
138
+ return t.breakStatement();
139
}
140
}
336
-
337
- cx.unscheduleAll(scheduleIds);
338
- if (fallthroughId !== null) {
339
- if (consequent === null && alternate === null) {
340
- body.push(t.expressionStatement(test));
341
- } else {
342
- body.push(
343
- t.labeledStatement(
344
- t.identifier(`bb${fallthroughId}`),
345
- t.ifStatement(test, consequent ?? t.blockStatement([]), alternate)
346
- )
347
- );
348
- }
349
- writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
350
- } else {
351
- if (consequent === null && alternate === null) {
352
- body.push(t.expressionStatement(test));
141
+ case "continue": {
142
+ if (terminal.label) {
143
+ return t.continueStatement(t.identifier(terminal.label));
144
} else {
354
- body.push(
355
- t.ifStatement(test, consequent ?? t.blockStatement([]), alternate)
356
- );
145
+ return t.continueStatement();
146
}
147
}
359
- break;
360
- }
361
- case "switch": {
362
- const test = codegenPlace(cx, terminal.test);
363
- const fallthroughId =
364
- terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
365
- ? terminal.fallthrough
366
- : null;
367
- if (fallthroughId !== null) {
368
- const scheduleId = cx.schedule(fallthroughId, "switch");
369
- scheduleIds.push(scheduleId);
148
+ case "if": {
149
+ return t.ifStatement(
150
+ terminal.test,
151
+ terminal.consequent,
152
+ terminal.alternate
153
+ );
154
}
371
-
372
- const cases: Array<t.SwitchCase> = [];
373
- [...terminal.cases].reverse().forEach((case_, index) => {
374
- const test = case_.test !== null ? codegenPlace(cx, case_.test) : null;
375
-
376
- let consequent;
377
- if (cx.isScheduled(case_.block)) {
378
- // cases which are empty or contain only a `break` may point to blocks
379
- // that are already scheduled. emit as follows:
380
- // - if the block is for another case branch, don't emit a break and fall-through
381
- // - else, emit an explicit break.
382
- const break_ = codegenBreak(cx, case_.block);
383
- if (
384
- index === 0 &&
385
- break_ === null &&
386
- case_.block === terminal.fallthrough &&
387
- case_.test === null
388
- ) {
389
- // If the last case statement (first in reverse order) is a default that
390
- // jumps to the fallthrough, then we would emit a useless `default: {}`,
391
- // so instead skip this case.
392
- return;
393
- }
394
- const block = [];
395
- if (break_ !== null) {
396
- block.push(break_);
397
- }
398
- consequent = t.blockStatement(block);
155
+ case "switch": {
156
+ return t.switchStatement(terminal.test, terminal.cases);
157
+ }
158
+ case "while": {
159
+ return t.whileStatement(terminal.test, terminal.loop);
160
+ }
161
+ case "return": {
162
+ if (terminal.value !== null) {
163
+ return t.returnStatement(terminal.value);
164
} else {
400
- consequent = codegenBlock(cx, cx.ir.blocks.get(case_.block)!);
401
- const scheduleId = cx.schedule(case_.block, "case");
402
- scheduleIds.push(scheduleId);
165
+ return t.returnStatement();
166
}
404
- cases.push(t.switchCase(test, [consequent]));
405
- });
406
- cases.reverse();
407
-
408
- cx.unscheduleAll(scheduleIds);
409
- if (fallthroughId !== null) {
410
- body.push(
411
- t.labeledStatement(
412
- t.identifier(`bb${fallthroughId}`),
413
- t.switchStatement(test, cases)
414
- )
415
- );
416
- writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
417
- } else {
418
- body.push(t.switchStatement(test, cases));
167
}
420
- break;
421
- }
422
- case "while": {
423
- const testBlock = cx.ir.blocks.get(terminal.test)!;
424
- const testTerminal = testBlock.terminal;
425
- invariant(
426
- testTerminal.kind === "if",
427
- "Expected while loop test block to end in an if"
428
- );
429
- const bodyLength = body.length;
430
- for (const instr of testBlock.instructions) {
431
- writeInstr(cx, instr, body);
168
+ case "throw": {
169
+ return t.throwStatement(terminal.value);
170
}
433
- invariant(
434
- body.length === bodyLength,
435
- "Expected test to produce only temporaries"
436
- );
437
- const testValue =
438
- cx.temp.get(testTerminal.test.identifier.id) ??
439
- codegenPlace(cx, testTerminal.test);
440
- invariant(
441
- testValue != null,
442
- "Expected test to produce a temporary value"
443
- );
444
-
445
- const fallthroughId =
446
- terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
447
- ? terminal.fallthrough
448
- : null;
449
- const loopId =
450
- !cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough
451
- ? terminal.loop
452
- : null;
453
- const scheduleId = cx.scheduleLoop(
454
- terminal.fallthrough,
455
- terminal.test,
456
- terminal.loop
457
- );
458
- scheduleIds.push(scheduleId);
459
-
460
- let loopBody: t.Statement;
461
- if (loopId) {
462
- loopBody = codegenBlock(cx, cx.ir.blocks.get(loopId)!);
463
- } else {
464
- const break_ = codegenBreak(cx, terminal.loop);
465
- invariant(
466
- break_ !== null,
467
- "If loop body is already scheduled it must be a break"
171
+ default: {
172
+ assertExhaustive(
173
+ terminal,
174
+ `Unexpected terminal kind '${(terminal as any).kind}'`
175
);
469
- loopBody = t.blockStatement([break_]);
470
- }
471
-
472
- cx.unscheduleAll(scheduleIds);
473
- if (fallthroughId !== null) {
474
- body.push(
475
- t.labeledStatement(
476
- t.identifier(`bb${fallthroughId}`),
477
- t.whileStatement(testValue, loopBody)
478
- )
479
- );
480
- writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
481
- } else {
482
- body.push(t.whileStatement(testValue, loopBody));
176
}
484
- break;
485
- }
486
- case "goto": {
487
- switch (terminal.variant) {
488
- case GotoVariant.Break: {
489
- const break_ = codegenBreak(cx, terminal.block);
490
- if (break_ !== null) {
491
- body.push(break_);
492
- }
493
- break;
494
- }
495
- case GotoVariant.Continue: {
496
- const continue_ = codegenContinue(cx, terminal.block);
497
- if (continue_ !== null) {
498
- body.push(continue_);
499
- }
500
- break;
501
- }
502
- default: {
503
- assertExhaustive(
504
- terminal.variant,
505
- `Unexpected goto variant '${terminal.variant}'`
506
- );
507
- }
508
- }
509
- break;
510
- }
511
- default: {
512
- assertExhaustive(terminal, "Unexpected terminal");
177
}
178
}
515
-}
516
-
517
-function codegenBreak(cx: Context, block: BlockId): t.Statement | null {
518
- const target = cx.getBreakTarget(block);
519
- if (target === null) {
520
- // TODO: we should always have a target
521
- return null;
179
+ visitCase(test: t.Expression | null, block: t.Statement): t.SwitchCase {
180
+ return t.switchCase(test, [block]);
181
}
523
- switch (target.type) {
524
- case "implicit": {
525
- return null;
526
- }
527
- case "unlabeled": {
528
- return t.breakStatement();
529
- }
530
- case "labeled": {
531
- return t.breakStatement(t.identifier(`bb${target.block}`));
182
+ appendBlock(
183
+ block: t.Statement[],
184
+ item: t.Statement,
185
+ label?: string | undefined
186
+ ): void {
187
+ if (item.type === "EmptyStatement") {
188
+ return;
189
+ }
190
+ if (label !== undefined) {
191
+ block.push(t.labeledStatement(t.identifier(label), item));
192
+ } else {
193
+ block.push(item);
194
}
195
}
534
-}
535
-
536
-function codegenContinue(cx: Context, block: BlockId): t.Statement | null {
537
- const target = cx.getContinueTarget(block);
538
- invariant(
539
- target !== null,
540
- `Expected continue target to be scheduled for bb${block}`
541
- );
542
- switch (target.type) {
543
- case "labeled": {
544
- return t.continueStatement(t.identifier(`bb${target.block}`));
545
- }
546
- case "unlabeled": {
547
- return t.continueStatement();
548
- }
549
- case "implicit": {
550
- return null;
551
- }
552
- default: {
553
- assertExhaustive(
554
- target.type,
555
- `Unexpected continue target kind '${(target as any).type}'`
556
- );
557
- }
196
+ leaveBlock(block: t.Statement[]): t.Statement {
197
+ return t.blockStatement(block);
198
}
199
}
200
561
-function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {
201
+function codegenInstructionValue(
202
+ temp: Temporaries,
203
+ instrValue: InstructionValue
204
+): t.Expression | t.JSXFragment | t.Statement {
205
let value: t.Expression;
563
- const instrValue = instr.value;
206
switch (instrValue.kind) {
207
case "ArrayExpression": {
208
const elements = instrValue.elements.map((element) =>
567
- codegenPlace(cx, element)
209
+ codegenPlace(temp, element)
210
);
211
value = t.arrayExpression(elements);
212
break;
213
}
214
case "BinaryExpression": {
573
- const left = codegenPlace(cx, instrValue.left);
574
- const right = codegenPlace(cx, instrValue.right);
215
+ const left = codegenPlace(temp, instrValue.left);
216
+ const right = codegenPlace(temp, instrValue.right);
217
value = t.binaryExpression(instrValue.operator, left, right);
218
break;
219
}
220
case "UnaryExpression": {
221
value = t.unaryExpression(
222
instrValue.operator as "throw", // todo
581
- codegenPlace(cx, instrValue.value)
223
+ codegenPlace(temp, instrValue.value)
224
);
225
break;
226
}
227
case "Primitive": {
586
- value = codegenValue(cx, instrValue.value);
228
+ value = codegenValue(temp, instrValue.value);
229
break;
230
}
231
case "CallExpression": {
590
- const callee = codegenPlace(cx, instrValue.callee);
591
- const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
232
+ const callee = codegenPlace(temp, instrValue.callee);
233
+ const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
234
value = t.callExpression(callee, args);
235
break;
236
}
237
case "NewExpression": {
596
- const callee = codegenPlace(cx, instrValue.callee);
597
- const args = instrValue.args.map((arg) => codegenPlace(cx, arg));
238
+ const callee = codegenPlace(temp, instrValue.callee);
239
+ const args = instrValue.args.map((arg) => codegenPlace(temp, arg));
240
value = t.newExpression(callee, args);
241
break;
242
}
@@ -603,7 +245,10 @@ function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {
245
if (instrValue.properties !== null) {
246
for (const [property, value] of instrValue.properties) {
247
properties.push(
606
- t.objectProperty(t.stringLiteral(property), codegenPlace(cx, value))
248
+ t.objectProperty(
249
+ t.stringLiteral(property),
250
+ codegenPlace(temp, value)
251
+ )
252
);
253
}
254
}
@@ -620,11 +265,11 @@ function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {
265
attributes.push(
266
t.jsxAttribute(
267
t.jsxIdentifier(prop),
623
- t.jsxExpressionContainer(codegenPlace(cx, value))
268
+ t.jsxExpressionContainer(codegenPlace(temp, value))
269
)
270
);
271
}
627
- let tagValue = codegenPlace(cx, instrValue.tag);
272
+ let tagValue = codegenPlace(temp, instrValue.tag);
273
let tag: string;
274
if (tagValue.type === "Identifier") {
275
tag = tagValue.name;
@@ -637,7 +282,7 @@ function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {
282
}
283
const children =
284
instrValue.children !== null
640
- ? instrValue.children.map((child) => codegenJsxElement(cx, child))
285
+ ? instrValue.children.map((child) => codegenJsxElement(temp, child))
286
: [];
287
value = t.jsxElement(
288
t.jsxOpeningElement(
@@ -657,75 +302,34 @@ function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {
302
value = t.jsxFragment(
303
t.jsxOpeningFragment(),
304
t.jsxClosingFragment(),
660
- instrValue.children.map((child) => codegenJsxElement(cx, child))
305
+ instrValue.children.map((child) => codegenJsxElement(temp, child))
306
);
307
break;
308
}
309
case "OtherStatement": {
310
const node = instrValue.node;
666
- if (t.isStatement(node)) {
667
- body.push(node);
668
- return;
311
+ if (!t.isExpression(node)) {
312
+ return node as any; // TODO handle statements, jsx fragments
313
}
670
- value = node as any; // TODO(josephsavona) complete handling of JSX fragment/spreadchild elements
314
+ value = node;
315
break;
316
}
317
case "Identifier": {
674
- value = codegenPlace(cx, instrValue);
318
+ value = codegenPlace(temp, instrValue);
319
break;
320
}
321
default: {
678
- assertExhaustive(instrValue, "Unexpected instruction kind");
679
- }
680
- }
681
- if (instr.lvalue !== null) {
682
- if (
683
- instr.lvalue.place.identifier.name === null &&
684
- instr.lvalue.place.memberPath === null
685
- ) {
686
- // Temporary value: don't immediately emit, instead save the value to refer to later
687
- cx.temp.set(instr.lvalue.place.identifier.id, value);
688
- } else {
689
- switch (instr.lvalue.kind) {
690
- case InstructionKind.Const: {
691
- body.push(
692
- t.variableDeclaration("const", [
693
- t.variableDeclarator(codegenLVal(instr.lvalue), value),
694
- ])
695
- );
696
- break;
697
- }
698
- case InstructionKind.Let: {
699
- body.push(
700
- t.variableDeclaration("let", [
701
- t.variableDeclarator(codegenLVal(instr.lvalue), value),
702
- ])
703
- );
704
- break;
705
- }
706
- case InstructionKind.Reassign: {
707
- body.push(
708
- t.expressionStatement(
709
- t.assignmentExpression("=", codegenLVal(instr.lvalue), value)
710
- )
711
- );
712
- break;
713
- }
714
- default: {
715
- assertExhaustive(
716
- instr.lvalue.kind,
717
- `Unexpected instruction kind '${instr.lvalue.kind}'`
718
- );
719
- }
720
- }
322
+ assertExhaustive(
323
+ instrValue,
324
+ `Unexpected instruction value kind '${(instrValue as any).kind}'`
325
+ );
326
}
722
- } else {
723
- body.push(t.expressionStatement(value));
327
}
328
+ return value;
329
}
330
331
function codegenJsxElement(
728
- cx: Context,
332
+ temp: Temporaries,
333
place: Place
334
):
335
| t.JSXText
@@ -733,7 +337,7 @@ function codegenJsxElement(
337
| t.JSXSpreadChild
338
| t.JSXElement
339
| t.JSXFragment {
736
- const value = codegenPlace(cx, place);
340
+ const value = codegenPlace(temp, place);
341
switch (value.type) {
342
case "StringLiteral": {
343
return t.jsxText(value.value);
@@ -757,7 +361,7 @@ function codegenLVal(lval: LValue): t.LVal {
361
}
362
363
function codegenValue(
760
- cx: Context,
364
+ temp: Temporaries,
365
value: boolean | number | string | null | undefined
366
): t.Expression {
367
if (typeof value === "number") {
@@ -775,10 +379,10 @@ function codegenValue(
379
}
380
}
381
778
-function codegenPlace(cx: Context, place: Place): t.Expression {
382
+function codegenPlace(temp: Temporaries, place: Place): t.Expression {
383
todoInvariant(place.kind === "Identifier", "support scope values");
384
if (place.memberPath === null) {
781
- let tmp = cx.temp.get(place.identifier.id);
385
+ let tmp = temp.get(place.identifier.id);
386
if (tmp != null) {
387
return tmp;
388
}
compiler/forget/src/HIR/HIRTreeVisitor.ts
new
+684
@@ -0,0 +1,684 @@
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 { assertExhaustive } from "../Common/utils";
10
+import {
11
+ BasicBlock,
12
+ BlockId,
13
+ GotoVariant,
14
+ HIR,
15
+ HIRFunction,
16
+ Instruction,
17
+ InstructionValue,
18
+ Place,
19
+} from "./HIR";
20
+
21
+/**
22
+ * Function to visit HIR as a tree of high-level constructs rather than as a sequence
23
+ * of lower-level basic blocks. Intended for use in codegen and reactive scope
24
+ * construction which need to see the original "shape" of the code.
25
+ *
26
+ * See the {@link Visitor} interface for more about implementing a visitor.
27
+ */
28
+export function visitTree<TBlock, TValue, TItem, TCase>(
29
+ fn: HIRFunction,
30
+ visitor: Visitor<TBlock, TValue, TItem, TCase>
31
+): TItem {
32
+ const cx = new Context(fn.body);
33
+ const driver = new Driver(cx, visitor);
34
+ return driver.traverseBlock(cx.block(fn.body.entry));
35
+}
36
+
37
+class Driver<TBlock, TValue, TItem, TCase> {
38
+ cx: Context;
39
+ visitor: Visitor<TBlock, TValue, TItem, TCase>;
40
+
41
+ constructor(cx: Context, visitor: Visitor<TBlock, TValue, TItem, TCase>) {
42
+ this.cx = cx;
43
+ this.visitor = visitor;
44
+ }
45
+
46
+ traverseBlock(block: BasicBlock): TItem {
47
+ const blockValue = this.visitor.enterBlock();
48
+ this.visitBlock(block, blockValue);
49
+ return this.visitor.leaveBlock(blockValue);
50
+ }
51
+
52
+ visitBlock(block: BasicBlock, blockValue: TBlock): void {
53
+ invariant(
54
+ !this.cx.emitted.has(block.id),
55
+ `Cannot emit the same block twice: bb${block.id}`
56
+ );
57
+ this.cx.emitted.add(block.id);
58
+ for (const instr of block.instructions) {
59
+ this.visitInstr(instr, blockValue);
60
+ }
61
+
62
+ const terminal = block.terminal;
63
+ const scheduleIds = [];
64
+ switch (terminal.kind) {
65
+ case "return": {
66
+ const value =
67
+ terminal.value != null ? this.visitPlace(terminal.value) : null;
68
+ this.visitor.appendBlock(
69
+ blockValue,
70
+ this.visitor.visitTerminal({
71
+ kind: "return",
72
+ value,
73
+ })
74
+ );
75
+ break;
76
+ }
77
+ case "throw": {
78
+ const value = this.visitPlace(terminal.value);
79
+ this.visitor.appendBlock(
80
+ blockValue,
81
+ this.visitor.visitTerminal({
82
+ kind: "throw",
83
+ value,
84
+ })
85
+ );
86
+ break;
87
+ }
88
+ case "if": {
89
+ const test = this.visitPlace(terminal.test);
90
+ const fallthroughId =
91
+ terminal.fallthrough !== null &&
92
+ !this.cx.isScheduled(terminal.fallthrough)
93
+ ? terminal.fallthrough
94
+ : null;
95
+ const alternateId =
96
+ terminal.alternate !== terminal.fallthrough
97
+ ? terminal.alternate
98
+ : null;
99
+
100
+ if (fallthroughId !== null) {
101
+ const scheduleId = this.cx.schedule(fallthroughId, "if");
102
+ scheduleIds.push(scheduleId);
103
+ }
104
+
105
+ let consequent: TItem | null = null;
106
+ if (this.cx.isScheduled(terminal.consequent)) {
107
+ consequent = this.visitBreak(terminal.consequent);
108
+ } else {
109
+ consequent = this.traverseBlock(
110
+ this.cx.ir.blocks.get(terminal.consequent)!
111
+ );
112
+ }
113
+
114
+ let alternate: TItem | null = null;
115
+ if (alternateId !== null) {
116
+ if (this.cx.isScheduled(alternateId)) {
117
+ alternate = this.visitBreak(alternateId);
118
+ } else {
119
+ alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
120
+ }
121
+ }
122
+
123
+ this.cx.unscheduleAll(scheduleIds);
124
+ if (fallthroughId !== null) {
125
+ this.visitor.appendBlock(
126
+ blockValue,
127
+ this.visitor.visitTerminal({
128
+ kind: "if",
129
+ test,
130
+ consequent: consequent ?? this.emptyBlock(),
131
+ alternate: alternate,
132
+ }),
133
+ `bb${fallthroughId}` //
134
+ );
135
+ this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
136
+ } else {
137
+ this.visitor.appendBlock(
138
+ blockValue,
139
+ this.visitor.visitTerminal({
140
+ kind: "if",
141
+ test,
142
+ consequent: consequent ?? this.emptyBlock(),
143
+ alternate: alternate,
144
+ })
145
+ );
146
+ }
147
+ break;
148
+ }
149
+ case "switch": {
150
+ const test = this.visitPlace(terminal.test);
151
+ const fallthroughId =
152
+ terminal.fallthrough !== null &&
153
+ !this.cx.isScheduled(terminal.fallthrough)
154
+ ? terminal.fallthrough
155
+ : null;
156
+ if (fallthroughId !== null) {
157
+ const scheduleId = this.cx.schedule(fallthroughId, "switch");
158
+ scheduleIds.push(scheduleId);
159
+ }
160
+
161
+ const cases: Array<TCase> = [];
162
+ [...terminal.cases].reverse().forEach((case_, index) => {
163
+ const test = case_.test !== null ? this.visitPlace(case_.test) : null;
164
+
165
+ let consequent;
166
+ if (this.cx.isScheduled(case_.block)) {
167
+ // cases which are empty or contain only a `break` may point to blocks
168
+ // that are already scheduled. emit as follows:
169
+ // - if the block is for another case branch, don't emit a break and fall-through
170
+ // - else, emit an explicit break.
171
+ const break_ = this.visitBreak(case_.block);
172
+ if (
173
+ index === 0 &&
174
+ break_ === null &&
175
+ case_.block === terminal.fallthrough &&
176
+ case_.test === null
177
+ ) {
178
+ // If the last case statement (first in reverse order) is a default that
179
+ // jumps to the fallthrough, then we would emit a useless `default: {}`,
180
+ // so instead skip this case.
181
+ return;
182
+ }
183
+ const block = this.visitor.enterBlock();
184
+ if (break_ !== null) {
185
+ this.visitor.appendBlock(block, break_);
186
+ }
187
+ consequent = this.visitor.leaveBlock(block);
188
+ } else {
189
+ consequent = this.traverseBlock(
190
+ this.cx.ir.blocks.get(case_.block)!
191
+ );
192
+ const scheduleId = this.cx.schedule(case_.block, "case");
193
+ scheduleIds.push(scheduleId);
194
+ }
195
+ cases.push(this.visitor.visitCase(test, consequent));
196
+ });
197
+ cases.reverse();
198
+
199
+ this.cx.unscheduleAll(scheduleIds);
200
+ if (fallthroughId !== null) {
201
+ this.visitor.appendBlock(
202
+ blockValue,
203
+ this.visitor.visitTerminal({ kind: "switch", test, cases }),
204
+ `bb${fallthroughId}`
205
+ );
206
+ this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
207
+ } else {
208
+ this.visitor.appendBlock(
209
+ blockValue,
210
+ this.visitor.visitTerminal({ kind: "switch", test, cases })
211
+ );
212
+ }
213
+ break;
214
+ }
215
+ case "while": {
216
+ const testBlock = this.cx.ir.blocks.get(terminal.test)!;
217
+ const testTerminal = testBlock.terminal;
218
+ invariant(
219
+ testTerminal.kind === "if",
220
+ "Expected while loop test block to end in an if"
221
+ );
222
+ // const bodyLength = blockValue.length;
223
+ for (const instr of testBlock.instructions) {
224
+ this.visitInstr(instr, blockValue);
225
+ }
226
+ // invariant(
227
+ // body.length === bodyLength,
228
+ // "Expected test to produce only temporaries"
229
+ // );
230
+ const testValue = this.visitPlace(testTerminal.test);
231
+ invariant(
232
+ testValue != null,
233
+ "Expected test to produce a temporary value"
234
+ );
235
+
236
+ const fallthroughId =
237
+ terminal.fallthrough !== null &&
238
+ !this.cx.isScheduled(terminal.fallthrough)
239
+ ? terminal.fallthrough
240
+ : null;
241
+ const loopId =
242
+ !this.cx.isScheduled(terminal.loop) &&
243
+ terminal.loop !== terminal.fallthrough
244
+ ? terminal.loop
245
+ : null;
246
+ const scheduleId = this.cx.scheduleLoop(
247
+ terminal.fallthrough,
248
+ terminal.test,
249
+ terminal.loop
250
+ );
251
+ scheduleIds.push(scheduleId);
252
+
253
+ let loopBody: TItem;
254
+ if (loopId) {
255
+ loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
256
+ } else {
257
+ const break_ = this.visitBreak(terminal.loop);
258
+ invariant(
259
+ break_ !== null,
260
+ "If loop body is already scheduled it must be a break"
261
+ );
262
+ const body = this.visitor.enterBlock();
263
+ this.visitor.appendBlock(body, break_);
264
+ loopBody = this.visitor.leaveBlock(body);
265
+ }
266
+
267
+ this.cx.unscheduleAll(scheduleIds);
268
+ if (fallthroughId !== null) {
269
+ this.visitor.appendBlock(
270
+ blockValue,
271
+ this.visitor.visitTerminal({
272
+ kind: "while",
273
+ test: testValue,
274
+ loop: loopBody,
275
+ }),
276
+ `bb${fallthroughId}`
277
+ );
278
+ this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
279
+ } else {
280
+ this.visitor.appendBlock(
281
+ blockValue,
282
+ this.visitor.visitTerminal({
283
+ kind: "while",
284
+ test: testValue,
285
+ loop: loopBody,
286
+ })
287
+ );
288
+ }
289
+ break;
290
+ }
291
+ case "goto": {
292
+ switch (terminal.variant) {
293
+ case GotoVariant.Break: {
294
+ const break_ = this.visitBreak(terminal.block);
295
+ if (break_ !== null) {
296
+ this.visitor.appendBlock(blockValue, break_);
297
+ }
298
+ break;
299
+ }
300
+ case GotoVariant.Continue: {
301
+ const continue_ = this.visitContinue(terminal.block);
302
+ if (continue_ !== null) {
303
+ this.visitor.appendBlock(blockValue, continue_);
304
+ }
305
+ break;
306
+ }
307
+ default: {
308
+ assertExhaustive(
309
+ terminal.variant,
310
+ `Unexpected goto variant '${terminal.variant}'`
311
+ );
312
+ }
313
+ }
314
+ break;
315
+ }
316
+ default: {
317
+ assertExhaustive(terminal, "Unexpected terminal");
318
+ }
319
+ }
320
+ }
321
+
322
+ emptyBlock(): TItem {
323
+ const block = this.visitor.enterBlock();
324
+ return this.visitor.leaveBlock(block);
325
+ }
326
+
327
+ visitBreak(block: BlockId): TItem | null {
328
+ const target = this.cx.getBreakTarget(block);
329
+ if (target === null) {
330
+ // TODO: we should always have a target
331
+ return null;
332
+ }
333
+ switch (target.type) {
334
+ case "implicit": {
335
+ return this.visitor.visitImplicitTerminal();
336
+ }
337
+ case "unlabeled": {
338
+ return this.visitor.visitTerminal({ kind: "break", label: null });
339
+ }
340
+ case "labeled": {
341
+ return this.visitor.visitTerminal({
342
+ kind: "break",
343
+ label: `bb${target.block}`,
344
+ });
345
+ }
346
+ }
347
+ }
348
+
349
+ visitContinue(block: BlockId): TItem | null {
350
+ const target = this.cx.getContinueTarget(block);
351
+ invariant(
352
+ target !== null,
353
+ `Expected continue target to be scheduled for bb${block}`
354
+ );
355
+ switch (target.type) {
356
+ case "labeled": {
357
+ return this.visitor.visitTerminal({
358
+ kind: "continue",
359
+ label: `bb${target.block}`,
360
+ });
361
+ }
362
+ case "unlabeled": {
363
+ return this.visitor.visitTerminal({ kind: "continue", label: null });
364
+ }
365
+ case "implicit": {
366
+ return this.visitor.visitImplicitTerminal();
367
+ }
368
+ default: {
369
+ assertExhaustive(
370
+ target.type,
371
+ `Unexpected continue target kind '${(target as any).type}'`
372
+ );
373
+ }
374
+ }
375
+ }
376
+
377
+ visitInstr(instr: Instruction, blockValue: TBlock): void {
378
+ const value = this.visitor.visitValue(instr.value);
379
+ const item = this.visitor.visitInstruction(instr, value);
380
+ this.visitor.appendBlock(blockValue, item);
381
+ }
382
+
383
+ visitPlace(place: Place): TValue {
384
+ return this.visitor.visitValue(place);
385
+ }
386
+}
387
+
388
+class Context {
389
+ ir: HIR;
390
+ #nextScheduleId: number = 0;
391
+
392
+ /**
393
+ * Used to track which blocks *have been* generated already in order to
394
+ * abort if a block is generated a second time. This is an error catching
395
+ * mechanism for debugging purposes, and is not used by the codegen algorithm
396
+ * to drive decisions about how to emit blocks.
397
+ */
398
+ emitted: Set<BlockId> = new Set();
399
+
400
+ /**
401
+ * A set of blocks that are already scheduled to be emitted by eg a parent.
402
+ * This allows child nodes to avoid re-emitting the same block and emit eg
403
+ * a break instead.
404
+ */
405
+ #scheduled: Set<BlockId> = new Set();
406
+
407
+ /**
408
+ * Represents which control flow operations are currently in scope, with the innermost
409
+ * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
410
+ * control will implicitly transfer, such that gotos to that block can be elided. Gotos
411
+ * targeting items higher up the stack may need labeled break or continue; see
412
+ * getBreakTarget() and getContinueTarget() for more details.
413
+ */
414
+ #controlFlowStack: Array<ControlFlowTarget> = [];
415
+
416
+ constructor(ir: HIR) {
417
+ this.ir = ir;
418
+ }
419
+
420
+ block(id: BlockId): BasicBlock {
421
+ return this.ir.blocks.get(id)!;
422
+ }
423
+
424
+ /**
425
+ * Record that the given block will be emitted (eg by the codegen of a parent node)
426
+ * so that child nodes can avoid re-emitting it.
427
+ */
428
+ schedule(block: BlockId, type: "if" | "switch" | "case"): number {
429
+ const id = this.#nextScheduleId++;
430
+ invariant(
431
+ !this.#scheduled.has(block),
432
+ `Break block is already scheduled: bb${block}`
433
+ );
434
+ this.#scheduled.add(block);
435
+ this.#controlFlowStack.push({ block, id, type });
436
+ return id;
437
+ }
438
+
439
+ scheduleLoop(
440
+ fallthroughBlock: BlockId,
441
+ continueBlock: BlockId,
442
+ loopBlock: BlockId | null
443
+ ): number {
444
+ const id = this.#nextScheduleId++;
445
+ const ownsBlock = !this.#scheduled.has(fallthroughBlock);
446
+ this.#scheduled.add(fallthroughBlock);
447
+ invariant(
448
+ !this.#scheduled.has(continueBlock),
449
+ `Continue block is already scheduled: bb${continueBlock}`
450
+ );
451
+ this.#scheduled.add(continueBlock);
452
+ let ownsLoop = false;
453
+ if (loopBlock !== null) {
454
+ ownsLoop = !this.#scheduled.has(loopBlock);
455
+ this.#scheduled.add(loopBlock);
456
+ }
457
+
458
+ this.#controlFlowStack.push({
459
+ block: fallthroughBlock,
460
+ ownsBlock,
461
+ id,
462
+ type: "loop",
463
+ continueBlock,
464
+ loopBlock,
465
+ ownsLoop,
466
+ });
467
+ return id;
468
+ }
469
+
470
+ /**
471
+ * Removes a block that was scheduled; must be called after that block is emitted.
472
+ */
473
+ unschedule(scheduleId: number): void {
474
+ const last = this.#controlFlowStack.pop();
475
+ invariant(
476
+ last !== undefined && last.id === scheduleId,
477
+ "Can only unschedule the last target"
478
+ );
479
+ if (last.type !== "loop" || last.ownsBlock !== null) {
480
+ this.#scheduled.delete(last.block);
481
+ }
482
+ if (last.type === "loop") {
483
+ this.#scheduled.delete(last.continueBlock);
484
+ if (last.ownsLoop && last.loopBlock !== null) {
485
+ this.#scheduled.delete(last.loopBlock);
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Helper to unschedule multiple scheduled blocks. The ids should be in
492
+ * the order in which they were scheduled, ie most recently scheduled last.
493
+ */
494
+ unscheduleAll(scheduleIds: Array<number>): void {
495
+ for (let i = scheduleIds.length - 1; i >= 0; i--) {
496
+ this.unschedule(scheduleIds[i]!);
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Check if the given @param block is scheduled or not.
502
+ */
503
+ isScheduled(block: BlockId): boolean {
504
+ return this.#scheduled.has(block);
505
+ }
506
+
507
+ /**
508
+ * Given the current control flow stack, determines how a `break` to the given @param block
509
+ * must be emitted. Returns as follows:
510
+ * - 'implicit' if control would implicitly transfer to that block
511
+ * - 'labeled' if a labeled break is required to transfer control to that block
512
+ * - 'unlabeled' if an unlabeled break would transfer to that block
513
+ * - null if there is no information for this block
514
+ *
515
+ * The returned 'block' value should be used as the label if necessary.
516
+ */
517
+ getBreakTarget(
518
+ block: BlockId
519
+ ): { block: BlockId; type: ControlFlowKind } | null {
520
+ let hasPrecedingLoop = false;
521
+ for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
522
+ const target = this.#controlFlowStack[i]!;
523
+ if (target.block === block) {
524
+ let type: ControlFlowKind;
525
+ if (target.type === "loop") {
526
+ // breaking out of a loop requires an explicit break,
527
+ // but only requires a label if breaking past the innermost loop.
528
+ type = hasPrecedingLoop ? "labeled" : "unlabeled";
529
+ } else if (i === this.#controlFlowStack.length - 1) {
530
+ // breaking to the last break point, which is where control will transfer
531
+ // implicitly
532
+ type = "implicit";
533
+ } else {
534
+ // breaking somewhere else requires an explicit break
535
+ type = "labeled";
536
+ }
537
+ return {
538
+ block: target.block,
539
+ type,
540
+ };
541
+ }
542
+ hasPrecedingLoop ||= target.type === "loop";
543
+ }
544
+ return null;
545
+ }
546
+
547
+ /**
548
+ * Given the current control flow stack, determines how a `continue` to the given @param block
549
+ * must be emitted. Returns as follows:
550
+ * - 'implicit' if control would implicitly continue to that block
551
+ * - 'labeled' if a labeled continue is required to continue to that block
552
+ * - 'unlabeled' if an unlabeled continue would transfer to that block
553
+ * - null if there is no information for this block
554
+ *
555
+ * The returned 'block' value should be used as the label if necessary.
556
+ */
557
+ getContinueTarget(
558
+ block: BlockId
559
+ ): { block: BlockId; type: ControlFlowKind } | null {
560
+ let hasPrecedingLoop = false;
561
+ for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
562
+ const target = this.#controlFlowStack[i]!;
563
+ if (target.type == "loop" && target.continueBlock === block) {
564
+ let type: ControlFlowKind;
565
+ if (hasPrecedingLoop) {
566
+ // continuing to a loop that is not the innermost loop always requires
567
+ // a label
568
+ type = "labeled";
569
+ } else if (i === this.#controlFlowStack.length - 1) {
570
+ // continuing to the last break point, which is where control will
571
+ // transfer to naturally
572
+ type = "implicit";
573
+ } else {
574
+ // the continue is inside some conditional logic, requires an explicit
575
+ // continue
576
+ type = "unlabeled";
577
+ }
578
+ return {
579
+ block: target.block,
580
+ type,
581
+ };
582
+ }
583
+ hasPrecedingLoop ||= target.type === "loop";
584
+ }
585
+ return null;
586
+ }
587
+
588
+ debugBreakTargets(): Array<ControlFlowTarget> {
589
+ return this.#controlFlowStack.map((target) => ({ ...target }));
590
+ }
591
+}
592
+
593
+type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
594
+
595
+type ControlFlowTarget =
596
+ | { type: "if"; block: BlockId; id: number }
597
+ | { type: "switch"; block: BlockId; id: number }
598
+ | { type: "case"; block: BlockId; id: number }
599
+ | {
600
+ type: "loop";
601
+ block: BlockId;
602
+ ownsBlock: boolean;
603
+ continueBlock: BlockId;
604
+ loopBlock: BlockId | null;
605
+ ownsLoop: boolean;
606
+ id: number;
607
+ };
608
+
609
+/**
610
+ * An object that can receive structured callbacks to visit HIR as a tree,
611
+ * and convert it to an alternate format.
612
+ *
613
+ * TBlock = representation of a list of statements
614
+ * TValue = represenation of an InstructionValue
615
+ * TItem = representation of an Instruction
616
+ * TCase = representation of a switch case
617
+ */
618
+export interface Visitor<TBlock, TValue, TItem, TCase> {
619
+ /**
620
+ * Must create an "empty" instance of the visitor's represenation for
621
+ * the contents of a block.
622
+ */
623
+ enterBlock(): TBlock;
624
+
625
+ /**
626
+ * Convert an InstructionValue into the visitor's own representation
627
+ * of a value.
628
+ */
629
+ visitValue(value: InstructionValue): TValue;
630
+
631
+ /**
632
+ * Convert an Instruction into the visitor's own representation of
633
+ * a block item.
634
+ */
635
+ visitInstruction(instruction: Instruction, value: TValue): TItem;
636
+
637
+ /**
638
+ * Converts a break/continue that is implicit — that does not strictly
639
+ * have to be emitted — to the visitor's representation. The visitor
640
+ * can choose to return null if this does not need to be represented.
641
+ */
642
+ visitImplicitTerminal(): TItem | null;
643
+
644
+ /**
645
+ * Converts a terminal into the visitor's own representation of a block
646
+ * item. Note that the terminal differs from HIR Terminals, because
647
+ * values and block ids will have already been converted into the visitor's
648
+ * own representations.
649
+ */
650
+ visitTerminal(terminal: BlockTerminal<TBlock, TValue, TItem, TCase>): TItem;
651
+
652
+ /**
653
+ * Visits a switch case statement, which is collected into a switch terminal
654
+ * variant.
655
+ */
656
+ visitCase(test: TValue | null, block: TItem): TCase;
657
+
658
+ /**
659
+ * Appends an item onto the given block, with an optional label. The label
660
+ * indicates that a break/continue will proceed to code *after* the given item.
661
+ */
662
+ appendBlock(block: TBlock, item: TItem, label?: string): void;
663
+
664
+ /**
665
+ * Converts the visitor's block representation into the representation of a
666
+ * block item, simultaneously "closing" the given block. The block will no
667
+ * longer be modified by the visitor driver.
668
+ */
669
+ leaveBlock(block: TBlock): TItem;
670
+}
671
+
672
+export type BlockTerminal<TBlock, TValue, TItem, TCase> =
673
+ | { kind: "return"; value: TValue | null }
674
+ | { kind: "throw"; value: TValue }
675
+ | {
676
+ kind: "if";
677
+ test: TValue;
678
+ consequent: TItem;
679
+ alternate: TItem | null;
680
+ }
681
+ | { kind: "switch"; test: TValue; cases: Array<TCase> }
682
+ | { kind: "while"; test: TValue; loop: TItem }
683
+ | { kind: "break"; label: string | null }
684
+ | { kind: "continue"; label: string | null };
compiler/forget/src/HIR/PrintHIRTree.ts
new
+127
@@ -0,0 +1,127 @@
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 { assertExhaustive } from "../Common/utils";
9
+import { HIRFunction, Instruction, InstructionValue } from "./HIR";
10
+import { BlockTerminal, Visitor, visitTree } from "./HIRTreeVisitor";
11
+import { printLValue, printMixedHIR } from "./PrintHIR";
12
+
13
+/**
14
+ * Returns a text description of the HIR that has the overall tree shape
15
+ * of the original AST, but with the contents of each block printed
16
+ * similarly to printHIR's instruction formatting.
17
+ */
18
+export function printHIRTree(fn: HIRFunction): string {
19
+ return visitTree(fn, new PrintVisitor());
20
+}
21
+
22
+class PrintVisitor implements Visitor<Array<string>, string, string, string> {
23
+ depth: number = 0; // for indentation
24
+
25
+ enterBlock(): string[] {
26
+ this.depth++;
27
+ return [];
28
+ }
29
+ visitValue(value: InstructionValue): string {
30
+ return printMixedHIR(value);
31
+ }
32
+ visitInstruction(instr: Instruction, value: string): string {
33
+ if (instr.lvalue !== null) {
34
+ return `[${instr.id}] ${printLValue(instr.lvalue)} = ${value}`;
35
+ } else {
36
+ return `[${instr.id}] ${value}`;
37
+ }
38
+ }
39
+ visitImplicitTerminal(): string | null {
40
+ return null;
41
+ }
42
+ visitTerminal(
43
+ terminal: BlockTerminal<string[], string, string, string>
44
+ ): string {
45
+ let value: string;
46
+ switch (terminal.kind) {
47
+ case "break": {
48
+ if (terminal.label !== null) {
49
+ value = `Break ${terminal.label}`;
50
+ } else {
51
+ value = "Break";
52
+ }
53
+ break;
54
+ }
55
+ case "continue": {
56
+ if (terminal.label !== null) {
57
+ value = `Continue ${terminal.label}`;
58
+ } else {
59
+ value = "Continue";
60
+ }
61
+ break;
62
+ }
63
+ case "if": {
64
+ if (terminal.alternate !== null) {
65
+ value = `If (${
66
+ terminal.test
67
+ }) ${terminal.consequent.trimStart()} else ${terminal.alternate}`;
68
+ } else {
69
+ value = `If (${terminal.test}) ${terminal.consequent.trimStart()}`;
70
+ }
71
+ break;
72
+ }
73
+ case "switch": {
74
+ const prefix = " ".repeat(this.depth);
75
+ value = `Switch (${terminal.test}) {\n${terminal.cases
76
+ .flatMap((case_) => case_.split("\n").map((line) => ` ${line}`))
77
+ .join("\n")}\n${prefix}}`;
78
+ break;
79
+ }
80
+ case "while": {
81
+ value = `While (${terminal.test}) ${terminal.loop.trimStart()}`;
82
+ break;
83
+ }
84
+ case "return": {
85
+ if (terminal.value !== null) {
86
+ value = `Return ${terminal.value}`;
87
+ } else {
88
+ value = "Return";
89
+ }
90
+ break;
91
+ }
92
+ case "throw": {
93
+ value = `Throw ${terminal.value}`;
94
+ break;
95
+ }
96
+ default: {
97
+ assertExhaustive(
98
+ terminal,
99
+ `Unexpected terminal kind '${(terminal as any).kind}'`
100
+ );
101
+ }
102
+ }
103
+ return value;
104
+ }
105
+ visitCase(test: string | null, block: string): string {
106
+ const prefix = " ".repeat(this.depth);
107
+ if (test === null) {
108
+ return `${prefix}default: ${block.trimStart()}`;
109
+ } else {
110
+ return `${prefix}case ${test}: ${block.trimStart()}`;
111
+ }
112
+ }
113
+ appendBlock(block: string[], item: string, label?: string | undefined): void {
114
+ const prefix = " ".repeat(this.depth);
115
+ if (item !== "") {
116
+ block.push(`${prefix}${item.trimStart()}`);
117
+ }
118
+ if (label !== undefined) {
119
+ block.push(`${prefix}${label}:`);
120
+ }
121
+ }
122
+ leaveBlock(block: string[]): string {
123
+ this.depth--;
124
+ const prefix = " ".repeat(this.depth);
125
+ return `${prefix}{\n${block.join("\n")}\n${prefix}}`;
126
+ }
127
+}
compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md
+12
-4
@@ -234,10 +234,18 @@ function Component$0(props$1) {
234
bb4: if (cond$8(a$2)) break;
235
}
236
237
- a$2;
238
- b$3;
239
- c$4;
240
- d$5;
237
+ bb7: if (a$2) {
238
+ }
239
+
240
+ bb9: if (b$3) {
241
+ }
242
+
243
+ bb11: if (c$4) {
244
+ }
245
+
246
+ bb13: if (d$5) {
247
+ }
248
+
249
mutate$7(d$5, null);
250
return;
251
}
compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.expect.md
+15
-5
@@ -158,11 +158,21 @@ function Component$0(props$1) {
158
const x$6 = {};
159
x$6.b = b$3;
160
const y$7 = mutate$8(x$6, d$5);
161
- a$2;
162
- b$3;
163
- c$4;
164
- d$5;
165
- y$7;
161
+ bb1: if (a$2) {
162
+ }
163
+
164
+ bb3: if (b$3) {
165
+ }
166
+
167
+ bb5: if (c$4) {
168
+ }
169
+
170
+ bb7: if (d$5) {
171
+ }
172
+
173
+ bb9: if (y$7) {
174
+ }
175
+
176
mutate$8(x$6, null);
177
return;
178
}
compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md
+12
-4
@@ -214,10 +214,18 @@ function Component$0(props$1) {
214
bb4: if (cond$7(a$2)) break;
215
}
216
217
- a$2;
218
- b$3;
219
- c$4;
220
- d$5;
217
+ bb7: if (a$2) {
218
+ }
219
+
220
+ bb9: if (b$3) {
221
+ }
222
+
223
+ bb11: if (c$4) {
224
+ }
225
+
226
+ bb13: if (d$5) {
227
+ }
228
+
229
mutate$6(d$5, null);
230
return;
231
}
compiler/forget/src/__tests__/hir-test.ts
+1
@@ -72,6 +72,7 @@ describe("React Forget (HIR version)", () => {
72
);
73
74
const textHIR = printHIR(ir.body);
75
+ // const textHIR = visitTree(ir, new PrintVisitor());
76
const visualization = visualizeHIRMermaid(ir);
77
const text = prettier.format(
78
generate(ast).code.replace("\n\n", "\n"),