@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +56 -14
  3. package/dist/Ascii.d.ts +11 -4
  4. package/dist/Ascii.d.ts.map +1 -1
  5. package/dist/Ascii.js +12 -4
  6. package/dist/Ascii.js.map +1 -1
  7. package/dist/Binary.d.ts +5 -2
  8. package/dist/Binary.d.ts.map +1 -1
  9. package/dist/Binary.js +83 -37
  10. package/dist/Binary.js.map +1 -1
  11. package/dist/Bits.d.ts.map +1 -1
  12. package/dist/Bits.js +11 -4
  13. package/dist/Bits.js.map +1 -1
  14. package/dist/Compiler.d.ts +74 -2
  15. package/dist/Compiler.d.ts.map +1 -1
  16. package/dist/Compiler.js +78 -1
  17. package/dist/Compiler.js.map +1 -1
  18. package/dist/Expression.d.ts.map +1 -1
  19. package/dist/Expression.js +86 -74
  20. package/dist/Expression.js.map +1 -1
  21. package/dist/Input.d.ts +56 -5
  22. package/dist/Input.d.ts.map +1 -1
  23. package/dist/Input.js +70 -1
  24. package/dist/Input.js.map +1 -1
  25. package/dist/Nom.d.ts +5 -15
  26. package/dist/Nom.d.ts.map +1 -1
  27. package/dist/Nom.js.map +1 -1
  28. package/dist/ParseError.d.ts +69 -11
  29. package/dist/ParseError.d.ts.map +1 -1
  30. package/dist/ParseError.js +129 -28
  31. package/dist/ParseError.js.map +1 -1
  32. package/dist/Parser.d.ts +375 -29
  33. package/dist/Parser.d.ts.map +1 -1
  34. package/dist/Parser.js +394 -37
  35. package/dist/Parser.js.map +1 -1
  36. package/dist/ParserSchema.d.ts.map +1 -1
  37. package/dist/ParserSchema.js +2 -3
  38. package/dist/ParserSchema.js.map +1 -1
  39. package/dist/ParserStream.d.ts +122 -14
  40. package/dist/ParserStream.d.ts.map +1 -1
  41. package/dist/ParserStream.js +293 -62
  42. package/dist/ParserStream.js.map +1 -1
  43. package/dist/Syntax.d.ts +23 -5
  44. package/dist/Syntax.d.ts.map +1 -1
  45. package/dist/Syntax.js +1 -1
  46. package/dist/Syntax.js.map +1 -1
  47. package/dist/Text.d.ts.map +1 -1
  48. package/dist/Text.js +21 -15
  49. package/dist/Text.js.map +1 -1
  50. package/dist/Token.d.ts +65 -11
  51. package/dist/Token.d.ts.map +1 -1
  52. package/dist/Token.js +73 -26
  53. package/dist/Token.js.map +1 -1
  54. package/dist/internal/compiler/closure.d.ts +1 -1
  55. package/dist/internal/compiler/closure.d.ts.map +1 -1
  56. package/dist/internal/compiler/closure.js +263 -113
  57. package/dist/internal/compiler/closure.js.map +1 -1
  58. package/dist/internal/compiler/entries.d.ts +3 -1
  59. package/dist/internal/compiler/entries.d.ts.map +1 -1
  60. package/dist/internal/compiler/entries.js +3 -2
  61. package/dist/internal/compiler/entries.js.map +1 -1
  62. package/dist/internal/compiler/first.d.ts.map +1 -1
  63. package/dist/internal/compiler/first.js +9 -2
  64. package/dist/internal/compiler/first.js.map +1 -1
  65. package/dist/internal/compiler/graph.d.ts +39 -0
  66. package/dist/internal/compiler/graph.d.ts.map +1 -0
  67. package/dist/internal/compiler/graph.js +78 -0
  68. package/dist/internal/compiler/graph.js.map +1 -0
  69. package/dist/internal/compiler/jit.d.ts +5 -0
  70. package/dist/internal/compiler/jit.d.ts.map +1 -1
  71. package/dist/internal/compiler/jit.js +208 -106
  72. package/dist/internal/compiler/jit.js.map +1 -1
  73. package/dist/internal/compiler/registry.d.ts +11 -3
  74. package/dist/internal/compiler/registry.d.ts.map +1 -1
  75. package/dist/internal/compiler/registry.js.map +1 -1
  76. package/dist/internal/compiler/search.d.ts +32 -0
  77. package/dist/internal/compiler/search.d.ts.map +1 -0
  78. package/dist/internal/compiler/search.js +68 -0
  79. package/dist/internal/compiler/search.js.map +1 -0
  80. package/dist/internal/compiler/sets.d.ts.map +1 -1
  81. package/dist/internal/compiler/sets.js +1 -1
  82. package/dist/internal/compiler/sets.js.map +1 -1
  83. package/dist/internal/compiler/wasm.d.ts +8 -0
  84. package/dist/internal/compiler/wasm.d.ts.map +1 -0
  85. package/dist/internal/compiler/wasm.js +2840 -0
  86. package/dist/internal/compiler/wasm.js.map +1 -0
  87. package/dist/internal/compiler/wasmAsm.d.ts +199 -0
  88. package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
  89. package/dist/internal/compiler/wasmAsm.js +427 -0
  90. package/dist/internal/compiler/wasmAsm.js.map +1 -0
  91. package/dist/internal/entries.d.ts +3 -1
  92. package/dist/internal/entries.d.ts.map +1 -1
  93. package/dist/internal/entries.js.map +1 -1
  94. package/dist/internal/node.d.ts +77 -3
  95. package/dist/internal/node.d.ts.map +1 -1
  96. package/dist/internal/node.js +54 -10
  97. package/dist/internal/node.js.map +1 -1
  98. package/dist/internal/numbers.d.ts.map +1 -1
  99. package/dist/internal/numbers.js.map +1 -1
  100. package/dist/internal/run.d.ts +14 -0
  101. package/dist/internal/run.d.ts.map +1 -1
  102. package/dist/internal/run.js +53 -11
  103. package/dist/internal/run.js.map +1 -1
  104. package/dist/internal/stack.d.ts +14 -0
  105. package/dist/internal/stack.d.ts.map +1 -1
  106. package/dist/internal/stack.js +24 -2
  107. package/dist/internal/stack.js.map +1 -1
  108. package/dist/internal/syntax.d.ts +4 -4
  109. package/dist/internal/syntax.d.ts.map +1 -1
  110. package/dist/internal/syntax.js +18 -10
  111. package/dist/internal/syntax.js.map +1 -1
  112. package/dist/internal/text.d.ts +32 -0
  113. package/dist/internal/text.d.ts.map +1 -1
  114. package/dist/internal/text.js +68 -14
  115. package/dist/internal/text.js.map +1 -1
  116. package/dist/internal/tokens.d.ts +20 -0
  117. package/dist/internal/tokens.d.ts.map +1 -1
  118. package/dist/internal/tokens.js +72 -39
  119. package/dist/internal/tokens.js.map +1 -1
  120. package/dist/internal/tuple.d.ts +6 -0
  121. package/dist/internal/tuple.d.ts.map +1 -0
  122. package/dist/internal/tuple.js +170 -0
  123. package/dist/internal/tuple.js.map +1 -0
  124. package/package.json +18 -10
  125. package/src/Ascii.ts +47 -12
  126. package/src/Binary.ts +174 -73
  127. package/src/Bits.ts +16 -6
  128. package/src/Compiler.ts +101 -10
  129. package/src/Expression.ts +94 -81
  130. package/src/Input.ts +99 -7
  131. package/src/Nom.ts +15 -5
  132. package/src/ParseError.ts +209 -52
  133. package/src/Parser.ts +684 -143
  134. package/src/ParserSchema.ts +1 -2
  135. package/src/ParserStream.ts +361 -68
  136. package/src/Syntax.ts +60 -36
  137. package/src/Text.ts +32 -27
  138. package/src/Token.ts +152 -34
  139. package/src/internal/compiler/closure.ts +371 -153
  140. package/src/internal/compiler/entries.ts +4 -2
  141. package/src/internal/compiler/first.ts +16 -0
  142. package/src/internal/compiler/graph.ts +96 -0
  143. package/src/internal/compiler/jit.ts +319 -169
  144. package/src/internal/compiler/registry.ts +10 -2
  145. package/src/internal/compiler/search.ts +68 -0
  146. package/src/internal/compiler/sets.ts +7 -3
  147. package/src/internal/compiler/wasm.ts +3276 -0
  148. package/src/internal/compiler/wasmAsm.ts +484 -0
  149. package/src/internal/entries.ts +8 -2
  150. package/src/internal/node.ts +103 -7
  151. package/src/internal/numbers.ts +3 -1
  152. package/src/internal/run.ts +73 -12
  153. package/src/internal/stack.ts +25 -1
  154. package/src/internal/syntax.ts +39 -18
  155. package/src/internal/text.ts +97 -9
  156. package/src/internal/tokens.ts +106 -14
  157. package/src/internal/tuple.ts +141 -0
package/src/Compiler.ts CHANGED
@@ -4,11 +4,15 @@
4
4
  * `Parser.parseSync`, `Parser.parsePeek`, `Parser.parsePartial` and
5
5
  * `Parser.parseEffect` faster, from any `offset`.
6
6
  *
7
- * There are two compilers, one function each, so a program bundles only the
8
- * one it uses, and a program that compiles nothing bundles neither:
7
+ * There are three compilers, one function each, so a program bundles only
8
+ * the ones it uses, and a program that compiles nothing bundles none:
9
9
  *
10
+ * - {@link wasm} scans the input in WebAssembly and builds the value with
11
+ * generated JavaScript. It is the fastest on strings and bytes, and uses
12
+ * the JIT for array input and streaming, and where WebAssembly or
13
+ * `new Function` is unavailable.
10
14
  * - {@link jit} generates code with `new Function` where it is allowed, and
11
- * otherwise uses the closure compiler. It is the fastest.
15
+ * otherwise uses the closure compiler.
12
16
  * - {@link closure} builds specialized closures only. It never generates
13
17
  * code, and a bundle that uses only it contains no `new Function`, so it
14
18
  * is the choice under a Content Security Policy, where even probing for
@@ -32,7 +36,9 @@
32
36
  * **Results and errors are unchanged.** Compiled code records no
33
37
  * diagnostics. When it fails, the parser runs again, interpreted, from the
34
38
  * same offset, so every `ParseError` (offset, expected, context, cause and
35
- * message) is exactly the one the interpreter reports.
39
+ * message) is exactly the one the interpreter reports. Running out of
40
+ * partial input is not a failure that needs it: compiled code reports
41
+ * `Incomplete` where the interpreter does, with the same `needed`.
36
42
  *
37
43
  * **Callbacks should be pure.** On an input that fails, the callbacks given
38
44
  * to `map`, `filter`, `tryMap`, `tap`, `fold`, ... may run twice, once
@@ -41,19 +47,57 @@
41
47
  * nor those of a branch that is certain to fail, and token-set predicates
42
48
  * may be called once per character code and cached.
43
49
  *
44
- * `Parser.parsePartial` and `ParserStream.parseAll` run a build that
45
- * handles partial input. `ParserStream.parseMany`, `Parser.parseRecoverable`
46
- * and runs with the `trace` option are always interpreted. Parsers that
50
+ * `Parser.parsePartial`, `ParserStream.parseAll` and
51
+ * `ParserStream.parseMany` run a build that handles partial input;
52
+ * `parseMany` runs it once per value and parses a value again, interpreted,
53
+ * only when it fails. `Parser.parseRecoverable`, `Syntax`
54
+ * parses and runs with the `trace` option are always interpreted. Parsers that
47
55
  * can't be described, such as `Parser.make` and `Parser.gen`, run
48
- * interpreted inside compiled code.
56
+ * interpreted inside compiled code; the parsers given to a
57
+ * `Parser.combinator` compile, and its step is built from theirs.
49
58
  *
50
59
  * @since 0.2.0
51
60
  */
52
- import type { Input } from "./Input.js"
53
- import type { Parser } from "./Parser.js"
61
+ import type { Fail, Input, State } from "./Input.js"
62
+ import type { Parser, RunOptions } from "./Parser.js"
54
63
  import { backend as closureBackend } from "./internal/compiler/closure.js"
55
64
  import { backend as jitBackend } from "./internal/compiler/jit.js"
56
65
  import { register } from "./internal/compiler/registry.js"
66
+ import { backend as wasmBackend } from "./internal/compiler/wasm.js"
67
+ import { prepare } from "./internal/run.js"
68
+
69
+ /**
70
+ * Compiles a parser with the WebAssembly compiler. Returns the same parser.
71
+ *
72
+ * The input is copied into WebAssembly memory and scanned there, by code
73
+ * generated from the grammar; a JavaScript function generated with
74
+ * `new Function` then builds the value from what the scan recorded, with
75
+ * the same callbacks and the same objects as the JIT. Parts of the grammar
76
+ * with no WebAssembly form (such as `Parser.make`, `flatMap`, or `filter`
77
+ * on a value) run their JIT-compiled code from inside the scan.
78
+ *
79
+ * It uses the JIT (see {@link jit}) for array input, for the streaming build
80
+ * of `Parser.parsePartial` and `ParserStream`, and where `WebAssembly` or
81
+ * `new Function` is unavailable: a Content Security Policy needs both
82
+ * `'wasm-unsafe-eval'` and `'unsafe-eval'` for it. Short strings with the
83
+ * same characters may be returned as one string, which no program can tell
84
+ * apart from two.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { Ascii, Compiler, Parser, Token } from "@thomasfosterau/effect-parse"
89
+ *
90
+ * const list = Compiler.wasm(Parser.separated(Ascii.decInt, Token.literal(",")))
91
+ * Parser.parse(list, "1,2,3") // Result.succeed([1, 2, 3])
92
+ * ```
93
+ *
94
+ * @since 0.5.0
95
+ * @category compilation
96
+ */
97
+ export const wasm = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Parser<A, E, R, I> => {
98
+ register(self, wasmBackend)
99
+ return self
100
+ }
57
101
 
58
102
  /**
59
103
  * Compiles a parser with the JIT: generated code where `new Function` is
@@ -96,3 +140,50 @@ export const closure = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Par
96
140
  register(self, closureBackend)
97
141
  return self
98
142
  }
143
+
144
+ /**
145
+ * Prepares a parser for running many times on states you own, as
146
+ * `Parser.run` does: for drivers that make one small run per token, such as
147
+ * a lexer over a buffer. `Parser.run` looks the parser's compiled form up
148
+ * on every run, which costs about a quarter of a small token's time; the
149
+ * function returned here looks it up once (again only when the input kind
150
+ * changes, or between complete and partial states) and calls the compiled
151
+ * code directly.
152
+ *
153
+ * It returns exactly what `Parser.run(self, state, options)` returns, and
154
+ * leaves the state as it would: the output with the cursor where the
155
+ * parser stopped, or `Fail` with the interpreter's failure (or, with
156
+ * `diagnostics: false`, only the mode). Compile the parser first with
157
+ * {@link jit} or {@link closure}; a parser that isn't compiled runs
158
+ * interpreted. The compiled form is looked up on the first run, so compile
159
+ * the parser before using the runner: compiling it again later doesn't
160
+ * reach a runner that has already run.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { Ascii, Compiler, Input, Parser, Token } from "@thomasfosterau/effect-parse"
165
+ *
166
+ * const token = Compiler.jit(Parser.preceded(Ascii.space0, Parser.alt(Ascii.alpha1, Token.oneOf("+-"))))
167
+ * const next = Compiler.runner(token)
168
+ *
169
+ * const tokens = (text: string): Array<string> => {
170
+ * const state = new Input.State(text)
171
+ * const out: Array<string> = []
172
+ * while (state.pos < state.end) {
173
+ * const t = next(state.reset(state.pos))
174
+ * if (t === Input.Fail) break
175
+ * out.push(t)
176
+ * }
177
+ * return out
178
+ * }
179
+ *
180
+ * tokens("a + b") // ["a", "+", "b"]
181
+ * ```
182
+ *
183
+ * @since 0.5.0
184
+ * @category running
185
+ */
186
+ export const runner = <A, E, R, I extends Input>(
187
+ self: Parser<A, E, R, I>,
188
+ options?: RunOptions
189
+ ): ((state: State<I>) => A | Fail) => prepare(self, self.step, options?.diagnostics !== false)
package/src/Expression.ts CHANGED
@@ -38,7 +38,10 @@ import * as Predicate from "effect/Predicate"
38
38
  import type { Input, State } from "./Input.js"
39
39
  import { Fail } from "./Input.js"
40
40
  import type { Parser } from "./Parser.js"
41
- import { make as makeParser } from "./Parser.js"
41
+ import { combinator } from "./Parser.js"
42
+
43
+ /** A parser's step, interpreted or compiled. */
44
+ type Step = (state: State<any>) => unknown
42
45
  import { TiedFailures } from "./internal/ties.js"
43
46
 
44
47
  /**
@@ -154,101 +157,111 @@ export const make = <A, E, R, I extends Input, const Ops extends ReadonlyArray<O
154
157
  readonly operand: Parser<A, E, R, I>
155
158
  readonly operators: Ops
156
159
  }): Parser<A, E | OpError<Ops[number]>, R | OpServices<Ops[number]>, I & OpInput<Ops[number]>> => {
157
- const operand = options.operand.step
158
160
  const prefixes = options.operators.filter((o): o is Prefix<A, any, any, any> => Predicate.isTagged(o, "Prefix"))
159
161
  const postfixes = options.operators.filter((o): o is Postfix<A, any, any, any> => Predicate.isTagged(o, "Postfix"))
160
162
  const infixes = options.operators.filter((o): o is Infix<A, any, any, any> => Predicate.isTagged(o, "Infix"))
163
+ const operators = [...prefixes, ...postfixes, ...infixes]
164
+ const parsers = [options.operand, ...operators.map((o) => o.parser)]
165
+ // Builds the step from the operand's and the operators' steps, in that
166
+ // order: their own steps when interpreted, compiled ones when compiled
167
+ const build = (steps: ReadonlyArray<Step>) => {
168
+ const operand = steps[0]
169
+ const prefixSteps = steps.slice(1, 1 + prefixes.length)
170
+ const postfixSteps = steps.slice(1 + prefixes.length, 1 + prefixes.length + postfixes.length)
171
+ const infixSteps = steps.slice(1 + prefixes.length + postfixes.length)
161
172
 
162
- // Attempts each operator in turn. Returns the index of the first match
163
- // (with its output in `matched`), -1 if none matched, or -2 on a fatal
164
- // failure. Failed attempts are folded into `failures`, merged only when
165
- // read (#44): a round in which an operator matches discards them unmerged.
166
- // Like `matched`, it is shared by nested expressions, which clear it.
167
- let matched: unknown
168
- const failures = new TiedFailures()
169
- const tryOps = (state: State<any>, ops: ReadonlyArray<OperatorBase<any, any, any>>, mustConsume: boolean): number => {
170
- const start = state.pos
171
- for (let i = 0; i < ops.length; i++) {
172
- const out = ops[i].parser.step(state)
173
- if (out !== Fail) {
174
- // A prefix or postfix operator matching nothing would apply forever
175
- if (mustConsume && state.pos === start) {
176
- state.failMessage("Cut", "Assert", "prefix and postfix operators must always consume")
177
- return -2
173
+ // Attempts each operator in turn. Returns the index of the first match
174
+ // (with its output in `matched`), -1 if none matched, or -2 on a fatal
175
+ // failure. Failed attempts are folded into `failures`, merged only when
176
+ // read (#44): a round in which an operator matches discards them unmerged.
177
+ // Like `matched`, it is shared by nested expressions, which clear it.
178
+ let matched: unknown
179
+ const failures = new TiedFailures()
180
+ const tryOps = (state: State<any>, ops: ReadonlyArray<Step>, mustConsume: boolean): number => {
181
+ const start = state.pos
182
+ for (let i = 0; i < ops.length; i++) {
183
+ const out = ops[i](state)
184
+ if (out !== Fail) {
185
+ // A prefix or postfix operator matching nothing would apply forever
186
+ if (mustConsume && state.pos === start) {
187
+ state.failMessage("Cut", "Assert", "prefix and postfix operators must always consume")
188
+ return -2
189
+ }
190
+ matched = out
191
+ return i
178
192
  }
179
- matched = out
180
- return i
193
+ if (state.mode !== "Backtrack") return -2
194
+ state.rewind(start)
195
+ failures.add(state)
181
196
  }
182
- if (state.mode !== "Backtrack") return -2
183
- state.rewind(start)
184
- failures.add(state)
197
+ return -1
185
198
  }
186
- return -1
187
- }
188
199
 
189
- const expr = (state: State<any>, minPower: number): A | Fail => {
190
- let lhs: A | Fail
191
- failures.clear()
192
- const p = tryOps(state, prefixes, true)
193
- if (p === -2) return Fail
194
- if (p >= 0) {
195
- const op = prefixes[p]
196
- const opOut = matched
197
- const operandValue = expr(state, op.precedence * 2)
198
- if (operandValue === Fail) return Fail
199
- lhs = op.fold(opOut, operandValue)
200
- } else {
201
- const prefixFailure = failures.failure
202
- const prefixTie1 = failures.tie1
203
- const prefixTie2 = failures.tie2
204
- lhs = operand(state)
205
- if (lhs === Fail) {
206
- if (prefixFailure !== undefined && state.mode === "Backtrack") {
207
- // The operand may have parsed nested expressions, which reuse `failures`
208
- failures.failure = prefixFailure
209
- failures.tie1 = prefixTie1
210
- failures.tie2 = prefixTie2
211
- failures.add(state)
212
- failures.fail(state)
200
+ const expr = (state: State<any>, minPower: number): A | Fail => {
201
+ let lhs: A | Fail
202
+ failures.clear()
203
+ const p = tryOps(state, prefixSteps, true)
204
+ if (p === -2) return Fail
205
+ if (p >= 0) {
206
+ const op = prefixes[p]
207
+ const opOut = matched
208
+ const operandValue = expr(state, op.precedence * 2)
209
+ if (operandValue === Fail) return Fail
210
+ lhs = op.fold(opOut, operandValue)
211
+ } else {
212
+ const prefixFailure = failures.failure
213
+ const prefixTie1 = failures.tie1
214
+ const prefixTie2 = failures.tie2
215
+ // SAFETY: the operand's step, interpreted or compiled, returns its output (an `A`) or `Fail`
216
+ lhs = operand(state) as A | Fail
217
+ if (lhs === Fail) {
218
+ if (prefixFailure !== undefined && state.mode === "Backtrack") {
219
+ // The operand may have parsed nested expressions, which reuse `failures`
220
+ failures.failure = prefixFailure
221
+ failures.tie1 = prefixTie1
222
+ failures.tie2 = prefixTie2
223
+ failures.add(state)
224
+ failures.fail(state)
225
+ }
226
+ return Fail
213
227
  }
214
- return Fail
215
228
  }
216
- }
217
229
 
218
- while (true) {
219
- const start = state.pos
220
- failures.clear()
221
- const q = tryOps(state, postfixes, true)
222
- if (q === -2) return Fail
223
- if (q >= 0) {
224
- const op = postfixes[q]
225
- if (op.precedence * 2 < minPower) {
230
+ while (true) {
231
+ const start = state.pos
232
+ failures.clear()
233
+ const q = tryOps(state, postfixSteps, true)
234
+ if (q === -2) return Fail
235
+ if (q >= 0) {
236
+ const op = postfixes[q]
237
+ if (op.precedence * 2 < minPower) {
238
+ state.rewind(start)
239
+ break
240
+ }
241
+ lhs = op.fold(lhs, matched)
242
+ continue
243
+ }
244
+ // The infix attempts are folded on top of the postfix ones
245
+ const i = tryOps(state, infixSteps, false)
246
+ if (i === -2) return Fail
247
+ if (i < 0) {
248
+ failures.hint(state)
249
+ break
250
+ }
251
+ const op = infixes[i]
252
+ const leftPower = op.precedence * 2
253
+ if (leftPower < minPower) {
226
254
  state.rewind(start)
227
255
  break
228
256
  }
229
- lhs = op.fold(lhs, matched)
230
- continue
231
- }
232
- // The infix attempts are folded on top of the postfix ones
233
- const i = tryOps(state, infixes, false)
234
- if (i === -2) return Fail
235
- if (i < 0) {
236
- failures.hint(state)
237
- break
257
+ const opOut = matched
258
+ const rhs = expr(state, op.associativity === "left" ? leftPower + 1 : leftPower)
259
+ if (rhs === Fail) return Fail
260
+ lhs = op.fold(lhs, opOut, rhs)
238
261
  }
239
- const op = infixes[i]
240
- const leftPower = op.precedence * 2
241
- if (leftPower < minPower) {
242
- state.rewind(start)
243
- break
244
- }
245
- const opOut = matched
246
- const rhs = expr(state, op.associativity === "left" ? leftPower + 1 : leftPower)
247
- if (rhs === Fail) return Fail
248
- lhs = op.fold(lhs, opOut, rhs)
262
+ return lhs
249
263
  }
250
- return lhs
264
+ return (state: State<any>) => expr(state, 0)
251
265
  }
252
-
253
- return makeParser((state: State<any>) => expr(state, 0), "expression")
266
+ return combinator(parsers, build, "expression")
254
267
  }
package/src/Input.ts CHANGED
@@ -19,6 +19,7 @@ import * as Context from "effect/Context"
19
19
  import type { Encoding } from "./Encoding.js"
20
20
  import type { ErrorKind } from "./ParseError.js"
21
21
  import type { Builder as SyntaxBuilder } from "./internal/syntax.js"
22
+ import type { AsciiRun } from "./internal/text.js"
22
23
 
23
24
  /**
24
25
  * The kinds of input a parser can consume.
@@ -56,6 +57,17 @@ export type BinaryInput = ArrayBufferLike | ArrayBufferView
56
57
  */
57
58
  export type Source<I> = I | ([I] extends [Uint8Array] ? BinaryInput : never)
58
59
 
60
+ /**
61
+ * The overloads of {@link fromSource}.
62
+ *
63
+ * @since 0.5.0
64
+ * @category constructors
65
+ */
66
+ export interface FromSourceFn {
67
+ (source: BinaryInput): Uint8Array
68
+ <I extends Input>(source: Source<I>): I
69
+ }
70
+
59
71
  /**
60
72
  * Converts a {@link Source} to the input parsers read: binary data becomes a
61
73
  * `Uint8Array` view of the same memory, everything else is returned as is.
@@ -70,10 +82,7 @@ export type Source<I> = I | ([I] extends [Uint8Array] ? BinaryInput : never)
70
82
  * @since 0.1.0
71
83
  * @category constructors
72
84
  */
73
- export const fromSource: {
74
- (source: BinaryInput): Uint8Array
75
- <I extends Input>(source: Source<I>): I
76
- } = (source: Input | BinaryInput): any => {
85
+ export const fromSource: FromSourceFn = (source: Input | BinaryInput): any => {
77
86
  if (Predicate.isString(source) || source instanceof Uint8Array || Array.isArray(source)) return source
78
87
  if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength)
79
88
  // An explicit length fixes the view's size even for a resizable buffer
@@ -267,6 +276,18 @@ export class State<I extends Input = Input> {
267
276
  this.deepestIsLatest = false
268
277
  this.deepestFailure = failure
269
278
  }
279
+ /**
280
+ * Whether compiled code is running on this state. A compiled run that fails
281
+ * is thrown away (the interpreter runs again to build the error) and one
282
+ * that succeeds records no diagnostics, so interpreted steps called from it
283
+ * (an opaque `Parser.make`, a leaf near the end of a partial input) record
284
+ * a `Backtrack` or `Cut` failure's mode only, without building its
285
+ * {@link Failure} (#82). `Incomplete` failures are still built: a compiled
286
+ * run's `Incomplete` is reported as it stands.
287
+ *
288
+ * @internal
289
+ */
290
+ compiledRun = false
270
291
  /** When defined, `Parser.resumeAfter`/`retryAfter` record failures here. */
271
292
  recovered: Array<Failure> | undefined = undefined
272
293
  /** When defined, `Parser.trace` reports events here. */
@@ -292,6 +313,33 @@ export class State<I extends Input = Input> {
292
313
  * @internal
293
314
  */
294
315
  syntax: SyntaxBuilder | undefined = undefined
316
+ /**
317
+ * Where the stop characters of compiled `takeTill`s next occur in this
318
+ * state's input, by search site; created on first use. It lives as long
319
+ * as the state. `reset` keeps it for string input (a string never
320
+ * changes, and the positions are absolute, so they hold wherever the next
321
+ * run starts) and drops it for bytes and arrays, which a caller may
322
+ * refill in place between runs.
323
+ *
324
+ * @internal
325
+ */
326
+ searches: Map<object, Array<number>> | undefined = undefined
327
+ /**
328
+ * A run of ASCII bytes of this state's UTF-8 input, decoded once, from
329
+ * which `Text.utf8` slices the long strings it decodes (see
330
+ * `internal/text.ts`); `reset` clears it with the other per-run fields.
331
+ *
332
+ * @internal
333
+ */
334
+ asciiRun: AsciiRun | undefined = undefined
335
+ /**
336
+ * A `DataView` of this state's byte input, for the `Binary` parsers;
337
+ * created on first use. It lives as long as the state, whose input never
338
+ * changes: one per read cost most of a number's time.
339
+ *
340
+ * @internal
341
+ */
342
+ view: DataView | undefined = undefined
295
343
 
296
344
  constructor(
297
345
  source: I | Source<I>,
@@ -301,7 +349,7 @@ export class State<I extends Input = Input> {
301
349
  readonly offset?: number | undefined
302
350
  }
303
351
  ) {
304
- const input = fromSource(source as Input) as I
352
+ const input = fromSource<I>(source)
305
353
  this.input = input
306
354
  this.end = input.length
307
355
  this.pos = options?.offset ?? 0
@@ -349,7 +397,11 @@ export class State<I extends Input = Input> {
349
397
  *
350
398
  * Every per-run field is reset: the cursor and end, the latest failure,
351
399
  * hints and furthest failure, recorded recoveries, the tracer and tracing
352
- * depth, the recorded encoding and streaming checkpoints. Like the
400
+ * depth, the recorded encoding and streaming checkpoints. For string
401
+ * input, what the state has learnt about the string (where the stop
402
+ * characters of `takeTill` and the literals of `takeUntilAny` occur) is
403
+ * kept, as a string cannot change; for bytes and arrays it is dropped, so
404
+ * a buffer refilled in place between runs is searched afresh. Like the
353
405
  * constructor, `partial` defaults to `false` and `context` to the empty
354
406
  * context, so pass them again on every reset.
355
407
  *
@@ -388,11 +440,20 @@ export class State<I extends Input = Input> {
388
440
  this.hintExpected = empty
389
441
  this.deepestFailure = undefined
390
442
  this.deepestIsLatest = false
443
+ this.compiledRun = false
391
444
  this.recovered = undefined
392
445
  this.tracer = undefined
393
446
  this.depth = 0
394
447
  this.encoding = undefined
395
448
  this.resume = undefined
449
+ // For a string, `searches` is kept: it remembers where stop characters
450
+ // and literals occur by absolute position, and a string never changes,
451
+ // so it holds at any offset. A lexer that resets before each token
452
+ // otherwise searched to the end of the buffer again for every token
453
+ // (#126). Bytes and arrays can be refilled in place between runs (a
454
+ // `Uint8Array` source is viewed, not copied), so theirs is dropped.
455
+ if (this.inputKind !== 0) this.searches = undefined
456
+ this.asciiRun = undefined
396
457
  return this
397
458
  }
398
459
 
@@ -445,7 +506,14 @@ export class State<I extends Input = Input> {
445
506
  * Extracts the slice `[start, end)` of the input.
446
507
  */
447
508
  slice(start: number, end: number): Slice<I> {
448
- const input = this.input as any
509
+ // SAFETY: `inputKind` 1 is a `Uint8Array`, whose `subarray` returns a
510
+ // `Uint8Array` view (its `Slice`); 0 and 2 are a string and an array, whose
511
+ // `slice` returns a string and an array of the same elements (their
512
+ // `Slice`). `subarray` is only called for kind 1
513
+ const input = this.input as {
514
+ slice(start: number, end: number): Slice<I>
515
+ subarray(start: number, end: number): Slice<I>
516
+ } & I
449
517
  switch (this.inputKind) {
450
518
  case 0:
451
519
  return input.slice(start, end)
@@ -460,6 +528,10 @@ export class State<I extends Input = Input> {
460
528
  * Records a recoverable failure at the current offset and returns {@link Fail}.
461
529
  */
462
530
  backtrack(kind: ErrorKind, expected: ReadonlyArray<string> = empty, offset: number = this.pos): Fail {
531
+ if (this.compiledRun) {
532
+ this.mode = "Backtrack"
533
+ return Fail
534
+ }
463
535
  return this.failWith("Backtrack", this.makeFailure(offset, kind, expected))
464
536
  }
465
537
 
@@ -467,6 +539,10 @@ export class State<I extends Input = Input> {
467
539
  * Records an unrecoverable failure at the current offset and returns {@link Fail}.
468
540
  */
469
541
  cut(kind: ErrorKind, expected: ReadonlyArray<string> = empty, offset: number = this.pos): Fail {
542
+ if (this.compiledRun) {
543
+ this.mode = "Cut"
544
+ return Fail
545
+ }
470
546
  return this.failWith("Cut", this.makeFailure(offset, kind, expected))
471
547
  }
472
548
 
@@ -598,14 +674,30 @@ export class State<I extends Input = Input> {
598
674
  }
599
675
  }
600
676
 
677
+ /**
678
+ * The last merges {@link mergeExpected} made, as `[a, b, merged]` triples. A
679
+ * parse merges the same few pairs of expectation arrays over and over (a
680
+ * whitespace skipper's hint with a repetition's, then with the failure of
681
+ * the token after them), and expectation arrays are never mutated, so a
682
+ * merge seen recently returns its earlier result, which in turn keeps the
683
+ * next merge's input the same array.
684
+ */
685
+ const merges: Array<ReadonlyArray<string>> = /*#__PURE__*/ new Array(12).fill(empty)
686
+ let mergeSlot = 0
687
+
601
688
  /** @internal */
602
689
  export const mergeExpected = (a: ReadonlyArray<string>, b: ReadonlyArray<string>): ReadonlyArray<string> => {
603
690
  if (Arr.isReadonlyArrayEmpty(a)) return b
604
691
  if (Arr.isReadonlyArrayEmpty(b)) return a
692
+ for (let i = 0; i < 12; i += 3) if (merges[i] === a && merges[i + 1] === b) return merges[i + 2]
605
693
  const out = a.slice()
606
694
  for (const e of b) {
607
695
  if (!out.includes(e)) out.push(e)
608
696
  }
697
+ merges[mergeSlot] = a
698
+ merges[mergeSlot + 1] = b
699
+ merges[mergeSlot + 2] = out
700
+ mergeSlot = mergeSlot === 9 ? 0 : mergeSlot + 3
609
701
  return out
610
702
  }
611
703
 
package/src/Nom.ts CHANGED
@@ -132,7 +132,9 @@ export const take = Token.take
132
132
  */
133
133
  export const takeWhile = <I extends Input = string>(
134
134
  predicate: (token: TokenOf<I>) => boolean
135
- ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> => Token.takeWhile<I>(predicate as any)
135
+ ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> =>
136
+ // SAFETY: a predicate over the input's tokens is a member of `TokenSet<I>` for each input kind
137
+ Token.takeWhile<I>(predicate as Token.TokenSet<I>)
136
138
 
137
139
  /**
138
140
  * @since 0.1.0
@@ -140,7 +142,9 @@ export const takeWhile = <I extends Input = string>(
140
142
  */
141
143
  export const takeWhile1 = <I extends Input = string>(
142
144
  predicate: (token: TokenOf<I>) => boolean
143
- ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> => Token.takeWhile<I>(predicate as any, { min: 1 })
145
+ ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> =>
146
+ // SAFETY: a predicate over the input's tokens is a member of `TokenSet<I>` for each input kind
147
+ Token.takeWhile<I>(predicate as Token.TokenSet<I>, { min: 1 })
144
148
 
145
149
  /**
146
150
  * @since 0.1.0
@@ -150,7 +154,9 @@ export const takeWhileMN = <I extends Input = string>(
150
154
  m: number,
151
155
  n: number,
152
156
  predicate: (token: TokenOf<I>) => boolean
153
- ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> => Token.takeWhile<I>(predicate as any, { min: m, max: n })
157
+ ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> =>
158
+ // SAFETY: a predicate over the input's tokens is a member of `TokenSet<I>` for each input kind
159
+ Token.takeWhile<I>(predicate as Token.TokenSet<I>, { min: m, max: n })
154
160
 
155
161
  /**
156
162
  * @since 0.1.0
@@ -158,7 +164,9 @@ export const takeWhileMN = <I extends Input = string>(
158
164
  */
159
165
  export const takeTill = <I extends Input = string>(
160
166
  predicate: (token: TokenOf<I>) => boolean
161
- ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> => Token.takeTill<I>(predicate as any)
167
+ ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> =>
168
+ // SAFETY: a predicate over the input's tokens is a member of `TokenSet<I>` for each input kind
169
+ Token.takeTill<I>(predicate as Token.TokenSet<I>)
162
170
 
163
171
  /**
164
172
  * @since 0.1.0
@@ -166,7 +174,9 @@ export const takeTill = <I extends Input = string>(
166
174
  */
167
175
  export const takeTill1 = <I extends Input = string>(
168
176
  predicate: (token: TokenOf<I>) => boolean
169
- ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> => Token.takeTill<I>(predicate as any, { min: 1 })
177
+ ): Parser<Slice<NoInfer<I>>, never, never, NoInfer<I>> =>
178
+ // SAFETY: a predicate over the input's tokens is a member of `TokenSet<I>` for each input kind
179
+ Token.takeTill<I>(predicate as Token.TokenSet<I>, { min: 1 })
170
180
 
171
181
  /**
172
182
  * @since 0.1.0