@thomasfosterau/effect-parse 0.2.0 → 0.4.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 (144) hide show
  1. package/README.md +44 -1
  2. package/dist/Ascii.d.ts +0 -2
  3. package/dist/Ascii.d.ts.map +1 -1
  4. package/dist/Ascii.js +6 -6
  5. package/dist/Ascii.js.map +1 -1
  6. package/dist/Binary.d.ts +0 -2
  7. package/dist/Binary.d.ts.map +1 -1
  8. package/dist/Binary.js +1 -2
  9. package/dist/Bits.d.ts +0 -2
  10. package/dist/Bits.d.ts.map +1 -1
  11. package/dist/Bits.js +2 -3
  12. package/dist/Bits.js.map +1 -1
  13. package/dist/Compiler.d.ts +0 -2
  14. package/dist/Compiler.d.ts.map +1 -1
  15. package/dist/Compiler.js +1 -2
  16. package/dist/Encoding.d.ts +0 -2
  17. package/dist/Encoding.d.ts.map +1 -1
  18. package/dist/Encoding.js +1 -2
  19. package/dist/Expression.d.ts +0 -2
  20. package/dist/Expression.d.ts.map +1 -1
  21. package/dist/Expression.js +61 -18
  22. package/dist/Expression.js.map +1 -1
  23. package/dist/Input.d.ts +80 -7
  24. package/dist/Input.d.ts.map +1 -1
  25. package/dist/Input.js +149 -18
  26. package/dist/Input.js.map +1 -1
  27. package/dist/Nom.d.ts +1 -3
  28. package/dist/Nom.d.ts.map +1 -1
  29. package/dist/Nom.js +1 -2
  30. package/dist/ParseError.d.ts +0 -2
  31. package/dist/ParseError.d.ts.map +1 -1
  32. package/dist/ParseError.js +6 -10
  33. package/dist/ParseError.js.map +1 -1
  34. package/dist/Parser.d.ts +2 -2
  35. package/dist/Parser.d.ts.map +1 -1
  36. package/dist/Parser.js +58 -32
  37. package/dist/Parser.js.map +1 -1
  38. package/dist/ParserSchema.d.ts +198 -2
  39. package/dist/ParserSchema.d.ts.map +1 -1
  40. package/dist/ParserSchema.js +646 -18
  41. package/dist/ParserSchema.js.map +1 -1
  42. package/dist/ParserStream.d.ts +0 -2
  43. package/dist/ParserStream.d.ts.map +1 -1
  44. package/dist/ParserStream.js +15 -9
  45. package/dist/ParserStream.js.map +1 -1
  46. package/dist/Syntax.d.ts +0 -2
  47. package/dist/Syntax.d.ts.map +1 -1
  48. package/dist/Syntax.js +13 -11
  49. package/dist/Syntax.js.map +1 -1
  50. package/dist/Text.d.ts +0 -2
  51. package/dist/Text.d.ts.map +1 -1
  52. package/dist/Text.js +1 -2
  53. package/dist/Token.d.ts +0 -2
  54. package/dist/Token.d.ts.map +1 -1
  55. package/dist/Token.js +1 -2
  56. package/dist/internal/compiler/closure.d.ts +5 -2
  57. package/dist/internal/compiler/closure.d.ts.map +1 -1
  58. package/dist/internal/compiler/closure.js +160 -92
  59. package/dist/internal/compiler/closure.js.map +1 -1
  60. package/dist/internal/compiler/entries.d.ts +0 -2
  61. package/dist/internal/compiler/entries.d.ts.map +1 -1
  62. package/dist/internal/compiler/entries.js +6 -7
  63. package/dist/internal/compiler/entries.js.map +1 -1
  64. package/dist/internal/compiler/first.d.ts +0 -2
  65. package/dist/internal/compiler/first.d.ts.map +1 -1
  66. package/dist/internal/compiler/first.js +10 -10
  67. package/dist/internal/compiler/first.js.map +1 -1
  68. package/dist/internal/compiler/jit.d.ts +0 -2
  69. package/dist/internal/compiler/jit.d.ts.map +1 -1
  70. package/dist/internal/compiler/jit.js +52 -51
  71. package/dist/internal/compiler/jit.js.map +1 -1
  72. package/dist/internal/compiler/registry.d.ts +0 -2
  73. package/dist/internal/compiler/registry.d.ts.map +1 -1
  74. package/dist/internal/compiler/registry.js +1 -2
  75. package/dist/internal/compiler/sets.d.ts +2 -2
  76. package/dist/internal/compiler/sets.d.ts.map +1 -1
  77. package/dist/internal/compiler/sets.js +8 -3
  78. package/dist/internal/compiler/sets.js.map +1 -1
  79. package/dist/internal/entries.d.ts +0 -2
  80. package/dist/internal/entries.d.ts.map +1 -1
  81. package/dist/internal/entries.js +1 -2
  82. package/dist/internal/key.d.ts +10 -0
  83. package/dist/internal/key.d.ts.map +1 -0
  84. package/dist/internal/key.js +11 -0
  85. package/dist/internal/key.js.map +1 -0
  86. package/dist/internal/node.d.ts +11 -4
  87. package/dist/internal/node.d.ts.map +1 -1
  88. package/dist/internal/node.js +15 -3
  89. package/dist/internal/node.js.map +1 -1
  90. package/dist/internal/numbers.d.ts +0 -2
  91. package/dist/internal/numbers.d.ts.map +1 -1
  92. package/dist/internal/numbers.js +1 -2
  93. package/dist/internal/resume.d.ts +5 -6
  94. package/dist/internal/resume.d.ts.map +1 -1
  95. package/dist/internal/resume.js +1 -2
  96. package/dist/internal/resume.js.map +1 -1
  97. package/dist/internal/run.d.ts +0 -2
  98. package/dist/internal/run.d.ts.map +1 -1
  99. package/dist/internal/run.js +2 -3
  100. package/dist/internal/run.js.map +1 -1
  101. package/dist/internal/stack.d.ts +0 -2
  102. package/dist/internal/stack.d.ts.map +1 -1
  103. package/dist/internal/stack.js +12 -3
  104. package/dist/internal/stack.js.map +1 -1
  105. package/dist/internal/syntax.d.ts +30 -19
  106. package/dist/internal/syntax.d.ts.map +1 -1
  107. package/dist/internal/syntax.js +74 -63
  108. package/dist/internal/syntax.js.map +1 -1
  109. package/dist/internal/text.d.ts +5 -2
  110. package/dist/internal/text.d.ts.map +1 -1
  111. package/dist/internal/text.js +14 -5
  112. package/dist/internal/text.js.map +1 -1
  113. package/dist/internal/ties.d.ts +28 -0
  114. package/dist/internal/ties.d.ts.map +1 -0
  115. package/dist/internal/ties.js +66 -0
  116. package/dist/internal/ties.js.map +1 -0
  117. package/dist/internal/tokens.d.ts +10 -9
  118. package/dist/internal/tokens.d.ts.map +1 -1
  119. package/dist/internal/tokens.js +4 -8
  120. package/dist/internal/tokens.js.map +1 -1
  121. package/package.json +6 -6
  122. package/src/Ascii.ts +6 -5
  123. package/src/Bits.ts +1 -1
  124. package/src/Expression.ts +27 -23
  125. package/src/Input.ts +157 -21
  126. package/src/ParseError.ts +10 -8
  127. package/src/Parser.ts +67 -32
  128. package/src/ParserSchema.ts +982 -23
  129. package/src/ParserStream.ts +20 -8
  130. package/src/Syntax.ts +12 -9
  131. package/src/internal/compiler/closure.ts +202 -90
  132. package/src/internal/compiler/entries.ts +14 -11
  133. package/src/internal/compiler/first.ts +9 -8
  134. package/src/internal/compiler/jit.ts +95 -74
  135. package/src/internal/compiler/sets.ts +7 -0
  136. package/src/internal/key.ts +11 -0
  137. package/src/internal/node.ts +19 -2
  138. package/src/internal/resume.ts +5 -4
  139. package/src/internal/run.ts +1 -1
  140. package/src/internal/stack.ts +6 -1
  141. package/src/internal/syntax.ts +80 -84
  142. package/src/internal/text.ts +24 -4
  143. package/src/internal/ties.ts +83 -0
  144. package/src/internal/tokens.ts +20 -13
package/src/Input.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * @since 0.1.0
15
15
  */
16
+ import * as Arr from "effect/Array"
16
17
  import * as Predicate from "effect/Predicate"
17
18
  import * as Context from "effect/Context"
18
19
  import type { Encoding } from "./Encoding.js"
@@ -80,7 +81,7 @@ export const fromSource: {
80
81
  return source
81
82
  }
82
83
 
83
- const isArrayBufferLike = (u: unknown): u is ArrayBufferLike =>
84
+ const isArrayBufferLike = (u: Input | BinaryInput): u is ArrayBufferLike =>
84
85
  u instanceof ArrayBuffer || (globalThis.SharedArrayBuffer !== undefined && u instanceof SharedArrayBuffer)
85
86
 
86
87
  /**
@@ -200,9 +201,11 @@ export interface TraceEvent {
200
201
  export class State<I extends Input = Input> {
201
202
  /** The full input being parsed. */
202
203
  readonly input: I
203
- /** The current offset. Advanced by parsers, rewound on backtracking. */
204
- // Declared, not defined, so that a subclass can make it an accessor
205
- declare pos: number
204
+ /**
205
+ * The current offset. Parsers advance it by assigning it; moving it back
206
+ * (backtracking, or after a lookahead) goes through {@link rewind}.
207
+ */
208
+ pos: number
206
209
  /** The end offset (the input's length; in bits inside `Bits.bits`). */
207
210
  end: number
208
211
  /** 0 = string, 1 = Uint8Array, 2 = array. */
@@ -213,8 +216,31 @@ export class State<I extends Input = Input> {
213
216
  context: Context.Context<never>
214
217
  /** Mode of the latest failure. Only meaningful after a step returned {@link Fail}. */
215
218
  mode: ErrMode = "Backtrack"
216
- /** Details of the latest failure. Only meaningful after a step returned {@link Fail}. */
217
- failure: Failure
219
+ /**
220
+ * The latest failure, kept unmerged: the failure is `failureHead` merged
221
+ * (as `mergeFailures` does) with `failureTie1` and then `failureTie2`, all
222
+ * at the same offset. `Parser.alt` records its tied branch failures here
223
+ * with {@link failTies} instead of merging them, so that an enclosing `alt`
224
+ * whose next branch succeeds never pays for the merge. Reading
225
+ * {@link failure} performs the merge, once.
226
+ *
227
+ * @internal
228
+ */
229
+ failureHead: Failure
230
+ /** @internal */
231
+ failureTie1: Failure | undefined = undefined
232
+ /** @internal */
233
+ failureTie2: Failure | undefined = undefined
234
+ /** Backs {@link deepest}. @internal */
235
+ deepestFailure: Failure | undefined = undefined
236
+ /**
237
+ * Whether the furthest failure is the latest failure, still unmerged, so
238
+ * that {@link deepest} is that failure once merged. Only set while
239
+ * `failureTie1` is.
240
+ *
241
+ * @internal
242
+ */
243
+ deepestIsLatest = false
218
244
  /**
219
245
  * The offset of a recoverable failure that occurred without consuming
220
246
  * input (for example the last attempt of a `repeat`). Its
@@ -233,7 +259,14 @@ export class State<I extends Input = Input> {
233
259
  * recorded inside it when it commits, and `Parser.peek` and `Parser.not`
234
260
  * forget the failures of a lookahead that succeeded.
235
261
  */
236
- deepest: Failure | undefined = undefined
262
+ get deepest(): Failure | undefined {
263
+ if (this.deepestIsLatest) this.settleFailure()
264
+ return this.deepestFailure
265
+ }
266
+ set deepest(failure: Failure | undefined) {
267
+ this.deepestIsLatest = false
268
+ this.deepestFailure = failure
269
+ }
237
270
  /** When defined, `Parser.resumeAfter`/`retryAfter` record failures here. */
238
271
  recovered: Array<Failure> | undefined = undefined
239
272
  /** When defined, `Parser.trace` reports events here. */
@@ -253,7 +286,8 @@ export class State<I extends Input = Input> {
253
286
  */
254
287
  resume: Map<object, Map<number, unknown>> | undefined = undefined
255
288
  /**
256
- * The tree builder of a `Syntax` parse; `undefined` otherwise.
289
+ * The tree builder of a `Syntax` parse; `undefined` otherwise. A builder
290
+ * rather than a subclass of `State`, so that every state has one shape.
257
291
  *
258
292
  * @internal
259
293
  */
@@ -274,7 +308,37 @@ export class State<I extends Input = Input> {
274
308
  this.partial = options?.partial ?? false
275
309
  this.context = options?.context ?? Context.empty()
276
310
  this.inputKind = Predicate.isString(input) ? 0 : input instanceof Uint8Array ? 1 : 2
277
- this.failure = initialFailure
311
+ this.failureHead = initialFailure
312
+ }
313
+
314
+ /** Details of the latest failure. Only meaningful after a step returned {@link Fail}. */
315
+ get failure(): Failure {
316
+ if (this.failureTie1 !== undefined) this.settleFailure()
317
+ return this.failureHead
318
+ }
319
+ set failure(failure: Failure) {
320
+ if (this.deepestIsLatest) this.settleFailure()
321
+ this.failureHead = failure
322
+ this.failureTie1 = undefined
323
+ this.failureTie2 = undefined
324
+ }
325
+
326
+ /** Merges the latest failure's ties into it (see {@link failureHead}). */
327
+ private settleFailure(): void {
328
+ let failure = this.failureHead
329
+ const tie1 = this.failureTie1
330
+ if (tie1 !== undefined) {
331
+ failure = mergeTiedFailures(failure, tie1, mergeExpected(failure.expected, tie1.expected))
332
+ const tie2 = this.failureTie2
333
+ if (tie2 !== undefined) failure = mergeTiedFailures(failure, tie2, mergeExpected(failure.expected, tie2.expected))
334
+ this.failureHead = failure
335
+ this.failureTie1 = undefined
336
+ this.failureTie2 = undefined
337
+ }
338
+ if (this.deepestIsLatest) {
339
+ this.deepestFailure = failure
340
+ this.deepestIsLatest = false
341
+ }
278
342
  }
279
343
 
280
344
  /**
@@ -317,10 +381,13 @@ export class State<I extends Input = Input> {
317
381
  this.partial = options?.partial ?? false
318
382
  this.context = options?.context ?? Context.empty()
319
383
  this.mode = "Backtrack"
320
- this.failure = initialFailure
384
+ this.failureHead = initialFailure
385
+ this.failureTie1 = undefined
386
+ this.failureTie2 = undefined
321
387
  this.hintOffset = -1
322
388
  this.hintExpected = empty
323
- this.deepest = undefined
389
+ this.deepestFailure = undefined
390
+ this.deepestIsLatest = false
324
391
  this.recovered = undefined
325
392
  this.tracer = undefined
326
393
  this.depth = 0
@@ -329,6 +396,37 @@ export class State<I extends Input = Input> {
329
396
  return this
330
397
  }
331
398
 
399
+ /**
400
+ * Moves the cursor back to `offset`, abandoning what was parsed after it:
401
+ * how a parser backtracks, or returns to where a lookahead started. A step
402
+ * that moves the cursor backwards must use this rather than assigning
403
+ * {@link pos}, so that a `Syntax` parse drops the nodes the abandoned
404
+ * attempt built. Moving forward is an assignment.
405
+ *
406
+ * @example
407
+ * ```ts
408
+ * import { Input, Parser } from "@thomasfosterau/effect-parse"
409
+ *
410
+ * // Like `Parser.opt`, for one parser
411
+ * const optional = <A>(p: Parser.Parser<A>) =>
412
+ * Parser.make<A | undefined>((state) => {
413
+ * const start = state.pos
414
+ * const a = p.step(state)
415
+ * if (a !== Input.Fail) return a
416
+ * if (state.mode !== "Backtrack") return Input.Fail
417
+ * state.rewind(start)
418
+ * return undefined
419
+ * })
420
+ * ```
421
+ *
422
+ * @since 0.4.0
423
+ */
424
+ rewind(offset: number): void {
425
+ const syntax = this.syntax
426
+ if (syntax !== undefined && offset < this.pos) syntax.rewound(this.pos, offset)
427
+ this.pos = offset
428
+ }
429
+
332
430
  /**
333
431
  * The number of input units (code units, bytes or elements) remaining.
334
432
  */
@@ -410,10 +508,36 @@ export class State<I extends Input = Input> {
410
508
  */
411
509
  failWith(mode: ErrMode, failure: Failure): Fail {
412
510
  this.mode = mode
413
- this.failure = failure
414
- if (mode === "Backtrack" && (this.deepest === undefined || failure.offset >= this.deepest.offset)) {
415
- this.deepest = failure
511
+ if (this.deepestIsLatest) {
512
+ // The furthest failure is the latest one: it is replaced below, unmerged, if `failure` is as far
513
+ if (mode === "Backtrack" && failure.offset >= this.failureHead.offset) this.deepestIsLatest = false
514
+ else this.settleFailure()
416
515
  }
516
+ this.failureHead = failure
517
+ this.failureTie1 = undefined
518
+ this.failureTie2 = undefined
519
+ if (mode === "Backtrack" && (this.deepestFailure === undefined || failure.offset >= this.deepestFailure.offset)) {
520
+ this.deepestFailure = failure
521
+ }
522
+ if (this.syntax !== undefined) this.syntax.reach(failure.offset)
523
+ return Fail
524
+ }
525
+
526
+ /**
527
+ * Records a recoverable failure made of tied failures, merged only when
528
+ * read: `failWith("Backtrack", f)` where `f` merges `head` with `tie1`
529
+ * and then `tie2` (all at the same offset), as `mergeFailures` would.
530
+ *
531
+ * @internal
532
+ */
533
+ failTies(head: Failure, tie1: Failure, tie2: Failure | undefined): Fail {
534
+ this.mode = "Backtrack"
535
+ if (this.deepestIsLatest && head.offset < this.failureHead.offset) this.settleFailure()
536
+ this.failureHead = head
537
+ this.failureTie1 = tie1
538
+ this.failureTie2 = tie2
539
+ this.deepestIsLatest = this.deepestFailure === undefined || head.offset >= this.deepestFailure.offset
540
+ if (this.syntax !== undefined) this.syntax.reach(head.offset)
417
541
  return Fail
418
542
  }
419
543
 
@@ -444,8 +568,9 @@ export class State<I extends Input = Input> {
444
568
  ) {
445
569
  return f
446
570
  }
447
- const context =
448
- d.context.length === 0 ? f.context : [...d.context, ...f.context.filter((l) => !d.context.includes(l))]
571
+ const context = Arr.isReadonlyArrayEmpty(d.context)
572
+ ? f.context
573
+ : [...d.context, ...f.context.filter((l) => !d.context.includes(l))]
449
574
  return new Failure(d.offset, d.kind, d.expected, context, d.hasCause, d.cause, d.message, d.needed)
450
575
  }
451
576
 
@@ -455,6 +580,7 @@ export class State<I extends Input = Input> {
455
580
  * `repeat` and `Ascii.digit1`.
456
581
  */
457
582
  setHint(offset: number, expected: ReadonlyArray<string>): void {
583
+ if (this.syntax !== undefined) this.syntax.reach(offset)
458
584
  if (this.hintOffset === offset) {
459
585
  this.hintExpected = mergeExpected(this.hintExpected, expected)
460
586
  } else {
@@ -465,7 +591,7 @@ export class State<I extends Input = Input> {
465
591
 
466
592
  /** @internal */
467
593
  makeFailure(offset: number, kind: ErrorKind, expected: ReadonlyArray<string>): Failure {
468
- if (this.hintOffset === offset && this.hintExpected.length > 0) {
594
+ if (this.hintOffset === offset && Arr.isReadonlyArrayNonEmpty(this.hintExpected)) {
469
595
  expected = mergeExpected(this.hintExpected, expected)
470
596
  }
471
597
  return new Failure(offset, kind, expected, empty, false, undefined, undefined, 0)
@@ -474,8 +600,8 @@ export class State<I extends Input = Input> {
474
600
 
475
601
  /** @internal */
476
602
  export const mergeExpected = (a: ReadonlyArray<string>, b: ReadonlyArray<string>): ReadonlyArray<string> => {
477
- if (a.length === 0) return b
478
- if (b.length === 0) return a
603
+ if (Arr.isReadonlyArrayEmpty(a)) return b
604
+ if (Arr.isReadonlyArrayEmpty(b)) return a
479
605
  const out = a.slice()
480
606
  for (const e of b) {
481
607
  if (!out.includes(e)) out.push(e)
@@ -492,13 +618,23 @@ export const mergeExpected = (a: ReadonlyArray<string>, b: ReadonlyArray<string>
492
618
  export const mergeFailures = (a: Failure, b: Failure): Failure => {
493
619
  if (a.offset > b.offset) return a
494
620
  if (b.offset > a.offset) return b
495
- const expected = mergeExpected(a.expected, b.expected)
621
+ return mergeTiedFailures(a, b, mergeExpected(a.expected, b.expected))
622
+ }
623
+
624
+ /**
625
+ * {@link mergeFailures} for two failures at the same offset, given their
626
+ * merged expectations (`mergeExpected(a.expected, b.expected)`, possibly
627
+ * memoised by the caller).
628
+ *
629
+ * @internal
630
+ */
631
+ export const mergeTiedFailures = (a: Failure, b: Failure, expected: ReadonlyArray<string>): Failure => {
496
632
  if (expected === b.expected) return b
497
633
  return new Failure(
498
634
  b.offset,
499
635
  b.kind,
500
636
  expected,
501
- b.context.length > 0 ? b.context : a.context,
637
+ Arr.isReadonlyArrayNonEmpty(b.context) ? b.context : a.context,
502
638
  b.hasCause || a.hasCause,
503
639
  b.hasCause ? b.cause : a.cause,
504
640
  b.message ?? a.message,
package/src/ParseError.ts CHANGED
@@ -19,9 +19,11 @@
19
19
  *
20
20
  * @since 0.1.0
21
21
  */
22
+ import * as Arr from "effect/Array"
22
23
  import * as Predicate from "effect/Predicate"
23
24
  import * as Data from "effect/Data"
24
25
  import * as Option from "effect/Option"
26
+ import * as Result from "effect/Result"
25
27
  import type { Encoding } from "./Encoding.js"
26
28
  import type { ErrMode, Failure, Input } from "./Input.js"
27
29
  import * as text from "./internal/text.js"
@@ -245,7 +247,7 @@ const codePointCount = (s: string, start: number, end: number): number => {
245
247
  * @category utils
246
248
  */
247
249
  export const formatExpected = (expected: ReadonlyArray<string>): string => {
248
- if (expected.length === 0) return ""
250
+ if (Arr.isReadonlyArrayEmpty(expected)) return ""
249
251
  if (expected.length === 1) return expected[0]
250
252
  return `${expected.slice(0, -1).join(", ")} or ${expected[expected.length - 1]}`
251
253
  }
@@ -277,11 +279,11 @@ const describeFound = (input: Input, offset: number, encoding: Encoding<any> | u
277
279
  export const inspectToken = (u: unknown): string => {
278
280
  if (Predicate.isString(u)) return JSON.stringify(u)
279
281
  if (Predicate.isObjectOrArray(u)) {
280
- try {
281
- return JSON.stringify(u)
282
- } catch {
283
- return String(u)
284
- }
282
+ // `JSON.stringify` throws on cycles and `BigInt`s
283
+ return Result.getOrElse(
284
+ Result.try(() => JSON.stringify(u)),
285
+ () => String(u)
286
+ )
285
287
  }
286
288
  return String(u)
287
289
  }
@@ -289,7 +291,7 @@ export const inspectToken = (u: unknown): string => {
289
291
  const causeMessage = (cause: unknown): string | undefined => {
290
292
  if (cause === undefined) return undefined
291
293
  if (Predicate.isString(cause)) return cause
292
- if (cause instanceof Error) return cause.message
294
+ // An `Error`'s message, or any other object's string `message`
293
295
  if (Predicate.isObjectOrArray(cause) && "message" in cause && Predicate.isString(cause.message)) {
294
296
  return cause.message
295
297
  }
@@ -366,7 +368,7 @@ const render = (error: ParseError<unknown>): string => {
366
368
  if (error.description !== undefined) lines.push(error.description)
367
369
  if (error.mode === "Incomplete") {
368
370
  lines.push("unexpected end of input (incomplete)")
369
- } else if (error.expected.length > 0) {
371
+ } else if (Arr.isReadonlyArrayNonEmpty(error.expected)) {
370
372
  lines.push(`expected ${formatExpected(error.expected)}, found ${describeFound(input, offset, encoding)}`)
371
373
  } else if (cause === undefined && error.description === undefined) {
372
374
  lines.push(`unexpected ${describeFound(input, offset, encoding)} (${error.kind})`)
package/src/Parser.ts CHANGED
@@ -40,14 +40,16 @@ import { pipeArguments } from "effect/Pipeable"
40
40
  import * as Result from "effect/Result"
41
41
  import type { Contravariant, Covariant, Simplify } from "effect/Types"
42
42
  import type { ErrMode, Input, Slice, Source, TraceEvent } from "./Input.js"
43
- import { Fail, Failure, mergeExpected, mergeFailures, State } from "./Input.js"
43
+ import { Fail, Failure, mergeExpected, mergeFailures, mergeTiedFailures, State } from "./Input.js"
44
44
  import type { ErrorKind } from "./ParseError.js"
45
45
  import * as ParseError from "./ParseError.js"
46
46
  import * as Registry from "./internal/compiler/registry.js"
47
47
  import * as Entries from "./internal/entries.js"
48
+ import { Key } from "./internal/key.js"
48
49
  import * as Resume from "./internal/resume.js"
49
50
  import * as Run from "./internal/run.js"
50
51
  import * as Stack from "./internal/stack.js"
52
+ import { TiedFailures } from "./internal/ties.js"
51
53
  import type { Node } from "./internal/node.js"
52
54
  import { bounds, NodeId } from "./internal/node.js"
53
55
 
@@ -272,6 +274,8 @@ const isTwoParserArgs = (args: IArguments) => isParser(args[0]) && isParser(args
272
274
  * The step function must either return the parsed value, having advanced
273
275
  * `state.pos` past the consumed input, or return the result of one of
274
276
  * `state.backtrack`, `state.cut`, `state.incomplete`, ... to signal failure.
277
+ * A step that moves the cursor backwards (to retry, or after a lookahead)
278
+ * calls `state.rewind(offset)` rather than assigning `state.pos`.
275
279
  *
276
280
  * @example
277
281
  * ```ts
@@ -722,7 +726,7 @@ export const tryMap: {
722
726
  if (a === Fail) return Fail
723
727
  const r = f(a)
724
728
  if (Result.isSuccess(r)) return r.success
725
- state.pos = start
729
+ state.rewind(start)
726
730
  return state.failCause("Backtrack", "Verify", r.failure)
727
731
  },
728
732
  self.name,
@@ -776,7 +780,7 @@ export const filterMap: {
776
780
  if (a === Fail) return Fail
777
781
  const o = f(a)
778
782
  if (Option.isSome(o)) return o.value
779
- state.pos = start
783
+ state.rewind(start)
780
784
  return state.backtrack("Verify")
781
785
  },
782
786
  self.name,
@@ -807,7 +811,7 @@ export const filter: {
807
811
  const a = step(state)
808
812
  if (a === Fail) return Fail
809
813
  if (predicate(a)) return a
810
- state.pos = start
814
+ state.rewind(start)
811
815
  return state.backtrack("Verify")
812
816
  },
813
817
  self.name,
@@ -851,7 +855,7 @@ export const filterOrFail: {
851
855
  const a = step(state)
852
856
  if (a === Fail) return Fail
853
857
  if (predicate(a)) return a
854
- state.pos = start
858
+ state.rewind(start)
855
859
  return state.failCause("Backtrack", "Verify", orFailWith(a))
856
860
  },
857
861
  self.name,
@@ -1031,7 +1035,7 @@ export const complete = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Pa
1031
1035
  const start = state.pos
1032
1036
  const a = step(state)
1033
1037
  if (a === Fail && state.mode === "Incomplete") {
1034
- state.pos = start
1038
+ state.rewind(start)
1035
1039
  return state.backtrack("Complete")
1036
1040
  }
1037
1041
  return a
@@ -1222,7 +1226,7 @@ export const orElseSucceed: {
1222
1226
 
1223
1227
  /** Rewinds after a recoverable failure, remembering it as a hint. */
1224
1228
  const recover = (state: State<any>, start: number): void => {
1225
- state.pos = start
1229
+ state.rewind(start)
1226
1230
  const f = state.failure
1227
1231
  if (f.offset === start) {
1228
1232
  state.hintOffset = start
@@ -1296,7 +1300,7 @@ export const peek = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Parser
1296
1300
  if (a !== Fail) {
1297
1301
  // Failures inside a lookahead that succeeded are about input that was
1298
1302
  // never consumed, so they are not reported as the furthest failure
1299
- state.pos = start
1303
+ state.rewind(start)
1300
1304
  state.hintOffset = hintOffset
1301
1305
  state.hintExpected = hintExpected
1302
1306
  state.deepest = deepest
@@ -1326,12 +1330,12 @@ export const not = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Parser<
1326
1330
  const a = step(state)
1327
1331
  if (a !== Fail) {
1328
1332
  // The failures inside `self` are not this parser's failure
1329
- state.pos = start
1333
+ state.rewind(start)
1330
1334
  state.deepest = deepest
1331
1335
  return state.backtrack("Not")
1332
1336
  }
1333
1337
  if (state.mode !== "Backtrack") return Fail
1334
- state.pos = start
1338
+ state.rewind(start)
1335
1339
  state.hintOffset = hintOffset
1336
1340
  state.hintExpected = hintExpected
1337
1341
  state.deepest = deepest
@@ -1785,19 +1789,48 @@ export const alt = <const Ps extends ReadonlyArray<Parser.Any>>(
1785
1789
  return described(
1786
1790
  (state: State<any>) => {
1787
1791
  const start = state.pos
1792
+ // The branches' failures merged left to right, as `mergeFailures` would,
1793
+ // except that the last two failures tied with `failure` wait in `tie1`
1794
+ // and `tie2` and are merged only when needed: when a third tie arrives,
1795
+ // when a succeeding branch stops where they failed (for the hint), or
1796
+ // when the merge is read from the state after `failTies`. An `alt` whose
1797
+ // later branch consumes input discards them unmerged.
1788
1798
  let failure: Failure | undefined
1799
+ let tie1: Failure | undefined
1800
+ let tie2: Failure | undefined
1789
1801
  for (let i = 0; i < n; i++) {
1790
1802
  const a = steps[i](state)
1791
1803
  if (a !== Fail) {
1792
- if (failure !== undefined && failure.offset === state.pos) state.setHint(failure.offset, failure.expected)
1804
+ if (failure !== undefined && failure.offset === state.pos) {
1805
+ let expected = failure.expected
1806
+ if (tie1 !== undefined) expected = mergeExpected(expected, tie1.expected)
1807
+ if (tie2 !== undefined) expected = mergeExpected(expected, tie2.expected)
1808
+ state.setHint(failure.offset, expected)
1809
+ }
1793
1810
  return a
1794
1811
  }
1795
1812
  if (state.mode !== "Backtrack") return Fail
1796
- failure = failure === undefined ? state.failure : mergeFailures(failure, state.failure)
1797
- state.pos = start
1813
+ state.rewind(start)
1814
+ // The branch's failure, whose own ties (from a nested `alt`) can be taken over unmerged
1815
+ const head = state.failureHead
1816
+ if (failure === undefined || head.offset > failure.offset) {
1817
+ failure = head
1818
+ tie1 = state.failureTie1
1819
+ tie2 = state.failureTie2
1820
+ } else if (head.offset === failure.offset) {
1821
+ const f = state.failure
1822
+ if (tie1 === undefined) tie1 = f
1823
+ else if (tie2 === undefined) tie2 = f
1824
+ else {
1825
+ failure = mergeTiedFailures(failure, tie1, mergeExpected(failure.expected, tie1.expected))
1826
+ tie1 = tie2
1827
+ tie2 = f
1828
+ }
1829
+ }
1798
1830
  }
1799
1831
  // `failure` is set: there are at least two alternatives, and each one that fails sets it
1800
- return state.failWith("Backtrack", failure ?? state.failure)
1832
+ if (failure === undefined) return state.failWith("Backtrack", state.failure)
1833
+ return tie1 === undefined ? state.failWith("Backtrack", failure) : state.failTies(failure, tie1, tie2)
1801
1834
  },
1802
1835
  "alt",
1803
1836
  { _tag: "Alt", parsers }
@@ -1876,7 +1909,7 @@ export const dispatch: {
1876
1909
  if (key === Fail) return Fail
1877
1910
  const parser = choose(key)
1878
1911
  if (parser === undefined) {
1879
- state.pos = start
1912
+ state.rewind(start)
1880
1913
  return state.backtrack("Dispatch")
1881
1914
  }
1882
1915
  return parser.step(state)
@@ -1911,11 +1944,13 @@ export const permutation = <const Ps extends ReadonlyArray<Parser.Any>>(
1911
1944
  const steps = parsers.map((p) => p.step)
1912
1945
  const n = steps.length
1913
1946
  return make((state: State<any>) => {
1914
- const out = new Array(n)
1947
+ // A round's failures, merged only if no parser matches in it (#44)
1948
+ const ties = new TiedFailures()
1915
1949
  const done = new Array<boolean>(n).fill(false)
1950
+ const out = new Array(n)
1916
1951
  let remaining = n
1917
1952
  while (remaining > 0) {
1918
- let failure: Failure | undefined
1953
+ ties.clear()
1919
1954
  let matched = false
1920
1955
  for (let i = 0; i < n; i++) {
1921
1956
  if (done[i]) continue
@@ -1929,11 +1964,11 @@ export const permutation = <const Ps extends ReadonlyArray<Parser.Any>>(
1929
1964
  break
1930
1965
  }
1931
1966
  if (state.mode !== "Backtrack") return Fail
1932
- failure = failure === undefined ? state.failure : mergeFailures(failure, state.failure)
1933
- state.pos = start
1967
+ state.rewind(start)
1968
+ ties.add(state)
1934
1969
  }
1935
- // `failure` is set: some parser is not done yet, and each one that fails sets it
1936
- if (!matched) return state.failWith("Backtrack", failure ?? state.failure)
1970
+ // Some parser is not done yet, and each one that fails is folded in
1971
+ if (!matched) return ties.fail(state)
1937
1972
  }
1938
1973
  return out as any
1939
1974
  }, "permutation")
@@ -1963,7 +1998,7 @@ interface FoldCheckpoint<B> {
1963
1998
  readonly pos: number
1964
1999
  }
1965
2000
 
1966
- const saveFold = <B>(state: State<any>, key: object, entry: number, acc: B, pos: number): void =>
2001
+ const saveFold = <B>(state: State<any>, key: Key, entry: number, acc: B, pos: number): void =>
1967
2002
  Resume.save(state, key, entry, { acc, count: 0, pos } satisfies FoldCheckpoint<B>)
1968
2003
 
1969
2004
  const foldImpl = <A, B, E, R, I extends Input>(
@@ -1978,7 +2013,7 @@ const foldImpl = <A, B, E, R, I extends Input>(
1978
2013
  const [min, max] = normalizeRange(range)
1979
2014
  const step = self.step
1980
2015
  const checkLoop = max === Infinity
1981
- const key = {}
2016
+ const key = new Key()
1982
2017
  return described(
1983
2018
  (state) => {
1984
2019
  const entry = state.pos
@@ -2157,7 +2192,7 @@ export const repeatTill: {
2157
2192
  if (b !== Fail) return [acc, b]
2158
2193
  if (state.mode !== "Backtrack") return Fail
2159
2194
  endFailure = state.failure
2160
- state.pos = start
2195
+ state.rewind(start)
2161
2196
  if (acc.length >= max) return Fail
2162
2197
  }
2163
2198
  const a = step(state)
@@ -2220,7 +2255,7 @@ export const separated: {
2220
2255
  const step = self.step
2221
2256
  const sepStep = separator.step
2222
2257
  const checkLoop = max === Infinity
2223
- const key = {}
2258
+ const key = new Key()
2224
2259
  return described(
2225
2260
  (state) => {
2226
2261
  const entry = state.pos
@@ -2263,7 +2298,7 @@ export const separated: {
2263
2298
  Resume.save(state, key, entry, { acc, pos: start } satisfies SeparatedCheckpoint<A>)
2264
2299
  }
2265
2300
  if (state.mode !== "Backtrack" || acc.length < min) return Fail
2266
- state.pos = start
2301
+ state.rewind(start)
2267
2302
  return Resume.done(state, key, entry, resumed !== undefined, acc)
2268
2303
  }
2269
2304
  acc.push(a)
@@ -2304,7 +2339,7 @@ export const separatedFoldl1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2304
2339
  ): Parser<A, E | E2, R | R2, I & I2> => {
2305
2340
  const step = self.step
2306
2341
  const sepStep = separator.step
2307
- const key = {}
2342
+ const key = new Key()
2308
2343
  return described(
2309
2344
  (state) => {
2310
2345
  const entry = state.pos
@@ -2335,7 +2370,7 @@ export const separatedFoldl1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2335
2370
  if (a === Fail) {
2336
2371
  if (state.mode === "Incomplete" && state.resume !== undefined) saveFold(state, key, entry, acc, start)
2337
2372
  if (state.mode !== "Backtrack") return Fail
2338
- state.pos = start
2373
+ state.rewind(start)
2339
2374
  return Resume.done(state, key, entry, resumed !== undefined, acc as A)
2340
2375
  }
2341
2376
  acc = op(acc, s, a)
@@ -2380,7 +2415,7 @@ export const separatedFoldr1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2380
2415
  const a = step(state)
2381
2416
  if (a === Fail) {
2382
2417
  if (state.mode !== "Backtrack") return Fail
2383
- state.pos = start
2418
+ state.rewind(start)
2384
2419
  break
2385
2420
  }
2386
2421
  seps.push(s)
@@ -2552,7 +2587,7 @@ export const resumeAfter: {
2552
2587
  if (state.recovered === undefined || state.mode === "Incomplete") return Fail
2553
2588
  const failure = state.failure
2554
2589
  const mode = state.mode
2555
- state.pos = start
2590
+ state.rewind(start)
2556
2591
  if (recoverStep(state) === Fail) return state.failWith(mode, failure)
2557
2592
  state.recovered.push(failure)
2558
2593
  state.syntax?.skipped(start, failure, mode)
@@ -2599,7 +2634,7 @@ export const retryAfter: {
2599
2634
  if (state.recovered === undefined || state.mode === "Incomplete") return Fail
2600
2635
  const failure = state.failure
2601
2636
  const mode = state.mode
2602
- state.pos = start
2637
+ state.rewind(start)
2603
2638
  if (recoverStep(state) === Fail || state.pos === start) return state.failWith(mode, failure)
2604
2639
  state.recovered.push(failure)
2605
2640
  state.syntax?.skipped(start, failure, mode)
@@ -2676,7 +2711,7 @@ const preview = (state: State<any>, offset: number): string => {
2676
2711
  */
2677
2712
  export const consoleTracer = (event: TraceEvent): void => {
2678
2713
  const indent = " ".repeat(event.depth)
2679
- if (event._tag === "Enter") {
2714
+ if (Predicate.isTagged(event, "Enter")) {
2680
2715
  console.log(`${indent}> ${event.name} @${event.offset} | ${preview(event.state, event.offset)}`)
2681
2716
  } else {
2682
2717
  const consumed = (event.endOffset ?? event.offset) - event.offset