10
import { invariant } from "../CompilerError";
11
import {
12
BasicBlock,
13
+ BlockId,
14
+ GotoVariant,
15
HIR,
16
HIRFunction,
17
Identifier,
45
*/
46
export default function codegen(fn: HIRFunction): t.Function {
47
const entry = fn.body.blocks.get(fn.body.entry)!;
46
- const cx: Context = { ir: fn.body, temp: new Map() };
48
+ const cx = new Context(fn.body);
49
const body = codegenBlock(cx, entry);
50
const params = fn.params.map((param) => convertIdentifier(param.identifier));
51
return t.functionDeclaration(
57
);
58
}
59
58
-type Context = {
60
+class Context {
61
ir: HIR;
60
- temp: Map<IdentifierId, t.Expression>;
62
+ temp: Map<IdentifierId, t.Expression> = new Map();
63
+ #nextScheduleId: number = 0;
64
+
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
+ * A stack of blocks that are in scope, used to decide whether/how to emit
82
+ * break and continue statements. All blocks in the stack must also be
83
+ * in 'scheduled'.
84
+ */
85
+ #breakTargets: Array<BreakTarget> = [];
86
+
87
+ constructor(ir: HIR) {
88
+ this.ir = ir;
89
+ }
90
+
91
+ /**
92
+ * Record that the given block will be emitted (eg by the codegen of a parent node)
93
+ * so that child nodes can avoid re-emitting it.
94
+ */
95
+ schedule(block: BlockId, type: "if" | "switch" | "case"): number {
96
+ const id = this.#nextScheduleId++;
97
+ invariant(!this.#scheduled.has(block), "Block is already scheduled");
98
+ this.#scheduled.add(block);
99
+ this.#breakTargets.push({ block, id, type });
100
+ return id;
101
+ }
102
+
103
+ /**
104
+ * Removes a block that was scheduled; must be called after that block is emitted.
105
+ */
106
+ unschedule(scheduleId: number): void {
107
+ const last = this.#breakTargets.pop();
108
+ invariant(
109
+ last !== undefined && last.id === scheduleId,
110
+ "Can only unschedule the last target"
111
+ );
112
+ this.#scheduled.delete(last.block);
113
+ }
114
+
115
+ /**
116
+ * Helper to unschedule multiple scheduled blocks. The ids should be in
117
+ * the order in which they were scheduled, ie most recently scheduled last.
118
+ */
119
+ unscheduleAll(scheduleIds: Array<number>): void {
120
+ for (let i = scheduleIds.length - 1; i >= 0; i--) {
121
+ this.unschedule(scheduleIds[i]!);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if the given @param block is scheduled or not.
127
+ */
128
+ isScheduled(block: BlockId): boolean {
129
+ return this.#scheduled.has(block);
130
+ }
131
+
132
+ /**
133
+ * Lookup the break target for the given @param block. This will return non-null
134
+ * if and only if isScheduled() returns true for the given @param block. Returns
135
+ * the break target and whether this is the most recent target (which can be used
136
+ * to elide unnecessary break statemetns).
137
+ */
138
+ getBreakTarget(
139
+ block: BlockId
140
+ ): { target: BreakTarget; last: boolean } | null {
141
+ for (let i = this.#breakTargets.length - 1; i >= 0; i--) {
142
+ const target = this.#breakTargets[i]!;
143
+ if (target.block === block) {
144
+ return {
145
+ target,
146
+ last: i === this.#breakTargets.length - 1,
147
+ };
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+}
153
+
154
+type BreakTarget = {
155
+ block: BlockId;
156
+ id: number;
157
+ type: "if" | "switch" | "case";
158
};
159
160
function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement {
161
+ invariant(
162
+ !cx.emitted.has(block.id),
163
+ `Cannot emit the same block twice: bb${block.id}`
164
+ );
165
+ cx.emitted.add(block.id);
166
const body: Array<t.Statement> = [];
167
writeBlock(cx, block, body);
168
return t.blockStatement(body);
173
writeInstr(cx, instr, body);
174
}
175
const terminal = block.terminal;
176
+ const scheduleIds = [];
177
switch (terminal.kind) {
178
case "return": {
179
const value =
188
}
189
case "if": {
190
const test = codegenPlace(cx, terminal.test);
88
- const consequent = codegenBlock(
89
- cx,
90
- cx.ir.blocks.get(terminal.consequent)!
91
- );
92
- const fallthrough =
93
- terminal.fallthrough !== null &&
94
- terminal.fallthrough !== terminal.alternate
191
+ const fallthroughId =
192
+ terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
193
? terminal.fallthrough
194
: null;
97
- if (fallthrough !== null) {
98
- const alternate = codegenBlock(
99
- cx,
100
- cx.ir.blocks.get(terminal.alternate)!
101
- );
102
- body.push(t.ifStatement(test, consequent, alternate));
103
- const fallthroughBlock = cx.ir.blocks.get(fallthrough)!;
104
- writeBlock(cx, fallthroughBlock, body);
195
+ const alternateId =
196
+ terminal.alternate !== terminal.fallthrough ? terminal.alternate : null;
197
+
198
+ if (fallthroughId !== null) {
199
+ const scheduleId = cx.schedule(fallthroughId, "if");
200
+ scheduleIds.push(scheduleId);
201
+ }
202
+
203
+ let consequent: t.Statement | null = null;
204
+ if (cx.isScheduled(terminal.consequent)) {
205
+ consequent = codegenBreak(cx, terminal.consequent);
206
} else {
106
- body.push(t.ifStatement(test, consequent));
107
- writeBlock(cx, cx.ir.blocks.get(terminal.alternate)!, body);
207
+ consequent = codegenBlock(cx, cx.ir.blocks.get(terminal.consequent)!);
208
+ }
209
+
210
+ let alternate: t.Statement | null = null;
211
+ if (alternateId !== null) {
212
+ if (cx.isScheduled(alternateId)) {
213
+ alternate = codegenBreak(cx, alternateId);
214
+ } else {
215
+ alternate = codegenBlock(cx, cx.ir.blocks.get(alternateId)!);
216
+ }
217
+ }
218
+
219
+ if (fallthroughId !== null) {
220
+ if (consequent === null && alternate === null) {
221
+ body.push(t.expressionStatement(test));
222
+ } else {
223
+ body.push(
224
+ t.labeledStatement(
225
+ t.identifier(`bb${fallthroughId}`),
226
+ t.ifStatement(test, consequent ?? t.blockStatement([]), alternate)
227
+ )
228
+ );
229
+ }
230
+ writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
231
+ } else {
232
+ if (consequent === null && alternate === null) {
233
+ body.push(t.expressionStatement(test));
234
+ } else {
235
+ body.push(
236
+ t.ifStatement(test, consequent ?? t.blockStatement([]), alternate)
237
+ );
238
+ }
239
}
109
- break;
110
- }
111
- case "goto": {
112
- body.push(
113
- t.expressionStatement(
114
- t.stringLiteral("<<TODO: handle complex control flow in codegen>>")
115
- )
116
- );
240
break;
241
}
242
case "switch": {
243
+ const test = codegenPlace(cx, terminal.test);
244
+ const fallthroughId =
245
+ terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
246
+ ? terminal.fallthrough
247
+ : null;
248
+ if (fallthroughId !== null) {
249
+ const scheduleId = cx.schedule(fallthroughId, "switch");
250
+ scheduleIds.push(scheduleId);
251
+ }
252
+
253
const cases: Array<t.SwitchCase> = [];
121
- terminal.cases.forEach((case_, index) => {
254
+ [...terminal.cases].reverse().forEach((case_, index) => {
255
const test = case_.test !== null ? codegenPlace(cx, case_.test) : null;
123
- // If the final case is a `default` *and* points directly to the
124
- // fallthrough branch, then we can skip emitting `default: break`
125
- // since this implied. For a default in any other position, or
126
- // for a default pointing to a different block, emit a case
127
- // normally.
128
- if (
129
- index === terminal.cases.length - 1 &&
130
- test === null &&
131
- case_.block === terminal.fallthrough
132
- ) {
133
- return;
134
- } else if (case_.block === terminal.fallthrough) {
135
- // Otherwise for any block that points directly to the fallthrough,
136
- // emit a break instead
137
- cases.push(t.switchCase(test, [t.breakStatement()]));
256
+
257
+ let consequent;
258
+ if (cx.isScheduled(case_.block)) {
259
+ // cases which are empty or contain only a `break` may point to blocks
260
+ // that are already scheduled. emit as follows:
261
+ // - if the block is for another case branch, don't emit a break and fall-through
262
+ // - else, emit an explicit break.
263
+ const break_ = codegenBreak(cx, case_.block);
264
+ if (
265
+ index === 0 &&
266
+ break_ === null &&
267
+ case_.block === terminal.fallthrough &&
268
+ case_.test === null
269
+ ) {
270
+ // If the last case statement (first in reverse order) is a default that
271
+ // jumps to the fallthrough, then we would emit a useless `default: {}`,
272
+ // so instead skip this case.
273
+ return;
274
+ }
275
+ const block = [];
276
+ if (break_ !== null) {
277
+ block.push(break_);
278
+ }
279
+ consequent = t.blockStatement(block);
280
} else {
139
- const consequent = codegenBlock(cx, cx.ir.blocks.get(case_.block)!);
140
- cases.push(t.switchCase(test, [consequent]));
281
+ consequent = codegenBlock(cx, cx.ir.blocks.get(case_.block)!);
282
+ const scheduleId = cx.schedule(case_.block, "case");
283
+ scheduleIds.push(scheduleId);
284
}
285
+ cases.push(t.switchCase(test, [consequent]));
286
});
143
- body.push(t.switchStatement(codegenPlace(cx, terminal.test), cases));
144
- if (terminal.fallthrough !== null) {
145
- writeBlock(cx, cx.ir.blocks.get(terminal.fallthrough)!, body);
287
+ cases.reverse();
288
+
289
+ if (fallthroughId !== null) {
290
+ body.push(
291
+ t.labeledStatement(
292
+ t.identifier(`bb${fallthroughId}`),
293
+ t.switchStatement(test, cases)
294
+ )
295
+ );
296
+ writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
297
+ } else {
298
+ body.push(t.switchStatement(test, cases));
299
+ }
300
+ break;
301
+ }
302
+ case "goto": {
303
+ switch (terminal.variant) {
304
+ case GotoVariant.Break: {
305
+ const break_ = codegenBreak(cx, terminal.block);
306
+ if (break_ !== null) {
307
+ body.push(break_);
308
+ }
309
+ break;
310
+ }
311
+ case GotoVariant.Continue: {
312
+ invariant(
313
+ cx.isScheduled(terminal.block),
314
+ "Expected continue target to be scheduled"
315
+ );
316
+ body.push(t.continueStatement(t.identifier(`bb${terminal.block}`)));
317
+ break;
318
+ }
319
+ default: {
320
+ assertExhaustive(
321
+ terminal.variant,
322
+ `Unexpected goto variant '${terminal.variant}'`
323
+ );
324
+ }
325
}
326
break;
327
}
329
assertExhaustive(terminal, "Unexpected terminal");
330
}
331
}
332
+ cx.unscheduleAll(scheduleIds);
333
+}
334
+
335
+function codegenBreak(cx: Context, block: BlockId): t.Statement | null {
336
+ const breakTarget = cx.getBreakTarget(block);
337
+ if (breakTarget === null) {
338
+ // TODO: we should always have a target
339
+ return null;
340
+ }
341
+ const { target, last } = breakTarget;
342
+ if (target.type === "case") {
343
+ // This break is transitioning to the next case statement. JS doesn't allow
344
+ // labeling cases, the only option is to emit a plain break.
345
+ return null;
346
+ } else if (last) {
347
+ // This break is to the most recent break target. Control flow will naturally
348
+ // transition to this target, so a break is not required.
349
+ return null;
350
+ } else {
351
+ // We're trying to break somewhere else, emit a label
352
+ return t.breakStatement(t.identifier(`bb${block}`));
353
+ }
354
}
355
356
function writeInstr(cx: Context, instr: Instruction, body: Array<t.Statement>) {