parseman 0.56.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 (106) hide show
  1. package/dist/analysis/diagnostics.js +2232 -1088
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +2247 -1103
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/combinators/node.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts +1 -1
  8. package/dist/compiler/build-arity.d.ts.map +1 -1
  9. package/dist/compiler/token-alphabet.d.ts +11 -1
  10. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  11. package/dist/compiler/token-capability.js +69 -16
  12. package/dist/compiler/token-capability.js.map +2 -2
  13. package/dist/cst/capture-buffer.d.ts +6 -0
  14. package/dist/cst/capture-buffer.d.ts.map +1 -1
  15. package/dist/cst/trivia-kinds.d.ts +9 -1
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.js +1446 -236
  18. package/dist/index.js.map +3 -3
  19. package/dist/language-service/index.js +1 -0
  20. package/dist/language-service/index.js.map +1 -1
  21. package/dist/parse-context.d.ts.map +1 -1
  22. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  23. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  24. package/dist/plugin/index.d.ts +51 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +2126 -380
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +2 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -1
  30. package/dist/regex/classes.d.ts.map +1 -1
  31. package/dist/run/index.js +1 -0
  32. package/dist/run/index.js.map +1 -1
  33. package/dist/table/assemble.d.ts +9 -0
  34. package/dist/table/assemble.d.ts.map +1 -1
  35. package/dist/table/child-slots.d.ts +4 -6
  36. package/dist/table/child-slots.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +14 -2
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/emit-assembly.d.ts +17 -2
  40. package/dist/table/emit-assembly.d.ts.map +1 -1
  41. package/dist/table/emit.d.ts +5 -1
  42. package/dist/table/emit.d.ts.map +1 -1
  43. package/dist/table/encode.d.ts.map +1 -1
  44. package/dist/table/exec-baseline.d.ts.map +1 -1
  45. package/dist/table/exec.d.ts.map +1 -1
  46. package/dist/table/factor-choice.d.ts +42 -0
  47. package/dist/table/factor-choice.d.ts.map +1 -0
  48. package/dist/table/fail-clean.d.ts +3 -0
  49. package/dist/table/fail-clean.d.ts.map +1 -0
  50. package/dist/table/hold-gap.d.ts +8 -0
  51. package/dist/table/hold-gap.d.ts.map +1 -0
  52. package/dist/table/index.js +1798 -316
  53. package/dist/table/index.js.map +3 -3
  54. package/dist/table/lex-program.d.ts +7 -3
  55. package/dist/table/lex-program.d.ts.map +1 -1
  56. package/dist/table/ops.d.ts +17 -6
  57. package/dist/table/ops.d.ts.map +1 -1
  58. package/dist/table/program.d.ts +13 -0
  59. package/dist/table/program.d.ts.map +1 -1
  60. package/dist/table/run-support.d.ts +19 -0
  61. package/dist/table/run-support.d.ts.map +1 -1
  62. package/dist/table/scalar-terminal.d.ts +2 -0
  63. package/dist/table/scalar-terminal.d.ts.map +1 -1
  64. package/dist/table/select-hot.d.ts +6 -0
  65. package/dist/table/select-hot.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts +66 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/table/trivia-gap.d.ts +31 -0
  69. package/dist/table/trivia-gap.d.ts.map +1 -0
  70. package/dist/types.d.ts +17 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/version.d.ts +1 -1
  73. package/package.json +4 -1
  74. package/src/combinators/dispatch.ts +12 -3
  75. package/src/combinators/node.ts +6 -13
  76. package/src/combinators/trivia-skip.ts +44 -1
  77. package/src/compiler/build-arity.ts +3 -3
  78. package/src/compiler/token-alphabet.ts +57 -17
  79. package/src/cst/capture-buffer.ts +25 -0
  80. package/src/cst/trivia-kinds.ts +12 -1
  81. package/src/parse-context.ts +1 -0
  82. package/src/plugin/entry-supercompiler.ts +8 -9
  83. package/src/plugin/index.ts +362 -42
  84. package/src/plugin/ir-eval.ts +7 -1
  85. package/src/regex/classes.ts +3 -0
  86. package/src/table/assemble.ts +371 -83
  87. package/src/table/child-slots.ts +6 -8
  88. package/src/table/compile-rule-map.ts +64 -15
  89. package/src/table/emit-assembly.ts +244 -42
  90. package/src/table/emit.ts +11 -8
  91. package/src/table/encode.ts +149 -35
  92. package/src/table/exec-baseline.ts +1 -3
  93. package/src/table/exec.ts +34 -14
  94. package/src/table/factor-choice.ts +173 -0
  95. package/src/table/fail-clean.ts +130 -0
  96. package/src/table/hold-gap.ts +68 -0
  97. package/src/table/lex-program.ts +197 -43
  98. package/src/table/ops.ts +18 -7
  99. package/src/table/program.ts +24 -0
  100. package/src/table/run-support.ts +35 -0
  101. package/src/table/scalar-terminal.ts +8 -0
  102. package/src/table/select-hot.ts +70 -0
  103. package/src/table/site-labels.ts +135 -4
  104. package/src/table/trivia-gap.ts +109 -0
  105. package/src/types.ts +18 -0
  106. package/src/version.ts +1 -1
@@ -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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
3
+ *
4
+ * The emitted engine beats the closure engine per site, but its bytes are paid by
5
+ * every process that loads the module. Emitting every site made jess's Less AST
6
+ * artifact 20.8x its grammar source. So a build emits only the sites with the
7
+ * highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
8
+ * runs the rest (`assemble.ts`, the `HYBRID` path).
9
+ *
10
+ * Hotness is estimated statically, since a macro build has no workload to
11
+ * profile: every rule entry starts at 1, a repetition multiplies its body by
12
+ * `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
13
+ * weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
14
+ * instead of diverging. Sites are then taken greedily by weight per emitted byte.
15
+ * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
16
+ */
17
+ import { childSlots } from './child-slots.ts'
18
+ import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
19
+
20
+ const LOOP = 8
21
+ const ROUNDS = 60
22
+ const CAP = 1e12
23
+
24
+ /** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
25
+ export function selectHotSites(
26
+ code: readonly number[],
27
+ entries: readonly number[],
28
+ siteBytes: ReadonlyMap<number, number>,
29
+ budget: number,
30
+ /** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
31
+ * static estimate when given. */
32
+ profile?: ArrayLike<number>,
33
+ ): Set<number> {
34
+ const selected = new Set<number>()
35
+ if (budget <= 0) return selected
36
+ const weight = profile === undefined ? staticWeights(code, entries) : undefined
37
+ const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
38
+ // Ties (saturated cycles) break by site offset, so a build is deterministic.
39
+ const ranked = [...siteBytes.keys()].sort((a, b) =>
40
+ w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
41
+ let spent = 0
42
+ for (const ip of ranked) {
43
+ if (w(ip) === 0) break
44
+ const bytes = siteBytes.get(ip)!
45
+ if (spent + bytes > budget) continue
46
+ spent += bytes
47
+ selected.add(ip)
48
+ }
49
+ return selected
50
+ }
51
+
52
+ function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
53
+ let weight = new Map<number, number>()
54
+ const kids: number[] = []
55
+ for (let round = 0; round < ROUNDS; round++) {
56
+ const next = new Map<number, number>()
57
+ for (const ip of entries) next.set(ip, 1)
58
+ for (const [ip, w] of weight) {
59
+ kids.length = 0
60
+ childSlots(code, ip, kids)
61
+ const op = code[ip]
62
+ const scale = op === OP_REP || op === OP_REPV ? LOOP
63
+ : op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
64
+ : 1
65
+ for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
66
+ }
67
+ weight = next
68
+ }
69
+ return weight
70
+ }
@@ -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
+ }
@@ -0,0 +1,109 @@
1
+ import type { CharRange, Combinator, ParseContext } from '../types.ts'
2
+ import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
3
+ import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
4
+
5
+ /**
6
+ * THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
7
+ * the path every labelled trivia scan takes once a log is live, answered without
8
+ * the scan where the answer is already known. Both engines call it: the closure
9
+ * engine's `skipTrivia` and the emitted assembly's skips.
10
+ *
11
+ * ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
12
+ * and a run in which no character can start a selected arm holds none: its end
13
+ * is the whole answer, and the unlabelled scanner gives it without the
14
+ * chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
15
+ * selected leaders is free of selected chunks. jess parses with
16
+ * `rootTrivia: { select: [comment labels] }`, which is this case for every run
17
+ * of plain whitespace.
18
+ *
19
+ * A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
20
+ * trivia, so the next term asks for the same gap again: on `benchmark.less`,
21
+ * 21,741 of the closure engine's 53,607 trivia scans per parse started where
22
+ * the previous one did. A scan that recorded nothing records nothing again from
23
+ * the same position, in the same parse, under the same trivia, root selection,
24
+ * root-capture policy, trivia log and state, and with no CST capture, and it
25
+ * ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
26
+ * The state is part of the key because a trivia arm can read it (`gate`), and
27
+ * the log because a `leaf` sets it aside while its child runs. The memo lives
28
+ * on the context so it is released with the parse.
29
+ *
30
+ * Neither applies with `trackLines` (a scan records line ranges) or under CST
31
+ * capture (a scan records into the open buffer).
32
+ */
33
+ export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
34
+ const triviaP = ctx.trivia
35
+ const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
36
+ const gap = ctx._gap
37
+ if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
38
+ && ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
39
+ && ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
40
+ const end = clearRunEnd(input, cur, ctx, triviaP, plain)
41
+ if (end >= 0) {
42
+ remember(cur, end, ctx, input, triviaP)
43
+ return end
44
+ }
45
+ const scan = scanTriviaCompact(input, cur, ctx)
46
+ if (typeof scan !== 'number') return commitTriviaScan(scan)
47
+ if (plain) remember(cur, scan, ctx, input, triviaP)
48
+ return scan
49
+ }
50
+
51
+ /** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
52
+ function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
53
+ const rootKinds = ctx._rootTriviaKindIndex
54
+ const spec = triviaP?._meta.labeledTriviaSpec
55
+ if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
56
+ if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
57
+ const fast = spec.rootScan
58
+ if (fast === null) return -1
59
+ const end = fast(input, cur)
60
+ return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
61
+ }
62
+
63
+ function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
64
+ const gap = ctx._gap
65
+ if (gap === undefined) {
66
+ ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
67
+ return
68
+ }
69
+ gap.at = cur
70
+ gap.end = end
71
+ gap.input = input
72
+ gap.of = triviaP
73
+ gap.root = ctx._rootTriviaCapture
74
+ gap.rootKinds = ctx._rootTriviaKindIndex
75
+ gap.log = ctx._triviaLog
76
+ gap.state = ctx.state
77
+ }
78
+
79
+ /**
80
+ * Prime `spec` for the selection `rootKinds`: the first-character ranges of the
81
+ * arms it selects, and the scanner, which is `null` where one of those arms is
82
+ * unbounded or the trivia has no unlabelled scanner. `run()` builds one
83
+ * selection per parse, so this runs once per parse per trivia.
84
+ */
85
+ function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
86
+ const ranges: CharRange[] = []
87
+ let bounded = true
88
+ for (const arm of spec.arms) {
89
+ if (rootKinds[arm.label] === undefined) continue
90
+ const fs = arm.parser._meta.firstSet
91
+ if (fs.kind === 'any') bounded = false
92
+ else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
93
+ }
94
+ const classified = triviaP._meta.triviaScanner
95
+ spec.rootKinds = rootKinds
96
+ spec.rootLeaders = ranges
97
+ spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
98
+ }
99
+
100
+ /** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
101
+ * the ranges are code points, and a code unit cannot be checked against them. */
102
+ function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
103
+ for (let i = from; i < to; i++) {
104
+ const c = input.charCodeAt(i)
105
+ if (c >= 0xD800 && c <= 0xDFFF) return false
106
+ for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
107
+ }
108
+ return true
109
+ }
package/src/types.ts CHANGED
@@ -398,6 +398,12 @@ export type ParseContext = {
398
398
  * selected markers into the document-root capture.
399
399
  */
400
400
  _rootTriviaCapture?: boolean | undefined
401
+ /**
402
+ * Framework-internal: the last deferred-commit trivia scan in this parse that
403
+ * recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
404
+ * with the parse.
405
+ */
406
+ _gap?: TriviaGapMemo | undefined
401
407
  /**
402
408
  * Framework-internal: flat per-node trivia log for CST capture mode.
403
409
  * When set alongside _cstRawChildren, each trivia entry is recorded as three
@@ -606,3 +612,15 @@ export type CompiledParser<T> = {
606
612
  /** Present only when compiled with `{ coverage: true }`. */
607
613
  coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
608
614
  }
615
+
616
+ /** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
617
+ export interface TriviaGapMemo {
618
+ at: number
619
+ end: number
620
+ input: string
621
+ of: Combinator<unknown> | undefined
622
+ root: boolean | undefined
623
+ rootKinds: Readonly<Record<string, number>> | undefined
624
+ log: number[] | undefined
625
+ state: unknown
626
+ }
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.56.0'
16
+ export const PARSEMAN_VERSION = '0.58.0'