parseman 0.57.0 → 0.58.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 (64) hide show
  1. package/dist/analysis/diagnostics.js +1799 -962
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +1928 -1091
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/compiler/token-alphabet.d.ts +11 -1
  7. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  8. package/dist/compiler/token-capability.js +69 -16
  9. package/dist/compiler/token-capability.js.map +2 -2
  10. package/dist/cst/capture-buffer.d.ts +6 -0
  11. package/dist/cst/capture-buffer.d.ts.map +1 -1
  12. package/dist/index.js +1016 -177
  13. package/dist/index.js.map +3 -3
  14. package/dist/plugin/index.d.ts +25 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +1209 -236
  17. package/dist/plugin/index.js.map +3 -3
  18. package/dist/table/assemble.d.ts.map +1 -1
  19. package/dist/table/compile-rule-map.d.ts.map +1 -1
  20. package/dist/table/emit-assembly.d.ts +1 -1
  21. package/dist/table/emit-assembly.d.ts.map +1 -1
  22. package/dist/table/emit.d.ts.map +1 -1
  23. package/dist/table/encode.d.ts.map +1 -1
  24. package/dist/table/exec.d.ts.map +1 -1
  25. package/dist/table/factor-choice.d.ts +42 -0
  26. package/dist/table/factor-choice.d.ts.map +1 -0
  27. package/dist/table/fail-clean.d.ts +3 -0
  28. package/dist/table/fail-clean.d.ts.map +1 -0
  29. package/dist/table/hold-gap.d.ts +8 -0
  30. package/dist/table/hold-gap.d.ts.map +1 -0
  31. package/dist/table/index.js +1369 -259
  32. package/dist/table/index.js.map +3 -3
  33. package/dist/table/lex-program.d.ts +7 -3
  34. package/dist/table/lex-program.d.ts.map +1 -1
  35. package/dist/table/ops.d.ts +8 -0
  36. package/dist/table/ops.d.ts.map +1 -1
  37. package/dist/table/run-support.d.ts +19 -0
  38. package/dist/table/run-support.d.ts.map +1 -1
  39. package/dist/table/scalar-terminal.d.ts +2 -0
  40. package/dist/table/scalar-terminal.d.ts.map +1 -1
  41. package/dist/table/site-labels.d.ts +66 -0
  42. package/dist/table/site-labels.d.ts.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/dispatch.ts +12 -3
  46. package/src/compiler/token-alphabet.ts +57 -17
  47. package/src/cst/capture-buffer.ts +25 -0
  48. package/src/plugin/index.ts +230 -35
  49. package/src/table/assemble.ts +252 -62
  50. package/src/table/child-slots.ts +2 -2
  51. package/src/table/compile-rule-map.ts +14 -5
  52. package/src/table/emit-assembly.ts +112 -23
  53. package/src/table/emit.ts +3 -1
  54. package/src/table/encode.ts +80 -14
  55. package/src/table/exec.ts +27 -8
  56. package/src/table/factor-choice.ts +173 -0
  57. package/src/table/fail-clean.ts +130 -0
  58. package/src/table/hold-gap.ts +68 -0
  59. package/src/table/lex-program.ts +197 -43
  60. package/src/table/ops.ts +9 -1
  61. package/src/table/run-support.ts +35 -0
  62. package/src/table/scalar-terminal.ts +8 -0
  63. package/src/table/site-labels.ts +135 -4
  64. package/src/version.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { Combinator, ParseContext } from '../types.ts'
2
2
  import { failAt } from '../combinators/probe.ts'
3
+ import { lead } from './run-support.ts'
3
4
  import type { LexBodyRecognizer } from './lex-body.ts'
4
5
  import type { LexProgramSpec, ResolvedClass } from './program.ts'
5
6
 
@@ -16,11 +17,15 @@ export const LEX_NODE_SEQUENCE2 = 1
16
17
  export const LEX_NODE_SEQUENCE3 = 2
17
18
  export const LEX_NODE_SEQUENCE4 = 3
18
19
  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
20
+ /**
21
+ * `[op, armCount, exclusive, choiceFx, (armClass, armFx) × armCount]`: any arity,
22
+ * no padding. `exclusive` is the character choice's one-arm dispatch decision.
23
+ */
24
+ export const LEX_NODE_CHOICE = 5
22
25
  export const LEX_NODE_NOT = 8
23
26
  export const LEX_NODE_OPTIONAL = 9
27
+ /** `[op, min, max]` after the item: a separator-less repeat, `max` −1 unbounded. */
28
+ export const LEX_NODE_REPEAT = 10
24
29
 
25
30
  export function lexProgramDigest(words: readonly number[]): number {
26
31
  let hash = 0x811C9DC5
@@ -60,73 +65,88 @@ function terminalNode(
60
65
  failure: readonly string[],
61
66
  tracksLines: boolean,
62
67
  ): BuiltLexNode {
63
- const match: PureLexRunner = (input, pos) => {
64
- const result = matcher(input, pos)
65
- return result < 0 ? -1 : result / 2
66
- }
67
68
  return {
68
- match,
69
+ match: (input, pos) => {
70
+ const result = matcher(input, pos)
71
+ return result < 0 ? -1 : result / 2
72
+ },
69
73
  run: (input, pos, ctx) => {
70
- const end = match(input, pos)
71
- if (end < 0) {
74
+ const result = matcher(input, pos)
75
+ if (result < 0) {
72
76
  ctx._fe = pos
73
77
  ctx._fx = failure as string[]
74
78
  if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
75
79
  return -1
76
80
  }
81
+ const end = result / 2
77
82
  if (tracksLines) trackLexLines(ctx, input, end)
78
83
  return end
79
84
  },
80
85
  }
81
86
  }
82
87
 
88
+ // Each sequence captures its parts' runners as scalars: a part is reached by a
89
+ // direct call, not a property read, and no arity nests another sequence.
83
90
  function sequence2(a: BuiltLexNode, b: BuiltLexNode): BuiltLexNode {
91
+ const am = a.match, bm = b.match, ar = a.run, br = b.run
84
92
  return {
85
93
  match: (input, pos) => {
86
- const e0 = a.match(input, pos)
87
- return e0 < 0 ? -1 : b.match(input, e0)
94
+ const e0 = am(input, pos)
95
+ return e0 < 0 ? -1 : bm(input, e0)
88
96
  },
89
97
  run: (input, pos, ctx) => {
90
- const e0 = a.run(input, pos, ctx)
98
+ const e0 = ar(input, pos, ctx)
91
99
  if (e0 < 0) return -1
92
100
  ctx._fc = false
93
- return b.run(input, e0, ctx)
101
+ return br(input, e0, ctx)
94
102
  },
95
103
  }
96
104
  }
97
105
 
98
106
  function sequence3(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode): BuiltLexNode {
107
+ const am = a.match, bm = b.match, cm = c.match, ar = a.run, br = b.run, cr = c.run
99
108
  return {
100
109
  match: (input, pos) => {
101
- const e0 = a.match(input, pos)
110
+ const e0 = am(input, pos)
102
111
  if (e0 < 0) return -1
103
- const e1 = b.match(input, e0)
104
- return e1 < 0 ? -1 : c.match(input, e1)
112
+ const e1 = bm(input, e0)
113
+ return e1 < 0 ? -1 : cm(input, e1)
105
114
  },
106
115
  run: (input, pos, ctx) => {
107
- const e0 = a.run(input, pos, ctx)
116
+ const e0 = ar(input, pos, ctx)
108
117
  if (e0 < 0) return -1
109
118
  ctx._fc = false
110
- const e1 = b.run(input, e0, ctx)
119
+ const e1 = br(input, e0, ctx)
111
120
  if (e1 < 0) return -1
112
121
  ctx._fc = false
113
- return c.run(input, e1, ctx)
122
+ return cr(input, e1, ctx)
114
123
  },
115
124
  }
116
125
  }
117
126
 
118
127
  function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode): BuiltLexNode {
119
- const abc = sequence3(a, b, c)
128
+ const am = a.match, bm = b.match, cm = c.match, dm = d.match
129
+ const ar = a.run, br = b.run, cr = c.run, dr = d.run
120
130
  return {
121
131
  match: (input, pos) => {
122
- const end = abc.match(input, pos)
123
- return end < 0 ? -1 : d.match(input, end)
132
+ const e0 = am(input, pos)
133
+ if (e0 < 0) return -1
134
+ const e1 = bm(input, e0)
135
+ if (e1 < 0) return -1
136
+ const e2 = cm(input, e1)
137
+ return e2 < 0 ? -1 : dm(input, e2)
124
138
  },
125
139
  run: (input, pos, ctx) => {
126
- const end = abc.run(input, pos, ctx)
127
- if (end < 0) return -1
140
+ const e0 = ar(input, pos, ctx)
141
+ if (e0 < 0) return -1
142
+ ctx._fc = false
143
+ const e1 = br(input, e0, ctx)
144
+ if (e1 < 0) return -1
145
+ ctx._fc = false
146
+ const e2 = cr(input, e1, ctx)
147
+ if (e2 < 0) return -1
128
148
  ctx._fc = false
129
- return d.run(input, end, ctx)
149
+ return dr(input, e2, ctx)
130
150
  },
131
151
  }
132
152
  }
@@ -134,17 +154,33 @@ function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLe
134
154
  function sequence5(
135
155
  a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode, e: BuiltLexNode,
136
156
  ): BuiltLexNode {
137
- const abcd = sequence4(a, b, c, d)
157
+ const am = a.match, bm = b.match, cm = c.match, dm = d.match, em = e.match
158
+ const ar = a.run, br = b.run, cr = c.run, dr = d.run, er = e.run
138
159
  return {
139
160
  match: (input, pos) => {
140
- const end = abcd.match(input, pos)
141
- return end < 0 ? -1 : e.match(input, end)
161
+ const e0 = am(input, pos)
162
+ if (e0 < 0) return -1
163
+ const e1 = bm(input, e0)
164
+ if (e1 < 0) return -1
165
+ const e2 = cm(input, e1)
166
+ if (e2 < 0) return -1
167
+ const e3 = dm(input, e2)
168
+ return e3 < 0 ? -1 : em(input, e3)
142
169
  },
143
170
  run: (input, pos, ctx) => {
144
- const end = abcd.run(input, pos, ctx)
145
- if (end < 0) return -1
171
+ const e0 = ar(input, pos, ctx)
172
+ if (e0 < 0) return -1
146
173
  ctx._fc = false
147
- return e.run(input, end, ctx)
174
+ const e1 = br(input, e0, ctx)
175
+ if (e1 < 0) return -1
176
+ ctx._fc = false
177
+ const e2 = cr(input, e1, ctx)
178
+ if (e2 < 0) return -1
179
+ ctx._fc = false
180
+ const e3 = dr(input, e2, ctx)
181
+ if (e3 < 0) return -1
182
+ ctx._fc = false
183
+ return er(input, e3, ctx)
148
184
  },
149
185
  }
150
186
  }
@@ -169,9 +205,9 @@ function choiceNode(
169
205
  ): BuiltLexNode {
170
206
  let matchTail: PureLexRunner = () => -1
171
207
  for (let i = arms.length - 1; i >= 0; i--) {
172
- const arm = arms[i]!, next = matchTail
208
+ const armMatch = arms[i]!.match, next = matchTail
173
209
  matchTail = (input, pos) => {
174
- const end = arm.match(input, pos)
210
+ const end = armMatch(input, pos)
175
211
  return end < 0 ? next(input, pos) : end
176
212
  }
177
213
  }
@@ -181,7 +217,7 @@ function choiceNode(
181
217
  return -1
182
218
  }
183
219
  for (let i = arms.length - 1; i >= 0; i--) {
184
- const arm = arms[i]!, cls = classes[i], staticExpected = armExpected[i]!, next = tail
220
+ const armRun = arms[i]!.run, cls = classes[i], staticExpected = armExpected[i]!, next = tail
185
221
  tail = (input, pos, ctx, lead, acc, best) => {
186
222
  if (cls !== undefined && !classHas(cls, lead)) {
187
223
  return next(
@@ -191,7 +227,7 @@ function choiceNode(
191
227
  )
192
228
  }
193
229
  ctx._fc = false
194
- const end = arm.run(input, pos, ctx)
230
+ const end = armRun(input, pos, ctx)
195
231
  if (end >= 0) return end
196
232
  const at = ctx._fe ?? pos
197
233
  if (at > best) { best = at; acc = undefined }
@@ -211,11 +247,68 @@ function choiceNode(
211
247
  }
212
248
  }
213
249
 
250
+ /**
251
+ * A choice whose arms' classes are disjoint, as the character choice's exclusive
252
+ * dispatch: the arm the lead selects runs alone, its failure is the choice's at
253
+ * the choice's position, and a lead no arm claims reports the arms' openers. The
254
+ * selected index reaches its arm through a fixed chain, never an array.
255
+ */
256
+ function exclusiveChoiceNode(
257
+ arms: readonly BuiltLexNode[],
258
+ classes: readonly ResolvedClass[],
259
+ choiceFx: readonly string[],
260
+ ): BuiltLexNode {
261
+ // A signed byte holds every arm index up to 127, which is every real grammar's
262
+ // choice; a wider choice takes a wider table so its 129th arm is not read as -128.
263
+ const ascii = (classes.length <= 128 ? new Int8Array(128) : new Int32Array(128)).fill(-1)
264
+ const hi: number[] = []
265
+ for (let i = 0; i < classes.length; i++) {
266
+ const cls = classes[i]!
267
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) ascii[c] = i
268
+ for (let r = 0; r < cls.hi.length; r += 2) hi.push(cls.hi[r]!, cls.hi[r + 1]!, i)
269
+ }
270
+ const select = (code: number): number => {
271
+ if (code < 0) return -1
272
+ if (code < 128) return ascii[code]!
273
+ for (let r = 0; r < hi.length; r += 3) if (code >= hi[r]! && code <= hi[r + 1]!) return hi[r + 2]!
274
+ return -1
275
+ }
276
+ let matchArm: (arm: number, input: string, pos: number) => number = () => -1
277
+ let selectRun: (arm: number, input: string, pos: number, ctx: ParseContext) => number = () => -1
278
+ for (let i = arms.length - 1; i >= 0; i--) {
279
+ const armMatch = arms[i]!.match, armRun = arms[i]!.run, nextMatch = matchArm, nextRun = selectRun
280
+ matchArm = (selected, input, pos) => selected === i ? armMatch(input, pos) : nextMatch(selected, input, pos)
281
+ selectRun = (selected, input, pos, ctx) => selected === i ? armRun(input, pos, ctx) : nextRun(selected, input, pos, ctx)
282
+ }
283
+ const runArm: LexProgramRunner = (input, pos, ctx) => {
284
+ const arm = select(lead(input, pos))
285
+ if (arm >= 0) {
286
+ ctx._fc = false
287
+ const end = selectRun(arm, input, pos, ctx)
288
+ if (end >= 0) return end
289
+ if (committed(ctx)) return -1
290
+ const failed = ctx._fx
291
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return -1 }
292
+ }
293
+ ctx._fe = pos
294
+ ctx._fx = choiceFx as string[]
295
+ return -1
296
+ }
297
+ return {
298
+ match: (input, pos) => {
299
+ const arm = select(lead(input, pos))
300
+ return arm < 0 ? -1 : matchArm(arm, input, pos)
301
+ },
302
+ run: runArm,
303
+ }
304
+ }
305
+
214
306
  function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
307
+ const bodyMatch = body.match
215
308
  return {
216
- match: (input, pos) => body.match(input, pos) < 0 ? pos : -1,
309
+ match: (input, pos) => bodyMatch(input, pos) < 0 ? pos : -1,
217
310
  run: (input, pos, ctx) => {
218
- if (body.match(input, pos) < 0) return pos
311
+ if (bodyMatch(input, pos) < 0) return pos
219
312
  ctx._fe = pos
220
313
  ctx._fx = failure as string[]
221
314
  return -1
@@ -224,19 +317,65 @@ function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
224
317
  }
225
318
 
226
319
  function optionalNode(body: BuiltLexNode): BuiltLexNode {
320
+ const bodyMatch = body.match, bodyRun = body.run
227
321
  return {
228
322
  match: (input, pos) => {
229
- const end = body.match(input, pos)
323
+ const end = bodyMatch(input, pos)
230
324
  return end < 0 ? pos : end
231
325
  },
232
326
  run: (input, pos, ctx) => {
233
327
  ctx._fc = false
234
- const end = body.run(input, pos, ctx)
328
+ const end = bodyRun(input, pos, ctx)
235
329
  return end < 0 && !committed(ctx) ? pos : end
236
330
  },
237
331
  }
238
332
  }
239
333
 
334
+ /**
335
+ * The table's separator-less `OP_REP` under `token()`: no trivia, no separator,
336
+ * nothing to roll back. Only an optional item stops at end of input or on a
337
+ * zero-width match (`repItem`); a failed item ends the loop with its own
338
+ * failure published, and a committed one fails the repeat.
339
+ */
340
+ function repeatNode(item: BuiltLexNode, min: number, max: number): BuiltLexNode {
341
+ const itemMatch = item.match, itemRun = item.run
342
+ return {
343
+ match: (input, pos) => {
344
+ let cur = pos
345
+ let count = 0
346
+ for (;;) {
347
+ if (max >= 0 && count >= max) break
348
+ const optional = count >= min && (count > 0 || min === 0)
349
+ if (optional && cur >= input.length) break
350
+ const end = itemMatch(input, cur)
351
+ if (end < 0 || (optional && end === cur)) break
352
+ cur = end
353
+ count++
354
+ }
355
+ return count < min ? -1 : cur
356
+ },
357
+ run: (input, pos, ctx) => {
358
+ let cur = pos
359
+ let count = 0
360
+ for (;;) {
361
+ if (max >= 0 && count >= max) break
362
+ const optional = count >= min && (count > 0 || min === 0)
363
+ if (optional && cur >= input.length) break
364
+ ctx._fc = false
365
+ const end = itemRun(input, cur, ctx)
366
+ if (end < 0) {
367
+ if (committed(ctx)) return -1
368
+ break
369
+ }
370
+ if (optional && end === cur) break
371
+ cur = end
372
+ count++
373
+ }
374
+ return count < min ? -1 : cur
375
+ },
376
+ }
377
+ }
378
+
240
379
  function fixedTreeRunner(
241
380
  words: readonly number[],
242
381
  matchers: readonly LexBodyRecognizer[],
@@ -269,9 +408,14 @@ function fixedTreeRunner(
269
408
  else stack.push(sequence5(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!, nodes[4]!))
270
409
  continue
271
410
  }
272
- if (op >= LEX_NODE_CHOICE2 && op <= LEX_NODE_CHOICE8) {
273
- const count = op === LEX_NODE_CHOICE2 ? 2 : op === LEX_NODE_CHOICE4 ? 4 : 8
411
+ if (op === LEX_NODE_CHOICE) {
412
+ const count = words[at++]!
413
+ const exclusive = words[at++]!
414
+ if (exclusive !== 0 && exclusive !== 1) throw new TypeError('table lexical tree choice has an invalid dispatch mode')
274
415
  const choiceFx = expected[words[at++]!]
416
+ if (!Number.isInteger(count) || count < 2 || count > stack.length) {
417
+ throw new TypeError('table lexical tree choice has an invalid arm count')
418
+ }
275
419
  if (choiceFx === undefined) throw new TypeError('table lexical tree choice has an invalid expected set')
276
420
  const armClasses: Array<ResolvedClass | undefined> = []
277
421
  const armFx: Array<readonly string[]> = []
@@ -284,7 +428,9 @@ function fixedTreeRunner(
284
428
  armFx.push(fx)
285
429
  }
286
430
  const arms = Array.from({ length: count }, pop).reverse()
287
- stack.push(choiceNode(arms, armClasses, armFx, choiceFx))
431
+ stack.push(exclusive === 1
432
+ ? exclusiveChoiceNode(arms, armClasses as ResolvedClass[], choiceFx)
433
+ : choiceNode(arms, armClasses, armFx, choiceFx))
288
434
  continue
289
435
  }
290
436
  if (op === LEX_NODE_NOT) {
@@ -294,6 +440,14 @@ function fixedTreeRunner(
294
440
  continue
295
441
  }
296
442
  if (op === LEX_NODE_OPTIONAL) { stack.push(optionalNode(pop())); continue }
443
+ if (op === LEX_NODE_REPEAT) {
444
+ const min = words[at++]!, max = words[at++]!
445
+ if (!Number.isInteger(min) || min < 0 || !Number.isInteger(max) || (max >= 0 && max < min)) {
446
+ throw new TypeError('table lexical tree repeat has invalid bounds')
447
+ }
448
+ stack.push(repeatNode(pop(), min, max))
449
+ continue
450
+ }
297
451
  throw new TypeError('table lexical tree has an unknown fixed body')
298
452
  }
299
453
  if (stack.length !== 1) throw new TypeError('table lexical tree does not resolve to one root body')
package/src/table/ops.ts CHANGED
@@ -478,6 +478,14 @@ export const OP_LEX_BODY = 42
478
478
  export const OP_LEX_PROGRAM = 43
479
479
  /** `SOURCE_LEAF f c` — `sourceLeaf()`: reduce semantically, capture source text. */
480
480
  export const OP_SOURCE_LEAF = 44
481
+ /**
482
+ * `RGATE cls child fx` — a choice arm that opens with the token its `dispatch()`
483
+ * already routed. `cls` is what may follow that token (the arm's next term,
484
+ * with the sequence's trivia), tested at the routed token's END; a miss fails
485
+ * there with the next term's set `fx`, as running the arm would. No routed token
486
+ * at this position, a completions probe, or tolerant recovery enters the arm.
487
+ */
488
+ export const OP_RGATE = 45
481
489
  /**
482
490
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
483
491
  *
@@ -501,7 +509,7 @@ export const OP_NAMES: Record<number, string> = {
501
509
  [OP_LIT]: 'LIT', [OP_RX]: 'RX', [OP_SEQ]: 'SEQ', [OP_SEQV]: 'SEQV',
502
510
  [OP_CHOICE]: 'CHOICE', [OP_REP]: 'REP', [OP_REPV]: 'REPV', [OP_OPT]: 'OPT',
503
511
  [OP_XFORM]: 'XFORM', [OP_NODE]: 'NODE', [OP_RULE]: 'RULE', [OP_GATE]: 'GATE',
504
- [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_EMPTY]: 'EMPTY',
512
+ [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_RGATE]: 'RGATE', [OP_EMPTY]: 'EMPTY',
505
513
  [OP_LIT_TRACK]: 'LIT_TRACK', [OP_RX_TRACK]: 'RX_TRACK', [OP_NODE_TRACK]: 'NODE_TRACK',
506
514
  [OP_SCOPE]: 'SCOPE', [OP_EXPECT]: 'EXPECT', [OP_SEQX]: 'SEQX', [OP_SCAN]: 'SCAN',
507
515
  [OP_FIELD]: 'FIELD', [OP_LIT_CI]: 'LIT_CI', [OP_LIT_CI_TRACK]: 'LIT_CI_TRACK', [OP_DISPATCH]: 'DISPATCH', [OP_ROUTED]: 'ROUTED',
@@ -13,6 +13,7 @@
13
13
  * reads only its arguments.
14
14
  */
15
15
  import type { ParseContext } from '../types.ts'
16
+ import { popCstChild, pushCstChild } from '../cst/capture-buffer.ts'
16
17
 
17
18
  export type Span = { start: number; end: number }
18
19
  export type LineSpan = Span & {
@@ -64,3 +65,37 @@ export function lead(input: string, pos: number): number {
64
65
  if (c < 0xd800 || c > 0xdbff) return c
65
66
  return input.codePointAt(pos) ?? c
66
67
  }
68
+
69
+ const EMPTY_KIDS: readonly unknown[] = Object.freeze([])
70
+ const EMPTY_TRIVIA: readonly number[] = Object.freeze([])
71
+
72
+ type CollapseBuild = (
73
+ children: readonly unknown[], fields: undefined, span: { start: number; end: number },
74
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: undefined,
75
+ ) => unknown
76
+
77
+ /**
78
+ * A PASS-THROUGH COLLAPSE whose one child built `undefined` (`site-labels.ts`
79
+ * `passCollapse`).
80
+ *
81
+ * A collapse over a single node is that node: the node publishes its value into
82
+ * the collector the collapse's parent opened, exactly where the collapse would
83
+ * have published it, over the same span. So both engines run the body with no
84
+ * frame of the collapse's own and hand its value up. The one value a frame would
85
+ * have treated differently is `undefined`, which a frame does not count as a
86
+ * child: the collapse then builds from no children. This is that case — take the
87
+ * child's publication back and publish the build instead, as the frame would
88
+ * have. The caller has already set `EC.e`.
89
+ */
90
+ export function collapseEmpty(
91
+ ctx: ParseContext, build: CollapseBuild, input: string, pos: number, end: number, readsRaw: boolean,
92
+ ): unknown {
93
+ const collected = ctx._cstBuf !== undefined || ctx._cstChildren !== undefined
94
+ if (collected) popCstChild(ctx)
95
+ // A frame keeps the raw entry of the child it does not count, for a reducer
96
+ // that reads `rawChildren`.
97
+ const raw = readsRaw ? [rawEntry(undefined, input, pos, end)] : EMPTY_KIDS
98
+ const nd = build(EMPTY_KIDS, undefined, { start: pos, end }, raw, EMPTY_TRIVIA, undefined)
99
+ if (collected) pushCstChild(ctx, nd, ctx._cstBuf?.noRaw === true ? undefined : rawEntry(nd, input, pos, end))
100
+ return nd
101
+ }
@@ -54,6 +54,14 @@ export function scalarTerminalNotChild(code: ArrayLike<number>, ip: number): num
54
54
  return op === OP_RX || op === OP_LIT ? child : -1
55
55
  }
56
56
 
57
+ /** The same terminal under `peek()`: its match decides the lookahead, and nothing it captured survives it. */
58
+ export function scalarTerminalPeekChild(code: ArrayLike<number>, ip: number): number {
59
+ if (code[ip] !== OP_PEEK) return -1
60
+ const child = code[ip + 1]!
61
+ const op = code[child]
62
+ return op === OP_RX || op === OP_LIT ? child : -1
63
+ }
64
+
57
65
  /**
58
66
  * A direct scalar terminal that every successful execution of `ip` must match
59
67
  * first, through wrappers that cannot consume or select before their child.
@@ -60,9 +60,9 @@
60
60
  * label licenses at a leaf is dropping the TEST — never the capture.
61
61
  */
62
62
  import {
63
- OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF,
63
+ OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_SOURCE_LEAF,
64
64
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE,
65
- OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
65
+ OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
66
66
  } from './ops.ts'
67
67
  import { childSlots } from './child-slots.ts'
68
68
 
@@ -142,6 +142,61 @@ export function reachableSites(code: Int32Array, roots: Iterable<number>): Set<n
142
142
  return seen
143
143
  }
144
144
 
145
+ /**
146
+ * A node both engines lower as a PASS-THROUGH COLLAPSE (`run-support.ts`
147
+ * `collapseEmpty`): `collapse: true` over a direct reducer, no projection, no CST
148
+ * host, no line tracking, whose body is one node behind wrappers that consume and
149
+ * capture nothing (a first-set gate, a trivia scope, a rule reference). One
150
+ * predicate, so the two engines and the labels cannot disagree on a site.
151
+ */
152
+ export function passCollapse(code: Int32Array, ip: number, hostCst: boolean): boolean {
153
+ const flags = code[ip + 3]!
154
+ if (hostCst || code[ip] !== OP_NODE || code[ip + 1]! < 0 || code[ip + 4]! >= 0) return false
155
+ if (flags !== 32 && flags !== 34) return false
156
+ let at = code[ip + 2]!
157
+ for (let n = 0; n < 8; n++) {
158
+ const op = code[at]
159
+ if (op === OP_GATE || op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[at + 3] === 0)) at = code[at + 2]!
160
+ else if (op === OP_RULE) at = code[at + 1]!
161
+ else break
162
+ }
163
+ return code[at] === OP_NODE
164
+ }
165
+
166
+ /**
167
+ * A collapse that a FIRST CHARACTER can reduce to a pass-through: its body is a
168
+ * sequence of `optional`/`not` terms, each behind a first-character gate, then
169
+ * one node. Where the character at the collapse's start opens none of those
170
+ * terms (nor its sequence's trivia — the engines test that against the site's
171
+ * label), every one of them matches empty there and the body is that node alone.
172
+ * `leads` are the gate rows, `lits` the constant indices of direct literal terms,
173
+ * `last` the node term. Not a `passCollapse`.
174
+ */
175
+ export function gatedPassCollapse(code: Int32Array, ip: number, hostCst: boolean): { leads: number[]; lits: number[]; last: number } | undefined {
176
+ const flags = code[ip + 3]!
177
+ if (hostCst || code[ip] !== OP_NODE || code[ip + 1]! < 0 || code[ip + 4]! >= 0) return undefined
178
+ if (flags !== 32 && flags !== 34) return undefined
179
+ const seq = code[ip + 2]!
180
+ if (code[seq] !== OP_SEQ && code[seq] !== OP_SEQV) return undefined
181
+ const n = code[seq + 1]!
182
+ if (n < 2) return undefined
183
+ const leads: number[] = []
184
+ const lits: number[] = []
185
+ for (let i = 0; i < n - 1; i++) {
186
+ const t = code[seq + 2 + i]!
187
+ if (code[t] !== OP_OPT && code[t] !== OP_NOT) return undefined
188
+ const g = code[t + 1]!
189
+ // A gate row carries the term's first characters; a direct literal is its own.
190
+ if (code[g] === OP_GATE) leads.push(g)
191
+ else if (code[g] === OP_LIT) lits.push(code[g + 1]!)
192
+ else return undefined
193
+ }
194
+ const last = code[seq + 2 + n - 1]!
195
+ let at = last
196
+ for (let k = 0; k < 8 && (code[at] === OP_GATE || code[at] === OP_RULE); k++) at = code[at] === OP_RULE ? code[at + 1]! : code[at + 2]!
197
+ return code[at] === OP_NODE ? { leads, lits, last } : undefined
198
+ }
199
+
145
200
  /**
146
201
  * What a site hands DOWN to its children. Every op but the two that write the
147
202
  * context passes its own label through unchanged.
@@ -159,6 +214,10 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
159
214
  }
160
215
  if (op === OP_NODE || op === OP_NODE_TRACK) {
161
216
  const flags = code[ip + 3]!
217
+ // A pass-through collapse opens nothing: its body runs in its parent's
218
+ // collector and capture setting.
219
+ if (passCollapse(code, ip, hostCst)) return at
220
+ const gated = gatedPassCollapse(code, ip, hostCst) !== undefined
162
221
  // The same selection `emit-assembly.ts` makes: the trailing-trivia bit (128)
163
222
  // keeps the children-only shape.
164
223
  const directChildren = !hostCst && op === OP_NODE && code[ip + 1]! >= 0 && code[ip + 4]! < 0
@@ -168,7 +227,9 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
168
227
  // arrays instead of a CstCaptureBuf. Descendants still capture, but they
169
228
  // must take the generic collector branch rather than dereference `_cstBuf`.
170
229
  // False means UNKNOWN in this lattice, which is exactly the safe answer.
171
- return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: CAP_OFF }
230
+ const framed = { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: CAP_OFF }
231
+ // A gated pass collapse runs its node term in its parent's collector too.
232
+ return gated ? meet(at, framed) : framed
172
233
  }
173
234
  // `ctx._cstBuf = buf` and `ctx.captureTrivia = <literal>`, both unconditional
174
235
  // — `emit-assembly.ts`'s `OP_NODE` body opens the buffer before it descends
@@ -178,7 +239,8 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
178
239
  ? RAW_OMIT
179
240
  : RAW_CAPTURE
180
241
  if (at.buf && raw === at.raw && cap === at.cap) return at
181
- return { tri: at.tri, buf: true, raw, cap }
242
+ const framed = { tri: at.tri, buf: true, raw, cap }
243
+ return gated ? meet(at, framed) : framed
182
244
  }
183
245
  // THE TWO BOUNDARIES clear every capture sink for their child and contribute
184
246
  // ONE leaf for the whole match; `token()` clears `ctx.trivia` as well. So a
@@ -251,3 +313,72 @@ export function computeSiteLabels(
251
313
 
252
314
  return { at: (ip: number): SiteLabel => labels.get(ip) ?? TOP }
253
315
  }
316
+
317
+ /**
318
+ * Does a piece entered at `ip` need the caller's INSTALLED trivia scanner handed
319
+ * across an engine boundary?
320
+ *
321
+ * Each table engine keeps its own installed-scanner slot, and the two hand theirs
322
+ * across at every call between them. A site whose label names its trivia (`tri`)
323
+ * reads no slot: both engines lower its skips onto that trivia's own scanner, and
324
+ * a call out of it can hand that scanner across instead of the slot. Only a site
325
+ * whose trivia the labels cannot name (`TRI_UNKNOWN`) reads the slot. So the
326
+ * handoff is needed exactly when such a site is reachable from `ip` before a
327
+ * scope installs a scanner of its own (an aliased scope, which installs what is
328
+ * already installed, does not). `false` is also the answer for a body with no
329
+ * trivia at all.
330
+ */
331
+ /**
332
+ * A trivia scope that writes only what its label proves is already in force, so
333
+ * it runs as its child. A slot scope under that same slot is one; so is a scope
334
+ * with no trivia where the label proves none (inside `token()` or another such
335
+ * scope), since every way to `TRI_NONE` leaves no trivia, no kind labels and no
336
+ * scanner — what this scope would write. `OP_SCOPE_PLAIN` has no root policy; a
337
+ * policy-bearing `OP_SCOPE` is one only with policy zero, and `OP_SCOPE_CAP`
338
+ * never is, since it raises `captureTrivia`. `TRI_UNKNOWN` matches nothing.
339
+ */
340
+ export function scopeIsIdentity(code: Int32Array, ip: number, at: SiteLabel): boolean {
341
+ const op = code[ip]
342
+ if (op !== OP_SCOPE_PLAIN && !(op === OP_SCOPE && code[ip + 3]! === 0)) return false
343
+ const ki = code[ip + 1]!
344
+ return at.tri === (ki < 0 ? TRI_NONE : ki)
345
+ }
346
+
347
+ export function scanHandoffNeeded(code: Int32Array, ip: number, labels: SiteLabels): boolean {
348
+ const seen = new Set<number>()
349
+ const stack = [ip]
350
+ const kids: number[] = []
351
+ while (stack.length > 0) {
352
+ const at = stack.pop()!
353
+ if (seen.has(at)) continue
354
+ seen.add(at)
355
+ const l = labels.at(at)
356
+ if (l.tri === TRI_UNKNOWN) return true
357
+ const op = code[at]
358
+ // A scan runs interpreted combinators that reach pieces through `prog.scans`.
359
+ if (op === OP_SCAN) return true
360
+ if (op === OP_TOKEN || op === OP_SCOPE_CAP) continue
361
+ if ((op === OP_SCOPE_PLAIN || op === OP_SCOPE) && !(code[at + 1]! >= 0 && l.tri === code[at + 1]!
362
+ && (op === OP_SCOPE_PLAIN || code[at + 3]! === 0))) continue
363
+ kids.length = 0
364
+ if (!childSlots(code, at, kids)) return true
365
+ for (const c of kids) stack.push(c)
366
+ }
367
+ return false
368
+ }
369
+
370
+ /**
371
+ * The first characters that can start trivia at a site labelled `tri`, as code
372
+ * point ranges: none for `TRI_NONE`, `undefined` where the labels cannot name the
373
+ * trivia or its first set is unbounded.
374
+ */
375
+ export function triviaLeadRanges(
376
+ tri: number, trivia: readonly ({ _meta: { firstSet: { kind: string; ranges?: readonly { lo: number; hi: number }[] } } } | undefined)[],
377
+ ): readonly { lo: number; hi: number }[] | undefined {
378
+ if (tri === TRI_NONE) return []
379
+ if (tri < 0) return undefined
380
+ const fs = trivia[tri]?._meta.firstSet
381
+ if (fs === undefined) return undefined
382
+ if (fs.kind === 'empty') return []
383
+ return fs.kind === 'ranges' && fs.ranges !== undefined ? fs.ranges : undefined
384
+ }
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.57.0'
16
+ export const PARSEMAN_VERSION = '0.58.0'