@thomasfosterau/effect-parse 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Input.ts CHANGED
@@ -213,8 +213,31 @@ export class State<I extends Input = Input> {
213
213
  context: Context.Context<never>
214
214
  /** Mode of the latest failure. Only meaningful after a step returned {@link Fail}. */
215
215
  mode: ErrMode = "Backtrack"
216
- /** Details of the latest failure. Only meaningful after a step returned {@link Fail}. */
217
- failure: Failure
216
+ /**
217
+ * The latest failure, kept unmerged: the failure is `failureHead` merged
218
+ * (as `mergeFailures` does) with `failureTie1` and then `failureTie2`, all
219
+ * at the same offset. `Parser.alt` records its tied branch failures here
220
+ * with {@link failTies} instead of merging them, so that an enclosing `alt`
221
+ * whose next branch succeeds never pays for the merge. Reading
222
+ * {@link failure} performs the merge, once.
223
+ *
224
+ * @internal
225
+ */
226
+ failureHead: Failure
227
+ /** @internal */
228
+ failureTie1: Failure | undefined = undefined
229
+ /** @internal */
230
+ failureTie2: Failure | undefined = undefined
231
+ /** Backs {@link deepest}. @internal */
232
+ deepestFailure: Failure | undefined = undefined
233
+ /**
234
+ * Whether the furthest failure is the latest failure, still unmerged, so
235
+ * that {@link deepest} is that failure once merged. Only set while
236
+ * `failureTie1` is.
237
+ *
238
+ * @internal
239
+ */
240
+ deepestIsLatest = false
218
241
  /**
219
242
  * The offset of a recoverable failure that occurred without consuming
220
243
  * input (for example the last attempt of a `repeat`). Its
@@ -233,7 +256,14 @@ export class State<I extends Input = Input> {
233
256
  * recorded inside it when it commits, and `Parser.peek` and `Parser.not`
234
257
  * forget the failures of a lookahead that succeeded.
235
258
  */
236
- deepest: Failure | undefined = undefined
259
+ get deepest(): Failure | undefined {
260
+ if (this.deepestIsLatest) this.settleFailure()
261
+ return this.deepestFailure
262
+ }
263
+ set deepest(failure: Failure | undefined) {
264
+ this.deepestIsLatest = false
265
+ this.deepestFailure = failure
266
+ }
237
267
  /** When defined, `Parser.resumeAfter`/`retryAfter` record failures here. */
238
268
  recovered: Array<Failure> | undefined = undefined
239
269
  /** When defined, `Parser.trace` reports events here. */
@@ -274,7 +304,37 @@ export class State<I extends Input = Input> {
274
304
  this.partial = options?.partial ?? false
275
305
  this.context = options?.context ?? Context.empty()
276
306
  this.inputKind = Predicate.isString(input) ? 0 : input instanceof Uint8Array ? 1 : 2
277
- this.failure = initialFailure
307
+ this.failureHead = initialFailure
308
+ }
309
+
310
+ /** Details of the latest failure. Only meaningful after a step returned {@link Fail}. */
311
+ get failure(): Failure {
312
+ if (this.failureTie1 !== undefined) this.settleFailure()
313
+ return this.failureHead
314
+ }
315
+ set failure(failure: Failure) {
316
+ if (this.deepestIsLatest) this.settleFailure()
317
+ this.failureHead = failure
318
+ this.failureTie1 = undefined
319
+ this.failureTie2 = undefined
320
+ }
321
+
322
+ /** Merges the latest failure's ties into it (see {@link failureHead}). */
323
+ private settleFailure(): void {
324
+ let failure = this.failureHead
325
+ const tie1 = this.failureTie1
326
+ if (tie1 !== undefined) {
327
+ failure = mergeTiedFailures(failure, tie1, mergeExpected(failure.expected, tie1.expected))
328
+ const tie2 = this.failureTie2
329
+ if (tie2 !== undefined) failure = mergeTiedFailures(failure, tie2, mergeExpected(failure.expected, tie2.expected))
330
+ this.failureHead = failure
331
+ this.failureTie1 = undefined
332
+ this.failureTie2 = undefined
333
+ }
334
+ if (this.deepestIsLatest) {
335
+ this.deepestFailure = failure
336
+ this.deepestIsLatest = false
337
+ }
278
338
  }
279
339
 
280
340
  /**
@@ -317,10 +377,13 @@ export class State<I extends Input = Input> {
317
377
  this.partial = options?.partial ?? false
318
378
  this.context = options?.context ?? Context.empty()
319
379
  this.mode = "Backtrack"
320
- this.failure = initialFailure
380
+ this.failureHead = initialFailure
381
+ this.failureTie1 = undefined
382
+ this.failureTie2 = undefined
321
383
  this.hintOffset = -1
322
384
  this.hintExpected = empty
323
- this.deepest = undefined
385
+ this.deepestFailure = undefined
386
+ this.deepestIsLatest = false
324
387
  this.recovered = undefined
325
388
  this.tracer = undefined
326
389
  this.depth = 0
@@ -410,10 +473,34 @@ export class State<I extends Input = Input> {
410
473
  */
411
474
  failWith(mode: ErrMode, failure: Failure): Fail {
412
475
  this.mode = mode
413
- this.failure = failure
414
- if (mode === "Backtrack" && (this.deepest === undefined || failure.offset >= this.deepest.offset)) {
415
- this.deepest = failure
476
+ if (this.deepestIsLatest) {
477
+ // The furthest failure is the latest one: it is replaced below, unmerged, if `failure` is as far
478
+ if (mode === "Backtrack" && failure.offset >= this.failureHead.offset) this.deepestIsLatest = false
479
+ else this.settleFailure()
416
480
  }
481
+ this.failureHead = failure
482
+ this.failureTie1 = undefined
483
+ this.failureTie2 = undefined
484
+ if (mode === "Backtrack" && (this.deepestFailure === undefined || failure.offset >= this.deepestFailure.offset)) {
485
+ this.deepestFailure = failure
486
+ }
487
+ return Fail
488
+ }
489
+
490
+ /**
491
+ * Records a recoverable failure made of tied failures, merged only when
492
+ * read: `failWith("Backtrack", f)` where `f` merges `head` with `tie1`
493
+ * and then `tie2` (all at the same offset), as `mergeFailures` would.
494
+ *
495
+ * @internal
496
+ */
497
+ failTies(head: Failure, tie1: Failure, tie2: Failure | undefined): Fail {
498
+ this.mode = "Backtrack"
499
+ if (this.deepestIsLatest && head.offset < this.failureHead.offset) this.settleFailure()
500
+ this.failureHead = head
501
+ this.failureTie1 = tie1
502
+ this.failureTie2 = tie2
503
+ this.deepestIsLatest = this.deepestFailure === undefined || head.offset >= this.deepestFailure.offset
417
504
  return Fail
418
505
  }
419
506
 
@@ -492,7 +579,17 @@ export const mergeExpected = (a: ReadonlyArray<string>, b: ReadonlyArray<string>
492
579
  export const mergeFailures = (a: Failure, b: Failure): Failure => {
493
580
  if (a.offset > b.offset) return a
494
581
  if (b.offset > a.offset) return b
495
- const expected = mergeExpected(a.expected, b.expected)
582
+ return mergeTiedFailures(a, b, mergeExpected(a.expected, b.expected))
583
+ }
584
+
585
+ /**
586
+ * {@link mergeFailures} for two failures at the same offset, given their
587
+ * merged expectations (`mergeExpected(a.expected, b.expected)`, possibly
588
+ * memoised by the caller).
589
+ *
590
+ * @internal
591
+ */
592
+ export const mergeTiedFailures = (a: Failure, b: Failure, expected: ReadonlyArray<string>): Failure => {
496
593
  if (expected === b.expected) return b
497
594
  return new Failure(
498
595
  b.offset,
package/src/ParseError.ts CHANGED
@@ -289,7 +289,7 @@ export const inspectToken = (u: unknown): string => {
289
289
  const causeMessage = (cause: unknown): string | undefined => {
290
290
  if (cause === undefined) return undefined
291
291
  if (Predicate.isString(cause)) return cause
292
- if (cause instanceof Error) return cause.message
292
+ // An `Error`'s message, or any other object's string `message`
293
293
  if (Predicate.isObjectOrArray(cause) && "message" in cause && Predicate.isString(cause.message)) {
294
294
  return cause.message
295
295
  }
package/src/Parser.ts CHANGED
@@ -40,7 +40,7 @@ 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"
@@ -48,6 +48,7 @@ import * as Entries from "./internal/entries.js"
48
48
  import * as Resume from "./internal/resume.js"
49
49
  import * as Run from "./internal/run.js"
50
50
  import * as Stack from "./internal/stack.js"
51
+ import { TiedFailures } from "./internal/ties.js"
51
52
  import type { Node } from "./internal/node.js"
52
53
  import { bounds, NodeId } from "./internal/node.js"
53
54
 
@@ -1785,19 +1786,48 @@ export const alt = <const Ps extends ReadonlyArray<Parser.Any>>(
1785
1786
  return described(
1786
1787
  (state: State<any>) => {
1787
1788
  const start = state.pos
1789
+ // The branches' failures merged left to right, as `mergeFailures` would,
1790
+ // except that the last two failures tied with `failure` wait in `tie1`
1791
+ // and `tie2` and are merged only when needed: when a third tie arrives,
1792
+ // when a succeeding branch stops where they failed (for the hint), or
1793
+ // when the merge is read from the state after `failTies`. An `alt` whose
1794
+ // later branch consumes input discards them unmerged.
1788
1795
  let failure: Failure | undefined
1796
+ let tie1: Failure | undefined
1797
+ let tie2: Failure | undefined
1789
1798
  for (let i = 0; i < n; i++) {
1790
1799
  const a = steps[i](state)
1791
1800
  if (a !== Fail) {
1792
- if (failure !== undefined && failure.offset === state.pos) state.setHint(failure.offset, failure.expected)
1801
+ if (failure !== undefined && failure.offset === state.pos) {
1802
+ let expected = failure.expected
1803
+ if (tie1 !== undefined) expected = mergeExpected(expected, tie1.expected)
1804
+ if (tie2 !== undefined) expected = mergeExpected(expected, tie2.expected)
1805
+ state.setHint(failure.offset, expected)
1806
+ }
1793
1807
  return a
1794
1808
  }
1795
1809
  if (state.mode !== "Backtrack") return Fail
1796
- failure = failure === undefined ? state.failure : mergeFailures(failure, state.failure)
1797
1810
  state.pos = start
1811
+ // The branch's failure, whose own ties (from a nested `alt`) can be taken over unmerged
1812
+ const head = state.failureHead
1813
+ if (failure === undefined || head.offset > failure.offset) {
1814
+ failure = head
1815
+ tie1 = state.failureTie1
1816
+ tie2 = state.failureTie2
1817
+ } else if (head.offset === failure.offset) {
1818
+ const f = state.failure
1819
+ if (tie1 === undefined) tie1 = f
1820
+ else if (tie2 === undefined) tie2 = f
1821
+ else {
1822
+ failure = mergeTiedFailures(failure, tie1, mergeExpected(failure.expected, tie1.expected))
1823
+ tie1 = tie2
1824
+ tie2 = f
1825
+ }
1826
+ }
1798
1827
  }
1799
1828
  // `failure` is set: there are at least two alternatives, and each one that fails sets it
1800
- return state.failWith("Backtrack", failure ?? state.failure)
1829
+ if (failure === undefined) return state.failWith("Backtrack", state.failure)
1830
+ return tie1 === undefined ? state.failWith("Backtrack", failure) : state.failTies(failure, tie1, tie2)
1801
1831
  },
1802
1832
  "alt",
1803
1833
  { _tag: "Alt", parsers }
@@ -1911,11 +1941,13 @@ export const permutation = <const Ps extends ReadonlyArray<Parser.Any>>(
1911
1941
  const steps = parsers.map((p) => p.step)
1912
1942
  const n = steps.length
1913
1943
  return make((state: State<any>) => {
1914
- const out = new Array(n)
1944
+ // A round's failures, merged only if no parser matches in it (#44)
1945
+ const ties = new TiedFailures()
1915
1946
  const done = new Array<boolean>(n).fill(false)
1947
+ const out = new Array(n)
1916
1948
  let remaining = n
1917
1949
  while (remaining > 0) {
1918
- let failure: Failure | undefined
1950
+ ties.clear()
1919
1951
  let matched = false
1920
1952
  for (let i = 0; i < n; i++) {
1921
1953
  if (done[i]) continue
@@ -1929,11 +1961,11 @@ export const permutation = <const Ps extends ReadonlyArray<Parser.Any>>(
1929
1961
  break
1930
1962
  }
1931
1963
  if (state.mode !== "Backtrack") return Fail
1932
- failure = failure === undefined ? state.failure : mergeFailures(failure, state.failure)
1933
1964
  state.pos = start
1965
+ ties.add(state)
1934
1966
  }
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)
1967
+ // Some parser is not done yet, and each one that fails is folded in
1968
+ if (!matched) return ties.fail(state)
1937
1969
  }
1938
1970
  return out as any
1939
1971
  }, "permutation")