parseman 0.47.1 → 0.48.1
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/README.md +27 -21
- package/dist/analysis/diagnostics.js +2964 -347
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +2960 -343
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts +1 -0
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/expected.d.ts +8 -0
- package/dist/combinators/expected.d.ts.map +1 -0
- package/dist/combinators/first-set.d.ts +8 -0
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/keywords.d.ts +4 -0
- package/dist/combinators/keywords.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/not.d.ts.map +1 -1
- package/dist/combinators/peek.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +16 -3
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/direct-projection.d.ts +12 -0
- package/dist/compiler/direct-projection.d.ts.map +1 -0
- package/dist/compiler/token-alphabet.d.ts +845 -5
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.d.ts +3 -0
- package/dist/compiler/token-capability.d.ts.map +1 -0
- package/dist/compiler/token-capability.js +3192 -0
- package/dist/compiler/token-capability.js.map +6 -0
- package/dist/cst/trivia-charscan.d.ts +3 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -1
- package/dist/cst/trivia-css-scanner.d.ts +5 -0
- package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.js +2973 -343
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +5 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/index.js +2982 -352
- package/dist/plugin/index.js.map +3 -3
- package/dist/spec/index.js +325 -47
- package/dist/spec/index.js.map +3 -3
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +3 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +12 -13
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec-baseline.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +2937 -311
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-body.d.ts +9 -0
- package/dist/table/lex-body.d.ts.map +1 -0
- package/dist/table/lex-program.d.ts +25 -0
- package/dist/table/lex-program.d.ts.map +1 -0
- package/dist/table/ops.d.ts +47 -11
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +50 -2
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +13 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +11 -11
- package/src/combinators/choice.ts +10 -1
- package/src/combinators/expect.ts +1 -0
- package/src/combinators/expected.ts +17 -0
- package/src/combinators/first-set.ts +58 -5
- package/src/combinators/grammar.ts +8 -3
- package/src/combinators/keywords.ts +22 -6
- package/src/combinators/literal.ts +2 -1
- package/src/combinators/not.ts +5 -1
- package/src/combinators/peek.ts +5 -1
- package/src/combinators/regex.ts +5 -3
- package/src/combinators/repeat.ts +4 -10
- package/src/combinators/sequence.ts +17 -14
- package/src/combinators/trivia-skip.ts +46 -13
- package/src/compiler/direct-projection.ts +43 -0
- package/src/compiler/token-alphabet.ts +4316 -12
- package/src/compiler/token-capability.ts +6 -0
- package/src/cst/trivia-charscan.ts +34 -0
- package/src/cst/trivia-css-scanner.ts +142 -0
- package/src/cst/trivia-kinds.ts +3 -1
- package/src/plugin/index.ts +4 -4
- package/src/table/assemble.ts +1253 -240
- package/src/table/child-slots.ts +2 -1
- package/src/table/compile-rule-map.ts +21 -3
- package/src/table/emit-assembly.ts +335 -87
- package/src/table/emit.ts +16 -2
- package/src/table/encode.ts +248 -60
- package/src/table/exec-baseline.ts +3 -7
- package/src/table/exec.ts +134 -25
- package/src/table/lex-body.ts +30 -0
- package/src/table/lex-program.ts +431 -0
- package/src/table/ops.ts +49 -11
- package/src/table/program.ts +46 -2
- package/src/table/scalar-terminal.ts +51 -0
- package/src/table/site-labels.ts +11 -1
- package/src/table/stamp.ts +1 -1
- package/src/types.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/analysis/diagnostics.cjs +0 -13824
- package/dist/analysis/diagnostics.cjs.map +0 -6
- package/dist/cli/index.cjs +0 -14925
- package/dist/cli/index.cjs.map +0 -6
- package/dist/index.cjs +0 -16391
- package/dist/index.cjs.map +0 -6
- package/dist/language-service/index.cjs +0 -1304
- package/dist/language-service/index.cjs.map +0 -6
- package/dist/oracle/index.cjs +0 -254
- package/dist/oracle/index.cjs.map +0 -6
- package/dist/plugin/index.cjs +0 -16097
- package/dist/plugin/index.cjs.map +0 -6
- package/dist/run/index.cjs +0 -577
- package/dist/run/index.cjs.map +0 -6
- package/dist/spec/index.cjs +0 -6236
- package/dist/spec/index.cjs.map +0 -6
- package/dist/table/index.cjs +0 -11463
- package/dist/table/index.cjs.map +0 -6
package/src/table/emit.ts
CHANGED
|
@@ -15,8 +15,20 @@ function jsString(s: string): string {
|
|
|
15
15
|
return JSON.stringify(s)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
18
|
+
/**
|
|
19
|
+
* Emit the char-class pool in the shorter of two equivalent forms.
|
|
20
|
+
*
|
|
21
|
+
* A normal array repeats two quote bytes around every class. Real grammars have
|
|
22
|
+
* dozens of classes, and the recursion-stack first-set correction legitimately
|
|
23
|
+
* discovers more of them. One delimiter-joined string pays for `.split()` once
|
|
24
|
+
* and removes those repeated quotes; the resulting value handed to
|
|
25
|
+
* `expandCompact` is still the exact `string[]` its wire contract declares.
|
|
26
|
+
*
|
|
27
|
+
* Pick a delimiter absent from every encoded class. These strings contain RANGE
|
|
28
|
+
* ENDPOINTS, not regex source, so no character is universally reserved. If this
|
|
29
|
+
* small printable set is exhausted, keep the ordinary array — never escape or
|
|
30
|
+
* reinterpret class data to force the compact form.
|
|
31
|
+
*/
|
|
20
32
|
function emitClassPool(classes: readonly string[]): string {
|
|
21
33
|
const array = `[${classes.map(jsString).join(',')}]`
|
|
22
34
|
if (classes.length < 2) return array
|
|
@@ -287,6 +299,8 @@ function programFields(prog: TableProgram, fns: readonly string[], opts: EmitOpt
|
|
|
287
299
|
// `run({ rootTrivia })` rejected a grammar that plainly has labels — the
|
|
288
300
|
// exact failure the root-trivia work exists to prevent, one hop downstream.
|
|
289
301
|
...(prog.dsp.length === 0 ? [] : [`p:[${prog.dsp.map(emitDispatchSpec).join(',')}],`]),
|
|
302
|
+
...(prog.lex === undefined ? [] : [`lx:[${prog.lex.map(row => `[${row.join(',')}]`).join(',')}],`]),
|
|
303
|
+
...(prog.lexPrograms === undefined ? [] : [`lp:[${prog.lexPrograms.map(row => `[${row.join(',')}]`).join(',')}],`]),
|
|
290
304
|
...(prog.labels === undefined ? [] : [`lb:[${prog.labels.map(jsString).join(',')}],`]),
|
|
291
305
|
...(prog.classified === 1 ? ['rc:1,'] : []),
|
|
292
306
|
// Without this a recovery table's MODULE loads as a strict one: the extra
|
package/src/table/encode.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
|
-
import { firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
2
|
+
import { classifyFinalChoice, firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
3
|
import { childrenOf } from '../analysis/gating.ts'
|
|
4
4
|
import { getCoreLiteralValue } from '../combinators/choice.ts'
|
|
5
5
|
import { deriveExpected } from '../combinators/expect.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
|
|
7
|
+
import { buildReadsRaw, buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
|
|
7
8
|
import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
|
|
8
9
|
import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
|
|
9
10
|
import {
|
|
@@ -13,15 +14,28 @@ import {
|
|
|
13
14
|
OP_LIVE, OP_ATTEMPT, OP_LABEL,
|
|
14
15
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
|
|
15
16
|
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
|
|
17
|
+
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
16
18
|
} from './ops.ts'
|
|
17
19
|
import { adjacencyExpected } from '../combinators/adjacency.ts'
|
|
18
20
|
import { resolveAdjacencyKindMask } from '../cst/trivia-kinds.ts'
|
|
19
21
|
import { missingInferredType } from '../combinators/node.ts'
|
|
20
22
|
import { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
|
|
21
23
|
import type { BalancedSpec } from '../combinators/scanTo.ts'
|
|
22
|
-
import type { DispatchSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
|
|
24
|
+
import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
|
|
25
|
+
import {
|
|
26
|
+
lexProgramDigest,
|
|
27
|
+
LEX_NODE_CHOICE2, LEX_NODE_CHOICE4, LEX_NODE_CHOICE8,
|
|
28
|
+
LEX_NODE_NOT, LEX_NODE_OPTIONAL,
|
|
29
|
+
LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
|
|
30
|
+
LEX_NODE_TERMINAL,
|
|
31
|
+
} from './lex-program.ts'
|
|
23
32
|
import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
24
33
|
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
34
|
+
import { directArrayProjection } from '../compiler/direct-projection.ts'
|
|
35
|
+
import {
|
|
36
|
+
assertLexicalCapabilityClosure, collectLexicalCapabilities, winnerWrapsReference,
|
|
37
|
+
} from '../compiler/token-capability.ts'
|
|
38
|
+
import { directExecutableTokenBody, type ExecutableLexNode } from '../compiler/token-alphabet.ts'
|
|
25
39
|
|
|
26
40
|
/**
|
|
27
41
|
* Can `emitConst` print this? Mirrors the guard in `emit.ts` — scalars, arrays
|
|
@@ -42,37 +56,6 @@ function emittableConst(v: unknown): boolean {
|
|
|
42
56
|
return false
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
/**
|
|
46
|
-
* Does the rule-map entry `winner` bottom out AT the reference `p`?
|
|
47
|
-
*
|
|
48
|
-
* The question `Encoder.winners` has to ask before resolving a `g.X` by name. A
|
|
49
|
-
* map entry that IS the reference — directly, or under the scope wrappers that
|
|
50
|
-
* `rules()` puts there — is not an override to resolve to; it is the reference's
|
|
51
|
-
* own binding, and the only thing that reaches the rule's BODY from there is the
|
|
52
|
-
* ref's thunk. Resolving by name instead hands back the row already in flight for
|
|
53
|
-
* that entry, which is an alias cycle rather than a parser (see `winners`).
|
|
54
|
-
*
|
|
55
|
-
* ONLY WRAPPERS `rules()` ITSELF INTRODUCES are unwrapped — `parser()`/`noTrivia`
|
|
56
|
-
* scopes (`grammar`) and `trivia()`. Both are single-child and both can encode to
|
|
57
|
-
* NO ROW, which is what makes the cycle degenerate. Walking further, into a
|
|
58
|
-
* `seq()` or a `choice()` arm, would answer a different question: a rule whose
|
|
59
|
-
* body legitimately contains a reference to itself is ordinary recursion and must
|
|
60
|
-
* keep resolving by name.
|
|
61
|
-
*/
|
|
62
|
-
function wrapsRef(winner: Combinator<unknown>, p: Combinator<unknown>): boolean {
|
|
63
|
-
let cur = winner
|
|
64
|
-
// Bounded rather than `while (true)`: `_def.parser` is author-supplied and a
|
|
65
|
-
// hand-built cycle of scopes would otherwise hang the encoder. Nothing legal
|
|
66
|
-
// nests these more than a couple deep.
|
|
67
|
-
for (let n = 0; n < 16; n++) {
|
|
68
|
-
if (cur === p) return true
|
|
69
|
-
const d = cur._def as ParserDef
|
|
70
|
-
if (d.tag !== 'grammar' && d.tag !== 'trivia') return false
|
|
71
|
-
cur = d.parser
|
|
72
|
-
}
|
|
73
|
-
return false
|
|
74
|
-
}
|
|
75
|
-
|
|
76
59
|
/** Raised when a construct has no opcode yet. Prototype scope is explicit. */
|
|
77
60
|
export class UnsupportedConstruct extends Error {
|
|
78
61
|
readonly tag: string
|
|
@@ -154,6 +137,10 @@ class Encoder {
|
|
|
154
137
|
fx: (readonly string[])[] = []
|
|
155
138
|
disp: (readonly number[])[] = []
|
|
156
139
|
dsp: DispatchSpec[] = []
|
|
140
|
+
lex: LexBodySpec[] = []
|
|
141
|
+
private lexIndex = new Map<string, number>()
|
|
142
|
+
lexPrograms: LexProgramSpec[] = []
|
|
143
|
+
private lexProgramIndex = new Map<string, number>()
|
|
157
144
|
labels: readonly string[] | undefined = undefined
|
|
158
145
|
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
159
146
|
activeTrivia: Combinator<unknown> | undefined = undefined
|
|
@@ -207,7 +194,6 @@ class Encoder {
|
|
|
207
194
|
winners: Readonly<Record<string, Combinator<unknown>>> | undefined = undefined
|
|
208
195
|
triviaSpecs: TriviaSpec[] = []
|
|
209
196
|
private triviaIndex = new Map<Combinator<unknown>, number>()
|
|
210
|
-
|
|
211
197
|
/**
|
|
212
198
|
* A trivia combinator as DATA where its shape allows.
|
|
213
199
|
*
|
|
@@ -226,6 +212,160 @@ class Encoder {
|
|
|
226
212
|
return idx
|
|
227
213
|
}
|
|
228
214
|
|
|
215
|
+
private lexicalBodySlot(p: Combinator<unknown>): {
|
|
216
|
+
kind: 'body'
|
|
217
|
+
body: number
|
|
218
|
+
expectedFx: number
|
|
219
|
+
suffixFx: number
|
|
220
|
+
lineFlags: number
|
|
221
|
+
} | { kind: 'program'; program: number } | undefined {
|
|
222
|
+
if (!this.selectLexicalBodies) return undefined
|
|
223
|
+
const body = directExecutableTokenBody(p, name => this.winners?.[name])
|
|
224
|
+
if (body === undefined) return undefined
|
|
225
|
+
const tokenDef = p._def
|
|
226
|
+
if (tokenDef.tag !== 'token') throw new Error('parseman: selected lexical body lost token boundary')
|
|
227
|
+
const matcher = (arm: Extract<typeof body, { kind: 'regex-terminal' }>): number => {
|
|
228
|
+
const flags = arm.flags.includes('y') ? arm.flags : `${arm.flags.replace(/g/g, '')}y`
|
|
229
|
+
const regex = this.constant(new RegExp(arm.source, flags))
|
|
230
|
+
const key = `${regex}:-1`
|
|
231
|
+
const prior = this.lexIndex.get(key)
|
|
232
|
+
if (prior !== undefined) return prior
|
|
233
|
+
const id = this.lex.length
|
|
234
|
+
this.lexIndex.set(key, id)
|
|
235
|
+
this.lex.push([regex, -1])
|
|
236
|
+
return id
|
|
237
|
+
}
|
|
238
|
+
if (body.kind === 'balanced-scan') {
|
|
239
|
+
const scan = this.scanSlot({
|
|
240
|
+
kind: 1,
|
|
241
|
+
flags: (body.raw ? 1 : 0) | (body.strict ? 4 : 0),
|
|
242
|
+
skip: body.ownSkip.map(combinator => this.subtree(combinator)),
|
|
243
|
+
open: body.open,
|
|
244
|
+
close: body.close,
|
|
245
|
+
})
|
|
246
|
+
const words = [scan]
|
|
247
|
+
const spec = [3, lexProgramDigest(words), scan] as LexProgramSpec
|
|
248
|
+
const key = spec.join(',')
|
|
249
|
+
const prior = this.lexProgramIndex.get(key)
|
|
250
|
+
if (prior !== undefined) return { kind: 'program', program: prior }
|
|
251
|
+
const id = this.lexPrograms.length
|
|
252
|
+
this.lexProgramIndex.set(key, id)
|
|
253
|
+
this.lexPrograms.push(spec)
|
|
254
|
+
return { kind: 'program', program: id }
|
|
255
|
+
}
|
|
256
|
+
if (body.kind === 'fixed-tree') {
|
|
257
|
+
const words: number[] = []
|
|
258
|
+
const emitNode = (node: ExecutableLexNode): boolean => {
|
|
259
|
+
switch (node.kind) {
|
|
260
|
+
case 'terminal':
|
|
261
|
+
words.push(
|
|
262
|
+
LEX_NODE_TERMINAL,
|
|
263
|
+
matcher(node.terminal),
|
|
264
|
+
this.expected(node.terminal.expected),
|
|
265
|
+
this.track ? 1 : 0,
|
|
266
|
+
)
|
|
267
|
+
return true
|
|
268
|
+
case 'sequence': {
|
|
269
|
+
for (const part of node.parts) if (!emitNode(part)) return false
|
|
270
|
+
const op = node.parts.length === 2 ? LEX_NODE_SEQUENCE2
|
|
271
|
+
: node.parts.length === 3 ? LEX_NODE_SEQUENCE3
|
|
272
|
+
: node.parts.length === 4 ? LEX_NODE_SEQUENCE4
|
|
273
|
+
: node.parts.length === 5 ? LEX_NODE_SEQUENCE5 : -1
|
|
274
|
+
if (op < 0) return false
|
|
275
|
+
words.push(op)
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
case 'choice': {
|
|
279
|
+
for (const arm of node.arms) if (!emitNode(arm)) return false
|
|
280
|
+
const op = node.arms.length === 2 ? LEX_NODE_CHOICE2
|
|
281
|
+
: node.arms.length === 4 ? LEX_NODE_CHOICE4
|
|
282
|
+
: node.arms.length === 8 ? LEX_NODE_CHOICE8 : -1
|
|
283
|
+
if (op < 0) return false
|
|
284
|
+
words.push(op, this.expected(node.expected))
|
|
285
|
+
for (let i = 0; i < node.arms.length; i++) {
|
|
286
|
+
const first = node.firstSets[i]!
|
|
287
|
+
words.push(first.kind === 'ranges' ? this.charClass(first) : -1)
|
|
288
|
+
words.push(this.expected(node.armExpected[i]!))
|
|
289
|
+
}
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
case 'not':
|
|
293
|
+
if (!emitNode(node.body)) return false
|
|
294
|
+
words.push(LEX_NODE_NOT, this.expected(node.expected))
|
|
295
|
+
return true
|
|
296
|
+
case 'optional':
|
|
297
|
+
if (!emitNode(node.body)) return false
|
|
298
|
+
words.push(LEX_NODE_OPTIONAL)
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!emitNode(body.root)) return undefined
|
|
303
|
+
const spec = [2, lexProgramDigest(words), ...words] as LexProgramSpec
|
|
304
|
+
const key = spec.join(',')
|
|
305
|
+
const prior = this.lexProgramIndex.get(key)
|
|
306
|
+
if (prior !== undefined) return { kind: 'program', program: prior }
|
|
307
|
+
const id = this.lexPrograms.length
|
|
308
|
+
this.lexProgramIndex.set(key, id)
|
|
309
|
+
this.lexPrograms.push(spec)
|
|
310
|
+
return { kind: 'program', program: id }
|
|
311
|
+
}
|
|
312
|
+
if (body.kind === 'not2-ordered4-terminal') {
|
|
313
|
+
const words = [
|
|
314
|
+
...body.guard0.map(matcher), this.expected(body.guard0Expected),
|
|
315
|
+
matcher(body.guard1), this.expected(body.guard1Expected),
|
|
316
|
+
matcher(body.terminal), this.expected(body.terminalExpected),
|
|
317
|
+
this.track ? 1 : 0,
|
|
318
|
+
]
|
|
319
|
+
const spec = [1, lexProgramDigest(words), ...words] as unknown as LexProgramSpec
|
|
320
|
+
const key = spec.join(',')
|
|
321
|
+
const prior = this.lexProgramIndex.get(key)
|
|
322
|
+
if (prior !== undefined) return { kind: 'program', program: prior }
|
|
323
|
+
const id = this.lexPrograms.length
|
|
324
|
+
this.lexProgramIndex.set(key, id)
|
|
325
|
+
this.lexPrograms.push(spec)
|
|
326
|
+
return { kind: 'program', program: id }
|
|
327
|
+
}
|
|
328
|
+
if (body.kind === 'ordered4-terminals') {
|
|
329
|
+
const commonClass = this.charClass(body.commonFirstSet)
|
|
330
|
+
if (commonClass < 0) return undefined
|
|
331
|
+
const words: number[] = [commonClass, this.expected(body.commonMissExpected)]
|
|
332
|
+
for (const arm of body.arms) {
|
|
333
|
+
words.push(matcher(arm), this.expected(arm.expected))
|
|
334
|
+
}
|
|
335
|
+
words.push(this.track ? 1 : 0)
|
|
336
|
+
const spec = [0, lexProgramDigest(words), ...words] as unknown as LexProgramSpec
|
|
337
|
+
const key = spec.join(',')
|
|
338
|
+
const prior = this.lexProgramIndex.get(key)
|
|
339
|
+
if (prior !== undefined) return { kind: 'program', program: prior }
|
|
340
|
+
const id = this.lexPrograms.length
|
|
341
|
+
this.lexProgramIndex.set(key, id)
|
|
342
|
+
this.lexPrograms.push(spec)
|
|
343
|
+
return { kind: 'program', program: id }
|
|
344
|
+
}
|
|
345
|
+
const expectedFx = this.expected(body.expected)
|
|
346
|
+
const flags = body.flags.includes('y') ? body.flags : `${body.flags.replace(/g/g, '')}y`
|
|
347
|
+
const regex = this.constant(new RegExp(body.source, flags))
|
|
348
|
+
const suffix = body.kind === 'regex-terminal' ? -1 : body.suffix.charCodeAt(0)
|
|
349
|
+
const key = `${regex}:${suffix}`
|
|
350
|
+
const prior = this.lexIndex.get(key)
|
|
351
|
+
const suffixFx = this.expected(body.kind === 'regex-terminal' ? [] : directTerminalFailureExpected({
|
|
352
|
+
tag: 'literal', value: body.suffix, caseInsensitive: false,
|
|
353
|
+
}))
|
|
354
|
+
// Tracked terminal rows advance the high-water mark even when their static
|
|
355
|
+
// language cannot contain a newline. Otherwise a preceding newline stays
|
|
356
|
+
// behind an incorrectly advanced boundary and the next CST node receives a
|
|
357
|
+
// stale line. The suffix advances only when it actually matched (reader bit
|
|
358
|
+
// 2), exactly like the authored optional literal.
|
|
359
|
+
const lineFlags = this.track
|
|
360
|
+
? (body.kind === 'regex-terminal' ? 1 : 1 | 2 | 4)
|
|
361
|
+
: body.kind === 'regex-terminal' ? 0 : 4
|
|
362
|
+
if (prior !== undefined) return { kind: 'body', body: prior, expectedFx, suffixFx, lineFlags }
|
|
363
|
+
const id = this.lex.length
|
|
364
|
+
this.lexIndex.set(key, id)
|
|
365
|
+
this.lex.push([regex, suffix])
|
|
366
|
+
return { kind: 'body', body: id, expectedFx, suffixFx, lineFlags }
|
|
367
|
+
}
|
|
368
|
+
|
|
229
369
|
private triviaSpecOf(t: Combinator<unknown>): TriviaSpec {
|
|
230
370
|
const unlowered = (why: string): TriviaSpec => {
|
|
231
371
|
this.runtimeOnly.add(`rules({ trivia }) — ${why}`)
|
|
@@ -345,6 +485,9 @@ class Encoder {
|
|
|
345
485
|
readonly settings: TableSettings
|
|
346
486
|
/** Resolved once, HERE, at table-build time — never consulted at run time. */
|
|
347
487
|
readonly track: boolean
|
|
488
|
+
/** TOKEN bodies are an all-program decision. A complete occurrence inside an
|
|
489
|
+
* otherwise incomplete final graph stays on CHARACTER with every sibling. */
|
|
490
|
+
readonly selectLexicalBodies: boolean
|
|
348
491
|
/**
|
|
349
492
|
* Decides the extra operands laid down below. ALWAYS TRUE — see
|
|
350
493
|
* `TableSettings.recovery`. Kept as a field rather than folded away because it
|
|
@@ -352,9 +495,10 @@ class Encoder {
|
|
|
352
495
|
* row is stated.
|
|
353
496
|
*/
|
|
354
497
|
readonly rec: boolean
|
|
355
|
-
constructor(settings: TableSettings) {
|
|
498
|
+
constructor(settings: TableSettings, selectLexicalBodies: boolean) {
|
|
356
499
|
this.settings = settings
|
|
357
500
|
this.track = settings.trackLines === true
|
|
501
|
+
this.selectLexicalBodies = selectLexicalBodies
|
|
358
502
|
this.rec = true
|
|
359
503
|
this.plan = settings.coverage
|
|
360
504
|
this.cov = settings.coverage === undefined
|
|
@@ -483,6 +627,16 @@ class Encoder {
|
|
|
483
627
|
return i
|
|
484
628
|
}
|
|
485
629
|
|
|
630
|
+
/** Bind a guard only when its class is already part of the program. Repeat
|
|
631
|
+
* gating must not grow the compact artifact merely to avoid a failed call. */
|
|
632
|
+
private existingCharClass(fs: FirstSet): number {
|
|
633
|
+
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
634
|
+
const hit = this.ccIndex.get(encodeClassSpec(fs.ranges))
|
|
635
|
+
// This reuses the recovery row's existing `-1` slot. Keep its decimal width
|
|
636
|
+
// at two bytes or less so a speed hint cannot grow a compact artifact.
|
|
637
|
+
return hit !== undefined && hit < 100 ? hit : -1
|
|
638
|
+
}
|
|
639
|
+
|
|
486
640
|
/**
|
|
487
641
|
* Wrap one choice arm in its `autoNot` checks (`OP_REJECT`).
|
|
488
642
|
*
|
|
@@ -618,7 +772,7 @@ class Encoder {
|
|
|
618
772
|
* in the target rule's own ambient trivia when that differs from the trivia
|
|
619
773
|
* lexically active at the reference.
|
|
620
774
|
*
|
|
621
|
-
* `encodeRule` wraps a rule ENTRY in `
|
|
775
|
+
* `encodeRule` wraps a rule ENTRY in `OP_SCOPE_PLAIN`, but only the entry: a `g.X`
|
|
622
776
|
* reference resolves straight to the body's offset and jumps past it (the note
|
|
623
777
|
* at `encodeRule`). So a rule referenced from inside a `noTrivia(...)` region ran
|
|
624
778
|
* with NO ambient trivia of its own — the table scoped trivia DYNAMICALLY, by
|
|
@@ -713,6 +867,20 @@ class Encoder {
|
|
|
713
867
|
|
|
714
868
|
private encodeDef(p: Combinator<unknown>): number {
|
|
715
869
|
const d = p._def as ParserDef
|
|
870
|
+
// `balanced()` returns a token carrying `_balancedSpec` on that same
|
|
871
|
+
// object. Give a complete selected token shell first refusal; otherwise the
|
|
872
|
+
// generic scan row below would permanently shadow it before the `token`
|
|
873
|
+
// switch is reached.
|
|
874
|
+
if (d.tag === 'token') {
|
|
875
|
+
const selected = this.lexicalBodySlot(p)
|
|
876
|
+
if (selected !== undefined) {
|
|
877
|
+
if (selected.kind === 'program') return this.emit(OP_LEX_PROGRAM, selected.program)
|
|
878
|
+
return this.emit(
|
|
879
|
+
OP_LEX_BODY, selected.body, selected.expectedFx,
|
|
880
|
+
selected.suffixFx, selected.lineFlags,
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
716
884
|
// THE SCANNING CONSTRUCTS, as their constructor arguments.
|
|
717
885
|
//
|
|
718
886
|
// Neither is recoverable from `_def` alone — `balanced()` overrides `.parse`
|
|
@@ -746,13 +914,13 @@ class Encoder {
|
|
|
746
914
|
switch (d.tag) {
|
|
747
915
|
case 'literal': {
|
|
748
916
|
if (d.caseInsensitive) {
|
|
749
|
-
return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(
|
|
917
|
+
return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(directTerminalFailureExpected(d)))
|
|
750
918
|
}
|
|
751
|
-
return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(
|
|
919
|
+
return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(directTerminalFailureExpected(d)))
|
|
752
920
|
}
|
|
753
921
|
case 'regex': {
|
|
754
922
|
const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
|
|
755
|
-
return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(
|
|
923
|
+
return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(directTerminalFailureExpected(d)))
|
|
756
924
|
}
|
|
757
925
|
case 'keywords': {
|
|
758
926
|
// `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
|
|
@@ -770,7 +938,7 @@ class Encoder {
|
|
|
770
938
|
// failing keyword arm names the CATEGORY rather than reciting every
|
|
771
939
|
// literal in the family. `deriveExpected` reports the literals, so the
|
|
772
940
|
// table said `'"media"'` where both other engines say `'keyword'`.
|
|
773
|
-
this.expected(
|
|
941
|
+
this.expected(directTerminalFailureExpected(d)),
|
|
774
942
|
)
|
|
775
943
|
}
|
|
776
944
|
case 'expect': {
|
|
@@ -875,6 +1043,7 @@ class Encoder {
|
|
|
875
1043
|
// covered on a parse where the choice picked somebody else.
|
|
876
1044
|
return this.covWrap(gated, armCovIds?.[src], 1)
|
|
877
1045
|
})
|
|
1046
|
+
const rr = this.refResolver()
|
|
878
1047
|
// O(1) first-char dispatch is only SOUND when the arms are disjoint.
|
|
879
1048
|
// With overlapping first sets, "the first arm whose class contains this
|
|
880
1049
|
// char" is not "the first arm that matches": a `Keyword` arm whose first
|
|
@@ -904,24 +1073,21 @@ class Encoder {
|
|
|
904
1073
|
// the O(1) table (`exclusive`) or falls to the ordered per-arm path.
|
|
905
1074
|
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
906
1075
|
// change rather than a reordering.
|
|
907
|
-
const
|
|
908
|
-
const classes = arms.map(
|
|
1076
|
+
const finalChoice = classifyFinalChoice(arms, rr)
|
|
1077
|
+
const classes = arms.map((_, i) => finalChoice.nullable[i]
|
|
1078
|
+
? -1 : this.charClass(finalChoice.firstSets[i]!))
|
|
909
1079
|
const dispIdx = this.disp.length
|
|
910
1080
|
this.disp.push(classes)
|
|
911
|
-
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets.
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
// opener set back. Without these words a partially-gated site reported
|
|
916
|
-
// only the arms that happened to run.
|
|
1081
|
+
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
|
|
1082
|
+
// failure remains at the choice position, an arm declined by the driver's
|
|
1083
|
+
// char gate contributes the static opener set the interpreter would get by
|
|
1084
|
+
// entering it. A deeper dynamic arm failure discards those shallower sets.
|
|
917
1085
|
const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
|
|
918
1086
|
this.code[head + 1] = dispIdx
|
|
919
1087
|
this.code[head + 2] = kids.length
|
|
920
1088
|
// THE CHOICE'S OWN SET IS THE ARMS' FLATMAP, NOT `deriveExpected(p)`.
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
// choice whose two arms both open with `/::?/` reports it TWICE. The
|
|
924
|
-
// deduped union silently collapsed that to one.
|
|
1089
|
+
// It is the pure-dispatch-miss and empty-dynamic fallback, where source
|
|
1090
|
+
// order and duplicates remain observable. `deriveExpected` would dedupe.
|
|
925
1091
|
this.code[head + 3] = this.expected(arms.flatMap(a => deriveExpected(a)))
|
|
926
1092
|
for (let i = 0; i < kids.length; i++) {
|
|
927
1093
|
this.code[head + 4 + i] = kids[i]!
|
|
@@ -932,6 +1098,10 @@ class Encoder {
|
|
|
932
1098
|
case 'many':
|
|
933
1099
|
case 'oneOrMore': {
|
|
934
1100
|
const child = this.node(d.parser).ip
|
|
1101
|
+
const rr = this.refResolver()
|
|
1102
|
+
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1103
|
+
? -1
|
|
1104
|
+
: this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
|
|
935
1105
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
936
1106
|
const min = d.tag === 'many' ? 0 : d.min
|
|
937
1107
|
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
@@ -940,7 +1110,7 @@ class Encoder {
|
|
|
940
1110
|
// set — `deriveExpected(combinator)` in repeat.ts:170, and
|
|
941
1111
|
// `deriveExpectedArr([def.parser])` in codegen's emitMany.
|
|
942
1112
|
return this.rec
|
|
943
|
-
? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)),
|
|
1113
|
+
? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
|
|
944
1114
|
: this.emit(op, child, min, d.max ?? -1, -1, 0)
|
|
945
1115
|
}
|
|
946
1116
|
case 'sepBy': {
|
|
@@ -987,7 +1157,14 @@ class Encoder {
|
|
|
987
1157
|
if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
|
|
988
1158
|
const kids = inner.parsers.map(c => this.node(c).ip)
|
|
989
1159
|
const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
|
|
990
|
-
|
|
1160
|
+
const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
|
|
1161
|
+
// A negative operand describes an already-parsed child (`~index`),
|
|
1162
|
+
// while every function-pool index remains non-negative. The row is
|
|
1163
|
+
// otherwise unchanged, so projection fusion costs no table words and
|
|
1164
|
+
// does not create a second closure/factory shape.
|
|
1165
|
+
this.code[head + 1] = projection === null
|
|
1166
|
+
? this.fn(d.fn, d.fnSrc ?? null)
|
|
1167
|
+
: ~projection
|
|
991
1168
|
this.code[head + 2] = kids.length
|
|
992
1169
|
for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
|
|
993
1170
|
if (this.rec) {
|
|
@@ -1046,12 +1223,14 @@ class Encoder {
|
|
|
1046
1223
|
// a `project` replaces the value outright.
|
|
1047
1224
|
const noBuildCaptures = d.project === undefined
|
|
1048
1225
|
const derivedTrivia = d.build !== undefined ? buildReadsTrivia(d) : noBuildCaptures
|
|
1226
|
+
const omitsRaw = d.build !== undefined && !buildReadsRaw(d)
|
|
1049
1227
|
const derivedState = d.build !== undefined ? buildReadsState(d) : noBuildCaptures
|
|
1050
1228
|
const derivedFields = d.build !== undefined ? buildReadsFields(d) : noBuildCaptures
|
|
1051
1229
|
// Field capture additionally requires the body to CONTAIN `field()`
|
|
1052
1230
|
// captures: a node that reads fields but has none allocates nothing.
|
|
1053
1231
|
const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
|
|
1054
1232
|
const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
|
|
1233
|
+
| (!cstOut && omitsRaw ? 2 : 0)
|
|
1055
1234
|
| (cstOut || derivedState ? 8 : 0)
|
|
1056
1235
|
| (wantsFields ? 16 : 0)
|
|
1057
1236
|
| (d.collapse === true ? 32 : 0)
|
|
@@ -1091,8 +1270,9 @@ class Encoder {
|
|
|
1091
1270
|
const e = deriveExpected(d.parser)
|
|
1092
1271
|
return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1093
1272
|
}
|
|
1094
|
-
case 'token':
|
|
1273
|
+
case 'token': {
|
|
1095
1274
|
return this.emit(OP_TOKEN, this.node(d.parser).ip)
|
|
1275
|
+
}
|
|
1096
1276
|
case 'routed':
|
|
1097
1277
|
return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
|
|
1098
1278
|
case 'dispatch': {
|
|
@@ -1204,7 +1384,7 @@ class Encoder {
|
|
|
1204
1384
|
// merged (composed) map, and why the identity guard is required.
|
|
1205
1385
|
const refName = (p as unknown as { _ruleName?: string })._ruleName
|
|
1206
1386
|
const winner = refName === undefined ? undefined : this.winners?.[refName]
|
|
1207
|
-
if (winner !== undefined && !
|
|
1387
|
+
if (winner !== undefined && !winnerWrapsReference(winner, p)) return this.scopedRef(p, winner)
|
|
1208
1388
|
let resolved: Combinator<unknown>
|
|
1209
1389
|
try { resolved = d.thunk() }
|
|
1210
1390
|
catch {
|
|
@@ -1219,7 +1399,7 @@ class Encoder {
|
|
|
1219
1399
|
// other engine reported `['not(literal)']`. It hid because `expected` is a
|
|
1220
1400
|
// TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
|
|
1221
1401
|
case 'not':
|
|
1222
|
-
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(
|
|
1402
|
+
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1223
1403
|
case 'guard':
|
|
1224
1404
|
// `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
|
|
1225
1405
|
return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
|
|
@@ -1259,7 +1439,7 @@ class Encoder {
|
|
|
1259
1439
|
// lookahead's failure is "the guard did not hold", and naming the body's
|
|
1260
1440
|
// internals offers a token the parse never asked the author for. The
|
|
1261
1441
|
// table propagated the body's set instead.
|
|
1262
|
-
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(
|
|
1442
|
+
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1263
1443
|
// A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
|
|
1264
1444
|
// lowering was correct only for a choice arm.
|
|
1265
1445
|
case 'attempt': {
|
|
@@ -1458,6 +1638,8 @@ class Encoder {
|
|
|
1458
1638
|
return ownTableProgram({
|
|
1459
1639
|
code: this.code, k: this.k, fns: this.fns, cc: this.cc,
|
|
1460
1640
|
fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
|
|
1641
|
+
...(this.lex.length === 0 ? {} : { lex: this.lex }),
|
|
1642
|
+
...(this.lexPrograms.length === 0 ? {} : { lexPrograms: this.lexPrograms }),
|
|
1461
1643
|
...(this.labels === undefined ? {} : { labels: this.labels }),
|
|
1462
1644
|
...(this.classified ? { classified: 1 as const } : {}),
|
|
1463
1645
|
...(this.scanSkipSets.length === 0
|
|
@@ -1561,7 +1743,13 @@ export function encodeTableProgram(
|
|
|
1561
1743
|
...(hostMode === undefined ? {} : { hostMode }),
|
|
1562
1744
|
...(track ? { trackLines: true } : {}),
|
|
1563
1745
|
}
|
|
1564
|
-
|
|
1746
|
+
// Collect once from the FINAL winner-resolved graph. Sorting affects only
|
|
1747
|
+
// content-id discovery; ordinary rule/code order remains the caller's order.
|
|
1748
|
+
const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
|
|
1749
|
+
const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
|
|
1750
|
+
const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
|
|
1751
|
+
assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
|
|
1752
|
+
const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
|
|
1565
1753
|
enc.winners = ruleMap
|
|
1566
1754
|
for (const name of names) enc.encodeRule(name, ruleMap[name]!)
|
|
1567
1755
|
const prog = enc.finish()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParseContext, ParseResult } from '../types.ts'
|
|
2
|
-
import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark,
|
|
2
|
+
import { advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTriviaCompact } from '../combinators/trivia-skip.ts'
|
|
3
3
|
import {
|
|
4
4
|
beginCstNodeCapture, cstCaptureActive, endCstNodeCapture, pushCstChild,
|
|
5
5
|
pushCstLeaf, rollbackCstCapture, saveCstMark, type CstRollbackMark,
|
|
@@ -142,9 +142,7 @@ function makeDriver(
|
|
|
142
142
|
*/
|
|
143
143
|
function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
144
144
|
if (needsDeferredTriviaCommit(ctx)) {
|
|
145
|
-
|
|
146
|
-
scan.commit()
|
|
147
|
-
return scan.end
|
|
145
|
+
return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
148
146
|
}
|
|
149
147
|
return advanceTrivia(input, cur, ctx)
|
|
150
148
|
}
|
|
@@ -256,9 +254,7 @@ function makeDriver(
|
|
|
256
254
|
const mark = rollbackNeeded(ctx) ? saveTriviaMark(ctx) : null
|
|
257
255
|
let scanEnd: number
|
|
258
256
|
if (needsDeferredTriviaCommit(ctx)) {
|
|
259
|
-
|
|
260
|
-
scan.commit()
|
|
261
|
-
scanEnd = scan.end
|
|
257
|
+
scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
262
258
|
} else {
|
|
263
259
|
scanEnd = advanceTrivia(input, cur, ctx)
|
|
264
260
|
}
|