parseman 0.59.3 → 0.60.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.
Files changed (94) hide show
  1. package/dist/analysis/diagnostics.js +876 -190
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +876 -190
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts +13 -1
  6. package/dist/combinators/dispatch.d.ts.map +1 -1
  7. package/dist/combinators/first-set.d.ts +35 -2
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/regex.d.ts.map +1 -1
  10. package/dist/combinators/trivia-boundary.d.ts +11 -0
  11. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  12. package/dist/combinators/trivia-skip.d.ts +1 -1
  13. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  14. package/dist/compiler/token-alphabet.d.ts +2 -0
  15. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  16. package/dist/compiler/token-capability.js +65 -53
  17. package/dist/compiler/token-capability.js.map +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +896 -199
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +903 -201
  24. package/dist/plugin/index.js.map +3 -3
  25. package/dist/regex/first-set.d.ts +19 -0
  26. package/dist/regex/first-set.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts +15 -1
  28. package/dist/table/assemble.d.ts.map +1 -1
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +1 -1
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/compile.d.ts +1 -1
  33. package/dist/table/emit-assembly.d.ts +8 -2
  34. package/dist/table/emit-assembly.d.ts.map +1 -1
  35. package/dist/table/emit.d.ts +4 -3
  36. package/dist/table/emit.d.ts.map +1 -1
  37. package/dist/table/encode.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts +0 -35
  39. package/dist/table/exec.d.ts.map +1 -1
  40. package/dist/table/factor-choice.d.ts +27 -0
  41. package/dist/table/factor-choice.d.ts.map +1 -1
  42. package/dist/table/gap-gate.d.ts +11 -0
  43. package/dist/table/gap-gate.d.ts.map +1 -0
  44. package/dist/table/index.js +926 -201
  45. package/dist/table/index.js.map +3 -3
  46. package/dist/table/lex-program.d.ts.map +1 -1
  47. package/dist/table/ops.d.ts +35 -16
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +6 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/types.d.ts +16 -4
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +2 -4
  55. package/src/analysis/duplication.ts +1 -1
  56. package/src/combinators/attempt.ts +1 -1
  57. package/src/combinators/dispatch.ts +30 -6
  58. package/src/combinators/first-set.ts +55 -12
  59. package/src/combinators/node.ts +1 -1
  60. package/src/combinators/regex.ts +9 -4
  61. package/src/combinators/repeat.ts +6 -6
  62. package/src/combinators/sequence.ts +1 -1
  63. package/src/combinators/trivia-boundary.ts +18 -0
  64. package/src/combinators/trivia-skip.ts +1 -1
  65. package/src/compiler/ir-serialize.ts +1 -1
  66. package/src/compiler/token-alphabet.ts +3 -1
  67. package/src/coverage.ts +1 -1
  68. package/src/index.ts +1 -1
  69. package/src/plugin/evaluator.ts +8 -3
  70. package/src/regex/first-set.ts +37 -3
  71. package/src/table/assemble.ts +176 -30
  72. package/src/table/child-slots.ts +8 -3
  73. package/src/table/compile-rule-map.ts +7 -4
  74. package/src/table/compile.ts +1 -1
  75. package/src/table/emit-assembly.ts +184 -33
  76. package/src/table/emit.ts +6 -4
  77. package/src/table/encode.ts +469 -74
  78. package/src/table/exec.ts +38 -82
  79. package/src/table/factor-choice.ts +62 -0
  80. package/src/table/fail-clean.ts +2 -2
  81. package/src/table/gap-gate.ts +55 -0
  82. package/src/table/hold-gap.ts +2 -2
  83. package/src/table/lex-program.ts +15 -6
  84. package/src/table/ops.ts +38 -16
  85. package/src/table/program.ts +7 -2
  86. package/src/table/select-hot.ts +7 -2
  87. package/src/types.ts +12 -5
  88. package/src/version.ts +1 -1
  89. package/dist/table/encode-baseline.d.ts +0 -16
  90. package/dist/table/encode-baseline.d.ts.map +0 -1
  91. package/dist/table/exec-baseline.d.ts +0 -9
  92. package/dist/table/exec-baseline.d.ts.map +0 -1
  93. package/src/table/encode-baseline.ts +0 -346
  94. package/src/table/exec-baseline.ts +0 -557
package/src/types.ts CHANGED
@@ -58,14 +58,14 @@ export type ParserDef =
58
58
  // force interpreter fallback). Absent under runtime compile() — the real gate
59
59
  // closures live in `gates`.
60
60
  | { tag: 'choice'; parsers: Combinator<unknown>[]; gates: (((state: unknown) => boolean) | null)[]; gateSrcs?: (string | null)[]; disjoint: boolean; strategy: ChoiceStrategy; autoNot: (AutoNotCheck[] | null)[] }
61
- | { tag: 'dispatch'; selector: Combinator<string>; cases: readonly DispatchCase[]; matchers?: readonly DispatchMatcherCase[] | undefined; otherwise?: Combinator<unknown> | undefined; otherwiseUsesRouted?: boolean | undefined }
62
- | { tag: 'attempt'; parser: Combinator<unknown>; /** `attempt(p, { contain: true })`: a committed failure inside is reported uncommitted. */ contain?: true }
61
+ | { tag: 'dispatch'; selector: Combinator<string>; cases: readonly DispatchCase[]; matchers?: readonly DispatchMatcherCase[] | undefined; otherwise?: Combinator<unknown> | undefined; otherwiseUsesRouted?: boolean | undefined; otherwiseCommit?: false | undefined }
62
+ | { tag: 'attempt'; parser: Combinator<unknown>; /** `attempt(p, { contain: true })`: a committed failure inside is reported uncommitted. */ contain?: true; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
63
63
  // The TAG carries NULLABILITY (what every downstream switch keys on): `many` is
64
64
  // the nullable min-0 repeat, `oneOrMore` the non-nullable min>=1 one. `min`/`max`
65
65
  // carry the actual ITEM bounds (`many(x, { min: 3, max: 8 })` is a `oneOrMore`
66
66
  // def with `min: 3, max: 8`). `max` absent = unbounded.
67
- | { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean }
68
- | { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean }
67
+ | { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
68
+ | { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
69
69
  | { tag: 'optional'; parser: Combinator<unknown> }
70
70
  // `min`/`max` count ITEMS, not separators. `min: 0` (the default) is
71
71
  // `(item (sep item)*)?` — NULLABLE, matches the empty string. `min >= 1` is
@@ -92,7 +92,7 @@ export type ParserDef =
92
92
  // it requires, so it carries `parser`'s first-set and a leading `peek()` gates
93
93
  // its choice arm (see `isPositiveLookahead` / `sequenceFirstSet`).
94
94
  | { tag: 'peek'; parser: Combinator<unknown> }
95
- | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
95
+ | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[]; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
96
96
  // `predSrc`/`extraSrc` (set by the macro evaluator): SOURCE TEXT of the guard
97
97
  // predicate / the withCtx `extra` value, so codegen inlines them into `_mf`
98
98
  // rather than pushing a `null` source. Absent under runtime compile() (the real
@@ -461,6 +461,13 @@ export type ParseError = {
461
461
  export type ParserMeta = {
462
462
  /** Character codes / ranges that can start this parser (for choice dispatch) */
463
463
  firstSet: FirstSet
464
+ /**
465
+ * Set only by `regex()` for a pattern that can match empty: the first chars
466
+ * of its NON-EMPTY matches. `firstSet` stays `any` for such a pattern, because
467
+ * it may match empty anywhere; a sequence, which tracks nullability itself,
468
+ * unions this set with the next term's instead (`sequenceSet`).
469
+ */
470
+ consumingFirstSet?: FirstSet
464
471
  /** Whether this parser can consume a newline character */
465
472
  canMatchNewline: boolean
466
473
  /** Whether this parser is marked as trivia (auto-skip) */
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.59.3'
16
+ export const PARSEMAN_VERSION = '0.60.1'
@@ -1,16 +0,0 @@
1
- import type { Combinator } from '../types.ts';
2
- import type { TableProgram } from './program.ts';
3
- /** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
4
- export type TableSettings = {
5
- readonly hostMode?: 'ast' | 'cst';
6
- readonly trackLines?: boolean;
7
- };
8
- /**
9
- * Encode a rule map into ONE table for ONE settings pair.
10
- *
11
- * Two settings pairs give two programs and therefore two cached reference
12
- * tables; the driver that reads them is the same function in both cases and
13
- * never sees `settings`.
14
- */
15
- export declare function encodeTableBaseline(ruleMap: Record<string, Combinator<unknown>>, settings?: TableSettings): TableProgram;
16
- //# sourceMappingURL=encode-baseline.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"encode-baseline.d.ts","sourceRoot":"","sources":["../../src/table/encode-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,aAAa,CAAA;AAUlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAQhD,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAC9B,CAAA;AAqTD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAC5C,QAAQ,GAAE,aAAkB,GAC3B,YAAY,CAId"}
@@ -1,9 +0,0 @@
1
- import { type CompactProgram, type TableProgram, type TableRule } from './program.ts';
2
- /**
3
- * Turn a program into the rule map a compiled artifact exports.
4
- *
5
- * The entries have the SAME signature as codegen rule functions, so `run()`,
6
- * the linker's public wrappers and every consumer are unchanged.
7
- */
8
- export declare function execRulesBaseline(source: TableProgram | CompactProgram): Record<string, TableRule>;
9
- //# sourceMappingURL=exec-baseline.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"exec-baseline.d.ts","sourceRoot":"","sources":["../../src/table/exec-baseline.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EAA6C,KAAK,YAAY,EAAE,KAAK,SAAS,EAClG,MAAM,cAAc,CAAA;AAggBrB;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAwBlG"}
@@ -1,346 +0,0 @@
1
- import type { Combinator, FirstSet, ParserDef } from '../types.ts'
2
- import { firstSetOf, matchesEmpty } from '../combinators/first-set.ts'
3
- import { leadRanges } from '../lead-ranges.ts'
4
- import { deriveExpected } from '../combinators/expect.ts'
5
- import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
6
- import {
7
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
8
- OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
9
- OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_EXPECT,
10
- } from './ops.ts'
11
- import type { TableProgram } from './program.ts'
12
-
13
- /** Raised when a construct has no opcode yet. Prototype scope is explicit. */
14
- class UnsupportedConstructBaseline extends Error {
15
- readonly tag: string
16
- constructor(tag: string) { super(`table lowering: no opcode for '${tag}'`); this.tag = tag }
17
- }
18
-
19
- /** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
20
- export type TableSettings = {
21
- readonly hostMode?: 'ast' | 'cst'
22
- readonly trackLines?: boolean
23
- }
24
-
25
- type Emitted = { readonly ip: number }
26
-
27
- /**
28
- * Do these first sets share no character?
29
- *
30
- * `any` and `empty` are never disjoint from anything — an over-approximating arm
31
- * has to fall back to ordered first-match or the choice silently picks the wrong
32
- * one. See the `Keyword`-ahead-of-`Num` case in the choice encoder.
33
- */
34
- function disjointSets(sets: readonly FirstSet[]): boolean {
35
- const seen: Array<{ lo: number; hi: number }> = []
36
- for (const fs of sets) {
37
- if (fs.kind !== 'ranges' || fs.ranges.length === 0) return false
38
- for (const r of leadRanges(fs.ranges)) {
39
- for (const p of seen) if (r.lo <= p.hi && p.lo <= r.hi) return false
40
- seen.push(r)
41
- }
42
- }
43
- return true
44
- }
45
-
46
- class Encoder {
47
- code: number[] = []
48
- k: unknown[] = []
49
- fns: unknown[] = []
50
- cc: string[] = []
51
- fx: (readonly string[])[] = []
52
- disp: (readonly number[])[] = []
53
- rules: Record<string, number> = {}
54
-
55
- private kIndex = new Map<unknown, number>()
56
- private ccIndex = new Map<string, number>()
57
- private fxIndex = new Map<string, number>()
58
- /** Memoized by combinator identity: a shared sub-combinator is ONE row. */
59
- private memo = new Map<Combinator<unknown>, number>()
60
- private pending = new Map<Combinator<unknown>, number[]>()
61
-
62
- readonly settings: TableSettings
63
- /** Resolved once, HERE, at table-build time — never consulted at run time. */
64
- readonly track: boolean
65
- constructor(settings: TableSettings) {
66
- this.settings = settings
67
- this.track = settings.trackLines === true
68
- }
69
-
70
- private constant(v: unknown): number {
71
- const key = v instanceof RegExp ? `re:${JSON.stringify([v.source, v.flags])}` : v
72
- const hit = this.kIndex.get(key)
73
- if (hit !== undefined) return hit
74
- const i = this.k.length
75
- this.k.push(v)
76
- this.kIndex.set(key, i)
77
- return i
78
- }
79
-
80
- private fn(v: unknown): number {
81
- const i = this.fns.length
82
- this.fns.push(v)
83
- return i
84
- }
85
-
86
- private expected(list: readonly string[]): number {
87
- const key = JSON.stringify(list)
88
- const hit = this.fxIndex.get(key)
89
- if (hit !== undefined) return hit
90
- const i = this.fx.length
91
- this.fx.push(list)
92
- this.fxIndex.set(key, i)
93
- return i
94
- }
95
-
96
- /** A first set becomes a char class STRING of `[lo, hi]` pairs, or −1 for `any`. */
97
- private charClass(fs: FirstSet): number {
98
- if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
99
- let spec = ''
100
- for (const r of fs.ranges) spec += String.fromCharCode(r.lo, r.hi)
101
- const hit = this.ccIndex.get(spec)
102
- if (hit !== undefined) return hit
103
- const i = this.cc.length
104
- this.cc.push(spec)
105
- this.ccIndex.set(spec, i)
106
- return i
107
- }
108
-
109
- private emit(...words: number[]): number {
110
- const ip = this.code.length
111
- for (const w of words) this.code.push(w)
112
- return ip
113
- }
114
-
115
- /** Reserve `n` operand slots to patch once children are laid out. */
116
- private emitHead(op: number, n: number): number {
117
- const ip = this.code.length
118
- this.code.push(op)
119
- for (let i = 0; i < n; i++) this.code.push(0)
120
- return ip
121
- }
122
-
123
- encodeRule(name: string, p: Combinator<unknown>): void {
124
- this.rules[name] = this.node(p).ip
125
- }
126
-
127
- node(p: Combinator<unknown>): Emitted {
128
- const hit = this.memo.get(p)
129
- if (hit !== undefined) return { ip: hit }
130
- // Recursion: reserve a trampoline row now, patch its target when the real
131
- // body lands. One extra indirection per recursive rule, zero per call site.
132
- const inFlight = this.pending.get(p)
133
- if (inFlight !== undefined) {
134
- const ip = this.emitHead(OP_RULE, 1)
135
- inFlight.push(ip + 1)
136
- return { ip }
137
- }
138
- const patches: number[] = []
139
- this.pending.set(p, patches)
140
- const ip = this.encodeDef(p)
141
- this.pending.delete(p)
142
- this.memo.set(p, ip)
143
- for (const slot of patches) this.code[slot] = ip
144
- return { ip }
145
- }
146
-
147
- private encodeDef(p: Combinator<unknown>): number {
148
- const d = p._def as ParserDef
149
- // REFUSE what this frozen copy cannot lower, rather than lowering it wrong.
150
- //
151
- // `scanTo` and `token` have no `case` below, so they reach `default:` and throw
152
- // — a refusal, which is safe. `balanced()` does NOT: it overrides `.parse` and
153
- // leaves `_def` as the EAGER INTERIOR with no distinguishing tag, so the switch
154
- // would encode the interior, report nothing, and produce a table that parses and
155
- // yields a different tree. An ablation measurement taken on a grammar using
156
- // `balanced()` would then be comparing against a parser that is not the one
157
- // under test. Refused explicitly, since this copy has no OP_CALL to fall back on.
158
- if ((p as { _balancedAmbient?: unknown })._balancedAmbient !== undefined) {
159
- throw new UnsupportedConstructBaseline('balanced() — the frozen ablation encoder cannot lower it')
160
- }
161
- switch (d.tag) {
162
- case 'literal': {
163
- if (d.caseInsensitive) throw new UnsupportedConstructBaseline('literal(caseInsensitive)')
164
- return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(deriveExpected(p)))
165
- }
166
- case 'regex': {
167
- const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
168
- return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(deriveExpected(p)))
169
- }
170
- case 'keywords': {
171
- // `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
172
- // exactly what OP_RX does. Rebuilding the same regex here needs no new
173
- // opcode. Construction mirrors src/combinators/keywords.ts:87-106; the
174
- // words arrive already sorted longest-first on the def.
175
- const alt = d.words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
176
- const boundary = d.boundary ? `(?![${d.boundary}])` : ''
177
- const flags = d.caseInsensitive ? 'iy' : 'uy'
178
- return this.emit(
179
- this.track ? OP_RX_TRACK : OP_RX,
180
- this.constant(new RegExp(`(?:${alt})${boundary}`, flags)),
181
- this.expected(deriveExpected(p)),
182
- )
183
- }
184
- case 'expect': {
185
- // `expect()` NEVER fails: on a failed child it yields a ParseError value
186
- // at zero width. That is a distinct outcome, so it gets its own row.
187
- return this.emit(OP_EXPECT, this.node(d.parser).ip, this.expected([...d.expected]))
188
- }
189
- case 'sequence': {
190
- const kids = d.parsers.map(c => this.node(c).ip)
191
- const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length)
192
- this.code[head + 1] = kids.length
193
- for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
194
- return head
195
- }
196
- case 'choice': {
197
- if (d.gates.some(g => g !== null)) throw new UnsupportedConstructBaseline('choice(gate:)')
198
- // Arm ORDER is semantics. `literalsLongestFirst` / `sharedPrefix` /
199
- // `greedyClassify` reorder or restructure the arms, so encoding them as
200
- // a plain ordered choice would pick a different arm and build a
201
- // different tree while still parsing. Refuse them rather than lower them
202
- // wrong.
203
- // `greedyClassify` runs ONE arm and then re-attributes the match to a
204
- // DIFFERENT arm by string equality, re-applying that arm's transforms.
205
- // That is a different execution, not a different order, so it is refused
206
- // rather than approximated.
207
- if (d.strategy.tag === 'greedyClassify') throw new UnsupportedConstructBaseline('choice(strategy=greedyClassify)')
208
- // `autoNot` rejects an arm that matched but is followed by a char in a
209
- // sibling's first set, so a later arm can win. Ignoring it would pick a
210
- // different arm and build a different tree behind a successful parse.
211
- if (d.autoNot.some(a => a !== null)) throw new UnsupportedConstructBaseline('choice(autoNot)')
212
- // ORDER is the only thing the remaining strategies change, and order is
213
- // table data. `literalsLongestFirst` carries its order explicitly;
214
- // `sharedPrefix` is documented in choice.ts:52 as a firstMatch
215
- // specialization, so declared order is already correct for it.
216
- const order = d.strategy.tag === 'literalsLongestFirst'
217
- ? d.strategy.sortedIndices
218
- : d.parsers.map((_, i) => i)
219
- const arms = order.map(i => d.parsers[i]!)
220
- const kids = arms.map(c => this.node(c).ip)
221
- // O(1) first-char dispatch is only SOUND when the arms are disjoint.
222
- // With overlapping first sets, "the first arm whose class contains this
223
- // char" is not "the first arm that matches": a `Keyword` arm whose first
224
- // set over-approximates to include digits was selected ahead of `Num`
225
- // for the input `0` — the parse still succeeded, and only the TREE moved.
226
- // Disjointness is computed HERE, not read off `d.disjoint`. That flag is
227
- // set when `choice()` is CONSTRUCTED, at which point `g.X` arms are
228
- // unresolved refs whose first set is `any` — so every recursive grammar
229
- // reports non-disjoint and loses its dispatch. (Codegen has the same
230
- // problem and solves it the same way: a placeholder resolved at fuse.)
231
- let dispIdx = -1
232
- if (arms.every(a => !matchesEmpty(a))) {
233
- const sets = arms.map(a => firstSetOf(a))
234
- if (disjointSets(sets)) {
235
- const classes = sets.map(fs => this.charClass(fs))
236
- if (classes.every(c => c >= 0)) {
237
- dispIdx = this.disp.length
238
- this.disp.push(classes)
239
- }
240
- }
241
- }
242
- const head = this.emitHead(OP_CHOICE, 2 + kids.length)
243
- this.code[head + 1] = dispIdx
244
- this.code[head + 2] = kids.length
245
- for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
246
- return head
247
- }
248
- case 'many':
249
- case 'oneOrMore': {
250
- const child = this.node(d.parser).ip
251
- return this.emit(
252
- d.valueUnused ? OP_REPV : OP_REP,
253
- child, d.tag === 'many' ? 0 : d.min, d.max ?? -1, -1, 0,
254
- )
255
- }
256
- case 'sepBy': {
257
- const child = this.node(d.parser).ip
258
- const sep = this.node(d.separator).ip
259
- return this.emit(OP_REP, child, d.min, d.max ?? -1, sep, d.trailing === 'allow' ? 1 : 0)
260
- }
261
- case 'optional':
262
- return this.emit(OP_OPT, this.node(d.parser).ip)
263
- case 'transform': {
264
- const child = this.node(d.parser).ip
265
- return this.emit(OP_XFORM, this.fn(d.fn), child)
266
- }
267
- case 'leaf': {
268
- const child = this.node(d.parser).ip
269
- return this.emit(d.cstValue === 'source' ? OP_SOURCE_LEAF : OP_LEAF, this.fn(d.fn), child)
270
- }
271
- case 'node': {
272
- if (d.unwrap || d.collapse || d.project !== undefined) throw new UnsupportedConstructBaseline('node(unwrap|collapse|project)')
273
- if (d.build === undefined) throw new UnsupportedConstructBaseline('node(no build)')
274
- const child = this.node(d.parser).ip
275
- // Capture flags, resolved HERE from the reducer's declared arity using the
276
- // same analysis codegen runs (`src/compiler/build-arity.ts`). `hostMode:
277
- // 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
278
- // driver reads a bit; it re-derives nothing and sees no setting.
279
- const cstOut = this.settings.hostMode === 'cst'
280
- const flags = (cstOut || buildReadsTrivia(d) ? 4 : 0) | (cstOut || buildReadsState(d) ? 8 : 0)
281
- const body = this.emit(this.track ? OP_NODE_TRACK : OP_NODE, this.fn(d.build), child, flags)
282
- // The rule's own first-set gate — the emitted code's `_ngc` test, as data.
283
- // A NULLABLE rule has no gate: it succeeds on input its first set does
284
- // not contain (including EOF), so gating it would reject a legal empty
285
- // match. Caught by the ladder's `empty` and `garbage` cases.
286
- if (matchesEmpty(p)) return body
287
- const cls = this.charClass(firstSetOf(p))
288
- if (cls < 0) return body
289
- return this.emit(OP_GATE, cls, body, this.expected(deriveExpected(p)))
290
- }
291
- case 'lazy':
292
- // A named reference is not a hop: it resolves to the target's row.
293
- // Emitting a trampoline here cost one dispatch per reference for nothing.
294
- return this.node(d.thunk()).ip
295
- case 'not':
296
- return this.emit(OP_NOT, this.node(d.parser).ip)
297
- case 'peek':
298
- return this.emit(OP_PEEK, this.node(d.parser).ip)
299
- // A contained transaction changes what an enclosing choice does next, so
300
- // it cannot be transparent.
301
- case 'attempt':
302
- if (d.contain === true) throw new UnsupportedConstructBaseline('attempt(contain)')
303
- return this.node(d.parser).ip
304
- // Transparent wrappers: no row of their own, no dispatch at run time.
305
- case 'token':
306
- case 'label':
307
- case 'trivia':
308
- return this.node(d.parser).ip
309
- case 'grammar': {
310
- // A trivia scope is a ROW, not a lowering decision: the scope's trivia
311
- // combinator goes in the const pool and the driver installs it.
312
- if (d.clearTrivia === true) return this.emit(OP_SCOPE, -1, this.node(d.parser).ip)
313
- if (d.triviaParser === undefined) return this.node(d.parser).ip
314
- return this.emit(OP_SCOPE, this.constant(d.triviaParser), this.node(d.parser).ip)
315
- }
316
- default:
317
- throw new UnsupportedConstructBaseline(d.tag)
318
- }
319
- }
320
-
321
- finish(): TableProgram {
322
- if (this.code.length === 0) this.emit(OP_EMPTY)
323
- return {
324
- code: this.code, k: this.k, fns: this.fns, cc: this.cc,
325
- fx: this.fx, disp: this.disp, dsp: [], rules: this.rules,
326
- lines: this.track ? 1 : 0,
327
- ...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
328
- }
329
- }
330
- }
331
-
332
- /**
333
- * Encode a rule map into ONE table for ONE settings pair.
334
- *
335
- * Two settings pairs give two programs and therefore two cached reference
336
- * tables; the driver that reads them is the same function in both cases and
337
- * never sees `settings`.
338
- */
339
- export function encodeTableBaseline(
340
- ruleMap: Record<string, Combinator<unknown>>,
341
- settings: TableSettings = {},
342
- ): TableProgram {
343
- const enc = new Encoder(settings)
344
- for (const name of Object.keys(ruleMap)) enc.encodeRule(name, ruleMap[name]!)
345
- return enc.finish()
346
- }