@thomasfosterau/effect-parse 0.3.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 (137) hide show
  1. package/dist/Ascii.d.ts +0 -2
  2. package/dist/Ascii.d.ts.map +1 -1
  3. package/dist/Ascii.js +6 -6
  4. package/dist/Ascii.js.map +1 -1
  5. package/dist/Binary.d.ts +0 -2
  6. package/dist/Binary.d.ts.map +1 -1
  7. package/dist/Binary.js +1 -2
  8. package/dist/Bits.d.ts +0 -2
  9. package/dist/Bits.d.ts.map +1 -1
  10. package/dist/Bits.js +2 -3
  11. package/dist/Bits.js.map +1 -1
  12. package/dist/Compiler.d.ts +0 -2
  13. package/dist/Compiler.d.ts.map +1 -1
  14. package/dist/Compiler.js +1 -2
  15. package/dist/Encoding.d.ts +0 -2
  16. package/dist/Encoding.d.ts.map +1 -1
  17. package/dist/Encoding.js +1 -2
  18. package/dist/Expression.d.ts +0 -2
  19. package/dist/Expression.d.ts.map +1 -1
  20. package/dist/Expression.js +44 -8
  21. package/dist/Expression.js.map +1 -1
  22. package/dist/Input.d.ts +32 -4
  23. package/dist/Input.d.ts.map +1 -1
  24. package/dist/Input.js +47 -8
  25. package/dist/Input.js.map +1 -1
  26. package/dist/Nom.d.ts +0 -2
  27. package/dist/Nom.d.ts.map +1 -1
  28. package/dist/Nom.js +1 -2
  29. package/dist/ParseError.d.ts +0 -2
  30. package/dist/ParseError.d.ts.map +1 -1
  31. package/dist/ParseError.js +6 -9
  32. package/dist/ParseError.js.map +1 -1
  33. package/dist/Parser.d.ts +2 -2
  34. package/dist/Parser.d.ts.map +1 -1
  35. package/dist/Parser.js +26 -24
  36. package/dist/Parser.js.map +1 -1
  37. package/dist/ParserSchema.d.ts +0 -2
  38. package/dist/ParserSchema.d.ts.map +1 -1
  39. package/dist/ParserSchema.js +2 -3
  40. package/dist/ParserStream.d.ts +0 -2
  41. package/dist/ParserStream.d.ts.map +1 -1
  42. package/dist/ParserStream.js +15 -9
  43. package/dist/ParserStream.js.map +1 -1
  44. package/dist/Syntax.d.ts +0 -2
  45. package/dist/Syntax.d.ts.map +1 -1
  46. package/dist/Syntax.js +13 -11
  47. package/dist/Syntax.js.map +1 -1
  48. package/dist/Text.d.ts +0 -2
  49. package/dist/Text.d.ts.map +1 -1
  50. package/dist/Text.js +1 -2
  51. package/dist/Token.d.ts +0 -2
  52. package/dist/Token.d.ts.map +1 -1
  53. package/dist/Token.js +1 -2
  54. package/dist/internal/compiler/closure.d.ts +5 -2
  55. package/dist/internal/compiler/closure.d.ts.map +1 -1
  56. package/dist/internal/compiler/closure.js +160 -92
  57. package/dist/internal/compiler/closure.js.map +1 -1
  58. package/dist/internal/compiler/entries.d.ts +0 -2
  59. package/dist/internal/compiler/entries.d.ts.map +1 -1
  60. package/dist/internal/compiler/entries.js +6 -7
  61. package/dist/internal/compiler/entries.js.map +1 -1
  62. package/dist/internal/compiler/first.d.ts +0 -2
  63. package/dist/internal/compiler/first.d.ts.map +1 -1
  64. package/dist/internal/compiler/first.js +10 -10
  65. package/dist/internal/compiler/first.js.map +1 -1
  66. package/dist/internal/compiler/jit.d.ts +0 -2
  67. package/dist/internal/compiler/jit.d.ts.map +1 -1
  68. package/dist/internal/compiler/jit.js +52 -51
  69. package/dist/internal/compiler/jit.js.map +1 -1
  70. package/dist/internal/compiler/registry.d.ts +0 -2
  71. package/dist/internal/compiler/registry.d.ts.map +1 -1
  72. package/dist/internal/compiler/registry.js +1 -2
  73. package/dist/internal/compiler/sets.d.ts +2 -2
  74. package/dist/internal/compiler/sets.d.ts.map +1 -1
  75. package/dist/internal/compiler/sets.js +8 -3
  76. package/dist/internal/compiler/sets.js.map +1 -1
  77. package/dist/internal/entries.d.ts +0 -2
  78. package/dist/internal/entries.d.ts.map +1 -1
  79. package/dist/internal/entries.js +1 -2
  80. package/dist/internal/key.d.ts +10 -0
  81. package/dist/internal/key.d.ts.map +1 -0
  82. package/dist/internal/key.js +11 -0
  83. package/dist/internal/key.js.map +1 -0
  84. package/dist/internal/node.d.ts +11 -4
  85. package/dist/internal/node.d.ts.map +1 -1
  86. package/dist/internal/node.js +15 -3
  87. package/dist/internal/node.js.map +1 -1
  88. package/dist/internal/numbers.d.ts +0 -2
  89. package/dist/internal/numbers.d.ts.map +1 -1
  90. package/dist/internal/numbers.js +1 -2
  91. package/dist/internal/resume.d.ts +5 -6
  92. package/dist/internal/resume.d.ts.map +1 -1
  93. package/dist/internal/resume.js +1 -2
  94. package/dist/internal/resume.js.map +1 -1
  95. package/dist/internal/run.d.ts +0 -2
  96. package/dist/internal/run.d.ts.map +1 -1
  97. package/dist/internal/run.js +2 -3
  98. package/dist/internal/run.js.map +1 -1
  99. package/dist/internal/stack.d.ts +0 -2
  100. package/dist/internal/stack.d.ts.map +1 -1
  101. package/dist/internal/stack.js +1 -2
  102. package/dist/internal/syntax.d.ts +30 -20
  103. package/dist/internal/syntax.d.ts.map +1 -1
  104. package/dist/internal/syntax.js +73 -66
  105. package/dist/internal/syntax.js.map +1 -1
  106. package/dist/internal/text.d.ts +5 -2
  107. package/dist/internal/text.d.ts.map +1 -1
  108. package/dist/internal/text.js +14 -5
  109. package/dist/internal/text.js.map +1 -1
  110. package/dist/internal/ties.d.ts +0 -2
  111. package/dist/internal/ties.d.ts.map +1 -1
  112. package/dist/internal/ties.js +1 -2
  113. package/dist/internal/tokens.d.ts +10 -9
  114. package/dist/internal/tokens.d.ts.map +1 -1
  115. package/dist/internal/tokens.js +4 -8
  116. package/dist/internal/tokens.js.map +1 -1
  117. package/package.json +6 -6
  118. package/src/Ascii.ts +6 -5
  119. package/src/Bits.ts +1 -1
  120. package/src/Expression.ts +7 -6
  121. package/src/Input.ts +50 -11
  122. package/src/ParseError.ts +9 -7
  123. package/src/Parser.ts +26 -23
  124. package/src/ParserStream.ts +20 -8
  125. package/src/Syntax.ts +12 -9
  126. package/src/internal/compiler/closure.ts +202 -90
  127. package/src/internal/compiler/entries.ts +14 -11
  128. package/src/internal/compiler/first.ts +9 -8
  129. package/src/internal/compiler/jit.ts +95 -74
  130. package/src/internal/compiler/sets.ts +7 -0
  131. package/src/internal/key.ts +11 -0
  132. package/src/internal/node.ts +19 -2
  133. package/src/internal/resume.ts +5 -4
  134. package/src/internal/run.ts +1 -1
  135. package/src/internal/syntax.ts +78 -87
  136. package/src/internal/text.ts +24 -4
  137. package/src/internal/tokens.ts +20 -13
package/src/Parser.ts CHANGED
@@ -45,6 +45,7 @@ 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"
@@ -273,6 +274,8 @@ const isTwoParserArgs = (args: IArguments) => isParser(args[0]) && isParser(args
273
274
  * The step function must either return the parsed value, having advanced
274
275
  * `state.pos` past the consumed input, or return the result of one of
275
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`.
276
279
  *
277
280
  * @example
278
281
  * ```ts
@@ -723,7 +726,7 @@ export const tryMap: {
723
726
  if (a === Fail) return Fail
724
727
  const r = f(a)
725
728
  if (Result.isSuccess(r)) return r.success
726
- state.pos = start
729
+ state.rewind(start)
727
730
  return state.failCause("Backtrack", "Verify", r.failure)
728
731
  },
729
732
  self.name,
@@ -777,7 +780,7 @@ export const filterMap: {
777
780
  if (a === Fail) return Fail
778
781
  const o = f(a)
779
782
  if (Option.isSome(o)) return o.value
780
- state.pos = start
783
+ state.rewind(start)
781
784
  return state.backtrack("Verify")
782
785
  },
783
786
  self.name,
@@ -808,7 +811,7 @@ export const filter: {
808
811
  const a = step(state)
809
812
  if (a === Fail) return Fail
810
813
  if (predicate(a)) return a
811
- state.pos = start
814
+ state.rewind(start)
812
815
  return state.backtrack("Verify")
813
816
  },
814
817
  self.name,
@@ -852,7 +855,7 @@ export const filterOrFail: {
852
855
  const a = step(state)
853
856
  if (a === Fail) return Fail
854
857
  if (predicate(a)) return a
855
- state.pos = start
858
+ state.rewind(start)
856
859
  return state.failCause("Backtrack", "Verify", orFailWith(a))
857
860
  },
858
861
  self.name,
@@ -1032,7 +1035,7 @@ export const complete = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Pa
1032
1035
  const start = state.pos
1033
1036
  const a = step(state)
1034
1037
  if (a === Fail && state.mode === "Incomplete") {
1035
- state.pos = start
1038
+ state.rewind(start)
1036
1039
  return state.backtrack("Complete")
1037
1040
  }
1038
1041
  return a
@@ -1223,7 +1226,7 @@ export const orElseSucceed: {
1223
1226
 
1224
1227
  /** Rewinds after a recoverable failure, remembering it as a hint. */
1225
1228
  const recover = (state: State<any>, start: number): void => {
1226
- state.pos = start
1229
+ state.rewind(start)
1227
1230
  const f = state.failure
1228
1231
  if (f.offset === start) {
1229
1232
  state.hintOffset = start
@@ -1297,7 +1300,7 @@ export const peek = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Parser
1297
1300
  if (a !== Fail) {
1298
1301
  // Failures inside a lookahead that succeeded are about input that was
1299
1302
  // never consumed, so they are not reported as the furthest failure
1300
- state.pos = start
1303
+ state.rewind(start)
1301
1304
  state.hintOffset = hintOffset
1302
1305
  state.hintExpected = hintExpected
1303
1306
  state.deepest = deepest
@@ -1327,12 +1330,12 @@ export const not = <A, E, R, I extends Input>(self: Parser<A, E, R, I>): Parser<
1327
1330
  const a = step(state)
1328
1331
  if (a !== Fail) {
1329
1332
  // The failures inside `self` are not this parser's failure
1330
- state.pos = start
1333
+ state.rewind(start)
1331
1334
  state.deepest = deepest
1332
1335
  return state.backtrack("Not")
1333
1336
  }
1334
1337
  if (state.mode !== "Backtrack") return Fail
1335
- state.pos = start
1338
+ state.rewind(start)
1336
1339
  state.hintOffset = hintOffset
1337
1340
  state.hintExpected = hintExpected
1338
1341
  state.deepest = deepest
@@ -1807,7 +1810,7 @@ export const alt = <const Ps extends ReadonlyArray<Parser.Any>>(
1807
1810
  return a
1808
1811
  }
1809
1812
  if (state.mode !== "Backtrack") return Fail
1810
- state.pos = start
1813
+ state.rewind(start)
1811
1814
  // The branch's failure, whose own ties (from a nested `alt`) can be taken over unmerged
1812
1815
  const head = state.failureHead
1813
1816
  if (failure === undefined || head.offset > failure.offset) {
@@ -1906,7 +1909,7 @@ export const dispatch: {
1906
1909
  if (key === Fail) return Fail
1907
1910
  const parser = choose(key)
1908
1911
  if (parser === undefined) {
1909
- state.pos = start
1912
+ state.rewind(start)
1910
1913
  return state.backtrack("Dispatch")
1911
1914
  }
1912
1915
  return parser.step(state)
@@ -1961,7 +1964,7 @@ export const permutation = <const Ps extends ReadonlyArray<Parser.Any>>(
1961
1964
  break
1962
1965
  }
1963
1966
  if (state.mode !== "Backtrack") return Fail
1964
- state.pos = start
1967
+ state.rewind(start)
1965
1968
  ties.add(state)
1966
1969
  }
1967
1970
  // Some parser is not done yet, and each one that fails is folded in
@@ -1995,7 +1998,7 @@ interface FoldCheckpoint<B> {
1995
1998
  readonly pos: number
1996
1999
  }
1997
2000
 
1998
- 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 =>
1999
2002
  Resume.save(state, key, entry, { acc, count: 0, pos } satisfies FoldCheckpoint<B>)
2000
2003
 
2001
2004
  const foldImpl = <A, B, E, R, I extends Input>(
@@ -2010,7 +2013,7 @@ const foldImpl = <A, B, E, R, I extends Input>(
2010
2013
  const [min, max] = normalizeRange(range)
2011
2014
  const step = self.step
2012
2015
  const checkLoop = max === Infinity
2013
- const key = {}
2016
+ const key = new Key()
2014
2017
  return described(
2015
2018
  (state) => {
2016
2019
  const entry = state.pos
@@ -2189,7 +2192,7 @@ export const repeatTill: {
2189
2192
  if (b !== Fail) return [acc, b]
2190
2193
  if (state.mode !== "Backtrack") return Fail
2191
2194
  endFailure = state.failure
2192
- state.pos = start
2195
+ state.rewind(start)
2193
2196
  if (acc.length >= max) return Fail
2194
2197
  }
2195
2198
  const a = step(state)
@@ -2252,7 +2255,7 @@ export const separated: {
2252
2255
  const step = self.step
2253
2256
  const sepStep = separator.step
2254
2257
  const checkLoop = max === Infinity
2255
- const key = {}
2258
+ const key = new Key()
2256
2259
  return described(
2257
2260
  (state) => {
2258
2261
  const entry = state.pos
@@ -2295,7 +2298,7 @@ export const separated: {
2295
2298
  Resume.save(state, key, entry, { acc, pos: start } satisfies SeparatedCheckpoint<A>)
2296
2299
  }
2297
2300
  if (state.mode !== "Backtrack" || acc.length < min) return Fail
2298
- state.pos = start
2301
+ state.rewind(start)
2299
2302
  return Resume.done(state, key, entry, resumed !== undefined, acc)
2300
2303
  }
2301
2304
  acc.push(a)
@@ -2336,7 +2339,7 @@ export const separatedFoldl1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2336
2339
  ): Parser<A, E | E2, R | R2, I & I2> => {
2337
2340
  const step = self.step
2338
2341
  const sepStep = separator.step
2339
- const key = {}
2342
+ const key = new Key()
2340
2343
  return described(
2341
2344
  (state) => {
2342
2345
  const entry = state.pos
@@ -2367,7 +2370,7 @@ export const separatedFoldl1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2367
2370
  if (a === Fail) {
2368
2371
  if (state.mode === "Incomplete" && state.resume !== undefined) saveFold(state, key, entry, acc, start)
2369
2372
  if (state.mode !== "Backtrack") return Fail
2370
- state.pos = start
2373
+ state.rewind(start)
2371
2374
  return Resume.done(state, key, entry, resumed !== undefined, acc as A)
2372
2375
  }
2373
2376
  acc = op(acc, s, a)
@@ -2412,7 +2415,7 @@ export const separatedFoldr1 = <A, E, R, I extends Input, S, E2, R2, I2 extends
2412
2415
  const a = step(state)
2413
2416
  if (a === Fail) {
2414
2417
  if (state.mode !== "Backtrack") return Fail
2415
- state.pos = start
2418
+ state.rewind(start)
2416
2419
  break
2417
2420
  }
2418
2421
  seps.push(s)
@@ -2584,7 +2587,7 @@ export const resumeAfter: {
2584
2587
  if (state.recovered === undefined || state.mode === "Incomplete") return Fail
2585
2588
  const failure = state.failure
2586
2589
  const mode = state.mode
2587
- state.pos = start
2590
+ state.rewind(start)
2588
2591
  if (recoverStep(state) === Fail) return state.failWith(mode, failure)
2589
2592
  state.recovered.push(failure)
2590
2593
  state.syntax?.skipped(start, failure, mode)
@@ -2631,7 +2634,7 @@ export const retryAfter: {
2631
2634
  if (state.recovered === undefined || state.mode === "Incomplete") return Fail
2632
2635
  const failure = state.failure
2633
2636
  const mode = state.mode
2634
- state.pos = start
2637
+ state.rewind(start)
2635
2638
  if (recoverStep(state) === Fail || state.pos === start) return state.failWith(mode, failure)
2636
2639
  state.recovered.push(failure)
2637
2640
  state.syntax?.skipped(start, failure, mode)
@@ -2708,7 +2711,7 @@ const preview = (state: State<any>, offset: number): string => {
2708
2711
  */
2709
2712
  export const consoleTracer = (event: TraceEvent): void => {
2710
2713
  const indent = " ".repeat(event.depth)
2711
- if (event._tag === "Enter") {
2714
+ if (Predicate.isTagged(event, "Enter")) {
2712
2715
  console.log(`${indent}> ${event.name} @${event.offset} | ${preview(event.state, event.offset)}`)
2713
2716
  } else {
2714
2717
  const consumed = (event.endOffset ?? event.offset) - event.offset
@@ -36,6 +36,7 @@ import * as Registry from "./internal/compiler/registry.js"
36
36
  import type * as Resume from "./internal/resume.js"
37
37
  import * as Run from "./internal/run.js"
38
38
  import * as Stack from "./internal/stack.js"
39
+ import { isEmptyUnits } from "./internal/text.js"
39
40
  import type { Parser } from "./Parser.js"
40
41
 
41
42
  /**
@@ -49,7 +50,7 @@ export type Chunkable = string | Uint8Array | ReadonlyArray<unknown>
49
50
  const concat = <I extends Chunkable>(a: I, b: I): I => {
50
51
  if (Predicate.isString(a)) return (a + (b as string)) as I
51
52
  if (a instanceof Uint8Array) {
52
- if (a.length === 0) return b
53
+ if (a.byteLength === 0) return b
53
54
  const out = new Uint8Array(a.length + (b as Uint8Array).length)
54
55
  out.set(a)
55
56
  out.set(b as Uint8Array, a.length)
@@ -174,6 +175,7 @@ class Incremental<A, E, I extends Chunkable> {
174
175
  /** The input length at the last attempt, and where that attempt ran out of input. */
175
176
  private attempted = 0
176
177
  private restart = 0
178
+ private pushed = 0
177
179
  /** Set once an attempt settles: a success (with its end offset) or a definite failure. */
178
180
  private settled: { readonly value: A; readonly pos: number } | "failed" | undefined
179
181
 
@@ -221,9 +223,14 @@ class Incremental<A, E, I extends Chunkable> {
221
223
  }
222
224
 
223
225
  push(chunk: I): void {
224
- if (chunk.length === 0) return
226
+ if (isEmptyUnits(chunk)) return
225
227
  this.append(chunk)
226
228
  if (this.settled !== undefined) return
229
+ // The first attempt waits for a second chunk: an input that arrives in
230
+ // one chunk (most small responses) is parsed once, by `finish`, instead
231
+ // of once in partial mode and again in complete mode, since a repetition
232
+ // that completes without resuming keeps no checkpoint
233
+ if (++this.pushed === 1) return
227
234
  // An attempt re-reads what the last one could not finish, from `restart`;
228
235
  // waiting until at least half as much new input has arrived keeps the
229
236
  // total work linear in the input. Strings are also copied when they are
@@ -234,9 +241,12 @@ class Incremental<A, E, I extends Chunkable> {
234
241
  this.attempt()
235
242
  }
236
243
 
237
- /** The step attempts run: the parser's resumable compiled code if it was compiled, else the interpreter. */
238
- private step(state: State<I>): unknown {
239
- return Stack.run(Registry.stepFor(this.parser, state, true) ?? this.parser.step, state)
244
+ /**
245
+ * The step attempts run: the parser's compiled code if it was compiled
246
+ * (the resumable build, unless `resumable` is false), else the interpreter.
247
+ */
248
+ private step(state: State<I>, resumable = true): unknown {
249
+ return Stack.run(Registry.stepFor(this.parser, state, resumable) ?? this.parser.step, state)
240
250
  }
241
251
 
242
252
  private attempt(): void {
@@ -258,10 +268,12 @@ class Incremental<A, E, I extends Chunkable> {
258
268
  if (settled !== undefined && settled !== "failed") {
259
269
  if (settled.pos === this.length) return Effect.succeed(settled.value)
260
270
  } else if (settled === undefined) {
261
- // The last attempt, in complete mode, resumes from the checkpoints too
271
+ // The last attempt, in complete mode, resumes from the checkpoints too;
272
+ // with no attempt before it, it is an ordinary parse
262
273
  const state = new State(this.input(), { context: this.context })
263
- state.resume = this.resume
264
- const a = this.step(state)
274
+ const resumable = this.attempted > 0
275
+ if (resumable) state.resume = this.resume
276
+ const a = this.step(state, resumable)
265
277
  if (a !== Fail && state.pos === state.end) return Effect.succeed(a as A)
266
278
  }
267
279
  // The error comes from an ordinary parse of the whole input, so it is the
package/src/Syntax.ts CHANGED
@@ -46,17 +46,19 @@
46
46
  *
47
47
  * @since 0.2.0
48
48
  */
49
+ import * as Str from "effect/String"
49
50
  import * as Predicate from "effect/Predicate"
50
51
  import type * as Context from "effect/Context"
51
52
  import { dual } from "effect/Function"
52
53
  import * as Option from "effect/Option"
53
54
  import type { Input, Slice, Source } from "./Input.js"
54
- import { Fail, fromSource } from "./Input.js"
55
+ import { Fail, fromSource, State } from "./Input.js"
55
56
  import * as ParseError from "./ParseError.js"
56
57
  import { nodeOf } from "./internal/node.js"
58
+ import { Key } from "./internal/key.js"
57
59
  import * as Stack from "./internal/stack.js"
58
60
  import type { ErrorInfo, Green, Previous, Segment } from "./internal/syntax.js"
59
- import { absolute, errorKind, SyntaxState } from "./internal/syntax.js"
61
+ import { absolute, errorKind, SyntaxBuilder } from "./internal/syntax.js"
60
62
  import type { Parser } from "./Parser.js"
61
63
  import { described, isParser } from "./Parser.js"
62
64
 
@@ -67,7 +69,7 @@ import { described, isParser } from "./Parser.js"
67
69
  const syntaxNode = (self: Parser.Any, kind: string, named: boolean): Parser.Any => {
68
70
  const step = self.step
69
71
  // Identifies the nodes this parser builds, so a reparse can reuse them
70
- const key = {}
72
+ const key = new Key()
71
73
  return described<unknown, unknown, unknown, Input>(
72
74
  (state) => {
73
75
  const builder = state.syntax
@@ -133,7 +135,7 @@ export const token: {
133
135
 
134
136
  const defaultTokenKind = (self: Parser.Any): string => {
135
137
  const n = nodeOf(self)
136
- if (n._tag === "Literal" && Predicate.isString(n.literal) && !n.caseless) return n.literal
138
+ if (Predicate.isTagged(n, "Literal") && Predicate.isString(n.literal) && !n.caseless) return n.literal
137
139
  return self.name ?? "token"
138
140
  }
139
141
 
@@ -577,7 +579,7 @@ export class SyntaxNode<I extends Input = string> {
577
579
  let out = ""
578
580
  const visit = (green: Green, field: string | undefined) => {
579
581
  if (green.named || green === this.green) {
580
- if (out.length > 0) out += " "
582
+ if (Str.isNonEmpty(out)) out += " "
581
583
  if (field !== undefined) out += `${field}: `
582
584
  out += `(${green.kind}`
583
585
  for (let i = 0; i < green.children.length; i++) visit(green.children[i], green.fields?.[i])
@@ -708,7 +710,8 @@ export class TreeCursor<I extends Input = string> {
708
710
  const expectedEof = ["end of input"]
709
711
 
710
712
  const run = <A, E, I extends Input>(config: Config, input: I, previous: Previous | undefined): Tree<A, E, I> => {
711
- const state = new SyntaxState<I>(input, { context: config.context }, previous)
713
+ const state = new State<I>(input, { context: config.context })
714
+ const builder = new SyntaxBuilder(state, previous)
712
715
  const a = Stack.run(config.inner.step, state)
713
716
  // Text parsers record the encoding when they fail, which they may have done inside reused nodes
714
717
  if (state.encoding === undefined && previous !== undefined) state.encoding = previous.encoding
@@ -722,8 +725,8 @@ const run = <A, E, I extends Input>(config: Config, input: I, previous: Previous
722
725
  } else {
723
726
  value = Option.some(a as A)
724
727
  }
725
- const green = state.finish(config.kind, Option.getOrUndefined(value), fatal, state.pos)
726
- return new Tree<A, E, I>(input, value, state.reused, green, config, state.encoding)
728
+ const green = builder.finish(config.kind, Option.getOrUndefined(value), fatal, state.pos)
729
+ return new Tree<A, E, I>(input, value, builder.reused, green, config, state.encoding)
727
730
  }
728
731
 
729
732
  /**
@@ -756,7 +759,7 @@ export const parse: {
756
759
  options?: ParseOptions & { readonly context?: Context.Context<R> }
757
760
  ): Tree<A, E, I> => {
758
761
  const n = nodeOf(grammar)
759
- const isRoot = n._tag === "Syntax" && n.kind !== undefined && n.named
762
+ const isRoot = Predicate.isTagged(n, "Syntax") && n.kind !== undefined && n.named
760
763
  const config: Config = {
761
764
  kind: isRoot ? n.kind : (options?.kind ?? "document"),
762
765
  inner: isRoot ? (n.self as Parser.Any) : grammar,