Develop a partial syntax parser.
This commit is contained in:
parent
84cfc5863e
commit
b2e96b9a22
9 changed files with 721 additions and 257 deletions
|
|
@ -1,31 +1,35 @@
|
|||
import { createMemo, createSignal, Show } from 'solid-js';
|
||||
import { createMemo, createSignal } from 'solid-js';
|
||||
import { sourceText } from 'source-region';
|
||||
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
|
||||
import { parseDocument } from '../parser';
|
||||
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 { PaneHeader, PaneSplitter } from './Pane';
|
||||
import { SourceGrid } from './SourceGrid';
|
||||
import type { SourceGridAnnotation } from './SourceGrid';
|
||||
import { ErrorList, ExpressionList } from './SyntaxPane';
|
||||
import { StructureTree } from './SyntaxPane';
|
||||
import type { HoverTarget } from './types';
|
||||
|
||||
type ParsedDocument = {
|
||||
source: SourceText;
|
||||
region: SourceRegion;
|
||||
values: ConcreteSyntax[];
|
||||
syntax: ConcreteSyntaxResult;
|
||||
program: PartialConcreteSyntax;
|
||||
errors: ParseError[];
|
||||
};
|
||||
|
||||
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
|
||||
|
||||
(add 1 2)
|
||||
[add, 1, 2]
|
||||
|
||||
(define pyth (_ x y) (+ (square x) (square y)))
|
||||
|
||||
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() {
|
||||
const [input, setInput] = createSignal(SAMPLE_INPUT);
|
||||
|
|
@ -37,7 +41,7 @@ export function App() {
|
|||
const source = sourceText(input());
|
||||
const region = source.fullRegion();
|
||||
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 (
|
||||
|
|
@ -71,23 +75,14 @@ export function App() {
|
|||
<section class="pane structure-pane">
|
||||
<PaneHeader
|
||||
title="Structure"
|
||||
detail={`${parsed().values.length} expressions, ${parsed().errors.length} errors`}
|
||||
detail={`${parsed().syntax.tag}, ${parsed().program.expressions.length} expressions, ${parsed().errors.length} errors`}
|
||||
/>
|
||||
<StructureTree
|
||||
program={parsed().program}
|
||||
isValid={parsed().syntax.tag === "valid"}
|
||||
errorCount={parsed().errors.length}
|
||||
onHover={setHovered}
|
||||
/>
|
||||
<Show
|
||||
when={parsed().errors.length > 0}
|
||||
fallback={
|
||||
<ExpressionList
|
||||
values={parsed().values}
|
||||
onHover={setHovered}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ErrorList
|
||||
errors={parsed().errors}
|
||||
values={parsed().values}
|
||||
onHover={setHovered}
|
||||
/>
|
||||
</Show>
|
||||
</section>
|
||||
|
||||
<PaneSplitter
|
||||
|
|
|
|||
|
|
@ -1,55 +1,195 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
import type { CodePointSpan } from 'source-region';
|
||||
import type { ParseError } from '../parse_errors';
|
||||
import type { ConcreteSyntax } from '../syntax';
|
||||
import type {
|
||||
ConcreteError,
|
||||
ConcreteErrorNode,
|
||||
ConcreteInfo,
|
||||
List,
|
||||
ListItem,
|
||||
PartialConcreteSyntax,
|
||||
Expr as SyntaxExpr,
|
||||
} from '../syntax';
|
||||
import { Expr } from '../syntax';
|
||||
import { errorDetail, errorLabel, errorTitle } from './format';
|
||||
import { errorDetail, errorTitle, spanLabel } from './format';
|
||||
import type { HoverTarget } from './types';
|
||||
|
||||
export function ErrorList(props: {
|
||||
errors: ParseError[];
|
||||
values: ConcreteSyntax[];
|
||||
type PartialExpr = SyntaxExpr<ConcreteInfo, ConcreteError>;
|
||||
type PartialList = List<ConcreteInfo, ConcreteError>;
|
||||
type PartialListItem = ListItem<ConcreteInfo, ConcreteError>;
|
||||
|
||||
export function StructureTree(props: {
|
||||
program: PartialConcreteSyntax;
|
||||
isValid: boolean;
|
||||
errorCount: number;
|
||||
onHover: (target: HoverTarget | undefined) => void;
|
||||
}) {
|
||||
return (
|
||||
<div class="scroll-stack">
|
||||
<div class="section-label">Errors</div>
|
||||
<div class="error-list">
|
||||
<For each={props.errors}>
|
||||
{(error) => (
|
||||
<HoverBlock
|
||||
class="error-card"
|
||||
label={errorLabel(error)}
|
||||
span={error.span}
|
||||
onHover={props.onHover}
|
||||
>
|
||||
<div class="item-title">{errorTitle(error)}</div>
|
||||
<div class="item-meta">{errorDetail(error)}</div>
|
||||
</HoverBlock>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="structure-tree">
|
||||
<HoverBlock
|
||||
class="syntax-node program-node"
|
||||
label="program"
|
||||
span={props.program.span}
|
||||
onHover={props.onHover}
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class={props.isValid ? "status-dot status-valid" : "status-dot status-invalid"} />
|
||||
<span class="node-kind">program</span>
|
||||
<span class="item-meta">
|
||||
{props.isValid ? "valid" : "invalid"} · {props.program.expressions.length} expressions · {props.errorCount} errors
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={props.values.length > 0}>
|
||||
<div class="section-label">Recovered Expressions</div>
|
||||
<ExpressionList values={props.values} onHover={props.onHover} />
|
||||
</Show>
|
||||
<Show when={props.program.error}>
|
||||
{(error) => <ConcreteErrorView error={error()} label="program error" onHover={props.onHover} />}
|
||||
</Show>
|
||||
|
||||
<div class="list-children">
|
||||
<For each={props.program.expressions}>
|
||||
{(expr, index) => (
|
||||
<ExprView
|
||||
expr={expr}
|
||||
label={`expression ${index() + 1}`}
|
||||
onHover={props.onHover}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</HoverBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpressionList(props: {
|
||||
values: ConcreteSyntax[];
|
||||
function ExprView(props: {
|
||||
expr: PartialExpr;
|
||||
label: string;
|
||||
onHover: (target: HoverTarget | undefined) => void;
|
||||
}) {
|
||||
switch (props.expr.tag) {
|
||||
case "number":
|
||||
case "identifier":
|
||||
return (
|
||||
<HoverBlock
|
||||
class="syntax-node literal-node"
|
||||
label={`${props.label}: ${props.expr.tag}`}
|
||||
span={props.expr.span}
|
||||
onHover={props.onHover}
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class="node-kind">{props.expr.tag}</span>
|
||||
<span class="node-value">{Expr.show(props.expr)}</span>
|
||||
</div>
|
||||
</HoverBlock>
|
||||
);
|
||||
|
||||
case "error-number":
|
||||
case "error-identifier":
|
||||
case "error-expression":
|
||||
return (
|
||||
<HoverBlock
|
||||
class="syntax-node syntax-error-node"
|
||||
label={`${props.label}: ${props.expr.tag}`}
|
||||
span={props.expr.span}
|
||||
onHover={props.onHover}
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class="status-dot status-invalid" />
|
||||
<span class="node-kind">{props.expr.tag}</span>
|
||||
<span class="item-meta">{spanLabel(props.expr.span)}</span>
|
||||
</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 (
|
||||
<div class="expr-list">
|
||||
<For each={props.values}>
|
||||
{(value, index) => (
|
||||
<ExprView
|
||||
expr={value}
|
||||
label={`expression ${index() + 1}`}
|
||||
<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">
|
||||
<For each={props.list.items}>
|
||||
{(item, index) => (
|
||||
<ListItemView
|
||||
item={item}
|
||||
label={`${props.label}.${index() + 1}`}
|
||||
onHover={props.onHover}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</HoverBlock>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -58,48 +198,40 @@ export function ExpressionList(props: {
|
|||
);
|
||||
}
|
||||
|
||||
function ExprView(props: {
|
||||
expr: ConcreteSyntax;
|
||||
function ConcreteErrorNodeView(props: {
|
||||
node: ConcreteErrorNode;
|
||||
label: string;
|
||||
onHover: (target: HoverTarget | undefined) => void;
|
||||
}) {
|
||||
if (props.expr.tag === "literal") {
|
||||
return (
|
||||
<HoverBlock
|
||||
class="expr-node literal-node"
|
||||
label={`${props.label}: ${props.expr.value.tag}`}
|
||||
span={props.expr.span}
|
||||
onHover={props.onHover}
|
||||
>
|
||||
<span class="node-kind">{props.expr.value.tag}</span>
|
||||
<span class="node-value">{literalValue(props.expr)}</span>
|
||||
</HoverBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverBlock
|
||||
class="expr-node list-node"
|
||||
label={`${props.label}: list`}
|
||||
span={props.expr.span}
|
||||
onHover={props.onHover}
|
||||
<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)}
|
||||
>
|
||||
<div class="list-node-header">
|
||||
<span class="node-kind">list</span>
|
||||
<span class="item-meta">{props.expr.values.length} children</span>
|
||||
</div>
|
||||
<div class="list-children">
|
||||
<For each={props.expr.values}>
|
||||
{(child, index) => (
|
||||
<ExprView
|
||||
expr={child}
|
||||
label={`${props.label}.${index() + 1}`}
|
||||
onHover={props.onHover}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</HoverBlock>
|
||||
<span>{props.label}</span>
|
||||
<span>{spanLabel(props.span)}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +253,10 @@ function HoverBlock(props: {
|
|||
);
|
||||
}
|
||||
|
||||
function literalValue(expr: ConcreteSyntax): string {
|
||||
return expr.tag === "literal" ? Expr.show(expr) : "";
|
||||
function listLabel(tag: string): string {
|
||||
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) {
|
||||
case "expected-expression":
|
||||
return "Expected expression";
|
||||
case "expected-close-paren":
|
||||
return "Expected closing paren";
|
||||
case "unexpected-close-paren":
|
||||
return "Unexpected closing paren";
|
||||
case "expected-close-delimiter":
|
||||
return "Expected closing delimiter";
|
||||
case "unexpected-close-delimiter":
|
||||
return "Unexpected closing delimiter";
|
||||
case "expected-list-separator":
|
||||
return "Expected list separator";
|
||||
case "unexpected-code-point":
|
||||
return "Unexpected code point";
|
||||
case "invalid-number":
|
||||
|
|
@ -24,10 +26,12 @@ export function errorDetail(error: ParseError): string {
|
|||
switch (error.tag) {
|
||||
case "expected-expression":
|
||||
return `found ${foundLabel(error.found)}`;
|
||||
case "expected-close-paren":
|
||||
return `opened at ${spanLabel(error.openParen)}, found ${foundLabel(error.found)}`;
|
||||
case "unexpected-close-paren":
|
||||
return spanLabel(error.span);
|
||||
case "expected-close-delimiter":
|
||||
return `expected ${error.expected}, opened at ${spanLabel(error.open)}, found ${foundLabel(error.found)}`;
|
||||
case "unexpected-close-delimiter":
|
||||
return `${error.delimiter} ${spanLabel(error.span)}`;
|
||||
case "expected-list-separator":
|
||||
return `found ${foundLabel(error.found)}`;
|
||||
case "unexpected-code-point":
|
||||
return `found ${foundLabel(error.found)}`;
|
||||
case "invalid-number":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue