From 8e4dcb5de72d0fea7786927af2b52731b3661784 Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:59:49 +0100 Subject: [PATCH] Create proper validation library. Rewrite new-function-draft component. --- src/lang/parser/parser.ts | 27 +++- src/lang/program.ts | 4 +- src/ui/Function.tsx | 49 ------- src/ui/Function/FunctionDigith.tsx | 32 +++++ src/ui/Function/Helpers.tsx | 51 +++++++ src/ui/Function/NewFunctionDraftDigith.tsx | 155 +++++++++++++++++++++ src/ui/Scrowl.tsx | 3 +- src/ui/validation.ts | 100 +++++++++++++ 8 files changed, 366 insertions(+), 55 deletions(-) create mode 100644 src/ui/Function/FunctionDigith.tsx create mode 100644 src/ui/Function/Helpers.tsx create mode 100644 src/ui/Function/NewFunctionDraftDigith.tsx create mode 100644 src/ui/validation.ts diff --git a/src/lang/parser/parser.ts b/src/lang/parser/parser.ts index a6a29ee..194bd11 100644 --- a/src/lang/parser/parser.ts +++ b/src/lang/parser/parser.ts @@ -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 { + 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); + } +} diff --git a/src/lang/program.ts b/src/lang/program.ts index 29f6235..d05b1c9 100644 --- a/src/lang/program.ts +++ b/src/lang/program.ts @@ -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 = + export type Result = | { tag: "ok", value: T } | { tag: "error", error: Error } diff --git a/src/ui/Function.tsx b/src/ui/Function.tsx index 933e1cb..e69de29 100644 --- a/src/ui/Function.tsx +++ b/src/ui/Function.tsx @@ -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 ( -
-
Fn Draft
- -
TODO: New Fn Draft under construction
-
- ); -} - -// 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 ( -
-
Fn
- -
TODO: Fn under construction
-
- ); -} - diff --git a/src/ui/Function/FunctionDigith.tsx b/src/ui/Function/FunctionDigith.tsx new file mode 100644 index 0000000..7dd0d8d --- /dev/null +++ b/src/ui/Function/FunctionDigith.tsx @@ -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 ( +
+
Fn
+ +
TODO: Fn under construction
+
+ ); +} + diff --git a/src/ui/Function/Helpers.tsx b/src/ui/Function/Helpers.tsx new file mode 100644 index 0000000..bd0a41a --- /dev/null +++ b/src/ui/Function/Helpers.tsx @@ -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 { + 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 { + 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 { + 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 ( +
+ + Registration Failed + +

{message()}

+
+ ); +} diff --git a/src/ui/Function/NewFunctionDraftDigith.tsx b/src/ui/Function/NewFunctionDraftDigith.tsx new file mode 100644 index 0000000..b5e7f4a --- /dev/null +++ b/src/ui/Function/NewFunctionDraftDigith.tsx @@ -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 = { + name: "Function Name", + params: "Parameters", + body: "Function Body" +}; + +export function SingleErrorDisplay(props: { error: NewFnError }) { + return ( +
+ + ) : undefined} + > + {(err) => ( +
+
+ {fieldLabels[err().field]} Error +
+ +
+ )} +
+ + ) : undefined} + > + {(err) => ( )} + +
+
+ ); +} + +function ErrorListDisplay(props: { errors: NewFnError[] }) { + return ( +
+ + {(error) => } + +
+ ); +} + + +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 | null>(null); + + type Input = { + raw_name: string, + raw_params: string, + raw_body: string, + } + + const validator: Validation = 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 ( +
+
Fn Draft
+ +
+ + +
+ + + + +
+ +
+ + + + +
+ ); +} + diff --git a/src/ui/Scrowl.tsx b/src/ui/Scrowl.tsx index c072bcf..555d89b 100644 --- a/src/ui/Scrowl.tsx +++ b/src/ui/Scrowl.tsx @@ -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? diff --git a/src/ui/validation.ts b/src/ui/validation.ts new file mode 100644 index 0000000..6cc54cd --- /dev/null +++ b/src/ui/validation.ts @@ -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 = + | { tag: "ok", value: T } + | { tag: "errors", errors: E[] } + +export type Validation = (input: A) => V + +export namespace V { + export function ok(value: T): V { return { tag: "ok", value } } + export function errors(errors: E[]): V { return { tag: "errors", errors } } + export function error(error: E): V { return { tag: "errors", errors: [error] } } + + // basically just map-error + export function elseErr(result: V, f: (e0: E0) => E1): V { + 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( + v: Validation, + f: (val: B) => C + ): Validation { + return (input: A) => { + const res = v(input); + return res.tag === "ok" ? V.ok(f(res.value)) : res; + }; + } + + export function pure(f: (x: A) => B): Validation{ + 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, E, B>( + bindings: (input: A) => { [K in keyof T]: V }, + body: (fields: T, input: A) => V +): Validation{ + return (input: A): V => { + const results: Partial = {}; + 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; + return e; + } else { + const x = body(results as T, input); + return x; + } + }; +}