@thomasfosterau/effect-parse 0.0.0 → 0.2.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.
Files changed (155) hide show
  1. package/README.md +247 -230
  2. package/dist/Ascii.d.ts +32 -53
  3. package/dist/Ascii.d.ts.map +1 -1
  4. package/dist/Ascii.js +576 -571
  5. package/dist/Ascii.js.map +1 -1
  6. package/dist/Binary.d.ts +6 -29
  7. package/dist/Binary.d.ts.map +1 -1
  8. package/dist/Binary.js +129 -116
  9. package/dist/Binary.js.map +1 -1
  10. package/dist/Bits.d.ts +4 -2
  11. package/dist/Bits.d.ts.map +1 -1
  12. package/dist/Bits.js +121 -123
  13. package/dist/Bits.js.map +1 -1
  14. package/dist/Compiler.d.ts +39 -0
  15. package/dist/Compiler.d.ts.map +1 -0
  16. package/dist/Compiler.js +48 -0
  17. package/dist/Compiler.js.map +1 -0
  18. package/dist/Encoding.d.ts +29 -46
  19. package/dist/Encoding.d.ts.map +1 -1
  20. package/dist/Encoding.js +76 -75
  21. package/dist/Encoding.js.map +1 -1
  22. package/dist/Expression.d.ts +15 -49
  23. package/dist/Expression.d.ts.map +1 -1
  24. package/dist/Expression.js +143 -131
  25. package/dist/Expression.js.map +1 -1
  26. package/dist/Input.d.ts +206 -177
  27. package/dist/Input.d.ts.map +1 -1
  28. package/dist/Input.js +317 -299
  29. package/dist/Input.js.map +1 -1
  30. package/dist/Nom.d.ts +44 -84
  31. package/dist/Nom.d.ts.map +1 -1
  32. package/dist/Nom.js +423 -453
  33. package/dist/Nom.js.map +1 -1
  34. package/dist/ParseError.d.ts +54 -49
  35. package/dist/ParseError.d.ts.map +1 -1
  36. package/dist/ParseError.js +233 -275
  37. package/dist/ParseError.js.map +1 -1
  38. package/dist/Parser.d.ts +345 -313
  39. package/dist/Parser.d.ts.map +1 -1
  40. package/dist/Parser.js +2010 -1802
  41. package/dist/Parser.js.map +1 -1
  42. package/dist/ParserSchema.d.ts +8 -7
  43. package/dist/ParserSchema.d.ts.map +1 -1
  44. package/dist/ParserSchema.js +49 -49
  45. package/dist/ParserSchema.js.map +1 -1
  46. package/dist/ParserStream.d.ts +26 -20
  47. package/dist/ParserStream.d.ts.map +1 -1
  48. package/dist/ParserStream.js +267 -255
  49. package/dist/ParserStream.js.map +1 -1
  50. package/dist/Syntax.d.ts +401 -0
  51. package/dist/Syntax.d.ts.map +1 -0
  52. package/dist/Syntax.js +717 -0
  53. package/dist/Syntax.js.map +1 -0
  54. package/dist/Text.d.ts +98 -134
  55. package/dist/Text.d.ts.map +1 -1
  56. package/dist/Text.js +534 -496
  57. package/dist/Text.js.map +1 -1
  58. package/dist/Token.d.ts +60 -42
  59. package/dist/Token.d.ts.map +1 -1
  60. package/dist/Token.js +331 -265
  61. package/dist/Token.js.map +1 -1
  62. package/dist/index.d.ts +16 -17
  63. package/dist/index.js +16 -17
  64. package/dist/internal/compiler/closure.d.ts +14 -16
  65. package/dist/internal/compiler/closure.d.ts.map +1 -1
  66. package/dist/internal/compiler/closure.js +1458 -1637
  67. package/dist/internal/compiler/closure.js.map +1 -1
  68. package/dist/internal/compiler/entries.d.ts +11 -11
  69. package/dist/internal/compiler/entries.d.ts.map +1 -1
  70. package/dist/internal/compiler/entries.js +36 -26
  71. package/dist/internal/compiler/entries.js.map +1 -1
  72. package/dist/internal/compiler/first.d.ts +19 -19
  73. package/dist/internal/compiler/first.d.ts.map +1 -1
  74. package/dist/internal/compiler/first.js +251 -227
  75. package/dist/internal/compiler/first.js.map +1 -1
  76. package/dist/internal/compiler/jit.d.ts +6 -17
  77. package/dist/internal/compiler/jit.d.ts.map +1 -1
  78. package/dist/internal/compiler/jit.js +860 -988
  79. package/dist/internal/compiler/jit.js.map +1 -1
  80. package/dist/internal/compiler/registry.d.ts +20 -21
  81. package/dist/internal/compiler/registry.d.ts.map +1 -1
  82. package/dist/internal/compiler/registry.js +21 -41
  83. package/dist/internal/compiler/registry.js.map +1 -1
  84. package/dist/internal/compiler/sets.d.ts +9 -13
  85. package/dist/internal/compiler/sets.d.ts.map +1 -1
  86. package/dist/internal/compiler/sets.js +76 -73
  87. package/dist/internal/compiler/sets.js.map +1 -1
  88. package/dist/internal/entries.d.ts +3 -3
  89. package/dist/internal/entries.d.ts.map +1 -1
  90. package/dist/internal/entries.js +38 -30
  91. package/dist/internal/entries.js.map +1 -1
  92. package/dist/internal/node.d.ts +177 -173
  93. package/dist/internal/node.d.ts.map +1 -1
  94. package/dist/internal/node.js +77 -66
  95. package/dist/internal/node.js.map +1 -1
  96. package/dist/internal/numbers.d.ts +7 -8
  97. package/dist/internal/numbers.d.ts.map +1 -1
  98. package/dist/internal/numbers.js +246 -276
  99. package/dist/internal/numbers.js.map +1 -1
  100. package/dist/internal/resume.d.ts +6 -24
  101. package/dist/internal/resume.d.ts.map +1 -1
  102. package/dist/internal/resume.js +30 -29
  103. package/dist/internal/resume.js.map +1 -1
  104. package/dist/internal/run.d.ts +15 -0
  105. package/dist/internal/run.d.ts.map +1 -0
  106. package/dist/internal/run.js +36 -0
  107. package/dist/internal/run.js.map +1 -0
  108. package/dist/internal/stack.d.ts +16 -0
  109. package/dist/internal/stack.d.ts.map +1 -0
  110. package/dist/internal/stack.js +26 -0
  111. package/dist/internal/stack.js.map +1 -0
  112. package/dist/internal/syntax.d.ts +146 -0
  113. package/dist/internal/syntax.d.ts.map +1 -0
  114. package/dist/internal/syntax.js +305 -0
  115. package/dist/internal/syntax.js.map +1 -0
  116. package/dist/internal/text.d.ts +10 -2
  117. package/dist/internal/text.d.ts.map +1 -1
  118. package/dist/internal/text.js +295 -345
  119. package/dist/internal/text.js.map +1 -1
  120. package/dist/internal/tokens.d.ts +25 -30
  121. package/dist/internal/tokens.d.ts.map +1 -1
  122. package/dist/internal/tokens.js +369 -363
  123. package/dist/internal/tokens.js.map +1 -1
  124. package/package.json +42 -28
  125. package/src/Ascii.ts +278 -187
  126. package/src/Binary.ts +150 -44
  127. package/src/Bits.ts +6 -3
  128. package/src/Compiler.ts +98 -0
  129. package/src/Expression.ts +10 -5
  130. package/src/Input.ts +106 -28
  131. package/src/Nom.ts +4 -3
  132. package/src/ParseError.ts +48 -51
  133. package/src/Parser.ts +1163 -795
  134. package/src/ParserStream.ts +104 -80
  135. package/src/Syntax.ts +900 -0
  136. package/src/Text.ts +272 -187
  137. package/src/Token.ts +180 -87
  138. package/src/index.ts +2 -0
  139. package/src/internal/compiler/closure.ts +121 -88
  140. package/src/internal/compiler/entries.ts +21 -8
  141. package/src/internal/compiler/first.ts +17 -15
  142. package/src/internal/compiler/jit.ts +360 -157
  143. package/src/internal/compiler/registry.ts +12 -33
  144. package/src/internal/compiler/sets.ts +17 -11
  145. package/src/internal/entries.ts +4 -2
  146. package/src/internal/node.ts +51 -37
  147. package/src/internal/numbers.ts +22 -20
  148. package/src/internal/resume.ts +8 -4
  149. package/src/internal/run.ts +41 -0
  150. package/src/internal/stack.ts +36 -0
  151. package/src/internal/syntax.ts +421 -0
  152. package/src/internal/text.ts +32 -25
  153. package/src/internal/tokens.ts +96 -51
  154. package/dist/index.d.ts.map +0 -1
  155. 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
 
@@ -63,6 +55,7 @@ managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
63
55
  - [Streaming and partial input](#streaming-and-partial-input)
64
56
  - [Services](#services)
65
57
  - [Unicode text and encodings](#unicode-text-and-encodings)
58
+ - [Syntax trees and incremental parsing](#syntax-trees-and-incremental-parsing)
66
59
  - [Modules](#modules)
67
60
  - [Coming from nom or winnow](#coming-from-nom-or-winnow)
68
61
  - [Examples](#examples)
@@ -74,17 +67,14 @@ managed with Changesets; see [PUBLISHING.md](PUBLISHING.md).
74
67
  interface Parser<A, E = never, R = never, I extends Input = string>
75
68
  ```
76
69
 
77
- | Parameter | Meaning |
78
- |---|---|
79
- | `A` | the value produced |
80
- | `E` | the type of user errors (see [Errors](#errors)); `never` for pure syntax |
81
- | `R` | the services required (see [Services](#services)) |
82
- | `I` | the input: `string` (the default), `Uint8Array`, or `ReadonlyArray<T>` |
70
+ | Parameter | Meaning |
71
+ | --------- | ------------------------------------------------------------------------ |
72
+ | `A` | the value produced |
73
+ | `E` | the type of user errors (see [Errors](#errors)); `never` for pure syntax |
74
+ | `R` | the services required (see [Services](#services)) |
75
+ | `I` | the input: `string` (the default), `Uint8Array`, or `ReadonlyArray<T>` |
83
76
 
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.
77
+ 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
78
 
89
79
  ## Writing parsers
90
80
 
@@ -110,7 +100,7 @@ const list = Parser.separated(Ascii.decInt, Token.literal(",")).pipe(
110
100
  **Generators**, like `Effect.gen`:
111
101
 
112
102
  ```ts
113
- const header = Parser.gen(function*() {
103
+ const header = Parser.gen(function* () {
114
104
  const name = yield* Token.takeWhile(Ascii.isAlphanumeric, { min: 1 })
115
105
  yield* Token.literal(": ")
116
106
  const value = yield* Ascii.tillLineEnding
@@ -119,6 +109,21 @@ const header = Parser.gen(function*() {
119
109
  })
120
110
  ```
121
111
 
112
+ 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:
113
+
114
+ ```ts
115
+ const colon = Token.literal(": ")
116
+ const name = Token.takeWhile(Ascii.isAlphanumeric, { min: 1 })
117
+
118
+ const header = Parser.gen(function* () {
119
+ const n = yield* name
120
+ yield* colon
121
+ const value = yield* Ascii.tillLineEnding
122
+ yield* Ascii.lineEnding
123
+ return { name: n, value }
124
+ })
125
+ ```
126
+
122
127
  **Records, tuples and Do notation**:
123
128
 
124
129
  ```ts
@@ -140,13 +145,17 @@ Recursive grammars use `Parser.suspend`:
140
145
  type Tree = ReadonlyArray<Tree>
141
146
  const tree: Parser.Parser<Tree> = Parser.delimited(
142
147
  Token.literal("["),
143
- Parser.separated(Parser.suspend(() => tree), Token.literal(",")),
148
+ Parser.separated(
149
+ Parser.suspend(() => tree),
150
+ Token.literal(",")
151
+ ),
144
152
  Token.literal("]")
145
153
  )
146
154
  ```
147
155
 
148
- Low-level parsers can be written directly against the cursor with
149
- `Parser.make`, in the same way that any function can be a parser in winnow:
156
+ 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.
157
+
158
+ 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
159
 
151
160
  ```ts
152
161
  const upper = Parser.make<string>((state) => {
@@ -159,10 +168,24 @@ const upper = Parser.make<string>((state) => {
159
168
  })
160
169
  ```
161
170
 
171
+ 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`:
172
+
173
+ ```ts
174
+ const state = new Input.State(buffer)
175
+ const token = (offset: number) => {
176
+ const value = lexeme.step(state.reset(offset, { partial: true }))
177
+ if (value !== Input.Fail) return { value, end: state.pos }
178
+ if (state.mode === "Incomplete") return undefined // wait for more input
179
+ throw ParseError.fromFailure(state.input, state.reportedFailure(), state.mode, state.encoding)
180
+ }
181
+ ```
182
+
183
+ `step` always runs the interpreter; only the runners use [compiled code](#compiled-parsers).
184
+
162
185
  ### Combinator overview
163
186
 
164
187
  | Category | Combinators |
165
- |---|---|
188
+ | --- | --- |
166
189
  | Constructors | `succeed`, `sync`, `empty`, `fail`, `failMessage`, `reject`, `todo`, `suspend`, `make`, `location`, `eof` |
167
190
  | Mapping | `map`, `as`, `asVoid`, `asSome`, `tap`, `tryMap`, `tryMapUnknown`, `filterMap`, `filter`, `filterOrFail`, `mapParser`, `fromEntries` |
168
191
  | Sequencing | `seq`, `struct`, `zip`, `zipWith`, `zipLeft`, `zipRight`, `preceded`, `terminated`, `delimited`, `separatedPair`, `flatMap`, `gen`, `Do`/`bind`/`bindTo`/`let` |
@@ -175,29 +198,35 @@ const upper = Parser.make<string>((state) => {
175
198
  | Recovery | `resumeAfter`, `retryAfter` |
176
199
  | Debugging | `trace`, `named` |
177
200
 
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.
201
+ 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
202
 
183
203
  ## Running parsers
184
204
 
185
205
  | Runner | Result |
186
- |---|---|
206
+ | --- | --- |
187
207
  | `Parser.parse(p, input)` | `Result<A, ParseError<E>>`; the whole input must be consumed |
188
208
  | `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 |
209
+ | `Parser.parsePeek(p, input)` | `Result<[remaining, A], ParseError<E>>`, nom-style; the parser stopped at `input.length - remaining.length` |
210
+ | `Parser.parsePartial(p, input)` | `Result<Done \| Incomplete, ParseError<E>>` for streaming input; `Done` has the `offset` it stopped at |
191
211
  | `Parser.parseEffect(p, input)` | `Effect<A, ParseError<E>, R>`; services come from the Effect context |
192
212
  | `Parser.parseRecoverable(p, input)` | `{ value: Option<A>, errors }` with error recovery enabled |
193
213
  | `ParserStream.parseMany(p)` | `Stream<I> => Stream<A>` (see [Streaming](#streaming-and-partial-input)) |
194
214
  | `ParserSchema.make(p)` | a `Schema` decoding strings with the parser |
215
+ | `Syntax.parse(p, input)` | a `Syntax.Tree`: a syntax tree, the value and the errors (see [Syntax trees](#syntax-trees-and-incremental-parsing)) |
216
+
217
+ 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:
218
+
219
+ ```ts
220
+ const lexeme = Compiler.jit(Parser.preceded(Ascii.multispace0, Ascii.alpha1))
221
+
222
+ const buffer = "let x = 1"
223
+ Parser.parsePartial(lexeme, buffer, { offset: 3 })
224
+ // Result.succeed({ _tag: "Done", value: "x", offset: 5 })
225
+ Parser.parsePeek(lexeme, buffer, { offset: 0 })
226
+ // Result.succeed([" x = 1", "let"]): it stopped at 9 - 6 = 3
227
+ ```
195
228
 
196
- Parsers over bytes (`Uint8Array`) also accept an `ArrayBuffer`, a
197
- `SharedArrayBuffer`, a `DataView` or any other typed array, in the runners
198
- and as `ParserStream` chunks. The data is viewed as a `Uint8Array` of the
199
- same memory, not copied, so offsets are byte offsets and slices are
200
- `Uint8Array`s:
229
+ 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
230
 
202
231
  ```ts
203
232
  const response = await fetch(url)
@@ -205,71 +234,64 @@ Parser.parse(Text.utf8.rest, await response.arrayBuffer())
205
234
  Parser.parse(Binary.beU32, new DataView(buffer, 4))
206
235
  ```
207
236
 
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.
237
+ 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
238
 
213
239
  ## Compiled parsers
214
240
 
215
- Combinators are flexible but run as a tree of closures. `Parser.compile`
216
- turns a grammar into fused, specialized code, without changing how it is
217
- written or what it returns:
241
+ 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
242
 
219
243
  ```ts
220
- export const json = Parser.compile(Parser.preceded(ws, value))
244
+ import { Compiler } from "@thomasfosterau/effect-parse"
245
+
246
+ export const json = Compiler.jit(Parser.preceded(ws, value))
221
247
 
222
248
  Parser.parse(json, text) // same Result as before, about 5 times faster
223
249
  ```
224
250
 
225
- On the JSON example, compiled parsing runs at 5–6 times the speed of the
226
- interpreter, for strings, UTF-8 and UTF-16 alike, and at 1–4 times the
227
- time of `JSON.parse` depending on the document (see
228
- [the design notes](docs/DESIGN.md#results)).
229
-
230
- `compile` returns the same parser. The code is built on the first run, for
231
- each kind of input, and cached. `parse`, `parseSync`, `parsePeek` and
232
- `parseEffect` use it; partial input, `parseRecoverable` and tracing stay
233
- interpreted. With `{ backend: "auto" }` (the default) it generates
234
- JavaScript where `new Function` is allowed; `{ backend: "closure" }` never
235
- generates code, for pages with a Content Security Policy.
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.
251
+ 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)).
252
+
253
+ There are two compilers, one function each:
254
+
255
+ - `Compiler.jit` generates JavaScript where `new Function` is allowed, and falls back to the closure compiler where it isn't.
256
+ - `Compiler.closure` never generates code. It is meant for pages with a Content Security Policy.
257
+
258
+ 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.
259
+
260
+ 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.
261
+
262
+ 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
263
 
244
264
  Some ways of writing a grammar compile to faster code than others:
245
265
 
246
- - Build objects with `Parser.fromEntries` (or `Parser.map(Object.fromEntries)`)
247
- directly on a `separated` or `repeat` of pairs: compiled code assigns the
248
- properties as it parses, without the pairs and their array. A lambda that
249
- calls `Object.fromEntries` can't be recognized.
250
- - Prefer token sets (`"abc"`, `Token.range`, the `Ascii` predicates) to
251
- arbitrary predicates: they compile to comparisons.
266
+ - 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.
267
+ - 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:
268
+
269
+ ```ts
270
+ const isNameStart = Token.codePoints("name start", (c) => c === 0x5f || (c >= 0x61 && c <= 0x7a) || c >= 0xc0, [
271
+ [0x5f, 0x5f],
272
+ [0x61, 0x7a],
273
+ [0xc0, 0x10ffff]
274
+ ])
275
+ ```
276
+
252
277
  - Avoid `flatMap` on hot paths: the parser it returns runs interpreted.
278
+ - 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.
279
+ - 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.
280
+ - 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.
281
+ - 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
282
  - `Ascii.float` and `Text.float` convert numbers as they match them.
254
283
 
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).
284
+ [`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
285
 
259
286
  ## Errors
260
287
 
261
288
  Failures have a mode, as in winnow:
262
289
 
263
- - **Backtrack**: recoverable. `alt`, `opt`, `repeat` and friends try
264
- something else.
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.
290
+ - **Backtrack**: recoverable. `alt`, `opt`, `repeat` and friends try something else.
291
+ - **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
292
  - **Incomplete**: partial input ran out (see below).
269
293
 
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:
294
+ `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
295
 
274
296
  ```text
275
297
  Parse error at line 1, column 4:
@@ -281,33 +303,28 @@ expected value, found ","
281
303
 
282
304
  effect-parse tries hard to produce good messages without annotations:
283
305
 
284
- - `alt` merges the expectations of failed branches:
285
- `expected "true", "false" or "null"`.
286
- - Optional and repeated parsers leave *hints*, so after
287
- `repeat(digit)` a failure reads `expected digit or ")"`.
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".
306
+ - `alt` merges the expectations of failed branches: `expected "true", "false" or "null"`.
307
+ - Optional and repeated parsers leave _hints_, so after `repeat(digit)` a failure reads `expected digit or ")"`.
308
+ - The furthest failure wins (the PEG heuristic), so a value that fails after a separator is reported where it failed.
309
+ - `Parser.expected("value")` names a rule; `Parser.context("array")` adds "while parsing array".
292
310
 
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`:
311
+ 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
312
 
297
313
  ```ts
298
- class OutOfRange extends Data.TaggedError("OutOfRange")<{ value: number }> {}
314
+ class OutOfRange extends Schema.TaggedError<OutOfRange>()("OutOfRange", { value: Schema.Number }) {}
299
315
 
300
316
  const port = Ascii.decUint.pipe(
301
- Parser.filterOrFail((n) => n < 65536, (value) => new OutOfRange({ value }))
317
+ Parser.filterOrFail(
318
+ (n) => n < 65536,
319
+ (value) => new OutOfRange({ value })
320
+ )
302
321
  )
303
322
  // Parser<number, string | OutOfRange>
304
323
  ```
305
324
 
306
325
  ## Streaming and partial input
307
326
 
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:
327
+ 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
328
 
312
329
  ```ts
313
330
  Parser.parsePartial(Ascii.digit1, "123")
@@ -316,9 +333,7 @@ Parser.parsePartial(Ascii.digit1, "123;")
316
333
  // Result.succeed({ _tag: "Done", value: "123", offset: 3 })
317
334
  ```
318
335
 
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:
336
+ `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
337
 
323
338
  ```ts
324
339
  import { Effect, Stream } from "effect"
@@ -332,33 +347,22 @@ Stream.make("1\n2", "3\n", "4\n").pipe(ParserStream.parseMany(line), Stream.runC
332
347
 
333
348
  ### Parsing a download as it arrives
334
349
 
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:
350
+ `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
351
 
343
352
  ```ts
344
353
  import { Effect } from "effect"
345
- import { Parser, ParserStream, Text } from "@thomasfosterau/effect-parse"
354
+ import { Compiler, ParserStream, Text } from "@thomasfosterau/effect-parse"
346
355
 
347
- const json = Parser.compile(makeJson(Text.utf8)) // see examples/json-text.ts
356
+ const json = Compiler.jit(jsonGrammar(Text.utf8)) // see examples/json-text.ts
348
357
 
349
358
  const value = await Effect.runPromise(ParserStream.parseResponse(await fetch(url), json))
350
359
  ```
351
360
 
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`).
361
+ 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
362
 
358
363
  ## Services
359
364
 
360
- Parsers can require Effect services. These replace winnow's `Stateful`
361
- input: configuration, symbol tables, indentation stacks and so on.
365
+ Parsers can require Effect services. These replace winnow's `Stateful` input: configuration, symbol tables, indentation stacks and so on.
362
366
 
363
367
  ```ts
364
368
  import { Context, Effect } from "effect"
@@ -370,30 +374,22 @@ const number = Parser.service(Radix).pipe(
370
374
  )
371
375
  // Parser<number, never, Radix>
372
376
 
373
- Parser.parse(Parser.provideService(number, Radix, 16), "ff") // 255
377
+ Parser.parse(Parser.provideService(number, Radix, 16), "ff") // 255
374
378
  Parser.parseEffect(number, "777").pipe(Effect.provideService(Radix, 8)) // Effect<511>
375
379
  ```
376
380
 
377
381
  ## Unicode text and encodings
378
382
 
379
- `Token` reads strings by code point, but the `Ascii` character classes and
380
- numbers only cover ASCII, and they treat bytes as ASCII. The `Text` module
381
- adds Unicode parsers that work with any encoding. `Text.make(encoding)`
382
- builds a set of parsers (`Text.TextParsers<I>`), and sets are provided for
383
- the common encodings:
384
-
385
- | Parsers | Input | Offsets |
386
- |---|---|---|
387
- | `Text.string` | `string` | UTF-16 code units |
388
- | `Text.utf8` | `Uint8Array` | bytes |
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:
383
+ `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:
384
+
385
+ | Parsers | Input | Offsets |
386
+ | ------------------------------ | ------------ | ----------------- |
387
+ | `Text.string` | `string` | UTF-16 code units |
388
+ | `Text.utf8` | `Uint8Array` | bytes |
389
+ | `Text.utf16le`, `Text.utf16be` | `Uint8Array` | bytes |
390
+ | `Text.latin1` | `Uint8Array` | bytes |
391
+
392
+ 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
393
 
398
394
  ```ts
399
395
  import { Encoding, Parser, Text } from "@thomasfosterau/effect-parse"
@@ -401,24 +397,14 @@ import { Encoding, Parser, Text } from "@thomasfosterau/effect-parse"
401
397
  const assignment = <I extends string | Uint8Array>(T: Text.TextParsers<I>) =>
402
398
  Parser.separatedPair(T.identifier, Parser.delimited(T.space0, T.literal("≔"), T.space0), T.decInt)
403
399
 
404
- Parser.parse(assignment(Text.string), "größe ≔ 42") // ["größe", 42]
400
+ Parser.parse(assignment(Text.string), "größe ≔ 42") // ["größe", 42]
405
401
  Parser.parse(assignment(Text.utf8), new TextEncoder().encode("größe ≔ 42")) // ["größe", 42]
406
402
  Parser.parse(assignment(Text.utf16le), Encoding.utf16le.encode("größe ≔ 42"))
407
403
  ```
408
404
 
409
- - **Unicode classes.** `alpha1` matches `"Ωμέγα"`, `digit1` matches
410
- `"١٢٣"`, `space1` matches a no-break space, and `identifier` follows UAX
411
- #31. The predicates (`Text.isLetter`, `Text.isWhitespace`,
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:
405
+ - **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.
406
+ - **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.
407
+ - **Decoding is strict.** Invalid UTF-8 and unpaired surrogates never match. Errors on encoded bytes show the input as text:
422
408
 
423
409
  ```text
424
410
  Parse error at line 1, column 3 (byte 2):
@@ -427,125 +413,156 @@ Parser.parse(assignment(Text.utf16le), Encoding.utf16le.encode("größe ≔ 42")
427
413
  expected letter or end of input, found invalid UTF-8 0xff
428
414
  ```
429
415
 
430
- - **Byte order marks.** `Encoding.sniff(bytes)` picks an encoding from a BOM,
431
- and `T.bom` skips one.
432
- - **Binary data.** The byte parsers accept `ArrayBuffer`s, `DataView`s and
433
- typed arrays too (see [Running parsers](#running-parsers)).
434
- - **Custom encodings.** `Text.make` accepts any `Encoding.Encoding`: a
435
- `decode` function plus a few properties.
436
-
437
- JavaScript strings are parsed natively, and the `Text` parsers only decode
438
- byte input. Converting strings to bytes up front would be slower: a
439
- `TextDecoder` call to produce each slice costs several times more than
440
- `String.prototype.slice`. `pnpm bench` compares the paths with a JSON grammar
441
- written once for every encoding ([`json-text.ts`](examples/json-text.ts)).
442
- In our runs `Text.string` was roughly 5–15% slower than the `Token`/`Ascii`
443
- parsers, and parsing UTF-8 directly was within about 10% of decoding it
444
- first.
416
+ - **Byte order marks.** `Encoding.sniff(bytes)` picks an encoding from a BOM, and `T.bom` skips one.
417
+ - **Binary data.** The byte parsers accept `ArrayBuffer`s, `DataView`s and typed arrays too (see [Running parsers](#running-parsers)).
418
+ - **Custom encodings.** `Text.make` accepts any `Encoding.Encoding`: a `decode` function plus a few properties.
419
+
420
+ 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.
421
+
422
+ ## Syntax trees and incremental parsing
423
+
424
+ 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.
425
+
426
+ 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:
427
+
428
+ ```ts
429
+ import { Ascii, Parser, Syntax, Token } from "@thomasfosterau/effect-parse"
430
+
431
+ const name = Syntax.node(Ascii.alpha1, "name")
432
+ const number = Syntax.node(Ascii.decInt, "number")
433
+ const pair = Syntax.node(
434
+ Parser.separatedPair(Syntax.field(name, "key"), Syntax.token(Token.literal("=")), Syntax.field(number, "value")),
435
+ "pair"
436
+ )
437
+ const config = Parser.separated(pair, Token.literal("\n"))
438
+
439
+ const tree = Syntax.parse(config, "width=80\nheight=24")
440
+ String(tree.root)
441
+ // (document (pair key: (name) value: (number)) (pair key: (name) value: (number)))
442
+
443
+ const node = tree.descendantAt(16) // the deepest node at offset 16
444
+ node.kind // "number"
445
+ node.text // "24"
446
+ node.value // 24, the parser's output
447
+ node.parent?.childByField("key")?.text // "height"
448
+ tree.lineColumn(node.start) // { line: 2, column: 8 }
449
+ tree.value // Option.some([["width", 80], ["height", 24]])
450
+ ```
451
+
452
+ 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.
453
+
454
+ **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.
455
+
456
+ **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:
457
+
458
+ ```ts
459
+ const edited = Syntax.replace(tree, 6, 8, "100") // "width=100\nheight=24"
460
+ edited.value // Option.some([["width", 100], ["height", 24]])
461
+ edited.reused // 2: "width", and the second pair as a whole
462
+ ```
463
+
464
+ 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.
465
+
466
+ 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
467
 
446
468
  ## Modules
447
469
 
448
470
  | Module | Contents | winnow / nom equivalent |
449
- |---|---|---|
471
+ | --- | --- | --- |
450
472
  | `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` |
473
+ | `Token` | `any`, `literal`, `oneOf`, `noneOf`, `take`, `takeWhile`, `takeTill`, `takeUntil`, `rest`, `range`, `codePoints` | `token` / `bytes` |
452
474
  | `Text` | Unicode parsers for any encoding: `string`, `utf8`, `utf16le`, `utf16be`, `latin1`, `make`; Unicode predicates | — |
453
475
  | `Encoding` | text encodings for `Text`, `sniff` | — |
454
476
  | `Ascii` | character classes, `lineEnding`, `decInt`, `decUint`, `hexUint`, `float`, `caseless`, `takeEscaped`, `escaped`, `Ascii.bytes.*` | `ascii` / `character`, `number` |
455
477
  | `Binary` | `u8`, `beU16` … `leI128`, floats, `lengthTake`, `lengthRepeat`, `lengthAndThen` | `binary` / `number`, `multi::length_*` |
456
478
  | `Bits` | `bits`, `bytes`, `take`, `takeBig`, `pattern`, `bool` | `binary::bits` / `bits` |
457
479
  | `Expression` | Pratt parser with prefix, postfix and infix operators | `combinator::expression` / `nom-language::precedence` |
480
+ | `Compiler` | `jit`, `closure`: [compiled parsers](#compiled-parsers) | — |
458
481
  | `ParseError` | the error type, `lineColumn`, `formatExpected` | `error` |
459
482
  | `ParserStream` | `parseMany`, `parseAll` over Effect `Stream`s | `Partial` / `streaming` |
460
483
  | `ParserSchema` | `make`: a `Schema` from a parser | — |
484
+ | `Syntax` | syntax trees: `node`, `token`, `field`, `parse`, incremental `reparse` and `replace`, `Tree`, `SyntaxNode`, `TreeCursor` | — (Tree-sitter) |
461
485
  | `Nom` | nom names and argument orders (`tag`, `many0`, `separatedList1`, `mapRes`, …) | nom |
462
486
  | `Input` | input types and the `State` cursor for `Parser.make` | `stream` |
463
487
 
464
- Token parsers that cannot infer their input type take a type argument that
465
- defaults to `string`, like a Rust turbofish:
488
+ Token parsers that cannot infer their input type take a type argument that defaults to `string`, like a Rust turbofish:
466
489
 
467
490
  ```ts
468
- Token.take(4) // Parser<string>
469
- Token.take<Uint8Array>(4) // Parser<Uint8Array, never, never, Uint8Array>
491
+ Token.take(4) // Parser<string>
492
+ Token.take<Uint8Array>(4) // Parser<Uint8Array, never, never, Uint8Array>
470
493
  Token.oneOf<ReadonlyArray<Tok>>((t) => t.kind === "Number")
471
494
  ```
472
495
 
473
496
  ## Coming from nom or winnow
474
497
 
475
- | nom | winnow | effect-parse |
476
- |---|---|---|
477
- | `tag("x")` | `"x"`, `literal("x")` | `Token.literal("x")` |
478
- | `char('x')` | `'x'` | `Token.literal("x")` |
479
- | `alt((a, b))` | `alt((a, b))` | `Parser.alt(a, b)` |
480
- | `(a, b)` / `tuple` | `(a, b)` | `Parser.seq(a, b)` |
481
- | — | `seq!(S { a: .., b: .. })` | `Parser.struct({ a, b })` |
482
- | `many0(p)` | `repeat(0.., p)` | `Parser.repeat(p)` |
483
- | `many1(p)` | `repeat(1.., p)` | `Parser.repeat(p, { min: 1 })` |
484
- | `count(p, n)` | `repeat(n, p)` | `Parser.repeat(p, n)` |
485
- | `separated_list0(s, p)` | `separated(0.., p, s)` | `Parser.separated(p, s)` |
486
- | `fold_many0(p, i, f)` | `repeat(0.., p).fold(i, f)` | `Parser.fold(p, { init, f })` |
487
- | `many_till(p, e)` | `repeat_till(0.., p, e)` | `Parser.repeatTill(p, e)` |
488
- | `take_while1(f)` | `take_while(1.., f)` | `Token.takeWhile(f, { min: 1 })` |
489
- | `map(p, f)` | `p.map(f)` | `Parser.map(p, f)` / `p.pipe(Parser.map(f))` |
490
- | `map_res(p, f)` | `p.try_map(f)` | `Parser.tryMap(p, f)` (`f` returns a `Result`) |
491
- | `map_opt(p, f)` | `p.verify_map(f)` | `Parser.filterMap(p, f)` (`f` returns an `Option`) |
492
- | `verify(p, f)` | `p.verify(f)` | `Parser.filter(p, f)` |
493
- | `value(v, p)` | `p.value(v)` | `Parser.as(p, v)` |
494
- | `recognize(p)` | `p.take()` | `Parser.taken(p)` |
495
- | `consumed(p)` | `p.with_taken()` | `Parser.withTaken(p)` |
496
- | `map_parser(p, q)` | `p.and_then(q)` | `Parser.mapParser(p, q)` |
497
- | `flat_map(p, f)` | `p.flat_map(f)` | `Parser.flatMap(p, f)` |
498
- | `cut(p)` | `cut_err(p)` | `Parser.cut(p)` |
499
- | `context("x", p)` | `p.context(StrContext::Label("x"))` | `Parser.context(p, "x")` |
500
- | `complete(p)` | `p.complete_err()` | `Parser.complete(p)` |
501
- | `IResult` | `parse_peek` | `Parser.parsePeek` |
502
- | `streaming::*` | `Partial<I>` | `Parser.parsePartial`, `ParserStream` |
503
- | — | `dispatch!` | `Parser.dispatch(selector, cases)` |
504
- | — | `Stateful<I, S>` | `Parser.service` + Effect `Context` |
505
- | — | `LocatingSlice` | `Parser.span`, `Parser.withSpan`, `Parser.location` |
506
- | — | `resume_after` | `Parser.resumeAfter` + `Parser.parseRecoverable` |
507
-
508
- The `Nom` module provides the nom names directly, with nom's argument
509
- orders, for mechanical ports.
498
+ | nom | winnow | effect-parse |
499
+ | ----------------------- | ----------------------------------- | --------------------------------------------------- |
500
+ | `tag("x")` | `"x"`, `literal("x")` | `Token.literal("x")` |
501
+ | `char('x')` | `'x'` | `Token.literal("x")` |
502
+ | `alt((a, b))` | `alt((a, b))` | `Parser.alt(a, b)` |
503
+ | `(a, b)` / `tuple` | `(a, b)` | `Parser.seq(a, b)` |
504
+ | — | `seq!(S { a: .., b: .. })` | `Parser.struct({ a, b })` |
505
+ | `many0(p)` | `repeat(0.., p)` | `Parser.repeat(p)` |
506
+ | `many1(p)` | `repeat(1.., p)` | `Parser.repeat(p, { min: 1 })` |
507
+ | `count(p, n)` | `repeat(n, p)` | `Parser.repeat(p, n)` |
508
+ | `separated_list0(s, p)` | `separated(0.., p, s)` | `Parser.separated(p, s)` |
509
+ | `fold_many0(p, i, f)` | `repeat(0.., p).fold(i, f)` | `Parser.fold(p, { init, f })` |
510
+ | `many_till(p, e)` | `repeat_till(0.., p, e)` | `Parser.repeatTill(p, e)` |
511
+ | `take_while1(f)` | `take_while(1.., f)` | `Token.takeWhile(f, { min: 1 })` |
512
+ | `map(p, f)` | `p.map(f)` | `Parser.map(p, f)` / `p.pipe(Parser.map(f))` |
513
+ | `map_res(p, f)` | `p.try_map(f)` | `Parser.tryMap(p, f)` (`f` returns a `Result`) |
514
+ | `map_opt(p, f)` | `p.verify_map(f)` | `Parser.filterMap(p, f)` (`f` returns an `Option`) |
515
+ | `verify(p, f)` | `p.verify(f)` | `Parser.filter(p, f)` |
516
+ | `value(v, p)` | `p.value(v)` | `Parser.as(p, v)` |
517
+ | `recognize(p)` | `p.take()` | `Parser.taken(p)` |
518
+ | `consumed(p)` | `p.with_taken()` | `Parser.withTaken(p)` |
519
+ | `map_parser(p, q)` | `p.and_then(q)` | `Parser.mapParser(p, q)` |
520
+ | `flat_map(p, f)` | `p.flat_map(f)` | `Parser.flatMap(p, f)` |
521
+ | `cut(p)` | `cut_err(p)` | `Parser.cut(p)` |
522
+ | `context("x", p)` | `p.context(StrContext::Label("x"))` | `Parser.context(p, "x")` |
523
+ | `complete(p)` | `p.complete_err()` | `Parser.complete(p)` |
524
+ | `IResult` | `parse_peek` | `Parser.parsePeek` |
525
+ | `streaming::*` | `Partial<I>` | `Parser.parsePartial`, `ParserStream` |
526
+ | — | `dispatch!` | `Parser.dispatch(selector, cases)` |
527
+ | — | `Stateful<I, S>` | `Parser.service` + Effect `Context` |
528
+ | — | `LocatingSlice` | `Parser.span`, `Parser.withSpan`, `Parser.location` |
529
+ | — | `resume_after` | `Parser.resumeAfter` + `Parser.parseRecoverable` |
530
+
531
+ The `Nom` module provides the nom names directly, with nom's argument orders, for mechanical ports.
510
532
 
511
533
  ## Examples
512
534
 
513
- [`examples/`](examples) contains ports of winnow's examples, each with tests
514
- in [`test/examples.test.ts`](test/examples.test.ts):
535
+ [`examples/`](examples) contains ports of winnow's examples, each with tests in [`test/examples.test.ts`](test/examples.test.ts):
515
536
 
516
537
  - [`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
- against `Text.TextParsers`, for strings, UTF-8 and UTF-16
519
- - [`json-fast.ts`](examples/json-fast.ts): the same grammar written for
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
538
+ - [`json-text.ts`](examples/json-text.ts): the same grammar written once against `Text.TextParsers`, for strings, UTF-8 and UTF-16
539
+ - [`json-fast.ts`](examples/json-fast.ts): the same grammar written for compiled parsers, with `Parser.fromEntries`
540
+ - [`arithmetic.ts`](examples/arithmetic.ts): direct evaluation, an AST via the Pratt parser, and a lexer plus a parser over the token array
523
541
  - [`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
- streamed
526
- - [`sexpr.ts`](examples/sexpr.ts): s-expressions with typed evaluation
527
- errors
542
+ - [`http.ts`](examples/http.ts): HTTP requests over bytes, complete and streamed
543
+ - [`sexpr.ts`](examples/sexpr.ts): s-expressions with typed evaluation errors
544
+ - [`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
545
 
529
- `pnpm build && pnpm bench` compares the JSON examples, on each encoding,
530
- with `JSON.parse`.
546
+ `vp run bench` (from a clone) compares the JSON examples, on each encoding, with `JSON.parse`.
531
547
 
532
548
  ## Design
533
549
 
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.
550
+ [`docs/DESIGN.md`](docs/DESIGN.md) records the research into nom, winnow and Effect, and the reasoning behind each decision in this port.
536
551
 
537
552
  ## Development
538
553
 
539
554
  ```sh
540
555
  pnpm install
541
- pnpm check # type-check sources, tests and examples
542
- pnpm test # run the test suite
543
- pnpm build # emit dist/
544
- pnpm bench # JSON example: interpreted vs compiled, per encoding (after a build)
545
- pnpm bench:native # compiled JSON parser vs JSON.parse on several document shapes
546
- pnpm bench:stream # parsing a fetch body as it downloads vs Response.json()
556
+ vp check # format + lint
557
+ vp run check # type-check sources, tests, examples and benchmarks
558
+ vp run test:unit # run the test suite
559
+ vp run build # emit dist/
560
+ vp run check:dist # check dist/ as a consumer imports it
561
+ vp run bench # JSON example: interpreted vs compiled, per encoding
547
562
  ```
548
563
 
564
+ 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).
565
+
549
566
  ## License
550
567
 
551
568
  MIT