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 { 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 { valueToString } from '../debug/value_show';
|
||||
import { eval_start, Program } from '../value';
|
||||
import { Result } from '../result';
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: '> '
|
||||
});
|
||||
// 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",
|
||||
};
|
||||
|
||||
// We create one persistent program context
|
||||
const program = Program.makeEmpty();
|
||||
|
||||
console.log("=== Evaluator REPL ===");
|
||||
console.log("Input -> Parse -> Eval -> Value");
|
||||
console.log("Ctrl+C to exit.\n");
|
||||
|
||||
rl.prompt();
|
||||
|
||||
rl.on('line', (lineInput) => {
|
||||
const trimmed = lineInput.trim();
|
||||
if (!trimmed) {
|
||||
rl.prompt();
|
||||
return;
|
||||
}
|
||||
function runSource(inputRaw: string, isRepl: boolean): boolean {
|
||||
const input = inputRaw.trim();
|
||||
if (!input) return true; // Empty lines are fine
|
||||
|
||||
try {
|
||||
// === 1. PARSE ===
|
||||
const parseResult = parse(trimmed);
|
||||
// Wrap in SourceText
|
||||
const text = sourceText(input);
|
||||
|
||||
// === Parse ===
|
||||
const parseResult = parse(text);
|
||||
|
||||
if (parseResult.tag === "error") {
|
||||
const err = parseResult.error;
|
||||
console.log(`\n❌ [Parse Error]:`, err);
|
||||
// (Optional: Reuse your line/col logic here)
|
||||
rl.prompt();
|
||||
return;
|
||||
printPrettyError(text, parseResult.error);
|
||||
return false; // Failed
|
||||
}
|
||||
|
||||
const ast = parseResult.value;
|
||||
console.log(`\nAST: ${exprToString(ast)}`);
|
||||
|
||||
console.log(`${C.Green}AST:${C.Reset} ${exprToString(ast)}`);
|
||||
|
||||
// === 2. EVALUATE ===
|
||||
// === Eval ===
|
||||
const evalResult = eval_start(program, ast);
|
||||
|
||||
if (evalResult.tag === "ok") {
|
||||
console.log(`VAL: ${valueToString(evalResult.value)}`);
|
||||
// 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);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
rl.prompt();
|
||||
});
|
||||
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({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: '> '
|
||||
});
|
||||
|
||||
console.log("=== Evaluator REPL ===");
|
||||
console.log("Input -> Parse -> Eval -> Value");
|
||||
console.log("Ctrl+C to exit.\n");
|
||||
|
||||
rl.prompt();
|
||||
|
||||
rl.on('line', (line) => {
|
||||
runSource(line, true);
|
||||
console.log(""); // Empty line for spacing
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO: unicode-index ~> string-offset, make that into a separate function.
|
||||
currentOffset(): StringIndex {
|
||||
return this.text.chars[this.index]?.offset ?? this.text.source.length;
|
||||
}
|
||||
|
|
@ -79,15 +80,9 @@ export class Cursor {
|
|||
}
|
||||
|
||||
makeSpan(start: SourceLocation): Span {
|
||||
const startOffset =
|
||||
this.text.chars[start.index]?.offset ?? this.text.source.length;
|
||||
const endOffset = this.currentOffset();
|
||||
|
||||
return {
|
||||
start: startOffset,
|
||||
end: endOffset,
|
||||
line: start.line,
|
||||
column: start.column,
|
||||
start,
|
||||
end: this.currentLocation(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -444,8 +444,7 @@ function recordPatternField(cursor: Cursor): FieldPattern {
|
|||
}
|
||||
|
||||
|
||||
export function parse(input: string): Result<Expr, ParseError> {
|
||||
const source = new SourceText(input);
|
||||
export function parse(source: SourceText): Result<Expr, ParseError> {
|
||||
const cursor = new Cursor(source);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,21 @@ export class SourceText {
|
|||
// TODO: Consider removing \r or \n from the end if they exist.
|
||||
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) {
|
||||
|
|
@ -100,17 +115,15 @@ export function sourceText(s: string) {
|
|||
}
|
||||
|
||||
export type Span = {
|
||||
start: StringIndex,
|
||||
end: StringIndex,
|
||||
line: number,
|
||||
column: number,
|
||||
start: SourceLocation;
|
||||
end: SourceLocation;
|
||||
}
|
||||
|
||||
export type SourceLocation = {
|
||||
index: CodePointIndex;
|
||||
line: number; // 1-based
|
||||
column: number; // 1-based
|
||||
};
|
||||
}
|
||||
|
||||
// Whitespace
|
||||
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 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 tmp_repl/test.flux
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue