@thomasfosterau/effect-parse 0.4.1 → 0.5.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 (143) hide show
  1. package/README.md +37 -13
  2. package/dist/Ascii.d.ts +11 -4
  3. package/dist/Ascii.d.ts.map +1 -1
  4. package/dist/Ascii.js +7 -1
  5. package/dist/Ascii.js.map +1 -1
  6. package/dist/Binary.d.ts.map +1 -1
  7. package/dist/Binary.js +64 -35
  8. package/dist/Binary.js.map +1 -1
  9. package/dist/Bits.d.ts.map +1 -1
  10. package/dist/Bits.js +7 -4
  11. package/dist/Bits.js.map +1 -1
  12. package/dist/Compiler.d.ts +74 -2
  13. package/dist/Compiler.d.ts.map +1 -1
  14. package/dist/Compiler.js +78 -1
  15. package/dist/Compiler.js.map +1 -1
  16. package/dist/Expression.d.ts.map +1 -1
  17. package/dist/Expression.js +86 -74
  18. package/dist/Expression.js.map +1 -1
  19. package/dist/Input.d.ts +37 -4
  20. package/dist/Input.d.ts.map +1 -1
  21. package/dist/Input.js +42 -0
  22. package/dist/Input.js.map +1 -1
  23. package/dist/Nom.d.ts +5 -15
  24. package/dist/Nom.d.ts.map +1 -1
  25. package/dist/Nom.js.map +1 -1
  26. package/dist/ParseError.d.ts.map +1 -1
  27. package/dist/ParseError.js.map +1 -1
  28. package/dist/Parser.d.ts +366 -26
  29. package/dist/Parser.d.ts.map +1 -1
  30. package/dist/Parser.js +390 -19
  31. package/dist/Parser.js.map +1 -1
  32. package/dist/ParserSchema.js +1 -1
  33. package/dist/ParserStream.d.ts +3 -2
  34. package/dist/ParserStream.d.ts.map +1 -1
  35. package/dist/ParserStream.js +13 -9
  36. package/dist/ParserStream.js.map +1 -1
  37. package/dist/Syntax.d.ts +23 -5
  38. package/dist/Syntax.d.ts.map +1 -1
  39. package/dist/Syntax.js.map +1 -1
  40. package/dist/Text.d.ts.map +1 -1
  41. package/dist/Text.js +9 -9
  42. package/dist/Text.js.map +1 -1
  43. package/dist/Token.d.ts +55 -10
  44. package/dist/Token.d.ts.map +1 -1
  45. package/dist/Token.js +59 -21
  46. package/dist/Token.js.map +1 -1
  47. package/dist/internal/compiler/closure.d.ts.map +1 -1
  48. package/dist/internal/compiler/closure.js +257 -97
  49. package/dist/internal/compiler/closure.js.map +1 -1
  50. package/dist/internal/compiler/entries.d.ts +3 -1
  51. package/dist/internal/compiler/entries.d.ts.map +1 -1
  52. package/dist/internal/compiler/entries.js +3 -2
  53. package/dist/internal/compiler/entries.js.map +1 -1
  54. package/dist/internal/compiler/first.d.ts.map +1 -1
  55. package/dist/internal/compiler/first.js +7 -1
  56. package/dist/internal/compiler/first.js.map +1 -1
  57. package/dist/internal/compiler/graph.d.ts +17 -0
  58. package/dist/internal/compiler/graph.d.ts.map +1 -0
  59. package/dist/internal/compiler/graph.js +56 -0
  60. package/dist/internal/compiler/graph.js.map +1 -0
  61. package/dist/internal/compiler/jit.d.ts +5 -0
  62. package/dist/internal/compiler/jit.d.ts.map +1 -1
  63. package/dist/internal/compiler/jit.js +185 -96
  64. package/dist/internal/compiler/jit.js.map +1 -1
  65. package/dist/internal/compiler/registry.d.ts +10 -2
  66. package/dist/internal/compiler/registry.d.ts.map +1 -1
  67. package/dist/internal/compiler/registry.js.map +1 -1
  68. package/dist/internal/compiler/search.d.ts +32 -0
  69. package/dist/internal/compiler/search.d.ts.map +1 -0
  70. package/dist/internal/compiler/search.js +68 -0
  71. package/dist/internal/compiler/search.js.map +1 -0
  72. package/dist/internal/compiler/sets.d.ts.map +1 -1
  73. package/dist/internal/compiler/sets.js +1 -1
  74. package/dist/internal/compiler/sets.js.map +1 -1
  75. package/dist/internal/compiler/wasm.d.ts +8 -0
  76. package/dist/internal/compiler/wasm.d.ts.map +1 -0
  77. package/dist/internal/compiler/wasm.js +2769 -0
  78. package/dist/internal/compiler/wasm.js.map +1 -0
  79. package/dist/internal/compiler/wasmAsm.d.ts +199 -0
  80. package/dist/internal/compiler/wasmAsm.d.ts.map +1 -0
  81. package/dist/internal/compiler/wasmAsm.js +427 -0
  82. package/dist/internal/compiler/wasmAsm.js.map +1 -0
  83. package/dist/internal/entries.d.ts +3 -1
  84. package/dist/internal/entries.d.ts.map +1 -1
  85. package/dist/internal/entries.js.map +1 -1
  86. package/dist/internal/node.d.ts +58 -2
  87. package/dist/internal/node.d.ts.map +1 -1
  88. package/dist/internal/node.js +13 -3
  89. package/dist/internal/node.js.map +1 -1
  90. package/dist/internal/numbers.d.ts.map +1 -1
  91. package/dist/internal/numbers.js.map +1 -1
  92. package/dist/internal/run.d.ts +12 -0
  93. package/dist/internal/run.d.ts.map +1 -1
  94. package/dist/internal/run.js +51 -11
  95. package/dist/internal/run.js.map +1 -1
  96. package/dist/internal/stack.d.ts +6 -0
  97. package/dist/internal/stack.d.ts.map +1 -1
  98. package/dist/internal/stack.js +8 -2
  99. package/dist/internal/stack.js.map +1 -1
  100. package/dist/internal/syntax.d.ts +4 -4
  101. package/dist/internal/syntax.d.ts.map +1 -1
  102. package/dist/internal/syntax.js +18 -10
  103. package/dist/internal/syntax.js.map +1 -1
  104. package/dist/internal/text.d.ts +32 -0
  105. package/dist/internal/text.d.ts.map +1 -1
  106. package/dist/internal/text.js +68 -14
  107. package/dist/internal/text.js.map +1 -1
  108. package/dist/internal/tokens.d.ts +13 -0
  109. package/dist/internal/tokens.d.ts.map +1 -1
  110. package/dist/internal/tokens.js +59 -39
  111. package/dist/internal/tokens.js.map +1 -1
  112. package/package.json +5 -4
  113. package/src/Ascii.ts +42 -9
  114. package/src/Binary.ts +151 -71
  115. package/src/Bits.ts +12 -6
  116. package/src/Compiler.ts +101 -10
  117. package/src/Expression.ts +94 -81
  118. package/src/Input.ts +65 -6
  119. package/src/Nom.ts +15 -5
  120. package/src/ParseError.ts +3 -0
  121. package/src/Parser.ts +678 -127
  122. package/src/ParserStream.ts +37 -18
  123. package/src/Syntax.ts +60 -36
  124. package/src/Text.ts +16 -15
  125. package/src/Token.ts +138 -29
  126. package/src/internal/compiler/closure.ts +362 -135
  127. package/src/internal/compiler/entries.ts +4 -2
  128. package/src/internal/compiler/first.ts +10 -0
  129. package/src/internal/compiler/graph.ts +68 -0
  130. package/src/internal/compiler/jit.ts +284 -154
  131. package/src/internal/compiler/registry.ts +10 -2
  132. package/src/internal/compiler/search.ts +68 -0
  133. package/src/internal/compiler/sets.ts +7 -3
  134. package/src/internal/compiler/wasm.ts +3203 -0
  135. package/src/internal/compiler/wasmAsm.ts +484 -0
  136. package/src/internal/entries.ts +8 -2
  137. package/src/internal/node.ts +54 -4
  138. package/src/internal/numbers.ts +3 -1
  139. package/src/internal/run.ts +71 -12
  140. package/src/internal/stack.ts +8 -1
  141. package/src/internal/syntax.ts +39 -18
  142. package/src/internal/text.ts +97 -9
  143. package/src/internal/tokens.ts +96 -14
@@ -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 { 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,7 @@ 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>
222
227
  private readonly closure: ClosureCompiler
223
228
  private next = 0
224
229
 
@@ -228,27 +233,13 @@ class Emitter {
228
233
  readonly resumable = false
229
234
  ) {
230
235
  this.closure = createClosureCompiler(kind, resumable)
231
- this.countRefs(root)
236
+ this.refs = countRefs(root)
232
237
  }
233
238
 
234
239
  get fallbacks(): ReadonlyArray<Fallback> {
235
240
  return this.closure.fallbacks
236
241
  }
237
242
 
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
243
  const(value: unknown): string {
253
244
  let i = this.constantIndex.get(value)
254
245
  if (i === undefined) {
@@ -259,6 +250,13 @@ class Emitter {
259
250
  return `C[${i}]`
260
251
  }
261
252
 
253
+ /** The text of `[start, p)` in `enc`: UTF-8 decodes through the state's ASCII run (see `utf8Text`). */
254
+ text(enc: Encoding<any>, start: string): string {
255
+ const text = `${this.const(enc.text)}(I,${start},p)`
256
+ // Short strings are built by hand, which no run helps
257
+ return enc === utf8 ? `(p-${start}>${ShortText}?R.utf8Text($S,${start},p):${text})` : text
258
+ }
259
+
262
260
  /** A JavaScript literal for simple values, otherwise a constant. */
263
261
  lit(value: unknown): string {
264
262
  if (value === undefined) return "void 0"
@@ -325,29 +323,12 @@ class Emitter {
325
323
  gen(p: ParserLike, used: boolean, out: string | null, fail: string, fn: Fn, depth: number): string {
326
324
  const node = nodeOf(p)
327
325
  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)) {
326
+ if (!isLeaf(node) && (((this.refs.get(p) ?? 0) > 1 && !isSmall(p)) || depth > 32)) {
329
327
  return this.call(this.rule(p, used), out, fail)
330
328
  }
331
329
  return this.inline(p, used, out, fail, fn, depth)
332
330
  }
333
331
 
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
332
  /**
352
333
  * In a resumable build: token code (which fails by breaking out of `bad`)
353
334
  * that read within `lookahead` units of the end of a partial input re-runs
@@ -464,7 +445,8 @@ class Emitter {
464
445
  case "Eof":
465
446
  return (
466
447
  `if(p<E){${BT}${fail}}` +
467
- (this.resumable ? `if($S.partial){$S.mode="Incomplete";${fail}}` : "") +
448
+ // `eof`'s own failure: `Incomplete`, needing one more unit
449
+ (this.resumable ? `if($S.partial){$S.incomplete(1);${fail}}` : "") +
468
450
  set("void 0")
469
451
  )
470
452
  case "Location":
@@ -495,8 +477,8 @@ class Emitter {
495
477
  const code = this.float(node, out, fail, fn, d)
496
478
  if (code !== undefined) return code
497
479
  }
498
- // Streaming builds keep the array, which their checkpoints hold
499
- if (node.f === fromEntries && out !== null && !this.resumable && buildsEntries(node.self)) {
480
+ // Streaming builds too: their checkpoints hold the object built so far
481
+ if (node.f === fromEntries && out !== null && buildsEntries(node.self)) {
500
482
  return this.entries(node.self, out, fail, fn, d)
501
483
  }
502
484
  const t = this.v(fn)
@@ -518,6 +500,22 @@ class Emitter {
518
500
  `${r}=${this.const(node.f)}(${t});if(${r}._tag!=="${tag}"){${BT}${fail}}${set(`${r}.${field}`)}`
519
501
  )
520
502
  }
503
+ case "FastPath": {
504
+ // The fast path runs on the state; when it defers, the grammar runs from where it started
505
+ const st = this.v(fn)
506
+ const t = out ?? this.v(fn)
507
+ const done = this.label()
508
+ return (
509
+ `${st}=p;$S.pos=p;${t}=${this.const(node.fast)}($S);` +
510
+ `${done}:{if(${t}!==${this.const(Defer)}){p=$S.pos;break ${done}}p=${st};` +
511
+ `${this.gen(node.self, used, out, fail, fn, d)}}`
512
+ )
513
+ }
514
+ case "MapOrFail": {
515
+ const t = this.v(fn)
516
+ const r = out ?? this.v(fn)
517
+ return this.gen(node.self, true, t, fail, fn, d) + `${r}=${this.const(node.f)}(${t});if(${r}===F){${BT}${fail}}`
518
+ }
521
519
  case "Filter": {
522
520
  const t = out ?? this.v(fn)
523
521
  return this.gen(node.self, true, t, fail, fn, d) + `if(!${this.const(node.predicate)}(${t})){${BT}${fail}}`
@@ -553,6 +551,12 @@ class Emitter {
553
551
  const extra = Predicate.isTagged(node, "WithSpan") ? `{start:${st},end:p}` : `$S.slice(${st},p)`
554
552
  return `${st}=p;` + this.gen(node.self, true, t, fail, fn, d) + set(`[${t},${extra}]`)
555
553
  }
554
+ case "MapSpan": {
555
+ if (!used) return this.gen(node.self, false, null, fail, fn, d)
556
+ const st = this.v(fn)
557
+ const t = this.v(fn)
558
+ return `${st}=p;` + this.gen(node.self, true, t, fail, fn, d) + set(`${this.const(node.f)}(${t},${st},p)`)
559
+ }
556
560
  case "Span": {
557
561
  const st = this.v(fn)
558
562
  return `${st}=p;` + this.gen(node.self, false, null, fail, fn, d) + set(`{start:${st},end:p}`)
@@ -567,6 +571,7 @@ class Emitter {
567
571
  }
568
572
  case "Context":
569
573
  case "Expected":
574
+ case "Quiet":
570
575
  case "MapError":
571
576
  case "OrFail":
572
577
  case "Trace":
@@ -615,6 +620,12 @@ class Emitter {
615
620
  }
616
621
  return code + `${out}={${node.keys.map((k, i) => `${JSON.stringify(k)}:${vars[i]}`).join(",")}};`
617
622
  }
623
+ case "SeqWith": {
624
+ if (out === null) return node.parsers.map((q) => this.gen(q, false, null, fail, fn, d)).join("")
625
+ const vars = node.parsers.map(() => this.v(fn))
626
+ const code = node.parsers.map((q, i) => this.gen(q, true, vars[i], fail, fn, d)).join("")
627
+ return code + `${out}=${this.const(node.f)}(${vars.join(",")});`
628
+ }
618
629
  case "ZipWith": {
619
630
  if (out === null)
620
631
  return this.gen(node.self, false, null, fail, fn, d) + this.gen(node.that, false, null, fail, fn, d)
@@ -631,10 +642,20 @@ class Emitter {
631
642
  case "Dispatch":
632
643
  return this.dispatch(p, node, used, out, fail, fn, d)
633
644
  case "Repeat":
634
- case "Fold":
635
- return this.repeat(node, used, out, fail, fn, d)
645
+ case "Fold": {
646
+ // A run of a character set whose characters are not kept is a `takeWhile` (#79)
647
+ const run =
648
+ out === null && Predicate.isTagged(node, "Repeat") ? this.repeatRun(p, node, fail, fn, d) : undefined
649
+ return run ?? this.repeat(node, used, out, fail, fn, d)
650
+ }
636
651
  case "Separated":
637
652
  return this.separated(node, used, out, fail, fn, d)
653
+ case "Fixed": {
654
+ // On bytes, read inline; at the end of the input, the step reports it
655
+ if (this.kind !== 1) return this.delegate(p, used, out, fail, fn)
656
+ const read = used ? `${out ?? this.v(fn)}=${this.const(node.read)}(I,p,$S);` : ""
657
+ return `if(E-p>=${node.size}){${read}p+=${node.size}}else{${this.delegate(p, used, out, fail, fn)}}`
658
+ }
638
659
  default:
639
660
  return this.delegate(p, used, out, fail, fn)
640
661
  }
@@ -734,7 +755,7 @@ class Emitter {
734
755
  // The cases are known: number them, and run the chosen one inline
735
756
  const cases = new Map<ParserLike, number>()
736
757
  for (const key of Reflect.ownKeys(node.cases)) {
737
- const q = node.cases[key as any]
758
+ const q = node.cases[key]
738
759
  if (!cases.has(q)) cases.set(q, cases.size)
739
760
  }
740
761
  if (node.otherwise !== undefined && !cases.has(node.otherwise)) cases.set(node.otherwise, cases.size)
@@ -757,9 +778,7 @@ class Emitter {
757
778
  const r = out ?? this.v(fn)
758
779
  return code + `default:{$S.pos=p;${r}=${q}.step($S);if(${r}===F){${fail}}p=$S.pos;}}`
759
780
  }
760
- let sel = nodeOf(node.selector)
761
- const peek = Predicate.isTagged(sel, "Peek")
762
- if (peek) sel = nodeOf((sel as NodeOf<"Peek">).self)
781
+ const { node: sel, peek } = selectorOf(node.selector)
763
782
  if (!Predicate.isTagged(sel, "Any") || !this.native(sel.encoding)) return general(true)
764
783
  const enc = sel.encoding
765
784
  const r = enc === undefined ? undefined : this.reader(enc)
@@ -791,6 +810,55 @@ class Emitter {
791
810
  return code
792
811
  }
793
812
 
813
+ /**
814
+ * A repetition whose elements are not kept, rewritten to scan a run of a
815
+ * character set in one loop instead of one element at a time (#79):
816
+ *
817
+ * - `repeat(oneOf(S), range)` matches what `takeWhile(S, range)` matches,
818
+ * stopping, failing and reporting `Incomplete` where it does, so it
819
+ * compiles as that `takeWhile`, with the repetition's own step as the
820
+ * fallback near the end of a partial input.
821
+ * - `repeat(alt(oneOf(S), ...rest))` with `min` at most 1 and no `max`
822
+ * matches what `repeat(alt(takeWhile(S, { min: 1 }), ...rest))` matches:
823
+ * a run of `S` is a run of one-character elements, and the counts that
824
+ * differ only matter for other ranges. The first branch is a synthetic
825
+ * `takeWhile` node; outside resumable builds, which never run its step.
826
+ *
827
+ * Returns `undefined` when neither applies.
828
+ */
829
+ private repeatRun(p: ParserLike, node: NodeOf<"Repeat">, fail: string, fn: Fn, d: number): string | undefined {
830
+ if (this.kind === 2) return undefined
831
+ const element = nodeOf(node.self)
832
+ if (Predicate.isTagged(element, "OneOf") && this.native(element.encoding)) {
833
+ const { encoding, negate, set } = element
834
+ return this.leaf(
835
+ p,
836
+ { _tag: "TakeWhile", set, negate, encoding, min: node.min, max: node.max },
837
+ false,
838
+ null,
839
+ fail,
840
+ fn
841
+ )
842
+ }
843
+ if (this.resumable || node.min > 1 || node.max !== Infinity || !Predicate.isTagged(element, "Alt")) return undefined
844
+ const [first, ...rest] = element.parsers
845
+ const head = first === undefined ? undefined : nodeOf(first)
846
+ if (
847
+ head === undefined ||
848
+ !Predicate.isTagged(head, "OneOf") ||
849
+ !this.native(head.encoding) ||
850
+ Arr.isReadonlyArrayEmpty(rest)
851
+ ) {
852
+ return undefined
853
+ }
854
+ const run = synthetic(
855
+ { _tag: "TakeWhile", set: head.set, negate: head.negate, encoding: head.encoding, min: 1, max: Infinity },
856
+ node.self.step
857
+ )
858
+ const alt = synthetic({ _tag: "Alt", parsers: [run, ...rest] }, node.self.step)
859
+ return this.repeat({ ...node, self: alt }, false, null, fail, fn, d)
860
+ }
861
+
794
862
  repeat(
795
863
  node: NodeOf<"Repeat"> | NodeOf<"Fold">,
796
864
  used: boolean,
@@ -806,7 +874,7 @@ class Emitter {
806
874
  const n = this.v(fn)
807
875
  const st = this.v(fn)
808
876
  const isFold = Predicate.isTagged(node, "Fold")
809
- const cond = isFold ? (node as NodeOf<"Fold">).while : undefined
877
+ const cond = isFold ? node.while : undefined
810
878
  const keep = out !== null && used && collect === undefined
811
879
  // `while` needs the accumulator even when the output is unused
812
880
  const tracked = keep || cond !== undefined
@@ -816,8 +884,8 @@ class Emitter {
816
884
  let add = ""
817
885
  if (tracked) {
818
886
  if (isFold) {
819
- init = `${acc}=${this.const((node as NodeOf<"Fold">).init)}();`
820
- add = `${acc}=${this.const((node as NodeOf<"Fold">).f)}(${acc},${x});`
887
+ init = `${acc}=${this.const(node.init)}();`
888
+ add = `${acc}=${this.const(node.f)}(${acc},${x});`
821
889
  } else {
822
890
  init = `${acc}=[];`
823
891
  add = `${acc}.push(${x});`
@@ -826,11 +894,13 @@ class Emitter {
826
894
  const check = max === Infinity ? `if(p===${st}){$S.mode="Cut";${fail}}` : ""
827
895
  const bound = max === Infinity ? "" : `${n}<${max}`
828
896
  const r = this.resumable ? this.resumePoint(fn) : undefined
897
+ // A checkpoint holds the result so far: the accumulator, or what `collect` builds
898
+ const held = collect?.value ?? acc
829
899
  const save =
830
900
  r === undefined
831
901
  ? ""
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
902
+ : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${held},${collect?.saved ?? ""}count:${n},pos:${st}});`
903
+ // With `collect`, the element adds itself once the loop check passed
834
904
  const element =
835
905
  collect === undefined
836
906
  ? `${this.gen(node.self, tracked, x, `break ${bad};`, fn, d)}${check}${add}`
@@ -842,7 +912,12 @@ class Emitter {
842
912
  `${bad}:{${element}continue ${loop};}` +
843
913
  `${save}if($S.mode!=="Backtrack"${min > 0 ? `||${n}<${min}` : ""}){${fail}}p=${st};break ${loop};}`
844
914
  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)
915
+ return r.wrap(
916
+ held,
917
+ `${held}=cp.acc;${collect?.restore ?? ""}${n}=cp.count;p=cp.pos;`,
918
+ `${init}${n}=0;`,
919
+ `${body}${collect?.finish ?? ""}`
920
+ )
846
921
  }
847
922
 
848
923
  /**
@@ -887,10 +962,12 @@ class Emitter {
887
962
  const loop = this.label()
888
963
  const push = keep ? `${acc}.push(${x});` : ""
889
964
  const r = this.resumable ? this.resumePoint(fn) : undefined
965
+ // A checkpoint holds the result so far: the accumulator, or what `collect` builds
966
+ const held = collect?.value ?? acc
890
967
  const save =
891
968
  r === undefined
892
969
  ? ""
893
- : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${acc},count:${n},pos:${st}});`
970
+ : `if($S.mode==="Incomplete"&&$S.resume!==void 0)R.save($S,${r.key},${r.entry},{acc:${held},${collect?.saved ?? ""}count:${n},pos:${st}});`
894
971
  // Runs `body`; on its failure, stops the repetition (or fails if `fatal`)
895
972
  const attempt = (body: (fail: string) => string, success: string, fatal: string, checkpoint: string) => {
896
973
  const ok = this.label()
@@ -915,7 +992,12 @@ class Emitter {
915
992
  `}`
916
993
  if (r === undefined) return `${done}:{${first}${rest}}${collect?.finish ?? ""}`
917
994
  // `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}}`)
995
+ return r.wrap(
996
+ held,
997
+ `${held}=cp.acc;${collect?.restore ?? ""}${n}=cp.count;p=cp.pos;`,
998
+ "",
999
+ `${done}:{if(!${r.resumed}){${first}}${rest}}${collect?.finish ?? ""}`
1000
+ )
919
1001
  }
920
1002
 
921
1003
  /**
@@ -927,59 +1009,61 @@ class Emitter {
927
1009
  */
928
1010
  private entries(p: ParserLike, out: string, fail: string, fn: Fn, d: number): string {
929
1011
  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("")
1012
+ if (Predicate.isTagged(node, "Mode")) {
1013
+ const ok = this.label()
1014
+ const bad = this.label()
1015
+ return (
1016
+ `${ok}:{${bad}:{${this.entries(node.self, out, `break ${bad};`, fn, d)}break ${ok};}` +
1017
+ `if($S.mode===${JSON.stringify(node.from)})$S.mode=${JSON.stringify(node.to)};${fail}}`
1018
+ )
1019
+ }
1020
+ if (Predicate.isTagged(node, "Seq")) {
1021
+ // SAFETY: `buildsEntries` passes through a sequence only when it keeps one output, by index
1022
+ const i = node.output as number
1023
+ return node.parsers
1024
+ .map((q, j) => (j === i ? this.entries(q, out, fail, fn, d) : this.gen(q, false, null, fail, fn, d)))
1025
+ .join("")
1026
+ }
1027
+ if (Predicate.isTagged(node, "Repeat") || Predicate.isTagged(node, "Separated")) {
1028
+ const pair = pairOf(node.self)
1029
+ if (pair === undefined) {
1030
+ // Not reached (`buildsEntries` found the pair): build the pairs, then the object
1031
+ const t = this.v(fn)
1032
+ return this.gen(p, true, t, fail, fn, d) + `${out}=${this.const(fromEntries)}(${t});`
944
1033
  }
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
- )
1034
+ const deferred = this.v(fn)
1035
+ const key = this.v(fn)
1036
+ const value = this.v(fn)
1037
+ const collect: Collect = {
1038
+ init: `${out}={};${deferred}=void 0;`,
1039
+ element: (f, check) =>
1040
+ pair.parsers
1041
+ .map((q, j) =>
1042
+ this.gen(
1043
+ q,
1044
+ j === pair.key || j === pair.value,
1045
+ j === pair.key ? key : j === pair.value ? value : null,
1046
+ f,
1047
+ fn,
1048
+ d
969
1049
  )
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)
1050
+ )
1051
+ .join("") +
1052
+ check +
1053
+ `if(${deferred}===void 0&&R.isPlainKey(${key}))${out}[${key}]=${value};` +
1054
+ `else ${deferred}=R.addEntry(${out},${deferred},${key},${value});`,
1055
+ finish: `if(${deferred}!==void 0)R.flushEntries(${out},${deferred});`,
1056
+ value: out,
1057
+ // A streaming build's checkpoint holds the object so far, with its deferred entries
1058
+ saved: `def:${deferred},`,
1059
+ restore: `${deferred}=cp.def;`
979
1060
  }
980
- default:
981
- return this.entries((node as { readonly self: ParserLike }).self, out, fail, fn, d)
1061
+ return Predicate.isTagged(node, "Repeat")
1062
+ ? this.repeat(node, true, out, fail, fn, d, collect)
1063
+ : this.separated(node, true, out, fail, fn, d, collect)
982
1064
  }
1065
+ // SAFETY: the other nodes `buildsEntries` passes through are diagnostics, which all wrap a `self`
1066
+ return this.entries((node as { readonly self: ParserLike }).self, out, fail, fn, d)
983
1067
  }
984
1068
 
985
1069
  // ---------------------------------------------------------------------------
@@ -1027,6 +1111,7 @@ class Emitter {
1027
1111
  ): string | undefined {
1028
1112
  const set = (value: string) => (out === null ? "" : `${out}=${value};`)
1029
1113
  const enc = node.encoding
1114
+ // SAFETY: `leaf`, the only caller, checks `native` first, which never holds on array input (kind 2)
1030
1115
  const kind = this.kind as 0 | 1
1031
1116
  const failBT = `{${BT}${fail}}`
1032
1117
  switch (node._tag) {
@@ -1055,6 +1140,7 @@ class Emitter {
1055
1140
  const lit = node.literal
1056
1141
  if (enc !== undefined) {
1057
1142
  if (node.caseless) return undefined
1143
+ // SAFETY: a `Literal` with an encoding is a `Text` literal, which records the string it encodes
1058
1144
  const encoded = enc.encode(lit as string)
1059
1145
  return (
1060
1146
  this.literalUnits(Predicate.isString(encoded) ? unitsOf(encoded) : Array.from(encoded), false, fail) +
@@ -1062,12 +1148,10 @@ class Emitter {
1062
1148
  )
1063
1149
  }
1064
1150
  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))
1151
+ let codes: Array<number>
1152
+ if (Predicate.isString(lit)) codes = kind === 0 ? unitsOf(lit) : Array.from(new TextEncoder().encode(lit))
1153
+ else if (kind === 1 && lit instanceof Uint8Array) codes = Array.from(lit)
1154
+ else return undefined
1071
1155
  const len = codes.length
1072
1156
  const value =
1073
1157
  !node.caseless && literalKind === kind
@@ -1139,6 +1223,7 @@ class Emitter {
1139
1223
  private takeWhile(node: NodeOf<"TakeWhile">, out: string | null, fail: string, fn: Fn): string {
1140
1224
  const enc = node.encoding
1141
1225
  const { max, min, negate } = node
1226
+ // SAFETY: called from `token` only, whose `kind` is not 2
1142
1227
  const kind = this.kind as 0 | 1
1143
1228
  const cs = enc === undefined ? tokenSet(node.set, kind) : textSet(node.set)
1144
1229
  const accept = acceptTable(cs, negate)
@@ -1149,8 +1234,7 @@ class Emitter {
1149
1234
  const counted = max !== Infinity || min > 1
1150
1235
  const count = counted ? `${n}++;` : ""
1151
1236
  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)`
1237
+ const slice = enc !== undefined ? this.text(enc, st) : kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1154
1238
  const finish =
1155
1239
  (min > 0 ? `if(${counted ? n : `p-${st}`}<${min}){${BT}${fail}}` : "") + (out === null ? "" : `${out}=${slice};`)
1156
1240
  const head = `${st}=p;${counted ? `${n}=0;` : ""}${loop}:while(p<E${bound}){${c}=${this.unit("p")};`
@@ -1173,6 +1257,8 @@ class Emitter {
1173
1257
  `p+=${this.const(enc.width)}(${c})}${count}}${finish}`
1174
1258
  )
1175
1259
  }
1260
+ const stops = negate && !counted ? stopChars(cs) : undefined
1261
+ if (stops !== undefined) return this.stopSearch(stops, st, c, finish, fn)
1176
1262
  const fast = 0xd800
1177
1263
  const small = TableSize
1178
1264
  const highTest = (v: string) => this.memberHigh(cs, negate, v)
@@ -1193,6 +1279,34 @@ class Emitter {
1193
1279
  return `${head}if(${c}<${small}){if(!(${member}))break;p++}else ${rest}${count}}${finish}`
1194
1280
  }
1195
1281
 
1282
+ /**
1283
+ * `Token.takeTill` on a string, of one to `MaxStops` characters that are
1284
+ * whole code points: the set accepts every other unit, surrogates
1285
+ * included, so the run ends at the first stop character. The first
1286
+ * `SearchHead` units are tested one at a time, which beats a native
1287
+ * search on short runs; a longer run ends at the next occurrence that
1288
+ * `nextStop` finds with `indexOf` (#75).
1289
+ */
1290
+ private stopSearch(stops: ReadonlyArray<number>, st: string, c: string, finish: string, fn: Fn): string {
1291
+ const h = this.v(fn)
1292
+ const test = stops.map((u) => `${c}!==${u}`).join("&&")
1293
+ // One stop character is searched for inline: a run that doesn't find it ends at `E`, so it is
1294
+ // searched for once. Several are searched for through `nextStop`'s cache, so that one which
1295
+ // doesn't occur isn't searched for again, to the end of the input, on every run.
1296
+ const search =
1297
+ stops.length === 1
1298
+ ? `{p=I.indexOf(${JSON.stringify(String.fromCharCode(stops[0]))},p);if(p<0)p=E}`
1299
+ : `p=R.nextStop(${this.const(new StopCache(stops.map((u) => String.fromCharCode(u))))},$S,p);`
1300
+ // A cached search costs more than a plain one, so it waits for a longer run
1301
+ const head = stops.length === 1 ? SearchHead : 2 * SearchHead
1302
+ return (
1303
+ `${st}=p;${h}=p+${head}<E?p+${head}:E;` +
1304
+ `while(p<${h}&&(${c}=I.charCodeAt(p),${test}))p++;` +
1305
+ // A run that ends right after the head needs no search
1306
+ `if(p===${h}&&p<E&&(${c}=I.charCodeAt(p),${test}))${search}${finish}`
1307
+ )
1308
+ }
1309
+
1196
1310
  /**
1197
1311
  * `escaped`. In a resumable build it keeps the closure compiler's
1198
1312
  * checkpoints: where the next segment starts and, unless raw, the text
@@ -1222,12 +1336,7 @@ class Emitter {
1222
1336
  const onNormal = raw
1223
1337
  ? `if(p===${b}){$S.mode="Cut";${fail}}continue ${loop};`
1224
1338
  : `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)`
1339
+ const slice = enc !== undefined ? this.text(enc, st) : this.kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1231
1340
  const ctrlTest =
1232
1341
  rd === undefined ? `p<E&&${this.unit("p")}===${ctrl}` : `${rd.guard("p")}&&${rd.unit("p")}===${ctrl}`
1233
1342
  // In a resumable build, running out of a partial input (not stopping
@@ -1239,7 +1348,7 @@ class Emitter {
1239
1348
  `if(${okv}){${onNormal}}else{if($S.mode!=="Backtrack"){${save(b)}${fail}}p=${b}}` +
1240
1349
  `if(${ctrlTest}){p+=${rd?.width ?? 1};${escape}${raw ? "" : `${acc}+=${x};`}}else break ${stop};}`
1241
1350
  const body =
1242
- r === undefined ? loopCode : `${stop}:{${loopCode}if($S.partial){$S.mode="Incomplete";${save("p")}${fail}}}`
1351
+ r === undefined ? loopCode : `${stop}:{${loopCode}if($S.partial){$S.incomplete(0);${save("p")}${fail}}}`
1243
1352
  if (r === undefined) {
1244
1353
  return `${raw ? `${st}=p;` : `${acc}="";`}${body}${raw && out !== null ? `${out}=${slice};` : ""}`
1245
1354
  }
@@ -1250,6 +1359,18 @@ class Emitter {
1250
1359
  }
1251
1360
  }
1252
1361
 
1362
+ /**
1363
+ * A parser that exists only as a description, for the rewrites of
1364
+ * `Emitter.repeatRun`. `step` is never run on the paths they compile (the
1365
+ * token code of a native `takeWhile`, and alternatives inline), so it is
1366
+ * the step of the repetition's element, which describes the same language
1367
+ * when its value is unused.
1368
+ */
1369
+ const synthetic = (node: Node, step: Step): ParserLike => {
1370
+ const described: ParserLike & { readonly [NodeId]: Node } = { step, name: undefined, [NodeId]: node }
1371
+ return described
1372
+ }
1373
+
1253
1374
  /** The UTF-16 code units of a string. */
1254
1375
  const unitsOf = (s: string): Array<number> => Array.from({ length: s.length }, (_, i) => s.charCodeAt(i))
1255
1376
 
@@ -1264,10 +1385,20 @@ interface Generated {
1264
1385
  const generate = (root: ParserLike, kind: 0 | 1 | 2, resumable = false): Generated => {
1265
1386
  const e = new Emitter(root, kind, resumable)
1266
1387
  const main = e.rule(root, true)
1388
+ // Generated code reads past `end` expecting nothing to match there, which
1389
+ // holds when `end` is the input's length, as it is in the runners; other
1390
+ // states run the closure compiler's step, built on first use.
1391
+ let closure: Step | undefined
1392
+ const short = e.const((state: State<any>) =>
1393
+ (closure ??= createClosureCompiler(kind, resumable).compile(root, true))(state)
1394
+ )
1267
1395
  const source =
1268
1396
  `"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}}`
1397
+ `return function run(state){if(state.end!==state.input.length)return ${short}(state);` +
1398
+ `const sI=$I,sE=$E,sS=$S,sP=P;$I=state.input;$E=state.end;$S=state;` +
1399
+ `try{const v=${main}(state.pos);if(v!==F)state.pos=P;return v}` +
1400
+ // A call-stack overflow is a `TooDeep` failure, as `Stack.run` reports it
1401
+ `catch(e){if(!R.isStackOverflow(e))throw e;return R.tooDeep(state)}finally{$I=sI;$E=sE;$S=sS;P=sP}}`
1271
1402
  return { source, constants: e.constants, fallbacks: e.fallbacks }
1272
1403
  }
1273
1404
 
@@ -1276,17 +1407,15 @@ interface JitCompiled extends CompiledRoot {
1276
1407
  readonly source: string
1277
1408
  }
1278
1409
 
1279
- /** Compiles a root parser for one input kind with `new Function`. */
1410
+ /**
1411
+ * Compiles a root parser for one input kind with `new Function`. The step
1412
+ * is the generated entry, which handles every state and reports call-stack
1413
+ * overflows itself.
1414
+ */
1280
1415
  const compileJit = (root: ParserLike, kind: 0 | 1 | 2, resumable = false): JitCompiled => {
1281
1416
  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)
1417
+ // SAFETY: the generated source returns its `run` function, which takes a state and returns a value or `Fail`
1418
+ const step = globalThis.Function("C", "F", "R", source)(constants, Fail, runtime) as Step
1290
1419
  return { step, fallbacks, source }
1291
1420
  }
1292
1421
 
@@ -1303,6 +1432,7 @@ export const backend: Backend = {
1303
1432
  onSuccess: ({ fallbacks, source, step }) => ({
1304
1433
  backend: "jit" as const,
1305
1434
  step,
1435
+ guarded: true,
1306
1436
  fallbacks,
1307
1437
  error: undefined,
1308
1438
  source