Improve errors in digiths
This commit is contained in:
parent
b0280b9d74
commit
e841106029
5 changed files with 213 additions and 156 deletions
|
|
@ -294,10 +294,18 @@ export namespace Program {
|
|||
raw_body: string,
|
||||
}
|
||||
|
||||
export function getFunction(program: Program, name: FunctionName): Result<FunctionDefinition> {
|
||||
const fn = program.function_definitions.get(name);
|
||||
if (fn === undefined) {
|
||||
return Result.error({ tag: "FunctionNotFound", name });
|
||||
}
|
||||
return Result.ok(fn);
|
||||
}
|
||||
|
||||
export function registerFunction(
|
||||
program: Program,
|
||||
{ name, body, parameters, raw_parameters, raw_body }: CreateFunction
|
||||
): Result<void> {
|
||||
): Result<FunctionName> {
|
||||
if (program.function_definitions.has(name)) {
|
||||
return Result.error({ tag: "DuplicateFunctionName", name });
|
||||
}
|
||||
|
|
@ -317,13 +325,12 @@ export namespace Program {
|
|||
program.function_definitions.set(name, { tag: "user", def: newFunction });
|
||||
program.function_definition_order.push(name);
|
||||
|
||||
return Result.ok(undefined);
|
||||
return Result.ok(name);
|
||||
}
|
||||
|
||||
export type UpdateFunction = {
|
||||
parameters: ProductPattern[],
|
||||
body: Expr,
|
||||
raw_name: string,
|
||||
raw_parameters: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
|
|
|||
134
src/ui/DigithError.tsx
Normal file
134
src/ui/DigithError.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { For, Match, Show, Switch } from "solid-js";
|
||||
import { ParseError } from "src/lang/parser/parser";
|
||||
import { SourceText } from "src/lang/parser/source_text";
|
||||
import { ShowParseError } from "./ParseError";
|
||||
import { Program } from "src/lang/program";
|
||||
|
||||
export type DigithError = {
|
||||
payload: DigithError.Payload,
|
||||
ids: DigithError.Id[],
|
||||
tags: DigithError.Tag[],
|
||||
config: DigithError.Config,
|
||||
}
|
||||
|
||||
export namespace DigithError {
|
||||
export type Payload =
|
||||
| { tag: "Parse", err: ParseError, src: SourceText }
|
||||
| { tag: "Program", err: Program.Error };
|
||||
|
||||
export type Id = string;
|
||||
export type Tag = string;
|
||||
|
||||
export type Config = {
|
||||
title?: string,
|
||||
display?: "box" | "flat",
|
||||
}
|
||||
|
||||
function findById(errors: DigithError[], id: Id): DigithError | undefined {
|
||||
return errors.find((e) => e.ids.includes(id));
|
||||
}
|
||||
|
||||
function allWithTag(errors: DigithError[], tag: Tag): DigithError[] {
|
||||
return errors.filter((e) => e.tags.includes(tag));
|
||||
}
|
||||
|
||||
export function All(props: { errors: DigithError[] }) {
|
||||
return (
|
||||
<div class="digith-errors-container">
|
||||
<For each={props.errors}>
|
||||
{(error) => <Single error={error} />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ById(props: { errors: DigithError[], id: Id }) {
|
||||
const error = () => findById(props.errors, props.id);
|
||||
return (
|
||||
<Show when={error()}>
|
||||
{(e) => <Single error={e()} />}
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export function ByTag(props: { errors: DigithError[], tag: Tag }) {
|
||||
const matched = () => allWithTag(props.errors, props.tag);
|
||||
return (
|
||||
<Show when={matched().length > 0}>
|
||||
<All errors={matched()} />
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
function Single(props: { error: DigithError }) {
|
||||
const display = () => props.error.config.display ?? "box";
|
||||
return (
|
||||
<div style={{ "margin-bottom": display() === "box" ? "1rem" : "0.5rem" }} >
|
||||
<Switch>
|
||||
<Match when={display() === "box"}>
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem", margin: 0 }}>
|
||||
<Show when={props.error.config.title}>
|
||||
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
{props.error.config.title}
|
||||
</header>
|
||||
</Show>
|
||||
<PayloadView payload={props.error.payload} />
|
||||
</article>
|
||||
</Match>
|
||||
|
||||
<Match when={display() === "flat"}>
|
||||
<div style={{ color: "var(--pico-del-color)" }}>
|
||||
<Show when={props.error.config.title}>
|
||||
<small style={{ "font-weight": "bold", display: "block" }}>{props.error.config.title}:</small>
|
||||
</Show>
|
||||
<PayloadView payload={props.error.payload} />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PayloadView(props: { payload: Payload }) {
|
||||
return (
|
||||
<Switch>
|
||||
<Match
|
||||
when={props.payload.tag === "Parse" ? (props.payload as Extract<Payload, { tag: "Parse" }>) : undefined}
|
||||
>
|
||||
{(err) => <ShowParseError text={err().src} err={err().err} />}
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={props.payload.tag === "Program" ? (props.payload as Extract<Payload, { tag: "Program" }>) : undefined}
|
||||
>
|
||||
{(err) => <ProgramErrorDisplay error={err().err} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function ProgramErrorDisplay(props: { error: Program.Error }) {
|
||||
const message = () => {
|
||||
switch (props.error.tag) {
|
||||
case "DuplicateFunctionName":
|
||||
return `A function named '${props.error.name}' already exists.`;
|
||||
case "PrimitiveFunctionAlreadyExists":
|
||||
return `Cannot overwrite the primitive function '${props.error.name}'.`;
|
||||
// TODO: handle other cases
|
||||
default:
|
||||
return `Runtime Error: ${props.error.tag}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<small style={{ color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
Registration Failed
|
||||
</small>
|
||||
<p style={{ margin: 0 }}>{message()}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,23 +1,33 @@
|
|||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Digith } from "../Digith";
|
||||
import { useProgram } from "../ProgramProvider";
|
||||
import { CodeEditor } from "../CodeEditor";
|
||||
import { ParseError } from "src/lang/parser/parser";
|
||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||
import { ShowParseError } from "../ParseError";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, letValidate } from "../validation";
|
||||
import { ProgramErrorDisplay, validateExprRaw, validateParamsRaw } from "./Helpers";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
import { validateExprRaw, validateParamsRaw } from "./Helpers";
|
||||
import { updateDigith } from "../scrowlStore";
|
||||
import { DigithError } from "../DigithError";
|
||||
|
||||
type UpdateFnError =
|
||||
| { tag: "Parse", field: "params" | "body", err: ParseError, src: SourceText }
|
||||
| { tag: "Program", err: Program.Error }
|
||||
type Input = {
|
||||
raw_params: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator = letValidate(
|
||||
(input: { raw_params: string, raw_body: string }) => ({
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
||||
const validator: Validation<Input, Program.UpdateFunction, DigithError> = letValidate(
|
||||
(input) => ({
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
|
||||
payload: { tag: "Parse", field: "params", err, src: sourceText(input.raw_params) },
|
||||
ids: ["params"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Parameters" },
|
||||
})),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Body" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
return V.ok({
|
||||
|
|
@ -29,48 +39,6 @@ const validator = letValidate(
|
|||
}
|
||||
);
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
params: "Parameters",
|
||||
body: "Function Body"
|
||||
};
|
||||
|
||||
function SingleErrorDisplay(props: { error: UpdateFnError }) {
|
||||
return (
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<Switch>
|
||||
<Match
|
||||
when={props.error.tag === "Parse" ? (props.error as Extract<UpdateFnError, { tag: "Parse" }>) : undefined}
|
||||
>
|
||||
{(err) => (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
{fieldLabels[err().field]} Error
|
||||
</header>
|
||||
<ShowParseError text={err().src} err={err().err} />
|
||||
</article>
|
||||
)}
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={props.error.tag === "Program" ? (props.error as Extract<UpdateFnError, { tag: "Program" }>) : undefined}
|
||||
>
|
||||
{(err) => ( <ProgramErrorDisplay error={err().err} />)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorListDisplay(props: { errors: UpdateFnError[] }) {
|
||||
return (
|
||||
<div style={{ "margin-top": "2rem", "border-top": "1px solid var(--pico-muted-border-color)", "padding-top": "1rem" }}>
|
||||
<For each={props.errors}>
|
||||
{(error) => <SingleErrorDisplay error={error} />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: What about renaming?
|
||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||
const program = useProgram();
|
||||
|
|
@ -78,7 +46,7 @@ export function FunctionDigith(props: { function: Digith.Function }) {
|
|||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.function.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<UpdateFnError[]>([]);
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
const isDirty = () =>
|
||||
params() !== props.function.raw_parameters ||
|
||||
|
|
@ -89,7 +57,7 @@ export function FunctionDigith(props: { function: Digith.Function }) {
|
|||
|
||||
const validRes = validator({ raw_params: params(), raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors as UpdateFnError[]);
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const updateData = validRes.value;
|
||||
|
|
@ -102,7 +70,12 @@ export function FunctionDigith(props: { function: Digith.Function }) {
|
|||
});
|
||||
|
||||
if (progRes.tag === "error") {
|
||||
setErrors([{ tag: "Program", err: progRes.error }]);
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: progRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Update Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,9 +138,9 @@ export function FunctionDigith(props: { function: Digith.Function }) {
|
|||
|
||||
</footer>
|
||||
|
||||
<Show when={errors().length > 0}>
|
||||
<ErrorListDisplay errors={ errors() } />
|
||||
</Show>
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters } from "src/lang/parser/parser";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Expr, FunctionName, ProductPattern } from "src/lang/expr";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V } from "../validation";
|
||||
|
||||
// === Parser wrappers ===
|
||||
|
|
@ -23,29 +22,3 @@ export function validateExprRaw(input: string): V<Expr, ParseError> {
|
|||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
|
||||
// === Displaying Errors ===
|
||||
|
||||
// TODO: Move this into more appropriate place
|
||||
export function ProgramErrorDisplay(props: { error: Program.Error }) {
|
||||
const message = () => {
|
||||
switch (props.error.tag) {
|
||||
case "DuplicateFunctionName":
|
||||
return `A function named '${props.error.name}' already exists.`;
|
||||
case "PrimitiveFunctionAlreadyExists":
|
||||
return `Cannot overwrite the primitive function '${props.error.name}'.`;
|
||||
// TODO: handle other cases
|
||||
default:
|
||||
return `Runtime Error: ${props.error.tag}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<small style={{ color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
Registration Failed
|
||||
</small>
|
||||
<p style={{ margin: 0 }}>{message()}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,13 @@
|
|||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
import { Digith } from "../Digith";
|
||||
import { useProgram } from "../ProgramProvider";
|
||||
import { CodeEditor } from "../CodeEditor";
|
||||
import { ParseError } from "src/lang/parser/parser";
|
||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||
import { ShowParseError } from "../ParseError";
|
||||
import { sourceText } from "src/lang/parser/source_text";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
import { ProgramErrorDisplay, validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
||||
import { validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
||||
import { spawnFunctionDigith } from "../scrowlStore";
|
||||
|
||||
|
||||
type NewFnError =
|
||||
| { tag: "Parse", field: "name" | "params" | "body", err: ParseError, src: SourceText }
|
||||
| { tag: "Program", err: Program.Error };
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
name: "Function Name",
|
||||
params: "Parameters",
|
||||
body: "Function Body"
|
||||
};
|
||||
import { DigithError } from "../DigithError";
|
||||
|
||||
type Input = {
|
||||
raw_name: string,
|
||||
|
|
@ -27,11 +15,26 @@ type Input = {
|
|||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, Program.CreateFunction, NewFnError> = letValidate(
|
||||
(input: Input) =>({
|
||||
name: V.elseErr(validateNameRaw(input.raw_name), err => ({ tag: "Parse", field: "name", err, src: sourceText(input.raw_name) })),
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({ tag: "Parse", field: "params", err, src: sourceText(input.raw_params) })),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({ tag: "Parse", field: "body", err, src: sourceText(input.raw_body) })),
|
||||
const validator: Validation<Input, Program.CreateFunction, DigithError> = letValidate(
|
||||
(input) =>({
|
||||
name: V.elseErr(validateNameRaw(input.raw_name), err =>({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_name) },
|
||||
ids: ["name"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Name", display: "flat" },
|
||||
})),
|
||||
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_params) },
|
||||
ids: ["params"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Parameters", display: "flat" },
|
||||
})),
|
||||
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
|
||||
payload: { tag: "Parse", err, src: sourceText(input.raw_body) },
|
||||
ids: ["body"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Function Body", display: "flat" },
|
||||
})),
|
||||
}),
|
||||
(fields, input) => {
|
||||
const createFunction: Program.CreateFunction = {
|
||||
|
|
@ -44,44 +47,6 @@ const validator: Validation<Input, Program.CreateFunction, NewFnError> = letVali
|
|||
return V.ok(createFunction);
|
||||
})
|
||||
|
||||
export function SingleErrorDisplay(props: { error: NewFnError }) {
|
||||
return (
|
||||
<div style={{ "margin-bottom": "1rem" }}>
|
||||
<Switch>
|
||||
<Match
|
||||
when={props.error.tag === "Parse" ? (props.error as Extract<NewFnError, { tag: "Parse" }>) : undefined}
|
||||
>
|
||||
{(err) => (
|
||||
<article style={{ border: "1px solid var(--pico-del-color)", padding: "0.5rem 1rem" }}>
|
||||
<header style={{ "margin-bottom": "0.5rem", color: "var(--pico-del-color)", "font-weight": "bold" }}>
|
||||
{fieldLabels[err().field]} Error
|
||||
</header>
|
||||
<ShowParseError text={err().src} err={err().err} />
|
||||
</article>
|
||||
)}
|
||||
</Match>
|
||||
|
||||
<Match
|
||||
when={props.error.tag === "Program" ? (props.error as Extract<NewFnError, { tag: "Program" }>) : undefined}
|
||||
>
|
||||
{(err) => ( <ProgramErrorDisplay error={err().err} />)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorListDisplay(props: { errors: NewFnError[] }) {
|
||||
return (
|
||||
<div style={{ "margin-top": "2rem", "border-top": "1px solid var(--pico-muted-border-color)", "padding-top": "1rem" }}>
|
||||
<For each={props.errors}>
|
||||
{(error) => <SingleErrorDisplay error={error} />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }) {
|
||||
const program = useProgram();
|
||||
|
||||
|
|
@ -89,20 +54,25 @@ export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }
|
|||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const [errors, setErrors] = createSignal<NewFnError[]>([]);
|
||||
const [errors, setErrors] = createSignal<DigithError[]>([]);
|
||||
|
||||
function handleCommit() {
|
||||
setErrors([]);
|
||||
const validRes = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
||||
if (validRes.tag === "errors") {
|
||||
setErrors(validRes.errors as NewFnError[]);
|
||||
setErrors(validRes.errors);
|
||||
return;
|
||||
}
|
||||
const createFunction = validRes.value;
|
||||
|
||||
const programRes = Program.registerFunction(program, createFunction);
|
||||
if (programRes.tag === "error") {
|
||||
setErrors([{ tag: "Program", err: programRes.error }]);
|
||||
setErrors([{
|
||||
payload: { tag: "Program", err: programRes.error },
|
||||
ids: ["program"],
|
||||
tags: ["footer"],
|
||||
config: { title: "Registration Failed" },
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
const fnName = programRes.value;
|
||||
|
|
@ -146,9 +116,9 @@ export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }
|
|||
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||
</footer>
|
||||
|
||||
<Show when={errors().length > 0}>
|
||||
<ErrorListDisplay errors={errors()} />
|
||||
</Show>
|
||||
<div style={{ "margin-top": "1rem" }}>
|
||||
<DigithError.ByTag errors={errors()} tag="footer" />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue