Prettier errors
This commit is contained in:
parent
e389e46852
commit
3d1cd89067
6 changed files with 313 additions and 48 deletions
|
|
@ -1,63 +1,214 @@
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
import { parse } from '../parser/parser';
|
import * as fs from 'fs';
|
||||||
|
import { parse, ParseError } from '../parser/parser';
|
||||||
|
import { SourceText, renderSpan, sourceText } from '../parser/source_text';
|
||||||
import { exprToString } from '../debug/expr_show';
|
import { exprToString } from '../debug/expr_show';
|
||||||
import { valueToString } from '../debug/value_show';
|
import { valueToString } from '../debug/value_show';
|
||||||
import { eval_start, Program } from '../value';
|
import { eval_start, Program } from '../value';
|
||||||
import { Result } from '../result';
|
|
||||||
|
|
||||||
|
// ANSI Color Codes
|
||||||
|
const C = {
|
||||||
|
Reset: "\x1b[0m",
|
||||||
|
Red: "\x1b[31m",
|
||||||
|
Green: "\x1b[32m",
|
||||||
|
Yellow: "\x1b[33m",
|
||||||
|
Blue: "\x1b[34m",
|
||||||
|
Gray: "\x1b[90m",
|
||||||
|
Bold: "\x1b[1m",
|
||||||
|
};
|
||||||
|
|
||||||
|
const program = Program.makeEmpty();
|
||||||
|
|
||||||
|
function runSource(inputRaw: string, isRepl: boolean): boolean {
|
||||||
|
const input = inputRaw.trim();
|
||||||
|
if (!input) return true; // Empty lines are fine
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wrap in SourceText
|
||||||
|
const text = sourceText(input);
|
||||||
|
|
||||||
|
// === Parse ===
|
||||||
|
const parseResult = parse(text);
|
||||||
|
|
||||||
|
if (parseResult.tag === "error") {
|
||||||
|
printPrettyError(text, parseResult.error);
|
||||||
|
return false; // Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
const ast = parseResult.value;
|
||||||
|
|
||||||
|
console.log(`${C.Green}AST:${C.Reset} ${exprToString(ast)}`);
|
||||||
|
|
||||||
|
// === Eval ===
|
||||||
|
const evalResult = eval_start(program, ast);
|
||||||
|
|
||||||
|
if (evalResult.tag === "ok") {
|
||||||
|
// Always print the result value
|
||||||
|
console.log(`${C.Blue}VAL:${C.Reset} ${valueToString(evalResult.value)}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const err = evalResult.error;
|
||||||
|
console.log(`\n🔥 [Runtime Error]:`, err);
|
||||||
|
// If your Runtime Errors have spans, use printPrettyError(text, err) here too!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`\n💥 [System Crash]:`);
|
||||||
|
console.log(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 3. Entry Point Logic ===
|
||||||
|
|
||||||
|
// Check for command line arguments (ignoring 'node' and 'script.ts')
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length > 0) {
|
||||||
|
// === FILE MODE ===
|
||||||
|
const filePath = args[0];
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(`${C.Red}Error: File not found '${filePath}'${C.Reset}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Run the file
|
||||||
|
const success = runSource(fileContent, false);
|
||||||
|
|
||||||
|
// Exit with appropriate code
|
||||||
|
process.exit(success ? 0 : 1);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// === REPL MODE ===
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
prompt: '> '
|
prompt: '> '
|
||||||
});
|
});
|
||||||
|
|
||||||
// We create one persistent program context
|
|
||||||
const program = Program.makeEmpty();
|
|
||||||
|
|
||||||
console.log("=== Evaluator REPL ===");
|
console.log("=== Evaluator REPL ===");
|
||||||
console.log("Input -> Parse -> Eval -> Value");
|
console.log("Input -> Parse -> Eval -> Value");
|
||||||
console.log("Ctrl+C to exit.\n");
|
console.log("Ctrl+C to exit.\n");
|
||||||
|
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
|
||||||
rl.on('line', (lineInput) => {
|
rl.on('line', (line) => {
|
||||||
const trimmed = lineInput.trim();
|
runSource(line, true);
|
||||||
if (!trimmed) {
|
console.log(""); // Empty line for spacing
|
||||||
rl.prompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// === 1. PARSE ===
|
|
||||||
const parseResult = parse(trimmed);
|
|
||||||
|
|
||||||
if (parseResult.tag === "error") {
|
|
||||||
const err = parseResult.error;
|
|
||||||
console.log(`\n❌ [Parse Error]:`, err);
|
|
||||||
// (Optional: Reuse your line/col logic here)
|
|
||||||
rl.prompt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ast = parseResult.value;
|
|
||||||
console.log(`\nAST: ${exprToString(ast)}`);
|
|
||||||
|
|
||||||
// === 2. EVALUATE ===
|
|
||||||
const evalResult = eval_start(program, ast);
|
|
||||||
|
|
||||||
if (evalResult.tag === "ok") {
|
|
||||||
console.log(`VAL: ${valueToString(evalResult.value)}`);
|
|
||||||
} else {
|
|
||||||
const err = evalResult.error;
|
|
||||||
console.log(`\n🔥 [Runtime Error]:`, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`\n💥 [System Crash]:`);
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("");
|
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Formatting ===
|
||||||
|
|
||||||
|
// 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}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(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.`;
|
||||||
|
}
|
||||||
|
return "Invalid escape sequence.";
|
||||||
|
|
||||||
|
// 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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function printPrettyError(text: SourceText, err: ParseError) {
|
||||||
|
const msg = getErrorMessage(err);
|
||||||
|
console.log(`\n${C.Red}${C.Bold}Parse Error:${C.Reset} ${C.Bold}${msg}${C.Reset}`);
|
||||||
|
|
||||||
|
// Use your new renderSpan function
|
||||||
|
// We request 1 line of context before/after
|
||||||
|
const views = renderSpan(text, err.span, 3);
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
// Format the gutter (line number)
|
||||||
|
// e.g. " 10 | "
|
||||||
|
const gutter = `${C.Blue}${view.gutterPad}${view.lineNo} | ${C.Reset}`;
|
||||||
|
|
||||||
|
// Reconstruct the line with highlighting
|
||||||
|
// prefix (gray) + highlight (red/bold) + suffix (gray)
|
||||||
|
const code =
|
||||||
|
`${C.Gray}${view.prefix}${C.Reset}` +
|
||||||
|
`${C.Red}${C.Bold}${view.highlight}${C.Reset}` +
|
||||||
|
`${C.Gray}${view.suffix}${C.Reset}`;
|
||||||
|
|
||||||
|
console.log(`${gutter}${code}`);
|
||||||
|
|
||||||
|
// Render the underline if this line contains the error
|
||||||
|
if (view.underline.trim().length > 0) {
|
||||||
|
const emptyGutter = " ".repeat(view.gutterPad.length + String(view.lineNo).length + 3); // match " N | "
|
||||||
|
console.log(`${emptyGutter}${C.Red}${C.Bold}${view.underline}${C.Reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export class Cursor {
|
||||||
return this.index;
|
return this.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: unicode-index ~> string-offset, make that into a separate function.
|
||||||
currentOffset(): StringIndex {
|
currentOffset(): StringIndex {
|
||||||
return this.text.chars[this.index]?.offset ?? this.text.source.length;
|
return this.text.chars[this.index]?.offset ?? this.text.source.length;
|
||||||
}
|
}
|
||||||
|
|
@ -79,15 +80,9 @@ export class Cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
makeSpan(start: SourceLocation): Span {
|
makeSpan(start: SourceLocation): Span {
|
||||||
const startOffset =
|
|
||||||
this.text.chars[start.index]?.offset ?? this.text.source.length;
|
|
||||||
const endOffset = this.currentOffset();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: startOffset,
|
start,
|
||||||
end: endOffset,
|
end: this.currentLocation(),
|
||||||
line: start.line,
|
|
||||||
column: start.column,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -444,8 +444,7 @@ function recordPatternField(cursor: Cursor): FieldPattern {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function parse(input: string): Result<Expr, ParseError> {
|
export function parse(source: SourceText): Result<Expr, ParseError> {
|
||||||
const source = new SourceText(input);
|
|
||||||
const cursor = new Cursor(source);
|
const cursor = new Cursor(source);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,21 @@ export class SourceText {
|
||||||
// TODO: Consider removing \r or \n from the end if they exist.
|
// TODO: Consider removing \r or \n from the end if they exist.
|
||||||
return this.sliceByCp(startCp, endCp);
|
return this.sliceByCp(startCp, endCp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLineRange(line: number): { start: CodePointIndex, end: CodePointIndex } {
|
||||||
|
const lineIndex = line - 1;
|
||||||
|
if (lineIndex < 0 || lineIndex >= this.lineStarts.length) {
|
||||||
|
// TODO: This is a bit suspicious. Maybe return undefined?
|
||||||
|
return { start: 0, end: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = this.lineStarts[lineIndex];
|
||||||
|
const end = (lineIndex + 1 < this.lineStarts.length)
|
||||||
|
? this.lineStarts[lineIndex + 1]
|
||||||
|
: this.chars.length;
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sourceText(s: string) {
|
export function sourceText(s: string) {
|
||||||
|
|
@ -100,17 +115,15 @@ export function sourceText(s: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Span = {
|
export type Span = {
|
||||||
start: StringIndex,
|
start: SourceLocation;
|
||||||
end: StringIndex,
|
end: SourceLocation;
|
||||||
line: number,
|
|
||||||
column: number,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SourceLocation = {
|
export type SourceLocation = {
|
||||||
index: CodePointIndex;
|
index: CodePointIndex;
|
||||||
line: number; // 1-based
|
line: number; // 1-based
|
||||||
column: number; // 1-based
|
column: number; // 1-based
|
||||||
};
|
}
|
||||||
|
|
||||||
// Whitespace
|
// Whitespace
|
||||||
export const NEW_LINE: CodePoint = char('\n');
|
export const NEW_LINE: CodePoint = char('\n');
|
||||||
|
|
@ -130,3 +143,97 @@ export const UPPERCASE_A: CodePoint = char('A');
|
||||||
export const LOWERCASE_f: CodePoint = char('f');
|
export const LOWERCASE_f: CodePoint = char('f');
|
||||||
export const UPPERCASE_F: CodePoint = char('F');
|
export const UPPERCASE_F: CodePoint = char('F');
|
||||||
|
|
||||||
|
// === Rendering Utilities ===
|
||||||
|
|
||||||
|
export type LineView = {
|
||||||
|
lineNo: number;
|
||||||
|
sourceLine: string; // The full raw text of the line
|
||||||
|
|
||||||
|
// These split the line into 3 parts for coloring:
|
||||||
|
// prefix | highlight | suffix
|
||||||
|
prefix: string;
|
||||||
|
highlight: string;
|
||||||
|
suffix: string;
|
||||||
|
|
||||||
|
// Helpers for underlines (e.g., " ^^^^^")
|
||||||
|
gutterPad: string; // Padding to align line numbers
|
||||||
|
underline: string; // The literal "^^^" string for CLI usage
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderSpan(text: SourceText, span: Span, contextLines = 1): LineView[] {
|
||||||
|
const views: LineView[] = [];
|
||||||
|
|
||||||
|
// Determine range of lines to show (including context)
|
||||||
|
const startLine = Math.max(1, span.start.line - contextLines);
|
||||||
|
const endLine = Math.min(text.lineStarts.length, span.end.line + contextLines);
|
||||||
|
|
||||||
|
// Calculate the max width of line numbers for nice padding (e.g. " 9 |" vs " 10 |")
|
||||||
|
const maxLineNoWidth = endLine.toString().length;
|
||||||
|
|
||||||
|
for (let lineNo = startLine; lineNo <= endLine; lineNo++) {
|
||||||
|
const lineRange = text.getLineRange(lineNo);
|
||||||
|
|
||||||
|
// We strip the trailing newline for display purposes
|
||||||
|
let lineRaw = text.sliceByCp(lineRange.start, lineRange.end);
|
||||||
|
if (lineRaw.endsWith('\n') || lineRaw.endsWith('\r')) {
|
||||||
|
lineRaw = lineRaw.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the intersection of the Span with this specific Line
|
||||||
|
|
||||||
|
// 1. Where does the highlight start on this line?
|
||||||
|
// If this is the start line, use span.column. Otherwise start at 0 (beginning of line)
|
||||||
|
// We subtract 1 because columns are 1-based, string indices are 0-based.
|
||||||
|
const highlightStartCol = (lineNo === span.start.line)
|
||||||
|
? span.start.column - 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 2. Where does the highlight end on this line?
|
||||||
|
// If this is the end line, use span.column. Otherwise end at the string length.
|
||||||
|
const highlightEndCol = (lineNo === span.end.line)
|
||||||
|
? span.end.column - 1
|
||||||
|
: lineRaw.length;
|
||||||
|
|
||||||
|
// Logic to distinguish context lines from error lines
|
||||||
|
const isErrorLine = lineNo >= span.start.line && lineNo <= span.end.line;
|
||||||
|
|
||||||
|
let prefix = "", highlight = "", suffix = "";
|
||||||
|
|
||||||
|
if (isErrorLine) {
|
||||||
|
// Clamp indices to bounds (safety)
|
||||||
|
const safeStart = Math.max(0, Math.min(highlightStartCol, lineRaw.length));
|
||||||
|
const safeEnd = Math.max(0, Math.min(highlightEndCol, lineRaw.length));
|
||||||
|
|
||||||
|
prefix = lineRaw.substring(0, safeStart);
|
||||||
|
highlight = lineRaw.substring(safeStart, safeEnd);
|
||||||
|
suffix = lineRaw.substring(safeEnd);
|
||||||
|
} else {
|
||||||
|
// Pure context line
|
||||||
|
prefix = lineRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the "underline" string (e.g., " ^^^^")
|
||||||
|
// Note: This naive approach assumes monospaced fonts and no fancy unicode widths,
|
||||||
|
// which usually holds for code.
|
||||||
|
let underline = "";
|
||||||
|
if (isErrorLine) {
|
||||||
|
// Spaces for prefix
|
||||||
|
underline += " ".repeat(prefix.length);
|
||||||
|
// Carets for highlight (ensure at least 1 if it's a zero-width cursor position)
|
||||||
|
const hlLen = Math.max(1, highlight.length);
|
||||||
|
underline += "^".repeat(hlLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
views.push({
|
||||||
|
lineNo,
|
||||||
|
sourceLine: lineRaw,
|
||||||
|
prefix,
|
||||||
|
highlight,
|
||||||
|
suffix,
|
||||||
|
gutterPad: " ".repeat(maxLineNoWidth - lineNo.toString().length),
|
||||||
|
underline
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
|
||||||
11
tmp_repl/test.flux
Normal file
11
tmp_repl/test.flux
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let {
|
||||||
|
foo = "some important message" .
|
||||||
|
|
||||||
|
fn { x, y .
|
||||||
|
x // this should be an error
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -17,3 +17,5 @@ npx ts-node src/parser/cursor.test.ts
|
||||||
|
|
||||||
npx ts-node src/debug/repl.ts
|
npx ts-node src/debug/repl.ts
|
||||||
|
|
||||||
|
npx ts-node src/debug/repl.ts tmp_repl/test.flux
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue