@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.
@@ -7,6 +7,7 @@
7
7
  * engine's overflow error and report a `TooDeep` failure instead, keeping
8
8
  * their `Result` contract on untrusted input.
9
9
  */
10
+ import * as Predicate from "effect/Predicate"
10
11
  import type { Fail, State } from "../Input.js"
11
12
 
12
13
  /** The message of a `TooDeep` failure. */
@@ -19,7 +20,11 @@ const tooDeepMessage = "input is nested too deeply: parsing exhausted the call s
19
20
  */
20
21
  export const isStackOverflow = (e: unknown): boolean =>
21
22
  (e instanceof RangeError && /call stack/i.test(e.message)) ||
22
- (e instanceof Error && e.name === "InternalError" && /recursion/i.test(e.message))
23
+ (Predicate.hasProperty(e, "name") &&
24
+ Predicate.hasProperty(e, "message") &&
25
+ e.name === "InternalError" &&
26
+ Predicate.isString(e.message) &&
27
+ /recursion/i.test(e.message))
23
28
 
24
29
  /**
25
30
  * Runs `step`, turning a call-stack overflow into an unrecoverable
@@ -223,6 +223,11 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
223
223
  return super.failWith(mode, failure)
224
224
  }
225
225
 
226
+ override failTies(head: Failure, tie1: Failure, tie2: Failure | undefined): Fail {
227
+ if (head.offset > this.highWater) this.highWater = head.offset
228
+ return super.failTies(head, tie1, tie2)
229
+ }
230
+
226
231
  override setHint(offset: number, expected: ReadonlyArray<string>): void {
227
232
  if (offset > this.highWater) this.highWater = offset
228
233
  super.setHint(offset, expected)
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tied failures, merged only when read.
3
+ *
4
+ * A combinator that tries several parsers at the same position (an
5
+ * alternation, a permutation round, an operator table) reports the merge
6
+ * of their failures, left to right, as `mergeFailures` would. Most of the
7
+ * time one of the later attempts succeeds and the merge is thrown away, so
8
+ * {@link TiedFailures} keeps the last two failures tied with the running
9
+ * one unmerged, and {@link TiedFailures.fail} hands them to the state with
10
+ * `State.failTies`, which merges them on first read (#38, #44).
11
+ *
12
+ * `Parser.alt` does the same fold in locals, since it is the hottest
13
+ * combinator and cannot afford an accumulator per call.
14
+ */
15
+ import type { Failure, Fail, Input, State } from "../Input.js"
16
+ import { mergeExpected, mergeTiedFailures } from "../Input.js"
17
+
18
+ /** The failures of a round of attempts, folded lazily. */
19
+ export class TiedFailures {
20
+ /** The running failure, before the pending ties are merged into it. */
21
+ failure: Failure | undefined = undefined
22
+ /** The oldest pending tie, at `failure`'s offset. */
23
+ tie1: Failure | undefined = undefined
24
+ /** The newest pending tie, at `failure`'s offset. */
25
+ tie2: Failure | undefined = undefined
26
+
27
+ /** Forgets every failure, for a new round. */
28
+ clear(): void {
29
+ this.failure = undefined
30
+ this.tie1 = undefined
31
+ this.tie2 = undefined
32
+ }
33
+
34
+ /**
35
+ * Folds in the failure the latest attempt left on `state`, taking over
36
+ * its own unmerged ties when it is the furthest.
37
+ */
38
+ add<I extends Input>(state: State<I>): void {
39
+ const head = state.failureHead
40
+ const running = this.failure
41
+ if (running === undefined || head.offset > running.offset) {
42
+ this.failure = head
43
+ this.tie1 = state.failureTie1
44
+ this.tie2 = state.failureTie2
45
+ return
46
+ }
47
+ if (head.offset < running.offset) return
48
+ const failure = state.failure
49
+ const tie1 = this.tie1
50
+ if (tie1 === undefined) this.tie1 = failure
51
+ else if (this.tie2 === undefined) this.tie2 = failure
52
+ else {
53
+ // A third tie: fold the oldest pending one in, keeping the order
54
+ this.failure = mergeTiedFailures(running, tie1, mergeExpected(running.expected, tie1.expected))
55
+ this.tie1 = this.tie2
56
+ this.tie2 = failure
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Fails `state` with the folded failure (recoverably), or with its
62
+ * current failure when nothing was folded.
63
+ */
64
+ fail<I extends Input>(state: State<I>): Fail {
65
+ const failure = this.failure
66
+ if (failure === undefined) return state.failWith("Backtrack", state.failure)
67
+ const tie1 = this.tie1
68
+ return tie1 === undefined ? state.failWith("Backtrack", failure) : state.failTies(failure, tie1, this.tie2)
69
+ }
70
+
71
+ /**
72
+ * Records the folded expectations as a hint when the folded failure lies
73
+ * where `state` stopped (an attempt that fails without consuming input).
74
+ */
75
+ hint<I extends Input>(state: State<I>): void {
76
+ const failure = this.failure
77
+ if (failure === undefined || failure.offset !== state.pos) return
78
+ let expected = failure.expected
79
+ if (this.tie1 !== undefined) expected = mergeExpected(expected, this.tie1.expected)
80
+ if (this.tie2 !== undefined) expected = mergeExpected(expected, this.tie2.expected)
81
+ state.setHint(failure.offset, expected)
82
+ }
83
+ }