@thomasfosterau/effect-parse 0.4.1 → 0.6.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 (157) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +56 -14
  3. package/dist/Ascii.d.ts +11 -4
  4. package/dist/Ascii.d.ts.map +1 -1
  5. package/dist/Ascii.js +12 -4
  6. package/dist/Ascii.js.map +1 -1
  7. package/dist/Binary.d.ts +5 -2
  8. package/dist/Binary.d.ts.map +1 -1
  9. package/dist/Binary.js +83 -37
  10. package/dist/Binary.js.map +1 -1
  11. package/dist/Bits.d.ts.map +1 -1
  12. package/dist/Bits.js +11 -4
  13. package/dist/Bits.js.map +1 -1
  14. package/dist/Compiler.d.ts +74 -2
  15. package/dist/Compiler.d.ts.map +1 -1
  16. package/dist/Compiler.js +78 -1
  17. package/dist/Compiler.js.map +1 -1
  18. package/dist/Expression.d.ts.map +1 -1
  19. package/dist/Expression.js +86 -74
  20. package/dist/Expression.js.map +1 -1
  21. package/dist/Input.d.ts +56 -5
  22. package/dist/Input.d.ts.map +1 -1
  23. package/dist/Input.js +70 -1
  24. package/dist/Input.js.map +1 -1
  25. package/dist/Nom.d.ts +5 -15
  26. package/dist/Nom.d.ts.map +1 -1
  27. package/dist/Nom.js.map +1 -1
  28. package/dist/ParseError.d.ts +69 -11
  29. package/dist/ParseError.d.ts.map +1 -1
  30. package/dist/ParseError.js +129 -28
  31. package/dist/ParseError.js.map +1 -1
  32. package/dist/Parser.d.ts +375 -29
  33. package/dist/Parser.d.ts.map +1 -1
  34. package/dist/Parser.js +394 -37
  35. package/dist/Parser.js.map +1 -1
  36. package/dist/ParserSchema.d.ts.map +1 -1
  37. package/dist/ParserSchema.js +2 -3
  38. package/dist/ParserSchema.js.map +1 -1
  39. package/dist/ParserStream.d.ts +122 -14
  40. package/dist/ParserStream.d.ts.map +1 -1
  41. package/dist/ParserStream.js +293 -62
  42. package/dist/ParserStream.js.map +1 -1
  43. package/dist/Syntax.d.ts +23 -5
  44. package/dist/Syntax.d.ts.map +1 -1
  45. package/dist/Syntax.js +1 -1
  46. package/dist/Syntax.js.map +1 -1
  47. package/dist/Text.d.ts.map +1 -1
  48. package/dist/Text.js +21 -15
  49. package/dist/Text.js.map +1 -1
  50. package/dist/Token.d.ts +65 -11
  51. package/dist/Token.d.ts.map +1 -1
  52. package/dist/Token.js +73 -26
  53. package/dist/Token.js.map +1 -1
  54. package/dist/internal/compiler/closure.d.ts +1 -1
  55. package/dist/internal/compiler/closure.d.ts.map +1 -1
  56. package/dist/internal/compiler/closure.js +263 -113
  57. package/dist/internal/compiler/closure.js.map +1 -1
  58. package/dist/internal/compiler/entries.d.ts +3 -1
  59. package/dist/internal/compiler/entries.d.ts.map +1 -1
  60. package/dist/internal/compiler/entries.js +3 -2
  61. package/dist/internal/compiler/entries.js.map +1 -1
  62. package/dist/internal/compiler/first.d.ts.map +1 -1
  63. package/dist/internal/compiler/first.js +9 -2
  64. package/dist/internal/compiler/first.js.map +1 -1
  65. package/dist/internal/compiler/graph.d.ts +39 -0
  66. package/dist/internal/compiler/graph.d.ts.map +1 -0
  67. package/dist/internal/compiler/graph.js +78 -0
  68. package/dist/internal/compiler/graph.js.map +1 -0
  69. package/dist/internal/compiler/jit.d.ts +5 -0
  70. package/dist/internal/compiler/jit.d.ts.map +1 -1
  71. package/dist/internal/compiler/jit.js +208 -106
  72. package/dist/internal/compiler/jit.js.map +1 -1
  73. package/dist/internal/compiler/registry.d.ts +11 -3
  74. package/dist/internal/compiler/registry.d.ts.map +1 -1
  75. package/dist/internal/compiler/registry.js.map +1 -1
  76. package/dist/internal/compiler/search.d.ts +32 -0
  77. package/dist/internal/compiler/search.d.ts.map +1 -0
  78. package/dist/internal/compiler/search.js +68 -0
  79. package/dist/internal/compiler/search.js.map +1 -0
  80. package/dist/internal/compiler/sets.d.ts.map +1 -1
  81. package/dist/internal/compiler/sets.js +1 -1
  82. package/dist/internal/compiler/sets.js.map +1 -1
  83. package/dist/internal/compiler/wasm.d.ts +8 -0
  84. package/dist/internal/compiler/wasm.d.ts.map +1 -0
  85. package/dist/internal/compiler/wasm.js +2840 -0
  86. package/dist/internal/compiler/wasm.js.map +1 -0
  87. package/dist/internal/compiler/wasmAsm.d.ts +199 -0
  88. package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
  89. package/dist/internal/compiler/wasmAsm.js +427 -0
  90. package/dist/internal/compiler/wasmAsm.js.map +1 -0
  91. package/dist/internal/entries.d.ts +3 -1
  92. package/dist/internal/entries.d.ts.map +1 -1
  93. package/dist/internal/entries.js.map +1 -1
  94. package/dist/internal/node.d.ts +77 -3
  95. package/dist/internal/node.d.ts.map +1 -1
  96. package/dist/internal/node.js +54 -10
  97. package/dist/internal/node.js.map +1 -1
  98. package/dist/internal/numbers.d.ts.map +1 -1
  99. package/dist/internal/numbers.js.map +1 -1
  100. package/dist/internal/run.d.ts +14 -0
  101. package/dist/internal/run.d.ts.map +1 -1
  102. package/dist/internal/run.js +53 -11
  103. package/dist/internal/run.js.map +1 -1
  104. package/dist/internal/stack.d.ts +14 -0
  105. package/dist/internal/stack.d.ts.map +1 -1
  106. package/dist/internal/stack.js +24 -2
  107. package/dist/internal/stack.js.map +1 -1
  108. package/dist/internal/syntax.d.ts +4 -4
  109. package/dist/internal/syntax.d.ts.map +1 -1
  110. package/dist/internal/syntax.js +18 -10
  111. package/dist/internal/syntax.js.map +1 -1
  112. package/dist/internal/text.d.ts +32 -0
  113. package/dist/internal/text.d.ts.map +1 -1
  114. package/dist/internal/text.js +68 -14
  115. package/dist/internal/text.js.map +1 -1
  116. package/dist/internal/tokens.d.ts +20 -0
  117. package/dist/internal/tokens.d.ts.map +1 -1
  118. package/dist/internal/tokens.js +72 -39
  119. package/dist/internal/tokens.js.map +1 -1
  120. package/dist/internal/tuple.d.ts +6 -0
  121. package/dist/internal/tuple.d.ts.map +1 -0
  122. package/dist/internal/tuple.js +170 -0
  123. package/dist/internal/tuple.js.map +1 -0
  124. package/package.json +18 -10
  125. package/src/Ascii.ts +47 -12
  126. package/src/Binary.ts +174 -73
  127. package/src/Bits.ts +16 -6
  128. package/src/Compiler.ts +101 -10
  129. package/src/Expression.ts +94 -81
  130. package/src/Input.ts +99 -7
  131. package/src/Nom.ts +15 -5
  132. package/src/ParseError.ts +209 -52
  133. package/src/Parser.ts +684 -143
  134. package/src/ParserSchema.ts +1 -2
  135. package/src/ParserStream.ts +361 -68
  136. package/src/Syntax.ts +60 -36
  137. package/src/Text.ts +32 -27
  138. package/src/Token.ts +152 -34
  139. package/src/internal/compiler/closure.ts +371 -153
  140. package/src/internal/compiler/entries.ts +4 -2
  141. package/src/internal/compiler/first.ts +16 -0
  142. package/src/internal/compiler/graph.ts +96 -0
  143. package/src/internal/compiler/jit.ts +319 -169
  144. package/src/internal/compiler/registry.ts +10 -2
  145. package/src/internal/compiler/search.ts +68 -0
  146. package/src/internal/compiler/sets.ts +7 -3
  147. package/src/internal/compiler/wasm.ts +3276 -0
  148. package/src/internal/compiler/wasmAsm.ts +484 -0
  149. package/src/internal/entries.ts +8 -2
  150. package/src/internal/node.ts +103 -7
  151. package/src/internal/numbers.ts +3 -1
  152. package/src/internal/run.ts +73 -12
  153. package/src/internal/stack.ts +25 -1
  154. package/src/internal/syntax.ts +39 -18
  155. package/src/internal/text.ts +97 -9
  156. package/src/internal/tokens.ts +106 -14
  157. package/src/internal/tuple.ts +141 -0
@@ -1,5 +1,3 @@
1
- // fallow-ignore-file duplicate-export -- every compiler module exports its `backend` under the same name,
2
- // which the Compiler module imports by path; no barrel re-exports both.
3
1
  /**
4
2
  * The JIT: emits JavaScript source for a parser's description and builds
5
3
  * it with `new Function`.
@@ -25,29 +23,48 @@ import type { State } from "../../Input.js"
25
23
  import { Fail } from "../../Input.js"
26
24
  import type { Node, NodeOf, ParserLike } from "../node.js"
27
25
  import { addEntry, flushEntries, fromEntries, isPlainKey } from "../entries.js"
28
- import { children, nodeOf, scansFloat } from "../node.js"
26
+ import { Defer, NodeId, nodeOf, scansFloat } from "../node.js"
29
27
  import { floatScan, floatTexts, scanFloatBytes, scanFloatString, toFloat } from "../numbers.js"
30
28
  import { compileAny } from "../tokens.js"
31
29
  import { Key } from "../key.js"
32
30
  import * as Resume from "../resume.js"
33
- import { charOf, fastLimit, latin1, string, utf16be, utf16le, utf8, validate } from "../text.js"
31
+ import * as Stack from "../stack.js"
32
+ import {
33
+ ShortText,
34
+ charOf,
35
+ fastLimit,
36
+ latin1,
37
+ string,
38
+ textIn,
39
+ utf16be,
40
+ utf16le,
41
+ utf8,
42
+ utf8Text,
43
+ validate
44
+ } from "../text.js"
34
45
  import type { ClosureCompiler, CompiledRoot, Fallback, Step } from "./closure.js"
35
46
  import { backend as closureBackend, createClosureCompiler } from "./closure.js"
36
47
  import { buildsEntries, pairOf } from "./entries.js"
48
+ import { countInlined, countRefs, isLeaf, isLetter, isSmall, lowerAscii, selectorOf } from "./graph.js"
37
49
  import type { Backend } from "./registry.js"
38
50
  import type { First } from "./first.js"
39
51
  import { planAlt, Strict } from "./first.js"
40
52
  import type { CharSet } from "./sets.js"
41
53
  import { acceptTable, kindOfEncoding, reachesAbove, TableSize, textSet, tokenSet } from "./sets.js"
54
+ import { nextStop, SearchHead, StopCache, stopChars } from "./search.js"
42
55
 
43
56
  /** Helpers available to generated code as `R`. */
44
57
  const runtime = {
58
+ StopCache,
59
+ nextStop,
45
60
  take: Resume.take,
46
61
  save: Resume.save,
47
62
  Done: Resume.Done,
48
63
  /** Records the result of a resumed repetition (see `Resume.done`). */
49
64
  done: (state: State<any>, key: Key, entry: number, value: unknown, pos: number): void =>
50
65
  Resume.save(state, key, entry, new Resume.Done(value, pos)),
66
+ isStackOverflow: Stack.isStackOverflow,
67
+ tooDeep: Stack.tooDeep,
51
68
  some: Option.some,
52
69
  none: Option.none,
53
70
  charOf,
@@ -57,17 +74,20 @@ const runtime = {
57
74
  addEntry,
58
75
  flushEntries,
59
76
  /** `Text.taken`: the text of `[start, end)`, or `Fail` if it is malformed. */
60
- textOf: (enc: Encoding<any>, fast: number, s: State<any>, start: number, end: number): unknown => {
77
+ textOf: (enc: Encoding<any>, fast: number, s: State<any>, start: number, end: number): string | typeof Fail => {
61
78
  const input = s.input
62
79
  let i = start
80
+ // SAFETY: input kind 0 is a string input
63
81
  if (s.inputKind === 0) while (i < end && (input as string).charCodeAt(i) < fast) i++
82
+ // SAFETY: `Text` parsers are compiled natively, and so call this, on string (0) and byte (1) inputs only
64
83
  else while (i < end && (input as Uint8Array)[i] < fast) i++
65
84
  if (i < end && validate(enc, s, i, end) < 0) {
66
85
  s.mode = "Backtrack"
67
86
  return Fail
68
87
  }
69
- return enc.text(input, start, end)
70
- }
88
+ return textIn(enc, s, start, end)
89
+ },
90
+ utf8Text
71
91
  }
72
92
 
73
93
  let checked: FunctionConstructor | undefined
@@ -77,7 +97,7 @@ let supported = false
77
97
  * Whether `new Function` works here (it doesn't under a Content Security
78
98
  * Policy). Checked once, and again if `globalThis.Function` is replaced.
79
99
  */
80
- const jitSupported = (): boolean => {
100
+ export const jitSupported = (): boolean => {
81
101
  if (checked !== globalThis.Function) {
82
102
  checked = globalThis.Function
83
103
  const fn = checked
@@ -109,26 +129,6 @@ class Fn {
109
129
  readonly vars: Array<string> = []
110
130
  }
111
131
 
112
- const isLeaf = (node: Node): boolean => {
113
- switch (node._tag) {
114
- case "Succeed":
115
- case "Fail":
116
- case "Eof":
117
- case "Location":
118
- case "RestLen":
119
- case "Any":
120
- case "Literal":
121
- case "OneOf":
122
- case "TakeWhile":
123
- return true
124
- default:
125
- return false
126
- }
127
- }
128
-
129
- const lowerAscii = (c: number): number => (c >= 65 && c <= 90 ? c + 32 : c)
130
- const isLetter = (c: number): boolean => (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
131
-
132
132
  /** The inclusive ranges of 1s in a table. */
133
133
  const runsOf = (table: Uint8Array): Array<[number, number]> => {
134
134
  const out: Array<[number, number]> = []
@@ -211,6 +211,11 @@ interface Collect {
211
211
  readonly init: string
212
212
  readonly element: (fail: string, check: string) => string
213
213
  readonly finish: string
214
+ /** The variable holding the result. */
215
+ readonly value: string
216
+ /** In a resumable build: more fields of a checkpoint (`name:variable,`), and the code restoring them from `cp`. */
217
+ readonly saved: string
218
+ readonly restore: string
214
219
  }
215
220
 
216
221
  class Emitter {
@@ -218,7 +223,9 @@ class Emitter {
218
223
  private readonly constantIndex = new Map<unknown, number>()
219
224
  readonly functions: Array<string> = []
220
225
  private readonly rules = [new Map<ParserLike, string>(), new Map<ParserLike, string>()]
221
- private readonly refs = new Map<ParserLike, number>()
226
+ private readonly refs: WeakMap<ParserLike, number>
227
+ /** How many parsers have been inlined (see `countInlined`). */
228
+ private inlined = 0
222
229
  private readonly closure: ClosureCompiler
223
230
  private next = 0
224
231
 
@@ -228,27 +235,13 @@ class Emitter {
228
235
  readonly resumable = false
229
236
  ) {
230
237
  this.closure = createClosureCompiler(kind, resumable)
231
- this.countRefs(root)
238
+ this.refs = countRefs(root)
232
239
  }
233
240
 
234
241
  get fallbacks(): ReadonlyArray<Fallback> {
235
242
  return this.closure.fallbacks
236
243
  }
237
244
 
238
- /** Counts the distinct parents of each parser. */
239
- private countRefs(root: ParserLike): void {
240
- const seen = new Set<ParserLike>()
241
- const stack = [root]
242
- for (let p = stack.pop(); p !== undefined; p = stack.pop()) {
243
- if (seen.has(p)) continue
244
- seen.add(p)
245
- for (const c of new Set(children(nodeOf(p)))) {
246
- this.refs.set(c, (this.refs.get(c) ?? 0) + 1)
247
- stack.push(c)
248
- }
249
- }
250
- }
251
-
252
245
  const(value: unknown): string {
253
246
  let i = this.constantIndex.get(value)
254
247
  if (i === undefined) {
@@ -259,6 +252,25 @@ class Emitter {
259
252
  return `C[${i}]`
260
253
  }
261
254
 
255
+ /** The text of `[start, p)` in `enc`: UTF-8 decodes through the state's ASCII run (see `utf8Text`). */
256
+ text(enc: Encoding<any>, start: string): string {
257
+ const text = `${this.const(enc.text)}(I,${start},p)`
258
+ // Short strings are built by hand, which no run helps
259
+ return enc === utf8 ? `(p-${start}>${ShortText}?R.utf8Text($S,${start},p):${text})` : text
260
+ }
261
+
262
+ /**
263
+ * A count or bound as source text. Every number a description contributes
264
+ * to the generated source is written here, so nothing but a non-negative
265
+ * integer can reach it: a bound is a `number` only in the types, and a
266
+ * string written as given would run as code (#121).
267
+ */
268
+ num(n: number): string {
269
+ if (!Number.isSafeInteger(n) || n < 0)
270
+ throw new TypeError("effect-parse: a compiled bound must be a non-negative integer")
271
+ return String(n)
272
+ }
273
+
262
274
  /** A JavaScript literal for simple values, otherwise a constant. */
263
275
  lit(value: unknown): string {
264
276
  if (value === undefined) return "void 0"
@@ -325,29 +337,12 @@ class Emitter {
325
337
  gen(p: ParserLike, used: boolean, out: string | null, fail: string, fn: Fn, depth: number): string {
326
338
  const node = nodeOf(p)
327
339
  if (Predicate.isTagged(node, "Suspend")) return this.call(this.rule(node.resolve(), used), out, fail)
328
- if (!isLeaf(node) && (((this.refs.get(p) ?? 0) > 1 && !this.small(p)) || depth > 32)) {
340
+ if (!isLeaf(node) && (((this.refs.get(p) ?? 0) > 1 && !isSmall(p)) || depth > 32)) {
329
341
  return this.call(this.rule(p, used), out, fail)
330
342
  }
331
343
  return this.inline(p, used, out, fail, fn, depth)
332
344
  }
333
345
 
334
- /** Whether a parser is small enough to inline wherever it is used. */
335
- private small(p: ParserLike): boolean {
336
- let budget = 6
337
- const visit = (q: ParserLike): boolean => {
338
- const node = nodeOf(q)
339
- if (
340
- --budget < 0 ||
341
- Predicate.isTagged(node, "Suspend") ||
342
- Predicate.isTagged(node, "Escaped") ||
343
- Predicate.isTagged(node, "Dispatch")
344
- )
345
- return false
346
- return children(node).every(visit)
347
- }
348
- return visit(p)
349
- }
350
-
351
346
  /**
352
347
  * In a resumable build: token code (which fails by breaking out of `bad`)
353
348
  * that read within `lookahead` units of the end of a partial input re-runs
@@ -420,16 +415,16 @@ class Emitter {
420
415
 
421
416
  private arrayOneOf(node: NodeOf<"OneOf">, out: string | null, fail: string): string {
422
417
  const m = this.const(compileAny(node.set).forCode.token)
423
- return `if(p>=E||${m}(I[p])===${node.negate}){${BT}${fail}}${out === null ? "" : `${out}=I[p];`}p++;`
418
+ return `if(p>=E||${m}(I[p])===${node.negate === true}){${BT}${fail}}${out === null ? "" : `${out}=I[p];`}p++;`
424
419
  }
425
420
 
426
421
  private arrayTakeWhile(node: NodeOf<"TakeWhile">, out: string | null, fail: string, fn: Fn): string {
427
422
  const m = this.const(compileAny(node.set).forCode.token)
428
423
  const st = this.v(fn)
429
- const max = node.max === Infinity ? "" : `&&p-${st}<${node.max}`
424
+ const max = node.max === Infinity ? "" : `&&p-${st}<${this.num(node.max)}`
430
425
  return (
431
- `${st}=p;while(p<E${max}&&${m}(I[p])!==${node.negate})p++;` +
432
- (node.min > 0 ? `if(p-${st}<${node.min}){${BT}${fail}}` : "") +
426
+ `${st}=p;while(p<E${max}&&${m}(I[p])!==${node.negate === true})p++;` +
427
+ (node.min > 0 ? `if(p-${st}<${this.num(node.min)}){${BT}${fail}}` : "") +
433
428
  (out === null ? "" : `${out}=I.slice(${st},p);`)
434
429
  )
435
430
  }
@@ -451,6 +446,7 @@ class Emitter {
451
446
  }
452
447
 
453
448
  inline(p: ParserLike, used: boolean, out: string | null, fail: string, fn: Fn, depth: number): string {
449
+ this.inlined = countInlined(this.inlined)
454
450
  const node = nodeOf(p)
455
451
  const set = (value: string) => (out === null ? "" : `${out}=${value};`)
456
452
  const d = depth + 1
@@ -464,7 +460,8 @@ class Emitter {
464
460
  case "Eof":
465
461
  return (
466
462
  `if(p<E){${BT}${fail}}` +
467
- (this.resumable ? `if($S.partial){$S.mode="Incomplete";${fail}}` : "") +
463
+ // `eof`'s own failure: `Incomplete`, needing one more unit
464
+ (this.resumable ? `if($S.partial){$S.incomplete(1);${fail}}` : "") +
468
465
  set("void 0")
469
466
  )
470
467
  case "Location":
@@ -495,8 +492,8 @@ class Emitter {
495
492
  const code = this.float(node, out, fail, fn, d)
496
493
  if (code !== undefined) return code
497
494
  }
498
- // Streaming builds keep the array, which their checkpoints hold
499
- if (node.f === fromEntries && out !== null && !this.resumable && buildsEntries(node.self)) {
495
+ // Streaming builds too: their checkpoints hold the object built so far
496
+ if (node.f === fromEntries && out !== null && buildsEntries(node.self)) {
500
497
  return this.entries(node.self, out, fail, fn, d)
501
498
  }
502
499
  const t = this.v(fn)
@@ -518,6 +515,22 @@ class Emitter {
518
515
  `${r}=${this.const(node.f)}(${t});if(${r}._tag!=="${tag}"){${BT}${fail}}${set(`${r}.${field}`)}`
519
516
  )
520
517
  }
518
+ case "FastPath": {
519
+ // The fast path runs on the state; when it defers, the grammar runs from where it started
520
+ const st = this.v(fn)
521
+ const t = out ?? this.v(fn)
522
+ const done = this.label()
523
+ return (
524
+ `${st}=p;$S.pos=p;${t}=${this.const(node.fast)}($S);` +
525
+ `${done}:{if(${t}!==${this.const(Defer)}){p=$S.pos;break ${done}}p=${st};` +
526
+ `${this.gen(node.self, used, out, fail, fn, d)}}`
527
+ )
528
+ }
529
+ case "MapOrFail": {
530
+ const t = this.v(fn)
531
+ const r = out ?? this.v(fn)
532
+ return this.gen(node.self, true, t, fail, fn, d) + `${r}=${this.const(node.f)}(${t});if(${r}===F){${BT}${fail}}`
533
+ }
521
534
  case "Filter": {
522
535
  const t = out ?? this.v(fn)
523
536
  return this.gen(node.self, true, t, fail, fn, d) + `if(!${this.const(node.predicate)}(${t})){${BT}${fail}}`
@@ -553,6 +566,12 @@ class Emitter {
553
566
  const extra = Predicate.isTagged(node, "WithSpan") ? `{start:${st},end:p}` : `$S.slice(${st},p)`
554
567
  return `${st}=p;` + this.gen(node.self, true, t, fail, fn, d) + set(`[${t},${extra}]`)
555
568
  }
569
+ case "MapSpan": {
570
+ if (!used) return this.gen(node.self, false, null, fail, fn, d)
571
+ const st = this.v(fn)
572
+ const t = this.v(fn)
573
+ return `${st}=p;` + this.gen(node.self, true, t, fail, fn, d) + set(`${this.const(node.f)}(${t},${st},p)`)
574
+ }
556
575
  case "Span": {
557
576
  const st = this.v(fn)
558
577
  return `${st}=p;` + this.gen(node.self, false, null, fail, fn, d) + set(`{start:${st},end:p}`)
@@ -567,6 +586,7 @@ class Emitter {
567
586
  }
568
587
  case "Context":
569
588
  case "Expected":
589
+ case "Quiet":
570
590
  case "MapError":
571
591
  case "OrFail":
572
592
  case "Trace":
@@ -615,6 +635,12 @@ class Emitter {
615
635
  }
616
636
  return code + `${out}={${node.keys.map((k, i) => `${JSON.stringify(k)}:${vars[i]}`).join(",")}};`
617
637
  }
638
+ case "SeqWith": {
639
+ if (out === null) return node.parsers.map((q) => this.gen(q, false, null, fail, fn, d)).join("")
640
+ const vars = node.parsers.map(() => this.v(fn))
641
+ const code = node.parsers.map((q, i) => this.gen(q, true, vars[i], fail, fn, d)).join("")
642
+ return code + `${out}=${this.const(node.f)}(${vars.join(",")});`
643
+ }
618
644
  case "ZipWith": {
619
645
  if (out === null)
620
646
  return this.gen(node.self, false, null, fail, fn, d) + this.gen(node.that, false, null, fail, fn, d)
@@ -631,10 +657,20 @@ class Emitter {
631
657
  case "Dispatch":
632
658
  return this.dispatch(p, node, used, out, fail, fn, d)
633
659
  case "Repeat":
634
- case "Fold":
635
- return this.repeat(node, used, out, fail, fn, d)
660
+ case "Fold": {
661
+ // A run of a character set whose characters are not kept is a `takeWhile` (#79)
662
+ const run =
663
+ out === null && Predicate.isTagged(node, "Repeat") ? this.repeatRun(p, node, fail, fn, d) : undefined
664
+ return run ?? this.repeat(node, used, out, fail, fn, d)
665
+ }
636
666
  case "Separated":
637
667
  return this.separated(node, used, out, fail, fn, d)
668
+ case "Fixed": {
669
+ // On bytes, read inline; at the end of the input, the step reports it
670
+ if (this.kind !== 1) return this.delegate(p, used, out, fail, fn)
671
+ const read = used ? `${out ?? this.v(fn)}=${this.const(node.read)}(I,p,$S);` : ""
672
+ return `if(E-p>=${this.num(node.size)}){${read}p+=${this.num(node.size)}}else{${this.delegate(p, used, out, fail, fn)}}`
673
+ }
638
674
  default:
639
675
  return this.delegate(p, used, out, fail, fn)
640
676
  }
@@ -734,7 +770,7 @@ class Emitter {
734
770
  // The cases are known: number them, and run the chosen one inline
735
771
  const cases = new Map<ParserLike, number>()
736
772
  for (const key of Reflect.ownKeys(node.cases)) {
737
- const q = node.cases[key as any]
773
+ const q = node.cases[key]
738
774
  if (!cases.has(q)) cases.set(q, cases.size)
739
775
  }
740
776
  if (node.otherwise !== undefined && !cases.has(node.otherwise)) cases.set(node.otherwise, cases.size)
@@ -757,9 +793,7 @@ class Emitter {
757
793
  const r = out ?? this.v(fn)
758
794
  return code + `default:{$S.pos=p;${r}=${q}.step($S);if(${r}===F){${fail}}p=$S.pos;}}`
759
795
  }
760
- let sel = nodeOf(node.selector)
761
- const peek = Predicate.isTagged(sel, "Peek")
762
- if (peek) sel = nodeOf((sel as NodeOf<"Peek">).self)
796
+ const { node: sel, peek } = selectorOf(node.selector)
763
797
  if (!Predicate.isTagged(sel, "Any") || !this.native(sel.encoding)) return general(true)
764
798
  const enc = sel.encoding
765
799
  const r = enc === undefined ? undefined : this.reader(enc)
@@ -791,6 +825,55 @@ class Emitter {
791
825
  return code
792
826
  }
793
827
 
828
+ /**
829
+ * A repetition whose elements are not kept, rewritten to scan a run of a
830
+ * character set in one loop instead of one element at a time (#79):
831
+ *
832
+ * - `repeat(oneOf(S), range)` matches what `takeWhile(S, range)` matches,
833
+ * stopping, failing and reporting `Incomplete` where it does, so it
834
+ * compiles as that `takeWhile`, with the repetition's own step as the
835
+ * fallback near the end of a partial input.
836
+ * - `repeat(alt(oneOf(S), ...rest))` with `min` at most 1 and no `max`
837
+ * matches what `repeat(alt(takeWhile(S, { min: 1 }), ...rest))` matches:
838
+ * a run of `S` is a run of one-character elements, and the counts that
839
+ * differ only matter for other ranges. The first branch is a synthetic
840
+ * `takeWhile` node; outside resumable builds, which never run its step.
841
+ *
842
+ * Returns `undefined` when neither applies.
843
+ */
844
+ private repeatRun(p: ParserLike, node: NodeOf<"Repeat">, fail: string, fn: Fn, d: number): string | undefined {
845
+ if (this.kind === 2) return undefined
846
+ const element = nodeOf(node.self)
847
+ if (Predicate.isTagged(element, "OneOf") && this.native(element.encoding)) {
848
+ const { encoding, negate, set } = element
849
+ return this.leaf(
850
+ p,
851
+ { _tag: "TakeWhile", set, negate, encoding, min: node.min, max: node.max },
852
+ false,
853
+ null,
854
+ fail,
855
+ fn
856
+ )
857
+ }
858
+ if (this.resumable || node.min > 1 || node.max !== Infinity || !Predicate.isTagged(element, "Alt")) return undefined
859
+ const [first, ...rest] = element.parsers
860
+ const head = first === undefined ? undefined : nodeOf(first)
861
+ if (
862
+ head === undefined ||
863
+ !Predicate.isTagged(head, "OneOf") ||
864
+ !this.native(head.encoding) ||
865
+ Arr.isReadonlyArrayEmpty(rest)
866
+ ) {
867
+ return undefined
868
+ }
869
+ const run = synthetic(
870
+ { _tag: "TakeWhile", set: head.set, negate: head.negate, encoding: head.encoding, min: 1, max: Infinity },
871
+ node.self.step
872
+ )
873
+ const alt = synthetic({ _tag: "Alt", parsers: [run, ...rest] }, node.self.step)
874
+ return this.repeat({ ...node, self: alt }, false, null, fail, fn, d)
875
+ }
876
+
794
877
  repeat(
795
878
  node: NodeOf<"Repeat"> | NodeOf<"Fold">,
796
879
  used: boolean,
@@ -806,7 +889,7 @@ class Emitter {
806
889
  const n = this.v(fn)
807
890
  const st = this.v(fn)
808
891
  const isFold = Predicate.isTagged(node, "Fold")
809
- const cond = isFold ? (node as NodeOf<"Fold">).while : undefined
892
+ const cond = isFold ? node.while : undefined
810
893
  const keep = out !== null && used && collect === undefined
811
894
  // `while` needs the accumulator even when the output is unused
812
895
  const tracked = keep || cond !== undefined
@@ -816,33 +899,42 @@ class Emitter {
816
899
  let add = ""
817
900
  if (tracked) {
818
901
  if (isFold) {
819
- init = `${acc}=${this.const((node as NodeOf<"Fold">).init)}();`
820
- add = `${acc}=${this.const((node as NodeOf<"Fold">).f)}(${acc},${x});`
902
+ init = `${acc}=${this.const(node.init)}();`
903
+ add = `${acc}=${this.const(node.f)}(${acc},${x});`
821
904
  } else {
822
905
  init = `${acc}=[];`
823
906
  add = `${acc}.push(${x});`
824
907
  }
825
908
  }
826
- const check = max === Infinity ? `if(p===${st}){$S.mode="Cut";${fail}}` : ""
827
- const bound = max === Infinity ? "" : `${n}<${max}`
909
+ const check = `if(p===${st}){$S.mode="Cut";${fail}}`
910
+ const bound = max === Infinity ? "" : `${n}<${this.num(max)}`
828
911
  const r = this.resumable ? this.resumePoint(fn) : undefined
912
+ // A checkpoint holds the result so far: the accumulator, or what `collect` builds
913
+ const held = collect?.value ?? acc
829
914
  const save =
830
915
  r === undefined
831
916
  ? ""
832
- : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${acc},count:${n},pos:${st}});`
833
- // With `collect` (never in resumable builds), the element adds itself once the loop check passed
917
+ : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${held},${collect?.saved ?? ""}count:${n},pos:${st}});`
918
+ // With `collect`, the element adds itself once the loop check passed
834
919
  const element =
835
920
  collect === undefined
836
921
  ? `${this.gen(node.self, tracked, x, `break ${bad};`, fn, d)}${check}${add}`
837
922
  : collect.element(`break ${bad};`, check)
838
923
  const stop =
839
- cond === undefined ? "" : `if(${min > 0 ? `${n}>=${min}&&` : ""}!${this.const(cond)}(${acc}))break ${loop};`
924
+ cond === undefined
925
+ ? ""
926
+ : `if(${min > 0 ? `${n}>=${this.num(min)}&&` : ""}!${this.const(cond)}(${acc}))break ${loop};`
840
927
  const body =
841
928
  `${loop}:for(;${bound};${n}++){${stop}${st}=p;` +
842
929
  `${bad}:{${element}continue ${loop};}` +
843
- `${save}if($S.mode!=="Backtrack"${min > 0 ? `||${n}<${min}` : ""}){${fail}}p=${st};break ${loop};}`
930
+ `${save}if($S.mode!=="Backtrack"${min > 0 ? `||${n}<${this.num(min)}` : ""}){${fail}}p=${st};break ${loop};}`
844
931
  if (r === undefined) return `${init}${n}=0;${body}${collect?.finish ?? ""}`
845
- return r.wrap(acc, `${acc}=cp.acc;${n}=cp.count;p=cp.pos;`, `${init}${n}=0;`, body)
932
+ return r.wrap(
933
+ held,
934
+ `${held}=cp.acc;${collect?.restore ?? ""}${n}=cp.count;p=cp.pos;`,
935
+ `${init}${n}=0;`,
936
+ `${body}${collect?.finish ?? ""}`
937
+ )
846
938
  }
847
939
 
848
940
  /**
@@ -887,10 +979,12 @@ class Emitter {
887
979
  const loop = this.label()
888
980
  const push = keep ? `${acc}.push(${x});` : ""
889
981
  const r = this.resumable ? this.resumePoint(fn) : undefined
982
+ // A checkpoint holds the result so far: the accumulator, or what `collect` builds
983
+ const held = collect?.value ?? acc
890
984
  const save =
891
985
  r === undefined
892
986
  ? ""
893
- : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${acc},count:${n},pos:${st}});`
987
+ : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${held},${collect?.saved ?? ""}count:${n},pos:${st}});`
894
988
  // Runs `body`; on its failure, stops the repetition (or fails if `fatal`)
895
989
  const attempt = (body: (fail: string) => string, success: string, fatal: string, checkpoint: string) => {
896
990
  const ok = this.label()
@@ -903,19 +997,24 @@ class Emitter {
903
997
  const self = (f: string) =>
904
998
  collect === undefined ? this.gen(node.self, keep, x, f, fn, d) : collect.element(f, "")
905
999
  const sep = (f: string) => this.gen(node.separator, false, null, f, fn, d)
906
- const check = max === Infinity ? `if(p===${st}){$S.mode="Cut";${fail}}` : ""
1000
+ const check = `if(p===${st}){$S.mode="Cut";${fail}}`
907
1001
  const first =
908
1002
  `${collect?.init ?? (keep ? `${acc}=[];` : "")}${n}=0;${st}=p;` +
909
1003
  attempt(self, `${push}${n}++;`, min > 0 ? "||true" : "", "")
910
1004
  const rest =
911
- `${loop}:while(${max === Infinity ? "true" : `${n}<${max}`}){${st}=p;` +
912
- attempt(sep, "", `||${n}<${min}`, save) +
1005
+ `${loop}:while(${max === Infinity ? "true" : `${n}<${this.num(max)}`}){${st}=p;` +
1006
+ attempt(sep, "", `||${n}<${this.num(min)}`, save) +
913
1007
  check +
914
- attempt(self, `${push}${n}++;continue ${loop};`, `||${n}<${min}`, save) +
1008
+ attempt(self, `${push}${n}++;continue ${loop};`, `||${n}<${this.num(min)}`, save) +
915
1009
  `}`
916
1010
  if (r === undefined) return `${done}:{${first}${rest}}${collect?.finish ?? ""}`
917
1011
  // `done` ends the repetition; the checkpoint wrapper records the result after it
918
- return r.wrap(acc, `${acc}=cp.acc;${n}=cp.count;p=cp.pos;`, "", `${done}:{if(!${r.resumed}){${first}}${rest}}`)
1012
+ return r.wrap(
1013
+ held,
1014
+ `${held}=cp.acc;${collect?.restore ?? ""}${n}=cp.count;p=cp.pos;`,
1015
+ "",
1016
+ `${done}:{if(!${r.resumed}){${first}}${rest}}${collect?.finish ?? ""}`
1017
+ )
919
1018
  }
920
1019
 
921
1020
  /**
@@ -927,59 +1026,61 @@ class Emitter {
927
1026
  */
928
1027
  private entries(p: ParserLike, out: string, fail: string, fn: Fn, d: number): string {
929
1028
  const node = nodeOf(p)
930
- switch (node._tag) {
931
- case "Mode": {
932
- const ok = this.label()
933
- const bad = this.label()
934
- return (
935
- `${ok}:{${bad}:{${this.entries(node.self, out, `break ${bad};`, fn, d)}break ${ok};}` +
936
- `if($S.mode===${JSON.stringify(node.from)})$S.mode=${JSON.stringify(node.to)};${fail}}`
937
- )
938
- }
939
- case "Seq": {
940
- const i = node.output as number
941
- return node.parsers
942
- .map((q, j) => (j === i ? this.entries(q, out, fail, fn, d) : this.gen(q, false, null, fail, fn, d)))
943
- .join("")
1029
+ if (Predicate.isTagged(node, "Mode")) {
1030
+ const ok = this.label()
1031
+ const bad = this.label()
1032
+ return (
1033
+ `${ok}:{${bad}:{${this.entries(node.self, out, `break ${bad};`, fn, d)}break ${ok};}` +
1034
+ `if($S.mode===${JSON.stringify(node.from)})$S.mode=${JSON.stringify(node.to)};${fail}}`
1035
+ )
1036
+ }
1037
+ if (Predicate.isTagged(node, "Seq")) {
1038
+ // SAFETY: `buildsEntries` passes through a sequence only when it keeps one output, by index
1039
+ const i = node.output as number
1040
+ return node.parsers
1041
+ .map((q, j) => (j === i ? this.entries(q, out, fail, fn, d) : this.gen(q, false, null, fail, fn, d)))
1042
+ .join("")
1043
+ }
1044
+ if (Predicate.isTagged(node, "Repeat") || Predicate.isTagged(node, "Separated")) {
1045
+ const pair = pairOf(node.self)
1046
+ if (pair === undefined) {
1047
+ // Not reached (`buildsEntries` found the pair): build the pairs, then the object
1048
+ const t = this.v(fn)
1049
+ return this.gen(p, true, t, fail, fn, d) + `${out}=${this.const(fromEntries)}(${t});`
944
1050
  }
945
- case "Repeat":
946
- case "Separated": {
947
- const pair = pairOf(node.self)
948
- if (pair === undefined) {
949
- // Not reached (`buildsEntries` found the pair): build the pairs, then the object
950
- const t = this.v(fn)
951
- return this.gen(p, true, t, fail, fn, d) + `${out}=${this.const(fromEntries)}(${t});`
952
- }
953
- const deferred = this.v(fn)
954
- const key = this.v(fn)
955
- const value = this.v(fn)
956
- const collect: Collect = {
957
- init: `${out}={};${deferred}=void 0;`,
958
- element: (f, check) =>
959
- pair.parsers
960
- .map((q, j) =>
961
- this.gen(
962
- q,
963
- j === pair.key || j === pair.value,
964
- j === pair.key ? key : j === pair.value ? value : null,
965
- f,
966
- fn,
967
- d
968
- )
1051
+ const deferred = this.v(fn)
1052
+ const key = this.v(fn)
1053
+ const value = this.v(fn)
1054
+ const collect: Collect = {
1055
+ init: `${out}={};${deferred}=void 0;`,
1056
+ element: (f, check) =>
1057
+ pair.parsers
1058
+ .map((q, j) =>
1059
+ this.gen(
1060
+ q,
1061
+ j === pair.key || j === pair.value,
1062
+ j === pair.key ? key : j === pair.value ? value : null,
1063
+ f,
1064
+ fn,
1065
+ d
969
1066
  )
970
- .join("") +
971
- check +
972
- `if(${deferred}===void 0&&R.isPlainKey(${key}))${out}[${key}]=${value};` +
973
- `else ${deferred}=R.addEntry(${out},${deferred},${key},${value});`,
974
- finish: `if(${deferred}!==void 0)R.flushEntries(${out},${deferred});`
975
- }
976
- return Predicate.isTagged(node, "Repeat")
977
- ? this.repeat(node, true, out, fail, fn, d, collect)
978
- : this.separated(node, true, out, fail, fn, d, collect)
1067
+ )
1068
+ .join("") +
1069
+ check +
1070
+ `if(${deferred}===void 0&&R.isPlainKey(${key}))${out}[${key}]=${value};` +
1071
+ `else ${deferred}=R.addEntry(${out},${deferred},${key},${value});`,
1072
+ finish: `if(${deferred}!==void 0)R.flushEntries(${out},${deferred});`,
1073
+ value: out,
1074
+ // A streaming build's checkpoint holds the object so far, with its deferred entries
1075
+ saved: `def:${deferred},`,
1076
+ restore: `${deferred}=cp.def;`
979
1077
  }
980
- default:
981
- return this.entries((node as { readonly self: ParserLike }).self, out, fail, fn, d)
1078
+ return Predicate.isTagged(node, "Repeat")
1079
+ ? this.repeat(node, true, out, fail, fn, d, collect)
1080
+ : this.separated(node, true, out, fail, fn, d, collect)
982
1081
  }
1082
+ // SAFETY: the other nodes `buildsEntries` passes through are diagnostics, which all wrap a `self`
1083
+ return this.entries((node as { readonly self: ParserLike }).self, out, fail, fn, d)
983
1084
  }
984
1085
 
985
1086
  // ---------------------------------------------------------------------------
@@ -1027,6 +1128,7 @@ class Emitter {
1027
1128
  ): string | undefined {
1028
1129
  const set = (value: string) => (out === null ? "" : `${out}=${value};`)
1029
1130
  const enc = node.encoding
1131
+ // SAFETY: `leaf`, the only caller, checks `native` first, which never holds on array input (kind 2)
1030
1132
  const kind = this.kind as 0 | 1
1031
1133
  const failBT = `{${BT}${fail}}`
1032
1134
  switch (node._tag) {
@@ -1055,6 +1157,7 @@ class Emitter {
1055
1157
  const lit = node.literal
1056
1158
  if (enc !== undefined) {
1057
1159
  if (node.caseless) return undefined
1160
+ // SAFETY: a `Literal` with an encoding is a `Text` literal, which records the string it encodes
1058
1161
  const encoded = enc.encode(lit as string)
1059
1162
  return (
1060
1163
  this.literalUnits(Predicate.isString(encoded) ? unitsOf(encoded) : Array.from(encoded), false, fail) +
@@ -1062,12 +1165,10 @@ class Emitter {
1062
1165
  )
1063
1166
  }
1064
1167
  const literalKind = Predicate.isString(lit) ? 0 : lit instanceof Uint8Array ? 1 : 2
1065
- if (kind === 0 && literalKind !== 0) return undefined
1066
- if (literalKind === 2) return undefined
1067
- const codes =
1068
- kind === 0
1069
- ? unitsOf(lit as string)
1070
- : Array.from(literalKind === 0 ? new TextEncoder().encode(lit as string) : (lit as Uint8Array))
1168
+ let codes: Array<number>
1169
+ if (Predicate.isString(lit)) codes = kind === 0 ? unitsOf(lit) : Array.from(new TextEncoder().encode(lit))
1170
+ else if (kind === 1 && lit instanceof Uint8Array) codes = Array.from(lit)
1171
+ else return undefined
1071
1172
  const len = codes.length
1072
1173
  const value =
1073
1174
  !node.caseless && literalKind === kind
@@ -1139,6 +1240,7 @@ class Emitter {
1139
1240
  private takeWhile(node: NodeOf<"TakeWhile">, out: string | null, fail: string, fn: Fn): string {
1140
1241
  const enc = node.encoding
1141
1242
  const { max, min, negate } = node
1243
+ // SAFETY: called from `token` only, whose `kind` is not 2
1142
1244
  const kind = this.kind as 0 | 1
1143
1245
  const cs = enc === undefined ? tokenSet(node.set, kind) : textSet(node.set)
1144
1246
  const accept = acceptTable(cs, negate)
@@ -1148,11 +1250,11 @@ class Emitter {
1148
1250
  const loop = this.label()
1149
1251
  const counted = max !== Infinity || min > 1
1150
1252
  const count = counted ? `${n}++;` : ""
1151
- const bound = max !== Infinity ? `&&${n}<${max}` : ""
1152
- const slice =
1153
- enc !== undefined ? `${this.const(enc.text)}(I,${st},p)` : kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1253
+ const bound = max !== Infinity ? `&&${n}<${this.num(max)}` : ""
1254
+ const slice = enc !== undefined ? this.text(enc, st) : kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1154
1255
  const finish =
1155
- (min > 0 ? `if(${counted ? n : `p-${st}`}<${min}){${BT}${fail}}` : "") + (out === null ? "" : `${out}=${slice};`)
1256
+ (min > 0 ? `if(${counted ? n : `p-${st}`}<${this.num(min)}){${BT}${fail}}` : "") +
1257
+ (out === null ? "" : `${out}=${slice};`)
1156
1258
  const head = `${st}=p;${counted ? `${n}=0;` : ""}${loop}:while(p<E${bound}){${c}=${this.unit("p")};`
1157
1259
  const member = this.member(accept, c)
1158
1260
  if (kind === 1 && (enc === undefined || fastLimit(enc) >= TableSize)) {
@@ -1173,6 +1275,8 @@ class Emitter {
1173
1275
  `p+=${this.const(enc.width)}(${c})}${count}}${finish}`
1174
1276
  )
1175
1277
  }
1278
+ const stops = negate && !counted ? stopChars(cs) : undefined
1279
+ if (stops !== undefined) return this.stopSearch(stops, st, c, finish, fn)
1176
1280
  const fast = 0xd800
1177
1281
  const small = TableSize
1178
1282
  const highTest = (v: string) => this.memberHigh(cs, negate, v)
@@ -1193,6 +1297,34 @@ class Emitter {
1193
1297
  return `${head}if(${c}<${small}){if(!(${member}))break;p++}else ${rest}${count}}${finish}`
1194
1298
  }
1195
1299
 
1300
+ /**
1301
+ * `Token.takeTill` on a string, of one to `MaxStops` characters that are
1302
+ * whole code points: the set accepts every other unit, surrogates
1303
+ * included, so the run ends at the first stop character. The first
1304
+ * `SearchHead` units are tested one at a time, which beats a native
1305
+ * search on short runs; a longer run ends at the next occurrence that
1306
+ * `nextStop` finds with `indexOf` (#75).
1307
+ */
1308
+ private stopSearch(stops: ReadonlyArray<number>, st: string, c: string, finish: string, fn: Fn): string {
1309
+ const h = this.v(fn)
1310
+ const test = stops.map((u) => `${c}!==${u}`).join("&&")
1311
+ // One stop character is searched for inline: a run that doesn't find it ends at `E`, so it is
1312
+ // searched for once. Several are searched for through `nextStop`'s cache, so that one which
1313
+ // doesn't occur isn't searched for again, to the end of the input, on every run.
1314
+ const search =
1315
+ stops.length === 1
1316
+ ? `{p=I.indexOf(${JSON.stringify(String.fromCharCode(stops[0]))},p);if(p<0)p=E}`
1317
+ : `p=R.nextStop(${this.const(new StopCache(stops.map((u) => String.fromCharCode(u))))},$S,p);`
1318
+ // A cached search costs more than a plain one, so it waits for a longer run
1319
+ const head = stops.length === 1 ? SearchHead : 2 * SearchHead
1320
+ return (
1321
+ `${st}=p;${h}=p+${head}<E?p+${head}:E;` +
1322
+ `while(p<${h}&&(${c}=I.charCodeAt(p),${test}))p++;` +
1323
+ // A run that ends right after the head needs no search
1324
+ `if(p===${h}&&p<E&&(${c}=I.charCodeAt(p),${test}))${search}${finish}`
1325
+ )
1326
+ }
1327
+
1196
1328
  /**
1197
1329
  * `escaped`. In a resumable build it keeps the closure compiler's
1198
1330
  * checkpoints: where the next segment starts and, unless raw, the text
@@ -1222,14 +1354,11 @@ class Emitter {
1222
1354
  const onNormal = raw
1223
1355
  ? `if(p===${b}){$S.mode="Cut";${fail}}continue ${loop};`
1224
1356
  : `if(p!==${b}){${acc}+=${x};continue ${loop}}`
1225
- const slice =
1226
- enc !== undefined
1227
- ? `${this.const(enc.text)}(I,${st},p)`
1228
- : this.kind === 0
1229
- ? `I.slice(${st},p)`
1230
- : `I.subarray(${st},p)`
1357
+ const slice = enc !== undefined ? this.text(enc, st) : this.kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1231
1358
  const ctrlTest =
1232
- rd === undefined ? `p<E&&${this.unit("p")}===${ctrl}` : `${rd.guard("p")}&&${rd.unit("p")}===${ctrl}`
1359
+ rd === undefined
1360
+ ? `p<E&&${this.unit("p")}===${this.num(ctrl)}`
1361
+ : `${rd.guard("p")}&&${rd.unit("p")}===${this.num(ctrl)}`
1233
1362
  // In a resumable build, running out of a partial input (not stopping
1234
1363
  // before a non-control character) leaves the text incomplete
1235
1364
  const stop = r === undefined ? loop : this.label()
@@ -1239,7 +1368,7 @@ class Emitter {
1239
1368
  `if(${okv}){${onNormal}}else{if($S.mode!=="Backtrack"){${save(b)}${fail}}p=${b}}` +
1240
1369
  `if(${ctrlTest}){p+=${rd?.width ?? 1};${escape}${raw ? "" : `${acc}+=${x};`}}else break ${stop};}`
1241
1370
  const body =
1242
- r === undefined ? loopCode : `${stop}:{${loopCode}if($S.partial){$S.mode="Incomplete";${save("p")}${fail}}}`
1371
+ r === undefined ? loopCode : `${stop}:{${loopCode}if($S.partial){$S.incomplete(0);${save("p")}${fail}}}`
1243
1372
  if (r === undefined) {
1244
1373
  return `${raw ? `${st}=p;` : `${acc}="";`}${body}${raw && out !== null ? `${out}=${slice};` : ""}`
1245
1374
  }
@@ -1250,6 +1379,18 @@ class Emitter {
1250
1379
  }
1251
1380
  }
1252
1381
 
1382
+ /**
1383
+ * A parser that exists only as a description, for the rewrites of
1384
+ * `Emitter.repeatRun`. `step` is never run on the paths they compile (the
1385
+ * token code of a native `takeWhile`, and alternatives inline), so it is
1386
+ * the step of the repetition's element, which describes the same language
1387
+ * when its value is unused.
1388
+ */
1389
+ const synthetic = (node: Node, step: Step): ParserLike => {
1390
+ const described: ParserLike & { readonly [NodeId]: Node } = { step, name: undefined, [NodeId]: node }
1391
+ return described
1392
+ }
1393
+
1253
1394
  /** The UTF-16 code units of a string. */
1254
1395
  const unitsOf = (s: string): Array<number> => Array.from({ length: s.length }, (_, i) => s.charCodeAt(i))
1255
1396
 
@@ -1264,10 +1405,20 @@ interface Generated {
1264
1405
  const generate = (root: ParserLike, kind: 0 | 1 | 2, resumable = false): Generated => {
1265
1406
  const e = new Emitter(root, kind, resumable)
1266
1407
  const main = e.rule(root, true)
1408
+ // Generated code reads past `end` expecting nothing to match there, which
1409
+ // holds when `end` is the input's length, as it is in the runners; other
1410
+ // states run the closure compiler's step, built on first use.
1411
+ let closure: Step | undefined
1412
+ const short = e.const((state: State<any>) =>
1413
+ (closure ??= createClosureCompiler(kind, resumable).compile(root, true))(state)
1414
+ )
1267
1415
  const source =
1268
1416
  `"use strict";let $I,$E,$S,P;${e.functions.join("\n")}\n` +
1269
- `return function run(state){const sI=$I,sE=$E,sS=$S,sP=P;$I=state.input;$E=state.end;$S=state;` +
1270
- `try{const v=${main}(state.pos);if(v!==F)state.pos=P;return v}finally{$I=sI;$E=sE;$S=sS;P=sP}}`
1417
+ `return function run(state){if(state.end!==state.input.length)return ${short}(state);` +
1418
+ `const sI=$I,sE=$E,sS=$S,sP=P;$I=state.input;$E=state.end;$S=state;` +
1419
+ `try{const v=${main}(state.pos);if(v!==F)state.pos=P;return v}` +
1420
+ // A call-stack overflow is a `TooDeep` failure, as `Stack.run` reports it
1421
+ `catch(e){if(!R.isStackOverflow(e))throw e;return R.tooDeep(state)}finally{$I=sI;$E=sE;$S=sS;P=sP}}`
1271
1422
  return { source, constants: e.constants, fallbacks: e.fallbacks }
1272
1423
  }
1273
1424
 
@@ -1276,17 +1427,15 @@ interface JitCompiled extends CompiledRoot {
1276
1427
  readonly source: string
1277
1428
  }
1278
1429
 
1279
- /** Compiles a root parser for one input kind with `new Function`. */
1430
+ /**
1431
+ * Compiles a root parser for one input kind with `new Function`. The step
1432
+ * is the generated entry, which handles every state and reports call-stack
1433
+ * overflows itself.
1434
+ */
1280
1435
  const compileJit = (root: ParserLike, kind: 0 | 1 | 2, resumable = false): JitCompiled => {
1281
1436
  const { constants, fallbacks, source } = generate(root, kind, resumable)
1282
- const run = globalThis.Function("C", "F", "R", source)(constants, Fail, runtime) as Step
1283
- // Generated code reads past `end` expecting nothing to match there, which
1284
- // holds when `end` is the input's length, as it is in the runners.
1285
- let closure: Step | undefined
1286
- const step: Step = (state) =>
1287
- state.end === state.input.length
1288
- ? run(state)
1289
- : (closure ??= createClosureCompiler(kind, resumable).compile(root, true))(state)
1437
+ // SAFETY: the generated source returns its `run` function, which takes a state and returns a value or `Fail`
1438
+ const step = globalThis.Function("C", "F", "R", source)(constants, Fail, runtime) as Step
1290
1439
  return { step, fallbacks, source }
1291
1440
  }
1292
1441
 
@@ -1303,6 +1452,7 @@ export const backend: Backend = {
1303
1452
  onSuccess: ({ fallbacks, source, step }) => ({
1304
1453
  backend: "jit" as const,
1305
1454
  step,
1455
+ guarded: true,
1306
1456
  fallbacks,
1307
1457
  error: undefined,
1308
1458
  source