@samitouri / QOS-React-2 / commits / 7c65f62c27

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 +}