Compare commits

...

10 commits

Author SHA1 Message Date
Yura Dupyn
57f666118a define basic Partial CST for JSON 2026-04-25 17:44:05 +02:00
Yura Dupyn
ef1d81f597 Delete old files. Refactor experiments 2026-04-25 16:53:38 +02:00
Yura Dupyn
321e7aa4de Json syntax 2026-04-25 16:51:05 +02:00
Yura Dupyn
d1491ec5e6 Stub stuff out. Include markdown discussion 2026-04-25 16:42:57 +02:00
Yura Dupyn
8bca6e1f20 Renaming of files 2026-04-25 16:40:11 +02:00
Yura Dupyn
a824e2d9e8 SYNTAX.md for Lisp 2026-04-25 16:34:19 +02:00
Yura Dupyn
c3edf193c4 Modularize UI, make it a bit more Lisp independent 2026-04-25 16:20:25 +02:00
Yura Dupyn
e1e1b90579 Forgot to import one thing 2026-04-25 15:38:56 +02:00
Yura Dupyn
2129c26fe5 Moving stuff around 2026-04-25 15:36:52 +02:00
Yura Dupyn
309fa373f4 Get rid of the stub file 2026-04-25 15:25:49 +02:00
29 changed files with 1335 additions and 387 deletions

286
PARTIAL_SYNTAX.md Normal file
View file

@ -0,0 +1,286 @@
# Partial Syntax Notes
This document records the current candidate design for partial concrete syntax in the Lisp parser experiment.
The goal is not a generic parser framework. The goal is to make the current toy Lisp syntax rich enough to represent recovered malformed syntax while preserving the ability to distinguish valid trees from partial trees.
## Core Constraints
- `ValidConcreteSyntax` should be a subtype of `PartialConcreteSyntax`.
- If a `PartialConcreteSyntax` contains no errors, it should be safe to coerce it to `ValidConcreteSyntax` without rebuilding the tree.
- Concrete syntax may preserve syntactic choices that are semantically irrelevant.
- Error payloads should remain structured and span-aware.
The `never` parameter is the main trick: when `Error = never`, error branches and `error?: never` fields become unconstructable.
## Current Candidate Types
```ts
export type ConcreteSyntaxResult =
| { tag: "valid"; value: ValidConcreteSyntax }
| { tag: "invalid"; value: PartialConcreteSyntax };
export type ValidConcreteSyntax =
Program<{ span: CodePointSpan }, never>;
export type PartialConcreteSyntax =
Program<{ span: CodePointSpan }, ConcreteError>;
export type ConcreteError = ConcreteErrorNode[]; // Convention: non-empty.
export namespace ConcreteError {
export function single(node: ConcreteErrorNode): ConcreteError {
return [node];
}
}
export type ConcreteErrorNode = {
span: CodePointSpan;
error: ParseError;
panickedOver?: CodePointSpan;
};
export type DelimiterToken =
| { tag: "open-paren"; span: CodePointSpan }
| { tag: "close-paren"; span: CodePointSpan }
| { tag: "open-bracket"; span: CodePointSpan }
| { tag: "close-bracket"; span: CodePointSpan };
export type Program<Info, Error> = ({
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;
```
## Error Ownership
Errors are owned by the smallest useful syntax node.
- `error-expression`: syntax that cannot reasonably be interpreted as any expression node.
- `error-number`: malformed numeric literal, such as `123fasd`.
- `error-identifier`: malformed identifier, if the language later has such cases.
- `error-list-separator`: malformed relationship between neighboring list items.
- `list.error`: structural error about the whole list, such as missing or mismatched close delimiter.
- `program.error`: top-level recovery errors that do not belong to one expression.
`ConcreteErrorNode.span` is the primary diagnostic focus. `panickedOver` is recovery/debug metadata showing what source region was skipped while recovering.
## Delimiters
Delimiter tokens are stored explicitly because this is concrete syntax.
Even if round and square lists are semantically equivalent later, the concrete tree should preserve whether the source used:
```lisp
(a b c)
[a, b, c]
```
This is useful for UI, formatting, recovery diagnostics, and syntax experiments. A later semantic AST can erase this distinction.
## Lisp Syntax Under Test
The experiment now has two list syntaxes.
Round lists have no separators:
```lisp
(a b c d)
```
Square lists require commas between neighboring elements:
```lisp
[a, b, c, d]
```
Square lists allow optional leading and trailing commas:
```lisp
[,a, b, c, d]
[a, b, c, d,]
[,a, b, c, d,]
```
Adjacent top-level expressions are allowed:
```lisp
foo(bar)
```
This is equivalent to:
```lisp
foo (bar)
```
But malformed token fragments should not silently split into valid expressions:
```lisp
123fasd
```
This should probably become an `error-number`, not `number 123` followed by `identifier fasd`.
## Examples To Drive Implementation
### Valid Program
```lisp
foo 123 (a b) [c, d, e]
```
Expected: `ConcreteSyntaxResult.valid`.
### Unexpected Top-Level Close
```lisp
foo )
```
Likely: valid `foo` plus `program.error`, or invalid program containing a top-level recovery error.
### Unknown Expression In Round List
```lisp
(foo @@@ 1)
```
Likely: `error-expression` item inside the list, with recovery continuing at `1`.
### Missing Close Delimiter
```lisp
(foo 1
```
Likely: list node with `open`, no `close`, and `list.error`.
### Mismatched Close Delimiter
```lisp
[foo)
```
Likely: list node preserving `open-bracket` and `close-paren`, plus `list.error`.
### Missing Square List Separator
```lisp
[a, b c, d]
```
Likely: `error-list-separator` between `b` and `c`.
### Extra Square List Separator
```lisp
[a,, b]
```
Possible interpretations:
- allow repeated commas as empty separators
- produce `error-list-separator`
- produce `error-expression` for a missing element
This needs a deliberate choice.
### Malformed Number
```lisp
123fasd
```
Likely: `error-number` covering the full malformed fragment.
## Recovery Strategies To Compare
### Panic Until Expression Start
Skip until a plausible expression start appears.
Good for simple garbage recovery, but may split malformed token fragments too aggressively.
### Panic Until Delimiter Or Expression Start
Inside a list, skip until:
- close delimiter
- expression start
- EOF
Good for preserving list structure.
### Panic Until Whitespace Boundary
For token-like errors, skip the rest of the non-whitespace fragment.
Useful for:
```lisp
123fasd
```
### Separator-Aware Recovery
Inside square lists, use commas and close brackets as synchronization points.
Useful for:
```lisp
[a, b c, d]
[a,, b]
```
### Delimiter-Aware Recovery
Preserve exact open and close delimiter tokens, even if they mismatch.
Useful for:
```lisp
[foo)
(foo]
```
## Current Recommendation
The current type design is good enough to try.
Implementation should focus on concrete examples rather than further type abstraction:
```lisp
123fasd
(foo @@@ 1)
(foo 1
[a, b c, d]
[foo)
```
After implementing those, the UI should reveal whether node-owned errors, `error-list-separator`, and explicit delimiter tokens feel useful or too heavy.

18
QESTIONS.md Normal file
View file

@ -0,0 +1,18 @@
# tokens in Partial Concrete Syntax
What sort of tokens should I track in the syntax?
- delimiters like e.g. in `[a, b, c]`?
- more significant separators like the `:` in `{ "foo" : a }`?
- What about groupin symbols like `{ ... }`?
- What about keywords? e.g. `fn` or `for` or `while`?
Can these questions be answered universally, or is this application dependent?
- For example, maybe when building a compiler, we don't need to track so much stuff.
- But for formatter, we probably need to track a bit more.
- But what about something like a library in a IDE that handles various transformations of the code?
# delimiter confusion
I just realized that I've been misunderstanding the word `delimiter`.
I thought that a delimiter was like the `,` in `[ a, b, c]`. But that's called properly called a separator! Or item-separator.
I thought separator and delimiter where synonyms. But it seems like a `delimiter` is actually the grouping symbols like `[` or `]`.

View file

@ -0,0 +1,16 @@
# Source Region Parser Experiment Notes
## Current Parser Experiment
- The parser works directly with `CodePointIndex` and `CodePointSpan`; rich `Span` conversion is intentionally left to rendering or reporting code.
- Structured errors are currently enough to identify the failing region and the syntactic fact that failed, without committing to message text.
- Empty documents are valid because the document grammar is a sequence of expressions.
## Source Region Observations
- `SourceText.getSpan(CodePointSpan)` is the key boundary operation: parsers can stay low-level, while diagnostics can opt into line and column information later.
- Zero-width EOF spans are important for parse errors such as an unclosed list. The existing `eofSpan` support fits this well.
- Parser clients still need a few tiny local span helpers, such as current-code-point span and EOF cursor span.
- `renderSpan` returning structured `LineView` data works well for UI rendering because the application can choose its own DOM and styling without reimplementing line slicing.
## Potential Nice-To-Haves

View file

@ -0,0 +1,273 @@
# JSON Syntax
This is a JSON-like concrete syntax used for parser and source span experiments.
The starting point is standard JSON syntax, with one deliberate experiment-friendly choice: a `Document` may contain zero or more JSON values. Standard JSON allows exactly one top-level value, but allowing a sequence makes it easier to test recovery and multiple independent syntax trees.
## Grammar
This grammar is intentionally semi-formal. `Whitespace` may appear between the major syntactic parts shown below.
```txt
Document ::= Value*
Value ::= Object
| Array
| String
| Number
| "true"
| "false"
| "null"
Object ::= "{" ObjectBody? "}"
ObjectBody ::= Member ("," Member)*
Member ::= String ":" Value
Array ::= "[" ArrayBody? "]"
ArrayBody ::= Value ("," Value)*
String ::= '"' StringChar* '"'
StringChar ::= UnescapedStringChar
| Escape
Escape ::= '\"'
| "\\"
| "\/"
| "\b"
| "\f"
| "\n"
| "\r"
| "\t"
| UnicodeEscape
UnicodeEscape ::= "\u" HexDigit HexDigit HexDigit HexDigit
Number ::= "-"? Integer Fraction? Exponent?
Integer ::= "0" | NonZeroDigit Digit*
Fraction ::= "." Digit+
Exponent ::= ("e" | "E") ("+" | "-")? Digit+
Digit ::= "0" | "1" | "2" | "3" | "4"
| "5" | "6" | "7" | "8" | "9"
NonZeroDigit ::= "1" | "2" | "3" | "4"
| "5" | "6" | "7" | "8" | "9"
HexDigit ::= Digit | "a" ... "f" | "A" ... "F"
Whitespace ::= " " | "\t" | "\n" | "\r"
```
Notes:
- `Document` may be empty in this experiment.
- Objects and arrays do not allow trailing commas.
- Object keys must be strings.
- Strings use JSON escapes; raw newline and raw carriage return are not allowed inside strings.
- Number syntax follows JSON number rules, so leading zeroes like `012` are invalid.
## Document
A document is a sequence of zero or more values.
```json
true
true false null
{"x": 1} [1, 2, 3]
```
Empty input is valid for this experiment.
## Values
A value is one of:
- object
- array
- string
- number
- `true`
- `false`
- `null`
Examples:
```json
null
true
false
"hello"
123
{"name": "Ada"}
[1, 2, 3]
```
## Objects
Objects use braces and contain zero or more comma-separated members.
```json
{}
{"x": 1}
{"x": 1, "y": 2}
{"nested": {"ok": true}}
```
Members are string keys followed by a colon and a value:
```json
"name": "Ada"
```
Invalid objects:
```json
{x: 1}
{"x" 1}
{"x": 1,}
{"x": 1 "y": 2}
```
## Arrays
Arrays use brackets and contain zero or more comma-separated values.
```json
[]
[1]
[1, 2, 3]
[true, false, null]
[{"x": 1}, ["nested"]]
```
Trailing commas are invalid:
```json
[1, 2,]
```
Missing separators are invalid:
```json
[1 2]
[true false]
```
Repeated separators are invalid:
```json
[1,, 2]
```
## Strings
Strings use double quotes.
```json
""
"hello"
"quote: \""
"slash: \\"
"unicode: \u03bb"
```
Valid escapes:
```json
"\""
"\\"
"\/"
"\b"
"\f"
"\n"
"\r"
"\t"
"\u0041"
```
Invalid strings:
```json
"unterminated
"bad escape: \x"
"bad unicode: \u12"
"raw
newline"
```
## Numbers
Numbers follow JSON number syntax.
Valid numbers:
```json
0
-0
12
-12
1.5
0.25
1e10
1E-10
-12.34e+56
```
Invalid numbers:
```json
01
-
1.
.5
1e
1e+
123abc
```
## Keywords
The only keywords are:
```json
true
false
null
```
These must match exactly:
```json
true
false
null
```
Invalid keyword-like fragments:
```json
True
FALSE
nil
nullish
truefalse
```
## Delimiters
Objects and arrays must close with matching delimiters:
```json
{"x": 1}
[1, 2, 3]
```
These are invalid:
```json
{"x": 1]
[1, 2}
{"x": 1
[1, 2
```

View file

View file

@ -0,0 +1,4 @@
import type { CodePoint, CodePointSpan } from 'source-region';
export type ParseError =
| {} // TODO

View file

@ -0,0 +1,95 @@
import type { CodePointSpan } from 'source-region';
import type { ParseError } from './parse_errors.ts';
export type ConcreteInfo = { span: CodePointSpan };
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-brace", span: CodePointSpan }
| { tag: "close-brace", span: CodePointSpan }
| { tag: "open-bracket", span: CodePointSpan }
| { tag: "close-bracket", span: CodePointSpan }
export type Program<Info, Error> = {
tag: "program",
expressions: JsonValue<Info, Error>[],
error?: Error,
} & Info
export type JsonValue<Info, Error> =
| JsonObject<Info, Error>
| JsonArray<Info, Error>
| JsonScalar<Info, Error>
| { tag: "error-expression", error: Error } & Info
export type JsonObject<Info, Error> = {
tag: "object",
open: DelimiterToken,
members: MemberItem<Info, Error>[],
close?: DelimiterToken,
error?: Error
} & Info
export type MemberItem<Info, Error> =
| { tag: "member" } & Member<Info, Error>
| { tag: "error-object-separator", error: Error } & Info
export type Member<Info, Error> = {
key: StringLiteral<Info, Error>,
colon?: { tag: "colon", span: CodePointSpan },
value: JsonValue<Info, Error>,
error?: Error
} & Info
export type JsonArray<Info, Error> = {
tag: "array",
open: DelimiterToken,
items: ArrayItem<Info, Error>[],
close?: DelimiterToken,
error?: Error
} & Info
export type ArrayItem<Info, Error> =
| JsonValue<Info, Error>
| { tag: "error-array-separator", error: Error } & Info
export type StringLiteral<Info, Error> =
| {
tag: "string",
// TODO: There are various possibilities of storing the actual literal value. But I don't care about this right now.
value: string,
error?: Error,
} & Info
| { tag: "error-string", error: Error } & Info
export type NumberLiteral<Info, Error> =
| {
tag: "number",
// TODO: There are various possibilities of storing the actual literal value. But I don't care about this right now.
value: number,
error?: Error,
} & Info
| { tag: "error-number", error: Error } & Info
export type JsonScalar<Info, Error> =
// === number ===
| NumberLiteral<Info, Error>
// === string ===
| StringLiteral<Info, Error>
// === constants ===
| { tag: "null", error?: Error } & Info
| { tag: "true", error?: Error } & Info
| { tag: "false", error?: Error } & Info

View file

@ -0,0 +1,224 @@
# Lisp Syntax
This is a small Lisp-like concrete syntax used for parser and source span experiments.
## Grammar
This grammar is intentionally semi-formal. `Whitespace` may appear between the major syntactic parts shown below.
```txt
Program ::= Expr*
Expr ::= Identifier
| Number
| RoundList
| SquareList
RoundList ::= "(" Expr* ")"
SquareList ::= "[" SquareBody? "]"
SquareBody ::= LeadingComma? SquareItems? TrailingComma?
LeadingComma ::= ","
TrailingComma ::= ","
SquareItems ::= Expr ("," Expr)*
Number ::= Digit+
Identifier ::= IdentifierStart IdentifierPart*
IdentifierStart
::= AsciiLetter
| "_"
| "-"
IdentifierPart
::= IdentifierStart
| Digit
Digit ::= "0" | "1" | "2" | "3" | "4"
| "5" | "6" | "7" | "8" | "9"
AsciiLetter ::= "a" ... "z" | "A" ... "Z"
```
Notes:
- `Program` may be empty.
- `RoundList` elements are whitespace-separated only; commas have no special meaning there.
- `SquareList` elements require commas between neighboring expressions.
- `SquareList` permits at most one leading comma and at most one trailing comma.
- `foo(bar)` is allowed because it is parsed as two adjacent expressions: `foo` and `(bar)`.
## Program
A program is a sequence of zero or more expressions.
```lisp
foo
foo 123
foo(bar)
(foo 1 2) [bar, 3, baz]
```
Empty input is valid.
Whitespace may appear between expressions, but it is not required when the next expression starts with a delimiter:
```lisp
foo(bar)
```
is treated like:
```lisp
foo (bar)
```
## Expressions
An expression is one of:
- identifier
- number
- round list
- square list
## Identifiers
Identifiers must start with:
- ASCII letter
- `_`
- `-`
After the first code point, identifiers may contain:
- ASCII letters
- digits
- `_`
- `-`
Examples:
```lisp
foo
abc123
abc_123
name-with-dash
_private
-operator-like
```
Not identifiers:
```lisp
123abc
@foo
💥
```
## Numbers
Numbers are non-empty sequences of ASCII digits.
Examples:
```lisp
0
1
123
00123
```
No signs, decimals, exponents, separators, or non-ASCII digits are supported.
These are not valid numbers:
```lisp
-1
1.2
1e5
123abc
```
## Round Lists
Round lists use parentheses and contain zero or more expressions.
```lisp
()
(foo)
(foo 1 2)
(foo (bar 1) baz)
```
Round lists do not use separators. Commas are not special in round lists.
## Square Lists
Square lists use brackets and require commas between neighboring elements.
```lisp
[]
[foo]
[foo, bar]
[foo, 1, (bar 2)]
```
Square lists allow one optional leading comma:
```lisp
[,foo]
[,foo, bar]
```
Square lists allow one optional trailing comma:
```lisp
[foo,]
[foo, bar,]
```
Leading and trailing commas can be combined:
```lisp
[,foo, bar,]
```
Repeated leading commas are invalid:
```lisp
[, , foo]
```
Missing separators between neighboring elements are invalid:
```lisp
[foo bar]
[foo, bar baz]
```
Repeated separators after an element are invalid:
```lisp
[foo,, bar]
[foo, bar,,]
```
## Delimiters
Lists must be closed with the matching delimiter:
```lisp
(foo)
[foo]
```
These are invalid:
```lisp
(foo]
[foo)
(foo
[foo
```

View file

@ -0,0 +1,80 @@
import { CodePointString, sourceText } from 'source-region';
import { parseDocument, programOf } from './parser';
import { matchCodePointString } from '../../recognizers';
import { Program } from './syntax';
// === Experiments ===
function experiment00_emptyDocument(): void {
logParse("empty document", "");
}
function experiment01_topLevelExpressions(): void {
logParse("top-level expressions", "foo 123 (bar baz_1 qux-2) [a, b, c]");
}
function experiment02_nestedLists(): void {
logParse("nested lists", "(define square (_ x) (* x x))");
}
function experiment03_unclosedList(): void {
logParse("unclosed list", "(foo 123\n (bar 456)");
}
function experiment04_recoverAtDocumentLevel(): void {
logParse("document recovery", "foo ) @@@ (bar 1) 99");
}
function experiment05_recoverInsideList(): void {
logParse("list recovery", "(foo @@@ 1 (bar # 2) baz)");
}
function experiment06_unicodeSpans(): void {
logParse("unicode spans", "alpha 💥 (beta 2)");
}
function experiment07_matchCodePointString(): void {
const region = sourceText("λx").fullRegion();
const cursor = region.makeCursor();
const lambda = CodePointString.makeFromString("λ");
console.log("==== recognizer:match code point string ====");
console.dir(matchCodePointString(cursor, lambda), { depth: null });
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 {
const region = sourceText(input).fullRegion();
const result = parseDocument(region);
console.log(`==== parser:${name} ====`);
console.log(input);
console.log(result.syntax.tag, Program.show(programOf(result.syntax)));
console.dir(result.errors, { depth: null });
}
export function runExperiments(): void {
[
experiment00_emptyDocument,
experiment01_topLevelExpressions,
experiment02_nestedLists,
experiment03_unclosedList,
experiment04_recoverAtDocumentLevel,
experiment05_recoverInsideList,
experiment06_unicodeSpans,
experiment07_matchCodePointString,
experiment08_squareListSeparator,
experiment09_invalidNumberFragment,
experiment10_repeatedLeadingComma,
].forEach((experiment) => experiment());
}

View file

@ -0,0 +1,22 @@
export { parseDocument, programOf } from './parser';
export type {
ConcreteSyntaxResult,
ParseDocumentResult,
PartialConcreteSyntax,
ValidConcreteSyntax,
PartialExpr,
PartialList,
PartialListItem,
} from './parser';
export type { FoundSyntax, ParseError } from './parse_errors';
export type {
ConcreteError,
ConcreteErrorNode,
ConcreteInfo,
DelimiterToken,
Expr,
List,
ListItem,
Program,
} from './syntax';
export { Expr as LispExpr } from './syntax';

View file

@ -13,10 +13,9 @@ import type {
SourceRegion, SourceRegion,
} 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 { import {
ConcreteError, ConcreteError,
ConcreteSyntaxResult,
DelimiterToken, DelimiterToken,
Expr, Expr,
ListItem, ListItem,
@ -25,8 +24,7 @@ import {
import type { import type {
ConcreteInfo, ConcreteInfo,
ListItem as ListItemType, ListItem as ListItemType,
PartialConcreteSyntax, List as ListType,
ValidConcreteSyntax,
Expr as ExprType, Expr as ExprType,
} from './syntax'; } from './syntax';
@ -55,13 +53,39 @@ const COMMA = char(',');
const DASH = char('-'); const DASH = char('-');
const UNDERSCORE = char('_'); const UNDERSCORE = char('_');
export type ConcreteSyntaxResult =
| { tag: "valid", value: ValidConcreteSyntax }
| { tag: "invalid", value: PartialConcreteSyntax }
export type ParseDocumentResult = { export type ParseDocumentResult = {
syntax: ConcreteSyntaxResult; syntax: ConcreteSyntaxResult;
errors: ParseError[]; errors: ParseError[];
}; };
type PartialExpr = ExprType<ConcreteInfo, ConcreteError>; // The main constraints are
type PartialListItem = ListItemType<ConcreteInfo, ConcreteError>; // - `ValidConcreteSyntax` should be a subtype of `PartialConcreteSyntax`
// - if `PartialConcreteSyntax` doesn't contain any sort of error nodes, we should be able to coerce it to `ValidConcreteSyntax` without rebuilding the whole tree
export type ValidConcreteSyntax = Program<ConcreteInfo, never>
export type PartialConcreteSyntax = Program<ConcreteInfo, ConcreteError>
export type PartialExpr = ExprType<ConcreteInfo, ConcreteError>;
export type PartialList = ListType<ConcreteInfo, ConcreteError>;
export type PartialListItem = ListItemType<ConcreteInfo, ConcreteError>;
export namespace ConcreteSyntaxResult {
export function valid(value: ValidConcreteSyntax): ConcreteSyntaxResult {
return { tag: "valid", value };
}
export function invalid(value: PartialConcreteSyntax): ConcreteSyntaxResult {
return { tag: "invalid", value };
}
}
export function programOf(result: ConcreteSyntaxResult): PartialConcreteSyntax {
return result.value;
}
export function parseDocument(region: SourceRegion): ParseDocumentResult { export function parseDocument(region: SourceRegion): ParseDocumentResult {
return new Parser(region).parseDocument(); return new Parser(region).parseDocument();

View file

@ -3,13 +3,6 @@ import type { ParseError } from './parse_errors';
export type ConcreteInfo = { span: CodePointSpan }; export type ConcreteInfo = { span: CodePointSpan };
export type ConcreteSyntaxResult =
| { tag: "valid", value: ValidConcreteSyntax }
| { tag: "invalid", value: PartialConcreteSyntax }
export type ValidConcreteSyntax = Program<ConcreteInfo, never>
export type PartialConcreteSyntax = Program<ConcreteInfo, ConcreteError>
export type ConcreteError = ConcreteErrorNode[] // Convention: can't be empty. export type ConcreteError = ConcreteErrorNode[] // Convention: can't be empty.
export type ConcreteErrorNode = { export type ConcreteErrorNode = {
span: CodePointSpan, span: CodePointSpan,
@ -24,10 +17,10 @@ export namespace ConcreteError {
} }
export type DelimiterToken = export type DelimiterToken =
| { tag: "open-paren"; span: CodePointSpan } | { tag: "open-paren", span: CodePointSpan }
| { tag: "close-paren"; span: CodePointSpan } | { tag: "close-paren", span: CodePointSpan }
| { tag: "open-bracket"; span: CodePointSpan } | { tag: "open-bracket", span: CodePointSpan }
| { tag: "close-bracket"; span: CodePointSpan }; | { tag: "close-bracket", span: CodePointSpan }
export namespace DelimiterToken { export namespace DelimiterToken {
export function openParen(span: CodePointSpan): DelimiterToken { export function openParen(span: CodePointSpan): DelimiterToken {
@ -56,7 +49,7 @@ export type Program<Info, Error> = {
export type Expr<Info, Error> = export type Expr<Info, Error> =
| Literal<Info, Error> | Literal<Info, Error>
| List<Info, Error> | List<Info, Error>
| { tag: "error-expression", error: Error } & Info | { tag: "error-expression", error: Error } & Info // This is for errors that don't really correspond to any sort of node. Unknown errors.
export type List<Info, Error> = export type List<Info, Error> =
{ tag: "list", open: DelimiterToken, items: ListItem<Info, Error>[], close?: DelimiterToken, error?: Error } & Info { tag: "list", open: DelimiterToken, items: ListItem<Info, Error>[], close?: DelimiterToken, error?: Error } & Info
@ -66,23 +59,15 @@ export type ListItem<Info, Error> =
| { tag: "error-list-separator", error: Error } & Info | { tag: "error-list-separator", error: Error } & Info
export type Literal<Info, Error> = export type Literal<Info, Error> =
// === number ===
| { tag: "number", value: number } & Info | { tag: "number", value: number } & Info
| { tag: "error-number", error: Error } & Info | { tag: "error-number", error: Error } & Info
// === identifier ===
| { tag: "identifier", value: Identifier } & Info | { tag: "identifier", value: Identifier } & Info
| { tag: "error-identifier", error: Error } & Info | { tag: "error-identifier", error: Error } & Info
export type Identifier = string 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 namespace Program {
export function make<Info, Error>( export function make<Info, Error>(
expressions: Expr<Info, Error>[], expressions: Expr<Info, Error>[],
@ -165,7 +150,3 @@ export namespace ListItem {
return Expr.show(item); return Expr.show(item);
} }
} }
export function programOf(result: ConcreteSyntaxResult): PartialConcreteSyntax {
return result.value;
}

View file

@ -1,4 +1,4 @@
export { parseDocument } from './parser'; export { parseDocument } from './languages/lisp/index';
export type { ParseDocumentResult } from './parser'; export type { ParseDocumentResult } from './languages/lisp/parser';
export type { FoundSyntax, ParseError } from './parse_errors'; export type { FoundSyntax, ParseError } from './languages/lisp/parse_errors';
export * from './syntax'; export * from './languages/lisp/syntax';

View file

@ -1,60 +0,0 @@
import type { CodePointSpan } from 'source-region';
import type { ParseError } from './parse_errors';
export type ConcreteSyntaxResult =
| { tag: "valid", value: ValidConcreteSyntax }
| { tag: "invalid", value: PartialConcreteSyntax }
// The main constraints are
// - `ValidConcreteSyntax` should be a subtype of `PartialConcreteSyntax`
// - if `PartialConcreteSyntax` doesn't contain any sort of error nodes, we should be able to coerce it to `ValidConcreteSyntax` without rebuilding the whole tree
export type ValidConcreteSyntax = Program<{ span: CodePointSpan }, never>
export type PartialConcreteSyntax = Program<{ span: CodePointSpan }, ConcreteError >
export type ConcreteError = ConcreteErrorNode[] // Can't be empty array.
export type ConcreteErrorNode = {
span: CodePointSpan,
error: ParseError,
panickedOver?: CodePointSpan,
}
export type DelimiterToken =
| { tag: "open-paren"; span: CodePointSpan }
| { tag: "close-paren"; span: CodePointSpan }
| { tag: "open-bracket"; span: CodePointSpan }
| { tag: "close-bracket"; span: CodePointSpan };
export type Program<Info, Error> = {
expressions: Expr<Info, Error>[],
error?: Error,
} & Info
export type Expr<Info, Error> =
| Literal<Info, Error>
| List<Info, Error>
| { tag: "error-expression", error: Error } & Info // This is for errors that don't really correspond to any sort of node. Unknown errors.
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> =
// === number ===
| { tag: "number", value: number } & Info
| { tag: "error-number", error: Error } & Info
// === identifier ===
| { tag: "identifier", value: Identifier } & Info
| { tag: "error-identifier", error: Error } & Info
export type Identifier = string
export namespace ConcreteError {
export function single(node: ConcreteErrorNode): ConcreteError {
return [node];
}
}

View file

@ -1,78 +1,3 @@
import { CodePointString, sourceText } from 'source-region'; import { runExperiments as runLispExperiments } from './languages/lisp/experiments';
import { parseDocument } from './parser';
import { matchCodePointString } from './recognizers';
import { Program, programOf } from './syntax';
// === Experiments === runLispExperiments();
function experiment00_emptyDocument(): void {
logParse("empty document", "");
}
function experiment01_topLevelExpressions(): void {
logParse("top-level expressions", "foo 123 (bar baz_1 qux-2) [a, b, c]");
}
function experiment02_nestedLists(): void {
logParse("nested lists", "(define square (_ x) (* x x))");
}
function experiment03_unclosedList(): void {
logParse("unclosed list", "(foo 123\n (bar 456)");
}
function experiment04_recoverAtDocumentLevel(): void {
logParse("document recovery", "foo ) @@@ (bar 1) 99");
}
function experiment05_recoverInsideList(): void {
logParse("list recovery", "(foo @@@ 1 (bar # 2) baz)");
}
function experiment06_unicodeSpans(): void {
logParse("unicode spans", "alpha 💥 (beta 2)");
}
function experiment07_matchCodePointString(): void {
const region = sourceText("λx").fullRegion();
const cursor = region.makeCursor();
const lambda = CodePointString.makeFromString("λ");
console.log("==== recognizer:match code point string ====");
console.dir(matchCodePointString(cursor, lambda), { depth: null });
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 {
const region = sourceText(input).fullRegion();
const result = parseDocument(region);
console.log(`==== parser:${name} ====`);
console.log(input);
console.log(result.syntax.tag, Program.show(programOf(result.syntax)));
console.dir(result.errors, { depth: null });
}
[
experiment00_emptyDocument,
experiment01_topLevelExpressions,
experiment02_nestedLists,
experiment03_unclosedList,
experiment04_recoverAtDocumentLevel,
experiment05_recoverInsideList,
experiment06_unicodeSpans,
experiment07_matchCodePointString,
experiment08_squareListSeparator,
experiment09_invalidNumberFragment,
experiment10_repeatedLeadingComma,
].forEach((experiment) => experiment());

View file

@ -1,8 +1,33 @@
.app-root {
height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
min-width: 1200px;
}
.language-bar {
display: flex;
align-items: center;
gap: var(--gap-2);
padding: var(--gap-2) var(--gap-4);
border-bottom: 1px solid var(--border);
background: var(--panel);
color: var(--text-muted);
font-size: var(--text-sm);
}
.language-bar select {
border: 1px solid var(--border);
color: var(--text);
background: var(--panel-raised);
padding: var(--gap-1) var(--gap-2);
}
.app-shell { .app-shell {
display: grid; display: grid;
grid-template-columns: var(--left-width) 0.45rem var(--middle-width) 0.45rem minmax(360px, 1fr); grid-template-columns: var(--left-width) 0.45rem var(--middle-width) 0.45rem minmax(360px, 1fr);
gap: var(--gap-2); gap: var(--gap-2);
height: 100vh; min-height: 0;
padding: var(--gap-4); padding: var(--gap-4);
} }

View file

@ -1,122 +1,29 @@
import { createMemo, createSignal } from 'solid-js'; import { createSignal, Switch, Match } from 'solid-js';
import { sourceText } from 'source-region'; import { App as LispApp } from './languages/lisp/App';
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
import { parseDocument } from '../parser';
import type { ParseError } from '../parse_errors';
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 { StructureTree } from './SyntaxPane';
import type { HoverTarget } from './types';
type ParsedDocument = { type LanguageId = "lisp";
source: SourceText;
region: SourceRegion;
syntax: ConcreteSyntaxResult;
program: PartialConcreteSyntax;
errors: ParseError[];
};
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
[add, 1, 2]
(define pyth (_ x y) (+ (square x) (square y)))
foo ) @@@ (bar 1)
(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 [language, setLanguage] = createSignal<LanguageId>("lisp");
const [hovered, setHovered] = createSignal<HoverTarget | undefined>();
const [leftWidth, setLeftWidth] = createSignal(420);
const [middleWidth, setMiddleWidth] = createSignal(420);
const parsed = createMemo<ParsedDocument>(() => {
const source = sourceText(input());
const region = source.fullRegion();
const result = parseDocument(region);
return { source, region, syntax: result.syntax, program: programOf(result.syntax), errors: result.errors };
});
return ( return (
<main <div class="app-root">
class="app-shell" <header class="language-bar">
style={{ <label for="language-select">Language</label>
"--left-width": `${leftWidth()}px`, <select
"--middle-width": `${middleWidth()}px`, id="language-select"
}} value={language()}
> onChange={(event) => setLanguage(event.currentTarget.value as LanguageId)}
<section class="pane input-pane"> >
<PaneHeader title="Source" detail={`${input().length} UTF-16 units`} /> <option value="lisp">Lisp</option>
<textarea </select>
class="source-input" </header>
spellcheck={false}
value={input()}
onInput={(event) => {
setInput(event.currentTarget.value);
setHovered(undefined);
}}
/>
</section>
<PaneSplitter <Switch>
label="Resize source and structure panes" <Match when={language() === "lisp"}>
onDrag={(delta) => { <LispApp />
setLeftWidth((width) => clamp(width + delta, 280, 760)); </Match>
}} </Switch>
/> </div>
<section class="pane structure-pane">
<PaneHeader
title="Structure"
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}
/>
</section>
<PaneSplitter
label="Resize structure and source grid panes"
onDrag={(delta) => {
setMiddleWidth((width) => clamp(width + delta, 260, 760));
}}
/>
<section class="pane source-pane">
<PaneHeader
title="Source Grid"
detail={hovered() ? spanLabel(hovered()!.span) : "nothing hovered"}
/>
<SourceGrid
source={parsed().source}
region={parsed().region}
annotations={hovered() ? [hoverAnnotation(hovered()!)] : []}
/>
</section>
</main>
); );
} }
function hoverAnnotation(target: HoverTarget): SourceGridAnnotation {
return {
id: "hovered",
span: target.span,
label: target.label,
cellClass: "annotation-hovered",
markerClass: "annotation-hovered-marker",
};
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}

21
src/ui/HoverBlock.tsx Normal file
View file

@ -0,0 +1,21 @@
import type { JSX } from 'solid-js';
import type { CodePointSpan } from 'source-region';
import type { HoverTarget } from './types';
export function HoverBlock(props: {
class: string;
label: string;
span: CodePointSpan;
onHover: (target: HoverTarget | undefined) => void;
children: JSX.Element;
}) {
return (
<div
class={props.class}
onMouseEnter={() => props.onHover({ label: props.label, span: props.span })}
onMouseLeave={() => props.onHover(undefined)}
>
{props.children}
</div>
);
}

21
src/ui/SpanChip.tsx Normal file
View file

@ -0,0 +1,21 @@
import type { CodePointSpan } from 'source-region';
import { spanLabel } from './format';
import type { HoverTarget } from './types';
export 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>
);
}

12
src/ui/annotations.ts Normal file
View file

@ -0,0 +1,12 @@
import type { SourceGridAnnotation } from './SourceGrid';
import type { HoverTarget } from './types';
export function hoverAnnotation(target: HoverTarget): SourceGridAnnotation {
return {
id: "hovered",
span: target.span,
label: target.label,
cellClass: "annotation-hovered",
markerClass: "annotation-hovered-marker",
};
}

View file

@ -1,52 +1,4 @@
import type { CodePointSpan } from 'source-region'; import type { CodePointSpan } from 'source-region';
import type { FoundSyntax, ParseError } from '../parse_errors';
export function errorTitle(error: ParseError): string {
switch (error.tag) {
case "expected-expression":
return "Expected expression";
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":
return "Invalid number";
}
}
export function errorLabel(error: ParseError): string {
return `${errorTitle(error)} ${spanLabel(error.span)}`;
}
export function errorDetail(error: ParseError): string {
switch (error.tag) {
case "expected-expression":
return `found ${foundLabel(error.found)}`;
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":
return `${error.reason}: ${error.text}`;
}
}
export function foundLabel(found: FoundSyntax): string {
switch (found.tag) {
case "eof":
return `EOF ${spanLabel(found.span)}`;
case "code-point":
return `${String.fromCodePoint(found.value)} ${spanLabel(found.span)}`;
}
}
export function spanLabel(span: CodePointSpan): string { export function spanLabel(span: CodePointSpan): string {
return `[${span.start}, ${span.end})`; return `[${span.start}, ${span.end})`;

View file

View file

View file

View file

@ -0,0 +1,114 @@
import { createMemo, createSignal } from 'solid-js';
import { sourceText } from 'source-region';
import type { CodePointSpan, SourceRegion, SourceText } from 'source-region';
import {
parseDocument,
programOf,
} from '../../../languages/lisp';
import type {
ConcreteSyntaxResult,
ParseError,
PartialConcreteSyntax,
} from '../../../languages/lisp';
import { spanLabel } from '../../format';
import { hoverAnnotation } from '../../annotations';
import { PaneHeader, PaneSplitter } from '../../Pane';
import { SourceGrid } from '../../SourceGrid';
import type { HoverTarget } from '../../types';
import { clamp } from '../../utils';
import { StructureTree } from './StructurePane';
type ParsedDocument = {
source: SourceText;
region: SourceRegion;
syntax: ConcreteSyntaxResult;
program: PartialConcreteSyntax;
errors: ParseError[];
};
const SAMPLE_INPUT = `(define square (_ x) (mul x x))
[add, 1, 2]
(define pyth (_ x y) (+ (square x) (square y)))
foo ) @@@ (bar 1)
(nested [list, 123, abc_9, name-with-dash])
[a, b c, d]
123fasd`;
export function App() {
const [input, setInput] = createSignal(SAMPLE_INPUT);
const [hovered, setHovered] = createSignal<HoverTarget | undefined>();
const [leftWidth, setLeftWidth] = createSignal(420);
const [middleWidth, setMiddleWidth] = createSignal(420);
const parsed = createMemo<ParsedDocument>(() => {
const source = sourceText(input());
const region = source.fullRegion();
const result = parseDocument(region);
return { source, region, syntax: result.syntax, program: programOf(result.syntax), errors: result.errors };
});
return (
<main
class="app-shell"
style={{
"--left-width": `${leftWidth()}px`,
"--middle-width": `${middleWidth()}px`,
}}
>
<section class="pane input-pane">
<PaneHeader title="Source" detail={`${input().length} UTF-16 units`} />
<textarea
class="source-input"
spellcheck={false}
value={input()}
onInput={(event) => {
setInput(event.currentTarget.value);
setHovered(undefined);
}}
/>
</section>
<PaneSplitter
label="Resize source and structure panes"
onDrag={(delta) => {
setLeftWidth((width) => clamp(width + delta, 280, 760));
}}
/>
<section class="pane structure-pane">
<PaneHeader
title="Structure"
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}
/>
</section>
<PaneSplitter
label="Resize structure and source grid panes"
onDrag={(delta) => {
setMiddleWidth((width) => clamp(width + delta, 260, 760));
}}
/>
<section class="pane source-pane">
<PaneHeader
title="Source Grid"
detail={hovered() ? spanLabel(hovered()!.span) : "nothing hovered"}
/>
<SourceGrid
source={parsed().source}
region={parsed().region}
annotations={hovered() ? [hoverAnnotation(hovered()!)] : []}
/>
</section>
</main>
);
}

View file

@ -1,22 +1,18 @@
import { For, Show } from 'solid-js'; import { For, Show } from 'solid-js';
import type { JSX } from 'solid-js';
import type { CodePointSpan } from 'source-region';
import type { import type {
ConcreteError, ConcreteError,
ConcreteErrorNode, ConcreteErrorNode,
ConcreteInfo,
List,
ListItem,
PartialConcreteSyntax, PartialConcreteSyntax,
Expr as SyntaxExpr, PartialExpr,
} from '../syntax'; PartialList,
import { Expr } from '../syntax'; PartialListItem,
import { errorDetail, errorTitle, spanLabel } from './format'; } from '../../../languages/lisp';
import type { HoverTarget } from './types'; import { LispExpr } from '../../../languages/lisp';
import { spanLabel } from '../../format';
type PartialExpr = SyntaxExpr<ConcreteInfo, ConcreteError>; import { HoverBlock } from '../../HoverBlock';
type PartialList = List<ConcreteInfo, ConcreteError>; import { SpanChip } from '../../SpanChip';
type PartialListItem = ListItem<ConcreteInfo, ConcreteError>; import type { HoverTarget } from '../../types';
import { errorDetail, errorTitle } from './format';
export function StructureTree(props: { export function StructureTree(props: {
program: PartialConcreteSyntax; program: PartialConcreteSyntax;
@ -77,7 +73,7 @@ function ExprView(props: {
> >
<div class="node-header"> <div class="node-header">
<span class="node-kind">{props.expr.tag}</span> <span class="node-kind">{props.expr.tag}</span>
<span class="node-value">{Expr.show(props.expr)}</span> <span class="node-value">{LispExpr.show(props.expr)}</span>
</div> </div>
</HoverBlock> </HoverBlock>
); );
@ -217,42 +213,6 @@ function ConcreteErrorNodeView(props: {
); );
} }
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: {
class: string;
label: string;
span: CodePointSpan;
onHover: (target: HoverTarget | undefined) => void;
children: JSX.Element;
}) {
return (
<div
class={props.class}
onMouseEnter={() => props.onHover({ label: props.label, span: props.span })}
onMouseLeave={() => props.onHover(undefined)}
>
{props.children}
</div>
);
}
function listLabel(tag: string): string { function listLabel(tag: string): string {
return tag === "open-bracket" ? "square-list" : "round-list"; return tag === "open-bracket" ? "square-list" : "round-list";
} }

View file

@ -0,0 +1,45 @@
import type { FoundSyntax, ParseError } from '../../../languages/lisp';
import { spanLabel } from '../../format';
export function errorTitle(error: ParseError): string {
switch (error.tag) {
case "expected-expression":
return "Expected expression";
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":
return "Invalid number";
}
}
export function errorDetail(error: ParseError): string {
switch (error.tag) {
case "expected-expression":
return `found ${foundLabel(error.found)}`;
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":
return `${error.reason}: ${error.text}`;
}
}
function foundLabel(found: FoundSyntax): string {
switch (found.tag) {
case "eof":
return `EOF ${spanLabel(found.span)}`;
case "code-point":
return `${String.fromCodePoint(found.value)} ${spanLabel(found.span)}`;
}
}

3
src/ui/utils.ts Normal file
View file

@ -0,0 +1,3 @@
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}