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