Collect bailouts in BuildHIR::lowerStatement
Remove invariants and collect all errors
Lauren Tan committed
Jan 13, 2023 at 16:14 UTC
7c65f62c27921f4dc887f6e83eab433a0a39d956
12 files changed
+325
-63
compiler/forget/src/CompilerError.ts
new
+34
@@ -0,0 +1,34 @@
1
+import { SourceLocation } from "./HIR";
2
+import type { ExtractClassProperties } from "./Utils/types";
3
+
4
+export enum ErrorSeverity {
5
+ InvalidInput = "InvalidInput",
6
+ Todo = "Todo",
7
+}
8
+
9
+export type CompilerErrorOptions = ExtractClassProperties<CompilerError>;
10
+
11
+export class CompilerError {
12
+ severity: ErrorSeverity;
13
+ reason: string;
14
+ source: string | null;
15
+ loc: SourceLocation | null;
16
+
17
+ constructor(options: CompilerErrorOptions) {
18
+ this.severity = options.severity;
19
+ this.reason = options.reason;
20
+ this.source = options.source;
21
+ this.loc = options.loc;
22
+ }
23
+
24
+ toString(): string {
25
+ const buffer = [`[${this.severity.toUpperCase()}] ${this.reason}`];
26
+ if (this.loc != null && typeof this.loc != "symbol") {
27
+ buffer.push(` on lines ${this.loc.start.line}:${this.loc.end.line}`);
28
+ }
29
+ if (this.source != null) {
30
+ buffer.push(`\n${this.source}`);
31
+ }
32
+ return `Forget Error: ${buffer.join("")}`;
33
+ }
34
+}
compiler/forget/src/CompilerPipeline.ts
+6
-1
@@ -44,7 +44,12 @@ export function* run(
44
): Iterator<CompilerPipelineValue, t.Function> {
45
const env = new Environment();
46
47
- const hir = lower(func, env);
47
+ const lowering = lower(func, env).orElse((errors) => {
48
+ const msg = errors.map((error) => error.toString()).join("\n\n");
49
+ throw new Error(msg);
50
+ });
51
+
52
+ const hir = lowering.unwrap();
53
yield log({ kind: "hir", name: "HIR", value: hir });
54
55
mergeConsecutiveBlocks(hir);
compiler/forget/src/HIR/BuildHIR.ts
+127
-58
@@ -8,6 +8,8 @@
8
import { NodePath, Scope } from "@babel/traverse";
9
import * as t from "@babel/types";
10
import invariant from "invariant";
11
+import { CompilerError, ErrorSeverity } from "../CompilerError";
12
+import { Err, Ok, Result } from "../lib/Result";
13
import todo, { todoInvariant } from "../Utils/todo";
14
import { assertExhaustive } from "../Utils/utils";
15
import {
@@ -64,7 +66,7 @@ function getOrAddGlobal(identifierName: string): t.Identifier {
66
export function lower(
67
func: NodePath<t.Function>,
68
env: Environment
67
-): HIRFunction {
69
+): Result<HIRFunction, CompilerError[]> {
70
const builder = new HIRBuilder(env);
71
72
const id =
@@ -73,35 +75,63 @@ export function lower(
75
: null;
76
77
const params: Array<Place> = [];
76
- func.node.params.forEach((param) => {
77
- todoInvariant(t.isIdentifier(param), "todo: support non-identifier params");
78
- const identifier = builder.resolveIdentifier(param);
79
- const place: Place = {
80
- kind: "Identifier",
81
- identifier,
82
- effect: Effect.Unknown,
83
- loc: param.loc ?? GeneratedSource,
84
- };
85
- params.push(place);
86
- });
78
+ for (const param of func.get("params")) {
79
+ if (param.isIdentifier()) {
80
+ const identifier = builder.resolveIdentifier(param.node);
81
+ const place: Place = {
82
+ kind: "Identifier",
83
+ identifier,
84
+ effect: Effect.Unknown,
85
+ loc: param.node.loc ?? GeneratedSource,
86
+ };
87
+ params.push(place);
88
+ } else {
89
+ builder.pushError({
90
+ reason: `Support non-identifier params: ${param.node.type}`,
91
+ severity: ErrorSeverity.Todo,
92
+ // 1. We can't use NodePath.toString() because we're using @babel/traverse directly, see
93
+ // https://fburl.com/45x18dcm for details.
94
+ // 2. Only NodePaths override toString to print the source, so if you tried this with
95
+ // Node.toString() you'd just get [object Object].
96
+ // 3. You will see this in other calls to builder.pushError in this file.
97
+ source: func.toString(),
98
+ loc: param.node.loc ?? null,
99
+ });
100
+ continue;
101
+ }
102
+ }
103
104
const body = func.get("body");
105
if (body.isExpression()) {
90
- todoInvariant(false, "TODO handle arrow functions");
106
+ builder.pushError({
107
+ reason: "Support arrow functions",
108
+ severity: ErrorSeverity.Todo,
109
+ source: func.toString(),
110
+ loc: body.node.loc ?? null,
111
+ });
112
} else if (body.isBlockStatement()) {
113
lowerStatement(builder, body);
114
} else {
94
- invariant(false, "Unexpected function body kind");
115
+ builder.pushError({
116
+ reason: `Unexpected function body kind: ${body.type}}`,
117
+ severity: ErrorSeverity.InvalidInput,
118
+ source: func.toString(),
119
+ loc: body.node.loc ?? null,
120
+ });
121
+ }
122
+
123
+ if (builder.hasErrors()) {
124
+ return Err(builder.errors);
125
}
126
97
- return {
127
+ return Ok({
128
id,
129
params,
130
body: builder.build(),
131
generator: func.node.generator === true,
132
async: func.node.async === true,
133
loc: func.node.loc ?? GeneratedSource,
104
- };
134
+ });
135
}
136
137
/**
@@ -323,11 +353,16 @@ function lowerStatement(
353
const continuationBlock = builder.reserve();
354
355
const initBlock = builder.enter("value", (blockId) => {
326
- const init = stmt.get("init") as NodePath<t.VariableDeclaration>;
327
- todoInvariant(
328
- t.isVariableDeclaration(init.node),
329
- "handle non variable initialization in for"
330
- );
356
+ const init = stmt.get("init");
357
+ if (!init.isVariableDeclaration()) {
358
+ builder.pushError({
359
+ reason: "Support non-variable initialization in for",
360
+ severity: ErrorSeverity.Todo,
361
+ source: stmt.toString(),
362
+ loc: init.node?.loc ?? null,
363
+ });
364
+ return { kind: "error", id: makeInstructionId(0) };
365
+ }
366
lowerStatement(builder, init);
367
return {
368
kind: "goto",
@@ -339,10 +374,16 @@ function lowerStatement(
374
375
const updateBlock = builder.enter("value", (blockId) => {
376
const update = stmt.get("update");
342
- todoInvariant(update.hasNode(), "Handle empty for updater");
343
- if (update.hasNode()) {
344
- lowerExpressionToVoid(builder, update);
377
+ if (!update.hasNode()) {
378
+ builder.pushError({
379
+ reason: "Handle empty for updater",
380
+ severity: ErrorSeverity.Todo,
381
+ source: stmt.toString(),
382
+ loc: null,
383
+ });
384
+ return { kind: "error", id: makeInstructionId(0) };
385
}
386
+ lowerExpressionToVoid(builder, update);
387
return {
388
kind: "goto",
389
block: testBlock.id,
@@ -378,19 +419,27 @@ function lowerStatement(
419
);
420
421
const test = stmt.get("test");
381
- todoInvariant(test.hasNode(), "ForStatement without test");
382
- builder.terminateWithContinuation(
383
- "value",
384
- {
385
- kind: "if",
386
- test: lowerExpressionToPlace(builder, test),
387
- consequent: bodyBlock,
388
- alternate: continuationBlock.id,
389
- fallthrough: continuationBlock.id,
390
- id: makeInstructionId(0),
391
- },
392
- continuationBlock
393
- );
422
+ if (!test.hasNode()) {
423
+ builder.pushError({
424
+ reason: "ForStatement without test",
425
+ severity: ErrorSeverity.Todo,
426
+ source: stmt.toString(),
427
+ loc: null,
428
+ });
429
+ } else {
430
+ builder.terminateWithContinuation(
431
+ "value",
432
+ {
433
+ kind: "if",
434
+ test: lowerExpressionToPlace(builder, test),
435
+ consequent: bodyBlock,
436
+ alternate: continuationBlock.id,
437
+ fallthrough: continuationBlock.id,
438
+ id: makeInstructionId(0),
439
+ },
440
+ continuationBlock
441
+ );
442
+ }
443
return;
444
}
445
case "DoWhileStatement": {
@@ -458,19 +507,27 @@ function lowerStatement(
507
* to evaluate whether to enter the loop or bypass to the continuation.
508
*/
509
const loc = stmt.node.loc;
461
- invariant(loc, "while statement must have a location");
462
- builder.terminateWithContinuation(
463
- "block",
464
- {
465
- kind: "while",
466
- loc,
467
- test: conditionalBlock.id,
468
- loop: loopBlock,
469
- fallthrough: continuationBlock.id,
470
- id: makeInstructionId(0),
471
- },
472
- conditionalBlock
473
- );
510
+ if (loc == null) {
511
+ builder.pushError({
512
+ reason: "while statement must have a location",
513
+ severity: ErrorSeverity.InvalidInput,
514
+ source: stmt.toString(),
515
+ loc: null,
516
+ });
517
+ } else {
518
+ builder.terminateWithContinuation(
519
+ "block",
520
+ {
521
+ kind: "while",
522
+ loc,
523
+ test: conditionalBlock.id,
524
+ loop: loopBlock,
525
+ fallthrough: continuationBlock.id,
526
+ id: makeInstructionId(0),
527
+ },
528
+ conditionalBlock
529
+ );
530
+ }
531
/**
532
* The conditional block is empty and exists solely as conditional for
533
* (re)entering or exiting the loop
@@ -544,10 +601,16 @@ function lowerStatement(
601
const case_: NodePath<t.SwitchCase> = stmt.get("cases")[ii];
602
const test = case_.get("test");
603
if (!test.hasNode()) {
547
- invariant(
548
- !hasDefault,
549
- "Expected at most one `default` branch, this code should have failed to parse"
550
- );
604
+ if (hasDefault) {
605
+ builder.pushError({
606
+ reason:
607
+ "Expected at most one `default` branch, this code should have failed to parse",
608
+ severity: ErrorSeverity.InvalidInput,
609
+ source: stmt.toString(),
610
+ loc: case_.node.loc ?? null,
611
+ });
612
+ break;
613
+ }
614
hasDefault = true;
615
}
616
const block = builder.enter("block", (_blockId) => {
@@ -623,10 +686,16 @@ function lowerStatement(
686
case "VariableDeclaration": {
687
const stmt = stmtPath as NodePath<t.VariableDeclaration>;
688
const nodeKind: string = stmt.node.kind;
626
- invariant(
627
- nodeKind === "let" || nodeKind === "const",
628
- "`var` declarations are not supported, use let or const"
629
- );
689
+ if (nodeKind === "var") {
690
+ builder.pushError({
691
+ reason: "`var` declarations are not supported, use let or const",
692
+ severity: ErrorSeverity.Todo,
693
+ source: stmt.toString(),
694
+ loc: stmt.node.loc ?? null,
695
+ });
696
+ // TODO: should we lower this to an error variant
697
+ return;
698
+ }
699
const kind =
700
nodeKind === "let" ? InstructionKind.Let : InstructionKind.Const;
701
for (const declaration of stmt.get("declarations")) {
compiler/forget/src/HIR/HIR.ts
+3
-1
@@ -167,6 +167,7 @@ export type BasicBlock = {
167
* for-of, if-else, return, etc.
168
*/
169
export type Terminal =
170
+ | ErrorTerminal
171
| ThrowTerminal
172
| ReturnTerminal
173
| GotoTerminal
@@ -180,7 +181,8 @@ export type Terminal =
181
* Terminal nodes allowed for a value block
182
*/
183
export type ValueTerminal = IfTerminal | GotoTerminal;
183
-
184
+// A terminal that couldn't be lowered correctly.
185
+export type ErrorTerminal = { kind: "error"; id: InstructionId };
186
export type ThrowTerminal = { kind: "throw"; value: Place; id: InstructionId };
187
188
export type ReturnTerminal = {
compiler/forget/src/HIR/HIRBuilder.ts
+15
@@ -7,6 +7,7 @@
7
8
import * as t from "@babel/types";
9
import invariant from "invariant";
10
+import { CompilerError, CompilerErrorOptions } from "../CompilerError";
11
import { logHIR } from "../Utils/logger";
12
import { assertExhaustive } from "../Utils/utils";
13
import {
@@ -83,6 +84,7 @@ export default class HIRBuilder {
84
#scopes: Array<Scope> = [];
85
#bindings: Map<t.Identifier, Identifier> = new Map();
86
#env: Environment;
87
+ errors: CompilerError[] = [];
88
89
get nextIdentifierId() {
90
return this.#env.nextIdentifierId;
@@ -365,6 +367,16 @@ export default class HIRBuilder {
367
}
368
invariant(false, "Expected a loop to be in scope");
369
}
370
+
371
+ pushError(options: CompilerErrorOptions): CompilerError {
372
+ const error = new CompilerError(options);
373
+ this.errors.push(error);
374
+ return error;
375
+ }
376
+
377
+ hasErrors(): boolean {
378
+ return this.errors.length > 0;
379
+ }
380
}
381
382
/**
@@ -515,6 +527,9 @@ export function reversePostorderBlocks(func: HIR): void {
527
visit(terminal.init);
528
break;
529
}
530
+ case "error": {
531
+ break;
532
+ }
533
default: {
534
assertExhaustive(
535
terminal,
compiler/forget/src/HIR/HIRTreeVisitor.ts
+2
@@ -485,6 +485,8 @@ class Driver<
485
}
486
break;
487
}
488
+ case "error":
489
+ break;
490
default: {
491
assertExhaustive(terminal, "Unexpected terminal");
492
}
compiler/forget/src/HIR/PrintHIR.ts
+5
@@ -82,6 +82,7 @@ export function printMixedHIR(
82
case "throw":
83
case "while":
84
case "for":
85
+ case "error":
86
case "goto": {
87
const terminal = printTerminal(value);
88
if (Array.isArray(terminal)) {
@@ -179,6 +180,10 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
180
value = `[${terminal.id}] For init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} update=bb${terminal.update} fallthrough=bb${terminal.fallthrough}`;
181
break;
182
}
183
+ case "error": {
184
+ value = `Error`;
185
+ break;
186
+ }
187
default: {
188
assertExhaustive(
189
terminal,
compiler/forget/src/HIR/visitors.ts
+9
-2
@@ -292,6 +292,9 @@ export function mapTerminalSuccessors(
292
id: makeInstructionId(0),
293
};
294
}
295
+ case "error": {
296
+ return terminal;
297
+ }
298
default: {
299
assertExhaustive(
300
terminal,
@@ -341,6 +344,8 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
344
yield terminal.init;
345
break;
346
}
347
+ case "error":
348
+ break;
349
default: {
350
assertExhaustive(
351
terminal,
@@ -379,7 +384,8 @@ export function mapTerminalOperands(
384
case "logical":
385
case "while":
386
case "for":
382
- case "goto": {
387
+ case "goto":
388
+ case "error": {
389
// no-op
390
break;
391
}
@@ -418,7 +424,8 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
424
case "logical":
425
case "while":
426
case "for":
421
- case "goto": {
427
+ case "goto":
428
+ case "error": {
429
// no-op
430
break;
431
}
compiler/forget/src/Utils/VisualizeHIRMermaid.ts
+5
-1
@@ -105,6 +105,9 @@ function printTerminalLabel(terminal: Terminal): string {
105
case "for":
106
buffer.push("For");
107
break;
108
+ case "error": {
109
+ break;
110
+ }
111
default:
112
assertExhaustive(terminal, `unhandled terminal ${terminal}`);
113
}
@@ -168,7 +171,8 @@ function printTerminalArrows(blockId: BlockId, terminal: Terminal): string {
171
break;
172
}
173
case "throw":
171
- case "return": {
174
+ case "return":
175
+ case "error": {
176
break;
177
}
178
default:
compiler/forget/src/Utils/types.d.ts
new
+3
@@ -0,0 +1,3 @@
1
+export type ExtractClassProperties<C> = {
2
+ [K in keyof C as C[K] extends Function ? never : K]: C[K];
3
+};
compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.expect.md
new
+106
@@ -0,0 +1,106 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
6
+ let i = 0;
7
+ var x = [];
8
+
9
+ for (; i < 3; i += 1) {
10
+ x.push(i);
11
+ }
12
+ for (; i < 3; ) {}
13
+ for (;;) {}
14
+}
15
+
16
+```
17
+
18
+
19
+## Error
20
+
21
+```
22
+Forget Error: [TODO] Support non-identifier params: ArrayPattern on lines 1:1
23
+function foo([a, b], {
24
+ c,
25
+ d,
26
+ e = "e"
27
+}, f = "f", ...args) {
28
+ let i = 0;
29
+ var x = [];
30
+ for (; i < 3; i += 1) {
31
+ x.push(i);
32
+ }
33
+ for (; i < 3;) {}
34
+ for (;;) {}
35
+}
36
+
37
+Forget Error: [TODO] Support non-identifier params: ObjectPattern on lines 1:1
38
+function foo([a, b], {
39
+ c,
40
+ d,
41
+ e = "e"
42
+}, f = "f", ...args) {
43
+ let i = 0;
44
+ var x = [];
45
+ for (; i < 3; i += 1) {
46
+ x.push(i);
47
+ }
48
+ for (; i < 3;) {}
49
+ for (;;) {}
50
+}
51
+
52
+Forget Error: [TODO] Support non-identifier params: AssignmentPattern on lines 1:1
53
+function foo([a, b], {
54
+ c,
55
+ d,
56
+ e = "e"
57
+}, f = "f", ...args) {
58
+ let i = 0;
59
+ var x = [];
60
+ for (; i < 3; i += 1) {
61
+ x.push(i);
62
+ }
63
+ for (; i < 3;) {}
64
+ for (;;) {}
65
+}
66
+
67
+Forget Error: [TODO] Support non-identifier params: RestElement on lines 1:1
68
+function foo([a, b], {
69
+ c,
70
+ d,
71
+ e = "e"
72
+}, f = "f", ...args) {
73
+ let i = 0;
74
+ var x = [];
75
+ for (; i < 3; i += 1) {
76
+ x.push(i);
77
+ }
78
+ for (; i < 3;) {}
79
+ for (;;) {}
80
+}
81
+
82
+Forget Error: [TODO] `var` declarations are not supported, use let or const on lines 3:3
83
+var x = [];
84
+
85
+Forget Error: [TODO] Support non-variable initialization in for
86
+for (; i < 3; i += 1) {
87
+ x.push(i);
88
+}
89
+
90
+Forget Error: [TODO] Support non-variable initialization in for
91
+for (; i < 3;) {}
92
+
93
+Forget Error: [TODO] Handle empty for updater
94
+for (; i < 3;) {}
95
+
96
+Forget Error: [TODO] Support non-variable initialization in for
97
+for (;;) {}
98
+
99
+Forget Error: [TODO] Handle empty for updater
100
+for (;;) {}
101
+
102
+Forget Error: [TODO] ForStatement without test
103
+for (;;) {}
104
+```
105
+
106
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/error.todo-kitchensink.js
new
+10
@@ -0,0 +1,10 @@
1
+function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
2
+ let i = 0;
3
+ var x = [];
4
+
5
+ for (; i < 3; i += 1) {
6
+ x.push(i);
7
+ }
8
+ for (; i < 3; ) {}
9
+ for (;;) {}
10
+}