Refactor CompilerError

This commit repurposes CompilerError to represent an aggregate of error details 
accumulated during HIR lowering. It also fixes the playground to correctly 
render errors again.
This commit is contained in:
Lauren Tan
2023-01-24 12:44:51 -05:00
parent 3da8d52a0f
commit 109d7ba980
7 changed files with 85 additions and 51 deletions
@@ -146,8 +146,12 @@ function Output({ store }: Props) {
const compilerOutput = useMemo(() => compile(store.source), [store.source]);
if (typeof compilerOutput === "string") {
if (compilerOutput === "") return <div></div>;
return <div>error: ${compilerOutput}</div>;
if (compilerOutput === "") return <></>;
return (
<pre>
<code>${compilerOutput}</code>
</pre>
);
}
return (
+59 -33
View File
@@ -1,5 +1,4 @@
import { Node, NodePath } from "@babel/core";
import type { ExtractClassProperties } from "./Utils/types";
import { assertExhaustive } from "./Utils/utils";
export enum ErrorSeverity {
@@ -7,9 +6,24 @@ export enum ErrorSeverity {
Todo = "Todo",
}
export type CompilerErrorOptions = ExtractClassProperties<CompilerError>;
export type CompilerErrorOptions = {
reason: string;
severity: ErrorSeverity;
nodePath: AnyNodePath | null;
};
type AnyNodePath = NodePath<Node | null | undefined>;
type CompilerErrorKind = typeof InvalidInputError | typeof TodoError;
function mapSeverityToErrorCtor(severity: ErrorSeverity): CompilerErrorKind {
switch (severity) {
case ErrorSeverity.InvalidInput:
return InvalidInputError;
case ErrorSeverity.Todo:
return TodoError;
default:
assertExhaustive(severity, `Unhandled severity level: ${severity}`);
}
}
class InvalidInputError extends Error {
constructor(message: string) {
super(message);
@@ -23,30 +37,13 @@ class TodoError extends Error {
}
}
function mapSeverityToErrorCtor(severity: ErrorSeverity) {
switch (severity) {
case ErrorSeverity.InvalidInput:
return InvalidInputError;
case ErrorSeverity.Todo:
return TodoError;
default:
assertExhaustive(severity, `Unhandled severity level: ${severity}`);
}
}
function printPathCodeFrame(
reason: string,
severity: ErrorSeverity,
path: AnyNodePath
) {
return path
.buildCodeFrameError(reason, mapSeverityToErrorCtor(severity))
.toString();
}
export class CompilerError {
severity: ErrorSeverity;
/**
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
* aggregated into a single {@link CompilerError} later.
*/
export class CompilerErrorDetail {
reason: string;
severity: ErrorSeverity;
/**
* If a NodePath is provided, we will prefer Babel's built in codeframe error generation which
* will print error markers in the correct location.
@@ -54,20 +51,49 @@ export class CompilerError {
nodePath: AnyNodePath | null;
constructor(options: CompilerErrorOptions) {
this.severity = options.severity;
this.reason = options.reason;
this.severity = options.severity;
this.nodePath = options.nodePath;
}
toString(): string {
const buffer = [];
if (this.nodePath != null) {
get errorMessage(): string {
const buffer = [`${this.severity}: ${this.reason}`];
if (this.nodePath != null && this.nodePath.node?.loc != null) {
buffer.push(
printPathCodeFrame(this.reason, this.severity, this.nodePath)
` (${this.nodePath.node.loc.start.line}:${this.nodePath.node.loc.end.line})`
);
} else {
buffer.push(`${this.severity}: ${this.reason}`);
}
return `[ReactForget] ${buffer.join("")}`;
return buffer.join("");
}
get codeFrame() {
if (this.nodePath == null) {
return this.errorMessage;
}
try {
return this.nodePath
.buildCodeFrameError(this.reason, mapSeverityToErrorCtor(this.severity))
.toString();
} catch {
return this.errorMessage;
}
}
toString(): string {
return `[ReactForget] ${this.errorMessage}`;
}
}
export class CompilerError extends Error {
details: CompilerErrorDetail[] = [];
constructor(details: CompilerErrorDetail[], ...args: any[]) {
super(...args);
this.details = details;
this.message = this.toString();
}
override toString() {
return this.details.map((detail) => detail.toString()).join("\n\n");
}
}
+3 -4
View File
@@ -6,8 +6,8 @@
*/
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerError } from "./CompilerError";
import {
Environment,
HIRFunction,
lower,
mergeConsecutiveBlocks,
@@ -46,9 +46,8 @@ export type CompilerPipelineValue =
export function* run(
func: NodePath<t.FunctionDeclaration>
): Generator<CompilerPipelineValue, t.Function> {
const lowering = lower(func).orElse((errors) => {
const msg = errors.map((error) => error.toString()).join("\n\n");
throw new Error(msg);
const lowering = lower(func).orElse((details) => {
throw new CompilerError(details);
});
const hir = lowering.unwrap();
+2 -2
View File
@@ -7,7 +7,7 @@
import { NodePath, Scope } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerError, ErrorSeverity } from "../CompilerError";
import { CompilerErrorDetail, ErrorSeverity } from "../CompilerError";
import { Err, Ok, Result } from "../lib/Result";
import { assertExhaustive } from "../Utils/utils";
import {
@@ -47,7 +47,7 @@ import HIRBuilder, { Environment } from "./HIRBuilder";
*/
export function lower(
func: NodePath<t.Function>
): Result<HIRFunction, CompilerError[]> {
): Result<HIRFunction, CompilerErrorDetail[]> {
const env = new Environment();
const builder = new HIRBuilder(env);
+6 -6
View File
@@ -8,7 +8,7 @@
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import invariant from "invariant";
import { CompilerError, CompilerErrorOptions } from "../CompilerError";
import { CompilerErrorDetail, CompilerErrorOptions } from "../CompilerError";
import { logHIR } from "../Utils/logger";
import { assertExhaustive } from "../Utils/utils";
import { getOrAddGlobal } from "./Globals";
@@ -87,7 +87,7 @@ export default class HIRBuilder {
#bindings: Map<string, { node: t.Identifier; identifier: Identifier }> =
new Map();
#env: Environment;
errors: CompilerError[] = [];
errors: CompilerErrorDetail[] = [];
get nextIdentifierId() {
return this.#env.nextIdentifierId;
@@ -415,10 +415,10 @@ export default class HIRBuilder {
invariant(false, "Expected a loop to be in scope");
}
pushError(options: CompilerErrorOptions): CompilerError {
const error = new CompilerError(options);
this.errors.push(error);
return error;
pushError(options: CompilerErrorOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail(options);
this.errors.push(detail);
return detail;
}
hasErrors(): boolean {
+8 -4
View File
@@ -10,6 +10,7 @@
import { wasmFolder } from "@hpcc-js/wasm";
import path from "path";
import runReactForgetBabelPlugin from "../Babel/RunReactForgetBabelPlugin";
import { CompilerError } from "../CompilerError";
import { toggleLogging } from "../Utils/logger";
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
@@ -98,10 +99,13 @@ ${outputs.join("\n")}
});
function formatErrorOutput(error: Error): string {
// Babel outputs absolute paths of the filename in the error mesage, which means fixtures will
// contain paths that only pertain to your local machine. Strip it just here because that info
// is still useful in real world usage of the Babel plugin.
error.message = error.message.replace(/^\/.*?:\s/, "");
if (error instanceof CompilerError) {
error.message = error.details
.map((detail) => `[ReactForget] ${detail.codeFrame}`)
.join("\n\n");
} else {
error.message = error.message.replace(/^\/.*?:\s/, "");
}
return `
## Error
+1
View File
@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
export { default as BabelPlugin } from "./Babel/BabelPlugin";
export { ErrorSeverity } from "./CompilerError";
export { compile, run } from "./CompilerPipeline";
export { printHIR } from "./HIR";
export { printReactiveFunction } from "./ReactiveScopes";