diff --git a/src/ui/App.tsx b/src/ui/App.tsx new file mode 100644 index 0000000..4b88998 --- /dev/null +++ b/src/ui/App.tsx @@ -0,0 +1,26 @@ +import { createSignal } from 'solid-js'; +import { ExprREPL } from './REPL'; + +function Hello() { + const [ count, setCount ] = createSignal(0); + + return ( +
+ { count() > 5 ? too damn high : { count() } } +
+ + +
+
+ ); +} + +export default function App() { + + return ( +
+ + +
+ ); +} diff --git a/src/ui/Expr.tsx b/src/ui/Expr.tsx new file mode 100644 index 0000000..427234f --- /dev/null +++ b/src/ui/Expr.tsx @@ -0,0 +1,108 @@ +import { Expr, FieldAssignment, FieldPattern, Literal, Pattern, ProductPattern } from "../lang/expr"; + +export function Expression(prop: { expr: Expr }) { + return ( + { exprToString(prop.expr) } + ); +} + +export function exprToString(expr: Expr): string { + switch (expr.tag) { + case "literal": + return literalToString(expr.literal); + + case "var_use": + return `\$${expr.name}`; + + case "call": + return `${expr.name}(${expr.args.map(exprToString).join(", ")})`; + + case "tuple": + return `(${expr.exprs.map(exprToString).join(", ")})`; + + case "record": { + const fields = expr.fields.map(fieldAssignmentToString).join(", "); + return `{ ${fields} }`; + } + + case "tag": + return `#${expr.tag_name}`; + + case "tagged": { + const payload = exprToString(expr.expr); + return `#${expr.tag_name} ${payload}`; + } + + case "let": { + const bindings = expr.bindings + .map(b => `${productPatternToString(b.pattern)} = ${exprToString(b.expr)}`) + .join(", "); + return `let { ${bindings} . ${exprToString(expr.body)} }`; + } + + case "lambda": { + const params = expr.parameters.map(productPatternToString).join(", "); + return `fn { ${params} . ${exprToString(expr.body)} }`; + } + + case "apply": { + const args = expr.args.map(exprToString).join(", "); + return `apply(${exprToString(expr.callee)} ! ${args})`; + } + + case "match": { + const branches = expr.branches + .map(b => `${patternToString(b.pattern)} . ${exprToString(b.body)}`) + .join(" | "); + return `match ${exprToString(expr.arg)} { ${branches} }`; + } + } +} + +// === Helpers === + +function literalToString(lit: Literal): string { + switch (lit.tag) { + case "number": return lit.value.toString(); + case "string": return `"${lit.value}"`; // TODO: simplistic string escaping + } +} + +function fieldAssignmentToString(f: FieldAssignment): string { + return `${f.name} = ${exprToString(f.expr)}`; +} + +// === Pattern Printers === + +export function patternToString(pat: Pattern): string { + switch (pat.tag) { + case "tag": + return `#${pat.tag_name}`; + case "tagged": + return `#${pat.tag_name} ${patternToString(pat.pattern)}`; + // If it's a product pattern (any, tuple, record) + default: + return productPatternToString(pat); + } +} + +export function productPatternToString(pat: ProductPattern): string { + switch (pat.tag) { + case "any": + return pat.name; + + case "tuple": + return `(${pat.patterns.map(productPatternToString).join(", ")})`; + + case "record": + return `{ ${pat.fields.map(fieldPatternToString).join(", ")} }`; + } +} + +function fieldPatternToString(f: FieldPattern): string { + // Check for punning: if pattern is "any" and name matches fieldName + if (f.pattern.tag === "any" && f.pattern.name === f.fieldName) { + return f.fieldName; + } + return `${f.fieldName} = ${productPatternToString(f.pattern)}`; +} diff --git a/src/ui/LineView.tsx b/src/ui/LineView.tsx new file mode 100644 index 0000000..bc8f052 --- /dev/null +++ b/src/ui/LineView.tsx @@ -0,0 +1,35 @@ +import { LineView } from "src/lang/parser/source_text"; +import { For, Show } from "solid-js"; + +export function DisplayLineView(prop: { view: LineView }) { + return ( +
+ { prop.view.gutterPad }{ prop.view.lineNo } + + + { prop.view.prefix } + { prop.view.highlight } + { prop.view.suffix } + + + 0}> +
+ {" ".repeat(prop.view.gutterPad.length + String(prop.view.lineNo).length + 3)} + {prop.view.underline} +
+
+ +
+ ); +} + +export function DisplayLineViews(prop: { views: LineView[] }) { + return ( + + {(view) => ( + + )} + + ); +} + diff --git a/src/ui/ParseError.tsx b/src/ui/ParseError.tsx new file mode 100644 index 0000000..db8accc --- /dev/null +++ b/src/ui/ParseError.tsx @@ -0,0 +1,101 @@ +import { ParseError } from "src/lang/parser/parser"; +import { renderSpan, SourceText } from "src/lang/parser/source_text"; +import { DisplayLineViews } from "./LineView"; + +export function formatErrorMesage(err: ParseError): string { + switch (err.tag) { + case "UnexpectedToken": + return `Unexpected token. Expected: ${err.expected}`; + + case "UnexpectedTokenWhileParsingSequence": + return `Unexpected token in sequence. Expected delimiter ${formatChar(err.expectedDelimiter)} or terminator ${formatChar(err.expectedTerminator)}, but found ${formatChar(err.received)}.`; + + case "UnexpectedCharacter": + return `Unexpected character: ${formatChar(err.char)}`; + + case "UnexpectedEOF": + return "Unexpected end of file."; + + case "ExpectedNumber": + return "Expected a number here."; + + case "InvalidNumber": + return err.reason === "NotFinite" + ? "Number is too large or invalid." + : "Invalid number format (missing fractional digits?)."; + + case "InvalidIdentifier": + // Handle nested reasons if needed, e.g. "Keyword 'let' cannot be used as an identifier" + return `Invalid identifier '${err.text}': ${err.reason.tag}`; + + case "InvalidEscape": + switch (err.reason.tag) { + case "UnknownEscapeSequence": return `Unknown escape sequence: \\${formatChar(err.reason.char)}`; + case "UnicodeMissingBrace": return "Unicode escape missing opening brace '{'."; + case "UnicodeNoDigits": return "Unicode escape missing hex digits."; + case "UnicodeUnclosed": return "Unicode escape missing closing brace '}'."; + case "UnicodeOverflow": return `Unicode code point ${err.reason.value.toString(16)} is out of bounds.`; + } + + // Context specific errors + case "ExpectedExpression": return "Expected an expression here."; + case "ExpectedFieldAssignmentSymbol": return "Expected '=' for field assignment."; + case "ExpectedPatternAssignmentSymbol": return "Expected '=' for pattern assignment."; + case "ExpectedPatternBindingSymbol": return "Expected '.' for pattern binding."; + case "ExpectedFunctionCallStart": return "Expected '(' to start function call."; + case "ExpectedRecordOpen": return "Expected '(' to start record."; + case "ExpectedLetBlockOpen": return "Expected '{' to start let-block."; + case "ExpectedLetBlockClose": return "Expected '}' to close let-block."; + case "ExpectedMatchBlockOpen": return "Expected '{' to start match-block."; + case "ExpectedMatchBlockClose": return "Expected '}' to close match-block."; + case "ExpectedLambdaBlockOpen": return "Expected '{' to start lambda body."; + case "ExpectedLambdaBlockClose": return "Expected '}' to close lambda body."; + case "ExpectedApplyStart": return "Expected '(' after 'apply'."; + case "ExpectedApplySeparator": return "Expected '!' inside 'apply'."; + case "UnexpectedTagPattern": return "Unexpected tag pattern (expected product pattern)."; + case "ExpectedPattern": return "Expected a pattern here."; + case "ExpectedRecordPatternOpen": return "Expected '(' for record pattern."; + case "ExpectedRecordField": return "Expected a field name in record pattern."; + + default: + return `Unknown error: ${(err as any).tag}`; + } +} + +// Helper to safely print code points (handling special chars like \n) +function formatChar(cp: number | undefined): string { + // Handle EOF (undefined) or invalid numbers safely + if (cp === undefined || Number.isNaN(cp)) { + return "EOF"; + } + + const s = String.fromCodePoint(cp); + + if (s === '\n') return "\\n"; + if (s === '\r') return "\\r"; + if (s === '\t') return "\\t"; + + return `'${s}'`; +} + +export function ShowParseError(props: { text: SourceText, err: ParseError }) { + const msg = () => formatErrorMesage(props.err); + const views = () => renderSpan(props.text, props.err.span, 3); + + + // Parse Error: Expected '(' to start function call. + // 1 | +(23, x) + // ^ + + return ( +
+
+ Parse Error: + {msg()} +
+ + +
+ ); +} + diff --git a/src/ui/ProgramProvider.tsx b/src/ui/ProgramProvider.tsx new file mode 100644 index 0000000..63f53de --- /dev/null +++ b/src/ui/ProgramProvider.tsx @@ -0,0 +1,21 @@ +import { Program } from 'src/lang/program'; +import { createContext, useContext } from "solid-js"; + +const ProgramContext = createContext(); + +export function ProgramProvider(props: { program: Program; children: any }) { + return ( + + {props.children} + + ); +} + +export function useProgram() { + const context = useContext(ProgramContext); + if (!context) { + throw new Error("useProgram must be used within a ProgramProvider"); + } + return context; +} + diff --git a/src/ui/REPL.tsx b/src/ui/REPL.tsx new file mode 100644 index 0000000..24ea9f2 --- /dev/null +++ b/src/ui/REPL.tsx @@ -0,0 +1,107 @@ +import { createSignal, Match, Switch } from 'solid-js'; +import { useProgram } from './ProgramProvider'; +import { eval_start } from 'src/lang/eval/evaluator'; +import { Value } from 'src/lang/eval/value'; +import { RuntimeError } from 'src/lang/eval/error'; +import { SourceText, sourceText } from 'src/lang/parser/source_text'; +import { ParseError, parseExpr } from 'src/lang/parser/parser'; +import { ShowParseError } from './ParseError'; +import { Val } from './Value'; + +namespace ReplResult { + export type Idle = + { tag: "idle" } + export type Success = + { tag: "success", value: Value } + export type Parse_Error = + { tag: "parse_error", text: SourceText, err: ParseError } + export type Runtime_Error = + { tag: "runtime_error", err: RuntimeError } +} + +type ReplResult = + | ReplResult.Idle + | ReplResult.Success + | ReplResult.Parse_Error + | ReplResult.Runtime_Error + + +export function ExprREPL() { + const program = useProgram(); + + const [input, setInput] = createSignal(""); + + const [result, setResult] = createSignal({ tag: "idle" }); + + function runExecution() { + const raw = input(); + if (input().trim() === "") { + return; + } + const text = sourceText(raw); + const parseResult = parseExpr(text); + + if (parseResult.tag === "error") { + setResult({ tag: "parse_error", text: text, err: parseResult.error }); + } else { + const evalResult = eval_start(program, parseResult.value); + if (evalResult.tag === "ok") { + setResult({ tag: "success", value: evalResult.value }); + } else { + setResult({ tag: "runtime_error", err: evalResult.error }); + } + } + } + + return ( +
+