Compare commits
10 commits
b2e96b9a22
...
57f666118a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57f666118a | ||
|
|
ef1d81f597 | ||
|
|
321e7aa4de | ||
|
|
d1491ec5e6 | ||
|
|
8bca6e1f20 | ||
|
|
a824e2d9e8 | ||
|
|
c3edf193c4 | ||
|
|
e1e1b90579 | ||
|
|
2129c26fe5 | ||
|
|
309fa373f4 |
29 changed files with 1335 additions and 387 deletions
286
PARTIAL_SYNTAX.md
Normal file
286
PARTIAL_SYNTAX.md
Normal 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
18
QESTIONS.md
Normal 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 `]`.
|
||||||
|
|
||||||
16
SOURCE_REGION_EXPERIMENTS.md
Normal file
16
SOURCE_REGION_EXPERIMENTS.md
Normal 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
|
||||||
273
src/languages/json/SYNTAX.md
Normal file
273
src/languages/json/SYNTAX.md
Normal 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
|
||||||
|
```
|
||||||
0
src/languages/json/index.ts
Normal file
0
src/languages/json/index.ts
Normal file
4
src/languages/json/parse_errors.ts
Normal file
4
src/languages/json/parse_errors.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import type { CodePoint, CodePointSpan } from 'source-region';
|
||||||
|
|
||||||
|
export type ParseError =
|
||||||
|
| {} // TODO
|
||||||
95
src/languages/json/syntax.ts
Normal file
95
src/languages/json/syntax.ts
Normal 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
|
||||||
|
|
||||||
224
src/languages/lisp/SYNTAX.md
Normal file
224
src/languages/lisp/SYNTAX.md
Normal 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
|
||||||
|
```
|
||||||
80
src/languages/lisp/experiments.ts
Normal file
80
src/languages/lisp/experiments.ts
Normal 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());
|
||||||
|
}
|
||||||
22
src/languages/lisp/index.ts
Normal file
22
src/languages/lisp/index.ts
Normal 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';
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
135
src/ui/App.tsx
135
src/ui/App.tsx
|
|
@ -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
21
src/ui/HoverBlock.tsx
Normal 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
21
src/ui/SpanChip.tsx
Normal 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
12
src/ui/annotations.ts
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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})`;
|
||||||
|
|
|
||||||
0
src/ui/languages/json/App.tsx
Normal file
0
src/ui/languages/json/App.tsx
Normal file
0
src/ui/languages/json/StructurePane.tsx
Normal file
0
src/ui/languages/json/StructurePane.tsx
Normal file
0
src/ui/languages/json/format.ts
Normal file
0
src/ui/languages/json/format.ts
Normal file
114
src/ui/languages/lisp/App.tsx
Normal file
114
src/ui/languages/lisp/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
45
src/ui/languages/lisp/format.ts
Normal file
45
src/ui/languages/lisp/format.ts
Normal 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
3
src/ui/utils.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue