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.
Files changed (126) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2964 -347
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2960 -343
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/expect.d.ts +1 -0
  8. package/dist/combinators/expect.d.ts.map +1 -1
  9. package/dist/combinators/expected.d.ts +8 -0
  10. package/dist/combinators/expected.d.ts.map +1 -0
  11. package/dist/combinators/first-set.d.ts +8 -0
  12. package/dist/combinators/first-set.d.ts.map +1 -1
  13. package/dist/combinators/grammar.d.ts.map +1 -1
  14. package/dist/combinators/keywords.d.ts +4 -0
  15. package/dist/combinators/keywords.d.ts.map +1 -1
  16. package/dist/combinators/literal.d.ts.map +1 -1
  17. package/dist/combinators/not.d.ts.map +1 -1
  18. package/dist/combinators/peek.d.ts.map +1 -1
  19. package/dist/combinators/regex.d.ts.map +1 -1
  20. package/dist/combinators/repeat.d.ts.map +1 -1
  21. package/dist/combinators/sequence.d.ts.map +1 -1
  22. package/dist/combinators/trivia-skip.d.ts +16 -3
  23. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  24. package/dist/compiler/direct-projection.d.ts +12 -0
  25. package/dist/compiler/direct-projection.d.ts.map +1 -0
  26. package/dist/compiler/token-alphabet.d.ts +845 -5
  27. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  28. package/dist/compiler/token-capability.d.ts +3 -0
  29. package/dist/compiler/token-capability.d.ts.map +1 -0
  30. package/dist/compiler/token-capability.js +3192 -0
  31. package/dist/compiler/token-capability.js.map +6 -0
  32. package/dist/cst/trivia-charscan.d.ts +3 -0
  33. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  34. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  35. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  36. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  37. package/dist/index.js +2973 -343
  38. package/dist/index.js.map +3 -3
  39. package/dist/language-service/index.js +5 -1
  40. package/dist/language-service/index.js.map +1 -1
  41. package/dist/plugin/index.js +2982 -352
  42. package/dist/plugin/index.js.map +3 -3
  43. package/dist/spec/index.js +325 -47
  44. package/dist/spec/index.js.map +3 -3
  45. package/dist/table/assemble.d.ts.map +1 -1
  46. package/dist/table/child-slots.d.ts.map +1 -1
  47. package/dist/table/compile-rule-map.d.ts +3 -1
  48. package/dist/table/compile-rule-map.d.ts.map +1 -1
  49. package/dist/table/emit-assembly.d.ts +12 -13
  50. package/dist/table/emit-assembly.d.ts.map +1 -1
  51. package/dist/table/emit.d.ts.map +1 -1
  52. package/dist/table/encode.d.ts.map +1 -1
  53. package/dist/table/exec-baseline.d.ts.map +1 -1
  54. package/dist/table/exec.d.ts.map +1 -1
  55. package/dist/table/index.js +2937 -311
  56. package/dist/table/index.js.map +3 -3
  57. package/dist/table/lex-body.d.ts +9 -0
  58. package/dist/table/lex-body.d.ts.map +1 -0
  59. package/dist/table/lex-program.d.ts +25 -0
  60. package/dist/table/lex-program.d.ts.map +1 -0
  61. package/dist/table/ops.d.ts +47 -11
  62. package/dist/table/ops.d.ts.map +1 -1
  63. package/dist/table/program.d.ts +50 -2
  64. package/dist/table/program.d.ts.map +1 -1
  65. package/dist/table/scalar-terminal.d.ts +13 -0
  66. package/dist/table/scalar-terminal.d.ts.map +1 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/types.d.ts +2 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/package.json +11 -11
  72. package/src/combinators/choice.ts +10 -1
  73. package/src/combinators/expect.ts +1 -0
  74. package/src/combinators/expected.ts +17 -0
  75. package/src/combinators/first-set.ts +58 -5
  76. package/src/combinators/grammar.ts +8 -3
  77. package/src/combinators/keywords.ts +22 -6
  78. package/src/combinators/literal.ts +2 -1
  79. package/src/combinators/not.ts +5 -1
  80. package/src/combinators/peek.ts +5 -1
  81. package/src/combinators/regex.ts +5 -3
  82. package/src/combinators/repeat.ts +4 -10
  83. package/src/combinators/sequence.ts +17 -14
  84. package/src/combinators/trivia-skip.ts +46 -13
  85. package/src/compiler/direct-projection.ts +43 -0
  86. package/src/compiler/token-alphabet.ts +4316 -12
  87. package/src/compiler/token-capability.ts +6 -0
  88. package/src/cst/trivia-charscan.ts +34 -0
  89. package/src/cst/trivia-css-scanner.ts +142 -0
  90. package/src/cst/trivia-kinds.ts +3 -1
  91. package/src/plugin/index.ts +4 -4
  92. package/src/table/assemble.ts +1253 -240
  93. package/src/table/child-slots.ts +2 -1
  94. package/src/table/compile-rule-map.ts +21 -3
  95. package/src/table/emit-assembly.ts +335 -87
  96. package/src/table/emit.ts +16 -2
  97. package/src/table/encode.ts +248 -60
  98. package/src/table/exec-baseline.ts +3 -7
  99. package/src/table/exec.ts +134 -25
  100. package/src/table/lex-body.ts +30 -0
  101. package/src/table/lex-program.ts +431 -0
  102. package/src/table/ops.ts +49 -11
  103. package/src/table/program.ts +46 -2
  104. package/src/table/scalar-terminal.ts +51 -0
  105. package/src/table/site-labels.ts +11 -1
  106. package/src/table/stamp.ts +1 -1
  107. package/src/types.ts +1 -1
  108. package/src/version.ts +1 -1
  109. package/dist/analysis/diagnostics.cjs +0 -13824
  110. package/dist/analysis/diagnostics.cjs.map +0 -6
  111. package/dist/cli/index.cjs +0 -14925
  112. package/dist/cli/index.cjs.map +0 -6
  113. package/dist/index.cjs +0 -16391
  114. package/dist/index.cjs.map +0 -6
  115. package/dist/language-service/index.cjs +0 -1304
  116. package/dist/language-service/index.cjs.map +0 -6
  117. package/dist/oracle/index.cjs +0 -254
  118. package/dist/oracle/index.cjs.map +0 -6
  119. package/dist/plugin/index.cjs +0 -16097
  120. package/dist/plugin/index.cjs.map +0 -6
  121. package/dist/run/index.cjs +0 -577
  122. package/dist/run/index.cjs.map +0 -6
  123. package/dist/spec/index.cjs +0 -6236
  124. package/dist/spec/index.cjs.map +0 -6
  125. package/dist/table/index.cjs +0 -11463
  126. package/dist/table/index.cjs.map +0 -6
@@ -0,0 +1,431 @@
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,
154
+ acc: string[] | undefined, best: number,
155
+ ) => number
156
+
157
+ function appendExpected(acc: string[] | undefined, values: readonly string[] | undefined): string[] | undefined {
158
+ if (values === undefined || values.length === 0) return acc
159
+ if (acc === undefined) return values.slice()
160
+ acc.push(...values)
161
+ return acc
162
+ }
163
+
164
+ function choiceNode(
165
+ arms: readonly BuiltLexNode[],
166
+ classes: readonly (ResolvedClass | undefined)[],
167
+ armExpected: readonly (readonly string[])[],
168
+ choiceExpected: readonly string[],
169
+ ): BuiltLexNode {
170
+ let matchTail: PureLexRunner = () => -1
171
+ for (let i = arms.length - 1; i >= 0; i--) {
172
+ const arm = arms[i]!, next = matchTail
173
+ matchTail = (input, pos) => {
174
+ const end = arm.match(input, pos)
175
+ return end < 0 ? next(input, pos) : end
176
+ }
177
+ }
178
+ let tail: ChoiceTail = (_input, pos, ctx, _lead, acc) => {
179
+ ctx._fe = pos
180
+ ctx._fx = acc ?? choiceExpected as string[]
181
+ return -1
182
+ }
183
+ for (let i = arms.length - 1; i >= 0; i--) {
184
+ const arm = arms[i]!, cls = classes[i], staticExpected = armExpected[i]!, next = tail
185
+ tail = (input, pos, ctx, lead, acc, best) => {
186
+ if (cls !== undefined && !classHas(cls, lead)) {
187
+ return next(
188
+ input, pos, ctx, lead,
189
+ best === pos ? appendExpected(acc, staticExpected) : acc,
190
+ best,
191
+ )
192
+ }
193
+ ctx._fc = false
194
+ const end = arm.run(input, pos, ctx)
195
+ if (end >= 0) return end
196
+ const at = ctx._fe ?? pos
197
+ if (at > best) { best = at; acc = undefined }
198
+ const nextAcc = at === best ? appendExpected(acc, ctx._fx) : acc
199
+ if (committed(ctx)) {
200
+ if (nextAcc !== undefined) ctx._fx = nextAcc
201
+ return -1
202
+ }
203
+ return next(input, pos, ctx, lead, nextAcc, best)
204
+ }
205
+ }
206
+ return {
207
+ match: matchTail,
208
+ run: (input, pos, ctx) => tail(
209
+ input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined, pos,
210
+ ),
211
+ }
212
+ }
213
+
214
+ function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
215
+ return {
216
+ match: (input, pos) => body.match(input, pos) < 0 ? pos : -1,
217
+ run: (input, pos, ctx) => {
218
+ if (body.match(input, pos) < 0) return pos
219
+ ctx._fe = pos
220
+ ctx._fx = failure as string[]
221
+ return -1
222
+ },
223
+ }
224
+ }
225
+
226
+ function optionalNode(body: BuiltLexNode): BuiltLexNode {
227
+ return {
228
+ match: (input, pos) => {
229
+ const end = body.match(input, pos)
230
+ return end < 0 ? pos : end
231
+ },
232
+ run: (input, pos, ctx) => {
233
+ ctx._fc = false
234
+ const end = body.run(input, pos, ctx)
235
+ return end < 0 && !committed(ctx) ? pos : end
236
+ },
237
+ }
238
+ }
239
+
240
+ function fixedTreeRunner(
241
+ words: readonly number[],
242
+ matchers: readonly LexBodyRecognizer[],
243
+ classes: readonly ResolvedClass[],
244
+ expected: readonly (readonly string[])[],
245
+ ): LexProgramRunner {
246
+ const stack: BuiltLexNode[] = []
247
+ const pop = (): BuiltLexNode => {
248
+ const value = stack.pop()
249
+ if (value === undefined) throw new TypeError('table lexical tree has an incomplete fixed body')
250
+ return value
251
+ }
252
+ for (let at = 0; at < words.length;) {
253
+ const op = words[at++]!
254
+ if (op === LEX_NODE_TERMINAL) {
255
+ const matcher = matchers[words[at++]!], failure = expected[words[at++]!]
256
+ const tracks = words[at++]
257
+ if (matcher === undefined || failure === undefined || (tracks !== 0 && tracks !== 1)) {
258
+ throw new TypeError('table lexical tree terminal references an invalid pool entry')
259
+ }
260
+ stack.push(terminalNode(matcher, failure, tracks === 1))
261
+ continue
262
+ }
263
+ if (op >= LEX_NODE_SEQUENCE2 && op <= LEX_NODE_SEQUENCE5) {
264
+ const count = op - LEX_NODE_SEQUENCE2 + 2
265
+ const nodes = Array.from({ length: count }, pop).reverse()
266
+ if (count === 2) stack.push(sequence2(nodes[0]!, nodes[1]!))
267
+ else if (count === 3) stack.push(sequence3(nodes[0]!, nodes[1]!, nodes[2]!))
268
+ else if (count === 4) stack.push(sequence4(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!))
269
+ else stack.push(sequence5(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!, nodes[4]!))
270
+ continue
271
+ }
272
+ if (op >= LEX_NODE_CHOICE2 && op <= LEX_NODE_CHOICE8) {
273
+ const count = op === LEX_NODE_CHOICE2 ? 2 : op === LEX_NODE_CHOICE4 ? 4 : 8
274
+ const choiceFx = expected[words[at++]!]
275
+ if (choiceFx === undefined) throw new TypeError('table lexical tree choice has an invalid expected set')
276
+ const armClasses: Array<ResolvedClass | undefined> = []
277
+ const armFx: Array<readonly string[]> = []
278
+ for (let i = 0; i < count; i++) {
279
+ const cls = words[at++]!, fx = expected[words[at++]!]
280
+ if (cls < -1 || (cls >= 0 && classes[cls] === undefined) || fx === undefined) {
281
+ throw new TypeError('table lexical tree choice references an invalid pool entry')
282
+ }
283
+ armClasses.push(cls < 0 ? undefined : classes[cls])
284
+ armFx.push(fx)
285
+ }
286
+ const arms = Array.from({ length: count }, pop).reverse()
287
+ stack.push(choiceNode(arms, armClasses, armFx, choiceFx))
288
+ continue
289
+ }
290
+ if (op === LEX_NODE_NOT) {
291
+ const failure = expected[words[at++]!]
292
+ if (failure === undefined) throw new TypeError('table lexical tree assertion has an invalid expected set')
293
+ stack.push(notNode(pop(), failure))
294
+ continue
295
+ }
296
+ if (op === LEX_NODE_OPTIONAL) { stack.push(optionalNode(pop())); continue }
297
+ throw new TypeError('table lexical tree has an unknown fixed body')
298
+ }
299
+ if (stack.length !== 1) throw new TypeError('table lexical tree does not resolve to one root body')
300
+ return stack[0]!.run
301
+ }
302
+
303
+ /** Decode one fixed selected-body tuple. There is no lexical opcode loop on
304
+ * the parse path: each supported tuple becomes one scalar closure here. */
305
+ export function buildLexProgramRunner(
306
+ spec: LexProgramSpec,
307
+ matchers: readonly LexBodyRecognizer[],
308
+ classes: readonly ResolvedClass[],
309
+ expected: readonly (readonly string[])[],
310
+ ): LexProgramRunner {
311
+ if ((spec[0] !== 0 && spec[0] !== 1 && spec[0] !== 2 && spec[0] !== 3)
312
+ || (spec[0] === 0 && spec.length !== 13)
313
+ || (spec[0] === 1 && spec.length !== 12)
314
+ || (spec[0] === 2 && spec.length < 6)
315
+ || (spec[0] === 3 && spec.length !== 3)) {
316
+ throw new TypeError('table lexical program has an invalid fixed body width')
317
+ }
318
+ if (spec[1] !== lexProgramDigest(spec.slice(2))) {
319
+ throw new TypeError('table lexical program semantic digest is inconsistent')
320
+ }
321
+ if (spec[0] === 3) {
322
+ if (!Number.isInteger(spec[2]) || spec[2] < 0) {
323
+ throw new TypeError('table balanced lexical program has an invalid scan reference')
324
+ }
325
+ const run: LexProgramRunner = (_input, _pos, ctx, scan) => {
326
+ if (scan === undefined) throw new TypeError('table balanced lexical program lost its scan body')
327
+ const result = scan.parse(_input, _pos, ctx)
328
+ if (!result.ok) {
329
+ ctx._fe = result.span.start
330
+ ctx._fx = (result.expected ?? []) as string[]
331
+ return -1
332
+ }
333
+ return result.span.end
334
+ }
335
+ return Object.assign(run, { scan: spec[2] })
336
+ }
337
+ if (spec[0] === 2) return fixedTreeRunner(spec.slice(2), matchers, classes, expected)
338
+ if (spec[0] === 1) {
339
+ const g00 = matchers[spec[2]], g01 = matchers[spec[3]]
340
+ const g02 = matchers[spec[4]], g03 = matchers[spec[5]]
341
+ const guard0Fx = expected[spec[6]]
342
+ const g1 = matchers[spec[7]], guard1Fx = expected[spec[8]]
343
+ const terminal = matchers[spec[9]], terminalFx = expected[spec[10]]
344
+ const tracks = spec[11]
345
+ if (g00 === undefined || g01 === undefined || g02 === undefined || g03 === undefined
346
+ || guard0Fx === undefined || g1 === undefined || guard1Fx === undefined
347
+ || terminal === undefined || terminalFx === undefined || (tracks !== 0 && tracks !== 1)) {
348
+ throw new TypeError('table lexical program references an invalid pool entry')
349
+ }
350
+ return (input, pos, ctx) => {
351
+ ctx._fc = false
352
+ if (g00(input, pos) >= 0 || g01(input, pos) >= 0
353
+ || g02(input, pos) >= 0 || g03(input, pos) >= 0) {
354
+ ctx._fe = pos
355
+ ctx._fx = guard0Fx as string[]
356
+ return -1
357
+ }
358
+ ctx._fc = false
359
+ if (g1(input, pos) >= 0) {
360
+ ctx._fe = pos
361
+ ctx._fx = guard1Fx as string[]
362
+ return -1
363
+ }
364
+ ctx._fc = false
365
+ const result = terminal(input, pos)
366
+ if (result < 0) {
367
+ ctx._fe = pos
368
+ ctx._fx = terminalFx as string[]
369
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
370
+ return -1
371
+ }
372
+ const end = result / 2
373
+ if (tracks === 1) trackLexLines(ctx, input, end)
374
+ return end
375
+ }
376
+ }
377
+ const cls = classes[spec[2]]
378
+ const choiceFx = expected[spec[3]]
379
+ const m0 = matchers[spec[4]], x0 = expected[spec[5]]
380
+ const m1 = matchers[spec[6]], x1 = expected[spec[7]]
381
+ const m2 = matchers[spec[8]], x2 = expected[spec[9]]
382
+ const m3 = matchers[spec[10]], x3 = expected[spec[11]]
383
+ const tracks = spec[12]
384
+ if (cls === undefined || choiceFx === undefined
385
+ || m0 === undefined || x0 === undefined || m1 === undefined || x1 === undefined
386
+ || m2 === undefined || x2 === undefined || m3 === undefined || x3 === undefined
387
+ || (tracks !== 0 && tracks !== 1)) {
388
+ throw new TypeError('table lexical program references an invalid pool entry')
389
+ }
390
+
391
+ return (input, pos, ctx) => {
392
+ const lead = pos < input.length ? input.codePointAt(pos)! : -1
393
+ if (!classHas(cls, lead)) {
394
+ ctx._fe = pos
395
+ ctx._fx = choiceFx.slice()
396
+ return -1
397
+ }
398
+ let acc: string[] | undefined
399
+ const failed = (fx: readonly string[]): void => {
400
+ ctx._fe = pos
401
+ ctx._fx = fx as string[]
402
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
403
+ if (acc === undefined) acc = fx.slice()
404
+ else acc.push(...fx)
405
+ }
406
+ ctx._fc = false
407
+ const success = (result: number): number => {
408
+ const end = result / 2
409
+ if (tracks === 1) trackLexLines(ctx, input, end)
410
+ return end
411
+ }
412
+ let result = m0(input, pos)
413
+ if (result >= 0) return success(result)
414
+ failed(x0)
415
+ ctx._fc = false
416
+ result = m1(input, pos)
417
+ if (result >= 0) return success(result)
418
+ failed(x1)
419
+ ctx._fc = false
420
+ result = m2(input, pos)
421
+ if (result >= 0) return success(result)
422
+ failed(x2)
423
+ ctx._fc = false
424
+ result = m3(input, pos)
425
+ if (result >= 0) return success(result)
426
+ failed(x3)
427
+ ctx._fe = pos
428
+ ctx._fx = acc ?? choiceFx as string[]
429
+ return -1
430
+ }
431
+ }
package/src/table/ops.ts CHANGED
@@ -30,15 +30,18 @@ export const OP_SEQV = 4
30
30
  * `CHOICE d n fx c1 … cn` — `d` indexes a dispatch table (or −1 for ordered
31
31
  * try); `fx` indexes the choice's OWN expected set.
32
32
  *
33
- * Both shipped engines report the UNION of the arms' expectations when a choice
34
- * fails. The driver reported whatever the last attempted arm left behind — or,
35
- * on a dispatch miss with no arm claiming the lead character, NOTHING, so a user
36
- * got an error naming nothing at all.
33
+ * Ordered failure keeps the expected sets at the deepest arm offset, merging
34
+ * exact ties in source order. Exclusive dispatch propagates its sole selected
35
+ * arm. A pure dispatch miss uses `fx`, the static union of every arm opener, so
36
+ * the result never names nothing merely because no arm claimed the lead.
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
@@ -110,8 +114,12 @@ export const OP_NODE_TRACK = 19
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.
113
- * Synthetic rule-entry/restoration scopes use `OP_SCOPE_PLAIN` so a zero
114
- * policy stays a three-word row without changing this opcode's ABI.
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.
115
123
  */
116
124
  export const OP_SCOPE = 20
117
125
  /**
@@ -122,6 +130,11 @@ export const OP_EXPECT = 21
122
130
  /**
123
131
  * `SEQX f n c1 … cn` — a `transform()` whose child is a `sequence()`.
124
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
+ *
125
138
  * That pair is the dominant shape in every grammar here (json is nine of them),
126
139
  * and running it as two rows costs two switch dispatches and two JS call frames
127
140
  * per rule invocation where the emitted code pays neither. Fusing them into one
@@ -428,10 +441,33 @@ export const OP_LABEL = 39
428
441
  */
429
442
  export const OP_COV = 40
430
443
  /**
431
- * `SCOPE_PLAIN k c` — a synthetic ambient-trivia scope with policy fixed to zero.
432
- * A distinct opcode prevents the next row's opcode from being read as policy.
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.
433
454
  */
434
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
435
471
  /**
436
472
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
437
473
  *
@@ -464,4 +500,6 @@ export const OP_NAMES: Record<number, string> = {
464
500
  [OP_GREEDY]: 'GREEDY', [OP_REJECT]: 'REJECT', [OP_ARMGATE]: 'ARMGATE',
465
501
  [OP_LIVE]: 'LIVE', [OP_ATTEMPT]: 'ATTEMPT', [OP_LABEL]: 'LABEL', [OP_COV]: 'COV',
466
502
  [OP_SCOPE_PLAIN]: 'SCOPE_PLAIN',
503
+ [OP_LEX_BODY]: 'LEX_BODY',
504
+ [OP_LEX_PROGRAM]: 'LEX_PROGRAM',
467
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
  /**
@@ -291,6 +298,8 @@ export type CompactProgram = {
291
298
  readonly f: readonly unknown[]
292
299
  readonly l?: 0 | 1
293
300
  readonly p?: readonly DispatchSpec[]
301
+ readonly lx?: readonly LexBodySpec[]
302
+ readonly lp?: readonly LexProgramSpec[]
294
303
  readonly lb?: readonly string[]
295
304
  readonly rc?: 0 | 1
296
305
  readonly h?: 'ast' | 'cst'
@@ -309,6 +318,8 @@ export function expandCompact(p: TableProgram | CompactProgram): TableProgram {
309
318
  return ownTableProgram({
310
319
  code: p.c, k: p.k, cc: p.x, fx: p.e, disp: p.d, rules: p.r, fns: p.f,
311
320
  lines: p.l ?? 0, dsp: p.p ?? [],
321
+ ...(p.lx === undefined ? {} : { lex: p.lx }),
322
+ ...(p.lp === undefined ? {} : { lexPrograms: p.lp }),
312
323
  ...(p.lb === undefined ? {} : { labels: p.lb }),
313
324
  ...(p.rc === undefined ? {} : { classified: p.rc }),
314
325
  ...(p.h === undefined ? {} : { hostMode: p.h }),
@@ -322,6 +333,28 @@ export function expandCompact(p: TableProgram | CompactProgram): TableProgram {
322
333
  })
323
334
  }
324
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
+
325
358
  /** One `dispatch()`'s routing tables, in serialisable form. */
326
359
  export type DispatchSpec = {
327
360
  /** Exact keys, parallel to `keyArm`. */
@@ -353,7 +386,12 @@ export type ResolvedDispatchSpec = {
353
386
  readonly expected: readonly string[]
354
387
  }
355
388
 
356
- /** Refuse malformed dispatch arm references before closure/emitted linking. */
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
+ */
357
395
  export function validateDispatchSpec(
358
396
  spec: ResolvedDispatchSpec | undefined,
359
397
  arity: number,
@@ -428,6 +466,8 @@ export type ResolvedTable = {
428
466
  readonly fx: readonly (readonly string[])[]
429
467
  readonly disp: readonly ResolvedDispatch[]
430
468
  readonly dsp: readonly ResolvedDispatchSpec[]
469
+ readonly lex: readonly LexBodyRecognizer[]
470
+ readonly lexPrograms: readonly LexProgramRunner[]
431
471
  /** Trivia combinators rebuilt from `triviaSpecs`, once per table. */
432
472
  readonly trivia: readonly Combinator<unknown>[]
433
473
  /**
@@ -605,6 +645,7 @@ export function resolveTable(prog: TableProgram): ResolvedTable {
605
645
  if (hit !== undefined) return hit
606
646
  const cc = prog.cc.map(resolveClass)
607
647
  const triviaBuilt = (prog.triviaSpecs ?? []).map(buildTrivia)
648
+ const lex = (prog.lex ?? []).map(spec => buildLexBodyRecognizer(spec, prog.k))
608
649
  const built: ResolvedTable = {
609
650
  prog,
610
651
  code: Int32Array.from(prog.code),
@@ -623,6 +664,9 @@ export function resolveTable(prog: TableProgram): ResolvedTable {
623
664
  routed: d.routed,
624
665
  expected: d.expected,
625
666
  })),
667
+ lex,
668
+ lexPrograms: (prog.lexPrograms ?? []).map(spec =>
669
+ buildLexProgramRunner(spec, lex, cc, prog.fx)),
626
670
  rules: prog.rules,
627
671
  }
628
672
  if (owner !== undefined) owner.resolved = built
@@ -687,7 +731,7 @@ export type CompactFolded = {
687
731
  * Named rather than derived so the refusal can say which one broke.
688
732
  */
689
733
  const SHARED_FIELDS = [
690
- 'k', 'cc', 'fx', 'disp', 'dsp', 'rules', 'labels', 'classified',
734
+ 'k', 'cc', 'fx', 'disp', 'dsp', 'lex', 'lexPrograms', 'rules', 'labels', 'classified',
691
735
  'scanSkip', 'scanSkipOf', 'scans', 'triviaSpecs', 'runtimeOnly',
692
736
  // `rec` is a property of the GRAMMAR BUILD, not of the trackLines/hostMode
693
737
  // axis a fold varies — every variant of one export is encoded with the same