diff --git a/compiler/forget/packages/playground/components/Editor/Output.tsx b/compiler/forget/packages/playground/components/Editor/Output.tsx
index 9ba6de1b82..314d9fe59e 100644
--- a/compiler/forget/packages/playground/components/Editor/Output.tsx
+++ b/compiler/forget/packages/playground/components/Editor/Output.tsx
@@ -146,8 +146,12 @@ function Output({ store }: Props) {
const compilerOutput = useMemo(() => compile(store.source), [store.source]);
if (typeof compilerOutput === "string") {
- if (compilerOutput === "") return
;
- return error: ${compilerOutput}
;
+ if (compilerOutput === "") return <>>;
+ return (
+
+ ${compilerOutput}
+
+ );
}
return (
diff --git a/compiler/forget/src/CompilerError.ts b/compiler/forget/src/CompilerError.ts
index 3be1020c08..802c0d579e 100644
--- a/compiler/forget/src/CompilerError.ts
+++ b/compiler/forget/src/CompilerError.ts
@@ -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;
+export type CompilerErrorOptions = {
+ reason: string;
+ severity: ErrorSeverity;
+ nodePath: AnyNodePath | null;
+};
type AnyNodePath = NodePath;
+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");
}
}
diff --git a/compiler/forget/src/CompilerPipeline.ts b/compiler/forget/src/CompilerPipeline.ts
index deaeef6d30..0f70eeaf21 100644
--- a/compiler/forget/src/CompilerPipeline.ts
+++ b/compiler/forget/src/CompilerPipeline.ts
@@ -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
): Generator {
- 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();
diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts
index 6c7dec45e8..9a69f99560 100644
--- a/compiler/forget/src/HIR/BuildHIR.ts
+++ b/compiler/forget/src/HIR/BuildHIR.ts
@@ -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
-): Result {
+): Result {
const env = new Environment();
const builder = new HIRBuilder(env);
diff --git a/compiler/forget/src/HIR/HIRBuilder.ts b/compiler/forget/src/HIR/HIRBuilder.ts
index 831a18ea74..04c321366a 100644
--- a/compiler/forget/src/HIR/HIRBuilder.ts
+++ b/compiler/forget/src/HIR/HIRBuilder.ts
@@ -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 =
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 {
diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts
index 9880e80b44..6d6bb4fe7c 100644
--- a/compiler/forget/src/__tests__/hir-test.ts
+++ b/compiler/forget/src/__tests__/hir-test.ts
@@ -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
diff --git a/compiler/forget/src/index.ts b/compiler/forget/src/index.ts
index a3c6d8e2e3..a2ca50dc0c 100644
--- a/compiler/forget/src/index.ts
+++ b/compiler/forget/src/index.ts
@@ -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";