@thomasfosterau/effect-parse 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -231
- package/dist/Ascii.d.ts +32 -53
- package/dist/Ascii.d.ts.map +1 -1
- package/dist/Ascii.js +576 -571
- package/dist/Ascii.js.map +1 -1
- package/dist/Binary.d.ts +6 -29
- package/dist/Binary.d.ts.map +1 -1
- package/dist/Binary.js +129 -116
- package/dist/Binary.js.map +1 -1
- package/dist/Bits.d.ts +4 -2
- package/dist/Bits.d.ts.map +1 -1
- package/dist/Bits.js +121 -123
- package/dist/Bits.js.map +1 -1
- package/dist/Compiler.d.ts +39 -0
- package/dist/Compiler.d.ts.map +1 -0
- package/dist/Compiler.js +48 -0
- package/dist/Compiler.js.map +1 -0
- package/dist/Encoding.d.ts +29 -46
- package/dist/Encoding.d.ts.map +1 -1
- package/dist/Encoding.js +76 -75
- package/dist/Encoding.js.map +1 -1
- package/dist/Expression.d.ts +15 -49
- package/dist/Expression.d.ts.map +1 -1
- package/dist/Expression.js +151 -132
- package/dist/Expression.js.map +1 -1
- package/dist/Input.d.ts +251 -177
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +409 -299
- package/dist/Input.js.map +1 -1
- package/dist/Nom.d.ts +45 -85
- package/dist/Nom.d.ts.map +1 -1
- package/dist/Nom.js +423 -453
- package/dist/Nom.js.map +1 -1
- package/dist/ParseError.d.ts +54 -49
- package/dist/ParseError.d.ts.map +1 -1
- package/dist/ParseError.js +232 -275
- package/dist/ParseError.js.map +1 -1
- package/dist/Parser.d.ts +345 -313
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +2034 -1802
- package/dist/Parser.js.map +1 -1
- package/dist/ParserSchema.d.ts +206 -7
- package/dist/ParserSchema.d.ts.map +1 -1
- package/dist/ParserSchema.js +678 -49
- package/dist/ParserSchema.js.map +1 -1
- package/dist/ParserStream.d.ts +26 -20
- package/dist/ParserStream.d.ts.map +1 -1
- package/dist/ParserStream.js +267 -255
- package/dist/ParserStream.js.map +1 -1
- package/dist/Syntax.d.ts +401 -0
- package/dist/Syntax.d.ts.map +1 -0
- package/dist/Syntax.js +717 -0
- package/dist/Syntax.js.map +1 -0
- package/dist/Text.d.ts +98 -134
- package/dist/Text.d.ts.map +1 -1
- package/dist/Text.js +534 -496
- package/dist/Text.js.map +1 -1
- package/dist/Token.d.ts +60 -42
- package/dist/Token.d.ts.map +1 -1
- package/dist/Token.js +331 -265
- package/dist/Token.js.map +1 -1
- package/dist/index.d.ts +16 -17
- package/dist/index.js +16 -17
- package/dist/internal/compiler/closure.d.ts +14 -16
- package/dist/internal/compiler/closure.d.ts.map +1 -1
- package/dist/internal/compiler/closure.js +1458 -1637
- package/dist/internal/compiler/closure.js.map +1 -1
- package/dist/internal/compiler/entries.d.ts +11 -11
- package/dist/internal/compiler/entries.d.ts.map +1 -1
- package/dist/internal/compiler/entries.js +36 -26
- package/dist/internal/compiler/entries.js.map +1 -1
- package/dist/internal/compiler/first.d.ts +19 -19
- package/dist/internal/compiler/first.d.ts.map +1 -1
- package/dist/internal/compiler/first.js +251 -227
- package/dist/internal/compiler/first.js.map +1 -1
- package/dist/internal/compiler/jit.d.ts +6 -17
- package/dist/internal/compiler/jit.d.ts.map +1 -1
- package/dist/internal/compiler/jit.js +860 -988
- package/dist/internal/compiler/jit.js.map +1 -1
- package/dist/internal/compiler/registry.d.ts +20 -21
- package/dist/internal/compiler/registry.d.ts.map +1 -1
- package/dist/internal/compiler/registry.js +21 -41
- package/dist/internal/compiler/registry.js.map +1 -1
- package/dist/internal/compiler/sets.d.ts +9 -13
- package/dist/internal/compiler/sets.d.ts.map +1 -1
- package/dist/internal/compiler/sets.js +76 -73
- package/dist/internal/compiler/sets.js.map +1 -1
- package/dist/internal/entries.d.ts +3 -3
- package/dist/internal/entries.d.ts.map +1 -1
- package/dist/internal/entries.js +38 -30
- package/dist/internal/entries.js.map +1 -1
- package/dist/internal/node.d.ts +177 -173
- package/dist/internal/node.d.ts.map +1 -1
- package/dist/internal/node.js +77 -66
- package/dist/internal/node.js.map +1 -1
- package/dist/internal/numbers.d.ts +7 -8
- package/dist/internal/numbers.d.ts.map +1 -1
- package/dist/internal/numbers.js +246 -276
- package/dist/internal/numbers.js.map +1 -1
- package/dist/internal/resume.d.ts +6 -24
- package/dist/internal/resume.d.ts.map +1 -1
- package/dist/internal/resume.js +30 -29
- package/dist/internal/resume.js.map +1 -1
- package/dist/internal/run.d.ts +15 -0
- package/dist/internal/run.d.ts.map +1 -0
- package/dist/internal/run.js +36 -0
- package/dist/internal/run.js.map +1 -0
- package/dist/internal/stack.d.ts +16 -0
- package/dist/internal/stack.d.ts.map +1 -0
- package/dist/internal/stack.js +36 -0
- package/dist/internal/stack.js.map +1 -0
- package/dist/internal/syntax.d.ts +147 -0
- package/dist/internal/syntax.d.ts.map +1 -0
- package/dist/internal/syntax.js +309 -0
- package/dist/internal/syntax.js.map +1 -0
- package/dist/internal/text.d.ts +10 -2
- package/dist/internal/text.d.ts.map +1 -1
- package/dist/internal/text.js +295 -345
- package/dist/internal/text.js.map +1 -1
- package/dist/internal/ties.d.ts +30 -0
- package/dist/internal/ties.d.ts.map +1 -0
- package/dist/internal/ties.js +67 -0
- package/dist/internal/ties.js.map +1 -0
- package/dist/internal/tokens.d.ts +25 -30
- package/dist/internal/tokens.d.ts.map +1 -1
- package/dist/internal/tokens.js +369 -363
- package/dist/internal/tokens.js.map +1 -1
- package/package.json +42 -28
- package/src/Ascii.ts +278 -187
- package/src/Binary.ts +150 -44
- package/src/Bits.ts +6 -3
- package/src/Compiler.ts +98 -0
- package/src/Expression.ts +24 -16
- package/src/Input.ts +210 -35
- package/src/Nom.ts +4 -3
- package/src/ParseError.ts +49 -52
- package/src/Parser.ts +1199 -799
- package/src/ParserSchema.ts +982 -23
- package/src/ParserStream.ts +104 -80
- package/src/Syntax.ts +900 -0
- package/src/Text.ts +272 -187
- package/src/Token.ts +180 -87
- package/src/index.ts +2 -0
- package/src/internal/compiler/closure.ts +121 -88
- package/src/internal/compiler/entries.ts +21 -8
- package/src/internal/compiler/first.ts +17 -15
- package/src/internal/compiler/jit.ts +360 -157
- package/src/internal/compiler/registry.ts +12 -33
- package/src/internal/compiler/sets.ts +17 -11
- package/src/internal/entries.ts +4 -2
- package/src/internal/node.ts +51 -37
- package/src/internal/numbers.ts +22 -20
- package/src/internal/resume.ts +8 -4
- package/src/internal/run.ts +41 -0
- package/src/internal/stack.ts +41 -0
- package/src/internal/syntax.ts +426 -0
- package/src/internal/text.ts +32 -25
- package/src/internal/ties.ts +83 -0
- package/src/internal/tokens.ts +96 -51
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
# effect-parse
|
|
2
2
|
|
|
3
|
-
Parser combinators for [Effect](https://effect.website), ported from Rust's
|
|
4
|
-
[nom](https://github.com/rust-bakery/nom) and
|
|
5
|
-
[winnow](https://github.com/winnow-rs/winnow).
|
|
3
|
+
Parser combinators for [Effect](https://effect.website), ported from Rust's [nom](https://github.com/rust-bakery/nom) and [winnow](https://github.com/winnow-rs/winnow).
|
|
6
4
|
|
|
7
|
-
effect-parse keeps the core model of nom and winnow: small parsers composed
|
|
8
|
-
into larger ones; `Backtrack` / `Cut` / `Incomplete` error modes; one set of
|
|
9
|
-
parsers for both complete and streaming input; text, bytes, bits and token
|
|
10
|
-
streams. On top of that it follows Effect's conventions:
|
|
5
|
+
effect-parse keeps the core model of nom and winnow: small parsers composed into larger ones; `Backtrack` / `Cut` / `Incomplete` error modes; one set of parsers for both complete and streaming input; text, bytes, bits and token streams. On top of that it follows Effect's conventions:
|
|
11
6
|
|
|
12
7
|
- pipeable, dual (data-first or data-last) combinators
|
|
13
8
|
- generator syntax with `Parser.gen`
|
|
@@ -18,7 +13,7 @@ streams. On top of that it follows Effect's conventions:
|
|
|
18
13
|
```ts
|
|
19
14
|
import { Ascii, Parser, Token } from "@thomasfosterau/effect-parse"
|
|
20
15
|
|
|
21
|
-
const point = Parser.gen(function*() {
|
|
16
|
+
const point = Parser.gen(function* () {
|
|
22
17
|
yield* Token.literal("(")
|
|
23
18
|
const x = yield* Ascii.decInt
|
|
24
19
|
yield* Token.literal(",")
|
|
@@ -44,14 +39,11 @@ Parser.parse(point, "(1;-2)")
|
|
|
44
39
|
pnpm add @thomasfosterau/effect-parse effect
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
effect-parse requires `effect` 4.x (`^4.0.0`) as a peer dependency; the
|
|
48
|
-
consuming application owns the single copy of `effect`.
|
|
42
|
+
effect-parse requires `effect` 4.x (`^4.0.0`) as a peer dependency; the consuming application owns the single copy of `effect`.
|
|
49
43
|
|
|
50
|
-
Every module is available from the root entry as a namespace, and also on its
|
|
51
|
-
own subpath (`@thomasfosterau/effect-parse/Parser`, `…/Ascii`, and so on).
|
|
44
|
+
Every module is available from the root entry as a namespace, and also on its own subpath (`@thomasfosterau/effect-parse/Parser`, `…/Ascii`, and so on).
|
|
52
45
|
|
|
53
|
-
The package was developed as `effect-nom`. Releases and the changelog are
|
|
54
|
-
managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
|
|
46
|
+
The package was developed as `effect-nom`. Releases and the changelog are managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
|
|
55
47
|
|
|
56
48
|
## Contents
|
|
57
49
|
|
|
@@ -62,7 +54,9 @@ managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
|
|
|
62
54
|
- [Errors](#errors)
|
|
63
55
|
- [Streaming and partial input](#streaming-and-partial-input)
|
|
64
56
|
- [Services](#services)
|
|
57
|
+
- [Parsers from schemas](#parsers-from-schemas)
|
|
65
58
|
- [Unicode text and encodings](#unicode-text-and-encodings)
|
|
59
|
+
- [Syntax trees and incremental parsing](#syntax-trees-and-incremental-parsing)
|
|
66
60
|
- [Modules](#modules)
|
|
67
61
|
- [Coming from nom or winnow](#coming-from-nom-or-winnow)
|
|
68
62
|
- [Examples](#examples)
|
|
@@ -74,17 +68,14 @@ managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
|
|
|
74
68
|
interface Parser<A, E = never, R = never, I extends Input = string>
|
|
75
69
|
```
|
|
76
70
|
|
|
77
|
-
| Parameter | Meaning
|
|
78
|
-
|
|
79
|
-
| `A`
|
|
80
|
-
| `E`
|
|
81
|
-
| `R`
|
|
82
|
-
| `I`
|
|
71
|
+
| Parameter | Meaning |
|
|
72
|
+
| --------- | ------------------------------------------------------------------------ |
|
|
73
|
+
| `A` | the value produced |
|
|
74
|
+
| `E` | the type of user errors (see [Errors](#errors)); `never` for pure syntax |
|
|
75
|
+
| `R` | the services required (see [Services](#services)) |
|
|
76
|
+
| `I` | the input: `string` (the default), `Uint8Array`, or `ReadonlyArray<T>` |
|
|
83
77
|
|
|
84
|
-
Like winnow, a parser is a function over a mutable cursor. Failing is cheap,
|
|
85
|
-
and backtracking means resetting an offset. String tokens are Unicode code
|
|
86
|
-
points, `Uint8Array` tokens are bytes, and array tokens are elements, which
|
|
87
|
-
is useful for parsing the output of a lexer.
|
|
78
|
+
Like winnow, a parser is a function over a mutable cursor. Failing is cheap, and backtracking means resetting an offset. String tokens are Unicode code points, `Uint8Array` tokens are bytes, and array tokens are elements, which is useful for parsing the output of a lexer.
|
|
88
79
|
|
|
89
80
|
## Writing parsers
|
|
90
81
|
|
|
@@ -110,7 +101,7 @@ const list = Parser.separated(Ascii.decInt, Token.literal(",")).pipe(
|
|
|
110
101
|
**Generators**, like `Effect.gen`:
|
|
111
102
|
|
|
112
103
|
```ts
|
|
113
|
-
const header = Parser.gen(function*() {
|
|
104
|
+
const header = Parser.gen(function* () {
|
|
114
105
|
const name = yield* Token.takeWhile(Ascii.isAlphanumeric, { min: 1 })
|
|
115
106
|
yield* Token.literal(": ")
|
|
116
107
|
const value = yield* Ascii.tillLineEnding
|
|
@@ -119,6 +110,21 @@ const header = Parser.gen(function*() {
|
|
|
119
110
|
})
|
|
120
111
|
```
|
|
121
112
|
|
|
113
|
+
The generator body runs on every parse, so parsers created inside it, like `Token.literal(": ")` above, are rebuilt on every run. That is fine for most grammars, but on a hot path (a tokenizer, or a `gen` inside a `repeat`) build them once, outside the body, and `yield*` those:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const colon = Token.literal(": ")
|
|
117
|
+
const name = Token.takeWhile(Ascii.isAlphanumeric, { min: 1 })
|
|
118
|
+
|
|
119
|
+
const header = Parser.gen(function* () {
|
|
120
|
+
const n = yield* name
|
|
121
|
+
yield* colon
|
|
122
|
+
const value = yield* Ascii.tillLineEnding
|
|
123
|
+
yield* Ascii.lineEnding
|
|
124
|
+
return { name: n, value }
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
122
128
|
**Records, tuples and Do notation**:
|
|
123
129
|
|
|
124
130
|
```ts
|
|
@@ -140,13 +146,17 @@ Recursive grammars use `Parser.suspend`:
|
|
|
140
146
|
type Tree = ReadonlyArray<Tree>
|
|
141
147
|
const tree: Parser.Parser<Tree> = Parser.delimited(
|
|
142
148
|
Token.literal("["),
|
|
143
|
-
Parser.separated(
|
|
149
|
+
Parser.separated(
|
|
150
|
+
Parser.suspend(() => tree),
|
|
151
|
+
Token.literal(",")
|
|
152
|
+
),
|
|
144
153
|
Token.literal("]")
|
|
145
154
|
)
|
|
146
155
|
```
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
Each level of nesting uses some of the JavaScript call stack, so input nested a few thousand levels deep exhausts it. The runners catch the overflow and fail with a `ParseError` of kind `"TooDeep"` at the offset reached, so untrusted input can't make them throw. Grammars that must accept deeper nesting need a non-recursive formulation.
|
|
158
|
+
|
|
159
|
+
Low-level parsers can be written directly against the cursor with `Parser.make`, in the same way that any function can be a parser in winnow:
|
|
150
160
|
|
|
151
161
|
```ts
|
|
152
162
|
const upper = Parser.make<string>((state) => {
|
|
@@ -159,10 +169,24 @@ const upper = Parser.make<string>((state) => {
|
|
|
159
169
|
})
|
|
160
170
|
```
|
|
161
171
|
|
|
172
|
+
A driver that makes many small runs over one buffer, such as a tokenizer reading one token at a time, can run a parser's `step` on an `Input.State` itself and reuse that state between runs. `state.reset(offset, { partial, context })` resets every per-run field, so afterwards the state behaves exactly like `new Input.State(state.input, { offset, partial, context })`, even after a run that failed or returned `Incomplete`:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const state = new Input.State(buffer)
|
|
176
|
+
const token = (offset: number) => {
|
|
177
|
+
const value = lexeme.step(state.reset(offset, { partial: true }))
|
|
178
|
+
if (value !== Input.Fail) return { value, end: state.pos }
|
|
179
|
+
if (state.mode === "Incomplete") return undefined // wait for more input
|
|
180
|
+
throw ParseError.fromFailure(state.input, state.reportedFailure(), state.mode, state.encoding)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`step` always runs the interpreter; only the runners use [compiled code](#compiled-parsers).
|
|
185
|
+
|
|
162
186
|
### Combinator overview
|
|
163
187
|
|
|
164
188
|
| Category | Combinators |
|
|
165
|
-
|
|
189
|
+
| --- | --- |
|
|
166
190
|
| Constructors | `succeed`, `sync`, `empty`, `fail`, `failMessage`, `reject`, `todo`, `suspend`, `make`, `location`, `eof` |
|
|
167
191
|
| Mapping | `map`, `as`, `asVoid`, `asSome`, `tap`, `tryMap`, `tryMapUnknown`, `filterMap`, `filter`, `filterOrFail`, `mapParser`, `fromEntries` |
|
|
168
192
|
| Sequencing | `seq`, `struct`, `zip`, `zipWith`, `zipLeft`, `zipRight`, `preceded`, `terminated`, `delimited`, `separatedPair`, `flatMap`, `gen`, `Do`/`bind`/`bindTo`/`let` |
|
|
@@ -175,29 +199,36 @@ const upper = Parser.make<string>((state) => {
|
|
|
175
199
|
| Recovery | `resumeAfter`, `retryAfter` |
|
|
176
200
|
| Debugging | `trace`, `named` |
|
|
177
201
|
|
|
178
|
-
Repetition ranges follow winnow: `Parser.repeat(p)` matches zero or more
|
|
179
|
-
times, `Parser.repeat(p, 3)` exactly three times, and
|
|
180
|
-
`Parser.repeat(p, { min: 1, max: 4 })` between one and four times. When
|
|
181
|
-
`min` is a positive literal, the result is typed as a non-empty array.
|
|
202
|
+
Repetition ranges follow winnow: `Parser.repeat(p)` matches zero or more times, `Parser.repeat(p, 3)` exactly three times, and `Parser.repeat(p, { min: 1, max: 4 })` between one and four times. When `min` is a positive literal, the result is typed as a non-empty array.
|
|
182
203
|
|
|
183
204
|
## Running parsers
|
|
184
205
|
|
|
185
206
|
| Runner | Result |
|
|
186
|
-
|
|
207
|
+
| --- | --- |
|
|
187
208
|
| `Parser.parse(p, input)` | `Result<A, ParseError<E>>`; the whole input must be consumed |
|
|
188
209
|
| `Parser.parseSync(p, input)` | `A`; throws `ParseError<E>` |
|
|
189
|
-
| `Parser.parsePeek(p, input)` | `Result<[remaining, A], ParseError<E>>`, nom-style |
|
|
190
|
-
| `Parser.parsePartial(p, input)` | `Result<Done \| Incomplete, ParseError<E>>` for streaming input |
|
|
210
|
+
| `Parser.parsePeek(p, input)` | `Result<[remaining, A], ParseError<E>>`, nom-style; the parser stopped at `input.length - remaining.length` |
|
|
211
|
+
| `Parser.parsePartial(p, input)` | `Result<Done \| Incomplete, ParseError<E>>` for streaming input; `Done` has the `offset` it stopped at |
|
|
191
212
|
| `Parser.parseEffect(p, input)` | `Effect<A, ParseError<E>, R>`; services come from the Effect context |
|
|
192
213
|
| `Parser.parseRecoverable(p, input)` | `{ value: Option<A>, errors }` with error recovery enabled |
|
|
193
214
|
| `ParserStream.parseMany(p)` | `Stream<I> => Stream<A>` (see [Streaming](#streaming-and-partial-input)) |
|
|
194
215
|
| `ParserSchema.make(p)` | a `Schema` decoding strings with the parser |
|
|
216
|
+
| `ParserSchema.fromSchema(s)` | a `Schema` decoding strings with the parser derived from `s`, and encoding with the derived printer (see [Parsers from schemas](#parsers-from-schemas)) |
|
|
217
|
+
| `Syntax.parse(p, input)` | a `Syntax.Tree`: a syntax tree, the value and the errors (see [Syntax trees](#syntax-trees-and-incremental-parsing)) |
|
|
195
218
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
219
|
+
Every `Parser` runner takes an `offset` option, to start parsing part-way into the input. The result is that of parsing `input.slice(offset)`, without the copy, and offsets (`Done`'s `offset`, spans, `ParseError` offsets, lines and columns) are positions in the whole input. A lexer can read one token at a time from a buffer, compiled parsers included:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const lexeme = Compiler.jit(Parser.preceded(Ascii.multispace0, Ascii.alpha1))
|
|
223
|
+
|
|
224
|
+
const buffer = "let x = 1"
|
|
225
|
+
Parser.parsePartial(lexeme, buffer, { offset: 3 })
|
|
226
|
+
// Result.succeed({ _tag: "Done", value: "x", offset: 5 })
|
|
227
|
+
Parser.parsePeek(lexeme, buffer, { offset: 0 })
|
|
228
|
+
// Result.succeed([" x = 1", "let"]): it stopped at 9 - 6 = 3
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Parsers over bytes (`Uint8Array`) also accept an `ArrayBuffer`, a `SharedArrayBuffer`, a `DataView` or any other typed array, in the runners and as `ParserStream` chunks. The data is viewed as a `Uint8Array` of the same memory, not copied, so offsets are byte offsets and slices are `Uint8Array`s:
|
|
201
232
|
|
|
202
233
|
```ts
|
|
203
234
|
const response = await fetch(url)
|
|
@@ -205,71 +236,64 @@ Parser.parse(Text.utf8.rest, await response.arrayBuffer())
|
|
|
205
236
|
Parser.parse(Binary.beU32, new DataView(buffer, 4))
|
|
206
237
|
```
|
|
207
238
|
|
|
208
|
-
Wider typed arrays are viewed in the platform's byte order, which is little
|
|
209
|
-
endian on all common platforms. So a `Uint16Array` of UTF-16 code units can
|
|
210
|
-
be parsed with `Text.utf16le`. `Input.fromSource` performs the same
|
|
211
|
-
conversion.
|
|
239
|
+
Wider typed arrays are viewed in the platform's byte order, which is little endian on all common platforms. So a `Uint16Array` of UTF-16 code units can be parsed with `Text.utf16le`. `Input.fromSource` performs the same conversion.
|
|
212
240
|
|
|
213
241
|
## Compiled parsers
|
|
214
242
|
|
|
215
|
-
Combinators are flexible but run as a tree of closures. `
|
|
216
|
-
turns a grammar into fused, specialized code, without changing how it is
|
|
217
|
-
written or what it returns:
|
|
243
|
+
Combinators are flexible but run as a tree of closures. The `Compiler` module turns a grammar into fused, specialized code, without changing how it is written or what it returns:
|
|
218
244
|
|
|
219
245
|
```ts
|
|
220
|
-
|
|
246
|
+
import { Compiler } from "@thomasfosterau/effect-parse"
|
|
247
|
+
|
|
248
|
+
export const json = Compiler.jit(Parser.preceded(ws, value))
|
|
221
249
|
|
|
222
250
|
Parser.parse(json, text) // same Result as before, about 5 times faster
|
|
223
251
|
```
|
|
224
252
|
|
|
225
|
-
On the JSON example, compiled parsing runs at 5–6 times the speed of the
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
`
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Compiled code records no diagnostics. When it fails, the parser runs again
|
|
238
|
-
interpreted, so errors are exactly the same, byte for byte. Callbacks should
|
|
239
|
-
therefore be pure: on failing input they may run twice, and those whose
|
|
240
|
-
results are unused may be skipped. Parsers built with `Parser.make` or
|
|
241
|
-
`Parser.gen` run interpreted inside compiled code. See the
|
|
242
|
-
[design notes](docs/DESIGN.md#6-compiled-parsers) for details.
|
|
253
|
+
On the JSON example, compiled parsing runs at 5–6 times the speed of the interpreter, for strings, UTF-8 and UTF-16 alike, and at 1–4 times the time of `JSON.parse` depending on the document (see [the design notes](docs/DESIGN.md#results)).
|
|
254
|
+
|
|
255
|
+
There are two compilers, one function each:
|
|
256
|
+
|
|
257
|
+
- `Compiler.jit` generates JavaScript where `new Function` is allowed, and falls back to the closure compiler where it isn't.
|
|
258
|
+
- `Compiler.closure` never generates code. It is meant for pages with a Content Security Policy.
|
|
259
|
+
|
|
260
|
+
A bundle contains only the compilers it calls. A program that compiles nothing bundles neither, and one that uses only `Compiler.closure` contains no `new Function`. esbuild keeps every member of a namespace imported from the root entry, so with esbuild import from the subpaths instead (`import * as Compiler from "@thomasfosterau/effect-parse/Compiler"`). Rolldown and Rollup drop unused members either way.
|
|
261
|
+
|
|
262
|
+
Both return the same parser. The code is built on the first run, for each kind of input, and cached. `parse`, `parseSync`, `parsePeek`, `parsePartial`, `parseEffect`, `ParserStream.parseMany` and `ParserStream.parseAll` use it, from any `offset`; `parseRecoverable` and tracing stay interpreted. A driver that keeps its own `Input.State` (a lexer reading one token at a time, say) runs the compiled code with `Parser.run(p, state)`; calling `p.step(state)` directly always runs interpreted.
|
|
263
|
+
|
|
264
|
+
Compiled code records no diagnostics. When it fails, the parser runs again interpreted, so errors are exactly the same, byte for byte. Callbacks should therefore be pure: on failing input they may run twice, and those whose results are unused may be skipped. Parsers built with `Parser.make` or `Parser.gen` run interpreted inside compiled code. See the [design notes](docs/DESIGN.md#6-compiled-parsers) for details.
|
|
243
265
|
|
|
244
266
|
Some ways of writing a grammar compile to faster code than others:
|
|
245
267
|
|
|
246
|
-
- Build objects with `Parser.fromEntries` (or `Parser.map(Object.fromEntries)`)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
268
|
+
- Build objects with `Parser.fromEntries` (or `Parser.map(Object.fromEntries)`) directly on a `separated` or `repeat` of pairs: compiled code assigns the properties as it parses, without the pairs and their array. A lambda that calls `Object.fromEntries` can't be recognized.
|
|
269
|
+
- Prefer token sets (`"abc"`, `Token.range`, the `Ascii` and `Text` predicates) to arbitrary predicates: they test code points without allocating, and strings, ranges and the `Ascii` predicates compile to comparisons. A `(c: string) => boolean` predicate receives each character as a new string. For a class of your own, declare it over code points with `Token.codePoints`, listing its ranges if you know them:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const isNameStart = Token.codePoints("name start", (c) => c === 0x5f || (c >= 0x61 && c <= 0x7a) || c >= 0xc0, [
|
|
273
|
+
[0x5f, 0x5f],
|
|
274
|
+
[0x61, 0x7a],
|
|
275
|
+
[0xc0, 0x10ffff]
|
|
276
|
+
])
|
|
277
|
+
```
|
|
278
|
+
|
|
252
279
|
- Avoid `flatMap` on hot paths: the parser it returns runs interpreted.
|
|
280
|
+
- Give `Parser.dispatch` a record of cases, not a function. With a record, compiled code switches on the key and runs the chosen case compiled, and on `Token.any` or `Parser.peek(Token.any)` it switches on the next unit without building the key. The parser a function returns runs interpreted, and if the function builds it (`(c) => Parser.cut(p)`), it is built again on every run.
|
|
281
|
+
- Build parsers once, outside callbacks and `Parser.gen` bodies. A parser built while parsing costs an allocation per combinator, every time, and is never compiled.
|
|
282
|
+
- On token arrays (the output of a lexer), match tokens with `Token.oneOf`, `Token.noneOf`, `Token.takeWhile` and `Token.anyToken`, with a predicate or a list of tokens: the JIT calls the predicate inline. A `Parser.make` that reads `state.input[state.pos]` runs interpreted, and records a failure, with its expected list, every time it doesn't match.
|
|
283
|
+
- Drive a lexer with `Parser.run` on one `Input.State` that you `reset` before each token: no run allocates a state, a `Result` or a slice, and the token parser runs compiled. When a failure only means "try something else", pass `{ diagnostics: false }`, so a failed compiled run isn't repeated interpreted to build an error you won't read.
|
|
253
284
|
- `Ascii.float` and `Text.float` convert numbers as they match them.
|
|
254
285
|
|
|
255
|
-
[`json-fast.ts`](examples/json-fast.ts) is the JSON example written this
|
|
256
|
-
way; it runs records-heavy documents about 1.5 times as fast as
|
|
257
|
-
[`json-text.ts`](examples/json-text.ts).
|
|
286
|
+
[`json-fast.ts`](examples/json-fast.ts) is the JSON example written this way; it runs records-heavy documents about 1.5 times as fast as [`json-text.ts`](examples/json-text.ts).
|
|
258
287
|
|
|
259
288
|
## Errors
|
|
260
289
|
|
|
261
290
|
Failures have a mode, as in winnow:
|
|
262
291
|
|
|
263
|
-
- **Backtrack**: recoverable. `alt`, `opt`, `repeat` and friends try
|
|
264
|
-
|
|
265
|
-
- **Cut**: unrecoverable. Use `Parser.cut(p)` once a branch is certain
|
|
266
|
-
(e.g. after an opening `[`), so errors point at the real problem instead of
|
|
267
|
-
backtracking to an unhelpful alternative.
|
|
292
|
+
- **Backtrack**: recoverable. `alt`, `opt`, `repeat` and friends try something else.
|
|
293
|
+
- **Cut**: unrecoverable. Use `Parser.cut(p)` once a branch is certain (e.g. after an opening `[`), so errors point at the real problem instead of backtracking to an unhelpful alternative.
|
|
268
294
|
- **Incomplete**: partial input ran out (see below).
|
|
269
295
|
|
|
270
|
-
`ParseError` is a `Data.TaggedError("ParseError")`. It records the
|
|
271
|
-
`offset`, `kind`, `expected` set, `context` labels and an optional typed
|
|
272
|
-
`cause`, and its `message` points at the problem:
|
|
296
|
+
`ParseError` is a `Data.TaggedError("ParseError")`. It records the `offset`, `kind`, `expected` set, `context` labels and an optional typed `cause`, and its `message` points at the problem:
|
|
273
297
|
|
|
274
298
|
```text
|
|
275
299
|
Parse error at line 1, column 4:
|
|
@@ -281,33 +305,28 @@ expected value, found ","
|
|
|
281
305
|
|
|
282
306
|
effect-parse tries hard to produce good messages without annotations:
|
|
283
307
|
|
|
284
|
-
- `alt` merges the expectations of failed branches:
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
|
|
288
|
-
- The furthest failure wins (the PEG heuristic), so a value that fails
|
|
289
|
-
after a separator is reported where it failed.
|
|
290
|
-
- `Parser.expected("value")` names a rule; `Parser.context("array")` adds
|
|
291
|
-
"while parsing array".
|
|
308
|
+
- `alt` merges the expectations of failed branches: `expected "true", "false" or "null"`.
|
|
309
|
+
- Optional and repeated parsers leave _hints_, so after `repeat(digit)` a failure reads `expected digit or ")"`.
|
|
310
|
+
- The furthest failure wins (the PEG heuristic), so a value that fails after a separator is reported where it failed.
|
|
311
|
+
- `Parser.expected("value")` names a rule; `Parser.context("array")` adds "while parsing array".
|
|
292
312
|
|
|
293
|
-
User errors are typed. `Parser.fail(e)`, `Parser.tryMap(f)` and
|
|
294
|
-
`Parser.filterOrFail(pred, f)` add their error type to `E`, and the error
|
|
295
|
-
arrives as `ParseError.cause`:
|
|
313
|
+
User errors are typed. `Parser.fail(e)`, `Parser.tryMap(f)` and `Parser.filterOrFail(pred, f)` add their error type to `E`, and the error arrives as `ParseError.cause`:
|
|
296
314
|
|
|
297
315
|
```ts
|
|
298
|
-
class OutOfRange extends
|
|
316
|
+
class OutOfRange extends Schema.TaggedError<OutOfRange>()("OutOfRange", { value: Schema.Number }) {}
|
|
299
317
|
|
|
300
318
|
const port = Ascii.decUint.pipe(
|
|
301
|
-
Parser.filterOrFail(
|
|
319
|
+
Parser.filterOrFail(
|
|
320
|
+
(n) => n < 65536,
|
|
321
|
+
(value) => new OutOfRange({ value })
|
|
322
|
+
)
|
|
302
323
|
)
|
|
303
324
|
// Parser<number, string | OutOfRange>
|
|
304
325
|
```
|
|
305
326
|
|
|
306
327
|
## Streaming and partial input
|
|
307
328
|
|
|
308
|
-
nom has separate `streaming` and `complete` modules. winnow marks the input
|
|
309
|
-
as `Partial`. In effect-parse, partiality is decided by the runner, so every
|
|
310
|
-
parser works in both modes:
|
|
329
|
+
nom has separate `streaming` and `complete` modules. winnow marks the input as `Partial`. In effect-parse, partiality is decided by the runner, so every parser works in both modes:
|
|
311
330
|
|
|
312
331
|
```ts
|
|
313
332
|
Parser.parsePartial(Ascii.digit1, "123")
|
|
@@ -316,9 +335,7 @@ Parser.parsePartial(Ascii.digit1, "123;")
|
|
|
316
335
|
// Result.succeed({ _tag: "Done", value: "123", offset: 3 })
|
|
317
336
|
```
|
|
318
337
|
|
|
319
|
-
`ParserStream.parseMany` connects this to Effect streams. It buffers chunks,
|
|
320
|
-
retries on `Incomplete`, and parses the tail in complete mode when the stream
|
|
321
|
-
ends:
|
|
338
|
+
`ParserStream.parseMany` connects this to Effect streams. It buffers chunks, retries on `Incomplete`, and parses the tail in complete mode when the stream ends:
|
|
322
339
|
|
|
323
340
|
```ts
|
|
324
341
|
import { Effect, Stream } from "effect"
|
|
@@ -332,33 +349,22 @@ Stream.make("1\n2", "3\n", "4\n").pipe(ParserStream.parseMany(line), Stream.runC
|
|
|
332
349
|
|
|
333
350
|
### Parsing a download as it arrives
|
|
334
351
|
|
|
335
|
-
`ParserStream.parseAll` parses a stream as one document, and does not wait
|
|
336
|
-
for it to end: each chunk is parsed as it arrives. When the input runs out
|
|
337
|
-
mid-way, repetitions (`repeat`, `separated`, `fold`, `escaped`, ...)
|
|
338
|
-
remember how far they got, so the next attempt carries on from there rather
|
|
339
|
-
than starting over. `parseResponse` and `parseReadable` do the same for web
|
|
340
|
-
streams, so the body of a `fetch` response is parsed straight from its
|
|
341
|
-
bytes, without decoding it first or waiting for the download:
|
|
352
|
+
`ParserStream.parseAll` parses a stream as one document, and does not wait for it to end: each chunk is parsed as it arrives. When the input runs out mid-way, repetitions (`repeat`, `separated`, `fold`, `escaped`, ...) remember how far they got, so the next attempt carries on from there rather than starting over. `parseResponse` and `parseReadable` do the same for web streams, so the body of a `fetch` response is parsed straight from its bytes, without decoding it first or waiting for the download:
|
|
342
353
|
|
|
343
354
|
```ts
|
|
344
355
|
import { Effect } from "effect"
|
|
345
|
-
import {
|
|
356
|
+
import { Compiler, ParserStream, Text } from "@thomasfosterau/effect-parse"
|
|
346
357
|
|
|
347
|
-
const json =
|
|
358
|
+
const json = Compiler.jit(jsonGrammar(Text.utf8)) // see examples/json-text.ts
|
|
348
359
|
|
|
349
360
|
const value = await Effect.runPromise(ParserStream.parseResponse(await fetch(url), json))
|
|
350
361
|
```
|
|
351
362
|
|
|
352
|
-
The result, and any error, is the same as parsing the whole body at once.
|
|
353
|
-
Compiled parsers stream compiled. With the JSON example on a 2 MB document,
|
|
354
|
-
`parseResponse` returns about 1 ms after the last byte at 10 MB/s, and keeps
|
|
355
|
-
pace with `Response.json()` at 25 MB/s; on faster connections the native
|
|
356
|
-
parser is quicker (`pnpm bench:stream`).
|
|
363
|
+
The result, and any error, is the same as parsing the whole body at once. Compiled parsers stream compiled. With the JSON example on a 2 MB document, `parseResponse` returns about 1 ms after the last byte at 10 MB/s, and keeps pace with `Response.json()` at 25 MB/s; on faster connections the native parser is quicker (`vp run bench:stream`).
|
|
357
364
|
|
|
358
365
|
## Services
|
|
359
366
|
|
|
360
|
-
Parsers can require Effect services. These replace winnow's `Stateful`
|
|
361
|
-
input: configuration, symbol tables, indentation stacks and so on.
|
|
367
|
+
Parsers can require Effect services. These replace winnow's `Stateful` input: configuration, symbol tables, indentation stacks and so on.
|
|
362
368
|
|
|
363
369
|
```ts
|
|
364
370
|
import { Context, Effect } from "effect"
|
|
@@ -370,30 +376,63 @@ const number = Parser.service(Radix).pipe(
|
|
|
370
376
|
)
|
|
371
377
|
// Parser<number, never, Radix>
|
|
372
378
|
|
|
373
|
-
Parser.parse(Parser.provideService(number, Radix, 16), "ff")
|
|
379
|
+
Parser.parse(Parser.provideService(number, Radix, 16), "ff") // 255
|
|
374
380
|
Parser.parseEffect(number, "777").pipe(Effect.provideService(Radix, 8)) // Effect<511>
|
|
375
381
|
```
|
|
376
382
|
|
|
383
|
+
## Parsers from schemas
|
|
384
|
+
|
|
385
|
+
`ParserSchema.toParser` derives a parser from an Effect `Schema`, and `ParserSchema.toPrinter` the printer that writes the same text, so a text format can be declared once, as the schema of the values it holds. Annotations, set with `ParserSchema.annotate` (under the key `ParserSchema.annotationKey`), say how each part is written: a `prefix` and `suffix`, the `separator` between fields, elements or entries, the `keySeparator` of `key=value` entries, whether `whitespace` may surround that text, or a `parser` and `print` that replace the derived ones.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { Schema } from "effect"
|
|
389
|
+
|
|
390
|
+
const Point = Schema.Struct({ x: Schema.Int, y: Schema.Int }).pipe(
|
|
391
|
+
ParserSchema.annotate({ prefix: "(", separator: ",", suffix: ")", whitespace: true })
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const Config = Schema.Struct({
|
|
395
|
+
name: Schema.String,
|
|
396
|
+
origin: Point,
|
|
397
|
+
color: Schema.optionalKey(
|
|
398
|
+
Schema.Int.pipe(ParserSchema.annotate({ parser: Ascii.hexUint, print: (n) => n.toString(16), prefix: "#" }))
|
|
399
|
+
)
|
|
400
|
+
}).pipe(ParserSchema.annotate({ separator: ";", keySeparator: "=" }))
|
|
401
|
+
|
|
402
|
+
const config = ParserSchema.toParser(Config)
|
|
403
|
+
Parser.parse(config, "origin=( 1, -2 );name=main;color=#ff")
|
|
404
|
+
// Result.succeed({ name: "main", origin: { x: 1, y: -2 }, color: 255 })
|
|
405
|
+
|
|
406
|
+
Parser.parse(config, "name=main;origin=(1,x)")
|
|
407
|
+
// Result.fail(ParseError):
|
|
408
|
+
// Parse error at line 1, column 21:
|
|
409
|
+
// 1 | name=main;origin=(1,x)
|
|
410
|
+
// | ^
|
|
411
|
+
// expected one of "+-" or digit, found "x"
|
|
412
|
+
|
|
413
|
+
const print = ParserSchema.toPrinter(Config)
|
|
414
|
+
print({ name: "main", origin: { x: 1, y: -2 }, color: 255 })
|
|
415
|
+
// Result.succeed("name=main;origin=(1,-2);color=#ff")
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Without annotations, a struct or tuple is its fields in order, an array its elements, a union its first member that parses, and a `suspend`ed schema recursion. A string runs to the next separator or suffix; a number is decimal, and an integer when the schema checks `isInt`. A record, or a struct with a `keySeparator`, is `key=value` entries in any order. A schema's checks and transformations (a `Class`, `NumberFromString`, a brand) run as `Schema.decode` runs them, and their failures are parse errors whose `cause` is the `SchemaError`. The doc comment of `toParser` lists every rule.
|
|
419
|
+
|
|
420
|
+
A printer writes the text its parser reads, without optional whitespace, and fails with a `SchemaError` on a value it cannot write so that it reads back: a string holding the separator that would end it, or a part with a `parser` but no `print`.
|
|
421
|
+
|
|
422
|
+
A derived parser is an ordinary parser: it composes with the combinators, and the [compilers](#compiled-parsers) compile it. `ParserSchema.fromSchema(schema)` wraps the parser and the printer as a `Schema` from `string`. Encoding parses the printed text back, and fails if that gives a different value, so decoding what it encoded always gives the value back, even where the grammar is ambiguous.
|
|
423
|
+
|
|
377
424
|
## Unicode text and encodings
|
|
378
425
|
|
|
379
|
-
`Token` reads strings by code point, but the `Ascii` character classes and
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
| `Text.utf16le`, `Text.utf16be` | `Uint8Array` | bytes |
|
|
390
|
-
| `Text.latin1` | `Uint8Array` | bytes |
|
|
391
|
-
|
|
392
|
-
Every set has the same members (`any`, `literal`, `caseless`, `oneOf`,
|
|
393
|
-
`takeWhile`, `takeUntil`, `alpha1`, `digit1`, `space1`, `identifier`,
|
|
394
|
-
`lineEnding`, `decInt`, `float`, `escaped`, `bom`, ...), and every parser
|
|
395
|
-
returns `string`s. A grammar written once against `TextParsers<I>`
|
|
396
|
-
therefore parses any of these inputs:
|
|
426
|
+
`Token` reads strings by code point, but the `Ascii` character classes and numbers only cover ASCII, and they treat bytes as ASCII. The `Text` module adds Unicode parsers that work with any encoding. `Text.make(encoding)` builds a set of parsers (`Text.TextParsers<I>`), and sets are provided for the common encodings:
|
|
427
|
+
|
|
428
|
+
| Parsers | Input | Offsets |
|
|
429
|
+
| ------------------------------ | ------------ | ----------------- |
|
|
430
|
+
| `Text.string` | `string` | UTF-16 code units |
|
|
431
|
+
| `Text.utf8` | `Uint8Array` | bytes |
|
|
432
|
+
| `Text.utf16le`, `Text.utf16be` | `Uint8Array` | bytes |
|
|
433
|
+
| `Text.latin1` | `Uint8Array` | bytes |
|
|
434
|
+
|
|
435
|
+
Every set has the same members (`any`, `literal`, `caseless`, `oneOf`, `takeWhile`, `takeUntil`, `alpha1`, `digit1`, `space1`, `identifier`, `lineEnding`, `decInt`, `float`, `escaped`, `bom`, ...), and every parser returns `string`s. A grammar written once against `TextParsers<I>` therefore parses any of these inputs:
|
|
397
436
|
|
|
398
437
|
```ts
|
|
399
438
|
import { Encoding, Parser, Text } from "@thomasfosterau/effect-parse"
|
|
@@ -401,24 +440,14 @@ import { Encoding, Parser, Text } from "@thomasfosterau/effect-parse"
|
|
|
401
440
|
const assignment = <I extends string | Uint8Array>(T: Text.TextParsers<I>) =>
|
|
402
441
|
Parser.separatedPair(T.identifier, Parser.delimited(T.space0, T.literal("≔"), T.space0), T.decInt)
|
|
403
442
|
|
|
404
|
-
Parser.parse(assignment(Text.string), "größe ≔ 42")
|
|
443
|
+
Parser.parse(assignment(Text.string), "größe ≔ 42") // ["größe", 42]
|
|
405
444
|
Parser.parse(assignment(Text.utf8), new TextEncoder().encode("größe ≔ 42")) // ["größe", 42]
|
|
406
445
|
Parser.parse(assignment(Text.utf16le), Encoding.utf16le.encode("größe ≔ 42"))
|
|
407
446
|
```
|
|
408
447
|
|
|
409
|
-
- **Unicode classes.** `alpha1` matches `"Ωμέγα"`, `digit1` matches
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
`Text.isIdStart`, ...) also work with `Token`, and `Text.property("Script=Greek")`
|
|
413
|
-
tests any Unicode property. Numbers (`decInt`, `float`) take ASCII digits
|
|
414
|
-
only, as most languages do.
|
|
415
|
-
- **Bytes are decoded as they are parsed.** Offsets stay byte offsets, and
|
|
416
|
-
`Parser.taken` still returns a zero-copy `Uint8Array`. On partial input,
|
|
417
|
-
a character split across chunks reports `Incomplete`, so
|
|
418
|
-
`ParserStream.parseMany` parses a byte stream as text without decoding it
|
|
419
|
-
first.
|
|
420
|
-
- **Decoding is strict.** Invalid UTF-8 and unpaired surrogates never match.
|
|
421
|
-
Errors on encoded bytes show the input as text:
|
|
448
|
+
- **Unicode classes.** `alpha1` matches `"Ωμέγα"`, `digit1` matches `"١٢٣"`, `space1` matches a no-break space, and `identifier` follows UAX #31. The predicates (`Text.isLetter`, `Text.isWhitespace`, `Text.isIdStart`, ...) also work with `Token`, and `Text.property("Script=Greek")` tests any Unicode property. Numbers (`decInt`, `float`) take ASCII digits only, as most languages do.
|
|
449
|
+
- **Bytes are decoded as they are parsed.** Offsets stay byte offsets, and `Parser.taken` still returns a zero-copy `Uint8Array`. On partial input, a character split across chunks reports `Incomplete`, so `ParserStream.parseMany` parses a byte stream as text without decoding it first.
|
|
450
|
+
- **Decoding is strict.** Invalid UTF-8 and unpaired surrogates never match. Errors on encoded bytes show the input as text:
|
|
422
451
|
|
|
423
452
|
```text
|
|
424
453
|
Parse error at line 1, column 3 (byte 2):
|
|
@@ -427,125 +456,156 @@ Parser.parse(assignment(Text.utf16le), Encoding.utf16le.encode("größe ≔ 42")
|
|
|
427
456
|
expected letter or end of input, found invalid UTF-8 0xff
|
|
428
457
|
```
|
|
429
458
|
|
|
430
|
-
- **Byte order marks.** `Encoding.sniff(bytes)` picks an encoding from a BOM,
|
|
431
|
-
|
|
432
|
-
- **
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
459
|
+
- **Byte order marks.** `Encoding.sniff(bytes)` picks an encoding from a BOM, and `T.bom` skips one.
|
|
460
|
+
- **Binary data.** The byte parsers accept `ArrayBuffer`s, `DataView`s and typed arrays too (see [Running parsers](#running-parsers)).
|
|
461
|
+
- **Custom encodings.** `Text.make` accepts any `Encoding.Encoding`: a `decode` function plus a few properties.
|
|
462
|
+
|
|
463
|
+
JavaScript strings are parsed natively, and the `Text` parsers only decode byte input. Converting strings to bytes up front would be slower: a `TextDecoder` call to produce each slice costs several times more than `String.prototype.slice`. `vp run bench` compares the paths with a JSON grammar written once for every encoding ([`json-text.ts`](examples/json-text.ts)). In our runs `Text.string` was roughly 5–15% slower than the `Token`/`Ascii` parsers, and parsing UTF-8 directly was within about 10% of decoding it first.
|
|
464
|
+
|
|
465
|
+
## Syntax trees and incremental parsing
|
|
466
|
+
|
|
467
|
+
The `Syntax` module builds concrete syntax trees, like [Tree-sitter](https://tree-sitter.github.io): trees of nodes with spans, parents and siblings, which can be searched by position, walked with a cursor, and parsed again _incrementally_ after an edit.
|
|
468
|
+
|
|
469
|
+
Mark the rules that should appear in the tree with `Syntax.node`. Outside `Syntax.parse` the marks change nothing (compiled parsers compile them away), so a grammar can be used both for values and for trees:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
import { Ascii, Parser, Syntax, Token } from "@thomasfosterau/effect-parse"
|
|
473
|
+
|
|
474
|
+
const name = Syntax.node(Ascii.alpha1, "name")
|
|
475
|
+
const number = Syntax.node(Ascii.decInt, "number")
|
|
476
|
+
const pair = Syntax.node(
|
|
477
|
+
Parser.separatedPair(Syntax.field(name, "key"), Syntax.token(Token.literal("=")), Syntax.field(number, "value")),
|
|
478
|
+
"pair"
|
|
479
|
+
)
|
|
480
|
+
const config = Parser.separated(pair, Token.literal("\n"))
|
|
481
|
+
|
|
482
|
+
const tree = Syntax.parse(config, "width=80\nheight=24")
|
|
483
|
+
String(tree.root)
|
|
484
|
+
// (document (pair key: (name) value: (number)) (pair key: (name) value: (number)))
|
|
485
|
+
|
|
486
|
+
const node = tree.descendantAt(16) // the deepest node at offset 16
|
|
487
|
+
node.kind // "number"
|
|
488
|
+
node.text // "24"
|
|
489
|
+
node.value // 24, the parser's output
|
|
490
|
+
node.parent?.childByField("key")?.text // "height"
|
|
491
|
+
tree.lineColumn(node.start) // { line: 2, column: 8 }
|
|
492
|
+
tree.value // Option.some([["width", 80], ["height", 24]])
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Each node's `value` is its parser's output, so a tree carries the parsed values as well as the structure. `Syntax.token` marks _anonymous_ nodes (punctuation, keywords), which are left out of `namedChildren` and of the printed tree, and `Syntax.field` names a child in its parent. Nodes have `children`, `namedChildren`, `parent`, `nextSibling`, `previousSibling`, `childAt(offset)`, `descendantAt(start, end?)` and `descendants()`; `tree.walk()` returns a `TreeCursor` with Tree-sitter's `gotoFirstChild`, `gotoNextSibling`, `gotoParent`, `gotoFirstChildForOffset`, and so on.
|
|
496
|
+
|
|
497
|
+
**Errors.** A syntax parse always returns a tree. Failures recovered with `Parser.resumeAfter` or `Parser.retryAfter` (recovery is always on here) become `ERROR` nodes over the input that was skipped, and so does any input left after a fatal failure. `tree.errors` lists them as `ParseError`s.
|
|
498
|
+
|
|
499
|
+
**Incremental parsing.** After an edit, `Syntax.reparse(tree, newInput, edits?)` parses the new input reusing the nodes of the old tree that the edit leaves unchanged; `Syntax.replace(tree, start, end, text)` does the edit too. The result is exactly the tree a fresh parse gives (the test suite checks this on random edits), but only the nodes around the edit are parsed again:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const edited = Syntax.replace(tree, 6, 8, "100") // "width=100\nheight=24"
|
|
503
|
+
edited.value // Option.some([["width", 100], ["height", 24]])
|
|
504
|
+
edited.reused // 2: "width", and the second pair as a whole
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The edits are Tree-sitter's `{ start, oldEnd, newEnd }`, applied in order; without them the edit is found by comparing the two inputs. A node is reused when it was built by the same `Syntax.node` and the input its parse _looked at_ is unchanged. That includes lookahead: a node is parsed again when an edit lands just after it, if its parse read that far (to see that a number ended, say). Built-in parsers record how far they read; custom ones may need the `lookahead` option. Values are reused as they are, so a node's value should depend only on its input: not on services, closure state or absolute offsets (`Parser.span`, `Parser.location`); use the node's `start` and `end` instead.
|
|
508
|
+
|
|
509
|
+
On a 0.58 M-character JSON document (`vp run bench:syntax`), building the tree takes about twice as long as parsing the value interpreted, and reparsing after a one-character edit takes about 2 ms, against 80 ms for the full parse. Most of that is stepping over the 2000 reused elements of the top-level array; the tree is not rebalanced as Tree-sitter's is.
|
|
445
510
|
|
|
446
511
|
## Modules
|
|
447
512
|
|
|
448
513
|
| Module | Contents | winnow / nom equivalent |
|
|
449
|
-
|
|
514
|
+
| --- | --- | --- |
|
|
450
515
|
| `Parser` | the type, combinators, runners | `Parser` trait, `combinator` / `combinator`, `branch`, `multi`, `sequence` |
|
|
451
|
-
| `Token` | `any`, `literal`, `oneOf`, `noneOf`, `take`, `takeWhile`, `takeTill`, `takeUntil`, `rest`, `range` | `token` / `bytes` |
|
|
516
|
+
| `Token` | `any`, `literal`, `oneOf`, `noneOf`, `take`, `takeWhile`, `takeTill`, `takeUntil`, `rest`, `range`, `codePoints` | `token` / `bytes` |
|
|
452
517
|
| `Text` | Unicode parsers for any encoding: `string`, `utf8`, `utf16le`, `utf16be`, `latin1`, `make`; Unicode predicates | — |
|
|
453
518
|
| `Encoding` | text encodings for `Text`, `sniff` | — |
|
|
454
519
|
| `Ascii` | character classes, `lineEnding`, `decInt`, `decUint`, `hexUint`, `float`, `caseless`, `takeEscaped`, `escaped`, `Ascii.bytes.*` | `ascii` / `character`, `number` |
|
|
455
520
|
| `Binary` | `u8`, `beU16` … `leI128`, floats, `lengthTake`, `lengthRepeat`, `lengthAndThen` | `binary` / `number`, `multi::length_*` |
|
|
456
521
|
| `Bits` | `bits`, `bytes`, `take`, `takeBig`, `pattern`, `bool` | `binary::bits` / `bits` |
|
|
457
522
|
| `Expression` | Pratt parser with prefix, postfix and infix operators | `combinator::expression` / `nom-language::precedence` |
|
|
523
|
+
| `Compiler` | `jit`, `closure`: [compiled parsers](#compiled-parsers) | — |
|
|
458
524
|
| `ParseError` | the error type, `lineColumn`, `formatExpected` | `error` |
|
|
459
525
|
| `ParserStream` | `parseMany`, `parseAll` over Effect `Stream`s | `Partial` / `streaming` |
|
|
460
|
-
| `ParserSchema` | `make`: a `Schema` from a parser | — |
|
|
526
|
+
| `ParserSchema` | `make`: a `Schema` from a parser; `toParser`, `toPrinter`, `fromSchema`, `annotate`: a parser and printer from an annotated `Schema` | — |
|
|
527
|
+
| `Syntax` | syntax trees: `node`, `token`, `field`, `parse`, incremental `reparse` and `replace`, `Tree`, `SyntaxNode`, `TreeCursor` | — (Tree-sitter) |
|
|
461
528
|
| `Nom` | nom names and argument orders (`tag`, `many0`, `separatedList1`, `mapRes`, …) | nom |
|
|
462
529
|
| `Input` | input types and the `State` cursor for `Parser.make` | `stream` |
|
|
463
530
|
|
|
464
|
-
Token parsers that cannot infer their input type take a type argument that
|
|
465
|
-
defaults to `string`, like a Rust turbofish:
|
|
531
|
+
Token parsers that cannot infer their input type take a type argument that defaults to `string`, like a Rust turbofish:
|
|
466
532
|
|
|
467
533
|
```ts
|
|
468
|
-
Token.take(4)
|
|
469
|
-
Token.take<Uint8Array>(4)
|
|
534
|
+
Token.take(4) // Parser<string>
|
|
535
|
+
Token.take<Uint8Array>(4) // Parser<Uint8Array, never, never, Uint8Array>
|
|
470
536
|
Token.oneOf<ReadonlyArray<Tok>>((t) => t.kind === "Number")
|
|
471
537
|
```
|
|
472
538
|
|
|
473
539
|
## Coming from nom or winnow
|
|
474
540
|
|
|
475
|
-
| nom
|
|
476
|
-
|
|
477
|
-
| `tag("x")`
|
|
478
|
-
| `char('x')`
|
|
479
|
-
| `alt((a, b))`
|
|
480
|
-
| `(a, b)` / `tuple`
|
|
481
|
-
| —
|
|
482
|
-
| `many0(p)`
|
|
483
|
-
| `many1(p)`
|
|
484
|
-
| `count(p, n)`
|
|
485
|
-
| `separated_list0(s, p)` | `separated(0.., p, s)`
|
|
486
|
-
| `fold_many0(p, i, f)`
|
|
487
|
-
| `many_till(p, e)`
|
|
488
|
-
| `take_while1(f)`
|
|
489
|
-
| `map(p, f)`
|
|
490
|
-
| `map_res(p, f)`
|
|
491
|
-
| `map_opt(p, f)`
|
|
492
|
-
| `verify(p, f)`
|
|
493
|
-
| `value(v, p)`
|
|
494
|
-
| `recognize(p)`
|
|
495
|
-
| `consumed(p)`
|
|
496
|
-
| `map_parser(p, q)`
|
|
497
|
-
| `flat_map(p, f)`
|
|
498
|
-
| `cut(p)`
|
|
499
|
-
| `context("x", p)`
|
|
500
|
-
| `complete(p)`
|
|
501
|
-
| `IResult`
|
|
502
|
-
| `streaming::*`
|
|
503
|
-
| —
|
|
504
|
-
| —
|
|
505
|
-
| —
|
|
506
|
-
| —
|
|
507
|
-
|
|
508
|
-
The `Nom` module provides the nom names directly, with nom's argument
|
|
509
|
-
orders, for mechanical ports.
|
|
541
|
+
| nom | winnow | effect-parse |
|
|
542
|
+
| ----------------------- | ----------------------------------- | --------------------------------------------------- |
|
|
543
|
+
| `tag("x")` | `"x"`, `literal("x")` | `Token.literal("x")` |
|
|
544
|
+
| `char('x')` | `'x'` | `Token.literal("x")` |
|
|
545
|
+
| `alt((a, b))` | `alt((a, b))` | `Parser.alt(a, b)` |
|
|
546
|
+
| `(a, b)` / `tuple` | `(a, b)` | `Parser.seq(a, b)` |
|
|
547
|
+
| — | `seq!(S { a: .., b: .. })` | `Parser.struct({ a, b })` |
|
|
548
|
+
| `many0(p)` | `repeat(0.., p)` | `Parser.repeat(p)` |
|
|
549
|
+
| `many1(p)` | `repeat(1.., p)` | `Parser.repeat(p, { min: 1 })` |
|
|
550
|
+
| `count(p, n)` | `repeat(n, p)` | `Parser.repeat(p, n)` |
|
|
551
|
+
| `separated_list0(s, p)` | `separated(0.., p, s)` | `Parser.separated(p, s)` |
|
|
552
|
+
| `fold_many0(p, i, f)` | `repeat(0.., p).fold(i, f)` | `Parser.fold(p, { init, f })` |
|
|
553
|
+
| `many_till(p, e)` | `repeat_till(0.., p, e)` | `Parser.repeatTill(p, e)` |
|
|
554
|
+
| `take_while1(f)` | `take_while(1.., f)` | `Token.takeWhile(f, { min: 1 })` |
|
|
555
|
+
| `map(p, f)` | `p.map(f)` | `Parser.map(p, f)` / `p.pipe(Parser.map(f))` |
|
|
556
|
+
| `map_res(p, f)` | `p.try_map(f)` | `Parser.tryMap(p, f)` (`f` returns a `Result`) |
|
|
557
|
+
| `map_opt(p, f)` | `p.verify_map(f)` | `Parser.filterMap(p, f)` (`f` returns an `Option`) |
|
|
558
|
+
| `verify(p, f)` | `p.verify(f)` | `Parser.filter(p, f)` |
|
|
559
|
+
| `value(v, p)` | `p.value(v)` | `Parser.as(p, v)` |
|
|
560
|
+
| `recognize(p)` | `p.take()` | `Parser.taken(p)` |
|
|
561
|
+
| `consumed(p)` | `p.with_taken()` | `Parser.withTaken(p)` |
|
|
562
|
+
| `map_parser(p, q)` | `p.and_then(q)` | `Parser.mapParser(p, q)` |
|
|
563
|
+
| `flat_map(p, f)` | `p.flat_map(f)` | `Parser.flatMap(p, f)` |
|
|
564
|
+
| `cut(p)` | `cut_err(p)` | `Parser.cut(p)` |
|
|
565
|
+
| `context("x", p)` | `p.context(StrContext::Label("x"))` | `Parser.context(p, "x")` |
|
|
566
|
+
| `complete(p)` | `p.complete_err()` | `Parser.complete(p)` |
|
|
567
|
+
| `IResult` | `parse_peek` | `Parser.parsePeek` |
|
|
568
|
+
| `streaming::*` | `Partial<I>` | `Parser.parsePartial`, `ParserStream` |
|
|
569
|
+
| — | `dispatch!` | `Parser.dispatch(selector, cases)` |
|
|
570
|
+
| — | `Stateful<I, S>` | `Parser.service` + Effect `Context` |
|
|
571
|
+
| — | `LocatingSlice` | `Parser.span`, `Parser.withSpan`, `Parser.location` |
|
|
572
|
+
| — | `resume_after` | `Parser.resumeAfter` + `Parser.parseRecoverable` |
|
|
573
|
+
|
|
574
|
+
The `Nom` module provides the nom names directly, with nom's argument orders, for mechanical ports.
|
|
510
575
|
|
|
511
576
|
## Examples
|
|
512
577
|
|
|
513
|
-
[`examples/`](examples) contains ports of winnow's examples, each with tests
|
|
514
|
-
in [`test/examples.test.ts`](test/examples.test.ts):
|
|
578
|
+
[`examples/`](examples) contains ports of winnow's examples, each with tests in [`test/examples.test.ts`](test/examples.test.ts):
|
|
515
579
|
|
|
516
580
|
- [`json.ts`](examples/json.ts): JSON with `dispatch`, `cut` and escapes
|
|
517
|
-
- [`json-text.ts`](examples/json-text.ts): the same grammar written once
|
|
518
|
-
|
|
519
|
-
- [`
|
|
520
|
-
compiled parsers, with `Parser.fromEntries`
|
|
521
|
-
- [`arithmetic.ts`](examples/arithmetic.ts): direct evaluation, an AST via
|
|
522
|
-
the Pratt parser, and a lexer plus a parser over the token array
|
|
581
|
+
- [`json-text.ts`](examples/json-text.ts): the same grammar written once against `Text.TextParsers`, for strings, UTF-8 and UTF-16
|
|
582
|
+
- [`json-fast.ts`](examples/json-fast.ts): the same grammar written for compiled parsers, with `Parser.fromEntries`
|
|
583
|
+
- [`arithmetic.ts`](examples/arithmetic.ts): direct evaluation, an AST via the Pratt parser, and a lexer plus a parser over the token array
|
|
523
584
|
- [`ini.ts`](examples/ini.ts): INI files with a generator-style parser
|
|
524
|
-
- [`http.ts`](examples/http.ts): HTTP requests over bytes, complete and
|
|
525
|
-
|
|
526
|
-
- [`
|
|
527
|
-
errors
|
|
585
|
+
- [`http.ts`](examples/http.ts): HTTP requests over bytes, complete and streamed
|
|
586
|
+
- [`sexpr.ts`](examples/sexpr.ts): s-expressions with typed evaluation errors
|
|
587
|
+
- [`json-syntax.ts`](examples/json-syntax.ts): JSON as a syntax tree for editors, with fields, `ERROR` nodes for malformed elements, and incremental reparsing (tested in [`test/Syntax.test.ts`](test/Syntax.test.ts))
|
|
528
588
|
|
|
529
|
-
`
|
|
530
|
-
with `JSON.parse`.
|
|
589
|
+
`vp run bench` (from a clone) compares the JSON examples, on each encoding, with `JSON.parse`.
|
|
531
590
|
|
|
532
591
|
## Design
|
|
533
592
|
|
|
534
|
-
[`docs/DESIGN.md`](docs/DESIGN.md) records the research into nom, winnow and
|
|
535
|
-
Effect, and the reasoning behind each decision in this port.
|
|
593
|
+
[`docs/DESIGN.md`](docs/DESIGN.md) records the research into nom, winnow and Effect, and the reasoning behind each decision in this port.
|
|
536
594
|
|
|
537
595
|
## Development
|
|
538
596
|
|
|
539
597
|
```sh
|
|
540
598
|
pnpm install
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
599
|
+
vp check # format + lint
|
|
600
|
+
vp run check # type-check sources, tests, examples and benchmarks
|
|
601
|
+
vp run test:unit # run the test suite
|
|
602
|
+
vp run build # emit dist/
|
|
603
|
+
vp run check:dist # check dist/ as a consumer imports it
|
|
604
|
+
vp run bench # JSON example: interpreted vs compiled, per encoding
|
|
547
605
|
```
|
|
548
606
|
|
|
607
|
+
The other benchmarks (`bench:native`, `bench:stream`, `bench:gen`, `bench:syntax`) and single checks are listed in [CONTRIBUTING.md](CONTRIBUTING.md). Repository conventions are in [AGENTS.md](AGENTS.md), the release pipeline in [PUBLISHING.md](PUBLISHING.md) and the vulnerability-reporting policy in [SECURITY.md](SECURITY.md).
|
|
608
|
+
|
|
549
609
|
## License
|
|
550
610
|
|
|
551
611
|
MIT
|