@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.
- package/CHANGELOG.md +202 -0
- package/README.md +19 -1
- package/dist/Ascii.d.ts.map +1 -1
- package/dist/Ascii.js +5 -3
- package/dist/Ascii.js.map +1 -1
- package/dist/Binary.d.ts +5 -2
- package/dist/Binary.d.ts.map +1 -1
- package/dist/Binary.js +20 -3
- package/dist/Binary.js.map +1 -1
- package/dist/Bits.d.ts.map +1 -1
- package/dist/Bits.js +4 -0
- package/dist/Bits.js.map +1 -1
- package/dist/Input.d.ts +21 -3
- package/dist/Input.d.ts.map +1 -1
- package/dist/Input.js +31 -4
- package/dist/Input.js.map +1 -1
- package/dist/Nom.d.ts.map +1 -1
- package/dist/ParseError.d.ts +69 -11
- package/dist/ParseError.d.ts.map +1 -1
- package/dist/ParseError.js +129 -28
- package/dist/ParseError.js.map +1 -1
- package/dist/Parser.d.ts +10 -4
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +10 -24
- package/dist/Parser.js.map +1 -1
- package/dist/ParserSchema.d.ts.map +1 -1
- package/dist/ParserSchema.js +1 -2
- package/dist/ParserSchema.js.map +1 -1
- package/dist/ParserStream.d.ts +119 -12
- package/dist/ParserStream.d.ts.map +1 -1
- package/dist/ParserStream.js +282 -55
- package/dist/ParserStream.js.map +1 -1
- package/dist/Syntax.js +1 -1
- package/dist/Text.d.ts.map +1 -1
- package/dist/Text.js +13 -7
- package/dist/Text.js.map +1 -1
- package/dist/Token.d.ts +10 -1
- package/dist/Token.d.ts.map +1 -1
- package/dist/Token.js +14 -5
- package/dist/Token.js.map +1 -1
- package/dist/internal/compiler/closure.d.ts +1 -1
- package/dist/internal/compiler/closure.d.ts.map +1 -1
- package/dist/internal/compiler/closure.js +7 -17
- package/dist/internal/compiler/closure.js.map +1 -1
- package/dist/internal/compiler/first.d.ts.map +1 -1
- package/dist/internal/compiler/first.js +2 -1
- package/dist/internal/compiler/first.js.map +1 -1
- package/dist/internal/compiler/graph.d.ts +23 -1
- package/dist/internal/compiler/graph.d.ts.map +1 -1
- package/dist/internal/compiler/graph.js +27 -5
- package/dist/internal/compiler/graph.js.map +1 -1
- package/dist/internal/compiler/jit.d.ts.map +1 -1
- package/dist/internal/compiler/jit.js +26 -13
- package/dist/internal/compiler/jit.js.map +1 -1
- package/dist/internal/compiler/registry.d.ts +1 -1
- package/dist/internal/compiler/wasm.d.ts.map +1 -1
- package/dist/internal/compiler/wasm.js +105 -34
- package/dist/internal/compiler/wasm.js.map +1 -1
- package/dist/internal/node.d.ts +19 -1
- package/dist/internal/node.d.ts.map +1 -1
- package/dist/internal/node.js +42 -8
- package/dist/internal/node.js.map +1 -1
- package/dist/internal/run.d.ts +3 -1
- package/dist/internal/run.d.ts.map +1 -1
- package/dist/internal/run.js +4 -2
- package/dist/internal/run.js.map +1 -1
- package/dist/internal/stack.d.ts +8 -0
- package/dist/internal/stack.d.ts.map +1 -1
- package/dist/internal/stack.js +17 -1
- package/dist/internal/stack.js.map +1 -1
- package/dist/internal/tokens.d.ts +10 -3
- package/dist/internal/tokens.d.ts.map +1 -1
- package/dist/internal/tokens.js +22 -9
- package/dist/internal/tokens.js.map +1 -1
- package/dist/internal/tuple.d.ts +6 -0
- package/dist/internal/tuple.d.ts.map +1 -0
- package/dist/internal/tuple.js +170 -0
- package/dist/internal/tuple.js.map +1 -0
- package/package.json +17 -10
- package/src/Ascii.ts +5 -3
- package/src/Binary.ts +24 -3
- package/src/Bits.ts +4 -0
- package/src/Input.ts +37 -4
- package/src/ParseError.ts +206 -52
- package/src/Parser.ts +19 -29
- package/src/ParserSchema.ts +1 -2
- package/src/ParserStream.ts +336 -62
- package/src/Text.ts +16 -12
- package/src/Token.ts +14 -5
- package/src/internal/compiler/closure.ts +9 -18
- package/src/internal/compiler/first.ts +6 -0
- package/src/internal/compiler/graph.ts +32 -4
- package/src/internal/compiler/jit.ts +37 -17
- package/src/internal/compiler/wasm.ts +141 -68
- package/src/internal/node.ts +49 -3
- package/src/internal/run.ts +4 -2
- package/src/internal/stack.ts +17 -0
- package/src/internal/tokens.ts +20 -10
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
|
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 =
|
|
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}}` : "") +
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
3033
|
-
|
|
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
|
-
|
|
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
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
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
|
|
3087
|
-
|
|
3088
|
-
const
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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 })
|