parseman 0.53.0 → 0.55.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 (69) hide show
  1. package/dist/analysis/diagnostics.js +295 -119
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +295 -119
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/field-trivia.d.ts +40 -0
  6. package/dist/combinators/field-trivia.d.ts.map +1 -0
  7. package/dist/combinators/map.d.ts +19 -2
  8. package/dist/combinators/map.d.ts.map +1 -1
  9. package/dist/combinators/repeat.d.ts.map +1 -1
  10. package/dist/combinators/sequence.d.ts.map +1 -1
  11. package/dist/combinators/withCtx.d.ts +22 -7
  12. package/dist/combinators/withCtx.d.ts.map +1 -1
  13. package/dist/compiler/fields.d.ts +2 -0
  14. package/dist/compiler/fields.d.ts.map +1 -1
  15. package/dist/cst/trivia-charscan.d.ts +11 -3
  16. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  17. package/dist/cst/trivia-kinds.d.ts +11 -0
  18. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +319 -130
  22. package/dist/index.js.map +3 -3
  23. package/dist/plugin/evaluator.d.ts.map +1 -1
  24. package/dist/plugin/index.js +324 -132
  25. package/dist/plugin/index.js.map +3 -3
  26. package/dist/table/assemble.d.ts +9 -0
  27. package/dist/table/assemble.d.ts.map +1 -1
  28. package/dist/table/emit-assembly.d.ts +1 -1
  29. package/dist/table/emit-assembly.d.ts.map +1 -1
  30. package/dist/table/emit.d.ts +6 -2
  31. package/dist/table/emit.d.ts.map +1 -1
  32. package/dist/table/encode.d.ts.map +1 -1
  33. package/dist/table/exec.d.ts.map +1 -1
  34. package/dist/table/field-spec.d.ts +13 -0
  35. package/dist/table/field-spec.d.ts.map +1 -0
  36. package/dist/table/index.js +262 -113
  37. package/dist/table/index.js.map +3 -3
  38. package/dist/table/ops.d.ts +3 -1
  39. package/dist/table/ops.d.ts.map +1 -1
  40. package/dist/table/scalar-terminal.d.ts +11 -0
  41. package/dist/table/scalar-terminal.d.ts.map +1 -1
  42. package/dist/types.d.ts +14 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/version.d.ts +1 -1
  45. package/package.json +1 -1
  46. package/src/analysis/duplication.ts +1 -1
  47. package/src/analysis/rebuild.ts +1 -1
  48. package/src/combinators/field-trivia.ts +44 -0
  49. package/src/combinators/map.ts +48 -6
  50. package/src/combinators/repeat.ts +6 -1
  51. package/src/combinators/sequence.ts +9 -1
  52. package/src/combinators/withCtx.ts +61 -28
  53. package/src/compiler/fields.ts +5 -2
  54. package/src/compiler/ir-serialize.ts +7 -1
  55. package/src/coverage.ts +1 -1
  56. package/src/cst/trivia-charscan.ts +17 -56
  57. package/src/cst/trivia-kinds.ts +24 -8
  58. package/src/index.ts +1 -0
  59. package/src/plugin/evaluator.ts +13 -3
  60. package/src/table/assemble.ts +137 -23
  61. package/src/table/emit-assembly.ts +38 -18
  62. package/src/table/emit.ts +15 -4
  63. package/src/table/encode.ts +23 -2
  64. package/src/table/exec.ts +17 -2
  65. package/src/table/field-spec.ts +17 -0
  66. package/src/table/ops.ts +3 -1
  67. package/src/table/scalar-terminal.ts +25 -0
  68. package/src/types.ts +14 -2
  69. package/src/version.ts +1 -1
@@ -226,74 +226,35 @@ function plainRegexSource(p: Combinator<unknown>): string | null {
226
226
  return d.tag === 'regex' && d.flags === '' ? d.source : null
227
227
  }
228
228
 
229
- let visitSpec0: LabeledTriviaSpec | undefined
230
- let visitSpec1: LabeledTriviaSpec | undefined
231
- let visitSpec2: LabeledTriviaSpec | undefined
232
- let visitSpec3: LabeledTriviaSpec | undefined
233
- let visitValue0: CssTriviaVisitor | null | undefined
234
- let visitValue1: CssTriviaVisitor | null | undefined
235
- let visitValue2: CssTriviaVisitor | null | undefined
236
- let visitValue3: CssTriviaVisitor | null | undefined
229
+ /** The fields of a spec the char-level lowerings below are computed from. */
230
+ type TriviaArmsSpec = Pick<LabeledTriviaSpec, 'arms' | 'minRepeats'>
237
231
 
238
- /** A direct classified scanner for the four canonical CSS/Less trivia tuples. */
239
- export function commonLabeledTriviaVisitor(spec: LabeledTriviaSpec): CssTriviaVisitor | null {
240
- if (spec === visitSpec0) return visitValue0!
241
- if (spec === visitSpec1) return visitValue1!
242
- if (spec === visitSpec2) return visitValue2!
243
- if (spec === visitSpec3) return visitValue3!
244
- const sources = spec.minRepeats <= 1 ? spec.arms.map(arm => plainRegexSource(arm.parser)) : []
245
- const visitor = commonCssTriviaVisitor(sources)
246
- visitSpec3 = visitSpec2
247
- visitValue3 = visitValue2
248
- visitSpec2 = visitSpec1
249
- visitValue2 = visitValue1
250
- visitSpec1 = visitSpec0
251
- visitValue1 = visitValue0
252
- visitSpec0 = spec
253
- visitValue0 = visitor
254
- return visitor
232
+ /**
233
+ * A direct classified scanner for the four canonical CSS/Less trivia tuples.
234
+ * Computed once per spec, by `analyzeLabeledTrivia`, and read off it as
235
+ * `spec.visitor`: a scan never asks again.
236
+ */
237
+ export function commonLabeledTriviaVisitor(spec: TriviaArmsSpec): CssTriviaVisitor | null {
238
+ return commonCssTriviaVisitor(spec.minRepeats <= 1 ? spec.arms.map(arm => plainRegexSource(arm.parser)) : [])
255
239
  }
256
240
 
257
- let armSpec0: LabeledTriviaSpec | undefined
258
- let armSpec1: LabeledTriviaSpec | undefined
259
- let armSpec2: LabeledTriviaSpec | undefined
260
- let armSpec3: LabeledTriviaSpec | undefined
261
- let armValue0: readonly TriviaArmMatcher[] | null | undefined
262
- let armValue1: readonly TriviaArmMatcher[] | null | undefined
263
- let armValue2: readonly TriviaArmMatcher[] | null | undefined
264
- let armValue3: readonly TriviaArmMatcher[] | null | undefined
265
-
266
241
  /**
267
242
  * The character-level matchers for a spec's arms — index-parallel to `spec.arms`,
268
243
  * so a matcher's index IS its kind index — or `null` when any arm has no lowering.
244
+ * Computed once per spec, as `spec.charArms`.
269
245
  *
270
246
  * `spec.minRepeats > 1` is excluded: the loops below report only an END, and a run
271
247
  * that fails a minimum of two must report NOTHING. `classifiedTrivia()` never
272
248
  * produces one, and the combinator path still handles it if a host writes one.
273
249
  */
274
- export function charArmsFor(spec: LabeledTriviaSpec): readonly TriviaArmMatcher[] | null {
275
- if (spec === armSpec0) return armValue0!
276
- if (spec === armSpec1) return armValue1!
277
- if (spec === armSpec2) return armValue2!
278
- if (spec === armSpec3) return armValue3!
279
- let arms: TriviaArmMatcher[] | null = []
280
- if (spec.minRepeats > 1) {
281
- arms = null
282
- } else {
283
- for (const arm of spec.arms) {
284
- const c = classifyParser(arm.parser)
285
- if (!c) { arms = null; break }
286
- arms.push(c)
287
- }
250
+ export function charArmsFor(spec: TriviaArmsSpec): readonly TriviaArmMatcher[] | null {
251
+ if (spec.minRepeats > 1) return null
252
+ const arms: TriviaArmMatcher[] = []
253
+ for (const arm of spec.arms) {
254
+ const c = classifyParser(arm.parser)
255
+ if (!c) return null
256
+ arms.push(c)
288
257
  }
289
- armSpec3 = armSpec2
290
- armValue3 = armValue2
291
- armSpec2 = armSpec1
292
- armValue2 = armValue1
293
- armSpec1 = armSpec0
294
- armValue1 = armValue0
295
- armSpec0 = spec
296
- armValue0 = arms
297
258
  return arms
298
259
  }
299
260
 
@@ -2,7 +2,10 @@ import type { Combinator, ParseContext } from '../types.ts'
2
2
  import { pushCstTriviaEntry, pushTriviaLogEntry } from './capture-buffer.ts'
3
3
  import { startsFirstSet } from '../combinators/first-set.ts'
4
4
  import { createDetachedParseContext } from '../parse-context.ts'
5
- import { charArmsFor, charTriviaEnd, charTriviaKindMask, charTriviaVisit, commonLabeledTriviaVisitor } from './trivia-charscan.ts'
5
+ import {
6
+ charArmsFor, charTriviaEnd, charTriviaKindMask, charTriviaVisit, commonLabeledTriviaVisitor, type TriviaArmMatcher,
7
+ } from './trivia-charscan.ts'
8
+ import type { CssTriviaVisitor } from './trivia-css-scanner.ts'
6
9
 
7
10
  export type TriviaChunk = { start: number; end: number; kindIndex: number }
8
11
 
@@ -10,6 +13,13 @@ export type LabeledTriviaSpec = {
10
13
  readonly labels: readonly string[]
11
14
  readonly arms: ReadonlyArray<{ label: string; kindIndex: number; parser: Combinator<unknown> }>
12
15
  readonly minRepeats: number
16
+ /**
17
+ * The spec's char-level scanners, built with it. A scan reads them here: the
18
+ * four-slot memo they used to sit behind thrashed on a grammar with more trivia
19
+ * tables than slots, and rebuilt one on every miss.
20
+ */
21
+ readonly visitor: CssTriviaVisitor | null
22
+ readonly charArms: readonly TriviaArmMatcher[] | null
13
23
  }
14
24
 
15
25
  /** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
@@ -93,12 +103,18 @@ export function analyzeLabeledTrivia(trivia: Combinator<unknown>): LabeledTrivia
93
103
  arms.push({ label: peeled.label, kindIndex: 0, parser: peeled.parser })
94
104
  }
95
105
 
96
- const spec = {
106
+ return cacheLabeledTrivia(trivia, labeledTriviaSpec(arms, minRepeats))
107
+ }
108
+
109
+ /** A spec, with the char-level scanners every scan of it reads. */
110
+ export function labeledTriviaSpec(arms: LabeledTriviaSpec['arms'], minRepeats: number): LabeledTriviaSpec {
111
+ return {
97
112
  labels: arms.map(a => a.label),
98
113
  arms,
99
114
  minRepeats,
115
+ visitor: commonLabeledTriviaVisitor({ arms, minRepeats }),
116
+ charArms: charArmsFor({ arms, minRepeats }),
100
117
  }
101
- return cacheLabeledTrivia(trivia, spec)
102
118
  }
103
119
 
104
120
  /** Label table on a `trivia()` combinator, if all arms are labeled. */
@@ -169,9 +185,9 @@ export function scanLabeledTriviaEnd(
169
185
  spec: LabeledTriviaSpec,
170
186
  state?: unknown,
171
187
  ): number {
172
- // `charArmsFor` only lowers `minRepeats <= 1`, where "no chunk matched" and
188
+ // `charArms` only lowers `minRepeats <= 1`, where "no chunk matched" and
173
189
  // "the minimum was not met" are the same position — so an end IS the answer.
174
- const chars = charArmsFor(spec)
190
+ const chars = spec.charArms
175
191
  if (chars !== null) return charTriviaEnd(input, cur, chars)
176
192
 
177
193
  let pos = cur
@@ -205,9 +221,9 @@ export function visitLabeledTrivia(
205
221
  visit: (start: number, end: number, kindIndex: number) => void,
206
222
  ): number | undefined {
207
223
  if (spec.minRepeats > 1) return undefined
208
- const common = commonLabeledTriviaVisitor(spec)
224
+ const common = spec.visitor
209
225
  if (common !== null) return common(input, cur, visit)
210
- const chars = charArmsFor(spec)
226
+ const chars = spec.charArms
211
227
  if (chars !== null) return charTriviaVisit(input, cur, chars, visit)
212
228
 
213
229
  let pos = cur
@@ -331,7 +347,7 @@ export function triviaKindMaskAt(
331
347
  spec: LabeledTriviaSpec,
332
348
  state?: unknown,
333
349
  ): number {
334
- const chars = charArmsFor(spec)
350
+ const chars = spec.charArms
335
351
  if (chars !== null) return charTriviaKindMask(input, pos, chars)
336
352
 
337
353
  let cur = pos
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export { node } from './combinators/node.ts'
30
30
  export type { BuildNode, NodeCombinator, NodeOptions, NodeProjectOptions } from './combinators/node.ts'
31
31
  // lazy() is intentionally NOT exported.
32
32
  export { transform, trivia, classifiedTrivia, label, field } from './combinators/map.ts'
33
+ export type { FieldOptions } from './combinators/map.ts'
33
34
  export { parse, parser, noTrivia } from './combinators/grammar.ts'
34
35
  export type { ParseOptions, ParserOptions, ParsemanParser } from './combinators/grammar.ts'
35
36
  export { token, leaf, sourceLeaf } from './combinators/token.ts'
@@ -96,6 +96,9 @@ function stripTsFromSource(node: Node, code: string): string {
96
96
  class UnevaluatedExtra {}
97
97
  const UNEVALUATED_EXTRA: unknown = new UnevaluatedExtra()
98
98
 
99
+ /** The stand-in for a derive-form `withCtx(s => next, …)`; see the `withCtx` case. */
100
+ const DERIVE_STAND_IN = (state: unknown): unknown => state
101
+
99
102
  // ---------------------------------------------------------------------------
100
103
  // Reducer resolution
101
104
  //
@@ -314,7 +317,7 @@ const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
314
317
  a[0] as Readonly<Record<string, Combinator<unknown>>>,
315
318
  ),
316
319
  label: (...a) => parseman.label(a[0] as string, a[1] as Combinator<unknown>),
317
- field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>),
320
+ field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>, a[2] as parseman.FieldOptions | undefined),
318
321
  noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
319
322
  token: (...a) => parseman.token(a[0] as Combinator<unknown>),
320
323
  // `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
@@ -1028,10 +1031,17 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
1028
1031
  // survives only when it cannot be evaluated, and then it is `emittableConst`
1029
1032
  // that decides — a plain `{}` extras object is indistinguishable from an
1030
1033
  // author's `{}`, so an unevaluable one must NOT masquerade as empty state.
1031
- const evaluated = anyValue(extraArg as Expression, scope, code, [])
1034
+ //
1035
+ // A function literal is the DERIVE form, `withCtx(s => next, inner)`. Its
1036
+ // value is never needed here — the encoder prints `extraSrc` into the
1037
+ // artifact — but the def must hold a FUNCTION so the encoder lowers the
1038
+ // derive form at all; the stand-in is that function, like `gate`'s.
1039
+ const extraType = (unwrapStaticExpr(extraArg as Expression) as { type: string }).type
1040
+ const derives = extraType === 'ArrowFunctionExpression' || extraType === 'FunctionExpression'
1041
+ const evaluated = derives ? null : anyValue(extraArg as Expression, scope, code, [])
1032
1042
  const usable = typeof evaluated === 'object' && evaluated !== null && !Array.isArray(evaluated)
1033
1043
  && Object.getPrototypeOf(evaluated) === Object.prototype
1034
- const combi = parseman.withCtx(usable ? evaluated : UNEVALUATED_EXTRA, inner)
1044
+ const combi = parseman.withCtx(derives ? DERIVE_STAND_IN : usable ? evaluated : UNEVALUATED_EXTRA, inner)
1035
1045
  if (combi._def.tag !== 'withCtx') return null
1036
1046
  combi._def.extraSrc = extraSrc
1037
1047
  return combi
@@ -66,6 +66,7 @@
66
66
  import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
67
67
  import { balanced, scanTo } from '../combinators/scanTo.ts'
68
68
  import { buildFieldMap } from '../compiler/fields.ts'
69
+ import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
69
70
  import { asciiFoldKey } from '../combinators/dispatch.ts'
70
71
  import { projectChild, unwrapChild } from '../combinators/node.ts'
71
72
  import { cstOutputHost } from '../compiler/build-arity.ts'
@@ -129,7 +130,7 @@ import { computeSiteLabels, reachableSites } from './site-labels.ts'
129
130
  import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
130
131
  import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
131
132
  import {
132
- leadingLiteralFamily, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
133
+ largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
133
134
  scalarTerminalNotChild, type ScalarRecognizer,
134
135
  } from './scalar-terminal.ts'
135
136
 
@@ -146,6 +147,7 @@ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string |
146
147
 
147
148
  /** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
148
149
  import { FAIL, newEndCell } from './cell.ts'
150
+ import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
149
151
 
150
152
  const EMPTY_TL: readonly number[] = Object.freeze([])
151
153
  /** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
@@ -1053,6 +1055,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1053
1055
  return cur
1054
1056
  }
1055
1057
 
1058
+ /**
1059
+ * A `field(…, { leadingTrivia: true })` term. The pre-scan cursor reaches the
1060
+ * field AFTER `step`'s scan, right before the field runs, so a field inside the
1061
+ * trivia grammar cannot take it first. One closure per call; only such terms pay.
1062
+ */
1063
+ function leadingFieldTerm(step: typeof nextTerm, child: Piece, input: string, cur: number, ctx: ParseContext): number {
1064
+ return step((i, p, c) => { LEAD.at = cur; return child(i, p, c) }, input, cur, ctx)
1065
+ }
1066
+
1056
1067
  function committed(c: ParseContext): boolean {
1057
1068
  return c._fc === true
1058
1069
  }
@@ -1793,12 +1804,27 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1793
1804
  }
1794
1805
 
1795
1806
  case OP_FIELD: {
1796
- const name = k[code[ip + 1]!] as string
1807
+ const { name, bits } = fieldSpec(k[code[ip + 1]!])
1797
1808
  const child = link(code[ip + 2]!)
1809
+ if (bits === 0) {
1810
+ return (input, pos, ctx) => {
1811
+ const v = child(input, pos, ctx)
1812
+ if (v === FAIL) return FAIL
1813
+ ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
1814
+ return v
1815
+ }
1816
+ }
1817
+ const leading = (bits & FIELD_LEADING_TRIVIA) !== 0
1818
+ const trailing = (bits & FIELD_TRAILING_TRIVIA) !== 0
1798
1819
  return (input, pos, ctx) => {
1820
+ const leadFrom = leading ? LEAD.at : -1
1821
+ LEAD.at = -1
1799
1822
  const v = child(input, pos, ctx)
1800
1823
  if (v === FAIL) return FAIL
1801
- ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
1824
+ const end = EC.e
1825
+ const trailEnd = trailing && ctx.trivia !== undefined ? consumeTrivia(input, end, ctx) : end
1826
+ ctx._fields?.push(triviaFieldCapture(name, v, input, pos, end, leadFrom, trailEnd))
1827
+ EC.e = trailEnd
1802
1828
  return v
1803
1829
  }
1804
1830
  }
@@ -1835,8 +1861,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1835
1861
 
1836
1862
  case OP_WITHCTX: {
1837
1863
  // `extra` and the child are both bound HERE; the piece only swaps a field.
1838
- const extra = k[code[ip + 1]!]
1864
+ const ki = code[ip + 1]!
1839
1865
  const child = link(code[ip + 2]!)
1866
+ if (ki < 0) {
1867
+ const derive = fns[~ki] as (s: unknown) => unknown
1868
+ return (input, pos, ctx) => {
1869
+ const saved = ctx.state
1870
+ ctx.state = derive(saved)
1871
+ try { return child(input, pos, ctx) }
1872
+ finally { ctx.state = saved }
1873
+ }
1874
+ }
1875
+ const extra = k[ki]
1840
1876
  return (input, pos, ctx) => {
1841
1877
  const saved = ctx.state
1842
1878
  ctx.state = extra
@@ -2322,6 +2358,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2322
2358
  const kidIp = code[base + i]!
2323
2359
  if (code[kidIp] !== OP_ADJ) {
2324
2360
  const kid = link(kidIp)
2361
+ if (leadingTriviaFieldAt(code, k, kidIp)) {
2362
+ return (input, cur, ctx, inherited) => {
2363
+ ctx._sync = sync ?? inherited
2364
+ return leadingFieldTerm(step, kid, input, cur, ctx)
2365
+ }
2366
+ }
2325
2367
  return (input, cur, ctx, inherited) => {
2326
2368
  ctx._sync = sync ?? inherited
2327
2369
  return step(kid, input, cur, ctx)
@@ -2389,14 +2431,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2389
2431
  }
2390
2432
  }
2391
2433
 
2434
+ // A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
2435
+ // runner hands it the cursor from BEFORE the scan (`LEAD`, cell.ts).
2392
2436
  let hasAdj = false
2393
- for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
2437
+ for (let i = 1; i < n; i++) {
2438
+ const kidIp = code[base + i]!
2439
+ if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp)) { hasAdj = true; break }
2440
+ }
2394
2441
  if (hasAdj) {
2395
2442
  const first = link(code[base]!)
2396
2443
  const runnerAt = (i: number): TermRunner => {
2397
2444
  const kidIp = code[base + i]!
2398
2445
  if (code[kidIp] !== OP_ADJ) {
2399
2446
  const kid = link(kidIp)
2447
+ if (leadingTriviaFieldAt(code, k, kidIp)) {
2448
+ return (input, cur, ctx) => leadingFieldTerm(step, kid, input, cur, ctx)
2449
+ }
2400
2450
  return (input, cur, ctx) => step(kid, input, cur, ctx)
2401
2451
  }
2402
2452
  const negated = code[kidIp + 1] === 1
@@ -2791,7 +2841,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2791
2841
  if (n === 2 || n === 3) {
2792
2842
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2793
2843
  const a2 = n === 3 ? link(code[base + 2]!) : undefined
2794
- const predecide = !hostCst && !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
2844
+ const predecide = !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
2795
2845
  const p0ip = predecide ? leadingScalarTerminal(code, code[base]!, 2, true, true) : -1
2796
2846
  const p1ip = predecide ? leadingScalarTerminal(code, code[base + 1]!, 2, true, true) : -1
2797
2847
  const p2ip = predecide && n === 3 ? leadingScalarTerminal(code, code[base + 2]!, 2, true, true) : -1
@@ -2958,6 +3008,56 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2958
3008
  )
2959
3009
  }
2960
3010
  const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
3011
+ // LITERAL PRETESTS. First-char dispatch cannot tell `@{…}` or `@@x` from
3012
+ // `@x`, so every arm sharing the char is entered, and under a CST host
3013
+ // each entered node opens a capture frame before failing on the second
3014
+ // char. An arm that must begin with a literal of two or more chars (or
3015
+ // one of a family of them) is tested on the literal first and, on a
3016
+ // miss, skipped exactly as a char-excluded arm is: its static expected
3017
+ // set is the one reported. The test is pure, so a miss leaves no
3018
+ // capture, trivia or field to roll back.
3019
+ const pBit: number[] = []
3020
+ const pRec: ScalarRecognizer[] = []
3021
+ if (!REC && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
3022
+ for (let i = 0; i < n; i++) {
3023
+ for (const terminal of largeChoiceLiteralPretest(code, k, code[base + i]!) ?? []) {
3024
+ pBit.push(1 << i)
3025
+ pRec.push(scalarFor(terminal)!)
3026
+ }
3027
+ }
3028
+ }
3029
+ const pn = pBit.length
3030
+ if (pn !== 0) {
3031
+ return (input, pos, ctx) => {
3032
+ const c = lead(input, pos)
3033
+ const need = markCst(ctx)
3034
+ const mRaw = MRAW
3035
+ const mTl = MTL
3036
+ const mLv = MLV
3037
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
3038
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
3039
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
3040
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
3041
+ if (c < 128) {
3042
+ const bits = mask[c < 0 ? 128 : c]!
3043
+ let tested = 0
3044
+ let hit = 0
3045
+ for (let i = 0; i < pn; i++) {
3046
+ const b = pBit[i]!
3047
+ if ((bits & b) === 0 || (hit & b) !== 0) continue
3048
+ tested |= b
3049
+ if (pRec[i]!(input, pos) >= 0) hit |= b
3050
+ }
3051
+ return masked(
3052
+ input, pos, ctx, bits & (~tested | hit),
3053
+ need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
3054
+ )
3055
+ }
3056
+ return general(
3057
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
3058
+ )
3059
+ }
3060
+ }
2961
3061
  return (input, pos, ctx) => {
2962
3062
  const c = lead(input, pos)
2963
3063
  const need = markCst(ctx)
@@ -3100,6 +3200,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3100
3200
  const max = code[ip + 3]!
3101
3201
  const sepIp = code[ip + 4]!
3102
3202
  const sep = sepIp >= 0 ? link(sepIp) : undefined
3203
+ // A `field(…, { leadingTrivia: true })` separator reads the pre-scan cursor.
3204
+ const sepLeads = sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp)
3103
3205
  const flags = code[ip + 5]!
3104
3206
  const keepSeparators = (flags & 2) !== 0
3105
3207
  const trailingAllowed = (flags & 1) !== 0
@@ -3183,6 +3285,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3183
3285
  let sp = cur
3184
3286
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3185
3287
  ctx._fc = false
3288
+ if (sepLeads) LEAD.at = cur
3186
3289
  const sv = sep(input, sp, ctx)
3187
3290
  if (sv === FAIL) {
3188
3291
  if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
@@ -3283,6 +3386,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3283
3386
  let sp = cur
3284
3387
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3285
3388
  ctx._fc = false
3389
+ if (sepLeads) LEAD.at = cur
3286
3390
  const sv = sep(input, sp, ctx)
3287
3391
  if (sv === FAIL) {
3288
3392
  if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
@@ -3706,20 +3810,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3706
3810
  if (child >= 0) scalarFor(child)
3707
3811
  continue
3708
3812
  }
3709
- if (child >= 0) {
3710
- const spec = k[code[child + 1]!]
3711
- if (code[child] === OP_LIT && typeof spec === 'string' && spec.length >= 2) {
3712
- scalarFor(child)
3713
- continue
3714
- }
3715
- }
3716
- const family = leadingLiteralFamily(code, k, armIp)
3717
- if (family !== undefined && family.length >= 2 && family.every(terminal => {
3718
- const value = k[code[terminal + 1]!]
3719
- return typeof value === 'string' && value.length >= 2
3720
- })) {
3721
- for (const terminal of family) scalarFor(terminal)
3722
- }
3813
+ const literals = largeChoiceLiteralPretest(code, k, armIp)
3814
+ if (literals !== undefined) for (const terminal of literals) scalarFor(terminal)
3815
+ if (literals !== undefined && literals.length === 1 && literals[0] === child) continue
3723
3816
  const sequence = leadingScalarSequence(code, armIp)
3724
3817
  if (sequence !== undefined && (sequence.trivia < 0
3725
3818
  || triviaScan[sequence.trivia] != null)) {
@@ -3767,6 +3860,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3767
3860
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3768
3861
  matchesAt, recoverScan, orSentinel, captureError,
3769
3862
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3863
+ LEAD, triviaFieldCapture,
3770
3864
  )
3771
3865
  emitReached = new Set(pre.reached)
3772
3866
  }
@@ -3848,6 +3942,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3848
3942
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3849
3943
  matchesAt, recoverScan, orSentinel, captureError,
3850
3944
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3945
+ LEAD, triviaFieldCapture,
3851
3946
  )
3852
3947
  emitReached = em.reached
3853
3948
  } catch (e) {
@@ -4067,6 +4162,13 @@ export class AssemblyCache {
4067
4162
  }
4068
4163
  }
4069
4164
 
4165
+ /**
4166
+ * One rule of the map. A lowered terminal is emitted as this call, not as
4167
+ * `tableRules(…)["Entry"]`: `@__PURE__` covers a call and not a property read
4168
+ * after it, so a bundler had to keep the whole table of every terminal nothing
4169
+ * referenced. With the lookup inside the call the statement is droppable.
4170
+ */
4171
+ export function tableRules(source: TableProgram | CompactProgram, entry: string): TableRule
4070
4172
  /**
4071
4173
  * The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
4072
4174
  * through linked closures instead of the bytecode interpreter.
@@ -4076,11 +4178,19 @@ export class AssemblyCache {
4076
4178
  * assembly built for it. Everything past that point is pieces, and no piece body
4077
4179
  * reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
4078
4180
  * carries the argument for why that read is irreducible rather than merely cheap.
4181
+ *
4182
+ * Declared last, so `ReturnType<typeof tableRules>` stays the map.
4079
4183
  */
4080
4184
  export function tableRules(
4081
4185
  source: TableProgram | CompactProgram,
4082
- artifactMetadata: Readonly<Record<symbol, unknown>> = {},
4083
- ): Record<string, TableRule> {
4186
+ artifactMetadata?: Readonly<Record<symbol, unknown>>,
4187
+ ): Record<string, TableRule>
4188
+ export function tableRules(
4189
+ source: TableProgram | CompactProgram,
4190
+ metadataOrEntry: Readonly<Record<symbol, unknown>> | string = {},
4191
+ ): Record<string, TableRule> | TableRule {
4192
+ const entry = typeof metadataOrEntry === 'string' ? metadataOrEntry : undefined
4193
+ const artifactMetadata = typeof metadataOrEntry === 'string' ? {} : metadataOrEntry
4084
4194
  const prog = expandCompact(source)
4085
4195
  const resolved = resolveTable(prog)
4086
4196
  const cache = new AssemblyCache(prog, resolved)
@@ -4105,7 +4215,7 @@ export function tableRules(
4105
4215
  * `runRule`, after the outer read.
4106
4216
  */
4107
4217
  let selected: Assembly | undefined
4108
- return stampRuleMap(prog, {
4218
+ const map = stampRuleMap(prog, {
4109
4219
  runRule: (ri, input, pos, ctx) => {
4110
4220
  const a = selected ?? cache.forCtx(ctx)
4111
4221
  selected = undefined
@@ -4128,4 +4238,8 @@ export function tableRules(
4128
4238
  return a.scanSkip[skipOf?.[ri] ?? -1]
4129
4239
  },
4130
4240
  }, artifactMetadata, resolved)
4241
+ if (entry === undefined) return map
4242
+ // Own properties only: the map inherits `toString`, `constructor` and the rest.
4243
+ if (!Object.hasOwn(map, entry)) throw new Error(`tableRules: the program has no rule ${JSON.stringify(entry)}`)
4244
+ return map[entry]!
4131
4245
  }
@@ -63,10 +63,12 @@ import {
63
63
  computeSiteLabels, reachableSites, type SiteLabel,
64
64
  } from './site-labels.ts'
65
65
  import {
66
- leadingLiteralFamily, leadingScalarSequence, leadingScalarTerminal,
66
+ largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal,
67
67
  scalarTerminalNodeChild, scalarTerminalNotChild, type LeadingScalarSequence,
68
68
  } from './scalar-terminal.ts'
69
69
  import { childSlots } from './child-slots.ts'
70
+ import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
71
+ import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
70
72
 
71
73
  /** What the compiled factory hands back — the emitted twin of `Assembly`. */
72
74
  export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
@@ -122,6 +124,9 @@ export const EMITTED_PARAMS = [
122
124
  'adjacencyHolds',
123
125
  // Appended for selected composite lexical programs; old factories ignore it.
124
126
  'LEXPROG',
127
+ // Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
128
+ // old factories ignore them.
129
+ 'LEAD', 'triviaFieldCapture',
125
130
  ] as const
126
131
 
127
132
  /**
@@ -607,20 +612,12 @@ export function emitAssemblySource(
607
612
  if (child >= 0) scalarSpecs.add(code[child + 1]!)
608
613
  continue
609
614
  }
610
- if (child >= 0) {
611
- const spec = k[code[child + 1]!]
612
- if (code[child] === OP_LIT && typeof spec === 'string' && spec.length >= 2) {
613
- largeChoiceScalarSpecs.add(code[child + 1]!)
614
- continue
615
- }
616
- }
617
- const family = leadingLiteralFamily(code, k, armIp)
618
- if (family !== undefined && family.length >= 2 && family.every(terminal => {
619
- const value = k[code[terminal + 1]!]
620
- return typeof value === 'string' && value.length >= 2
621
- })) {
622
- largeChoiceLiteralFamilies.set(armIp, family)
615
+ const literals = largeChoiceLiteralPretest(code, k, armIp)
616
+ if (literals !== undefined && literals.length === 1 && literals[0] === child) {
617
+ largeChoiceScalarSpecs.add(code[child + 1]!)
618
+ continue
623
619
  }
620
+ if (literals !== undefined) largeChoiceLiteralFamilies.set(armIp, literals)
624
621
  const sequence = leadingScalarSequence(code, armIp)
625
622
  // A labelled trivia scanner is still recognition authority here. This
626
623
  // pretest is pure: on a miss the ordinary arm's trivia log would have
@@ -1307,17 +1304,34 @@ return v
1307
1304
  }
1308
1305
 
1309
1306
  case OP_FIELD: {
1310
- const name = q(k[code[ip + 1]!] as string)
1307
+ const spec = fieldSpec(k[code[ip + 1]!])
1308
+ const name = q(spec.name)
1311
1309
  const child = link(code[ip + 2]!)
1312
1310
  // `ctx._fields?.push(…)` — conditional on a field-reading node being
1313
1311
  // open, exactly as `map.ts` has it, so a field outside one costs a load
1314
1312
  // and a branch. The ARRAY is what `OP_NODE` hands to `buildFieldMap`.
1315
- return `${head}
1313
+ if (spec.bits === 0) {
1314
+ return `${head}
1316
1315
  const v=${child}(input,pos,ctx)
1317
1316
  if(v===FAIL)return FAIL
1318
1317
  const f=ctx._fields
1319
1318
  if(f!==undefined)f.push({name:${name},value:v,span:{start:pos,end:EC.e}})
1320
1319
  return v
1320
+ }`
1321
+ }
1322
+ const leading = (spec.bits & FIELD_LEADING_TRIVIA) !== 0
1323
+ const trailing = (spec.bits & FIELD_TRAILING_TRIVIA) !== 0
1324
+ return `${head}
1325
+ const lf=${leading ? 'LEAD.at' : '-1'}
1326
+ LEAD.at=-1
1327
+ const v=${child}(input,pos,ctx)
1328
+ if(v===FAIL)return FAIL
1329
+ const e=EC.e
1330
+ const te=${trailing ? 'ctx.trivia!==undefined?consumeTrivia(input,e,ctx):e' : 'e'}
1331
+ const f=ctx._fields
1332
+ if(f!==undefined)f.push(triviaFieldCapture(${name},v,input,pos,e,lf,te))
1333
+ EC.e=te
1334
+ return v
1321
1335
  }`
1322
1336
  }
1323
1337
 
@@ -1687,7 +1701,13 @@ return v
1687
1701
  `if(!adjacencyHolds(input,cur,ctx,${negated ? 'true' : 'false'},${kinds})){ctx._fe=cur;ctx._fx=${expected};return FAIL}`,
1688
1702
  `${vn}=null`,
1689
1703
  )
1690
- } else parts.push(emitTerm(kids[i]!, vn, tmp(), L, skipFor(L)))
1704
+ } else {
1705
+ // A leading-trivia field reads the cursor from before the term's scan.
1706
+ // The callee expression hands it over AFTER the scan statement, right
1707
+ // before the call, so a field inside the trivia grammar cannot take it.
1708
+ const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
1709
+ parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L)))
1710
+ }
1691
1711
  names.push(vn)
1692
1712
  }
1693
1713
  parts.push('EC.e=cur')
@@ -2232,7 +2252,7 @@ ${sep !== undefined ? `if(count>0){
2232
2252
  const lb=cstLeavesLen(ctx)
2233
2253
  let sp=cur
2234
2254
  ${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
2235
- const sv=${sep}(input,sp,ctx)
2255
+ ${sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp) ? 'LEAD.at=cur\n' : ''}const sv=${sep}(input,sp,ctx)
2236
2256
  if(sv===FAIL){
2237
2257
  ${rb}
2238
2258
  if(ctx._fc===true)return FAIL