parseman 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2955 -357
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2946 -348
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/expect.d.ts +1 -0
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/expected.d.ts +8 -0
  9. package/dist/combinators/expected.d.ts.map +1 -0
  10. package/dist/combinators/first-set.d.ts +8 -0
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/grammar.d.ts.map +1 -1
  13. package/dist/combinators/keywords.d.ts +4 -0
  14. package/dist/combinators/keywords.d.ts.map +1 -1
  15. package/dist/combinators/literal.d.ts.map +1 -1
  16. package/dist/combinators/not.d.ts.map +1 -1
  17. package/dist/combinators/peek.d.ts.map +1 -1
  18. package/dist/combinators/regex.d.ts.map +1 -1
  19. package/dist/combinators/repeat.d.ts.map +1 -1
  20. package/dist/combinators/sequence.d.ts.map +1 -1
  21. package/dist/combinators/trivia-skip.d.ts +19 -1
  22. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  23. package/dist/compiler/direct-projection.d.ts +12 -0
  24. package/dist/compiler/direct-projection.d.ts.map +1 -0
  25. package/dist/compiler/token-alphabet.d.ts +845 -5
  26. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  27. package/dist/compiler/token-capability.d.ts +3 -0
  28. package/dist/compiler/token-capability.d.ts.map +1 -0
  29. package/dist/compiler/token-capability.js +3192 -0
  30. package/dist/compiler/token-capability.js.map +6 -0
  31. package/dist/cst/trivia-charscan.d.ts +3 -0
  32. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  33. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  34. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  35. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  36. package/dist/index.js +2959 -348
  37. package/dist/index.js.map +3 -3
  38. package/dist/language-service/index.js +7 -1
  39. package/dist/language-service/index.js.map +1 -1
  40. package/dist/plugin/index.js +2969 -358
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/spec/index.js +366 -49
  43. package/dist/spec/index.js.map +2 -2
  44. package/dist/table/assemble.d.ts.map +1 -1
  45. package/dist/table/child-slots.d.ts.map +1 -1
  46. package/dist/table/compile-rule-map.d.ts +3 -1
  47. package/dist/table/compile-rule-map.d.ts.map +1 -1
  48. package/dist/table/emit-assembly.d.ts +13 -14
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts.map +1 -1
  51. package/dist/table/encode.d.ts.map +1 -1
  52. package/dist/table/exec-baseline.d.ts.map +1 -1
  53. package/dist/table/exec.d.ts.map +1 -1
  54. package/dist/table/index.js +4839 -2232
  55. package/dist/table/index.js.map +3 -3
  56. package/dist/table/lex-body.d.ts +9 -0
  57. package/dist/table/lex-body.d.ts.map +1 -0
  58. package/dist/table/lex-program.d.ts +25 -0
  59. package/dist/table/lex-program.d.ts.map +1 -0
  60. package/dist/table/ops.d.ts +47 -4
  61. package/dist/table/ops.d.ts.map +1 -1
  62. package/dist/table/program.d.ts +53 -2
  63. package/dist/table/program.d.ts.map +1 -1
  64. package/dist/table/scalar-terminal.d.ts +13 -0
  65. package/dist/table/scalar-terminal.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/package.json +12 -11
  71. package/src/combinators/expect.ts +1 -0
  72. package/src/combinators/expected.ts +17 -0
  73. package/src/combinators/first-set.ts +82 -3
  74. package/src/combinators/grammar.ts +8 -3
  75. package/src/combinators/keywords.ts +22 -6
  76. package/src/combinators/literal.ts +2 -1
  77. package/src/combinators/not.ts +5 -1
  78. package/src/combinators/peek.ts +5 -1
  79. package/src/combinators/regex.ts +5 -3
  80. package/src/combinators/repeat.ts +4 -10
  81. package/src/combinators/sequence.ts +35 -16
  82. package/src/combinators/trivia-skip.ts +75 -11
  83. package/src/compiler/direct-projection.ts +43 -0
  84. package/src/compiler/token-alphabet.ts +4316 -12
  85. package/src/compiler/token-capability.ts +6 -0
  86. package/src/cst/trivia-charscan.ts +34 -0
  87. package/src/cst/trivia-css-scanner.ts +142 -0
  88. package/src/cst/trivia-kinds.ts +3 -1
  89. package/src/plugin/index.ts +4 -4
  90. package/src/table/assemble.ts +1224 -222
  91. package/src/table/child-slots.ts +4 -3
  92. package/src/table/compile-rule-map.ts +21 -3
  93. package/src/table/emit-assembly.ts +363 -109
  94. package/src/table/emit.ts +26 -1
  95. package/src/table/encode.ts +246 -54
  96. package/src/table/exec-baseline.ts +3 -7
  97. package/src/table/exec.ts +140 -18
  98. package/src/table/lex-body.ts +30 -0
  99. package/src/table/lex-program.ts +422 -0
  100. package/src/table/ops.ts +50 -4
  101. package/src/table/program.ts +67 -2
  102. package/src/table/scalar-terminal.ts +51 -0
  103. package/src/table/site-labels.ts +13 -3
  104. package/src/table/stamp.ts +3 -3
  105. package/src/types.ts +1 -1
  106. package/src/version.ts +1 -1
  107. package/dist/analysis/diagnostics.cjs +0 -13736
  108. package/dist/analysis/diagnostics.cjs.map +0 -6
  109. package/dist/cli/index.cjs +0 -14837
  110. package/dist/cli/index.cjs.map +0 -6
  111. package/dist/index.cjs +0 -16303
  112. package/dist/index.cjs.map +0 -6
  113. package/dist/language-service/index.cjs +0 -1302
  114. package/dist/language-service/index.cjs.map +0 -6
  115. package/dist/oracle/index.cjs +0 -254
  116. package/dist/oracle/index.cjs.map +0 -6
  117. package/dist/plugin/index.cjs +0 -16009
  118. package/dist/plugin/index.cjs.map +0 -6
  119. package/dist/run/index.cjs +0 -577
  120. package/dist/run/index.cjs.map +0 -6
  121. package/dist/spec/index.cjs +0 -6188
  122. package/dist/spec/index.cjs.map +0 -6
  123. package/dist/table/index.cjs +0 -11375
  124. package/dist/table/index.cjs.map +0 -6
@@ -0,0 +1,422 @@
1
+ import type { Combinator, ParseContext } from '../types.ts'
2
+ import { failAt } from '../combinators/probe.ts'
3
+ import type { LexBodyRecognizer } from './lex-body.ts'
4
+ import type { LexProgramSpec, ResolvedClass } from './program.ts'
5
+
6
+ /** A selected composite lexical body. Failure state is already published.
7
+ * `scan` is present only for the balanced shell and indexes the canonical
8
+ * driver-owned scan pool; it is not another recognizer implementation. */
9
+ export type LexProgramRunner = {
10
+ (input: string, pos: number, ctx: ParseContext, scan?: Combinator<unknown>): number
11
+ readonly scan?: number
12
+ }
13
+
14
+ export const LEX_NODE_TERMINAL = 0
15
+ export const LEX_NODE_SEQUENCE2 = 1
16
+ export const LEX_NODE_SEQUENCE3 = 2
17
+ export const LEX_NODE_SEQUENCE4 = 3
18
+ export const LEX_NODE_SEQUENCE5 = 4
19
+ export const LEX_NODE_CHOICE2 = 5
20
+ export const LEX_NODE_CHOICE4 = 6
21
+ export const LEX_NODE_CHOICE8 = 7
22
+ export const LEX_NODE_NOT = 8
23
+ export const LEX_NODE_OPTIONAL = 9
24
+
25
+ export function lexProgramDigest(words: readonly number[]): number {
26
+ let hash = 0x811C9DC5
27
+ for (const word of words) {
28
+ hash ^= word | 0
29
+ hash = Math.imul(hash, 0x01000193)
30
+ }
31
+ return hash | 0
32
+ }
33
+
34
+ function classHas(cls: ResolvedClass, code: number): boolean {
35
+ if (code < 0) return false
36
+ if (code < 128) return cls.ascii[code] === 1
37
+ for (let i = 0; i < cls.hi.length; i += 2) {
38
+ if (code >= cls.hi[i]! && code <= cls.hi[i + 1]!) return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ type PureLexRunner = (input: string, pos: number) => number
44
+ type BuiltLexNode = { readonly match: PureLexRunner; readonly run: LexProgramRunner }
45
+ const committed = (ctx: ParseContext): boolean => ctx._fc === true
46
+
47
+ function trackLexLines(ctx: ParseContext, input: string, end: number): void {
48
+ let at = ctx._lineScannedTo ?? 0
49
+ const starts = ctx._lineStarts
50
+ if (starts === undefined || end <= at) return
51
+ while (at < end) {
52
+ if (input.charCodeAt(at) === 10) starts.push(at + 1)
53
+ at++
54
+ }
55
+ ctx._lineScannedTo = end
56
+ }
57
+
58
+ function terminalNode(
59
+ matcher: LexBodyRecognizer,
60
+ failure: readonly string[],
61
+ tracksLines: boolean,
62
+ ): BuiltLexNode {
63
+ const match: PureLexRunner = (input, pos) => {
64
+ const result = matcher(input, pos)
65
+ return result < 0 ? -1 : result / 2
66
+ }
67
+ return {
68
+ match,
69
+ run: (input, pos, ctx) => {
70
+ const end = match(input, pos)
71
+ if (end < 0) {
72
+ ctx._fe = pos
73
+ ctx._fx = failure as string[]
74
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
75
+ return -1
76
+ }
77
+ if (tracksLines) trackLexLines(ctx, input, end)
78
+ return end
79
+ },
80
+ }
81
+ }
82
+
83
+ function sequence2(a: BuiltLexNode, b: BuiltLexNode): BuiltLexNode {
84
+ return {
85
+ match: (input, pos) => {
86
+ const e0 = a.match(input, pos)
87
+ return e0 < 0 ? -1 : b.match(input, e0)
88
+ },
89
+ run: (input, pos, ctx) => {
90
+ const e0 = a.run(input, pos, ctx)
91
+ if (e0 < 0) return -1
92
+ ctx._fc = false
93
+ return b.run(input, e0, ctx)
94
+ },
95
+ }
96
+ }
97
+
98
+ function sequence3(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode): BuiltLexNode {
99
+ return {
100
+ match: (input, pos) => {
101
+ const e0 = a.match(input, pos)
102
+ if (e0 < 0) return -1
103
+ const e1 = b.match(input, e0)
104
+ return e1 < 0 ? -1 : c.match(input, e1)
105
+ },
106
+ run: (input, pos, ctx) => {
107
+ const e0 = a.run(input, pos, ctx)
108
+ if (e0 < 0) return -1
109
+ ctx._fc = false
110
+ const e1 = b.run(input, e0, ctx)
111
+ if (e1 < 0) return -1
112
+ ctx._fc = false
113
+ return c.run(input, e1, ctx)
114
+ },
115
+ }
116
+ }
117
+
118
+ function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode): BuiltLexNode {
119
+ const abc = sequence3(a, b, c)
120
+ return {
121
+ match: (input, pos) => {
122
+ const end = abc.match(input, pos)
123
+ return end < 0 ? -1 : d.match(input, end)
124
+ },
125
+ run: (input, pos, ctx) => {
126
+ const end = abc.run(input, pos, ctx)
127
+ if (end < 0) return -1
128
+ ctx._fc = false
129
+ return d.run(input, end, ctx)
130
+ },
131
+ }
132
+ }
133
+
134
+ function sequence5(
135
+ a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode, e: BuiltLexNode,
136
+ ): BuiltLexNode {
137
+ const abcd = sequence4(a, b, c, d)
138
+ return {
139
+ match: (input, pos) => {
140
+ const end = abcd.match(input, pos)
141
+ return end < 0 ? -1 : e.match(input, end)
142
+ },
143
+ run: (input, pos, ctx) => {
144
+ const end = abcd.run(input, pos, ctx)
145
+ if (end < 0) return -1
146
+ ctx._fc = false
147
+ return e.run(input, end, ctx)
148
+ },
149
+ }
150
+ }
151
+
152
+ type ChoiceTail = (
153
+ input: string, pos: number, ctx: ParseContext, lead: number, acc: string[] | undefined,
154
+ ) => number
155
+
156
+ function appendExpected(acc: string[] | undefined, values: readonly string[] | undefined): string[] | undefined {
157
+ if (values === undefined || values.length === 0) return acc
158
+ if (acc === undefined) return values.slice()
159
+ acc.push(...values)
160
+ return acc
161
+ }
162
+
163
+ function choiceNode(
164
+ arms: readonly BuiltLexNode[],
165
+ classes: readonly (ResolvedClass | undefined)[],
166
+ armExpected: readonly (readonly string[])[],
167
+ choiceExpected: readonly string[],
168
+ ): BuiltLexNode {
169
+ let matchTail: PureLexRunner = () => -1
170
+ for (let i = arms.length - 1; i >= 0; i--) {
171
+ const arm = arms[i]!, next = matchTail
172
+ matchTail = (input, pos) => {
173
+ const end = arm.match(input, pos)
174
+ return end < 0 ? next(input, pos) : end
175
+ }
176
+ }
177
+ let tail: ChoiceTail = (_input, pos, ctx, _lead, acc) => {
178
+ ctx._fe = pos
179
+ ctx._fx = acc ?? choiceExpected as string[]
180
+ return -1
181
+ }
182
+ for (let i = arms.length - 1; i >= 0; i--) {
183
+ const arm = arms[i]!, cls = classes[i], staticExpected = armExpected[i]!, next = tail
184
+ tail = (input, pos, ctx, lead, acc) => {
185
+ if (cls !== undefined && !classHas(cls, lead)) {
186
+ return next(input, pos, ctx, lead, appendExpected(acc, staticExpected))
187
+ }
188
+ ctx._fc = false
189
+ const end = arm.run(input, pos, ctx)
190
+ if (end >= 0) return end
191
+ const nextAcc = appendExpected(acc, ctx._fx)
192
+ if (committed(ctx)) {
193
+ if (nextAcc !== undefined) ctx._fx = nextAcc
194
+ return -1
195
+ }
196
+ return next(input, pos, ctx, lead, nextAcc)
197
+ }
198
+ }
199
+ return {
200
+ match: matchTail,
201
+ run: (input, pos, ctx) => tail(input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined),
202
+ }
203
+ }
204
+
205
+ function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
206
+ return {
207
+ match: (input, pos) => body.match(input, pos) < 0 ? pos : -1,
208
+ run: (input, pos, ctx) => {
209
+ if (body.match(input, pos) < 0) return pos
210
+ ctx._fe = pos
211
+ ctx._fx = failure as string[]
212
+ return -1
213
+ },
214
+ }
215
+ }
216
+
217
+ function optionalNode(body: BuiltLexNode): BuiltLexNode {
218
+ return {
219
+ match: (input, pos) => {
220
+ const end = body.match(input, pos)
221
+ return end < 0 ? pos : end
222
+ },
223
+ run: (input, pos, ctx) => {
224
+ ctx._fc = false
225
+ const end = body.run(input, pos, ctx)
226
+ return end < 0 && !committed(ctx) ? pos : end
227
+ },
228
+ }
229
+ }
230
+
231
+ function fixedTreeRunner(
232
+ words: readonly number[],
233
+ matchers: readonly LexBodyRecognizer[],
234
+ classes: readonly ResolvedClass[],
235
+ expected: readonly (readonly string[])[],
236
+ ): LexProgramRunner {
237
+ const stack: BuiltLexNode[] = []
238
+ const pop = (): BuiltLexNode => {
239
+ const value = stack.pop()
240
+ if (value === undefined) throw new TypeError('table lexical tree has an incomplete fixed body')
241
+ return value
242
+ }
243
+ for (let at = 0; at < words.length;) {
244
+ const op = words[at++]!
245
+ if (op === LEX_NODE_TERMINAL) {
246
+ const matcher = matchers[words[at++]!], failure = expected[words[at++]!]
247
+ const tracks = words[at++]
248
+ if (matcher === undefined || failure === undefined || (tracks !== 0 && tracks !== 1)) {
249
+ throw new TypeError('table lexical tree terminal references an invalid pool entry')
250
+ }
251
+ stack.push(terminalNode(matcher, failure, tracks === 1))
252
+ continue
253
+ }
254
+ if (op >= LEX_NODE_SEQUENCE2 && op <= LEX_NODE_SEQUENCE5) {
255
+ const count = op - LEX_NODE_SEQUENCE2 + 2
256
+ const nodes = Array.from({ length: count }, pop).reverse()
257
+ if (count === 2) stack.push(sequence2(nodes[0]!, nodes[1]!))
258
+ else if (count === 3) stack.push(sequence3(nodes[0]!, nodes[1]!, nodes[2]!))
259
+ else if (count === 4) stack.push(sequence4(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!))
260
+ else stack.push(sequence5(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!, nodes[4]!))
261
+ continue
262
+ }
263
+ if (op >= LEX_NODE_CHOICE2 && op <= LEX_NODE_CHOICE8) {
264
+ const count = op === LEX_NODE_CHOICE2 ? 2 : op === LEX_NODE_CHOICE4 ? 4 : 8
265
+ const choiceFx = expected[words[at++]!]
266
+ if (choiceFx === undefined) throw new TypeError('table lexical tree choice has an invalid expected set')
267
+ const armClasses: Array<ResolvedClass | undefined> = []
268
+ const armFx: Array<readonly string[]> = []
269
+ for (let i = 0; i < count; i++) {
270
+ const cls = words[at++]!, fx = expected[words[at++]!]
271
+ if (cls < -1 || (cls >= 0 && classes[cls] === undefined) || fx === undefined) {
272
+ throw new TypeError('table lexical tree choice references an invalid pool entry')
273
+ }
274
+ armClasses.push(cls < 0 ? undefined : classes[cls])
275
+ armFx.push(fx)
276
+ }
277
+ const arms = Array.from({ length: count }, pop).reverse()
278
+ stack.push(choiceNode(arms, armClasses, armFx, choiceFx))
279
+ continue
280
+ }
281
+ if (op === LEX_NODE_NOT) {
282
+ const failure = expected[words[at++]!]
283
+ if (failure === undefined) throw new TypeError('table lexical tree assertion has an invalid expected set')
284
+ stack.push(notNode(pop(), failure))
285
+ continue
286
+ }
287
+ if (op === LEX_NODE_OPTIONAL) { stack.push(optionalNode(pop())); continue }
288
+ throw new TypeError('table lexical tree has an unknown fixed body')
289
+ }
290
+ if (stack.length !== 1) throw new TypeError('table lexical tree does not resolve to one root body')
291
+ return stack[0]!.run
292
+ }
293
+
294
+ /** Decode one fixed selected-body tuple. There is no lexical opcode loop on
295
+ * the parse path: each supported tuple becomes one scalar closure here. */
296
+ export function buildLexProgramRunner(
297
+ spec: LexProgramSpec,
298
+ matchers: readonly LexBodyRecognizer[],
299
+ classes: readonly ResolvedClass[],
300
+ expected: readonly (readonly string[])[],
301
+ ): LexProgramRunner {
302
+ if ((spec[0] !== 0 && spec[0] !== 1 && spec[0] !== 2 && spec[0] !== 3)
303
+ || (spec[0] === 0 && spec.length !== 13)
304
+ || (spec[0] === 1 && spec.length !== 12)
305
+ || (spec[0] === 2 && spec.length < 6)
306
+ || (spec[0] === 3 && spec.length !== 3)) {
307
+ throw new TypeError('table lexical program has an invalid fixed body width')
308
+ }
309
+ if (spec[1] !== lexProgramDigest(spec.slice(2))) {
310
+ throw new TypeError('table lexical program semantic digest is inconsistent')
311
+ }
312
+ if (spec[0] === 3) {
313
+ if (!Number.isInteger(spec[2]) || spec[2] < 0) {
314
+ throw new TypeError('table balanced lexical program has an invalid scan reference')
315
+ }
316
+ const run: LexProgramRunner = (_input, _pos, ctx, scan) => {
317
+ if (scan === undefined) throw new TypeError('table balanced lexical program lost its scan body')
318
+ const result = scan.parse(_input, _pos, ctx)
319
+ if (!result.ok) {
320
+ ctx._fe = result.span.start
321
+ ctx._fx = (result.expected ?? []) as string[]
322
+ return -1
323
+ }
324
+ return result.span.end
325
+ }
326
+ return Object.assign(run, { scan: spec[2] })
327
+ }
328
+ if (spec[0] === 2) return fixedTreeRunner(spec.slice(2), matchers, classes, expected)
329
+ if (spec[0] === 1) {
330
+ const g00 = matchers[spec[2]], g01 = matchers[spec[3]]
331
+ const g02 = matchers[spec[4]], g03 = matchers[spec[5]]
332
+ const guard0Fx = expected[spec[6]]
333
+ const g1 = matchers[spec[7]], guard1Fx = expected[spec[8]]
334
+ const terminal = matchers[spec[9]], terminalFx = expected[spec[10]]
335
+ const tracks = spec[11]
336
+ if (g00 === undefined || g01 === undefined || g02 === undefined || g03 === undefined
337
+ || guard0Fx === undefined || g1 === undefined || guard1Fx === undefined
338
+ || terminal === undefined || terminalFx === undefined || (tracks !== 0 && tracks !== 1)) {
339
+ throw new TypeError('table lexical program references an invalid pool entry')
340
+ }
341
+ return (input, pos, ctx) => {
342
+ ctx._fc = false
343
+ if (g00(input, pos) >= 0 || g01(input, pos) >= 0
344
+ || g02(input, pos) >= 0 || g03(input, pos) >= 0) {
345
+ ctx._fe = pos
346
+ ctx._fx = guard0Fx as string[]
347
+ return -1
348
+ }
349
+ ctx._fc = false
350
+ if (g1(input, pos) >= 0) {
351
+ ctx._fe = pos
352
+ ctx._fx = guard1Fx as string[]
353
+ return -1
354
+ }
355
+ ctx._fc = false
356
+ const result = terminal(input, pos)
357
+ if (result < 0) {
358
+ ctx._fe = pos
359
+ ctx._fx = terminalFx as string[]
360
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
361
+ return -1
362
+ }
363
+ const end = result / 2
364
+ if (tracks === 1) trackLexLines(ctx, input, end)
365
+ return end
366
+ }
367
+ }
368
+ const cls = classes[spec[2]]
369
+ const choiceFx = expected[spec[3]]
370
+ const m0 = matchers[spec[4]], x0 = expected[spec[5]]
371
+ const m1 = matchers[spec[6]], x1 = expected[spec[7]]
372
+ const m2 = matchers[spec[8]], x2 = expected[spec[9]]
373
+ const m3 = matchers[spec[10]], x3 = expected[spec[11]]
374
+ const tracks = spec[12]
375
+ if (cls === undefined || choiceFx === undefined
376
+ || m0 === undefined || x0 === undefined || m1 === undefined || x1 === undefined
377
+ || m2 === undefined || x2 === undefined || m3 === undefined || x3 === undefined
378
+ || (tracks !== 0 && tracks !== 1)) {
379
+ throw new TypeError('table lexical program references an invalid pool entry')
380
+ }
381
+
382
+ return (input, pos, ctx) => {
383
+ const lead = pos < input.length ? input.codePointAt(pos)! : -1
384
+ if (!classHas(cls, lead)) {
385
+ ctx._fe = pos
386
+ ctx._fx = choiceFx.slice()
387
+ return -1
388
+ }
389
+ let acc: string[] | undefined
390
+ const failed = (fx: readonly string[]): void => {
391
+ ctx._fe = pos
392
+ ctx._fx = fx as string[]
393
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
394
+ if (acc === undefined) acc = fx.slice()
395
+ else acc.push(...fx)
396
+ }
397
+ ctx._fc = false
398
+ const success = (result: number): number => {
399
+ const end = result / 2
400
+ if (tracks === 1) trackLexLines(ctx, input, end)
401
+ return end
402
+ }
403
+ let result = m0(input, pos)
404
+ if (result >= 0) return success(result)
405
+ failed(x0)
406
+ ctx._fc = false
407
+ result = m1(input, pos)
408
+ if (result >= 0) return success(result)
409
+ failed(x1)
410
+ ctx._fc = false
411
+ result = m2(input, pos)
412
+ if (result >= 0) return success(result)
413
+ failed(x2)
414
+ ctx._fc = false
415
+ result = m3(input, pos)
416
+ if (result >= 0) return success(result)
417
+ failed(x3)
418
+ ctx._fe = pos
419
+ ctx._fx = acc ?? choiceFx as string[]
420
+ return -1
421
+ }
422
+ }
package/src/table/ops.ts CHANGED
@@ -37,8 +37,11 @@ export const OP_SEQV = 4
37
37
  */
38
38
  export const OP_CHOICE = 5
39
39
  /**
40
- * `REP c min max sep flags` — `sep` is a child offset or −1; `max` −1 = ∞.
41
- * `flags` bit 0 = trailing separator allowed, bit 1 = `keepSeparators`.
40
+ * `REP c min max sep flags fx sepClass` — `sep` is a child offset or −1; `max`
41
+ * −1 = ∞. `fx` is recovery metadata. The last class is the separator sentinel
42
+ * for a separated list, otherwise the finite/non-nullable optional-item class
43
+ * (or −1). `flags` bit 0 = trailing separator allowed, bit 1 =
44
+ * `keepSeparators`, bit 2 = item expected reporting.
42
45
  */
43
46
  export const OP_REP = 6
44
47
  /** `REPV …` — `REP` with `valueUnused`. */
@@ -57,7 +60,8 @@ export const OP_XFORM = 9
57
60
  * none), `type` and `tags` index the const pool (`tags` −1 = none).
58
61
  *
59
62
  * `flags` is a BIT FIELD, not a boolean: bit 0 (`&1`) = grammar-owned explicit
60
- * `captureTrivia`, bit 2 (`&4`) = the builder reads
63
+ * `captureTrivia`, bit 1 (`&2`) = a direct builder is proven not to read
64
+ * `rawChildren`, bit 2 (`&4`) = the builder reads
61
65
  * `triviaLog`, bit 3 (`&8`) = it reads `ctx.state`, bit 4 (`&16`) = the node has
62
66
  * read fields, bit 5 (`&32`) = `collapse`, bit 6 (`&64`) = `unwrap`, bit 7
63
67
  * (`&128`) = `trailingTrivia`. Bit 0 distinguishes capture a host may not
@@ -104,12 +108,18 @@ export const OP_LIT_TRACK = 17
104
108
  export const OP_RX_TRACK = 18
105
109
  export const OP_NODE_TRACK = 19
106
110
  /**
107
- * `SCOPE k c` — a `parser({ trivia })` / `rules({ trivia })` scope.
111
+ * `SCOPE k c policy` — a policy-bearing `parser({ trivia })` scope.
108
112
  *
109
113
  * `k` is the scope's trivia COMBINATOR in the const pool. The driver installs it
110
114
  * on `ctx.trivia` for the duration and restores the outer one after, which is
111
115
  * how the runtime's own `advanceTrivia` fast scanner gets reached — the same
112
116
  * shared machinery the interpreter uses, not a second copy of it.
117
+ *
118
+ * `policy` bit 0 suppresses selected root capture for an opaque scope; bit 1
119
+ * refuses an unclassified local scope while selected root capture is active.
120
+ * Synthetic rule-entry and cross-rule restoration scopes have no policy and use
121
+ * the three-word `OP_SCOPE_PLAIN` row below. Keeping the opcodes distinct means
122
+ * a zero policy costs no extra program word without changing this opcode's ABI.
113
123
  */
114
124
  export const OP_SCOPE = 20
115
125
  /**
@@ -120,6 +130,11 @@ export const OP_EXPECT = 21
120
130
  /**
121
131
  * `SEQX f n c1 … cn` — a `transform()` whose child is a `sequence()`.
122
132
  *
133
+ * `f >= 0` indexes the reducer in `prog.fns`. `f < 0` is the descriptor
134
+ * `~childIndex`: the transform is the exact direct projection
135
+ * `([…, value, …]) => value`, so the row returns that already-parsed child and
136
+ * carries no reducer. Both forms have the same row width.
137
+ *
123
138
  * That pair is the dominant shape in every grammar here (json is nine of them),
124
139
  * and running it as two rows costs two switch dispatches and two JS call frames
125
140
  * per rule invocation where the emitted code pays neither. Fusing them into one
@@ -425,6 +440,34 @@ export const OP_LABEL = 39
425
440
  * which is the granularity the IDs are minted at.
426
441
  */
427
442
  export const OP_COV = 40
443
+ /**
444
+ * `SCOPE_PLAIN k c` — a synthetic zero-policy ambient-trivia scope.
445
+ *
446
+ * This is the same scanner/context swap as `OP_SCOPE`, with policy fixed to
447
+ * zero at assembly. `encodeRule()` uses it for a rule entry and `scopedRef()`
448
+ * for the lexical-trivia restoration around a reference reached from
449
+ * `noTrivia()`. Neither row represents an authored `parser()` scope, so neither
450
+ * can be opaque or require the unclassified-scope refusal.
451
+ *
452
+ * A separate opcode is load-bearing: treating a three-word synthetic row as the
453
+ * four-word `OP_SCOPE` makes the following row's opcode become the policy bits.
454
+ */
455
+ export const OP_SCOPE_PLAIN = 41
456
+ /**
457
+ * `LEX_BODY b fx suffixFx` — one compiler-selected childless lexical replacement
458
+ * body. `b` indexes `prog.lex`; `fx` is the exact base-recognizer failure set;
459
+ * `suffixFx` is optional()'s swallowed literal failure publication.
460
+ *
461
+ * There is deliberately no child operand and no mode bit. The complete
462
+ * CHARACTER and TOKEN candidates were compared before serialization; this row
463
+ * is only the winning TOKEN body. A program without a selected TOKEN body has
464
+ * no row or lexical pool.
465
+ */
466
+ /** `LEX_BODY body fx suffixFx lineFlags` — selected childless lexical body.
467
+ * lineFlags bit 0 publishes the regex range; bit 1 publishes a matched suffix. */
468
+ export const OP_LEX_BODY = 42
469
+ /** `LEX_PROGRAM p` — selected fixed composite lexical body, childless. */
470
+ export const OP_LEX_PROGRAM = 43
428
471
  /**
429
472
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
430
473
  *
@@ -456,4 +499,7 @@ export const OP_NAMES: Record<number, string> = {
456
499
  [OP_ADJ]: 'ADJ',
457
500
  [OP_GREEDY]: 'GREEDY', [OP_REJECT]: 'REJECT', [OP_ARMGATE]: 'ARMGATE',
458
501
  [OP_LIVE]: 'LIVE', [OP_ATTEMPT]: 'ATTEMPT', [OP_LABEL]: 'LABEL', [OP_COV]: 'COV',
502
+ [OP_SCOPE_PLAIN]: 'SCOPE_PLAIN',
503
+ [OP_LEX_BODY]: 'LEX_BODY',
504
+ [OP_LEX_PROGRAM]: 'LEX_PROGRAM',
459
505
  }
@@ -5,6 +5,8 @@ import { regex } from '../combinators/regex.ts'
5
5
  import { choice } from '../combinators/choice.ts'
6
6
  import { many } from '../combinators/repeat.ts'
7
7
  import type { EmittedFactory, PoolPlan } from './emit-assembly.ts'
8
+ import { buildLexBodyRecognizer, type LexBodyRecognizer } from './lex-body.ts'
9
+ import { buildLexProgramRunner, type LexProgramRunner } from './lex-program.ts'
8
10
 
9
11
  /**
10
12
  * One assembly the BUILD already compiled, so the run does not have to.
@@ -57,6 +59,11 @@ export type TableProgram = {
57
59
  * at `resolveTable`, exactly like the char-class ASCII lookups.
58
60
  */
59
61
  readonly dsp: readonly DispatchSpec[]
62
+ /** Selected childless lexical bodies. Losing compiler candidates never enter
63
+ * this pool. The first row is `[regexConst, suffixCodeUnit]`. */
64
+ readonly lex?: readonly LexBodySpec[]
65
+ /** Selected composite lexical programs. Losing candidates never enter it. */
66
+ readonly lexPrograms?: readonly LexProgramSpec[]
60
67
  /** Rule name → entry offset in `code`. */
61
68
  readonly rules: Readonly<Record<string, number>>
62
69
  /**
@@ -124,7 +131,8 @@ export type TableProgram = {
124
131
  */
125
132
  readonly runtimeOnly?: readonly string[]
126
133
  /**
127
- * Trivia specs, referenced by index from `OP_SCOPE` and from the rule entries.
134
+ * Trivia specs, referenced by index from `OP_SCOPE`, `OP_SCOPE_CAP` and
135
+ * `OP_SCOPE_PLAIN` rule/restoration entries.
128
136
  *
129
137
  * `classifiedTrivia()` is `trivia(oneOrMore(choice(label(name, arm)…)))` with
130
138
  * regex arms — entirely structural, so it lowers to DATA and is rebuilt at
@@ -290,6 +298,8 @@ export type CompactProgram = {
290
298
  readonly f: readonly unknown[]
291
299
  readonly l?: 0 | 1
292
300
  readonly p?: readonly DispatchSpec[]
301
+ readonly lx?: readonly LexBodySpec[]
302
+ readonly lp?: readonly LexProgramSpec[]
293
303
  readonly lb?: readonly string[]
294
304
  readonly rc?: 0 | 1
295
305
  readonly h?: 'ast' | 'cst'
@@ -308,6 +318,8 @@ export function expandCompact(p: TableProgram | CompactProgram): TableProgram {
308
318
  return ownTableProgram({
309
319
  code: p.c, k: p.k, cc: p.x, fx: p.e, disp: p.d, rules: p.r, fns: p.f,
310
320
  lines: p.l ?? 0, dsp: p.p ?? [],
321
+ ...(p.lx === undefined ? {} : { lex: p.lx }),
322
+ ...(p.lp === undefined ? {} : { lexPrograms: p.lp }),
311
323
  ...(p.lb === undefined ? {} : { labels: p.lb }),
312
324
  ...(p.rc === undefined ? {} : { classified: p.rc }),
313
325
  ...(p.h === undefined ? {} : { hostMode: p.h }),
@@ -321,6 +333,28 @@ export function expandCompact(p: TableProgram | CompactProgram): TableProgram {
321
333
  })
322
334
  }
323
335
 
336
+ /** Selected lexical body. `suffixCodeUnit === -1` is a direct regex terminal;
337
+ * 0..0xFFFF is the legacy regex + optional(one code unit) spelling. */
338
+ export type LexBodySpec = readonly [regexConst: number, suffixCodeUnit: number]
339
+
340
+ /** `kind=0`: fixed ordered four-terminal selected body. */
341
+ export type LexProgramSpec = readonly [
342
+ kind: 0, digest: number, commonClass: number, choiceFx: number,
343
+ arm0Matcher: number, arm0Fx: number,
344
+ arm1Matcher: number, arm1Fx: number,
345
+ arm2Matcher: number, arm2Fx: number,
346
+ arm3Matcher: number, arm3Fx: number,
347
+ trackLines: 0 | 1,
348
+ ] | readonly [
349
+ kind: 1, digest: number,
350
+ guard0Arm0: number, guard0Arm1: number, guard0Arm2: number, guard0Arm3: number,
351
+ guard0Fx: number,
352
+ guard1Matcher: number, guard1Fx: number,
353
+ terminalMatcher: number, terminalFx: number,
354
+ trackLines: 0 | 1,
355
+ ] | readonly [kind: 2, digest: number, ...fixedTreeWords: number[]]
356
+ | readonly [kind: 3, digest: number, scan: number]
357
+
324
358
  /** One `dispatch()`'s routing tables, in serialisable form. */
325
359
  export type DispatchSpec = {
326
360
  /** Exact keys, parallel to `keyArm`. */
@@ -352,6 +386,31 @@ export type ResolvedDispatchSpec = {
352
386
  readonly expected: readonly string[]
353
387
  }
354
388
 
389
+ /**
390
+ * Refuse malformed fixed arm references before closure or emitted linking.
391
+ *
392
+ * This is deliberately an assembly-time trust boundary. `execRules()` stays an
393
+ * independent semantic oracle and never pays artifact validation per parse.
394
+ */
395
+ export function validateDispatchSpec(
396
+ spec: ResolvedDispatchSpec | undefined,
397
+ arity: number,
398
+ fallbackRouted: number,
399
+ ): asserts spec is ResolvedDispatchSpec {
400
+ const invalid = (arms: Iterable<number>): boolean => {
401
+ for (const arm of arms) if (!Number.isInteger(arm) || arm < 0 || arm >= arity) return true
402
+ return false
403
+ }
404
+ if (spec === undefined || !Number.isInteger(arity) || arity < 0
405
+ || (fallbackRouted !== 0 && fallbackRouted !== 1)
406
+ || spec.routed.length !== arity
407
+ || spec.routed.some(value => value !== 0 && value !== 1)
408
+ || invalid(spec.byKey.values()) || invalid(spec.byFold.values())
409
+ || spec.match.some(matcher => invalid([matcher[3]]))) {
410
+ throw new TypeError('table: malformed dispatch')
411
+ }
412
+ }
413
+
355
414
  /** A char class expanded for execution: O(1) for ASCII, ranges above it. */
356
415
  export type ResolvedClass = {
357
416
  readonly ascii: Uint8Array
@@ -407,6 +466,8 @@ export type ResolvedTable = {
407
466
  readonly fx: readonly (readonly string[])[]
408
467
  readonly disp: readonly ResolvedDispatch[]
409
468
  readonly dsp: readonly ResolvedDispatchSpec[]
469
+ readonly lex: readonly LexBodyRecognizer[]
470
+ readonly lexPrograms: readonly LexProgramRunner[]
410
471
  /** Trivia combinators rebuilt from `triviaSpecs`, once per table. */
411
472
  readonly trivia: readonly Combinator<unknown>[]
412
473
  /**
@@ -584,6 +645,7 @@ export function resolveTable(prog: TableProgram): ResolvedTable {
584
645
  if (hit !== undefined) return hit
585
646
  const cc = prog.cc.map(resolveClass)
586
647
  const triviaBuilt = (prog.triviaSpecs ?? []).map(buildTrivia)
648
+ const lex = (prog.lex ?? []).map(spec => buildLexBodyRecognizer(spec, prog.k))
587
649
  const built: ResolvedTable = {
588
650
  prog,
589
651
  code: Int32Array.from(prog.code),
@@ -602,6 +664,9 @@ export function resolveTable(prog: TableProgram): ResolvedTable {
602
664
  routed: d.routed,
603
665
  expected: d.expected,
604
666
  })),
667
+ lex,
668
+ lexPrograms: (prog.lexPrograms ?? []).map(spec =>
669
+ buildLexProgramRunner(spec, lex, cc, prog.fx)),
605
670
  rules: prog.rules,
606
671
  }
607
672
  if (owner !== undefined) owner.resolved = built
@@ -666,7 +731,7 @@ export type CompactFolded = {
666
731
  * Named rather than derived so the refusal can say which one broke.
667
732
  */
668
733
  const SHARED_FIELDS = [
669
- 'k', 'cc', 'fx', 'disp', 'dsp', 'rules', 'labels', 'classified',
734
+ 'k', 'cc', 'fx', 'disp', 'dsp', 'lex', 'lexPrograms', 'rules', 'labels', 'classified',
670
735
  'scanSkip', 'scanSkipOf', 'scans', 'triviaSpecs', 'runtimeOnly',
671
736
  // `rec` is a property of the GRAMMAR BUILD, not of the trackLines/hostMode
672
737
  // axis a fold varies — every variant of one export is encoded with the same