Create proper validation library. Rewrite new-function-draft component.
This commit is contained in:
parent
0941756bf9
commit
8e4dcb5de7
8 changed files with 366 additions and 55 deletions
|
|
@ -2,7 +2,7 @@ import { Cursor } from './cursor';
|
|||
import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, skipWhitespaceAndComments } from './scanner';
|
||||
import { char, CodePoint, SourceText, Span } from './source_text';
|
||||
import { Result } from '../result';
|
||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, MatchBranch, Pattern, ProductPattern, SignalExpr } from '../expr';
|
||||
import { Expr, ExprBinding, FieldAssignment, FieldPattern, FunctionName, MatchBranch, Pattern, ProductPattern, SignalExpr } from '../expr';
|
||||
|
||||
// CONVENTION: Every parser is responsible to consume whitespace/comments at the end.
|
||||
// Every parser is not responsible for cleaning up whitespace/comments at the start - only the final `parse` that's exposed to the public.
|
||||
|
|
@ -492,8 +492,7 @@ function finishProductPattern(cursor: Cursor, token: PatternStartToken): Product
|
|||
switch (token.kw) {
|
||||
case ":": {
|
||||
// :( a = p, b )
|
||||
// TODO: parse open-paren
|
||||
if (!tryConsume(cursor, char('{'))) {
|
||||
if (!tryConsume(cursor, char('('))) {
|
||||
throw {
|
||||
tag: "ExpectedRecordPatternOpen",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
|
|
@ -598,3 +597,25 @@ export function parseFunctionParameters(source: SourceText): Result<ProductPatte
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export function parseFunctionName(source: SourceText): Result<FunctionName, ParseError> {
|
||||
const cursor = new Cursor(source);
|
||||
try {
|
||||
skipWhitespaceAndComments(cursor);
|
||||
// TODO: We should introduce new `function_name`
|
||||
const name = identifier(cursor, "function_call");
|
||||
|
||||
if (!cursor.eof()) {
|
||||
return Result.error({
|
||||
tag: "UnexpectedToken",
|
||||
expected: "EndOfFile",
|
||||
span: cursor.makeSpan(cursor.currentLocation())
|
||||
} as ParseError);
|
||||
}
|
||||
|
||||
return Result.ok(name.name);
|
||||
} catch (e) {
|
||||
// TODO: This is a bit sketchy. We maybe forced to have "checked" Exceptions for `ParseError` by wrapping it in something that has a proper tag.
|
||||
return Result.error(e as ParseError);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ type PrimitiveSignalDefinition = {
|
|||
|
||||
export namespace Program {
|
||||
|
||||
type Error =
|
||||
export type Error =
|
||||
| { tag: "DuplicateFunctionName", name: FunctionName }
|
||||
| { tag: "FunctionNotFound", name: FunctionName }
|
||||
| { tag: "CannotEditPrimitiveFunction", name: FunctionName }
|
||||
|
|
@ -98,7 +98,7 @@ export namespace Program {
|
|||
| { tag: "CannotDeletePrimitiveSignal", name: SignalName }
|
||||
| { tag: "PrimitiveSignalAlreadyExists", name: SignalName }
|
||||
|
||||
type Result<T> =
|
||||
export type Result<T> =
|
||||
| { tag: "ok", value: T }
|
||||
| { tag: "error", error: Error }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import { Digith } from "./Digith";
|
||||
import { useProgram } from "./ProgramProvider";
|
||||
|
||||
|
||||
export function NewFunctionDraftDigith(props: { draft: Digith.NewFunctionDraft }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [name, setName] = createSignal(props.draft.raw_name);
|
||||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const handleCommit = () => {
|
||||
// TODO: Add the new function to the draft
|
||||
console.log(`Committing ${name()} to the Program...`);
|
||||
};
|
||||
|
||||
// TODO: What about parsing errors?
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn Draft</strong></header>
|
||||
|
||||
<div>TODO: New Fn Draft under construction</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: What about renaming?
|
||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.function.raw_body);
|
||||
|
||||
const handleCommit = () => {
|
||||
// TODO: Update the old function with new code
|
||||
console.log(`Committing ${props.function.name} to the Program...`);
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn</strong></header>
|
||||
|
||||
<div>TODO: Fn under construction</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
32
src/ui/Function/FunctionDigith.tsx
Normal file
32
src/ui/Function/FunctionDigith.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
import { Digith } from "../Digith";
|
||||
import { useProgram } from "../ProgramProvider";
|
||||
import { CodeEditor } from "../CodeEditor";
|
||||
import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters } from "src/lang/parser/parser";
|
||||
import { sourceText, SourceText } from "src/lang/parser/source_text";
|
||||
import { Expr, FunctionName, ProductPattern } from "src/lang/expr";
|
||||
import { ShowParseError } from "../ParseError";
|
||||
import { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
|
||||
// TODO: What about renaming?
|
||||
export function FunctionDigith(props: { function: Digith.Function }) {
|
||||
const program = useProgram();
|
||||
|
||||
const [params, setParams] = createSignal(props.function.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.function.raw_body);
|
||||
|
||||
const handleCommit = () => {
|
||||
// TODO: Update the old function with new code
|
||||
console.log(`Committing ${props.function.name} to the Program...`);
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn</strong></header>
|
||||
|
||||
<div>TODO: Fn under construction</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
51
src/ui/Function/Helpers.tsx
Normal file
51
src/ui/Function/Helpers.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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 ===
|
||||
export function validateNameRaw(input: string): V<FunctionName, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionName(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateParamsRaw(input: string): V<ProductPattern[], ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseFunctionParameters(src);
|
||||
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
|
||||
};
|
||||
|
||||
export function validateExprRaw(input: string): V<Expr, ParseError> {
|
||||
const src = sourceText(input);
|
||||
const res = parseExpr(src);
|
||||
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>
|
||||
);
|
||||
}
|
||||
155
src/ui/Function/NewFunctionDraftDigith.tsx
Normal file
155
src/ui/Function/NewFunctionDraftDigith.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { createSignal, For, Match, Show, Switch } 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 { Program } from "src/lang/program";
|
||||
import { V, Validation, letValidate } from "../validation";
|
||||
import { ProgramErrorDisplay, validateExprRaw, validateNameRaw, validateParamsRaw } from "./Helpers";
|
||||
|
||||
|
||||
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"
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
const [name, setName] = createSignal(props.draft.raw_name);
|
||||
const [params, setParams] = createSignal(props.draft.raw_parameters);
|
||||
const [body, setBody] = createSignal(props.draft.raw_body);
|
||||
|
||||
const [validResult, setValidResult] = createSignal<V<void, NewFnError> | null>(null);
|
||||
|
||||
type Input = {
|
||||
raw_name: string,
|
||||
raw_params: string,
|
||||
raw_body: string,
|
||||
}
|
||||
|
||||
const validator: Validation<Input, void, 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) })),
|
||||
}),
|
||||
(fields, input) => {
|
||||
const createFunction: Program.CreateFunction = {
|
||||
name: fields.name,
|
||||
parameters: fields.parameters,
|
||||
body: fields.body,
|
||||
raw_parameters: input.raw_name,
|
||||
raw_body: input.raw_body,
|
||||
};
|
||||
|
||||
const regResult = Program.registerFunction(program, createFunction);
|
||||
if (regResult.tag === "ok") {
|
||||
// TODO: Side effects? Not sure about this. Ideally validator would be pure... but it is nice that we can return errors here.
|
||||
// But then again... these are not really normal errors, right? or? But that's probably a misuse...
|
||||
// For now we just return Ok
|
||||
return V.ok(undefined);
|
||||
} else {
|
||||
return V.error({ tag: "Program", err: regResult.error });
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: There's something wrong with this, it doesn't trigger when expected... WTF
|
||||
function handleCommit() {
|
||||
const result = validator({ raw_name: name(), raw_params: params(), raw_body: body() });
|
||||
setValidResult(result);
|
||||
if (result.tag === "ok") {
|
||||
// Handle success closure here if needed
|
||||
console.log("Function created successfully!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article>
|
||||
<header><strong>Fn Draft</strong></header>
|
||||
|
||||
<div class="grid">
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
placeholder="my_func"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Parameters (comma separated)
|
||||
<input
|
||||
type="text"
|
||||
placeholder="x, y"
|
||||
value={params()}
|
||||
onInput={(e) => setParams(e.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>Body</label>
|
||||
<CodeEditor
|
||||
value={body()}
|
||||
onUpdate={setBody}
|
||||
onRun={handleCommit}
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<button class="primary" onClick={handleCommit}>Commit</button>
|
||||
</footer>
|
||||
|
||||
<Show when={validResult()?.tag === "errors"}>
|
||||
<ErrorListDisplay
|
||||
errors={(validResult() as { tag: "errors", errors: NewFnError[] }).errors}
|
||||
/>
|
||||
</Show>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { For, Match, Switch } from "solid-js";
|
||||
import { ExprREPL } from "./REPL";
|
||||
import { scrowl } from "./scrowlStore";
|
||||
import { FunctionDigith, NewFunctionDraftDigith } from "./Function";
|
||||
import { NewFunctionDraftDigith } from "./Function/NewFunctionDraftDigith";
|
||||
import { FunctionDigith } from "./Function/FunctionDigith";
|
||||
import { Digith } from "./Digith";
|
||||
|
||||
// WTF are these names?
|
||||
|
|
|
|||
100
src/ui/validation.ts
Normal file
100
src/ui/validation.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Applicative Validation library.
|
||||
// Validation Result
|
||||
// `letValidate` is the main construct. It takes an input, runs parallel validations on fields, and if all succeed, runs a body function.
|
||||
// It came up as first trying to design a language from scratch where validation would be nice from the start:
|
||||
// ```
|
||||
// let-validate { { raw_username, raw_pwd, raw_pwd_check .
|
||||
// username := lengthAtleast(5, raw_username) else { err . #username :( msg = "too-short", err ) },
|
||||
// pwd := lengthAtleast(8, raw_pwd) else { err1 . #pwd :( msg = "too-short", err ) },
|
||||
// check := lengthAtleast(8, raw_pwd_check) else { err1 . #pwd :( msg = "check too-short", err ) },
|
||||
// .
|
||||
// if pwd == check {
|
||||
// ok :( username, pwd )
|
||||
// } else {
|
||||
// err #pwd :( msg = "pwd and check don't match" )
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
// and the below is the result of trying to port it to `ts`.
|
||||
// Note that `else` became `V.mapError`
|
||||
|
||||
export type V<T, E> =
|
||||
| { tag: "ok", value: T }
|
||||
| { tag: "errors", errors: E[] }
|
||||
|
||||
export type Validation<A, B, E> = (input: A) => V<B, E>
|
||||
|
||||
export namespace V {
|
||||
export function ok<T, E>(value: T): V<T, E> { return { tag: "ok", value } }
|
||||
export function errors<T, E>(errors: E[]): V<T, E> { return { tag: "errors", errors } }
|
||||
export function error<T, E>(error: E): V<T, E> { return { tag: "errors", errors: [error] } }
|
||||
|
||||
// basically just map-error
|
||||
export function elseErr<A, E0, E1>(result: V<A, E0>, f: (e0: E0) => E1): V<A, E1> {
|
||||
if (result.tag === "ok") {
|
||||
return V.ok(result.value);
|
||||
} else {
|
||||
const errors1 = [];
|
||||
for (const e0 of result.errors) {
|
||||
errors1.push(f(e0));
|
||||
}
|
||||
return V.errors(errors1);
|
||||
}
|
||||
}
|
||||
|
||||
export function map<A, B, C, E>(
|
||||
v: Validation<A, B, E>,
|
||||
f: (val: B) => C
|
||||
): Validation<A, C, E> {
|
||||
return (input: A) => {
|
||||
const res = v(input);
|
||||
return res.tag === "ok" ? V.ok(f(res.value)) : res;
|
||||
};
|
||||
}
|
||||
|
||||
export function pure<A, B>(f: (x: A) => B): Validation<A, B, never>{
|
||||
return (x: A) => { return V.ok(f(x)) };
|
||||
}
|
||||
}
|
||||
|
||||
// Example:
|
||||
// letValidate(
|
||||
// (input: LoginForm) => ({
|
||||
// // Bindings: Run validators on input fields
|
||||
// username: minLength(3)(input.u),
|
||||
// password: minLength(8)(input.p),
|
||||
// }),
|
||||
// // Body: Runs only if bindings succeed. 'fields' has the validated types.
|
||||
// (fields) => {
|
||||
// return V.ok({
|
||||
// username: fields.username.toLowerCase(),
|
||||
// passwordHash: hash(fields.password)
|
||||
// });
|
||||
// }
|
||||
// )
|
||||
export function letValidate<A, T extends Record<string, any>, E, B>(
|
||||
bindings: (input: A) => { [K in keyof T]: V<T[K], E> },
|
||||
body: (fields: T, input: A) => V<B, E>
|
||||
): Validation<A, B, E>{
|
||||
return (input: A): V<B, E> => {
|
||||
const results: Partial<T> = {};
|
||||
const allErrors: E[] = [];
|
||||
const args = bindings(input);
|
||||
for (const key in args) {
|
||||
const result = args[key];
|
||||
if (result.tag === 'ok') {
|
||||
results[key] = result.value;
|
||||
} else {
|
||||
allErrors.push(...result.errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
const e = V.errors(allErrors) as V<B, E>;
|
||||
return e;
|
||||
} else {
|
||||
const x = body(results as T, input);
|
||||
return x;
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue