Simplify UI/parser

This commit is contained in:
Yura Dupyn 2026-04-25 02:01:09 +02:00
parent f55b437037
commit a56020cd9f
3 changed files with 54 additions and 105 deletions

@ -1 +1 @@
Subproject commit 8471c60967eca6178d25fa3221035286a19856f2 Subproject commit 9c72959cd398909139137b0831a19c2e05161fe2

View file

@ -1,9 +1,9 @@
import { import {
CARRIAGE_RETURN, SourceCursor,
NEW_LINE,
SPACE,
TAB,
char, char,
isAsciiAlpha,
isAsciiAlphanumeric,
isAsciiWhitespace,
isDigit, isDigit,
} from 'source-region'; } from 'source-region';
import type { import type {
@ -35,10 +35,6 @@ const OPEN_PAREN = char('(');
const CLOSE_PAREN = char(')'); const CLOSE_PAREN = char(')');
const DASH = char('-'); const DASH = char('-');
const UNDERSCORE = char('_'); const UNDERSCORE = char('_');
const LOWERCASE_A = char('a');
const LOWERCASE_Z = char('z');
const UPPERCASE_A = char('A');
const UPPERCASE_Z = char('Z');
export type ParseDocumentResult = { export type ParseDocumentResult = {
values: ConcreteSyntax[]; values: ConcreteSyntax[];
@ -82,11 +78,11 @@ export function parseDocument(region: SourceRegion): ParseDocumentResult {
} }
class Parser { class Parser {
private index: CodePointIndex; private readonly cursor: SourceCursor;
private readonly errors: ParseError[] = []; private readonly errors: ParseError[] = [];
constructor(private readonly region: SourceRegion) { constructor(private readonly region: SourceRegion) {
this.index = region.span.start.index; this.cursor = new SourceCursor(region);
} }
parseDocument(): ParseDocumentResult { parseDocument(): ParseDocumentResult {
@ -94,9 +90,9 @@ class Parser {
while (true) { while (true) {
this.skipWhitespace(); this.skipWhitespace();
if (this.isAtEnd()) break; if (this.cursor.isAtEnd()) break;
const before = this.index; const before = this.cursor.checkpoint();
const value = this.parseExpr(); const value = this.parseExpr();
if (value) { if (value) {
values.push(value); values.push(value);
@ -110,12 +106,12 @@ class Parser {
} }
private parseExpr(): ConcreteSyntax | undefined { private parseExpr(): ConcreteSyntax | undefined {
const cp = this.peek(); const cp = this.cursor.peek();
if (cp === undefined) { if (cp === undefined) {
this.errors.push({ this.errors.push({
tag: "expected-expression", tag: "expected-expression",
span: this.eofSpan(), span: this.cursor.eofSpan(),
found: this.found(), found: this.found(),
}); });
return undefined; return undefined;
@ -124,7 +120,7 @@ class Parser {
if (cp === CLOSE_PAREN) { if (cp === CLOSE_PAREN) {
this.errors.push({ this.errors.push({
tag: "unexpected-close-paren", tag: "unexpected-close-paren",
span: this.currentSpan(), span: this.cursor.currentSpan(),
}); });
return undefined; return undefined;
} }
@ -135,16 +131,16 @@ class Parser {
this.errors.push({ this.errors.push({
tag: "expected-expression", tag: "expected-expression",
span: this.currentSpan(), span: this.cursor.currentSpan(),
found: this.found(), found: this.found(),
}); });
return undefined; return undefined;
} }
private parseList(): ConcreteSyntax | undefined { private parseList(): ConcreteSyntax | undefined {
const start = this.index; const start = this.cursor.checkpoint();
const openParen = this.currentSpan(); const openParen = this.cursor.currentSpan();
this.advance(); this.cursor.advance();
const values: ConcreteSyntax[] = []; const values: ConcreteSyntax[] = [];
@ -152,23 +148,23 @@ class Parser {
while (true) { while (true) {
this.skipWhitespace(); this.skipWhitespace();
const cp = this.peek(); const cp = this.cursor.peek();
if (cp === CLOSE_PAREN) { if (cp === CLOSE_PAREN) {
this.advance(); this.cursor.advance();
return ConcreteSyntax.list(values, this.spanFrom(start)); return ConcreteSyntax.list(values, this.cursor.spanFrom(start));
} }
if (cp === undefined) { if (cp === undefined) {
this.errors.push({ this.errors.push({
tag: "expected-close-paren", tag: "expected-close-paren",
span: this.eofSpan(), span: this.cursor.eofSpan(),
openParen, openParen,
found: this.found(), found: this.found(),
}); });
return ConcreteSyntax.list(values, this.spanFrom(start)); return ConcreteSyntax.list(values, this.cursor.spanFrom(start));
} }
const before = this.index; const before = this.cursor.checkpoint();
const value = this.parseExpr(); const value = this.parseExpr();
if (value) { if (value) {
values.push(value); values.push(value);
@ -180,14 +176,14 @@ class Parser {
} }
private parseNumber(): ConcreteSyntax { private parseNumber(): ConcreteSyntax {
const start = this.index; const start = this.cursor.checkpoint();
while (isDigit(this.peekOrInvalid())) { while (isDigit(this.cursor.peek() ?? -1)) {
this.advance(); this.cursor.advance();
} }
const span = this.spanFrom(start); const span = this.cursor.spanFrom(start);
const text = this.slice(span); const text = this.cursor.slice(span);
const value = Number(text); const value = Number(text);
if (!Number.isSafeInteger(value)) { if (!Number.isSafeInteger(value)) {
@ -203,99 +199,57 @@ class Parser {
} }
private parseIdentifier(): ConcreteSyntax { private parseIdentifier(): ConcreteSyntax {
const start = this.index; const start = this.cursor.checkpoint();
this.advance(); this.cursor.advance();
while (isIdentifierPart(this.peekOrInvalid())) { while (isIdentifierPart(this.cursor.peek() ?? -1)) {
this.advance(); this.cursor.advance();
} }
const span = this.spanFrom(start); const span = this.cursor.spanFrom(start);
return ConcreteSyntax.identifier(this.slice(span), span); return ConcreteSyntax.identifier(this.cursor.slice(span), span);
} }
private recoverDocument(failedAt: CodePointIndex): void { private recoverDocument(failedAt: CodePointIndex): void {
if (this.index === failedAt) this.advance(); if (this.cursor.current() === failedAt) this.cursor.advance();
while (!this.isAtEnd()) { while (!this.cursor.isAtEnd()) {
const cp = this.peek(); const cp = this.cursor.peek();
if (cp === CLOSE_PAREN) { if (cp === CLOSE_PAREN) {
this.errors.push({ this.errors.push({
tag: "unexpected-close-paren", tag: "unexpected-close-paren",
span: this.currentSpan(), span: this.cursor.currentSpan(),
}); });
this.advance(); this.cursor.advance();
return; return;
} }
if (isExpressionStart(cp)) return; if (isExpressionStart(cp)) return;
this.advance(); this.cursor.advance();
} }
} }
private recoverList(failedAt: CodePointIndex): void { private recoverList(failedAt: CodePointIndex): void {
if (this.index === failedAt) this.advance(); if (this.cursor.current() === failedAt) this.cursor.advance();
while (!this.isAtEnd()) { while (!this.cursor.isAtEnd()) {
const cp = this.peek(); const cp = this.cursor.peek();
if (cp === CLOSE_PAREN || isExpressionStart(cp)) return; if (cp === CLOSE_PAREN || isExpressionStart(cp)) return;
this.advance(); this.cursor.advance();
} }
} }
private skipWhitespace(): void { private skipWhitespace(): void {
while (isWhitespace(this.peekOrInvalid())) { while (isAsciiWhitespace(this.cursor.peek() ?? -1)) {
this.advance(); this.cursor.advance();
} }
} }
private peek(): CodePoint | undefined {
if (this.index >= this.region.span.end.index) return undefined;
return this.region.codePointAt(this.index);
}
private peekOrInvalid(): CodePoint {
return this.peek() ?? -1;
}
private advance(): CodePoint | undefined {
const cp = this.peek();
if (cp === undefined) return undefined;
this.index += 1;
return cp;
}
private isAtEnd(): boolean {
return this.index >= this.region.span.end.index;
}
private spanFrom(start: CodePointIndex): CodePointSpan {
return { start, end: this.index };
}
private currentSpan(): CodePointSpan {
const start = this.index;
const end = this.isAtEnd() ? start : start + 1;
return { start, end };
}
private eofSpan(): CodePointSpan {
return { start: this.region.span.end.index, end: this.region.span.end.index };
}
private found(): FoundSyntax { private found(): FoundSyntax {
const cp = this.peek(); const cp = this.cursor.peek();
if (cp === undefined) return { tag: "eof", span: this.eofSpan() }; if (cp === undefined) return { tag: "eof", span: this.cursor.eofSpan() };
return { tag: "code-point", value: cp, span: this.currentSpan() }; return { tag: "code-point", value: cp, span: this.cursor.currentSpan() };
} }
private slice(span: CodePointSpan): string {
return this.region.source.sliceByCp(span.start, span.end);
}
}
function isWhitespace(cp: CodePoint): boolean {
return cp === SPACE || cp === TAB || cp === NEW_LINE || cp === CARRIAGE_RETURN;
} }
function isExpressionStart(cp: CodePoint | undefined): boolean { function isExpressionStart(cp: CodePoint | undefined): boolean {
@ -303,14 +257,9 @@ function isExpressionStart(cp: CodePoint | undefined): boolean {
} }
function isIdentifierStart(cp: CodePoint): boolean { function isIdentifierStart(cp: CodePoint): boolean {
return isAsciiLetter(cp) || cp === DASH || cp === UNDERSCORE; return isAsciiAlpha(cp) || cp === DASH || cp === UNDERSCORE;
} }
function isIdentifierPart(cp: CodePoint): boolean { function isIdentifierPart(cp: CodePoint): boolean {
return isIdentifierStart(cp) || isDigit(cp); return isAsciiAlphanumeric(cp) || cp === DASH || cp === UNDERSCORE;
}
function isAsciiLetter(cp: CodePoint): boolean {
return (LOWERCASE_A <= cp && cp <= LOWERCASE_Z)
|| (UPPERCASE_A <= cp && cp <= UPPERCASE_Z);
} }

View file

@ -5,6 +5,7 @@ import {
NEW_LINE, NEW_LINE,
SPACE, SPACE,
TAB, TAB,
containsIndex,
} from 'source-region'; } from 'source-region';
import type { import type {
CodePoint, CodePoint,
@ -123,8 +124,8 @@ function makeSourceGrid(source: SourceText, region: SourceRegion): SourceGridMod
for (let lineNo = region.span.start.line; lineNo <= region.span.end.line; lineNo++) { for (let lineNo = region.span.start.line; lineNo <= region.span.end.line; lineNo++) {
const range = source.getLineRange(lineNo); const range = source.getLineRange(lineNo);
const start = Math.max(range.start, region.span.start.index); const start = Math.max(range.start, region.codePointSpan.start);
const end = Math.min(range.end, region.span.end.index); const end = Math.min(range.end, region.codePointSpan.end);
const cells: SourceGridCell[] = []; const cells: SourceGridCell[] = [];
for (let index = start; index < end; index++) { for (let index = start; index < end; index++) {
@ -183,8 +184,7 @@ function cellTitle(cell: SourceGridCell, annotations: SourceGridAnnotation[]): s
function annotationsForCell(cell: SourceGridCell, annotations: SourceGridAnnotation[]): SourceGridAnnotation[] { function annotationsForCell(cell: SourceGridCell, annotations: SourceGridAnnotation[]): SourceGridAnnotation[] {
return annotations.filter((annotation) => return annotations.filter((annotation) =>
annotation.span.start < annotation.span.end annotation.span.start < annotation.span.end
&& annotation.span.start <= cell.index && containsIndex(annotation.span, cell.index)
&& cell.index < annotation.span.end
); );
} }