@thomasfosterau/effect-parse 0.4.1 → 0.6.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/CHANGELOG.md +202 -0
- package/README.md +56 -14
- package/dist/Ascii.d.ts +11 -4
- package/dist/Ascii.d.ts.map +1 -1
- package/dist/Ascii.js +12 -4
- package/dist/Ascii.js.map +1 -1
- package/dist/Binary.d.ts +5 -2
- package/dist/Binary.d.ts.map +1 -1
- package/dist/Binary.js +83 -37
- package/dist/Binary.js.map +1 -1
- package/dist/Bits.d.ts.map +1 -1
- package/dist/Bits.js +11 -4
- package/dist/Bits.js.map +1 -1
- package/dist/Compiler.d.ts +74 -2
- package/dist/Compiler.d.ts.map +1 -1
- package/dist/Compiler.js +78 -1
- package/dist/Compiler.js.map +1 -1
- package/dist/Expression.d.ts.map +1 -1
- package/dist/Expression.js +86 -74
- package/dist/Expression.js.map +1 -1
- package/dist/Input.d.ts +56 -5
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +70 -1
- package/dist/Input.js.map +1 -1
- package/dist/Nom.d.ts +5 -15
- package/dist/Nom.d.ts.map +1 -1
- package/dist/Nom.js.map +1 -1
- package/dist/ParseError.d.ts +69 -11
- package/dist/ParseError.d.ts.map +1 -1
- package/dist/ParseError.js +129 -28
- package/dist/ParseError.js.map +1 -1
- package/dist/Parser.d.ts +375 -29
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +394 -37
- package/dist/Parser.js.map +1 -1
- package/dist/ParserSchema.d.ts.map +1 -1
- package/dist/ParserSchema.js +2 -3
- package/dist/ParserSchema.js.map +1 -1
- package/dist/ParserStream.d.ts +122 -14
- package/dist/ParserStream.d.ts.map +1 -1
- package/dist/ParserStream.js +293 -62
- package/dist/ParserStream.js.map +1 -1
- package/dist/Syntax.d.ts +23 -5
- package/dist/Syntax.d.ts.map +1 -1
- package/dist/Syntax.js +1 -1
- package/dist/Syntax.js.map +1 -1
- package/dist/Text.d.ts.map +1 -1
- package/dist/Text.js +21 -15
- package/dist/Text.js.map +1 -1
- package/dist/Token.d.ts +65 -11
- package/dist/Token.d.ts.map +1 -1
- package/dist/Token.js +73 -26
- package/dist/Token.js.map +1 -1
- package/dist/internal/compiler/closure.d.ts +1 -1
- package/dist/internal/compiler/closure.d.ts.map +1 -1
- package/dist/internal/compiler/closure.js +263 -113
- package/dist/internal/compiler/closure.js.map +1 -1
- package/dist/internal/compiler/entries.d.ts +3 -1
- package/dist/internal/compiler/entries.d.ts.map +1 -1
- package/dist/internal/compiler/entries.js +3 -2
- package/dist/internal/compiler/entries.js.map +1 -1
- package/dist/internal/compiler/first.d.ts.map +1 -1
- package/dist/internal/compiler/first.js +9 -2
- package/dist/internal/compiler/first.js.map +1 -1
- package/dist/internal/compiler/graph.d.ts +39 -0
- package/dist/internal/compiler/graph.d.ts.map +1 -0
- package/dist/internal/compiler/graph.js +78 -0
- package/dist/internal/compiler/graph.js.map +1 -0
- package/dist/internal/compiler/jit.d.ts +5 -0
- package/dist/internal/compiler/jit.d.ts.map +1 -1
- package/dist/internal/compiler/jit.js +208 -106
- package/dist/internal/compiler/jit.js.map +1 -1
- package/dist/internal/compiler/registry.d.ts +11 -3
- package/dist/internal/compiler/registry.d.ts.map +1 -1
- package/dist/internal/compiler/registry.js.map +1 -1
- package/dist/internal/compiler/search.d.ts +32 -0
- package/dist/internal/compiler/search.d.ts.map +1 -0
- package/dist/internal/compiler/search.js +68 -0
- package/dist/internal/compiler/search.js.map +1 -0
- package/dist/internal/compiler/sets.d.ts.map +1 -1
- package/dist/internal/compiler/sets.js +1 -1
- package/dist/internal/compiler/sets.js.map +1 -1
- package/dist/internal/compiler/wasm.d.ts +8 -0
- package/dist/internal/compiler/wasm.d.ts.map +1 -0
- package/dist/internal/compiler/wasm.js +2840 -0
- package/dist/internal/compiler/wasm.js.map +1 -0
- package/dist/internal/compiler/wasmAsm.d.ts +199 -0
- package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
- package/dist/internal/compiler/wasmAsm.js +427 -0
- package/dist/internal/compiler/wasmAsm.js.map +1 -0
- package/dist/internal/entries.d.ts +3 -1
- package/dist/internal/entries.d.ts.map +1 -1
- package/dist/internal/entries.js.map +1 -1
- package/dist/internal/node.d.ts +77 -3
- package/dist/internal/node.d.ts.map +1 -1
- package/dist/internal/node.js +54 -10
- package/dist/internal/node.js.map +1 -1
- package/dist/internal/numbers.d.ts.map +1 -1
- package/dist/internal/numbers.js.map +1 -1
- package/dist/internal/run.d.ts +14 -0
- package/dist/internal/run.d.ts.map +1 -1
- package/dist/internal/run.js +53 -11
- package/dist/internal/run.js.map +1 -1
- package/dist/internal/stack.d.ts +14 -0
- package/dist/internal/stack.d.ts.map +1 -1
- package/dist/internal/stack.js +24 -2
- package/dist/internal/stack.js.map +1 -1
- package/dist/internal/syntax.d.ts +4 -4
- package/dist/internal/syntax.d.ts.map +1 -1
- package/dist/internal/syntax.js +18 -10
- package/dist/internal/syntax.js.map +1 -1
- package/dist/internal/text.d.ts +32 -0
- package/dist/internal/text.d.ts.map +1 -1
- package/dist/internal/text.js +68 -14
- package/dist/internal/text.js.map +1 -1
- package/dist/internal/tokens.d.ts +20 -0
- package/dist/internal/tokens.d.ts.map +1 -1
- package/dist/internal/tokens.js +72 -39
- package/dist/internal/tokens.js.map +1 -1
- package/dist/internal/tuple.d.ts +6 -0
- package/dist/internal/tuple.d.ts.map +1 -0
- package/dist/internal/tuple.js +170 -0
- package/dist/internal/tuple.js.map +1 -0
- package/package.json +18 -10
- package/src/Ascii.ts +47 -12
- package/src/Binary.ts +174 -73
- package/src/Bits.ts +16 -6
- package/src/Compiler.ts +101 -10
- package/src/Expression.ts +94 -81
- package/src/Input.ts +99 -7
- package/src/Nom.ts +15 -5
- package/src/ParseError.ts +209 -52
- package/src/Parser.ts +684 -143
- package/src/ParserSchema.ts +1 -2
- package/src/ParserStream.ts +361 -68
- package/src/Syntax.ts +60 -36
- package/src/Text.ts +32 -27
- package/src/Token.ts +152 -34
- package/src/internal/compiler/closure.ts +371 -153
- package/src/internal/compiler/entries.ts +4 -2
- package/src/internal/compiler/first.ts +16 -0
- package/src/internal/compiler/graph.ts +96 -0
- package/src/internal/compiler/jit.ts +319 -169
- package/src/internal/compiler/registry.ts +10 -2
- package/src/internal/compiler/search.ts +68 -0
- package/src/internal/compiler/sets.ts +7 -3
- package/src/internal/compiler/wasm.ts +3276 -0
- package/src/internal/compiler/wasmAsm.ts +484 -0
- package/src/internal/entries.ts +8 -2
- package/src/internal/node.ts +103 -7
- package/src/internal/numbers.ts +3 -1
- package/src/internal/run.ts +73 -12
- package/src/internal/stack.ts +25 -1
- package/src/internal/syntax.ts +39 -18
- package/src/internal/text.ts +97 -9
- package/src/internal/tokens.ts +106 -14
- package/src/internal/tuple.ts +141 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @thomasfosterau/effect-parse
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#128](https://github.com/thomasfosterau/effect-parse/pull/128) [`8cf4008`](https://github.com/thomasfosterau/effect-parse/commit/8cf4008eaadd38fc85c6d0b09bff35cc225fdb0e) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Every repetition checks progress, not only an unbounded one ([#115](https://github.com/thomasfosterau/effect-parse/issues/115)). `repeat`, `fold`, `separated`, `repeatTill` (and `Nom.count`, `Nom.manyMN`, `Binary.lengthRepeat`, which are built on them) now fail with an unrecoverable `Assert` failure when an element succeeds without consuming input, whatever the range, in the interpreter and in all three compilers. Before, a bounded repetition of such an element ran to its bound, and in `Binary.lengthRepeat` the bound comes from the input: three bytes made `lengthRepeat(beU24, succeed(0))` build an array of 16.7 M elements, and four bytes made `lengthRepeat(beU32, …)` throw a `RangeError` out of `Parser.parse`. A repetition now never runs more iterations than the input has tokens.
|
|
8
|
+
|
|
9
|
+
**Breaking:** `Parser.repeat(Parser.opt(x), 3)` and other bounded repetitions of a parser that can match empty are an error where the element matches nothing. Make the element consume (`repeat(x, { max: 3 })`), or write the optional parts out (`seq(opt(x), opt(x), opt(x))`).
|
|
10
|
+
|
|
11
|
+
- [#133](https://github.com/thomasfosterau/effect-parse/pull/133) [`6ff1607`](https://github.com/thomasfosterau/effect-parse/commit/6ff1607e10025b41c0dd84dd5194437c287ac67a) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - A `ParseError` no longer serialises the input it was parsing, and its message is bounded ([#117](https://github.com/thomasfosterau/effect-parse/issues/117)).
|
|
12
|
+
|
|
13
|
+
- `error.input` and `error.encoding` are still readable (and `position`, `message` and `ParseError.lineColumn` work as before), but they are now accessors over private fields rather than own properties, and `toJSON` leaves them out. `JSON.stringify(error)`, object spread, `Object.keys`, Node's `inspect`, Effect's `Cause` rendering and loggers that walk an error's own properties therefore no longer write out everything that was being parsed (a failed parse of a 1 MB `Uint8Array` used to serialise as 12.9 MB of JSON). **Breaking** for code that read `input` from a serialised or spread error (read `error.input` from the error itself); `input` is also no longer among the properties that `Equal.equals` and `Hash.hash` walk (they still compare an error's other own properties, its stack trace included). `toJSON` now returns a `ParseError.ParseErrorJson`: `_tag`, `offset`, `position`, `kind`, `mode`, `expected`, `context`, `cause`, `description` and `message`.
|
|
14
|
+
- `message` shows at most 60 characters on each side of the failure, marking what it leaves out with `…`, instead of the whole line: a failure at the end of a 5 MB line with no newline produced a 10 MB message, rebuilt on every access. Lines shorter than the window render exactly as before. Encoded byte input (`Text.utf8`, …) is bounded the same way, and a surrogate pair is never split at the window's edge.
|
|
15
|
+
|
|
16
|
+
- [#131](https://github.com/thomasfosterau/effect-parse/pull/131) [`a2da059`](https://github.com/thomasfosterau/effect-parse/commit/a2da05901f41b8474c71ee21feea0c86966878f0) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The stream runners bound what they buffer, and `ParserStream.parseMany` no longer re-parses an unfinished value on every chunk ([#116](https://github.com/thomasfosterau/effect-parse/issues/116)).
|
|
17
|
+
|
|
18
|
+
- `maxBufferSize` (tokens: UTF-16 code units, bytes or array elements) is a new option of `parseMany`, `parseAll`, `parseReadable` and `parseResponse`, defaulting to 64 MiB (`ParserStream.defaultMaxBufferSize`); `Infinity` turns it off. `parseMany` counts the input it has not consumed yet (one unfinished value); the other three count the whole input. `parseResponse` also fails before reading the body when `Content-Length` is over the limit.
|
|
19
|
+
- Exceeding the limit fails with the new `ParserStream.BufferLimitError` (`limit`, `size`), not a `ParseError`. **Breaking:** it joins the error type of the four runners, so code that treats their error as a `ParseError` has to narrow it first (`Effect.catchTag("BufferLimitError", …)` or `error._tag === "ParseError"`), and a stream over 64 MiB that used to parse now fails unless it passes a larger `maxBufferSize`.
|
|
20
|
+
- `parseReadable` and `parseResponse` take an options argument (`ParserStream.BufferOptions`).
|
|
21
|
+
- `parseMany` was quadratic in the size of a value that had not completed: it copied the buffer and parsed the value again from its start on every chunk (4 MiB with no delimiter in 1 KiB chunks: 40 s). Its byte buffer now grows geometrically, and a value over 64 KiB is retried only once the buffer has grown by an eighth. **Behaviour change:** the chunk that completes a value larger than 64 KiB may not emit it until more input arrives or the stream ends; smaller values are emitted as soon as they complete, as before.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [#108](https://github.com/thomasfosterau/effect-parse/pull/108) [`a09a738`](https://github.com/thomasfosterau/effect-parse/commit/a09a7380412bf8efc9d281d014f92c57ac0812d3) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Compiled `alt`s now dispatch on the first characters of a branch that starts with `not(p)`: the lookahead consumes nothing, so the branch can only start with `p`'s first characters or with those of what follows it, and is skipped on any other input without running the lookahead.
|
|
26
|
+
|
|
27
|
+
- [#142](https://github.com/thomasfosterau/effect-parse/pull/142) [`3dcc9f0`](https://github.com/thomasfosterau/effect-parse/commit/3dcc9f0a154bdbcc64b46a7e5e0b603d9af6bbdf) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Compiler.jit` and `Compiler.wasm` generate code in proportion to the grammar ([#122](https://github.com/thomasfosterau/effect-parse/issues/122)). A parser used twice by the same parent (`Parser.seq(p, p)`) counted as used once, so it was inlined at both places, and a grammar that did so at every level doubled its generated code per level: twenty lines compiled to 160 MB of JavaScript in 22 s, and ran the WebAssembly compiler out of memory. Uses are now counted, so such a parser becomes one rule that both places call. A grammar that is itself enormous (over 100 000 parsers to inline for one root, which no sharing can reduce) is no longer turned into source text at all: the closure compiler takes over, and `Compiler.fallbacks` / the registry record why.
|
|
28
|
+
|
|
29
|
+
- [#109](https://github.com/thomasfosterau/effect-parse/pull/109) [`82f57f0`](https://github.com/thomasfosterau/effect-parse/commit/82f57f09cf23c64b1e669e3a8ff6544b0bca74d5) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Interpreted steps called from compiled code, such as a `Parser.make` guard, no longer allocate a `Failure` each time they backtrack or cut: a compiled run throws its failures away and the interpreter builds the error, so `State.backtrack` and `State.cut` record only the mode there.
|
|
30
|
+
|
|
31
|
+
- [#140](https://github.com/thomasfosterau/effect-parse/pull/140) [`c118e6a`](https://github.com/thomasfosterau/effect-parse/commit/c118e6ac8d83273fa3bac4e5a9c9415fff3bc770) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Input.State.reset` keeps, for string input, what the state has learnt about its input: where the stop characters of compiled `Token.takeTill` searches and the literals of `Token.takeUntilAny` occur ([#126](https://github.com/thomasfosterau/effect-parse/issues/126)). The positions are absolute and a string never changes, so they hold wherever the next run starts. Before, a lexer that reset one state before each token searched for a stop character that was rare or absent (`&` in ordinary XML text) to the end of the buffer again for every token, which made lexing a whole string quadratic (4 MB took about 4 s through effect-xml).
|
|
32
|
+
|
|
33
|
+
For byte and array input `reset` still drops what was remembered, because a caller may refill such a buffer in place between runs (a `Uint8Array` source is viewed, not copied): reuse a `State` over a buffer you overwrite and every run searches afresh, as before.
|
|
34
|
+
|
|
35
|
+
Also fixed: a `takeUntilAny` search that found nothing while a parser had narrowed the input's end was remembered as "not found" after the end widened again, so a literal further on could be missed within the same run. A miss is now remembered together with the end it was searched to.
|
|
36
|
+
|
|
37
|
+
- [#110](https://github.com/thomasfosterau/effect-parse/pull/110) [`2e24b10`](https://github.com/thomasfosterau/effect-parse/commit/2e24b102296d70329c4d09e58c2d36c4bf1952a1) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `seq` builds its tuples with an array literal per arity (up to 8) in the interpreter and the closure compiler, as the JIT already did, so V8 can allocate tuples that a parse keeps straight into the old generation instead of copying them through scavenges.
|
|
38
|
+
|
|
39
|
+
- [#105](https://github.com/thomasfosterau/effect-parse/pull/105) [`62687e5`](https://github.com/thomasfosterau/effect-parse/commit/62687e5640ea3e209a93f4b61a918225ec4e3823) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Include `CHANGELOG.md` in the published package, so the release notes can be read from `node_modules`.
|
|
40
|
+
|
|
41
|
+
- [#127](https://github.com/thomasfosterau/effect-parse/pull/127) [`fdd1cba`](https://github.com/thomasfosterau/effect-parse/commit/fdd1cbadacda7dcc76875586959ef1b4c082939a) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Counts and repetition bounds are validated when a parser is built, and a length read from the input is validated when it is read ([#114](https://github.com/thomasfosterau/effect-parse/issues/114), [#121](https://github.com/thomasfosterau/effect-parse/issues/121)). `Token.take`, `Text.*.take`, `Bits.take` / `takeBig` / `pattern` and every combinator that takes a `Range` (`repeat`, `fold`, `repeatTill`, `separated`, `takeWhile`, `takeTill`, `takeUntil`, `takeUntilAny` and their `Text` and `Nom` forms) now throw a `RangeError` unless the count, `min` and `max` are non-negative integers (`max` may be `Infinity`) with `min <= max`; `escaped` / `takeEscaped` throw unless the control character is a character or its code. Before, a negative or fractional count moved the cursor backwards (`Parser.repeat(Binary.lengthTake(Binary.i8))` on the single byte `0xfe` never terminated and ran the process out of memory), and the JIT wrote a bound into the source it compiles as given, so a bound that was a string at runtime (one read from JSON without a `Number()`) ran as code. `Binary.lengthTake`, `lengthRepeat` and `lengthAndThen` now fail the parse at the length, with kind `Verify`, when it is negative, fractional, `NaN` or above `Number.MAX_SAFE_INTEGER`. The JIT also writes every number through one helper that refuses anything but a non-negative integer.
|
|
42
|
+
|
|
43
|
+
- [#138](https://github.com/thomasfosterau/effect-parse/pull/138) [`90ed4ab`](https://github.com/thomasfosterau/effect-parse/commit/90ed4abefd4ad390abee441f0b9a22e486fc614c) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Compiler.wasm` stays within its memory and agrees with the interpreter at the limits ([#123](https://github.com/thomasfosterau/effect-parse/issues/123)):
|
|
44
|
+
|
|
45
|
+
- A parse whose input and trace need more linear memory than the engine will give, or more than 2 GiB, now runs on the JIT (which parses the input in place) instead of throwing `RangeError: WebAssembly.Memory.grow()` out of `Parser.parse`. This holds when the trace runs out part-way through a parse as well.
|
|
46
|
+
- After a parse that grew a module's memory past 128 MiB, the module gets a new memory, so one large input no longer holds five to ten times its size for the life of the process (a `WebAssembly.Memory` cannot shrink), nor keeps the last string parsed alive.
|
|
47
|
+
- Repetition bounds of 2^32 or more were truncated to their low 32 bits: `repeat(p, { min: 2 ** 32 })` succeeded where the interpreter fails, and `{ max: 2 ** 32 }` matched nothing. A minimum that large can now never be met and a maximum that large is no limit, as in the other backends.
|
|
48
|
+
- Trace offsets are read unsigned.
|
|
49
|
+
|
|
50
|
+
## 0.5.0
|
|
51
|
+
|
|
52
|
+
### Minor Changes
|
|
53
|
+
|
|
54
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.failAt(offset, error)`, a parser that fails with a user error reported at a chosen offset. A check that can only run once a construct has parsed (`tryMap`, `filterOrFail`) can only fail where the construct started; returned from a `flatMap` continuation, `failAt` points the error at the part at fault instead, such as a mismatched end tag. Interpreted and compiled parsers report the same position ([#73](https://github.com/thomasfosterau/effect-parse/issues/73)).
|
|
55
|
+
|
|
56
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.withFastPath(grammar, fast)`, a supported way to put a hand-written scanner in front of a grammar. Compiled code calls `fast(state)` first; it returns the grammar's value with `state.pos` where the grammar would stop, or `Parser.Defer` to run the grammar from the same offset. Interpreted runs use the grammar alone, so every error, `Syntax` tree and trace is exactly the grammar's. `Parser.verifyFastPath(grammar, fast, inputs)` runs both at every offset of every input, complete and partial, and returns the places where they disagree ([#78](https://github.com/thomasfosterau/effect-parse/issues/78)).
|
|
57
|
+
|
|
58
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.mapOrFail(p, f, orFailWith?)`, the allocation-free form of `tryMap` and `filterMap`: `f` returns `Input.Fail` to reject a value, and compiled code compares the result with the sentinel instead of allocating and unwrapping a `Result` or `Option` on every success. `orFailWith` builds the user error (the `ParseError`'s `cause`) only on failure; without it, the failure has no cause, as with `filterMap`. Where V8 does not inline the callback and remove the wrapper (a large grammar that exhausts its inlining budget), a check on every token runs about 1.3× faster than with `tryMap` ([#66](https://github.com/thomasfosterau/effect-parse/issues/66)).
|
|
59
|
+
|
|
60
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.mapSpan(p, (a, start, end) => b)` and `Parser.seqWith(parsers, (a, b, ...) => c)`, the allocation-free spellings of `map(withSpan(p), ...)` and `map(seq(...), ...)`: compiled code passes the offsets, or one value per parser, straight to the callback instead of building a tuple and a span object on every success. V8 often removes those allocations itself when it inlines the callback; in a large grammar that exhausts its inlining budget, a lexer that records spans runs about 10% faster with `mapSpan` ([#64](https://github.com/thomasfosterau/effect-parse/issues/64)).
|
|
61
|
+
|
|
62
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The overloaded exports have named types: `Parser.ParseFn`, `ParseSyncFn`, `ParsePeekFn`, `ParsePartialFn`, `ParseRecoverableFn` and `DispatchFn`, `Input.FromSourceFn`, `Token.LiteralFn` and `TakeUntilFn`, `Ascii.CaselessFn` and `Syntax.ParseFn`, each holding the same call signatures as before, so calls infer the same types. `SyntaxNode.toJSON()` returns the new `Syntax.SyntaxNodeJson` (`{ kind, start, end }`) instead of `unknown` ([#77](https://github.com/thomasfosterau/effect-parse/issues/77)).
|
|
63
|
+
|
|
64
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Compiler.runner(p, options?)`, a prepared runner for drivers that run one compiled grammar many times on states they own, such as a lexer reading a token per run. It returns a function of the state that returns exactly what `Parser.run(p, state, options)` returns, with the compiled form looked up once (and again only when the input kind, or complete versus partial, changes) instead of on every run.
|
|
65
|
+
|
|
66
|
+
The JIT's generated entry now checks the state's end and reports a call-stack overflow as the same `TooDeep` failure itself, so every runner calls it directly instead of through a separate guard and a closure. On the drivers benchmark's lexer (240 K tokens, one per run), `Parser.run` takes 13.7 ms instead of 14.5, and `Compiler.runner` 11.0 ms: about 46 ns per token against 60 before ([#65](https://github.com/thomasfosterau/effect-parse/issues/65)).
|
|
67
|
+
|
|
68
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.combinator(parsers, build, name?)`, for writing combinators whose inner parsers compile. `build` makes the combinator's step from the parsers' steps, which `Parser.Steps<Ps>` types. Interpreted, it receives their own steps; compiled, their compiled steps. A combinator written with `Parser.make` is opaque to the compilers, so it and every parser it runs stay interpreted ([#83](https://github.com/thomasfosterau/effect-parse/issues/83)).
|
|
69
|
+
|
|
70
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.quiet(p)`, which runs a parser without letting it add to the expectations of later errors. A whitespace or comment skipper that matches zero or more times stops by failing, and what it would have accepted (`" "`, `"(:"`, ...) used to be listed in every error raised after it; wrapped in `quiet`, errors expect only what can actually fail there. When the parser succeeds, its hints and furthest failure are forgotten; when it fails recoverably without consuming input, its failure expects nothing. Compiled parsers treat it as an identity ([#74](https://github.com/thomasfosterau/effect-parse/issues/74)).
|
|
71
|
+
|
|
72
|
+
- [#100](https://github.com/thomasfosterau/effect-parse/pull/100) [`0a0f3b0`](https://github.com/thomasfosterau/effect-parse/commit/0a0f3b051319f7e5dc42ebdec14316dca695d2bc) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Token.takeUntilAny(literals)`: takes input up to the first occurrence of any of several literals, the search a multi-pattern prefilter makes. On byte input `Compiler.wasm` now compiles it, and `Token.takeUntil`, natively with WebAssembly SIMD: Teddy (as in ripgrep), which tests the first three bytes of every literal at sixteen positions at once with nibble-table lookups and checks each candidate in full. Elsewhere successive searches over one input remember where each literal next occurs, so each is searched for once per occurrence. Results are unchanged.
|
|
73
|
+
|
|
74
|
+
When each of the first three positions holds at most three distinct bytes across the literals (one literal, or the case variants of a few), the search compares with `i8x16.eq` instead of nibble tables, and a candidate is checked with the widest loads its literals fill.
|
|
75
|
+
|
|
76
|
+
- [#80](https://github.com/thomasfosterau/effect-parse/pull/80) [`60c1810`](https://github.com/thomasfosterau/effect-parse/commit/60c1810c7f45281460559601e107d86c27717df6) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Compiler.wasm`, a third parser compiler. It scans the input in WebAssembly, with code generated from the grammar, and builds the value with a generated JavaScript function that reads what the scan recorded, so results and errors are those of the interpreter, as with the other compilers. On the JSON benchmarks it parses strings, UTF-8 and UTF-16 bytes faster than `Compiler.jit`. It copies input into WebAssembly memory with standard `ArrayBuffer` views and `TextEncoder`, so it runs the same everywhere. Array input, streaming builds, and environments without `WebAssembly` or `new Function` use the JIT.
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Binary` integers of up to 32 bits are read from the bytes directly, without a `DataView`, and `lengthTake` and `lengthRepeat` reuse the parser they build for each length instead of building one per value: length-prefixed records parse 15–40% faster.
|
|
81
|
+
|
|
82
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Binary`'s fixed-size numbers compile: the JIT reads them inline on byte input instead of calling their step, about 3× faster for a sequence of mixed numbers and level with a hand-written `DataView` loop for a run of one kind.
|
|
83
|
+
|
|
84
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Binary` parsers no longer allocate a `DataView` per number: the view is created once per parse state. Parsing a run of 32-bit integers is 3–4× faster, and of floats or a `fold` of 16-bit integers up to 10× when compiled.
|
|
85
|
+
|
|
86
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Bits.take` and the other bit parsers read as many bits as each byte holds at once rather than one bit at a time: 16-bit fields parse about 2.5× faster, 3-bit fields about 15%.
|
|
87
|
+
|
|
88
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The parsers inside `Expression.make`, `Parser.permutation` and `Binary.lengthTake` now compile with the grammar around them instead of running interpreted: compiled expression grammars run about twice as fast.
|
|
89
|
+
|
|
90
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The `Compiler` module's doc comment no longer says that `ParserStream.parseMany` is always interpreted: it runs a compiled parser's resumable build, and parses a value again, interpreted, only when it fails or needs more input ([#76](https://github.com/thomasfosterau/effect-parse/issues/76)).
|
|
91
|
+
|
|
92
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Internal type cleanups from bringing every Effect lint rule to `error`: casts replaced by generics and narrowing, and the remaining type assertions documented. No behaviour changes; the emitted `.d.ts` prints `Nom.lineEnding`'s union in a different order ([#68](https://github.com/thomasfosterau/effect-parse/issues/68)).
|
|
93
|
+
|
|
94
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Interpreted parses merge expected-token lists through a small cache of recent merges instead of allocating a new list each time a hint meets a failure: 2–13% faster on the JSON benchmarks.
|
|
95
|
+
|
|
96
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Parser.parsePartial` returns a compiled parser's `Incomplete` without re-running the parser interpreted: compiled code now records the interpreter's `Incomplete` failure (`needed` included) where it reports it itself, at `eof` and at the end of an `escaped` text. Results are unchanged; a whole document parsed as partial input (ending in whitespace, say) no longer parses twice: 5× faster on a 2.4 MB JSON document.
|
|
97
|
+
|
|
98
|
+
`ParserStream.parseMany` likewise no longer parses a value cut short by its chunk a second time, interpreted; only a value that fails is.
|
|
99
|
+
|
|
100
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The JIT compiles a repetition of a character set whose values are unused (under `taken`, `asVoid`, `peek` or a dropped element) as one scan of the run: `repeat(oneOf(S))` as `takeWhile(S)`, and `repeat(alt(oneOf(S), ...rest))` (with `min` at most 1 and no `max`) with a `takeWhile` run as its first branch. A name written as a repetition of a shared `alt(nameChar, escape)` no longer calls that alternative once per character: about 20% faster on the drivers benchmark's names. The closure compiler compiles `repeat(oneOf(S))` with unused values as `takeWhile(S)` too: 2.2× faster on runs of 30–70 characters. Results are unchanged ([#79](https://github.com/thomasfosterau/effect-parse/issues/79)).
|
|
101
|
+
|
|
102
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Compiled streaming parses (`Parser.parsePartial`, `ParserStream.parseAll` and `parseMany`) build `Parser.fromEntries` objects directly, as ordinary compiled parses do, rather than building the array of pairs first: about 1.5× faster on a JSON document with the JIT.
|
|
103
|
+
|
|
104
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Interpreted `Token.takeWhile` / `takeTill` and the `Ascii` classes no longer allocate a result record per match: the shared token scanner fills one record that its caller reads at once. The interpreted JSON benchmark (`json.ts`, Token/Ascii) runs 4–7% faster.
|
|
105
|
+
|
|
106
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Syntax.parse` allocates less per node: leaves share one empty array of children, and nodes are built from the builder's stack without copying their children off it first (about 3% faster on a JSON document).
|
|
107
|
+
|
|
108
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Compiled `Token.takeTill` of one to three characters, on string input, finds the end of its run with `indexOf` once the run is longer than a few characters, with a per-state cache of where each stop character next occurs. On CSV-shaped input, runs of 64 characters parse about 3× as fast and runs of 256 characters up to 9× (JIT) and 14× (closure compiler); short runs are unchanged or faster. Results are unchanged ([#75](https://github.com/thomasfosterau/effect-parse/issues/75)).
|
|
109
|
+
|
|
110
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Text.utf8` parsers decode long strings that lie in a run of ASCII bytes by slicing them from the run's text, decoded once per parse, instead of making one `TextDecoder` call per string: a JSON document of ASCII strings parses about 27% faster from bytes.
|
|
111
|
+
|
|
112
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Text.utf8` builds short strings from bytes eight ASCII bytes at a time, and by hand up to 40 bytes (was 24) before calling `TextDecoder`: JSON with escaped strings parses about 25% faster from bytes, and records about 5%.
|
|
113
|
+
|
|
114
|
+
- [#85](https://github.com/thomasfosterau/effect-parse/pull/85) [`7287ad5`](https://github.com/thomasfosterau/effect-parse/commit/7287ad556f688dbb71267c422869e77a4149f313) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Compiler.wasm` scans `Binary`'s fixed-size numbers itself instead of calling out to JavaScript for each one: binary formats parse 3–12× faster with it, level with the JIT.
|
|
115
|
+
|
|
116
|
+
- [#100](https://github.com/thomasfosterau/effect-parse/pull/100) [`93d25ad`](https://github.com/thomasfosterau/effect-parse/commit/93d25add25894e877aa0b07cb13ce93b1d98f06a) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Compiler.wasm` vectorises `takeWhile` and `takeTill` on bytes (`Token` and Latin-1 text) with WebAssembly SIMD: sixteen bytes per step, then thirty-two once a run is that long, tested against at most three stop bytes with `i8x16.eq` or against nibble tables with `i8x16.swizzle`; the first byte that does not belong ends the run without a scalar step. A run of 8.5 MB scans about 3.7× as fast (1.6 GB/s to 5.8 GB/s with the copy into WebAssembly memory). Results are unchanged.
|
|
117
|
+
|
|
118
|
+
Where relaxed SIMD validates, the nibble-table lookups (in these runs and in `takeUntil`'s Teddy search) use `i8x16.relaxed_swizzle`, which is exact for their indices and skips `swizzle`'s fix-up for indices from 16 up.
|
|
119
|
+
|
|
120
|
+
- [#100](https://github.com/thomasfosterau/effect-parse/pull/100) [`09da6aa`](https://github.com/thomasfosterau/effect-parse/commit/09da6aa57fde2db66f3d8456ab93145e3992a700) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Compiler.wasm` compares a literal many bytes at a time: sixteen with a SIMD `v128` load, then eight, four, two and one with `i64` and `i32` loads (a caseless letter's byte ORed with 0x20 first), instead of one unit at a time. Results are unchanged.
|
|
121
|
+
|
|
122
|
+
## 0.4.1
|
|
123
|
+
|
|
124
|
+
### Patch Changes
|
|
125
|
+
|
|
126
|
+
- [#69](https://github.com/thomasfosterau/effect-parse/pull/69) [`9d72a0f`](https://github.com/thomasfosterau/effect-parse/commit/9d72a0f63efcf1de4b2e29b8ce81eb1d95359544) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Fixes from type-aware linting. `Parser` now declares the `toString()` it already had (`Parser(<name>)`), and `tryMapUnknown` spells its error type `unknown` rather than the equivalent `E | unknown`. An unexpected token that `JSON.stringify` cannot render (a cycle, a `BigInt`) is now shown as its elements joined (arrays) or its object tag such as `[object Map]`, without calling a user `toString`.
|
|
127
|
+
|
|
128
|
+
## 0.4.0
|
|
129
|
+
|
|
130
|
+
### Minor Changes
|
|
131
|
+
|
|
132
|
+
- [#58](https://github.com/thomasfosterau/effect-parse/pull/58) [`4e8f5fb`](https://github.com/thomasfosterau/effect-parse/commit/4e8f5fb8cc509c263ca7ecfce49e7cab08747d14) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Breaking: a parser step that moves the cursor backwards (to retry an alternative, or after a lookahead) must call the new `state.rewind(offset)` instead of assigning `state.pos`; moving forward is still an assignment. `Syntax` trees are now built by a separate builder rather than a `State` subclass whose `pos` was an accessor, so every parser sees one state shape. In a program that also builds syntax trees, ordinary interpreted parses no longer slow down by about 20%. Built-in encodings also share one object shape.
|
|
133
|
+
|
|
134
|
+
### Patch Changes
|
|
135
|
+
|
|
136
|
+
- [#55](https://github.com/thomasfosterau/effect-parse/pull/55) [`42e115d`](https://github.com/thomasfosterau/effect-parse/commit/42e115d13d0984ac76bb190e751c6359a03fdf01) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The closure compiler writes the `Text` `takeWhile` loop once per kind of unit read (string, bytes, UTF-16 bytes), so it keeps inlining its reads in a program that parses more than one input kind or encoding: about 17-20% faster on the JSON benchmark's `Text` grammars.
|
|
137
|
+
|
|
138
|
+
- [#55](https://github.com/thomasfosterau/effect-parse/pull/55) [`d9c3410`](https://github.com/thomasfosterau/effect-parse/commit/d9c341008772f5c0117fe40fdd528c1cca81ed80) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The closure compiler reads UTF-16 bytes a code unit at a time, as the JIT does, instead of decoding every code point: compiled `Text.utf16le` / `Text.utf16be` parsers run about 1.75× faster on the JSON benchmark when `new Function` is unavailable.
|
|
139
|
+
|
|
140
|
+
- [#57](https://github.com/thomasfosterau/effect-parse/pull/57) [`f1ac841`](https://github.com/thomasfosterau/effect-parse/commit/f1ac84175d7411cfa1fe76cb5f1ada98dc064b0f) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Streaming parses with a JIT-compiled parser run escapes (`escaped`, `takeEscaped`) as generated code that keeps checkpoints, instead of calling the closure compiler's: about 13% faster on a JSON document, within 5% of an ordinary compiled parse.
|
|
141
|
+
|
|
142
|
+
- [#56](https://github.com/thomasfosterau/effect-parse/pull/56) [`459f8fe`](https://github.com/thomasfosterau/effect-parse/commit/459f8fe12dedad0988ce67b372d46904ebe2706e) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `ParserStream.parseAll` (and `parseReadable` / `parseResponse`) parse an input that arrives in a single chunk, such as most small response bodies, once instead of twice: the first partial attempt waits for a second chunk, and a stream that ends first is parsed as an ordinary compiled parse. On a 2 MB JSON document in one chunk this takes 60 ms instead of 147 ms.
|
|
143
|
+
|
|
144
|
+
## 0.3.0
|
|
145
|
+
|
|
146
|
+
### Minor Changes
|
|
147
|
+
|
|
148
|
+
- [#41](https://github.com/thomasfosterau/effect-parse/pull/41) [`2a57d83`](https://github.com/thomasfosterau/effect-parse/commit/2a57d836303e8f5453112c8df89cf4ef59f01608) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add parsers and printers derived from Effect `Schema`s. `ParserSchema.toParser(schema)` derives a parser from a schema, guided by annotations (`ParserSchema.annotate`, under the key `ParserSchema.annotationKey`) that give a node's `prefix`, `suffix`, `separator`, `keySeparator` and `whitespace`, or a `parser` and `print` that replace the derived ones. Structs, tuples, arrays, records, unions, literals, enums, template literals and recursive schemas are derived; the schema's checks and transformations run on the result. `ParserSchema.toPrinter(schema)` derives the printer of the same text. `ParserSchema.fromSchema(schema)` wraps both as a `Schema` from `string`, whose encoding checks that the text reads back as the value.
|
|
149
|
+
|
|
150
|
+
The annotation key is declared on `Schema.Annotations.Annotations` by module augmentation.
|
|
151
|
+
|
|
152
|
+
### Patch Changes
|
|
153
|
+
|
|
154
|
+
- [#43](https://github.com/thomasfosterau/effect-parse/pull/43) [`6e69d9e`](https://github.com/thomasfosterau/effect-parse/commit/6e69d9ebe6c787430858192ff8eac5e96feb2db6) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Interpreted `Parser.alt` no longer merges the failures of branches that fail at the same offset until the merged failure is read. A nested `alt` hands its tied failures to the enclosing one unmerged, so an `alt` whose next branch consumes input never pays for the merge: about 30% faster on a token-heavy interpreted grammar. Error results are unchanged. `Input.State`'s `failure` and `deepest` are now accessors (reads and writes behave as before).
|
|
155
|
+
|
|
156
|
+
- [#46](https://github.com/thomasfosterau/effect-parse/pull/46) [`37bd245`](https://github.com/thomasfosterau/effect-parse/commit/37bd245604ea64a0236b844e933e91881c0be072) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Parser.permutation` and `Expression.make`'s operator loop no longer merge the failures of attempts that fail at the same offset until the merged failure is read, as `Parser.alt` already does. A round of a permutation in which some parser matches, or an operator round in which some operator matches, discards its tied failures without merging them, and a nested `alt`'s ties are taken over unmerged. Error results are unchanged.
|
|
157
|
+
|
|
158
|
+
## 0.2.0
|
|
159
|
+
|
|
160
|
+
### Minor Changes
|
|
161
|
+
|
|
162
|
+
- [#39](https://github.com/thomasfosterau/effect-parse/pull/39) [`bb8efed`](https://github.com/thomasfosterau/effect-parse/commit/bb8efed7a31c4ca413cb43ba2f51dc66f3cc10fd) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Parser.run(parser, state)`, which runs a parser on an `Input.State` you own and returns its output or `Fail`, like `parser.step(state)` but using the parser's compiled form when it has one. A lexer that keeps one state and `reset`s it before each token now runs its token parser compiled without allocating a state, a `Result` or a slice per token: about 1.9 times as fast as calling `step` on a reused state, and 15% faster than `parsePartial` with an `offset`. On failure the parser runs again interpreted from the same position, so the state holds the interpreter's failure; partial states run the build that handles partial input. A driver that only needs to know that a parser failed (it tries another, or skips a character) can pass `{ diagnostics: false }` to skip that re-run, keeping just the failure's `mode`: about 2.3 times as fast on a scan where most runs fail.
|
|
163
|
+
|
|
164
|
+
`ParserStream.parseMany` now runs a compiled parser compiled, with one state per buffer, instead of always interpreting it: about 2.8 times as fast on a stream of CSV-like rows.
|
|
165
|
+
|
|
166
|
+
- [#34](https://github.com/thomasfosterau/effect-parse/pull/34) [`53764ff`](https://github.com/thomasfosterau/effect-parse/commit/53764ffd7e612084b113f44c25408835405bcb39) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - **Breaking:** compile parsers with the new `Compiler` module (`@thomasfosterau/effect-parse/Compiler`) instead of `Parser.compile`. `Compiler.jit(p)` replaces `Parser.compile(p)` and `Parser.compile(p, { backend: "auto" })`, and `Compiler.closure(p)` replaces `Parser.compile(p, { backend: "closure" })`. `Parser.compile` and `Parser.CompileOptions` are removed.
|
|
167
|
+
|
|
168
|
+
The two backends are now separate functions, so bundles contain only the compilers a program uses ([#15](https://github.com/thomasfosterau/effect-parse/issues/15)). `Parser` and its runners no longer import a compiler. A program that never compiles a parser no longer bundles about 45 KB of minified compiler code. A program that uses only `Compiler.closure` contains no `new Function`, which Content Security Policy and supply-chain scanners flag even when it never runs.
|
|
169
|
+
|
|
170
|
+
- [#33](https://github.com/thomasfosterau/effect-parse/pull/33) [`737afc1`](https://github.com/thomasfosterau/effect-parse/commit/737afc15b3ff9e6f244cf107ab298a109a250651) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add a `while` option to `Parser.fold`, a predicate on the accumulator that stops the repetition, successfully and without consuming more input, once it is false. It is checked before each element once `range.min` elements are folded, and compiles to a loop-condition check, so a fold can stop at "N kept items" instead of scanning the rest of the input ([#16](https://github.com/thomasfosterau/effect-parse/issues/16)).
|
|
171
|
+
|
|
172
|
+
- [#27](https://github.com/thomasfosterau/effect-parse/pull/27) [`b6c5a94`](https://github.com/thomasfosterau/effect-parse/commit/b6c5a9469062bbfe56e963d94cf0b357ce615508) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add an `offset` option to every `Parser` runner (`parse`, `parseSync`, `parsePeek`, `parsePartial`, `parseEffect`, `parseRecoverable`), so an incremental driver such as a lexer can parse one token at a time from a buffer without slicing it. The result is that of parsing `input.slice(offset)`, with offsets (`Done`'s `offset`, spans, `ParseError` offsets, lines and columns) counted in the whole input. `parsePartial` now runs compiled parsers compiled, re-running the interpreter only to report `Incomplete` or an error, so a lexer loop over `parsePartial` runs compiled code from any offset ([#19](https://github.com/thomasfosterau/effect-parse/issues/19)).
|
|
173
|
+
|
|
174
|
+
- [#31](https://github.com/thomasfosterau/effect-parse/pull/31) [`3318a75`](https://github.com/thomasfosterau/effect-parse/commit/3318a751673fc3857f5dfd83cc714bf83e3576fe) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Deeply nested input no longer makes the runners throw `RangeError: Maximum call stack size exceeded`. When a recursive grammar (`Parser.suspend`) exhausts the call stack, interpreted or compiled, `parse`, `parsePeek`, `parsePartial`, `parseEffect`, `parseRecoverable`, `ParserStream.parseMany`, `ParserStream.parseAll` and `Syntax.parse` now fail with an unrecoverable `ParseError` of the new kind `"TooDeep"` at the offset reached, so untrusted input can't crash the caller. `Parser.tryMapUnknown` no longer catches the overflow as a user error ([#14](https://github.com/thomasfosterau/effect-parse/issues/14)).
|
|
175
|
+
|
|
176
|
+
- [#24](https://github.com/thomasfosterau/effect-parse/pull/24) [`da7891e`](https://github.com/thomasfosterau/effect-parse/commit/da7891ee50854c53fa737f1c90776bb0259e156a) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Input.State.reset(offset, options?)`, which prepares a state for another run over the same input, so that drivers making many small runs over one buffer (one per token, say) don't allocate a new `State` each time. It resets every per-run field, so callers no longer need to know which ones those are. After a reset the state behaves exactly like `new State(state.input, { ...options, offset })`, including after a run that failed or returned `Incomplete` ([#20](https://github.com/thomasfosterau/effect-parse/issues/20)).
|
|
177
|
+
|
|
178
|
+
- [#29](https://github.com/thomasfosterau/effect-parse/pull/29) [`2527c14`](https://github.com/thomasfosterau/effect-parse/commit/2527c1429a8d0e26b46555ad4979fabbe5ae3d1e) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add the `Syntax` module: Tree-sitter-style concrete syntax trees and incremental parsing. Mark a grammar's rules with `Syntax.node` (plus `Syntax.token` for anonymous nodes and `Syntax.field` for field names), and `Syntax.parse` returns a tree of nodes with spans, values, parents and siblings, to search by offset (`descendantAt`) or walk with a `TreeCursor`. Recovered and fatal failures become `ERROR` nodes. After an edit, `Syntax.reparse` and `Syntax.replace` parse again, reusing every node whose input is unchanged, and return the same tree a fresh parse would. Outside `Syntax.parse` the annotations change nothing, and compiled parsers compile them away.
|
|
179
|
+
|
|
180
|
+
- [#26](https://github.com/thomasfosterau/effect-parse/pull/26) [`9dba9e3`](https://github.com/thomasfosterau/effect-parse/commit/9dba9e3e066fffc94dc2681226da729621df5b90) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Add `Token.codePoints(name, predicate, ranges?)`, which declares a character class as a predicate over code points. In token sets on string, `Text` and byte input it receives each character as a number, so testing a character allocates nothing, and with `ranges` compiled parsers test the ranges instead of calling it. Plain `(c: string) => boolean` predicates still receive whole characters, now built with `String.fromCharCode` below U+10000 instead of `String.fromCodePoint`. The README says which token sets are the fast path. Closes [#22](https://github.com/thomasfosterau/effect-parse/issues/22).
|
|
181
|
+
|
|
182
|
+
### Patch Changes
|
|
183
|
+
|
|
184
|
+
- [#39](https://github.com/thomasfosterau/effect-parse/pull/39) [`bb8efed`](https://github.com/thomasfosterau/effect-parse/commit/bb8efed7a31c4ca413cb43ba2f51dc66f3cc10fd) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The JIT compiles two more shapes of grammar inline. A `Parser.dispatch` with a record of cases and a selector other than `Token.any` or `Parser.peek(Token.any)` now runs the chosen case inline, through a switch on its index, instead of calling its closure-compiled step. On token arrays, `Token.anyToken`, `Token.oneOf`, `Token.noneOf`, `Token.takeWhile` and `Token.takeTill` call their predicate (or membership test) inline instead of running their interpreted steps, which also recorded a failure on every miss: a grammar over 200 K tokens matched with `Token.oneOf(predicate)` runs about 3 times as fast. Results and errors are unchanged. The `Parser.dispatch` docs and the README now say which ways of writing a grammar compile best.
|
|
185
|
+
|
|
186
|
+
- [#36](https://github.com/thomasfosterau/effect-parse/pull/36) [`b54e94e`](https://github.com/thomasfosterau/effect-parse/commit/b54e94e71198be09202ed4e12dea16a5ab780e19) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Parser.separated` has its doc comment again: it sat above an internal declaration, so the emitted `.d.ts` and editor hovers showed none.
|
|
187
|
+
|
|
188
|
+
- [#36](https://github.com/thomasfosterau/effect-parse/pull/36) [`b54e94e`](https://github.com/thomasfosterau/effect-parse/commit/b54e94e71198be09202ed4e12dea16a5ab780e19) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - The package is now built with `vp pack` (tsdown) instead of `tsc`. `dist/` keeps the same layout, entry points and runtime exports, and every top-level call stays marked `/*#__PURE__*/`; the declaration files spell some types out where they used to write `typeof Token.take` and the like, and `Parser.Do` is typed `Parser<Record<never, never>, …>` (the same type as `Parser<{}, …>`). The root entry's `index.js` no longer ships a source map.
|
|
189
|
+
|
|
190
|
+
- [#25](https://github.com/thomasfosterau/effect-parse/pull/25) [`9b3ecc0`](https://github.com/thomasfosterau/effect-parse/commit/9b3ecc0ae7653d6652e866ae1e66b4ac411343ec) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Report a committed (`cut`) failure where it happened. The furthest-failure heuristic no longer replaces it with a deeper failure from a branch or lookahead tried before the commit; failures inside the `cut` parser itself still count. Failures inside a lookahead that succeeded (`peek`, or the parser given to `not`) are no longer reported as the furthest failure either. Ordinary backtracking (`alt`, `opt`, `separated`, ...) reports the same errors as before.
|
|
191
|
+
|
|
192
|
+
- [#28](https://github.com/thomasfosterau/effect-parse/pull/28) [`03313ed`](https://github.com/thomasfosterau/effect-parse/commit/03313ed7ecf11ff0065c29dd43cd27c9bef4ce55) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Make parsers cheaper to build, for those written inside a `Parser.gen` body, which are rebuilt on every run. Compiled token sets given as strings (`Token.oneOf("+-")`, `Token.takeWhile(" \t")`, ...) and the descriptions of string literals are now cached by value, so building `Token.oneOf("+-")` takes about 30 ns instead of 4 µs, and `Token.literal(",")` about 30 ns instead of 90 ns. Results and error messages are unchanged. The `Parser.gen` docs now show how to hoist parsers out of the body on hot paths.
|
|
193
|
+
|
|
194
|
+
- [#17](https://github.com/thomasfosterau/effect-parse/pull/17) [`ff2e2a5`](https://github.com/thomasfosterau/effect-parse/commit/ff2e2a5efcc964e07cff909dca1c48b88c5dbe03) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - Mark every parser, predicate and table built at module load `/*#__PURE__*/`, so bundlers drop the ones a program doesn't use. Importing only `decInt` from `@thomasfosterau/effect-parse/Ascii` now bundles about 7 KB of effect-parse instead of 24 KB.
|
|
195
|
+
|
|
196
|
+
- [#32](https://github.com/thomasfosterau/effect-parse/pull/32) [`bfad13b`](https://github.com/thomasfosterau/effect-parse/commit/bfad13bf7086175bcd508a7fb94d198314c6c565) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - `Token.takeUntil` and `Text.<encoding>.takeUntil` now stop at `State.end`, like the other token parsers, in interpreted and compiled code. A literal only counts as found when it lies wholly within the input up to `end`, so a parser run over a narrowed region no longer matches past it, returns text from outside it, or leaves `pos` past `end` ([#30](https://github.com/thomasfosterau/effect-parse/issues/30)).
|
|
197
|
+
|
|
198
|
+
## 0.1.0
|
|
199
|
+
|
|
200
|
+
### Minor Changes
|
|
201
|
+
|
|
202
|
+
- [#11](https://github.com/thomasfosterau/effect-parse/pull/11) [`8f77dad`](https://github.com/thomasfosterau/effect-parse/commit/8f77dad2cd9ca1afe000b9dfe8b370b94b06460f) Thanks [@thomasfosterau](https://github.com/thomasfosterau)! - First release of `@thomasfosterau/effect-parse` (developed as `effect-nom`): parser combinators for Effect, ported from Rust's nom and winnow. Text, byte, bit and token-stream input; `Backtrack` / `Cut` / `Incomplete` error modes; streaming input through `ParserStream`; compiled parsers through `Parser.compile`; `Schema` integration through `ParserSchema`; and `ParseError` diagnostics with line and column. Requires `effect` 4.x as a peer.
|
package/README.md
CHANGED
|
@@ -169,6 +169,20 @@ const upper = Parser.make<string>((state) => {
|
|
|
169
169
|
})
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
A combinator that runs other parsers should be written with `Parser.combinator` instead, which hands it their steps. A `make` parser is opaque to the [compilers](#compiled-parsers), so in a compiled grammar it runs interpreted, and so does every parser it calls; a `combinator` receives their compiled steps there, so the parsers inside compile with the grammar:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const twice = <A>(p: Parser.Parser<A>) =>
|
|
176
|
+
Parser.combinator([p], ([step]) => (state) => {
|
|
177
|
+
const a = step(state)
|
|
178
|
+
if (a === Input.Fail) return Input.Fail
|
|
179
|
+
const b = step(state)
|
|
180
|
+
return b === Input.Fail ? Input.Fail : ([a, b] as const)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
Parser.parseSync(twice(Token.oneOf("ab")), "ba") // ["b", "a"]
|
|
184
|
+
```
|
|
185
|
+
|
|
172
186
|
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
187
|
|
|
174
188
|
```ts
|
|
@@ -187,14 +201,14 @@ const token = (offset: number) => {
|
|
|
187
201
|
|
|
188
202
|
| Category | Combinators |
|
|
189
203
|
| --- | --- |
|
|
190
|
-
| Constructors | `succeed`, `sync`, `empty`, `fail`, `failMessage`, `reject`, `todo`, `suspend`, `make`, `location`, `eof` |
|
|
191
|
-
| Mapping | `map`, `as`, `asVoid`, `asSome`, `tap`, `tryMap`, `tryMapUnknown`, `filterMap`, `filter`, `filterOrFail`, `mapParser`, `fromEntries` |
|
|
192
|
-
| Sequencing | `seq`, `struct`, `zip`, `zipWith`, `zipLeft`, `zipRight`, `preceded`, `terminated`, `delimited`, `separatedPair`, `flatMap`, `gen`, `Do`/`bind`/`bindTo`/`let` |
|
|
204
|
+
| Constructors | `succeed`, `sync`, `empty`, `fail`, `failAt`, `failMessage`, `reject`, `todo`, `suspend`, `make`, `location`, `eof` |
|
|
205
|
+
| Mapping | `map`, `as`, `asVoid`, `asSome`, `tap`, `tryMap`, `tryMapUnknown`, `mapOrFail`, `filterMap`, `filter`, `filterOrFail`, `mapParser`, `fromEntries` |
|
|
206
|
+
| Sequencing | `seq`, `seqWith`, `struct`, `zip`, `zipWith`, `zipLeft`, `zipRight`, `preceded`, `terminated`, `delimited`, `separatedPair`, `flatMap`, `gen`, `Do`/`bind`/`bindTo`/`let` |
|
|
193
207
|
| Choice | `alt`, `orElse`, `orElseSucceed`, `dispatch`, `permutation` |
|
|
194
208
|
| Repetition | `repeat`, `repeatTill`, `separated`, `separatedFoldl1`, `separatedFoldr1`, `fold` |
|
|
195
|
-
| Control | `opt`, `optional`, `cond`, `peek`, `not`, `allConsuming` |
|
|
196
|
-
| Consumed input | `taken`, `withTaken`, `span`, `withSpan` |
|
|
197
|
-
| Errors | `cut`, `backtrack`, `complete`, `context`, `expected`, `mapError`, `orFail` |
|
|
209
|
+
| Control | `opt`, `optional`, `cond`, `peek`, `not`, `allConsuming`, `withFastPath` |
|
|
210
|
+
| Consumed input | `taken`, `withTaken`, `span`, `withSpan`, `mapSpan` |
|
|
211
|
+
| Errors | `cut`, `backtrack`, `complete`, `context`, `expected`, `quiet`, `mapError`, `orFail` |
|
|
198
212
|
| Services | `service`, `provideService`, `provide` |
|
|
199
213
|
| Recovery | `resumeAfter`, `retryAfter` |
|
|
200
214
|
| Debugging | `trace`, `named` |
|
|
@@ -250,18 +264,19 @@ export const json = Compiler.jit(Parser.preceded(ws, value))
|
|
|
250
264
|
Parser.parse(json, text) // same Result as before, about 5 times faster
|
|
251
265
|
```
|
|
252
266
|
|
|
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)).
|
|
267
|
+
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)). That is 3–5 times as fast as the same JSON grammar in Chevrotain, the fastest of the other JavaScript parser libraries measured, and about 8 and 20 times as fast as in Peggy and Parsimmon ([the comparison](docs/DESIGN.md#against-other-javascript-parser-libraries)).
|
|
254
268
|
|
|
255
|
-
There are
|
|
269
|
+
There are three compilers, one function each:
|
|
256
270
|
|
|
271
|
+
- `Compiler.wasm` scans the input in WebAssembly and builds the value with generated JavaScript. It is the fastest on strings and bytes: on the JSON benchmark it takes 17–36% less time than the JIT. It uses the JIT for array input, for streaming, and where `WebAssembly` or `new Function` is unavailable.
|
|
257
272
|
- `Compiler.jit` generates JavaScript where `new Function` is allowed, and falls back to the closure compiler where it isn't.
|
|
258
273
|
- `Compiler.closure` never generates code. It is meant for pages with a Content Security Policy.
|
|
259
274
|
|
|
260
|
-
A bundle contains only the compilers it calls. A program that compiles nothing bundles
|
|
275
|
+
A bundle contains only the compilers it calls. A program that compiles nothing bundles none, 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
276
|
|
|
262
|
-
|
|
277
|
+
Each returns 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
278
|
|
|
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.
|
|
279
|
+
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; the parsers given to a `Parser.combinator` compile. See the [design notes](docs/DESIGN.md#6-compiled-parsers) for details.
|
|
265
280
|
|
|
266
281
|
Some ways of writing a grammar compile to faster code than others:
|
|
267
282
|
|
|
@@ -280,8 +295,11 @@ Some ways of writing a grammar compile to faster code than others:
|
|
|
280
295
|
- 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
296
|
- 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
297
|
- 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.
|
|
298
|
+
- 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. `Compiler.runner(p)` prepares the parser once and returns a function of the state that does what `Parser.run` does, without looking the compiled code up on every run: about 20% faster per token.
|
|
284
299
|
- `Ascii.float` and `Text.float` convert numbers as they match them.
|
|
300
|
+
- Where a scanner written by hand beats compiled code on the tokens that dominate an input, put it in front of the grammar with `Parser.withFastPath(grammar, fast)`: compiled code runs `fast` first, and it returns `Parser.Defer` for anything it doesn't handle. The grammar stays the specification: interpreted runs, and so every error, use it alone, and `Parser.verifyFastPath(grammar, fast, inputs)` lists where the two disagree, for a test to assert that there are none.
|
|
301
|
+
- Build a string from parts with `Parser.fold(part, { init: () => "", f: (acc, x) => acc + x })` rather than `Parser.map(Parser.repeat(part), (parts) => parts.join(""))`: no array per string. A CSV grammar's all-quoted fields parse 40% faster that way.
|
|
302
|
+
- Write the end of a run as `Token.takeTill` of one to three characters (`Token.takeTill('"')`, `Token.takeTill(",\r\n")`), not as a predicate: on string input, compiled code finds the end with `indexOf`, up to 9 times as fast on long runs.
|
|
285
303
|
|
|
286
304
|
[`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).
|
|
287
305
|
|
|
@@ -303,12 +321,15 @@ while parsing array
|
|
|
303
321
|
expected value, found ","
|
|
304
322
|
```
|
|
305
323
|
|
|
324
|
+
An error is safe to log. It keeps the input (`error.input`, which `position` and `message` read, and which stays alive as long as the error does), but not as one of its own properties, and `toJSON` leaves it out: `JSON.stringify(error)`, Effect's `Cause` rendering and structured loggers write the offset, position, kind, expectations and message, not what was being parsed. The message shows at most 60 characters on each side of the failure (`…` marks the rest), so a failure in a 5 MB line of minified JSON is still a four-line message.
|
|
325
|
+
|
|
306
326
|
effect-parse tries hard to produce good messages without annotations:
|
|
307
327
|
|
|
308
328
|
- `alt` merges the expectations of failed branches: `expected "true", "false" or "null"`.
|
|
309
329
|
- Optional and repeated parsers leave _hints_, so after `repeat(digit)` a failure reads `expected digit or ")"`.
|
|
310
330
|
- The furthest failure wins (the PEG heuristic), so a value that fails after a separator is reported where it failed.
|
|
311
331
|
- `Parser.expected("value")` names a rule; `Parser.context("array")` adds "while parsing array".
|
|
332
|
+
- `Parser.quiet(skip)` keeps a whitespace or comment skipper out of later errors: without it, every error after the skipper also expects whitespace.
|
|
312
333
|
|
|
313
334
|
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`:
|
|
314
335
|
|
|
@@ -326,6 +347,8 @@ const port = Ascii.decUint.pipe(
|
|
|
326
347
|
// Parser<number, string | OutOfRange>
|
|
327
348
|
```
|
|
328
349
|
|
|
350
|
+
These checks fail where the checked parser started. A check that can only run once a whole construct has parsed, but should point at the part at fault, returns `Parser.failAt(offset, error)` from a `flatMap`, with an offset from `Parser.location` or `Parser.withSpan`: a mismatched end tag is reported at the end tag, not at the start of the element.
|
|
351
|
+
|
|
329
352
|
## Streaming and partial input
|
|
330
353
|
|
|
331
354
|
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:
|
|
@@ -364,6 +387,21 @@ const value = await Effect.runPromise(ParserStream.parseResponse(await fetch(url
|
|
|
364
387
|
|
|
365
388
|
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`).
|
|
366
389
|
|
|
390
|
+
### Untrusted streams
|
|
391
|
+
|
|
392
|
+
Every stream runner bounds what it buffers. `maxBufferSize` (in tokens: UTF-16 code units, bytes or array elements) defaults to 64 MiB (`ParserStream.defaultMaxBufferSize`) on `parseMany`, `parseAll`, `parseReadable` and `parseResponse`; a stream that exceeds it fails with a `ParserStream.BufferLimitError` (`limit`, `size`), a different tagged error from `ParseError`, so "too large" and "malformed" can be answered differently:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { Effect } from "effect"
|
|
396
|
+
import { ParserStream } from "@thomasfosterau/effect-parse"
|
|
397
|
+
|
|
398
|
+
const body = ParserStream.parseResponse(await fetch(url), json, { maxBufferSize: 8 * 1024 * 1024 }).pipe(
|
|
399
|
+
Effect.catchTag("BufferLimitError", (e) => Effect.fail(`payload too large: ${e.size} > ${e.limit} bytes`))
|
|
400
|
+
)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`parseMany` counts the input it has not consumed yet (one unfinished value, so a missing delimiter is caught); the other three hold the whole input and count all of it, and `parseResponse` fails before reading the body when `Content-Length` is already over the limit. Pass `Infinity` to turn the limit off for input you trust.
|
|
404
|
+
|
|
367
405
|
## Services
|
|
368
406
|
|
|
369
407
|
Parsers can require Effect services. These replace winnow's `Stateful` input: configuration, symbol tables, indentation stacks and so on.
|
|
@@ -515,7 +553,7 @@ On a 0.58 M-character JSON document (`vp run bench:syntax`), building the tree t
|
|
|
515
553
|
| Module | Contents | winnow / nom equivalent |
|
|
516
554
|
| --- | --- | --- |
|
|
517
555
|
| `Parser` | the type, combinators, runners | `Parser` trait, `combinator` / `combinator`, `branch`, `multi`, `sequence` |
|
|
518
|
-
| `Token` | `any`, `literal`, `oneOf`, `noneOf`, `take`, `takeWhile`, `takeTill`, `takeUntil`, `rest`, `range`, `codePoints` | `token` / `bytes` |
|
|
556
|
+
| `Token` | `any`, `literal`, `oneOf`, `noneOf`, `take`, `takeWhile`, `takeTill`, `takeUntil`, `takeUntilAny`, `rest`, `range`, `codePoints` | `token` / `bytes` |
|
|
519
557
|
| `Text` | Unicode parsers for any encoding: `string`, `utf8`, `utf16le`, `utf16be`, `latin1`, `make`; Unicode predicates | — |
|
|
520
558
|
| `Encoding` | text encodings for `Text`, `sniff` | — |
|
|
521
559
|
| `Ascii` | character classes, `lineEnding`, `decInt`, `decUint`, `hexUint`, `float`, `caseless`, `takeEscaped`, `escaped`, `Ascii.bytes.*` | `ascii` / `character`, `number` |
|
|
@@ -582,14 +620,18 @@ The `Nom` module provides the nom names directly, with nom's argument orders, fo
|
|
|
582
620
|
- [`json.ts`](examples/json.ts): JSON with `dispatch`, `cut` and escapes
|
|
583
621
|
- [`json-text.ts`](examples/json-text.ts): the same grammar written once against `Text.TextParsers`, for strings, UTF-8 and UTF-16
|
|
584
622
|
- [`json-fast.ts`](examples/json-fast.ts): the same grammar written for compiled parsers, with `Parser.fromEntries`
|
|
623
|
+
- [`json-tape.ts`](examples/json-tape.ts): the same grammar building no values at all: it appends 16-byte records to memory from [effect-ext](https://github.com/thomasfosterau/effect-ext)'s `Allocator`, and strings are read as views of the input's bytes. On UTF-8 input it parses about 1.4 times as fast as `TextDecoder` and `JSON.parse`, and leaves no garbage ([DESIGN.md](docs/DESIGN.md#zero-copy-output))
|
|
585
624
|
- [`arithmetic.ts`](examples/arithmetic.ts): direct evaluation, an AST via the Pratt parser, and a lexer plus a parser over the token array
|
|
586
625
|
- [`ini.ts`](examples/ini.ts): INI files with a generator-style parser
|
|
587
626
|
- [`http.ts`](examples/http.ts): HTTP requests over bytes, complete and streamed
|
|
588
627
|
- [`sexpr.ts`](examples/sexpr.ts): s-expressions with typed evaluation errors
|
|
628
|
+
- [`jq.ts`](examples/jq.ts): a subset of [jq](https://jqlang.org): its filter language with the Pratt parser and its JSON input with the compiled `json-fast.ts` grammar, behind an `effect/cli` command in [`jq-cli.ts`](examples/jq-cli.ts); `vp run bench:jq` races it against the real `jq`
|
|
589
629
|
- [`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))
|
|
590
630
|
|
|
591
631
|
`vp run bench` (from a clone) compares the JSON examples, on each encoding, with `JSON.parse`.
|
|
592
632
|
|
|
633
|
+
[`examples/ripgrep`](examples/ripgrep) is a larger example, a package of its own: `erg`, a search tool with ripgrep's command line whose regex engine is built from these combinators (Rust's regex syntax parsed with effect-parse, and compiled to a backtracking matcher over UTF-8 bytes, behind ripgrep-style literal prefilters and a lazy DFA), with a harness that benchmarks it against ripgrep, grep and other search tools, and tests that compare its output with ripgrep's.
|
|
634
|
+
|
|
593
635
|
## Design
|
|
594
636
|
|
|
595
637
|
[`docs/DESIGN.md`](docs/DESIGN.md) records the research into nom, winnow and Effect, and the reasoning behind each decision in this port.
|
|
@@ -606,7 +648,7 @@ vp run check:dist # check dist/ as a consumer imports it
|
|
|
606
648
|
vp run bench # JSON example: interpreted vs compiled, per encoding
|
|
607
649
|
```
|
|
608
650
|
|
|
609
|
-
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).
|
|
651
|
+
The other benchmarks (`bench:native`, `bench:stream`, `bench:gen`, `bench:syntax`, `bench:downstream`, which times the libraries built on this package, and `bench:zero-copy`) 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).
|
|
610
652
|
|
|
611
653
|
## License
|
|
612
654
|
|
package/dist/Ascii.d.ts
CHANGED
|
@@ -254,6 +254,16 @@ export declare const toFloat: (s: Input) => number;
|
|
|
254
254
|
* @category numbers
|
|
255
255
|
*/
|
|
256
256
|
export declare const float: Parser<number>;
|
|
257
|
+
/**
|
|
258
|
+
* The overloads of {@link caseless}.
|
|
259
|
+
*
|
|
260
|
+
* @since 0.5.0
|
|
261
|
+
* @category literals
|
|
262
|
+
*/
|
|
263
|
+
export interface CaselessFn {
|
|
264
|
+
(literal: string): Parser<string>;
|
|
265
|
+
(literal: Uint8Array): Parser<Uint8Array, never, never, Uint8Array>;
|
|
266
|
+
}
|
|
257
267
|
/**
|
|
258
268
|
* Matches a literal ignoring ASCII case (winnow's `Caseless`, nom's
|
|
259
269
|
* `tag_no_case`). Returns the input as written.
|
|
@@ -261,10 +271,7 @@ export declare const float: Parser<number>;
|
|
|
261
271
|
* @since 0.1.0
|
|
262
272
|
* @category literals
|
|
263
273
|
*/
|
|
264
|
-
export declare const caseless:
|
|
265
|
-
(literal: string): Parser<string>;
|
|
266
|
-
(literal: Uint8Array): Parser<Uint8Array, never, never, Uint8Array>;
|
|
267
|
-
};
|
|
274
|
+
export declare const caseless: CaselessFn;
|
|
268
275
|
/**
|
|
269
276
|
* Recognizes a string containing escape sequences, returning the raw slice
|
|
270
277
|
* (winnow's `take_escaped`, nom's `escaped`).
|
package/dist/Ascii.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Ascii.d.ts","names":[],"sources":["../src/Ascii.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"Ascii.d.ts","names":[],"sources":["../src/Ascii.ts"],"mappings":";;;;;;;;;;qBAmDa,UAAU;;;;;;;qBAkBV,UAAU;;;;;;;qBAeV,aAAa;;;;;;;qBAmBb,aAAa;;;;;;;qBAeb,iBAAiB;;;;;;;qBAgBjB,UAAU;;;;;;;qBAkBV,eAAe;;;;;;;qBAmBf,YAAY;;;;;;;qBAiCZ,QAAQ;;;;;;;qBAQR,QAAQ;;;;;;;qBAQR,QAAQ;;;;;;;qBAQR,QAAQ;;;;;;;qBAQR,WAAW;;;;;;;qBAQX,WAAW;;;;;;;qBAQX,WAAW;;;;;;;qBAQX,WAAW;;;;;;;qBAQX,eAAe;;;;;;;qBAQf,eAAe;;;;;;;qBAQf,QAAQ;;;;;;;qBAQR,QAAQ;;;;;;;;qBASR,aAAa;;;;;;;;qBASb,aAAa;;;;;;;qBAYb,MAAM;;;;;;;qBAQN,SAAS;;;;;;;qBAQT,KAAK;;;;;;;qBAQL,YAAY;;;;;;;;;qBAcZ,gBAAgB;;qBAsChB,WAAY,kBAAgB,OAAO;;;;;;;;qBAUnC,SAAS;;;;;;;;;qBAaT,QAAQ;;;;;;;qBAaR,YAAY;;;;;;;qBAUZ,WAAW;;;;;;;;qBAgBX,SAAS;;qBA0BT,UAAU,GAAG;;;;;;;;;;;;;;;;qBAiBb,OAAO;;;;;;;iBAeH;GACd,kBAAkB;GAClB,SAAS,aAAa,OAAO,0BAA0B;;;;;;;;;qBAU7C,UAAU;;;;;;;;;;;;;;;;;;;;qBAoCV,cAAe,GAAG,GAAG,GAAG,UAAU,OAAO,GAAG,IAAI,IAAI,WAAW,OAC1E,QAAQ,OAAO,GAAG,GAAG,GAAG,IACxB,0BACA,WAAW,OAAO,GAAG,IAAI,IAAI,QAC5B,OAAO,MAAM,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI;;qBA6BhC,kBAAmB,GAAG,GAAG,GAAG,UAAU,OAAO,GAAG,IAAI,IAAI,WAAW,OAAO,GACrF,QAAQ,OAAO,GAAG,GAAG,GAAG,IACxB,eAAe,OAAO,wBACtB,WAAW,OAAO,GAAG,IAAI,IAAI,KAC7B,QAAQ,OAAO,YAAY,kBAAkB,GAC7C,iBACA,UAAU,8BACT,OAAO,GAAG,IAAI,IAAI,IAAI,IAAI,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA6EpB,UAAW,GAAG,GAAG,UAAU,OAAO,IAAI,IAAI,WAAW,OAChE,QAAQ,eAAe,GAAG,GAAG,IAC7B,0BACA,QAAQ,eAAe,IAAI,IAAI,QAC9B,eAAe,IAAI,IAAI,IAAI,IAAI,IAAI;;qBAsBzB,cAAe,GAAG,GAAG,UAAU,OAAO,IAAI,IAAI,WAAW,OACpE,QAAQ,eAAe,GAAG,GAAG,IAC7B,eAAe,OAAO,wBACtB,QAAQ,eAAe,IAAI,IAAI,KAC/B,iBACA,UAAU,8BACT,eAAe,IAAI,IAAI,IAAI,IAAI,IAAI;KA4DjC,MAAM,KACT,UAAU,aAAa,SAAS,SAAS,SAAS,MAC9C,OAAO,mBAAmB,aAAa,GAAG,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;qBA8BzC;WACF,QAAQ,aAAa;WACrB,QAAQ,aAAa;WACrB,QAAQ,aAAa;WACrB,QAAQ,aAAa;WACrB,WAAW,aAAa;WACxB,WAAW,aAAa;WACxB,WAAW,aAAa;WACxB,WAAW,aAAa;WACxB,eAAe,aAAa;WAC5B,eAAe,aAAa;WAC5B,QAAQ,aAAa;WACrB,QAAQ,aAAa;WACrB,aAAa,aAAa;WAC1B,aAAa,aAAa;WAC1B,MAAM,OAAO,0BAA0B;WACvC,SAAS,OAAO,0BAA0B;WAC1C,KAAK,OAAO,0BAA0B;WACtC,YAAY,OAAO,0BAA0B;WAC7C,gBAAgB,aAAa;WAC7B,SAAS,8BAA8B;WACvC,QAAQ,8BAA8B;WACtC,YAAY,6BAA6B;WACzC,WAAW,6BAA6B;WACxC,SAAS,8BAA8B;WACvC,OAAO,6BAA6B"}
|
package/dist/Ascii.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Fail } from "./Input.js";
|
|
|
4
4
|
import * as P from "./Parser.js";
|
|
5
5
|
import * as numbers from "./internal/numbers.js";
|
|
6
6
|
import { Key } from "./internal/key.js";
|
|
7
|
+
import { controlCode } from "./internal/node.js";
|
|
7
8
|
import * as Resume from "./internal/resume.js";
|
|
8
9
|
import { codePointPredicate } from "./internal/tokens.js";
|
|
9
10
|
import * as Token$1 from "./Token.js";
|
|
@@ -372,7 +373,8 @@ const float = /*#__PURE__*/ floatText$1.pipe(/*#__PURE__*/ P.map(toFloat), /*#__
|
|
|
372
373
|
* @category literals
|
|
373
374
|
*/
|
|
374
375
|
const caseless = (lit) => literalImpl(lit, true);
|
|
375
|
-
|
|
376
|
+
/** The first UTF-16 unit of a control character: what `atControl` compares with a string's units. */
|
|
377
|
+
const firstUnit = (s) => s.charCodeAt(0);
|
|
376
378
|
const atControl = (state, control) => {
|
|
377
379
|
if (state.pos >= state.end) return false;
|
|
378
380
|
const input = state.input;
|
|
@@ -398,7 +400,7 @@ const atControl = (state, control) => {
|
|
|
398
400
|
* @category escapes
|
|
399
401
|
*/
|
|
400
402
|
const takeEscaped = (normal, control, escapable) => {
|
|
401
|
-
const ctrl = controlCode(control);
|
|
403
|
+
const ctrl = controlCode(control, firstUnit);
|
|
402
404
|
return takeEscapedImpl(normal, (state) => {
|
|
403
405
|
if (!atControl(state, ctrl)) return false;
|
|
404
406
|
state.pos++;
|
|
@@ -489,7 +491,7 @@ const takeEscapedImpl = (normal, matchControl, escapable, slice, control, encodi
|
|
|
489
491
|
* @category escapes
|
|
490
492
|
*/
|
|
491
493
|
const escaped = (normal, control, escape) => {
|
|
492
|
-
const ctrl = controlCode(control);
|
|
494
|
+
const ctrl = controlCode(control, firstUnit);
|
|
493
495
|
return escapedImpl(normal, (state) => {
|
|
494
496
|
if (!atControl(state, ctrl)) return false;
|
|
495
497
|
state.pos++;
|
|
@@ -560,6 +562,11 @@ const escapedImpl = (normal, matchControl, escape, control, encoding) => {
|
|
|
560
562
|
});
|
|
561
563
|
};
|
|
562
564
|
/**
|
|
565
|
+
* Gives a value the type of another view of the same parsers (see
|
|
566
|
+
* {@link bytes}).
|
|
567
|
+
*/
|
|
568
|
+
const retype = (value) => value;
|
|
569
|
+
/**
|
|
563
570
|
* The parsers of this module, typed for `Uint8Array` input. Slices are
|
|
564
571
|
* returned as `Uint8Array`s.
|
|
565
572
|
*
|
|
@@ -582,7 +589,7 @@ const bytes = /*#__PURE__*/ (() => {
|
|
|
582
589
|
const lit = (s) => Token$1.literal(new TextEncoder().encode(s));
|
|
583
590
|
const crlfB = lit("\r\n");
|
|
584
591
|
const newlineB = lit("\n");
|
|
585
|
-
|
|
592
|
+
const views = {
|
|
586
593
|
alpha0,
|
|
587
594
|
alpha1,
|
|
588
595
|
digit0,
|
|
@@ -609,6 +616,7 @@ const bytes = /*#__PURE__*/ (() => {
|
|
|
609
616
|
hexUint,
|
|
610
617
|
float
|
|
611
618
|
};
|
|
619
|
+
return retype(views);
|
|
612
620
|
})();
|
|
613
621
|
export { alpha0, alpha1, alphanumeric0, alphanumeric1, bytes, caseless, crlf, decBigInt, decBigUint, decInt, decUint, digit0, digit1, escaped, escapedImpl, float, hexDigit0, hexDigit1, hexUint, isAlpha, isAlphanumeric, isDigit, isHexDigit, isMultispace, isNewline, isOctDigit, isSpace, lineEnding, multispace0, multispace1, newline, octDigit0, octDigit1, overflow, space0, space1, tab, takeEscaped, takeEscapedImpl, tillLineEnding, toFloat };
|
|
614
622
|
|