Update to new source-region SourceRegion abstraction

This commit is contained in:
Yura Dupyn 2026-04-06 19:47:06 +02:00
parent 38b147c3e7
commit 909caaf7ac
12 changed files with 63 additions and 52 deletions

View file

@ -2,7 +2,7 @@
import * as readline from 'readline';
import * as fs from 'fs';
import { parseExpr, ParseError } from '../parser/parser';
import { SourceText, renderSpan, sourceText } from 'source-text';
import { SourceRegion, SourceText, renderSpan, sourceText } from 'source-text';
import { exprToString } from '../debug/expr_show';
import { valueToString } from '../debug/value_show';
import { Program } from '../program';
@ -27,7 +27,7 @@ function runSource(inputRaw: string, isRepl: boolean): boolean {
try {
// Wrap in SourceText
const text = sourceText(input);
const text = sourceText(input).fullRegion();
// === Parse ===
const parseResult = parseExpr(text);
@ -184,7 +184,7 @@ function getErrorMessage(err: ParseError): string {
}
function printPrettyError(text: SourceText, err: ParseError) {
function printPrettyError(text: SourceRegion, err: ParseError) {
const msg = getErrorMessage(err);
console.log(`\n${C.Red}${C.Bold}Parse Error:${C.Reset} ${C.Bold}${msg}${C.Reset}`);
@ -213,3 +213,4 @@ function printPrettyError(text: SourceText, err: ParseError) {
}
}
}

View file

@ -1,5 +1,5 @@
// AI GENERATED
import { SourceText } from "source-text";
import { SourceText, sourceText } from "source-text";
import { Cursor, scanString, scanNumber } from "./cursor";
import { Result } from "../result";
@ -53,13 +53,13 @@ function assertError(result: Result<any, any>, expectedTag: string, expectedReas
// === Number Tests ===
function test_integers() {
const src = new SourceText("123");
const src = sourceText("123").fullRegion();
const cursor = new Cursor(src);
const result = scanNumber(cursor);
assertOk(result, 123);
const src2 = new SourceText("-500");
const src2 = sourceText("-500").fullRegion();
const cursor2 = new Cursor(src2);
const result2 = scanNumber(cursor2);
@ -69,12 +69,12 @@ function test_integers() {
}
function test_floats() {
const src = new SourceText("3.14159");
const src = sourceText("3.14159").fullRegion();
const cursor = new Cursor(src);
const result = scanNumber(cursor);
assertOk(result, 3.14159);
const src2 = new SourceText("-0.001");
const src2 = sourceText("-0.001").fullRegion();
const cursor2 = new Cursor(src2);
const result2 = scanNumber(cursor2);
assertOk(result2, -0.001);
@ -84,14 +84,14 @@ function test_floats() {
function test_number_errors() {
// 1. Trailing Dot
const c1 = new Cursor(new SourceText("1."));
const c1 = new Cursor(sourceText("1.").fullRegion());
const r1 = scanNumber(c1);
assertError(r1, "InvalidNumber", "MissingFractionalDigits");
// 2. No leading digit (.5)
// Let's test "Saw Sign but no digits" which is a hard error
const c2 = new Cursor(new SourceText("-")); // Just a minus
const c2 = new Cursor(sourceText("-").fullRegion()); // Just a minus
const r2 = scanNumber(c2);
assertError(r2, "ExpectedNumber");
@ -101,13 +101,13 @@ function test_number_errors() {
// === String Tests ===
function test_basic_strings() {
const src = new SourceText('"hello world"');
const src = sourceText('"hello world"').fullRegion();
const cursor = new Cursor(src);
const result = scanString(cursor);
assertOk(result, "hello world");
const src2 = new SourceText('""'); // Empty string
const src2 = sourceText('""').fullRegion(); // Empty string
const cursor2 = new Cursor(src2);
const result2 = scanString(cursor2);
@ -117,26 +117,26 @@ function test_basic_strings() {
}
function test_string_escapes() {
const src = new SourceText('"line1\\nline2"');
const src = sourceText('"line1\\nline2"').fullRegion();
const cursor = new Cursor(src);
const result = scanString(cursor);
assertOk(result, "line1\nline2");
const src2 = new SourceText('"col1\\tcol2"');
const src2 = sourceText('"col1\\tcol2"').fullRegion();
const cursor2 = new Cursor(src2);
const result2 = scanString(cursor2);
assertOk(result2, "col1\tcol2");
const src3 = new SourceText('"quote: \\" slash: \\\\"');
const src3 = sourceText('"quote: \\" slash: \\\\"').fullRegion();
const cursor3 = new Cursor(src3);
const result3 = scanString(cursor3);
assertOk(result3, 'quote: " slash: \\');
// Null byte test
const src4 = new SourceText('"null\\0byte"');
const src4 = sourceText('"null\\0byte"').fullRegion();
const cursor4 = new Cursor(src4);
const result4 = scanString(cursor4);
assertOk(result4, "null\0byte");
@ -146,23 +146,23 @@ function test_string_escapes() {
function test_unicode_escapes() {
// Rocket emoji: 🚀 (U+1F680)
const c1 = new Cursor(new SourceText('"\\u{1F680}"'));
const c1 = new Cursor(sourceText('"\\u{1F680}"').fullRegion());
assertOk(scanString(c1), "🚀");
// Two escapes
const c2 = new Cursor(new SourceText('"\\u{41}\\u{42}"'));
const c2 = new Cursor(sourceText('"\\u{41}\\u{42}"').fullRegion());
assertOk(scanString(c2), "AB");
// Error: Missing Brace
const c3 = new Cursor(new SourceText('"\\u1F680"'));
const c3 = new Cursor(sourceText('"\\u1F680"').fullRegion());
assertError(scanString(c3), "InvalidEscape", { tag: "UnicodeMissingBrace" });
// Error: Empty
const c4 = new Cursor(new SourceText('"\\u{}"'));
const c4 = new Cursor(sourceText('"\\u{}"').fullRegion());
assertError(scanString(c4), "InvalidEscape", { tag: "UnicodeNoDigits" });
// Error: Overflow
const c5 = new Cursor(new SourceText('"\\u{110000}"'));
const c5 = new Cursor(sourceText('"\\u{110000}"').fullRegion());
const res5 = scanString(c5);
// Need to check the value inside the reason for overflow
if (res5.tag === 'ok') throw new Error("Should have failed overflow");
@ -180,7 +180,7 @@ function test_cursor_tracking() {
// Line 2: 456 (LF)
// Line 3: "foo"
const code = "123\r\n456\n\"foo\"";
const src = new SourceText(code);
const src = sourceText(code).fullRegion();
const cursor = new Cursor(src);
// 1. Scan 123

View file

@ -1,5 +1,5 @@
import { char, NEW_LINE, CARRIAGE_RETURN, DOT, DIGIT_0, DIGIT_9, LOWERCASE_a, LOWERCASE_f, UPPERCASE_A, UPPERCASE_F, SPACE, TAB } from 'source-text';
import type { SourceText, Span, SourceLocation, CodePoint, StringIndex, CodePointIndex } from 'source-text';
import type { SourceRegion, SourceText, Span, SourceLocation, CodePoint, StringIndex, CodePointIndex } from 'source-text';
import { Result } from '../result';
export type CursorState = {
@ -10,13 +10,21 @@ export type CursorState = {
}
export class Cursor {
private index: CodePointIndex = 0;
private line: number = 1;
private column: number = 1;
private index: CodePointIndex;
private line: number;
private column: number;
// Track previous char to handle \r\n correctly
private lastCharWasCR: boolean = false;
constructor(readonly text: SourceText) {}
constructor(readonly region: SourceRegion) {
this.index = region.span.start.index;
this.line = region.span.start.line;
this.column = region.span.start.column;
}
get text(): SourceText {
return this.region.source;
}
save(): CursorState {
return { index: this.index, line: this.line, column: this.column, lastCharWasCR: this.lastCharWasCR };
@ -30,14 +38,16 @@ export class Cursor {
}
eof(): boolean {
return this.index >= this.text.length;
return this.index >= this.region.span.end.index;
}
peek(n: number = 0): CodePoint | undefined {
if (this.index + n >= this.region.span.end.index) return undefined;
return this.text.chars[this.index + n]?.char;
}
next(): CodePoint | undefined {
if (this.eof()) return undefined;
const ref = this.text.chars[this.index];
if (!ref) return undefined;

View file

@ -1,6 +1,6 @@
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 { char, CodePoint, SourceRegion, SourceText, Span } from 'source-text';
import { Result } from '../result';
import { Expr, ExprBinding, FieldAssignment, FieldPattern, FunctionName, MatchBranch, Pattern, ProductPattern, SignalExpr, SignalExprBinding } from '../expr';
@ -531,7 +531,7 @@ function recordPatternField(cursor: Cursor): FieldPattern {
}
export function parseExpr(source: SourceText): Result<Expr, ParseError> {
export function parseExpr(source: SourceRegion): Result<Expr, ParseError> {
const cursor = new Cursor(source);
try {
@ -553,7 +553,7 @@ export function parseExpr(source: SourceText): Result<Expr, ParseError> {
}
}
export function parseSignalExpr(source: SourceText): Result<SignalExpr, ParseError> {
export function parseSignalExpr(source: SourceRegion): Result<SignalExpr, ParseError> {
const cursor = new Cursor(source);
try {
@ -581,7 +581,7 @@ function functionParameters(cursor: Cursor): ProductPattern[] {
return parameters;
}
export function parseFunctionParameters(source: SourceText): Result<ProductPattern[], ParseError> {
export function parseFunctionParameters(source: SourceRegion): Result<ProductPattern[], ParseError> {
const cursor = new Cursor(source);
try {
skipWhitespaceAndComments(cursor);
@ -603,7 +603,7 @@ export function parseFunctionParameters(source: SourceText): Result<ProductPatte
}
export function parseFunctionName(source: SourceText): Result<FunctionName, ParseError> {
export function parseFunctionName(source: SourceRegion): Result<FunctionName, ParseError> {
const cursor = new Cursor(source);
try {
skipWhitespaceAndComments(cursor);

View file

@ -1,5 +1,5 @@
import { ParseError } from "src/lang/parser/parser";
import { renderSpan, SourceText } from "source-text";
import { renderSpan, SourceRegion } from "source-text";
import { DisplayLineViews } from "./LineView";
export function formatErrorMesage(err: ParseError): string {
@ -118,7 +118,7 @@ function formatChar(cp: number | undefined): string {
return `'${s}'`;
}
export function ShowParseError(props: { text: SourceText, err: ParseError }) {
export function ShowParseError(props: { text: SourceRegion, err: ParseError }) {
const msg = () => formatErrorMesage(props.err);
const views = () => renderSpan(props.text, props.err.span, 3);

View file

@ -1,6 +1,6 @@
import { For, Match, Show, Switch } from "solid-js";
import { ParseError } from "src/lang/parser/parser";
import { SourceText } from "source-text";
import { SourceRegion } from "source-text";
import { ShowParseError } from 'src/ui/Component/ParseError';
import { Program } from "src/lang/program";
@ -13,7 +13,7 @@ export type DigithError = {
export namespace DigithError {
export type Payload =
| { tag: "Parse", err: ParseError, src: SourceText }
| { tag: "Parse", err: ParseError, src: SourceRegion }
| { tag: "Program", err: Program.Error };
export type Id = string;

View file

@ -17,13 +17,13 @@ type Input = {
const validator: Validation<Input, Program.UpdateFunction, DigithError> = letValidate(
(input) => ({
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
payload: { tag: "Parse", field: "params", err, src: sourceText(input.raw_params) },
payload: { tag: "Parse", field: "params", err, src: sourceText(input.raw_params).fullRegion() },
ids: ["params"],
tags: ["footer"],
config: { title: "Parameters" },
})),
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body) },
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body).fullRegion() },
ids: ["body"],
tags: ["footer"],
config: { title: "Function Body" },

View file

@ -18,19 +18,19 @@ type Input = {
const validator: Validation<Input, Program.CreateFunction, DigithError> = letValidate(
(input) =>({
name: V.elseErr(validateNameRaw(input.raw_name), err =>({
payload: { tag: "Parse", err, src: sourceText(input.raw_name) },
payload: { tag: "Parse", err, src: sourceText(input.raw_name).fullRegion() },
ids: ["name"],
tags: ["footer"],
config: { title: "Function Name", display: "flat" },
})),
parameters: V.elseErr(validateParamsRaw(input.raw_params), err => ({
payload: { tag: "Parse", err, src: sourceText(input.raw_params) },
payload: { tag: "Parse", err, src: sourceText(input.raw_params).fullRegion() },
ids: ["params"],
tags: ["footer"],
config: { title: "Parameters", display: "flat" },
})),
body: V.elseErr(validateExprRaw(input.raw_body), err => ({
payload: { tag: "Parse", err, src: sourceText(input.raw_body) },
payload: { tag: "Parse", err, src: sourceText(input.raw_body).fullRegion() },
ids: ["body"],
tags: ["footer"],
config: { title: "Function Body", display: "flat" },

View file

@ -3,7 +3,7 @@ import { useProgram } from 'src/ui/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 'source-text';
import { SourceRegion, SourceText, sourceText } from 'source-text';
import { ParseError, parseExpr } from 'src/lang/parser/parser';
import { ShowParseError } from 'src/ui/Component/ParseError';
import { Val } from 'src/ui/Component/Value';
@ -15,7 +15,7 @@ namespace ReplResult {
export type Success =
{ tag: "success", value: Value }
export type Parse_Error =
{ tag: "parse_error", text: SourceText, err: ParseError }
{ tag: "parse_error", text: SourceRegion, err: ParseError }
export type Runtime_Error =
{ tag: "runtime_error", err: RuntimeError }
}
@ -39,7 +39,7 @@ export function ExprREPL() {
if (input().trim() === "") {
return;
}
const text = sourceText(raw);
const text = sourceText(raw).fullRegion();
const parseResult = parseExpr(text);
if (parseResult.tag === "error") {

View file

@ -17,13 +17,13 @@ type Input = {
const validator: Validation<Input, Program.CreateSignal, DigithError> = letValidate(
(input) =>({
name: V.elseErr(validateNameRaw(input.raw_name), err =>({
payload: { tag: "Parse", err, src: sourceText(input.raw_name) },
payload: { tag: "Parse", err, src: sourceText(input.raw_name).fullRegion() },
ids: ["name"],
tags: ["footer"],
config: { title: "Signal Name", display: "flat" },
})),
body: V.elseErr(validateSignalExprRaw(input.raw_body), err => ({
payload: { tag: "Parse", err, src: sourceText(input.raw_body) },
payload: { tag: "Parse", err, src: sourceText(input.raw_body).fullRegion() },
ids: ["body"],
tags: ["footer"],
config: { title: "Signal Body", display: "flat" },

View file

@ -19,7 +19,7 @@ type Input = {
const validator: Validation<Input, Program.UpdateSignal, DigithError> = letValidate(
(input) => ({
body: V.elseErr(validateSignalExprRaw(input.raw_body), err => ({
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body) },
payload: { tag: "Parse", field: "body", err, src: sourceText(input.raw_body).fullRegion() },
ids: ["body"],
tags: ["footer"],
config: { title: "Signal Body" },

View file

@ -5,25 +5,25 @@ import { V } from "./";
// === Parser wrappers ===
export function validateNameRaw(input: string): V<FunctionName, ParseError> {
const src = sourceText(input);
const src = sourceText(input).fullRegion();
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 src = sourceText(input).fullRegion();
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 src = sourceText(input).fullRegion();
const res = parseExpr(src);
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
};
export function validateSignalExprRaw(input: string): V<SignalExpr, ParseError> {
const src = sourceText(input);
const src = sourceText(input).fullRegion();
const res = parseSignalExpr(src);
return res.tag === "ok" ? V.ok(res.value) : V.errors([res.error]);
};