@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
@@ -14,23 +14,32 @@
14
14
  * completes and adopts them. Nodes built by an attempt that is later
15
15
  * abandoned must not end up in the tree. Every combinator that abandons an
16
16
  * attempt moves the cursor back to (or before) where the attempt started,
17
- * and every node has a positive width, so the nodes of an abandoned attempt
18
- * are exactly those that end after the position the cursor moves back to:
19
- * the `pos` setter of {@link SyntaxState} drops them. This also covers
20
- * lookahead (`peek`, `not`) and custom parsers written with `Parser.make`.
17
+ * with `State.rewind`, and every node has a positive width, so the nodes of
18
+ * an abandoned attempt are exactly those that end after the position the
19
+ * cursor moves back to: {@link SyntaxBuilder} drops them when told of the rewind. This also
20
+ * covers lookahead (`peek`, `not`) and custom parsers written with
21
+ * `Parser.make`, which rewind the same way.
22
+ *
23
+ * `pos` stays a plain field, not an accessor that would watch every move: a
24
+ * `pos` accessor on this subclass made every parser's cursor accesses
25
+ * polymorphic, slowing ordinary parses by about a fifth in any program that
26
+ * also built a syntax tree, and the syntax parse itself about twofold.
21
27
  *
22
28
  * For incremental parsing, every node records how far its parse looked: the
23
29
  * furthest position the cursor reached and the furthest failure recorded
24
- * while it was being parsed (its *lookahead*). A node from the previous tree
30
+ * while it was being parsed (its *lookahead*). The cursor only moves forward
31
+ * between rewinds, so the furthest position is where it was at a rewind, or
32
+ * where it is now. A node from the previous tree
25
33
  * is reused at a position when it was built by the same `Syntax.node`, and
26
34
  * the input it looked at, plus a margin for the tokens that read a little
27
35
  * past where they stop, is unchanged.
28
36
  */
37
+ import * as Arr from "effect/Array"
29
38
  import * as Predicate from "effect/Predicate"
30
- import type * as Context from "effect/Context"
31
39
  import type { Encoding } from "../Encoding.js"
32
- import type { ErrMode, Input, Source } from "../Input.js"
40
+ import type { ErrMode, Input } from "../Input.js"
33
41
  import { Fail, Failure, State } from "../Input.js"
42
+ import type { Key } from "./key.js"
34
43
 
35
44
  /** What a failure inside an `ERROR` node was. Offsets are relative to the node. */
36
45
  export interface ErrorInfo {
@@ -44,7 +53,7 @@ export class Green {
44
53
  readonly kind: string,
45
54
  readonly named: boolean,
46
55
  /** The `Syntax.node` that built it; `undefined` for roots and errors, which are never reused alone. */
47
- readonly key: object | undefined,
56
+ readonly key: Key | undefined,
48
57
  readonly width: number,
49
58
  /** How far its parse looked, relative to its start (at least `width`). */
50
59
  readonly lookahead: number,
@@ -103,7 +112,7 @@ export const absolute = (f: Failure, start: number): Failure => rebase(f, -start
103
112
  const build = (
104
113
  kind: string,
105
114
  named: boolean,
106
- key: object | undefined,
115
+ key: Key | undefined,
107
116
  start: number,
108
117
  end: number,
109
118
  lookahead: number,
@@ -153,7 +162,7 @@ export interface Previous {
153
162
 
154
163
  /** The hooks that `Syntax` combinators and `Parser.resumeAfter`/`retryAfter` call. */
155
164
  export interface Builder {
156
- node(state: State<any>, step: (state: State<any>) => unknown, key: object, kind: string, named: boolean): unknown
165
+ node(state: State<any>, step: (state: State<any>) => unknown, key: Key, kind: string, named: boolean): unknown
157
166
  field(state: State<any>, step: (state: State<any>) => unknown, name: string): unknown
158
167
  /** After error recovery: `[start, pos)` was skipped after `failure`. */
159
168
  skipped(start: number, failure: Failure, mode: ErrMode): void
@@ -162,74 +171,61 @@ export interface Builder {
162
171
  * where they leave the cursor or record a failure.
163
172
  */
164
173
  read(end: number): void
174
+ /** A failure or hint was recorded at `offset`. */
175
+ reach(offset: number): void
176
+ /** The cursor moved back from `from` to `to` (see `State.rewind`). */
177
+ rewound(from: number, to: number): void
165
178
  }
166
179
 
167
- /** The parse state of a `Syntax` parse. */
168
- export class SyntaxState<I extends Input = Input> extends State<I> implements Builder {
169
- // Declared, not defined: `State`'s constructor sets `pos` through the
170
- // accessor below before these could be initialized
171
- declare cursor: number
172
- /** The furthest position the cursor reached, or failure recorded, since the innermost open node started. */
173
- declare highWater: number
180
+ /**
181
+ * The tree builder of a `Syntax` parse, reached through `state.syntax`. It
182
+ * is not a `State` subclass: one class of state keeps every parser's cursor
183
+ * and failure accesses monomorphic, whether or not the program also builds
184
+ * syntax trees. `State` calls {@link reach} and {@link rewound} itself.
185
+ */
186
+ export class SyntaxBuilder implements Builder {
187
+ /**
188
+ * The furthest position the cursor reached before a rewind, or failure
189
+ * recorded, since the innermost open node started; {@link looked} adds
190
+ * where the cursor is now.
191
+ */
192
+ highWater: number
174
193
  /** Completed nodes waiting for their parent, in input order. */
175
- declare stack: Array<Child>
176
- declare previous: Previous | undefined
194
+ readonly stack: Array<Child> = []
177
195
  /** How many nodes were reused from the previous tree. */
178
- declare reused: number
196
+ reused = 0
179
197
  /** Where {@link find} found the last node it found. */
180
- declare lastParent: Green | undefined
181
- declare lastParentStart: number
182
-
183
- static {
184
- Object.defineProperty(this.prototype, "pos", {
185
- get(this: SyntaxState) {
186
- return this.cursor
187
- },
188
- set(this: SyntaxState, p: number) {
189
- if (p < this.cursor) {
190
- // An attempt was abandoned (or a lookahead finished): drop its nodes
191
- const stack = this.stack
192
- while (stack.length > 0 && stack[stack.length - 1].end > p) stack.pop()
193
- }
194
- if (p > this.highWater) this.highWater = p
195
- this.cursor = p
196
- },
197
- configurable: true,
198
- enumerable: true
199
- })
200
- }
198
+ lastParent: Green | undefined = undefined
199
+ lastParentStart = 0
201
200
 
202
201
  constructor(
203
- source: I | Source<I>,
204
- options: {
205
- readonly context?: Context.Context<never> | undefined
206
- },
207
- previous: Previous | undefined
202
+ readonly state: State<any>,
203
+ readonly previous: Previous | undefined
208
204
  ) {
209
- super(source, { context: options.context })
210
- this.highWater = this.cursor
211
- this.stack = []
212
- this.previous = previous
213
- this.reused = 0
214
- this.lastParent = undefined
215
- this.lastParentStart = 0
216
- this.syntax = this
205
+ this.highWater = state.pos
206
+ state.syntax = this
217
207
  // Error recovery is always on: like Tree-sitter, a syntax parse always produces a tree
218
- this.recovered = []
208
+ state.recovered = []
219
209
  }
220
210
 
221
- override failWith(mode: ErrMode, failure: Failure): Fail {
222
- if (failure.offset > this.highWater) this.highWater = failure.offset
223
- return super.failWith(mode, failure)
211
+ reach(offset: number): void {
212
+ if (offset > this.highWater) this.highWater = offset
224
213
  }
225
214
 
226
- override setHint(offset: number, expected: ReadonlyArray<string>): void {
227
- if (offset > this.highWater) this.highWater = offset
228
- super.setHint(offset, expected)
215
+ rewound(from: number, to: number): void {
216
+ if (from > this.highWater) this.highWater = from
217
+ // An attempt was abandoned (or a lookahead finished): drop its nodes
218
+ const stack = this.stack
219
+ while (Arr.isArrayNonEmpty(stack) && Arr.lastNonEmpty(stack).end > to) stack.pop()
220
+ }
221
+
222
+ /** How far the parse looked: {@link highWater}, or the cursor if it is further. */
223
+ private looked(): number {
224
+ return this.state.pos > this.highWater ? this.state.pos : this.highWater
229
225
  }
230
226
 
231
- node(state: State<any>, step: (state: State<any>) => unknown, key: object, kind: string, named: boolean): unknown {
232
- const start = this.cursor
227
+ node(state: State<any>, step: (state: State<any>) => unknown, key: Key, kind: string, named: boolean): unknown {
228
+ const start = this.state.pos
233
229
  const stack = this.stack
234
230
  if (this.previous !== undefined) {
235
231
  const green = this.reusable(this.previous, key, start)
@@ -242,7 +238,7 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
242
238
  const d = green.diagnostics
243
239
  if (d !== undefined) {
244
240
  if (d.deepest !== undefined) this.deepen(start === d.base ? d.deepest : rebase(d.deepest, d.base - start))
245
- if (d.hintExpected !== undefined) this.setHint(start + d.hintOffset, d.hintExpected)
241
+ if (d.hintExpected !== undefined) this.state.setHint(start + d.hintOffset, d.hintExpected)
246
242
  }
247
243
  stack.push(new Child(green, start, end, undefined))
248
244
  return green.value
@@ -250,34 +246,34 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
250
246
  }
251
247
  const mark = stack.length
252
248
  const outer = this.highWater
253
- const outerDeepest = this.deepest
254
- const hintOffset = this.hintOffset
255
- const hintExpected = this.hintExpected
249
+ const outerDeepest = this.state.deepest
250
+ const hintOffset = this.state.hintOffset
251
+ const hintExpected = this.state.hintExpected
256
252
  this.highWater = start
257
253
  // Collect the furthest failure of this node alone, then merge it as `failWith` would have
258
- this.deepest = undefined
254
+ this.state.deepest = undefined
259
255
  const a = step(state)
260
- const looked = this.highWater
261
- if (outer > looked) this.highWater = outer
262
- const deepest = this.deepest as Failure | undefined
263
- this.deepest = outerDeepest
256
+ const looked = this.looked()
257
+ this.highWater = outer > looked ? outer : looked
258
+ const deepest = this.state.deepest as Failure | undefined
259
+ this.state.deepest = outerDeepest
264
260
  if (deepest !== undefined) this.deepen(deepest)
265
261
  if (a === Fail) {
266
262
  if (stack.length > mark) stack.length = mark
267
263
  return a
268
264
  }
269
- const end = this.cursor
265
+ const end = this.state.pos
270
266
  // Like Tree-sitter's rules, nodes are never empty: an empty match adds nothing to the tree
271
267
  if (end === start) return a
272
- const hinted = this.hintOffset !== hintOffset || this.hintExpected !== hintExpected
268
+ const hinted = this.state.hintOffset !== hintOffset || this.state.hintExpected !== hintExpected
273
269
  const diagnostics: Diagnostics | undefined =
274
270
  deepest === undefined && !hinted
275
271
  ? undefined
276
272
  : {
277
273
  deepest,
278
274
  base: start,
279
- hintOffset: this.hintOffset - start,
280
- hintExpected: hinted ? this.hintExpected : undefined
275
+ hintOffset: this.state.hintOffset - start,
276
+ hintExpected: hinted ? this.state.hintExpected : undefined
281
277
  }
282
278
  const entries = stack.splice(mark)
283
279
  stack.push(
@@ -293,7 +289,7 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
293
289
 
294
290
  /** Records `failure` as the furthest failure if it is, as `failWith` does. */
295
291
  private deepen(failure: Failure): void {
296
- if (this.deepest === undefined || failure.offset >= this.deepest.offset) this.deepest = failure
292
+ if (this.state.deepest === undefined || failure.offset >= this.state.deepest.offset) this.state.deepest = failure
297
293
  }
298
294
 
299
295
  field(state: State<any>, step: (state: State<any>) => unknown, name: string): unknown {
@@ -308,8 +304,8 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
308
304
  const stack = this.stack
309
305
  let i = stack.length
310
306
  while (i > 0 && stack[i - 1].start >= start && stack[i - 1].end > start) i--
311
- const end = this.cursor
312
- const green = build(errorKind, true, undefined, start, end, this.highWater, stack.splice(i), undefined, {
307
+ const end = this.state.pos
308
+ const green = build(errorKind, true, undefined, start, end, this.looked(), stack.splice(i), undefined, {
313
309
  failure: rebase(failure, start),
314
310
  mode
315
311
  })
@@ -323,9 +319,9 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
323
319
  */
324
320
  finish(kind: string, value: unknown, fatal: ErrorInfo | undefined, errorStart: number): Green {
325
321
  const entries = this.stack.splice(0)
326
- const end = this.end
322
+ const end = this.state.end
327
323
  if (fatal !== undefined) {
328
- const last = entries.length > 0 ? entries[entries.length - 1].end : 0
324
+ const last = Arr.isArrayNonEmpty(entries) ? Arr.lastNonEmpty(entries).end : 0
329
325
  const start = Math.max(Math.min(errorStart, end), last)
330
326
  const error = build(errorKind, true, undefined, start, end, end, [], undefined, {
331
327
  failure: rebase(fatal.failure, start),
@@ -333,7 +329,7 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
333
329
  })
334
330
  entries.push(new Child(error, start, end, undefined))
335
331
  }
336
- return build(kind, true, undefined, 0, end, Math.max(end, this.highWater), entries, value, undefined)
332
+ return build(kind, true, undefined, 0, end, Math.max(end, this.looked()), entries, value, undefined)
337
333
  }
338
334
 
339
335
  /**
@@ -342,7 +338,7 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
342
338
  * so the search starts from the parent of the last one found when
343
339
  * `offset` lies inside it: no ancestor of that parent can start there.
344
340
  */
345
- private find(root: Green, offset: number, key: object): Green | undefined {
341
+ private find(root: Green, offset: number, key: Key): Green | undefined {
346
342
  let node = root
347
343
  let start = 0
348
344
  const last = this.lastParent
@@ -380,7 +376,7 @@ export class SyntaxState<I extends Input = Input> extends State<I> implements Bu
380
376
  }
381
377
 
382
378
  /** The node of the previous tree built by `key` at `pos` (in the new input), if it can be reused. */
383
- private reusable(previous: Previous, key: object, pos: number): Green | undefined {
379
+ private reusable(previous: Previous, key: Key, pos: number): Green | undefined {
384
380
  const segments = previous.segments
385
381
  // The segment containing `pos`
386
382
  let lo = 0
@@ -1,3 +1,6 @@
1
+ import * as Arr from "effect/Array"
2
+ import * as Predicate from "effect/Predicate"
3
+ import * as Str from "effect/String"
1
4
  import type { Encoding } from "../Encoding.js"
2
5
  import type { Fail, State } from "../Input.js"
3
6
 
@@ -22,6 +25,17 @@ export const End = -3
22
25
  */
23
26
  const FastLimit: unique symbol = /*#__PURE__*/ Symbol.for("effect-parse/Encoding/FastLimit")
24
27
 
28
+ /**
29
+ * Whether an input, a literal or a chunk has no units (characters, bytes or
30
+ * tokens).
31
+ */
32
+ export const isEmptyUnits = (units: string | Uint8Array | ReadonlyArray<unknown>): boolean =>
33
+ Predicate.isString(units)
34
+ ? Str.isEmpty(units)
35
+ : units instanceof Uint8Array
36
+ ? units.byteLength === 0
37
+ : Arr.isReadonlyArrayEmpty(units)
38
+
25
39
  /** @internal */
26
40
  export const fastLimit = (enc: Encoding<any>): number =>
27
41
  (enc as any)[FastLimit] ?? (enc.asciiCompatible && enc.unit === 1 ? 0x80 : 0)
@@ -115,6 +129,7 @@ const decodeRange = (decoder: TextDecoder, b: Uint8Array, start: number, end: nu
115
129
  const utf8Encoder = /*#__PURE__*/ new TextEncoder()
116
130
 
117
131
  /** UTF-8 bytes. */
132
+ // SAFETY: an `Encoding` with the internal `FastLimit` key, which its type doesn't declare
118
133
  export const utf8: Encoding<Uint8Array> = {
119
134
  name: "UTF-8",
120
135
  unit: 1,
@@ -137,13 +152,17 @@ export const utf8: Encoding<Uint8Array> = {
137
152
  }
138
153
  }
139
154
  return out
140
- }
141
- }
155
+ },
156
+ // Every built-in encoding has the same keys in the same order, so code
157
+ // reading them sees one object shape
158
+ [FastLimit]: 0x80
159
+ } as Encoding<Uint8Array>
142
160
 
143
161
  const makeUtf16 = (littleEndian: boolean): Encoding<Uint8Array> => {
144
162
  const lo = littleEndian ? 0 : 1
145
163
  const hi = 1 - lo
146
164
  let decoder: TextDecoder | undefined
165
+ // SAFETY: an `Encoding` with the internal `FastLimit` key, which its type doesn't declare
147
166
  return {
148
167
  name: littleEndian ? "UTF-16LE" : "UTF-16BE",
149
168
  unit: 2,
@@ -176,8 +195,9 @@ const makeUtf16 = (littleEndian: boolean): Encoding<Uint8Array> => {
176
195
  }
177
196
  decoder ??= new TextDecoder(littleEndian ? "utf-16le" : "utf-16be", { ignoreBOM: true })
178
197
  return decodeRange(decoder, b, start, end)
179
- }
180
- }
198
+ },
199
+ [FastLimit]: 0
200
+ } as Encoding<Uint8Array>
181
201
  }
182
202
 
183
203
  /** UTF-16 bytes, little endian. */
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  // fallow-ignore-file duplicate-export -- `End` and `scan` here are the token-set versions of the text
2
2
  // helpers in ./text.ts; both are internal modules, imported by path, never re-exported from one barrel.
3
3
  import * as Predicate from "effect/Predicate"
4
+ import * as Result from "effect/Result"
4
5
  import type { Input, State } from "../Input.js"
5
6
  import { charOf, firstCodePoint } from "./text.js"
6
7
 
@@ -160,11 +161,10 @@ const compileCodeSafe = (set: unknown, kind: "string" | "code"): ((code: number)
160
161
  return false
161
162
  }
162
163
  }
163
- try {
164
- return compileCode(set)
165
- } catch {
166
- return () => false
167
- }
164
+ return Result.getOrElse(
165
+ Result.try(() => compileCode(set)),
166
+ (): ((code: number) => boolean) => () => false
167
+ )
168
168
  }
169
169
 
170
170
  /**
@@ -276,17 +276,18 @@ export const nextIf = (state: State<Input>, m: AnyMatcher, negate: boolean): unk
276
276
  }
277
277
  }
278
278
 
279
+ /** How far a {@link scan} got. */
280
+ export interface Scanned extends Advanced {
281
+ /** Whether the end of input was hit before `max` tokens. */
282
+ readonly hitEnd: boolean
283
+ }
284
+
279
285
  /**
280
286
  * Scans forward while tokens satisfy the matcher (xor `negate`), up to `max`
281
287
  * tokens. Returns the offset reached, the number of tokens matched, and
282
288
  * whether the end of input was hit before `max` tokens.
283
289
  */
284
- export const scan = (
285
- state: State<Input>,
286
- m: AnyMatcher,
287
- negate: boolean,
288
- max: number
289
- ): { readonly offset: number; readonly count: number; readonly hitEnd: boolean } => {
290
+ export const scan = (state: State<Input>, m: AnyMatcher, negate: boolean, max: number): Scanned => {
290
291
  let i = state.pos
291
292
  const end = state.end
292
293
  let count = 0
@@ -335,8 +336,14 @@ export const scan = (
335
336
  }
336
337
  }
337
338
 
339
+ /** The offset reached and the number of tokens passed. */
340
+ export interface Advanced {
341
+ readonly offset: number
342
+ readonly count: number
343
+ }
344
+
338
345
  /** Advances `n` tokens from `start`, returning the offset reached and the count. */
339
- export const advance = (state: State<Input>, start: number, n: number): { offset: number; count: number } => {
346
+ export const advance = (state: State<Input>, start: number, n: number): Advanced => {
340
347
  if (state.inputKind !== 0) {
341
348
  const offset = Math.min(state.end, start + n)
342
349
  return { offset, count: offset - start }
@@ -379,7 +386,7 @@ export const indexOf = (state: State<Input>, needle: Input, from: number): numbe
379
386
  case 1: {
380
387
  const hay = state.input as Uint8Array
381
388
  const n = needle as Uint8Array
382
- if (n.length === 0) return from
389
+ if (n.byteLength === 0) return from
383
390
  const first = n[0]
384
391
  let i = hay.indexOf(first, from)
385
392
  while (i !== -1 && i <= last) {