Develop a partial syntax parser.
This commit is contained in:
parent
84cfc5863e
commit
b2e96b9a22
9 changed files with 721 additions and 257 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
export { parseDocument } from './parser';
|
export { parseDocument } from './parser';
|
||||||
export type { ParseDocumentResult } from './parser';
|
export type { ParseDocumentResult } from './parser';
|
||||||
export type { FoundSyntax, ParseError } from './parse_errors';
|
export type { FoundSyntax, ParseError } from './parse_errors';
|
||||||
export { ConcreteSyntax, Expr } from './syntax';
|
export * from './syntax';
|
||||||
export type { ConcreteSyntax as ConcreteSyntaxNode, Expr as ExprNode } from './syntax';
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,21 @@ export type ParseError =
|
||||||
found: FoundSyntax;
|
found: FoundSyntax;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: "expected-close-paren";
|
tag: "expected-close-delimiter";
|
||||||
span: CodePointSpan;
|
span: CodePointSpan;
|
||||||
openParen: CodePointSpan;
|
open: CodePointSpan;
|
||||||
|
expected: "paren" | "bracket";
|
||||||
found: FoundSyntax;
|
found: FoundSyntax;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: "unexpected-close-paren";
|
tag: "unexpected-close-delimiter";
|
||||||
span: CodePointSpan;
|
span: CodePointSpan;
|
||||||
|
delimiter: "paren" | "bracket";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
tag: "expected-list-separator";
|
||||||
|
span: CodePointSpan;
|
||||||
|
found: FoundSyntax;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
tag: "unexpected-code-point";
|
tag: "unexpected-code-point";
|
||||||
|
|
@ -25,7 +32,7 @@ export type ParseError =
|
||||||
tag: "invalid-number";
|
tag: "invalid-number";
|
||||||
span: CodePointSpan;
|
span: CodePointSpan;
|
||||||
text: string;
|
text: string;
|
||||||
reason: "unsafe-integer";
|
reason: "unsafe-integer" | "identifier-suffix";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FoundSyntax =
|
export type FoundSyntax =
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { CodePointString, sourceText } from 'source-region';
|
import { CodePointString, sourceText } from 'source-region';
|
||||||
import { parseDocument } from './parser';
|
import { parseDocument } from './parser';
|
||||||
import { matchCodePointString } from './recognizers';
|
import { matchCodePointString } from './recognizers';
|
||||||
import { Expr } from './syntax';
|
import { Program, programOf } from './syntax';
|
||||||
|
|
||||||
// === Experiments ===
|
// === Experiments ===
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ function experiment00_emptyDocument(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function experiment01_topLevelExpressions(): void {
|
function experiment01_topLevelExpressions(): void {
|
||||||
logParse("top-level expressions", "foo 123 (bar baz_1 qux-2)");
|
logParse("top-level expressions", "foo 123 (bar baz_1 qux-2) [a, b, c]");
|
||||||
}
|
}
|
||||||
|
|
||||||
function experiment02_nestedLists(): void {
|
function experiment02_nestedLists(): void {
|
||||||
|
|
@ -42,12 +42,24 @@ function experiment07_matchCodePointString(): void {
|
||||||
console.log("cursor", cursor.current());
|
console.log("cursor", cursor.current());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function experiment08_squareListSeparator(): void {
|
||||||
|
logParse("square list separator", "[a, b c, d]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function experiment09_invalidNumberFragment(): void {
|
||||||
|
logParse("invalid number fragment", "123fasd");
|
||||||
|
}
|
||||||
|
|
||||||
|
function experiment10_repeatedLeadingComma(): void {
|
||||||
|
logParse("repeated leading comma", "[, , foo, bar]");
|
||||||
|
}
|
||||||
|
|
||||||
function logParse(name: string, input: string): void {
|
function logParse(name: string, input: string): void {
|
||||||
const region = sourceText(input).fullRegion();
|
const region = sourceText(input).fullRegion();
|
||||||
const result = parseDocument(region);
|
const result = parseDocument(region);
|
||||||
console.log(`==== parser:${name} ====`);
|
console.log(`==== parser:${name} ====`);
|
||||||
console.log(input);
|
console.log(input);
|
||||||
console.log(result.values.map(Expr.show));
|
console.log(result.syntax.tag, Program.show(programOf(result.syntax)));
|
||||||
console.dir(result.errors, { depth: null });
|
console.dir(result.errors, { depth: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,4 +72,7 @@ function logParse(name: string, input: string): void {
|
||||||
experiment05_recoverInsideList,
|
experiment05_recoverInsideList,
|
||||||
experiment06_unicodeSpans,
|
experiment06_unicodeSpans,
|
||||||
experiment07_matchCodePointString,
|
experiment07_matchCodePointString,
|
||||||
|
experiment08_squareListSeparator,
|
||||||
|
experiment09_invalidNumberFragment,
|
||||||
|
experiment10_repeatedLeadingComma,
|
||||||
].forEach((experiment) => experiment());
|
].forEach((experiment) => experiment());
|
||||||
|
|
|
||||||
302
src/parser.ts
302
src/parser.ts
|
|
@ -14,20 +14,34 @@ import type {
|
||||||
} from 'source-region';
|
} from 'source-region';
|
||||||
import type { FoundSyntax, ParseError } from './parse_errors';
|
import type { FoundSyntax, ParseError } from './parse_errors';
|
||||||
import { consumeWhile, consumeWhile1, skipWhile } from './recognizers';
|
import { consumeWhile, consumeWhile1, skipWhile } from './recognizers';
|
||||||
import { ConcreteSyntax } from './syntax';
|
import {
|
||||||
|
ConcreteError,
|
||||||
|
ConcreteSyntaxResult,
|
||||||
|
DelimiterToken,
|
||||||
|
Expr,
|
||||||
|
ListItem,
|
||||||
|
Program,
|
||||||
|
} from './syntax';
|
||||||
|
import type {
|
||||||
|
ConcreteInfo,
|
||||||
|
ListItem as ListItemType,
|
||||||
|
PartialConcreteSyntax,
|
||||||
|
ValidConcreteSyntax,
|
||||||
|
Expr as ExprType,
|
||||||
|
} from './syntax';
|
||||||
|
|
||||||
// Whitespace convention:
|
// Whitespace convention:
|
||||||
// - parseDocument consumes leading whitespace before each top-level expression.
|
// - parseDocument consumes leading whitespace before each top-level expression.
|
||||||
// - parseExpr assumes leading whitespace has already been consumed.
|
// - parseExpr assumes leading whitespace has already been consumed.
|
||||||
// - Successful expression parsers stop immediately after the expression.
|
// - Successful expression parsers stop immediately after the expression.
|
||||||
// - parseList owns whitespace between list elements and before the closing paren.
|
// - list parsers own whitespace between list items and before the closing delimiter.
|
||||||
//
|
//
|
||||||
// Recovery policy:
|
// Recovery policy:
|
||||||
// - At document level, invalid input is skipped until EOF or a plausible expression
|
// - Unknown expressions consume at least one code point, then panic until a
|
||||||
// start. Unexpected ")" is reported and consumed immediately.
|
// delimiter, whitespace, or plausible expression start.
|
||||||
// - Inside lists, invalid input is skipped until EOF, ")", or a plausible
|
// - Round lists do not require separators.
|
||||||
// expression start. Recovery always consumes at least one code point when it
|
// - Square lists require commas between neighboring expressions, but allow
|
||||||
// cannot stop at a synchronization point.
|
// optional leading and trailing commas.
|
||||||
//
|
//
|
||||||
// Span convention:
|
// Span convention:
|
||||||
// - Parser internals and diagnostics use CodePointSpan.
|
// - Parser internals and diagnostics use CodePointSpan.
|
||||||
|
|
@ -35,14 +49,20 @@ import { ConcreteSyntax } from './syntax';
|
||||||
|
|
||||||
const OPEN_PAREN = char('(');
|
const OPEN_PAREN = char('(');
|
||||||
const CLOSE_PAREN = char(')');
|
const CLOSE_PAREN = char(')');
|
||||||
|
const OPEN_BRACKET = char('[');
|
||||||
|
const CLOSE_BRACKET = char(']');
|
||||||
|
const COMMA = char(',');
|
||||||
const DASH = char('-');
|
const DASH = char('-');
|
||||||
const UNDERSCORE = char('_');
|
const UNDERSCORE = char('_');
|
||||||
|
|
||||||
export type ParseDocumentResult = {
|
export type ParseDocumentResult = {
|
||||||
values: ConcreteSyntax[];
|
syntax: ConcreteSyntaxResult;
|
||||||
errors: ParseError[];
|
errors: ParseError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PartialExpr = ExprType<ConcreteInfo, ConcreteError>;
|
||||||
|
type PartialListItem = ListItemType<ConcreteInfo, ConcreteError>;
|
||||||
|
|
||||||
export function parseDocument(region: SourceRegion): ParseDocumentResult {
|
export function parseDocument(region: SourceRegion): ParseDocumentResult {
|
||||||
return new Parser(region).parseDocument();
|
return new Parser(region).parseDocument();
|
||||||
}
|
}
|
||||||
|
|
@ -56,152 +76,260 @@ class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDocument(): ParseDocumentResult {
|
parseDocument(): ParseDocumentResult {
|
||||||
const values: ConcreteSyntax[] = [];
|
const expressions: PartialExpr[] = [];
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (this.cursor.isAtEnd()) break;
|
if (this.cursor.isAtEnd()) break;
|
||||||
|
|
||||||
const before = this.cursor.checkpoint();
|
expressions.push(this.parseExpr());
|
||||||
const value = this.parseExpr();
|
|
||||||
if (value) {
|
|
||||||
values.push(value);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recoverDocument(before);
|
const program = Program.make(expressions, { span: this.region.codePointSpan });
|
||||||
|
return {
|
||||||
|
syntax: this.errors.length === 0
|
||||||
|
? ConcreteSyntaxResult.valid(program as ValidConcreteSyntax)
|
||||||
|
: ConcreteSyntaxResult.invalid(program as PartialConcreteSyntax),
|
||||||
|
errors: this.errors,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { values, errors: this.errors };
|
private parseExpr(): PartialExpr {
|
||||||
}
|
|
||||||
|
|
||||||
private parseExpr(): ConcreteSyntax | undefined {
|
|
||||||
const cp = this.cursor.peek();
|
const cp = this.cursor.peek();
|
||||||
|
|
||||||
if (cp === undefined) {
|
if (cp === undefined) {
|
||||||
this.errors.push({
|
return this.errorExpression(this.makeError({
|
||||||
tag: "expected-expression",
|
tag: "expected-expression",
|
||||||
span: this.cursor.eofSpan(),
|
span: this.cursor.eofSpan(),
|
||||||
found: this.found(),
|
found: this.found(),
|
||||||
});
|
}));
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cp === CLOSE_PAREN) {
|
if (cp === CLOSE_PAREN || cp === CLOSE_BRACKET) {
|
||||||
this.errors.push({
|
const delimiter = cp === CLOSE_PAREN ? "paren" : "bracket";
|
||||||
tag: "unexpected-close-paren",
|
const span = this.cursor.currentSpan();
|
||||||
span: this.cursor.currentSpan(),
|
this.cursor.advance();
|
||||||
});
|
return this.errorExpression(this.makeError({
|
||||||
return undefined;
|
tag: "unexpected-close-delimiter",
|
||||||
|
span,
|
||||||
|
delimiter,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cp === OPEN_PAREN) return this.parseList();
|
if (cp === OPEN_PAREN) return this.parseRoundList();
|
||||||
|
if (cp === OPEN_BRACKET) return this.parseSquareList();
|
||||||
if (isDigit(cp)) return this.parseNumber();
|
if (isDigit(cp)) return this.parseNumber();
|
||||||
if (isIdentifierStart(cp)) return this.parseIdentifier();
|
if (isIdentifierStart(cp)) return this.parseIdentifier();
|
||||||
|
|
||||||
this.errors.push({
|
return this.parseUnknownExpression();
|
||||||
tag: "expected-expression",
|
|
||||||
span: this.cursor.currentSpan(),
|
|
||||||
found: this.found(),
|
|
||||||
});
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseList(): ConcreteSyntax | undefined {
|
private parseRoundList(): PartialExpr {
|
||||||
const start = this.cursor.checkpoint();
|
const start = this.cursor.checkpoint();
|
||||||
const openParen = this.cursor.currentSpan();
|
const open = DelimiterToken.openParen(this.cursor.currentSpan());
|
||||||
this.cursor.advance();
|
this.cursor.advance();
|
||||||
|
|
||||||
const values: ConcreteSyntax[] = [];
|
const items: PartialListItem[] = [];
|
||||||
|
|
||||||
// === Body Parsing ===
|
|
||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
const cp = this.cursor.peek();
|
const cp = this.cursor.peek();
|
||||||
if (cp === CLOSE_PAREN) {
|
if (cp === CLOSE_PAREN) {
|
||||||
|
const close = DelimiterToken.closeParen(this.cursor.currentSpan());
|
||||||
this.cursor.advance();
|
this.cursor.advance();
|
||||||
return ConcreteSyntax.list(values, this.cursor.spanFrom(start));
|
return Expr.list(open, items, this.cursor.spanFrom(start), close);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp === CLOSE_BRACKET) {
|
||||||
|
const close = DelimiterToken.closeBracket(this.cursor.currentSpan());
|
||||||
|
const error = this.makeError({
|
||||||
|
tag: "expected-close-delimiter",
|
||||||
|
span: this.cursor.currentSpan(),
|
||||||
|
open: open.span,
|
||||||
|
expected: "paren",
|
||||||
|
found: this.found(),
|
||||||
|
});
|
||||||
|
this.cursor.advance();
|
||||||
|
return Expr.list(open, items, this.cursor.spanFrom(start), close, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cp === undefined) {
|
if (cp === undefined) {
|
||||||
this.errors.push({
|
const error = this.makeError({
|
||||||
tag: "expected-close-paren",
|
tag: "expected-close-delimiter",
|
||||||
span: this.cursor.eofSpan(),
|
span: this.cursor.eofSpan(),
|
||||||
openParen,
|
open: open.span,
|
||||||
|
expected: "paren",
|
||||||
found: this.found(),
|
found: this.found(),
|
||||||
});
|
});
|
||||||
return ConcreteSyntax.list(values, this.cursor.spanFrom(start));
|
return Expr.list(open, items, this.cursor.spanFrom(start), undefined, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const before = this.cursor.checkpoint();
|
items.push(this.parseExpr());
|
||||||
const value = this.parseExpr();
|
}
|
||||||
if (value) {
|
}
|
||||||
values.push(value);
|
|
||||||
|
private parseSquareList(): PartialExpr {
|
||||||
|
const start = this.cursor.checkpoint();
|
||||||
|
const open = DelimiterToken.openBracket(this.cursor.currentSpan());
|
||||||
|
this.cursor.advance();
|
||||||
|
|
||||||
|
const items: PartialListItem[] = [];
|
||||||
|
let sawExpression = false;
|
||||||
|
let sawLeadingComma = false;
|
||||||
|
let needsSeparator = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
const cp = this.cursor.peek();
|
||||||
|
if (cp === CLOSE_BRACKET) {
|
||||||
|
const close = DelimiterToken.closeBracket(this.cursor.currentSpan());
|
||||||
|
this.cursor.advance();
|
||||||
|
return Expr.list(open, items, this.cursor.spanFrom(start), close);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp === CLOSE_PAREN) {
|
||||||
|
const close = DelimiterToken.closeParen(this.cursor.currentSpan());
|
||||||
|
const error = this.makeError({
|
||||||
|
tag: "expected-close-delimiter",
|
||||||
|
span: this.cursor.currentSpan(),
|
||||||
|
open: open.span,
|
||||||
|
expected: "bracket",
|
||||||
|
found: this.found(),
|
||||||
|
});
|
||||||
|
this.cursor.advance();
|
||||||
|
return Expr.list(open, items, this.cursor.spanFrom(start), close, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp === undefined) {
|
||||||
|
const error = this.makeError({
|
||||||
|
tag: "expected-close-delimiter",
|
||||||
|
span: this.cursor.eofSpan(),
|
||||||
|
open: open.span,
|
||||||
|
expected: "bracket",
|
||||||
|
found: this.found(),
|
||||||
|
});
|
||||||
|
return Expr.list(open, items, this.cursor.spanFrom(start), undefined, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsSeparator) {
|
||||||
|
if (cp === COMMA) {
|
||||||
|
this.cursor.advance();
|
||||||
|
needsSeparator = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recoverList(before);
|
const error = this.makeError({
|
||||||
|
tag: "expected-list-separator",
|
||||||
|
span: this.cursor.currentSpan(),
|
||||||
|
found: this.found(),
|
||||||
|
});
|
||||||
|
items.push(ListItem.errorSeparator(error, this.cursor.currentSpan()));
|
||||||
|
needsSeparator = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cp === COMMA) {
|
||||||
|
const commaSpan = this.cursor.currentSpan();
|
||||||
|
this.cursor.advance();
|
||||||
|
|
||||||
|
if (sawExpression) {
|
||||||
|
const error = this.makeError({
|
||||||
|
tag: "expected-expression",
|
||||||
|
span: commaSpan,
|
||||||
|
found: { tag: "code-point", value: COMMA, span: commaSpan },
|
||||||
|
});
|
||||||
|
items.push(this.errorExpression(error, commaSpan));
|
||||||
|
} else if (sawLeadingComma) {
|
||||||
|
const error = this.makeError({
|
||||||
|
tag: "expected-expression",
|
||||||
|
span: commaSpan,
|
||||||
|
found: { tag: "code-point", value: COMMA, span: commaSpan },
|
||||||
|
});
|
||||||
|
items.push(this.errorExpression(error, commaSpan));
|
||||||
|
} else {
|
||||||
|
sawLeadingComma = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(this.parseExpr());
|
||||||
|
sawExpression = true;
|
||||||
|
needsSeparator = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseNumber(): ConcreteSyntax {
|
private parseNumber(): PartialExpr {
|
||||||
|
const start = this.cursor.checkpoint();
|
||||||
const match = consumeWhile1(this.cursor, isDigit);
|
const match = consumeWhile1(this.cursor, isDigit);
|
||||||
if (match.tag === "none") {
|
if (match.tag === "none") {
|
||||||
throw new Error("parseNumber called when cursor is not at a number");
|
throw new Error("parseNumber called when cursor is not at a number");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isIdentifierStart(this.cursor.peek() ?? -1)) {
|
||||||
|
consumeWhile(this.cursor, isIdentifierPart);
|
||||||
|
const span = this.cursor.spanFrom(start);
|
||||||
|
const text = this.cursor.slice(span);
|
||||||
|
return Expr.errorNumber(this.makeError({
|
||||||
|
tag: "invalid-number",
|
||||||
|
span,
|
||||||
|
text,
|
||||||
|
reason: "identifier-suffix",
|
||||||
|
}), span);
|
||||||
|
}
|
||||||
|
|
||||||
const { span, text } = match;
|
const { span, text } = match;
|
||||||
const value = Number(text);
|
const value = Number(text);
|
||||||
|
|
||||||
if (!Number.isSafeInteger(value)) {
|
if (!Number.isSafeInteger(value)) {
|
||||||
this.errors.push({
|
return Expr.errorNumber(this.makeError({
|
||||||
tag: "invalid-number",
|
tag: "invalid-number",
|
||||||
span,
|
span,
|
||||||
text,
|
text,
|
||||||
reason: "unsafe-integer",
|
reason: "unsafe-integer",
|
||||||
});
|
}), span);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConcreteSyntax.number(value, span);
|
return Expr.number(value, span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseIdentifier(): ConcreteSyntax {
|
private parseIdentifier(): PartialExpr {
|
||||||
const start = this.cursor.checkpoint();
|
const start = this.cursor.checkpoint();
|
||||||
this.cursor.advance();
|
this.cursor.advance();
|
||||||
|
|
||||||
consumeWhile(this.cursor, isIdentifierPart);
|
consumeWhile(this.cursor, isIdentifierPart);
|
||||||
const span = this.cursor.spanFrom(start);
|
const span = this.cursor.spanFrom(start);
|
||||||
return ConcreteSyntax.identifier(this.cursor.slice(span), span);
|
return Expr.identifier(this.cursor.slice(span), span);
|
||||||
}
|
}
|
||||||
|
|
||||||
private recoverDocument(failedAt: CodePointIndex): void {
|
private parseUnknownExpression(): PartialExpr {
|
||||||
if (this.cursor.current() === failedAt) this.cursor.advance();
|
const start = this.cursor.checkpoint();
|
||||||
|
const focus = this.cursor.currentSpan();
|
||||||
|
|
||||||
while (!this.cursor.isAtEnd()) {
|
this.cursor.advance();
|
||||||
|
while (true) {
|
||||||
const cp = this.cursor.peek();
|
const cp = this.cursor.peek();
|
||||||
if (cp === CLOSE_PAREN) {
|
if (
|
||||||
this.errors.push({
|
cp === undefined
|
||||||
tag: "unexpected-close-paren",
|
|| isAsciiWhitespace(cp)
|
||||||
span: this.cursor.currentSpan(),
|
|| isClosingDelimiter(cp)
|
||||||
});
|
|| isExpressionStart(cp)
|
||||||
this.cursor.advance();
|
) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpressionStart(cp)) return;
|
|
||||||
this.cursor.advance();
|
this.cursor.advance();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private recoverList(failedAt: CodePointIndex): void {
|
const panickedOver = this.cursor.spanFrom(start);
|
||||||
if (this.cursor.current() === failedAt) this.cursor.advance();
|
const error = this.makeError({
|
||||||
|
tag: "expected-expression",
|
||||||
|
span: focus,
|
||||||
|
found: { tag: "code-point", value: this.region.source.codePointAt(focus.start), span: focus },
|
||||||
|
}, panickedOver);
|
||||||
|
|
||||||
while (!this.cursor.isAtEnd()) {
|
return this.errorExpression(error, panickedOver);
|
||||||
const cp = this.cursor.peek();
|
|
||||||
if (cp === CLOSE_PAREN || isExpressionStart(cp)) return;
|
|
||||||
this.cursor.advance();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private skipWhitespace(): void {
|
private skipWhitespace(): void {
|
||||||
|
|
@ -213,10 +341,32 @@ class Parser {
|
||||||
if (cp === undefined) return { tag: "eof", span: this.cursor.eofSpan() };
|
if (cp === undefined) return { tag: "eof", span: this.cursor.eofSpan() };
|
||||||
return { tag: "code-point", value: cp, span: this.cursor.currentSpan() };
|
return { tag: "code-point", value: cp, span: this.cursor.currentSpan() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private makeError(error: ParseError, panickedOver?: CodePointSpan): ConcreteError {
|
||||||
|
this.errors.push(error);
|
||||||
|
return ConcreteError.single({
|
||||||
|
span: error.span,
|
||||||
|
error,
|
||||||
|
panickedOver,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private errorExpression(error: ConcreteError, span?: CodePointSpan): PartialExpr {
|
||||||
|
return Expr.errorExpression(error, span ?? error[0].span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExpressionStart(cp: CodePoint | undefined): boolean {
|
function isExpressionStart(cp: CodePoint | undefined): boolean {
|
||||||
return cp !== undefined && (cp === OPEN_PAREN || isDigit(cp) || isIdentifierStart(cp));
|
return cp !== undefined && (
|
||||||
|
cp === OPEN_PAREN
|
||||||
|
|| cp === OPEN_BRACKET
|
||||||
|
|| isDigit(cp)
|
||||||
|
|| isIdentifierStart(cp)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClosingDelimiter(cp: CodePoint): boolean {
|
||||||
|
return cp === CLOSE_PAREN || cp === CLOSE_BRACKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIdentifierStart(cp: CodePoint): boolean {
|
function isIdentifierStart(cp: CodePoint): boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,36 @@
|
||||||
.section-label {
|
.structure-tree {
|
||||||
margin: var(--gap-2) 0;
|
min-height: 0;
|
||||||
text-transform: uppercase;
|
overflow: auto;
|
||||||
|
padding: var(--gap-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-list,
|
.syntax-node {
|
||||||
.expr-list,
|
|
||||||
.list-children {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-card,
|
|
||||||
.expr-node {
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--panel-raised);
|
background: var(--panel-raised);
|
||||||
}
|
|
||||||
|
|
||||||
.error-card {
|
|
||||||
padding: var(--gap-3);
|
|
||||||
border-color: oklch(42% 0.05 28);
|
|
||||||
background: var(--error-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expr-node {
|
|
||||||
padding: var(--gap-2);
|
padding: var(--gap-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-card:hover,
|
.syntax-node:hover {
|
||||||
.expr-node:hover {
|
|
||||||
border-color: var(--border-strong);
|
border-color: var(--border-strong);
|
||||||
background: var(--accent-bg);
|
background: var(--accent-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.syntax-error-node {
|
||||||
|
border-color: oklch(42% 0.05 28);
|
||||||
|
background: var(--error-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header,
|
||||||
|
.list-node-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--gap-2);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.item-title,
|
.item-title,
|
||||||
.node-kind {
|
.node-kind,
|
||||||
|
.error-title {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
|
@ -42,7 +38,7 @@
|
||||||
|
|
||||||
.node-kind {
|
.node-kind {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-width: 4.5rem;
|
min-width: 7rem;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,15 +48,65 @@
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-node-header {
|
.item-meta {
|
||||||
display: flex;
|
color: var(--text-muted);
|
||||||
align-items: baseline;
|
font-size: var(--text-xs);
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--gap-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-children {
|
.list-children,
|
||||||
|
.concrete-error-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap-2);
|
||||||
margin-top: var(--gap-2);
|
margin-top: var(--gap-2);
|
||||||
padding-left: var(--gap-3);
|
padding-left: var(--gap-3);
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delimiter-row,
|
||||||
|
.span-chip-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--gap-2);
|
||||||
|
margin-top: var(--gap-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.concrete-error {
|
||||||
|
padding: var(--gap-2);
|
||||||
|
border: 1px solid oklch(42% 0.05 28);
|
||||||
|
background: oklch(21% 0.035 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--gap-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: var(--gap-1) var(--gap-2);
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--panel);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-chip:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 0.65rem;
|
||||||
|
height: 0.65rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-valid {
|
||||||
|
background: oklch(72% 0.12 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-invalid {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
|
||||||
176
src/syntax.ts
176
src/syntax.ts
|
|
@ -1,59 +1,171 @@
|
||||||
import type { CodePointSpan } from 'source-region';
|
import type { CodePointSpan } from 'source-region';
|
||||||
|
import type { ParseError } from './parse_errors';
|
||||||
|
|
||||||
export type ConcreteSyntax = Expr<{ span: CodePointSpan }>
|
export type ConcreteInfo = { span: CodePointSpan };
|
||||||
|
|
||||||
export type Expr<A> =
|
export type ConcreteSyntaxResult =
|
||||||
| { tag: "literal", value: Literal } & A
|
| { tag: "valid", value: ValidConcreteSyntax }
|
||||||
| { tag: "list", values: Expr<A>[] } & A
|
| { tag: "invalid", value: PartialConcreteSyntax }
|
||||||
|
|
||||||
export namespace ConcreteSyntax {
|
export type ValidConcreteSyntax = Program<ConcreteInfo, never>
|
||||||
export function number(value: number, span: CodePointSpan): ConcreteSyntax {
|
export type PartialConcreteSyntax = Program<ConcreteInfo, ConcreteError>
|
||||||
return { tag: "literal", value: { tag: "number", value }, span };
|
|
||||||
|
export type ConcreteError = ConcreteErrorNode[] // Convention: can't be empty.
|
||||||
|
export type ConcreteErrorNode = {
|
||||||
|
span: CodePointSpan,
|
||||||
|
error: ParseError,
|
||||||
|
panickedOver?: CodePointSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ConcreteError {
|
||||||
|
export function single(node: ConcreteErrorNode): ConcreteError {
|
||||||
|
return [node];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DelimiterToken =
|
||||||
|
| { tag: "open-paren"; span: CodePointSpan }
|
||||||
|
| { tag: "close-paren"; span: CodePointSpan }
|
||||||
|
| { tag: "open-bracket"; span: CodePointSpan }
|
||||||
|
| { tag: "close-bracket"; span: CodePointSpan };
|
||||||
|
|
||||||
|
export namespace DelimiterToken {
|
||||||
|
export function openParen(span: CodePointSpan): DelimiterToken {
|
||||||
|
return { tag: "open-paren", span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function identifier(value: Identifier, span: CodePointSpan): ConcreteSyntax {
|
export function closeParen(span: CodePointSpan): DelimiterToken {
|
||||||
return { tag: "literal", value: { tag: "identifier", value }, span };
|
return { tag: "close-paren", span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function list(values: ConcreteSyntax[], span: CodePointSpan): ConcreteSyntax {
|
export function openBracket(span: CodePointSpan): DelimiterToken {
|
||||||
return { tag: "list", values, span };
|
return { tag: "open-bracket", span };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeBracket(span: CodePointSpan): DelimiterToken {
|
||||||
|
return { tag: "close-bracket", span };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Program<Info, Error> = {
|
||||||
|
tag: "program",
|
||||||
|
expressions: Expr<Info, Error>[],
|
||||||
|
error?: Error,
|
||||||
|
} & Info
|
||||||
|
|
||||||
|
export type Expr<Info, Error> =
|
||||||
|
| Literal<Info, Error>
|
||||||
|
| List<Info, Error>
|
||||||
|
| { tag: "error-expression", error: Error } & Info
|
||||||
|
|
||||||
|
export type List<Info, Error> =
|
||||||
|
{ tag: "list", open: DelimiterToken, items: ListItem<Info, Error>[], close?: DelimiterToken, error?: Error } & Info
|
||||||
|
|
||||||
|
export type ListItem<Info, Error> =
|
||||||
|
| Expr<Info, Error>
|
||||||
|
| { tag: "error-list-separator", error: Error } & Info
|
||||||
|
|
||||||
|
export type Literal<Info, Error> =
|
||||||
|
| { tag: "number", value: number } & Info
|
||||||
|
| { tag: "error-number", error: Error } & Info
|
||||||
|
| { tag: "identifier", value: Identifier } & Info
|
||||||
|
| { tag: "error-identifier", error: Error } & Info
|
||||||
|
|
||||||
|
export type Identifier = string
|
||||||
|
|
||||||
|
export namespace ConcreteSyntaxResult {
|
||||||
|
export function valid(value: ValidConcreteSyntax): ConcreteSyntaxResult {
|
||||||
|
return { tag: "valid", value };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalid(value: PartialConcreteSyntax): ConcreteSyntaxResult {
|
||||||
|
return { tag: "invalid", value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Program {
|
||||||
|
export function make<Info, Error>(
|
||||||
|
expressions: Expr<Info, Error>[],
|
||||||
|
info: Info,
|
||||||
|
error?: Error,
|
||||||
|
): Program<Info, Error> {
|
||||||
|
return error === undefined
|
||||||
|
? { tag: "program", expressions, ...info }
|
||||||
|
: { tag: "program", expressions, error, ...info };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function show<Info, Error>(program: Program<Info, Error>): string {
|
||||||
|
return program.expressions.map(Expr.show).join(" ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Expr {
|
export namespace Expr {
|
||||||
export function number(value: number): Expr<void> {
|
export function number(value: number, span: CodePointSpan): Expr<ConcreteInfo, ConcreteError> {
|
||||||
return { tag: "literal", value: { tag: "number", value } };
|
return { tag: "number", value, span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function identifier(value: Identifier): Expr<void> {
|
export function errorNumber(error: ConcreteError, span: CodePointSpan): Expr<ConcreteInfo, ConcreteError> {
|
||||||
return { tag: "literal", value: { tag: "identifier", value } };
|
return { tag: "error-number", error, span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function list(values: Expr<void>[]): Expr<void> {
|
export function identifier(value: Identifier, span: CodePointSpan): Expr<ConcreteInfo, ConcreteError> {
|
||||||
return { tag: "list", values };
|
return { tag: "identifier", value, span };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function show<A>(e: Expr<A>): string {
|
export function errorIdentifier(error: ConcreteError, span: CodePointSpan): Expr<ConcreteInfo, ConcreteError> {
|
||||||
switch (e.tag) {
|
return { tag: "error-identifier", error, span };
|
||||||
case "literal":
|
|
||||||
return showLiteral(e.value);
|
|
||||||
case "list":
|
|
||||||
return `(${e.values.map(show).join(" ")})`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLiteral(e: Literal): string {
|
export function list(
|
||||||
switch (e.tag) {
|
open: DelimiterToken,
|
||||||
|
items: ListItem<ConcreteInfo, ConcreteError>[],
|
||||||
|
span: CodePointSpan,
|
||||||
|
close?: DelimiterToken,
|
||||||
|
error?: ConcreteError,
|
||||||
|
): Expr<ConcreteInfo, ConcreteError> {
|
||||||
|
return { tag: "list", open, items, close, error, span };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorExpression(error: ConcreteError, span: CodePointSpan): Expr<ConcreteInfo, ConcreteError> {
|
||||||
|
return { tag: "error-expression", error, span };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function show<Info, Error>(expr: Expr<Info, Error>): string {
|
||||||
|
switch (expr.tag) {
|
||||||
case "number":
|
case "number":
|
||||||
return `${e.value}`;
|
return `${expr.value}`;
|
||||||
case "identifier":
|
case "identifier":
|
||||||
return `${e.value}`;
|
return expr.value;
|
||||||
|
case "error-number":
|
||||||
|
return "<error-number>";
|
||||||
|
case "error-identifier":
|
||||||
|
return "<error-identifier>";
|
||||||
|
case "error-expression":
|
||||||
|
return "<error-expression>";
|
||||||
|
case "list":
|
||||||
|
return showList(expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showList<Info, Error>(list: List<Info, Error>): string {
|
||||||
|
const open = list.open.tag === "open-bracket" ? "[" : "(";
|
||||||
|
const close = list.open.tag === "open-bracket" ? "]" : ")";
|
||||||
|
const sep = list.open.tag === "open-bracket" ? ", " : " ";
|
||||||
|
return `${open}${list.items.map(ListItem.show).join(sep)}${close}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Literal =
|
export namespace ListItem {
|
||||||
| { tag: "number", value: number }
|
export function errorSeparator(error: ConcreteError, span: CodePointSpan): ListItem<ConcreteInfo, ConcreteError> {
|
||||||
| { tag: "identifier", value: Identifier }
|
return { tag: "error-list-separator", error, span };
|
||||||
|
}
|
||||||
|
|
||||||
type Identifier = string
|
export function show<Info, Error>(item: ListItem<Info, Error>): string {
|
||||||
|
if (item.tag === "error-list-separator") return "<error-separator>";
|
||||||
|
return Expr.show(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function programOf(result: ConcreteSyntaxResult): PartialConcreteSyntax {
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,35 @@
|
||||||
import { createMemo, createSignal, Show } from 'solid-js';
|
import { createMemo, createSignal } from 'solid-js';
|
||||||
import { sourceText } from 'source-region';
|
import { sourceText } from 'source-region';
|
||||||
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
|
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
|
||||||
import { parseDocument } from '../parser';
|
import { parseDocument } from '../parser';
|
||||||
import type { ParseError } from '../parse_errors';
|
import type { ParseError } from '../parse_errors';
|
||||||
import type { ConcreteSyntax } from '../syntax';
|
import { programOf } from '../syntax';
|
||||||
|
import type { ConcreteSyntaxResult, PartialConcreteSyntax } from '../syntax';
|
||||||
import { spanLabel } from './format';
|
import { spanLabel } from './format';
|
||||||
import { PaneHeader, PaneSplitter } from './Pane';
|
import { PaneHeader, PaneSplitter } from './Pane';
|
||||||
import { SourceGrid } from './SourceGrid';
|
import { SourceGrid } from './SourceGrid';
|
||||||
import type { SourceGridAnnotation } from './SourceGrid';
|
import type { SourceGridAnnotation } from './SourceGrid';
|
||||||
import { ErrorList, ExpressionList } from './SyntaxPane';
|
import { StructureTree } from './SyntaxPane';
|
||||||
import type { HoverTarget } from './types';
|
import type { HoverTarget } from './types';
|
||||||
|
|
||||||
type ParsedDocument = {
|
type ParsedDocument = {
|
||||||
source: SourceText;
|
source: SourceText;
|
||||||
region: SourceRegion;
|
region: SourceRegion;
|
||||||
values: ConcreteSyntax[];
|
syntax: ConcreteSyntaxResult;
|
||||||
|
program: PartialConcreteSyntax;
|
||||||
errors: ParseError[];
|
errors: ParseError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
|
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
|
||||||
|
|
||||||
(add 1 2)
|
[add, 1, 2]
|
||||||
|
|
||||||
(define pyth (_ x y) (+ (square x) (square y)))
|
(define pyth (_ x y) (+ (square x) (square y)))
|
||||||
|
|
||||||
foo ) @@@ (bar 1)
|
foo ) @@@ (bar 1)
|
||||||
(nested (list 123 abc_9 name-with-dash))`;
|
(nested [list, 123, abc_9, name-with-dash])
|
||||||
|
[a, b c, d]
|
||||||
|
123fasd`;
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [input, setInput] = createSignal(SAMPLE_INPUT);
|
const [input, setInput] = createSignal(SAMPLE_INPUT);
|
||||||
|
|
@ -37,7 +41,7 @@ export function App() {
|
||||||
const source = sourceText(input());
|
const source = sourceText(input());
|
||||||
const region = source.fullRegion();
|
const region = source.fullRegion();
|
||||||
const result = parseDocument(region);
|
const result = parseDocument(region);
|
||||||
return { source, region, values: result.values, errors: result.errors };
|
return { source, region, syntax: result.syntax, program: programOf(result.syntax), errors: result.errors };
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -71,23 +75,14 @@ export function App() {
|
||||||
<section class="pane structure-pane">
|
<section class="pane structure-pane">
|
||||||
<PaneHeader
|
<PaneHeader
|
||||||
title="Structure"
|
title="Structure"
|
||||||
detail={`${parsed().values.length} expressions, ${parsed().errors.length} errors`}
|
detail={`${parsed().syntax.tag}, ${parsed().program.expressions.length} expressions, ${parsed().errors.length} errors`}
|
||||||
/>
|
/>
|
||||||
<Show
|
<StructureTree
|
||||||
when={parsed().errors.length > 0}
|
program={parsed().program}
|
||||||
fallback={
|
isValid={parsed().syntax.tag === "valid"}
|
||||||
<ExpressionList
|
errorCount={parsed().errors.length}
|
||||||
values={parsed().values}
|
|
||||||
onHover={setHovered}
|
onHover={setHovered}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<ErrorList
|
|
||||||
errors={parsed().errors}
|
|
||||||
values={parsed().values}
|
|
||||||
onHover={setHovered}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PaneSplitter
|
<PaneSplitter
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,147 @@
|
||||||
import { For, Show } from 'solid-js';
|
import { For, Show } from 'solid-js';
|
||||||
import type { JSX } from 'solid-js';
|
import type { JSX } from 'solid-js';
|
||||||
import type { CodePointSpan } from 'source-region';
|
import type { CodePointSpan } from 'source-region';
|
||||||
import type { ParseError } from '../parse_errors';
|
import type {
|
||||||
import type { ConcreteSyntax } from '../syntax';
|
ConcreteError,
|
||||||
|
ConcreteErrorNode,
|
||||||
|
ConcreteInfo,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
PartialConcreteSyntax,
|
||||||
|
Expr as SyntaxExpr,
|
||||||
|
} from '../syntax';
|
||||||
import { Expr } from '../syntax';
|
import { Expr } from '../syntax';
|
||||||
import { errorDetail, errorLabel, errorTitle } from './format';
|
import { errorDetail, errorTitle, spanLabel } from './format';
|
||||||
import type { HoverTarget } from './types';
|
import type { HoverTarget } from './types';
|
||||||
|
|
||||||
export function ErrorList(props: {
|
type PartialExpr = SyntaxExpr<ConcreteInfo, ConcreteError>;
|
||||||
errors: ParseError[];
|
type PartialList = List<ConcreteInfo, ConcreteError>;
|
||||||
values: ConcreteSyntax[];
|
type PartialListItem = ListItem<ConcreteInfo, ConcreteError>;
|
||||||
|
|
||||||
|
export function StructureTree(props: {
|
||||||
|
program: PartialConcreteSyntax;
|
||||||
|
isValid: boolean;
|
||||||
|
errorCount: number;
|
||||||
onHover: (target: HoverTarget | undefined) => void;
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div class="scroll-stack">
|
<div class="structure-tree">
|
||||||
<div class="section-label">Errors</div>
|
|
||||||
<div class="error-list">
|
|
||||||
<For each={props.errors}>
|
|
||||||
{(error) => (
|
|
||||||
<HoverBlock
|
<HoverBlock
|
||||||
class="error-card"
|
class="syntax-node program-node"
|
||||||
label={errorLabel(error)}
|
label="program"
|
||||||
span={error.span}
|
span={props.program.span}
|
||||||
onHover={props.onHover}
|
onHover={props.onHover}
|
||||||
>
|
>
|
||||||
<div class="item-title">{errorTitle(error)}</div>
|
<div class="node-header">
|
||||||
<div class="item-meta">{errorDetail(error)}</div>
|
<span class={props.isValid ? "status-dot status-valid" : "status-dot status-invalid"} />
|
||||||
</HoverBlock>
|
<span class="node-kind">program</span>
|
||||||
)}
|
<span class="item-meta">
|
||||||
</For>
|
{props.isValid ? "valid" : "invalid"} · {props.program.expressions.length} expressions · {props.errorCount} errors
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.values.length > 0}>
|
<Show when={props.program.error}>
|
||||||
<div class="section-label">Recovered Expressions</div>
|
{(error) => <ConcreteErrorView error={error()} label="program error" onHover={props.onHover} />}
|
||||||
<ExpressionList values={props.values} onHover={props.onHover} />
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExpressionList(props: {
|
<div class="list-children">
|
||||||
values: ConcreteSyntax[];
|
<For each={props.program.expressions}>
|
||||||
onHover: (target: HoverTarget | undefined) => void;
|
{(expr, index) => (
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div class="expr-list">
|
|
||||||
<For each={props.values}>
|
|
||||||
{(value, index) => (
|
|
||||||
<ExprView
|
<ExprView
|
||||||
expr={value}
|
expr={expr}
|
||||||
label={`expression ${index() + 1}`}
|
label={`expression ${index() + 1}`}
|
||||||
onHover={props.onHover}
|
onHover={props.onHover}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</HoverBlock>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExprView(props: {
|
function ExprView(props: {
|
||||||
expr: ConcreteSyntax;
|
expr: PartialExpr;
|
||||||
label: string;
|
label: string;
|
||||||
onHover: (target: HoverTarget | undefined) => void;
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
}) {
|
}) {
|
||||||
if (props.expr.tag === "literal") {
|
switch (props.expr.tag) {
|
||||||
|
case "number":
|
||||||
|
case "identifier":
|
||||||
return (
|
return (
|
||||||
<HoverBlock
|
<HoverBlock
|
||||||
class="expr-node literal-node"
|
class="syntax-node literal-node"
|
||||||
label={`${props.label}: ${props.expr.value.tag}`}
|
label={`${props.label}: ${props.expr.tag}`}
|
||||||
span={props.expr.span}
|
span={props.expr.span}
|
||||||
onHover={props.onHover}
|
onHover={props.onHover}
|
||||||
>
|
>
|
||||||
<span class="node-kind">{props.expr.value.tag}</span>
|
<div class="node-header">
|
||||||
<span class="node-value">{literalValue(props.expr)}</span>
|
<span class="node-kind">{props.expr.tag}</span>
|
||||||
|
<span class="node-value">{Expr.show(props.expr)}</span>
|
||||||
|
</div>
|
||||||
</HoverBlock>
|
</HoverBlock>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
case "error-number":
|
||||||
|
case "error-identifier":
|
||||||
|
case "error-expression":
|
||||||
return (
|
return (
|
||||||
<HoverBlock
|
<HoverBlock
|
||||||
class="expr-node list-node"
|
class="syntax-node syntax-error-node"
|
||||||
label={`${props.label}: list`}
|
label={`${props.label}: ${props.expr.tag}`}
|
||||||
span={props.expr.span}
|
span={props.expr.span}
|
||||||
onHover={props.onHover}
|
onHover={props.onHover}
|
||||||
>
|
>
|
||||||
<div class="list-node-header">
|
<div class="node-header">
|
||||||
<span class="node-kind">list</span>
|
<span class="status-dot status-invalid" />
|
||||||
<span class="item-meta">{props.expr.values.length} children</span>
|
<span class="node-kind">{props.expr.tag}</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.expr.span)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ConcreteErrorView error={props.expr.error} label={props.expr.tag} onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "list":
|
||||||
|
return <ListView list={props.expr} label={props.label} onHover={props.onHover} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListView(props: {
|
||||||
|
list: PartialList;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class={props.list.error ? "syntax-node list-node syntax-error-node" : "syntax-node list-node"}
|
||||||
|
label={`${props.label}: list`}
|
||||||
|
span={props.list.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<Show when={props.list.error}>
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
</Show>
|
||||||
|
<span class="node-kind">{listLabel(props.list.open.tag)}</span>
|
||||||
|
<span class="item-meta">{props.list.items.length} items · {delimiterLabel(props.list)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delimiter-row">
|
||||||
|
<SpanChip label={props.list.open.tag} span={props.list.open.span} onHover={props.onHover} />
|
||||||
|
<Show when={props.list.close}>
|
||||||
|
{(close) => <SpanChip label={close().tag} span={close().span} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.list.error}>
|
||||||
|
{(error) => <ConcreteErrorView error={error()} label="list error" onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="list-children">
|
<div class="list-children">
|
||||||
<For each={props.expr.values}>
|
<For each={props.list.items}>
|
||||||
{(child, index) => (
|
{(item, index) => (
|
||||||
<ExprView
|
<ListItemView
|
||||||
expr={child}
|
item={item}
|
||||||
label={`${props.label}.${index() + 1}`}
|
label={`${props.label}.${index() + 1}`}
|
||||||
onHover={props.onHover}
|
onHover={props.onHover}
|
||||||
/>
|
/>
|
||||||
|
|
@ -103,6 +152,89 @@ function ExprView(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ListItemView(props: {
|
||||||
|
item: PartialListItem;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
if (props.item.tag === "error-list-separator") {
|
||||||
|
return (
|
||||||
|
<HoverBlock
|
||||||
|
class="syntax-node syntax-error-node"
|
||||||
|
label={`${props.label}: separator`}
|
||||||
|
span={props.item.span}
|
||||||
|
onHover={props.onHover}
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="status-dot status-invalid" />
|
||||||
|
<span class="node-kind">error-list-separator</span>
|
||||||
|
<span class="item-meta">{spanLabel(props.item.span)}</span>
|
||||||
|
</div>
|
||||||
|
<ConcreteErrorView error={props.item.error} label="separator error" onHover={props.onHover} />
|
||||||
|
</HoverBlock>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ExprView expr={props.item} label={props.label} onHover={props.onHover} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConcreteErrorView(props: {
|
||||||
|
error: ConcreteError;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="concrete-error-list">
|
||||||
|
<For each={props.error}>
|
||||||
|
{(node, index) => (
|
||||||
|
<ConcreteErrorNodeView
|
||||||
|
node={node}
|
||||||
|
label={`${props.label} ${index() + 1}`}
|
||||||
|
onHover={props.onHover}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConcreteErrorNodeView(props: {
|
||||||
|
node: ConcreteErrorNode;
|
||||||
|
label: string;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="concrete-error">
|
||||||
|
<div class="error-title">{errorTitle(props.node.error)}</div>
|
||||||
|
<div class="item-meta">{errorDetail(props.node.error)}</div>
|
||||||
|
<div class="span-chip-row">
|
||||||
|
<SpanChip label="focus" span={props.node.span} onHover={props.onHover} />
|
||||||
|
<Show when={props.node.panickedOver}>
|
||||||
|
{(span) => <SpanChip label="panicked over" span={span()} onHover={props.onHover} />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpanChip(props: {
|
||||||
|
label: string;
|
||||||
|
span: CodePointSpan;
|
||||||
|
onHover: (target: HoverTarget | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
class="span-chip"
|
||||||
|
type="button"
|
||||||
|
onMouseEnter={() => props.onHover({ label: props.label, span: props.span })}
|
||||||
|
onMouseLeave={() => props.onHover(undefined)}
|
||||||
|
>
|
||||||
|
<span>{props.label}</span>
|
||||||
|
<span>{spanLabel(props.span)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function HoverBlock(props: {
|
function HoverBlock(props: {
|
||||||
class: string;
|
class: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -121,6 +253,10 @@ function HoverBlock(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function literalValue(expr: ConcreteSyntax): string {
|
function listLabel(tag: string): string {
|
||||||
return expr.tag === "literal" ? Expr.show(expr) : "";
|
return tag === "open-bracket" ? "square-list" : "round-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
function delimiterLabel(list: PartialList): string {
|
||||||
|
return list.close ? `${list.open.tag} / ${list.close.tag}` : `${list.open.tag} / missing close`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ export function errorTitle(error: ParseError): string {
|
||||||
switch (error.tag) {
|
switch (error.tag) {
|
||||||
case "expected-expression":
|
case "expected-expression":
|
||||||
return "Expected expression";
|
return "Expected expression";
|
||||||
case "expected-close-paren":
|
case "expected-close-delimiter":
|
||||||
return "Expected closing paren";
|
return "Expected closing delimiter";
|
||||||
case "unexpected-close-paren":
|
case "unexpected-close-delimiter":
|
||||||
return "Unexpected closing paren";
|
return "Unexpected closing delimiter";
|
||||||
|
case "expected-list-separator":
|
||||||
|
return "Expected list separator";
|
||||||
case "unexpected-code-point":
|
case "unexpected-code-point":
|
||||||
return "Unexpected code point";
|
return "Unexpected code point";
|
||||||
case "invalid-number":
|
case "invalid-number":
|
||||||
|
|
@ -24,10 +26,12 @@ export function errorDetail(error: ParseError): string {
|
||||||
switch (error.tag) {
|
switch (error.tag) {
|
||||||
case "expected-expression":
|
case "expected-expression":
|
||||||
return `found ${foundLabel(error.found)}`;
|
return `found ${foundLabel(error.found)}`;
|
||||||
case "expected-close-paren":
|
case "expected-close-delimiter":
|
||||||
return `opened at ${spanLabel(error.openParen)}, found ${foundLabel(error.found)}`;
|
return `expected ${error.expected}, opened at ${spanLabel(error.open)}, found ${foundLabel(error.found)}`;
|
||||||
case "unexpected-close-paren":
|
case "unexpected-close-delimiter":
|
||||||
return spanLabel(error.span);
|
return `${error.delimiter} ${spanLabel(error.span)}`;
|
||||||
|
case "expected-list-separator":
|
||||||
|
return `found ${foundLabel(error.found)}`;
|
||||||
case "unexpected-code-point":
|
case "unexpected-code-point":
|
||||||
return `found ${foundLabel(error.found)}`;
|
return `found ${foundLabel(error.found)}`;
|
||||||
case "invalid-number":
|
case "invalid-number":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue