@thomasfosterau/effect-parse 0.5.0 → 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 (99) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +19 -1
  3. package/dist/Ascii.d.ts.map +1 -1
  4. package/dist/Ascii.js +5 -3
  5. package/dist/Ascii.js.map +1 -1
  6. package/dist/Binary.d.ts +5 -2
  7. package/dist/Binary.d.ts.map +1 -1
  8. package/dist/Binary.js +20 -3
  9. package/dist/Binary.js.map +1 -1
  10. package/dist/Bits.d.ts.map +1 -1
  11. package/dist/Bits.js +4 -0
  12. package/dist/Bits.js.map +1 -1
  13. package/dist/Input.d.ts +21 -3
  14. package/dist/Input.d.ts.map +1 -1
  15. package/dist/Input.js +31 -4
  16. package/dist/Input.js.map +1 -1
  17. package/dist/Nom.d.ts.map +1 -1
  18. package/dist/ParseError.d.ts +69 -11
  19. package/dist/ParseError.d.ts.map +1 -1
  20. package/dist/ParseError.js +129 -28
  21. package/dist/ParseError.js.map +1 -1
  22. package/dist/Parser.d.ts +10 -4
  23. package/dist/Parser.d.ts.map +1 -1
  24. package/dist/Parser.js +10 -24
  25. package/dist/Parser.js.map +1 -1
  26. package/dist/ParserSchema.d.ts.map +1 -1
  27. package/dist/ParserSchema.js +1 -2
  28. package/dist/ParserSchema.js.map +1 -1
  29. package/dist/ParserStream.d.ts +119 -12
  30. package/dist/ParserStream.d.ts.map +1 -1
  31. package/dist/ParserStream.js +282 -55
  32. package/dist/ParserStream.js.map +1 -1
  33. package/dist/Syntax.js +1 -1
  34. package/dist/Text.d.ts.map +1 -1
  35. package/dist/Text.js +13 -7
  36. package/dist/Text.js.map +1 -1
  37. package/dist/Token.d.ts +10 -1
  38. package/dist/Token.d.ts.map +1 -1
  39. package/dist/Token.js +14 -5
  40. package/dist/Token.js.map +1 -1
  41. package/dist/internal/compiler/closure.d.ts +1 -1
  42. package/dist/internal/compiler/closure.d.ts.map +1 -1
  43. package/dist/internal/compiler/closure.js +7 -17
  44. package/dist/internal/compiler/closure.js.map +1 -1
  45. package/dist/internal/compiler/first.d.ts.map +1 -1
  46. package/dist/internal/compiler/first.js +2 -1
  47. package/dist/internal/compiler/first.js.map +1 -1
  48. package/dist/internal/compiler/graph.d.ts +23 -1
  49. package/dist/internal/compiler/graph.d.ts.map +1 -1
  50. package/dist/internal/compiler/graph.js +27 -5
  51. package/dist/internal/compiler/graph.js.map +1 -1
  52. package/dist/internal/compiler/jit.d.ts.map +1 -1
  53. package/dist/internal/compiler/jit.js +26 -13
  54. package/dist/internal/compiler/jit.js.map +1 -1
  55. package/dist/internal/compiler/registry.d.ts +1 -1
  56. package/dist/internal/compiler/wasm.d.ts.map +1 -1
  57. package/dist/internal/compiler/wasm.js +105 -34
  58. package/dist/internal/compiler/wasm.js.map +1 -1
  59. package/dist/internal/node.d.ts +19 -1
  60. package/dist/internal/node.d.ts.map +1 -1
  61. package/dist/internal/node.js +42 -8
  62. package/dist/internal/node.js.map +1 -1
  63. package/dist/internal/run.d.ts +3 -1
  64. package/dist/internal/run.d.ts.map +1 -1
  65. package/dist/internal/run.js +4 -2
  66. package/dist/internal/run.js.map +1 -1
  67. package/dist/internal/stack.d.ts +8 -0
  68. package/dist/internal/stack.d.ts.map +1 -1
  69. package/dist/internal/stack.js +17 -1
  70. package/dist/internal/stack.js.map +1 -1
  71. package/dist/internal/tokens.d.ts +10 -3
  72. package/dist/internal/tokens.d.ts.map +1 -1
  73. package/dist/internal/tokens.js +22 -9
  74. package/dist/internal/tokens.js.map +1 -1
  75. package/dist/internal/tuple.d.ts +6 -0
  76. package/dist/internal/tuple.d.ts.map +1 -0
  77. package/dist/internal/tuple.js +170 -0
  78. package/dist/internal/tuple.js.map +1 -0
  79. package/package.json +17 -10
  80. package/src/Ascii.ts +5 -3
  81. package/src/Binary.ts +24 -3
  82. package/src/Bits.ts +4 -0
  83. package/src/Input.ts +37 -4
  84. package/src/ParseError.ts +206 -52
  85. package/src/Parser.ts +19 -29
  86. package/src/ParserSchema.ts +1 -2
  87. package/src/ParserStream.ts +336 -62
  88. package/src/Text.ts +16 -12
  89. package/src/Token.ts +14 -5
  90. package/src/internal/compiler/closure.ts +9 -18
  91. package/src/internal/compiler/first.ts +6 -0
  92. package/src/internal/compiler/graph.ts +32 -4
  93. package/src/internal/compiler/jit.ts +37 -17
  94. package/src/internal/compiler/wasm.ts +141 -68
  95. package/src/internal/node.ts +49 -3
  96. package/src/internal/run.ts +4 -2
  97. package/src/internal/stack.ts +17 -0
  98. package/src/internal/tokens.ts +20 -10
  99. package/src/internal/tuple.ts +141 -0
@@ -19,6 +19,7 @@ import { Defer, nodeOf, scansFloat } from "../node.js"
19
19
  import { floatScan, floatTexts, scanFloatBytes, scanFloatString, toFloat } from "../numbers.js"
20
20
  import { charOf, fastLimit, firstCodePoint, fold, textIn, utf16be, utf16le, utf8, utf8Text, validate } from "../text.js"
21
21
  import { countTokens, indexOf, indexOfAny } from "../tokens.js"
22
+ import { tupleStep } from "../tuple.js"
22
23
  import type { AltPlan } from "./first.js"
23
24
  import { Key } from "../key.js"
24
25
  import * as Resume from "../resume.js"
@@ -527,17 +528,7 @@ class Compiler {
527
528
  const steps = node.parsers.map((q, i) => this.compile(q, keep(i)))
528
529
  const n = steps.length
529
530
  if (!used) return this.chain(steps)
530
- if (output === "tuple") {
531
- return (s) => {
532
- const out = new Array(n)
533
- for (let i = 0; i < n; i++) {
534
- const a = steps[i](s)
535
- if (a === Fail) return Fail
536
- out[i] = a
537
- }
538
- return out
539
- }
540
- }
531
+ if (output === "tuple") return tupleStep(steps)
541
532
  if (Predicate.isNumber(output)) {
542
533
  if (n === 2) {
543
534
  const a = steps[0]
@@ -693,7 +684,7 @@ class Compiler {
693
684
 
694
685
  repeat(node: NodeOf<"Repeat"> | NodeOf<"Fold">, used: boolean): Step {
695
686
  const { max, min } = node
696
- const checkLoop = max === Infinity
687
+ // Bounded or not, the repetition checks progress below (#115)
697
688
  const isFold = Predicate.isTagged(node, "Fold")
698
689
  const cond = isFold ? node.while : undefined
699
690
  const c = this.compile(node.self, used)
@@ -740,7 +731,7 @@ class Compiler {
740
731
  s.pos = start
741
732
  return resumed ? Resume.done(s, key, entry, true, acc) : acc
742
733
  }
743
- if (checkLoop && s.pos === start) {
734
+ if (s.pos === start) {
744
735
  s.mode = CUT
745
736
  return Fail
746
737
  }
@@ -812,7 +803,7 @@ class Compiler {
812
803
  return true
813
804
  }
814
805
  const { max, min } = node
815
- const checkLoop = max === Infinity
806
+ // Bounded or not, the repetition checks progress below (#115)
816
807
  // A streaming build's checkpoint holds the object built so far (see `repeat`)
817
808
  const resumable = this.resumable
818
809
  const site = new Key()
@@ -840,7 +831,7 @@ class Compiler {
840
831
  if (!ok) {
841
832
  return stopEntries(s, site, at, resumed, acc, count, start, min)
842
833
  }
843
- if (checkLoop && s.pos === start) {
834
+ if (s.pos === start) {
844
835
  s.mode = CUT
845
836
  return Fail
846
837
  }
@@ -872,7 +863,7 @@ class Compiler {
872
863
  if (sep(s) === Fail) {
873
864
  return stopEntries(s, site, at, resumed, acc, count, start, min)
874
865
  }
875
- if (checkLoop && s.pos === start) {
866
+ if (s.pos === start) {
876
867
  s.mode = CUT
877
868
  return Fail
878
869
  }
@@ -913,7 +904,7 @@ class Compiler {
913
904
 
914
905
  separated(node: NodeOf<"Separated">, used: boolean): Step {
915
906
  const { max, min } = node
916
- const checkLoop = max === Infinity
907
+ // Bounded or not, the repetition checks progress below (#115)
917
908
  const c = this.compile(node.self, used)
918
909
  const sep = this.compile(node.separator, false)
919
910
  const resumable = this.resumable
@@ -961,7 +952,7 @@ class Compiler {
961
952
  s.pos = start
962
953
  return resumed ? Resume.done(s, key, entry, true, acc) : acc
963
954
  }
964
- if (checkLoop && s.pos === start) {
955
+ if (s.pos === start) {
965
956
  s.mode = CUT
966
957
  return Fail
967
958
  }
@@ -173,6 +173,12 @@ export class FirstSets {
173
173
  const f = this.of(node.self)
174
174
  return node.from === "Backtrack" ? (f.mode === Unknown ? f : unknown) : f
175
175
  }
176
+ // `not(p)` consumes nothing. Where `p`'s first set rules a unit out, `p`
177
+ // backtracks there, so `not(p)` succeeds without consuming: Optional
178
+ // over `p`'s units. On a unit in that set it may also fail, Cut
179
+ // included, which Optional leaves open. So `seq(not(p), q)` starts
180
+ // with `p`'s units and `q`'s, and an `alt` can skip it on any other (#84)
181
+ case "Not":
176
182
  case "Opt":
177
183
  case "OrElseSucceed": {
178
184
  const f = this.of(node.self)
@@ -19,7 +19,16 @@ export const isLeaf = (node: Node): boolean =>
19
19
  Predicate.isTagged(node, "OneOf") ||
20
20
  Predicate.isTagged(node, "TakeWhile")
21
21
 
22
- /** The number of distinct parents of each parser reachable from `root`. */
22
+ /**
23
+ * How many times each parser reachable from `root` is used: one for every
24
+ * place a parent mentions it. A parser used twice is generated once, as a
25
+ * rule the two places call (unless it is small, see {@link isSmall}).
26
+ *
27
+ * Uses are counted, not distinct parents: `seq(p, p)` uses `p` twice. When
28
+ * only parents were counted, such a `p` was inlined at both places, and a
29
+ * grammar that does it at every level (`p = seq(p, p)`, twenty times over)
30
+ * compiled to 2^20 copies: 160 MB of source from twenty lines (#122).
31
+ */
23
32
  export const countRefs = (root: ParserLike): WeakMap<ParserLike, number> => {
24
33
  const refs = new WeakMap<ParserLike, number>()
25
34
  const seen = new WeakSet<ParserLike>()
@@ -27,10 +36,7 @@ export const countRefs = (root: ParserLike): WeakMap<ParserLike, number> => {
27
36
  for (let p = stack.pop(); p !== undefined; p = stack.pop()) {
28
37
  if (seen.has(p)) continue
29
38
  seen.add(p)
30
- const counted = new WeakSet<ParserLike>()
31
39
  for (const c of children(nodeOf(p))) {
32
- if (counted.has(c)) continue
33
- counted.add(c)
34
40
  refs.set(c, (refs.get(c) ?? 0) + 1)
35
41
  stack.push(c)
36
42
  }
@@ -38,6 +44,28 @@ export const countRefs = (root: ParserLike): WeakMap<ParserLike, number> => {
38
44
  return refs
39
45
  }
40
46
 
47
+ /**
48
+ * The most parsers a code-generating compiler inlines for one root. Sharing
49
+ * is what keeps generated code proportional to the grammar; a grammar that
50
+ * is itself enormous (a tree of distinct parsers built by a loop, a derived
51
+ * schema) would still take minutes and gigabytes to generate, so past this
52
+ * many the compiler gives up and the next one down takes over (the JIT for
53
+ * WebAssembly, the closure compiler for the JIT), which costs a closure per
54
+ * parser instead of source text. The largest grammars in this repository
55
+ * and the libraries built on it inline a few thousand.
56
+ */
57
+ export const MaxInlined = 100_000
58
+
59
+ /** Counts a parser a compiler is about to inline, failing the compilation past {@link MaxInlined}. */
60
+ export const countInlined = (inlined: number): number => {
61
+ if (inlined >= MaxInlined) {
62
+ throw new RangeError(
63
+ `effect-parse: the grammar is too large to generate code for (over ${MaxInlined} parsers inlined)`
64
+ )
65
+ }
66
+ return inlined + 1
67
+ }
68
+
41
69
  /** Whether a parser is small enough to inline wherever it is used. */
42
70
  export const isSmall = (p: ParserLike): boolean => {
43
71
  let budget = 6
@@ -45,7 +45,7 @@ import {
45
45
  import type { ClosureCompiler, CompiledRoot, Fallback, Step } from "./closure.js"
46
46
  import { backend as closureBackend, createClosureCompiler } from "./closure.js"
47
47
  import { buildsEntries, pairOf } from "./entries.js"
48
- import { countRefs, isLeaf, isLetter, isSmall, lowerAscii, selectorOf } from "./graph.js"
48
+ import { countInlined, countRefs, isLeaf, isLetter, isSmall, lowerAscii, selectorOf } from "./graph.js"
49
49
  import type { Backend } from "./registry.js"
50
50
  import type { First } from "./first.js"
51
51
  import { planAlt, Strict } from "./first.js"
@@ -224,6 +224,8 @@ class Emitter {
224
224
  readonly functions: Array<string> = []
225
225
  private readonly rules = [new Map<ParserLike, string>(), new Map<ParserLike, string>()]
226
226
  private readonly refs: WeakMap<ParserLike, number>
227
+ /** How many parsers have been inlined (see `countInlined`). */
228
+ private inlined = 0
227
229
  private readonly closure: ClosureCompiler
228
230
  private next = 0
229
231
 
@@ -257,6 +259,18 @@ class Emitter {
257
259
  return enc === utf8 ? `(p-${start}>${ShortText}?R.utf8Text($S,${start},p):${text})` : text
258
260
  }
259
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
+
260
274
  /** A JavaScript literal for simple values, otherwise a constant. */
261
275
  lit(value: unknown): string {
262
276
  if (value === undefined) return "void 0"
@@ -401,16 +415,16 @@ class Emitter {
401
415
 
402
416
  private arrayOneOf(node: NodeOf<"OneOf">, out: string | null, fail: string): string {
403
417
  const m = this.const(compileAny(node.set).forCode.token)
404
- 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++;`
405
419
  }
406
420
 
407
421
  private arrayTakeWhile(node: NodeOf<"TakeWhile">, out: string | null, fail: string, fn: Fn): string {
408
422
  const m = this.const(compileAny(node.set).forCode.token)
409
423
  const st = this.v(fn)
410
- const max = node.max === Infinity ? "" : `&&p-${st}<${node.max}`
424
+ const max = node.max === Infinity ? "" : `&&p-${st}<${this.num(node.max)}`
411
425
  return (
412
- `${st}=p;while(p<E${max}&&${m}(I[p])!==${node.negate})p++;` +
413
- (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}}` : "") +
414
428
  (out === null ? "" : `${out}=I.slice(${st},p);`)
415
429
  )
416
430
  }
@@ -432,6 +446,7 @@ class Emitter {
432
446
  }
433
447
 
434
448
  inline(p: ParserLike, used: boolean, out: string | null, fail: string, fn: Fn, depth: number): string {
449
+ this.inlined = countInlined(this.inlined)
435
450
  const node = nodeOf(p)
436
451
  const set = (value: string) => (out === null ? "" : `${out}=${value};`)
437
452
  const d = depth + 1
@@ -654,7 +669,7 @@ class Emitter {
654
669
  // On bytes, read inline; at the end of the input, the step reports it
655
670
  if (this.kind !== 1) return this.delegate(p, used, out, fail, fn)
656
671
  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)}}`
672
+ return `if(E-p>=${this.num(node.size)}){${read}p+=${this.num(node.size)}}else{${this.delegate(p, used, out, fail, fn)}}`
658
673
  }
659
674
  default:
660
675
  return this.delegate(p, used, out, fail, fn)
@@ -891,8 +906,8 @@ class Emitter {
891
906
  add = `${acc}.push(${x});`
892
907
  }
893
908
  }
894
- const check = max === Infinity ? `if(p===${st}){$S.mode="Cut";${fail}}` : ""
895
- 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)}`
896
911
  const r = this.resumable ? this.resumePoint(fn) : undefined
897
912
  // A checkpoint holds the result so far: the accumulator, or what `collect` builds
898
913
  const held = collect?.value ?? acc
@@ -906,11 +921,13 @@ class Emitter {
906
921
  ? `${this.gen(node.self, tracked, x, `break ${bad};`, fn, d)}${check}${add}`
907
922
  : collect.element(`break ${bad};`, check)
908
923
  const stop =
909
- 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};`
910
927
  const body =
911
928
  `${loop}:for(;${bound};${n}++){${stop}${st}=p;` +
912
929
  `${bad}:{${element}continue ${loop};}` +
913
- `${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};}`
914
931
  if (r === undefined) return `${init}${n}=0;${body}${collect?.finish ?? ""}`
915
932
  return r.wrap(
916
933
  held,
@@ -980,15 +997,15 @@ class Emitter {
980
997
  const self = (f: string) =>
981
998
  collect === undefined ? this.gen(node.self, keep, x, f, fn, d) : collect.element(f, "")
982
999
  const sep = (f: string) => this.gen(node.separator, false, null, f, fn, d)
983
- const check = max === Infinity ? `if(p===${st}){$S.mode="Cut";${fail}}` : ""
1000
+ const check = `if(p===${st}){$S.mode="Cut";${fail}}`
984
1001
  const first =
985
1002
  `${collect?.init ?? (keep ? `${acc}=[];` : "")}${n}=0;${st}=p;` +
986
1003
  attempt(self, `${push}${n}++;`, min > 0 ? "||true" : "", "")
987
1004
  const rest =
988
- `${loop}:while(${max === Infinity ? "true" : `${n}<${max}`}){${st}=p;` +
989
- 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) +
990
1007
  check +
991
- attempt(self, `${push}${n}++;continue ${loop};`, `||${n}<${min}`, save) +
1008
+ attempt(self, `${push}${n}++;continue ${loop};`, `||${n}<${this.num(min)}`, save) +
992
1009
  `}`
993
1010
  if (r === undefined) return `${done}:{${first}${rest}}${collect?.finish ?? ""}`
994
1011
  // `done` ends the repetition; the checkpoint wrapper records the result after it
@@ -1233,10 +1250,11 @@ class Emitter {
1233
1250
  const loop = this.label()
1234
1251
  const counted = max !== Infinity || min > 1
1235
1252
  const count = counted ? `${n}++;` : ""
1236
- const bound = max !== Infinity ? `&&${n}<${max}` : ""
1253
+ const bound = max !== Infinity ? `&&${n}<${this.num(max)}` : ""
1237
1254
  const slice = enc !== undefined ? this.text(enc, st) : kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1238
1255
  const finish =
1239
- (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};`)
1240
1258
  const head = `${st}=p;${counted ? `${n}=0;` : ""}${loop}:while(p<E${bound}){${c}=${this.unit("p")};`
1241
1259
  const member = this.member(accept, c)
1242
1260
  if (kind === 1 && (enc === undefined || fastLimit(enc) >= TableSize)) {
@@ -1338,7 +1356,9 @@ class Emitter {
1338
1356
  : `if(p!==${b}){${acc}+=${x};continue ${loop}}`
1339
1357
  const slice = enc !== undefined ? this.text(enc, st) : this.kind === 0 ? `I.slice(${st},p)` : `I.subarray(${st},p)`
1340
1358
  const ctrlTest =
1341
- 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)}`
1342
1362
  // In a resumable build, running out of a partial input (not stopping
1343
1363
  // before a non-control character) leaves the text incomplete
1344
1364
  const stop = r === undefined ? loop : this.label()
@@ -35,7 +35,7 @@ import { charOf, fastLimit, latin1, string, utf16be, utf16le, utf8 } from "../te
35
35
  import type { Fallback, Step } from "./closure.js"
36
36
  import { backend as jitBackend, jitSupported } from "./jit.js"
37
37
  import { buildsEntries, isDiagnostic, pairOf } from "./entries.js"
38
- import { countRefs, isLeaf, isLetter, isSmall, lowerAscii, selectorOf } from "./graph.js"
38
+ import { countInlined, countRefs, isLeaf, isLetter, isSmall, lowerAscii, selectorOf } from "./graph.js"
39
39
  import type { Backend, Compiled } from "./registry.js"
40
40
  import type { CharSet } from "./sets.js"
41
41
  import { kindOfEncoding, TableSize, textSet, tokenSet } from "./sets.js"
@@ -80,6 +80,32 @@ const MaxWrites = 4096
80
80
 
81
81
  /** Bytes left after the cursor beyond which a driver's parse of the same buffer runs the JIT (see `run`). */
82
82
  const LargeRest = 4096
83
+ /**
84
+ * The most linear memory a module is grown to: 2 GiB. Every offset into the
85
+ * memory, the trace's included, is then a non-negative `i32`. A parse that
86
+ * would need more (its input copied, and four to seven times as much of
87
+ * trace), or whose `memory.grow` the engine refuses, runs on the JIT, which
88
+ * needs neither (#123).
89
+ */
90
+ const MaxMemory = 2 ** 31
91
+ /**
92
+ * Above this much linear memory after a parse (128 MiB), the module is
93
+ * given a new memory: `WebAssembly.Memory` never shrinks, so one large
94
+ * input would otherwise hold five to ten times its size for the life of
95
+ * the process, per compiled parser and input kind.
96
+ */
97
+ const ReleaseAbove = 128 * 1024 * 1024
98
+ /** What `grow` throws when the trace cannot grow: the parse starts again on the JIT. */
99
+ class MemoryExhausted {}
100
+ /**
101
+ * A repetition bound as the `i32` it is compared with, unsigned. A count
102
+ * never reaches 2^32 - 1 (the input is in a 32-bit memory), so a larger
103
+ * minimum is one that cannot be met, and a larger maximum is none at all
104
+ * (see `unbounded`); written as given, it was truncated to its low 32 bits.
105
+ */
106
+ const bound = (n: number): number => (n >= 0xffffffff ? 0xffffffff : n)
107
+ /** Whether a maximum can never be reached by a count (see {@link bound}). */
108
+ const unbounded = (max: number): boolean => max >= 0xffffffff
83
109
 
84
110
  /** The most literals a compiled `takeUntilAny` checks at each candidate; more are searched in JavaScript. */
85
111
  const MaxUntil = 64
@@ -372,6 +398,8 @@ class WasmCompiler {
372
398
  private readonly rules = [new WeakMap<ParserLike, number>(), new WeakMap<ParserLike, number>()]
373
399
  private readonly pending: Array<readonly [ParserLike, boolean, number]> = []
374
400
  private refs = new WeakMap<ParserLike, number>()
401
+ /** How many parsers have been inlined (see `countInlined`). */
402
+ private inlined = 0
375
403
  private readonly decoders = new WeakMap<Encoding<any>, number>()
376
404
  private readonly floatScanners = new WeakMap<Encoding<any>, number>()
377
405
  /** The float scanner of `Token` / `Ascii` text, which has no encoding. */
@@ -662,6 +690,7 @@ class WasmCompiler {
662
690
  }
663
691
 
664
692
  inline(p: ParserLike, out: string, b: Body, fail: Label, bf: Vars, depth: number): string {
693
+ this.inlined = countInlined(this.inlined)
665
694
  const node = nodeOf(p)
666
695
  const s: Site = { p, out, b, fail, bf, d: depth + 1 }
667
696
  const families: ReadonlyArray<(node: Node, s: Site) => Option.Option<string>> = [
@@ -1257,7 +1286,7 @@ class WasmCompiler {
1257
1286
  private stop(b: Body, fail: Label, n: number, min: number, restore: () => void, done: Label): void {
1258
1287
  this.unlessBacktrack(b, fail)
1259
1288
  if (min > 0) {
1260
- b.get(n).i32(min).op(Op.i32LtU)
1289
+ b.get(n).i32(bound(min)).op(Op.i32LtU)
1261
1290
  b.if(() => b.br(fail))
1262
1291
  }
1263
1292
  restore()
@@ -1299,13 +1328,13 @@ class WasmCompiler {
1299
1328
  b.i32(0).set(n)
1300
1329
  b.block((done) => {
1301
1330
  this.loop(b, (again) => {
1302
- if (max !== Infinity) b.get(n).i32(max).op(Op.i32GeU).brIf(done)
1331
+ if (!unbounded(max)) b.get(n).i32(bound(max)).op(Op.i32GeU).brIf(done)
1303
1332
  const saved = this.save(b)
1304
1333
  this.attempt(
1305
1334
  b,
1306
1335
  (bad) => {
1307
1336
  elementCode = element(b, bad, bf)
1308
- if (max === Infinity) this.progress(b, fail, saved[0])
1337
+ this.progress(b, fail, saved[0])
1309
1338
  this.nextElement(b, n, again)
1310
1339
  },
1311
1340
  () => this.stop(b, fail, n, min, () => this.restore(b, saved), done)
@@ -1366,14 +1395,14 @@ class WasmCompiler {
1366
1395
  }
1367
1396
  )
1368
1397
  this.loop(b, (again) => {
1369
- if (max !== Infinity) b.get(n).i32(max).op(Op.i32GeU).brIf(done)
1398
+ if (!unbounded(max)) b.get(n).i32(bound(max)).op(Op.i32GeU).brIf(done)
1370
1399
  mark()
1371
1400
  this.attempt(
1372
1401
  b,
1373
1402
  (bad) => this.gen(node.separator, "", b, bad, bf, d),
1374
1403
  () => this.stop(b, fail, n, min, back, done)
1375
1404
  )
1376
- if (max === Infinity) this.progress(b, fail, st)
1405
+ this.progress(b, fail, st)
1377
1406
  this.attempt(
1378
1407
  b,
1379
1408
  (bad) => {
@@ -1673,14 +1702,14 @@ class WasmCompiler {
1673
1702
  b.loop((again) => {
1674
1703
  this.atEnd(b, 0)
1675
1704
  b.brIf(stop)
1676
- if (max !== Infinity) b.get(n).i32(max).op(Op.i32GeU).brIf(stop)
1705
+ if (!unbounded(max)) b.get(n).i32(bound(max)).op(Op.i32GeU).brIf(stop)
1677
1706
  this.whileStep(enc, b, set, w, wide, stop)
1678
1707
  if (counted) b.get(n).i32(1).op(Op.i32Add).set(n)
1679
1708
  b.br(again)
1680
1709
  })
1681
1710
  })
1682
1711
  if (min > 0) {
1683
- if (counted) b.get(n).i32(min).op(Op.i32LtU)
1712
+ if (counted) b.get(n).i32(bound(min)).op(Op.i32LtU)
1684
1713
  else b.get(0).get(st).op(Op.i32Eq)
1685
1714
  this.failIf(b, s.fail)
1686
1715
  }
@@ -3004,6 +3033,13 @@ const compileWasm = (root: ParserLike, kind: 0 | 1): Result.Result<Compiled, str
3004
3033
  return run(root, c, bytes, kind, build, source)
3005
3034
  }
3006
3035
 
3036
+ /** What a parse calls in an instance of a module: its functions are typed by the module's own type section. */
3037
+ interface Entries {
3038
+ readonly widen: Function
3039
+ readonly start: Function
3040
+ readonly mode: WebAssembly.Global
3041
+ }
3042
+
3007
3043
  /** A global of a module's exports. */
3008
3044
  const exportedGlobal = (exports: WebAssembly.Exports, name: string): Result.Result<WebAssembly.Global, string> => {
3009
3045
  const g = exports[name]
@@ -3018,7 +3054,7 @@ const run = (
3018
3054
  build: Builder,
3019
3055
  source: string
3020
3056
  ): Result.Result<Compiled, string> => {
3021
- const memory = new WebAssembly.Memory({ initial: 2 })
3057
+ let memory = new WebAssembly.Memory({ initial: 2 })
3022
3058
  // `memory.buffer` is slow to read: its size, as of the latest growth
3023
3059
  let size = memory.buffer.byteLength
3024
3060
  let i32 = new Int32Array(memory.buffer)
@@ -3029,13 +3065,21 @@ const run = (
3029
3065
  f64 = new Float64Array(memory.buffer)
3030
3066
  u8 = new Uint8Array(memory.buffer)
3031
3067
  }
3032
- /** Grows the memory to at least `bytes`, doubling it. */
3033
- const reserve = (bytes: number) => {
3068
+ /**
3069
+ * Grows the memory to at least `bytes`, doubling it: false when that is
3070
+ * more than {@link MaxMemory}, or more than the engine will give.
3071
+ */
3072
+ const reserve = (bytes: number): boolean => {
3034
3073
  const have = size
3035
- if (have >= bytes) return
3036
- memory.grow(Math.ceil((Math.max(bytes, have * 2) - have) / PageSize))
3074
+ if (have >= bytes) return true
3075
+ if (bytes > MaxMemory) return false
3076
+ const pages = Math.ceil((Math.min(Math.max(bytes, have * 2), MaxMemory) - have) / PageSize)
3077
+ // `grow` throws a `RangeError` when the engine cannot allocate the pages
3078
+ const grown = Result.try(() => memory.grow(pages))
3079
+ if (Result.isFailure(grown)) return false
3037
3080
  size = memory.buffer.byteLength
3038
3081
  views()
3082
+ return true
3039
3083
  }
3040
3084
  const steps: Array<Step> = c.islands.map(({ parser }) => jitBackend.build(parser, kind, false).step ?? parser.step)
3041
3085
  const used = c.islands.map(({ used }) => used)
@@ -3043,58 +3087,59 @@ const run = (
3043
3087
  const checks = c.checks
3044
3088
  let state: State<any> = idle
3045
3089
  const values: Array<Value> = []
3046
- const imports = {
3047
- env: {
3048
- mem: memory,
3049
- island: (id: number, pos: number): number => {
3050
- const s = state
3051
- s.pos = pos
3052
- const a = steps[id](s)
3053
- if (a === Fail) return s.mode === "Cut" ? -2 : -1
3054
- if (used[id]) values.push(a)
3055
- return s.pos
3056
- },
3057
- // Doubles the memory, returning the new limit of the trace
3058
- grow: (): number => {
3059
- reserve(size * 2)
3060
- // The views changed
3061
- build.bind(i32, f64, state.input, state.end, values, state)
3062
- return size - Slack
3063
- },
3064
- test: (id: number, cp: number): number => tests[id](cp),
3065
- // A value island: the value of its parser, built from its trace at `from`, and the callback
3066
- check: (id: number, from: number): number => {
3067
- const s = state
3068
- const a = checks[id](build.value(id, from >> 2))
3069
- if (a === Fail) {
3070
- s.mode = "Backtrack"
3071
- return 0
3072
- }
3073
- values.push(a)
3074
- return 1
3075
- },
3076
- // The value of the float text at `[start, end)`, which is ASCII: one byte each, or a
3077
- // byte and a zero byte in UTF-16
3078
- float: (start: number, end: number): number => {
3079
- const input: string | Uint8Array = state.input
3080
- return toFloat(
3081
- Predicate.isString(input) ? input.slice(start, end) : latin1.text(input, start, end).replace(/\0/g, "")
3082
- )
3090
+ const env = {
3091
+ island: (id: number, pos: number): number => {
3092
+ const s = state
3093
+ s.pos = pos
3094
+ const a = steps[id](s)
3095
+ if (a === Fail) return s.mode === "Cut" ? -2 : -1
3096
+ if (used[id]) values.push(a)
3097
+ return s.pos
3098
+ },
3099
+ // Doubles the memory, returning the new limit of the trace
3100
+ grow: (): number => {
3101
+ if (size >= MaxMemory || !reserve(Math.min(size * 2, MaxMemory))) throw new MemoryExhausted()
3102
+ // The views changed
3103
+ build.bind(i32, f64, state.input, state.end, values, state)
3104
+ return size - Slack
3105
+ },
3106
+ test: (id: number, cp: number): number => tests[id](cp),
3107
+ // A value island: the value of its parser, built from its trace at `from`, and the callback
3108
+ check: (id: number, from: number): number => {
3109
+ const s = state
3110
+ // `from` is a byte offset below 2 GiB (`MaxMemory`); unsigned all the same, were it ever not
3111
+ const a = checks[id](build.value(id, from >>> 2))
3112
+ if (a === Fail) {
3113
+ s.mode = "Backtrack"
3114
+ return 0
3083
3115
  }
3116
+ values.push(a)
3117
+ return 1
3118
+ },
3119
+ // The value of the float text at `[start, end)`, which is ASCII: one byte each, or a
3120
+ // byte and a zero byte in UTF-16
3121
+ float: (start: number, end: number): number => {
3122
+ const input: string | Uint8Array = state.input
3123
+ return toFloat(
3124
+ Predicate.isString(input) ? input.slice(start, end) : latin1.text(input, start, end).replace(/\0/g, "")
3125
+ )
3084
3126
  }
3085
3127
  }
3086
- const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), imports)
3087
- const exported = instance.exports
3088
- const rootFn = exported.root
3089
- if (!Predicate.isFunction(rootFn)) return Result.fail("no root function")
3090
- const widenFn = kind === 0 ? exported.widen : rootFn
3091
- if (!Predicate.isFunction(widenFn)) return Result.fail("no widen function")
3092
- const startFn = exported.start
3093
- if (!Predicate.isFunction(startFn)) return Result.fail("no start function")
3094
- const names = ["mode"] as const
3095
- const globals = Result.all(names.map((name) => exportedGlobal(exported, name)))
3096
- if (Result.isFailure(globals)) return Result.fail(globals.failure)
3097
- const [mode] = globals.success
3128
+ const module = new WebAssembly.Module(bytes)
3129
+ /** The entry points of an instance of the module over `mem`. */
3130
+ const instantiate = (mem: WebAssembly.Memory): Result.Result<Entries, string> => {
3131
+ const exported = new WebAssembly.Instance(module, { env: { ...env, mem } }).exports
3132
+ const rootFn = exported.root
3133
+ if (!Predicate.isFunction(rootFn)) return Result.fail("no root function")
3134
+ const widenFn = kind === 0 ? exported.widen : rootFn
3135
+ if (!Predicate.isFunction(widenFn)) return Result.fail("no widen function")
3136
+ const startFn = exported.start
3137
+ if (!Predicate.isFunction(startFn)) return Result.fail("no start function")
3138
+ return Result.map(exportedGlobal(exported, "mode"), (mode) => ({ widen: widenFn, start: startFn, mode }))
3139
+ }
3140
+ const first = instantiate(memory)
3141
+ if (Result.isFailure(first)) return Result.fail(first.failure)
3142
+ let entries = first.success
3098
3143
  let busy = false
3099
3144
  /** The JIT's step for the parser, built on first use. */
3100
3145
  let fallback: Step = (s) => {
@@ -3110,7 +3155,7 @@ const run = (
3110
3155
  */
3111
3156
  const copyString = (input: string, staging: number) => {
3112
3157
  const { written } = utf8Encoder.encodeInto(input, u8.subarray(staging, staging + 3 * input.length))
3113
- const replaced: number = widenFn(staging, written)
3158
+ const replaced: number = entries.widen(staging, written)
3114
3159
  if (replaced === 0) return
3115
3160
  // U+FFFD in the string, or a lone surrogate that `encodeInto` replaced by it: its unit, as it is
3116
3161
  const view = new DataView(memory.buffer)
@@ -3128,7 +3173,9 @@ const run = (
3128
3173
  const inputBytes = kind === 0 ? 2 * n : n
3129
3174
  const traceStart = (Input + inputBytes + 15) & -16
3130
3175
  // Strings are staged where the trace goes: 3 bytes of UTF-8 and 4 of list at most per unit
3131
- reserve(traceStart + Math.max(Slack * 4, inputBytes * 4, kind === 0 ? 7 * n + 16 : 0))
3176
+ if (!reserve(traceStart + Math.max(Slack * 4, inputBytes * 4, kind === 0 ? 7 * n + 16 : 0))) {
3177
+ throw new MemoryExhausted()
3178
+ }
3132
3179
  if (Predicate.isString(input)) {
3133
3180
  // Strings never change: one already in memory (parsed again, or from another offset) stays there
3134
3181
  if (input !== copied) {
@@ -3143,13 +3190,30 @@ const run = (
3143
3190
  values.length = 0
3144
3191
  state = s
3145
3192
  build.bind(i32, f64, input, s.end, values, s)
3146
- const r: number = startFn(s.pos, s.end, n, traceStart, size - Slack)
3193
+ const r: number = entries.start(s.pos, s.end, n, traceStart, size - Slack)
3147
3194
  if (r < 0) {
3148
- s.mode = mode.value === Cut ? "Cut" : "Backtrack"
3195
+ s.mode = entries.mode.value === Cut ? "Cut" : "Backtrack"
3149
3196
  return Fail
3150
3197
  }
3151
3198
  s.pos = r
3152
- return build.root(traceStart >> 2)
3199
+ return build.root(traceStart >>> 2)
3200
+ }
3201
+ /**
3202
+ * Gives the module a new memory of the initial size, dropping the one a
3203
+ * large parse grew (and the string it held). An instance is bound to its
3204
+ * memory, so the module is instantiated again; its data segments fill
3205
+ * the tables in.
3206
+ */
3207
+ const release = () => {
3208
+ const fresh = new WebAssembly.Memory({ initial: 2 })
3209
+ const next = instantiate(fresh)
3210
+ // The module instantiated once already, with these imports; were it to fail, the grown memory stays
3211
+ if (Result.isFailure(next)) return
3212
+ memory = fresh
3213
+ size = fresh.buffer.byteLength
3214
+ views()
3215
+ copied = ""
3216
+ entries = next.success
3153
3217
  }
3154
3218
  /** The bytes copied by the latest parse: parsing them again from further on is a driver at work. */
3155
3219
  let lastBytes: Uint8Array = new Uint8Array(0)
@@ -3170,13 +3234,22 @@ const run = (
3170
3234
  busy = true
3171
3235
  // Whatever happens (a callback may throw), the next parse starts afresh
3172
3236
  pending = s
3237
+ const from = s.pos
3173
3238
  const result = Result.try(parsePending)
3174
3239
  build.unbind()
3175
3240
  state = idle
3176
3241
  pending = idle
3177
3242
  busy = false
3178
3243
  values.length = 0
3179
- if (Result.isFailure(result)) throw result.failure
3244
+ if (size > ReleaseAbove) release()
3245
+ if (Result.isFailure(result)) {
3246
+ // The input, or its trace, needs more memory than there is: the JIT parses it in place
3247
+ if (result.failure instanceof MemoryExhausted) {
3248
+ s.pos = from
3249
+ return fallback(s)
3250
+ }
3251
+ throw result.failure
3252
+ }
3180
3253
  return result.success
3181
3254
  }
3182
3255
  return Result.succeed({ backend: "wasm", step, guarded: false, fallbacks: c.fallbacks, error: undefined, source })