parseman 0.53.0 → 0.54.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 (62) hide show
  1. package/dist/analysis/diagnostics.js +278 -118
  2. package/dist/analysis/diagnostics.js.map +2 -2
  3. package/dist/cli/index.js +278 -118
  4. package/dist/cli/index.js.map +2 -2
  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/compiler/fields.d.ts +2 -0
  12. package/dist/compiler/fields.d.ts.map +1 -1
  13. package/dist/cst/trivia-charscan.d.ts +11 -3
  14. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  15. package/dist/cst/trivia-kinds.d.ts +11 -0
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +284 -120
  20. package/dist/index.js.map +2 -2
  21. package/dist/plugin/index.js +284 -120
  22. package/dist/plugin/index.js.map +2 -2
  23. package/dist/table/assemble.d.ts +9 -0
  24. package/dist/table/assemble.d.ts.map +1 -1
  25. package/dist/table/emit-assembly.d.ts +1 -1
  26. package/dist/table/emit-assembly.d.ts.map +1 -1
  27. package/dist/table/emit.d.ts +6 -2
  28. package/dist/table/emit.d.ts.map +1 -1
  29. package/dist/table/encode.d.ts.map +1 -1
  30. package/dist/table/exec.d.ts.map +1 -1
  31. package/dist/table/field-spec.d.ts +13 -0
  32. package/dist/table/field-spec.d.ts.map +1 -0
  33. package/dist/table/index.js +245 -112
  34. package/dist/table/index.js.map +2 -2
  35. package/dist/table/scalar-terminal.d.ts +11 -0
  36. package/dist/table/scalar-terminal.d.ts.map +1 -1
  37. package/dist/types.d.ts +14 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/src/analysis/duplication.ts +1 -1
  42. package/src/analysis/rebuild.ts +1 -1
  43. package/src/combinators/field-trivia.ts +44 -0
  44. package/src/combinators/map.ts +48 -6
  45. package/src/combinators/repeat.ts +6 -1
  46. package/src/combinators/sequence.ts +9 -1
  47. package/src/compiler/fields.ts +5 -2
  48. package/src/compiler/ir-serialize.ts +7 -1
  49. package/src/coverage.ts +1 -1
  50. package/src/cst/trivia-charscan.ts +17 -56
  51. package/src/cst/trivia-kinds.ts +24 -8
  52. package/src/index.ts +1 -0
  53. package/src/plugin/evaluator.ts +1 -1
  54. package/src/table/assemble.ts +126 -22
  55. package/src/table/emit-assembly.ts +38 -18
  56. package/src/table/emit.ts +15 -4
  57. package/src/table/encode.ts +18 -2
  58. package/src/table/exec.ts +15 -1
  59. package/src/table/field-spec.ts +17 -0
  60. package/src/table/scalar-terminal.ts +25 -0
  61. package/src/types.ts +12 -2
  62. package/src/version.ts +1 -1
@@ -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
  }
@@ -2322,6 +2348,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2322
2348
  const kidIp = code[base + i]!
2323
2349
  if (code[kidIp] !== OP_ADJ) {
2324
2350
  const kid = link(kidIp)
2351
+ if (leadingTriviaFieldAt(code, k, kidIp)) {
2352
+ return (input, cur, ctx, inherited) => {
2353
+ ctx._sync = sync ?? inherited
2354
+ return leadingFieldTerm(step, kid, input, cur, ctx)
2355
+ }
2356
+ }
2325
2357
  return (input, cur, ctx, inherited) => {
2326
2358
  ctx._sync = sync ?? inherited
2327
2359
  return step(kid, input, cur, ctx)
@@ -2389,14 +2421,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2389
2421
  }
2390
2422
  }
2391
2423
 
2424
+ // A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
2425
+ // runner hands it the cursor from BEFORE the scan (`LEAD`, cell.ts).
2392
2426
  let hasAdj = false
2393
- for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
2427
+ for (let i = 1; i < n; i++) {
2428
+ const kidIp = code[base + i]!
2429
+ if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp)) { hasAdj = true; break }
2430
+ }
2394
2431
  if (hasAdj) {
2395
2432
  const first = link(code[base]!)
2396
2433
  const runnerAt = (i: number): TermRunner => {
2397
2434
  const kidIp = code[base + i]!
2398
2435
  if (code[kidIp] !== OP_ADJ) {
2399
2436
  const kid = link(kidIp)
2437
+ if (leadingTriviaFieldAt(code, k, kidIp)) {
2438
+ return (input, cur, ctx) => leadingFieldTerm(step, kid, input, cur, ctx)
2439
+ }
2400
2440
  return (input, cur, ctx) => step(kid, input, cur, ctx)
2401
2441
  }
2402
2442
  const negated = code[kidIp + 1] === 1
@@ -2791,7 +2831,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2791
2831
  if (n === 2 || n === 3) {
2792
2832
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2793
2833
  const a2 = n === 3 ? link(code[base + 2]!) : undefined
2794
- const predecide = !hostCst && !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
2834
+ const predecide = !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
2795
2835
  const p0ip = predecide ? leadingScalarTerminal(code, code[base]!, 2, true, true) : -1
2796
2836
  const p1ip = predecide ? leadingScalarTerminal(code, code[base + 1]!, 2, true, true) : -1
2797
2837
  const p2ip = predecide && n === 3 ? leadingScalarTerminal(code, code[base + 2]!, 2, true, true) : -1
@@ -2958,6 +2998,56 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2958
2998
  )
2959
2999
  }
2960
3000
  const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
3001
+ // LITERAL PRETESTS. First-char dispatch cannot tell `@{…}` or `@@x` from
3002
+ // `@x`, so every arm sharing the char is entered, and under a CST host
3003
+ // each entered node opens a capture frame before failing on the second
3004
+ // char. An arm that must begin with a literal of two or more chars (or
3005
+ // one of a family of them) is tested on the literal first and, on a
3006
+ // miss, skipped exactly as a char-excluded arm is: its static expected
3007
+ // set is the one reported. The test is pure, so a miss leaves no
3008
+ // capture, trivia or field to roll back.
3009
+ const pBit: number[] = []
3010
+ const pRec: ScalarRecognizer[] = []
3011
+ if (!REC && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
3012
+ for (let i = 0; i < n; i++) {
3013
+ for (const terminal of largeChoiceLiteralPretest(code, k, code[base + i]!) ?? []) {
3014
+ pBit.push(1 << i)
3015
+ pRec.push(scalarFor(terminal)!)
3016
+ }
3017
+ }
3018
+ }
3019
+ const pn = pBit.length
3020
+ if (pn !== 0) {
3021
+ return (input, pos, ctx) => {
3022
+ const c = lead(input, pos)
3023
+ const need = markCst(ctx)
3024
+ const mRaw = MRAW
3025
+ const mTl = MTL
3026
+ const mLv = MLV
3027
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
3028
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
3029
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
3030
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
3031
+ if (c < 128) {
3032
+ const bits = mask[c < 0 ? 128 : c]!
3033
+ let tested = 0
3034
+ let hit = 0
3035
+ for (let i = 0; i < pn; i++) {
3036
+ const b = pBit[i]!
3037
+ if ((bits & b) === 0 || (hit & b) !== 0) continue
3038
+ tested |= b
3039
+ if (pRec[i]!(input, pos) >= 0) hit |= b
3040
+ }
3041
+ return masked(
3042
+ input, pos, ctx, bits & (~tested | hit),
3043
+ need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
3044
+ )
3045
+ }
3046
+ return general(
3047
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
3048
+ )
3049
+ }
3050
+ }
2961
3051
  return (input, pos, ctx) => {
2962
3052
  const c = lead(input, pos)
2963
3053
  const need = markCst(ctx)
@@ -3100,6 +3190,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3100
3190
  const max = code[ip + 3]!
3101
3191
  const sepIp = code[ip + 4]!
3102
3192
  const sep = sepIp >= 0 ? link(sepIp) : undefined
3193
+ // A `field(…, { leadingTrivia: true })` separator reads the pre-scan cursor.
3194
+ const sepLeads = sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp)
3103
3195
  const flags = code[ip + 5]!
3104
3196
  const keepSeparators = (flags & 2) !== 0
3105
3197
  const trailingAllowed = (flags & 1) !== 0
@@ -3183,6 +3275,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3183
3275
  let sp = cur
3184
3276
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3185
3277
  ctx._fc = false
3278
+ if (sepLeads) LEAD.at = cur
3186
3279
  const sv = sep(input, sp, ctx)
3187
3280
  if (sv === FAIL) {
3188
3281
  if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
@@ -3283,6 +3376,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3283
3376
  let sp = cur
3284
3377
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3285
3378
  ctx._fc = false
3379
+ if (sepLeads) LEAD.at = cur
3286
3380
  const sv = sep(input, sp, ctx)
3287
3381
  if (sv === FAIL) {
3288
3382
  if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
@@ -3706,20 +3800,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3706
3800
  if (child >= 0) scalarFor(child)
3707
3801
  continue
3708
3802
  }
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
- }
3803
+ const literals = largeChoiceLiteralPretest(code, k, armIp)
3804
+ if (literals !== undefined) for (const terminal of literals) scalarFor(terminal)
3805
+ if (literals !== undefined && literals.length === 1 && literals[0] === child) continue
3723
3806
  const sequence = leadingScalarSequence(code, armIp)
3724
3807
  if (sequence !== undefined && (sequence.trivia < 0
3725
3808
  || triviaScan[sequence.trivia] != null)) {
@@ -3767,6 +3850,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3767
3850
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3768
3851
  matchesAt, recoverScan, orSentinel, captureError,
3769
3852
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3853
+ LEAD, triviaFieldCapture,
3770
3854
  )
3771
3855
  emitReached = new Set(pre.reached)
3772
3856
  }
@@ -3848,6 +3932,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3848
3932
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
3849
3933
  matchesAt, recoverScan, orSentinel, captureError,
3850
3934
  scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
3935
+ LEAD, triviaFieldCapture,
3851
3936
  )
3852
3937
  emitReached = em.reached
3853
3938
  } catch (e) {
@@ -4067,6 +4152,13 @@ export class AssemblyCache {
4067
4152
  }
4068
4153
  }
4069
4154
 
4155
+ /**
4156
+ * One rule of the map. A lowered terminal is emitted as this call, not as
4157
+ * `tableRules(…)["Entry"]`: `@__PURE__` covers a call and not a property read
4158
+ * after it, so a bundler had to keep the whole table of every terminal nothing
4159
+ * referenced. With the lookup inside the call the statement is droppable.
4160
+ */
4161
+ export function tableRules(source: TableProgram | CompactProgram, entry: string): TableRule
4070
4162
  /**
4071
4163
  * The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
4072
4164
  * through linked closures instead of the bytecode interpreter.
@@ -4076,11 +4168,19 @@ export class AssemblyCache {
4076
4168
  * assembly built for it. Everything past that point is pieces, and no piece body
4077
4169
  * reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
4078
4170
  * carries the argument for why that read is irreducible rather than merely cheap.
4171
+ *
4172
+ * Declared last, so `ReturnType<typeof tableRules>` stays the map.
4079
4173
  */
4080
4174
  export function tableRules(
4081
4175
  source: TableProgram | CompactProgram,
4082
- artifactMetadata: Readonly<Record<symbol, unknown>> = {},
4083
- ): Record<string, TableRule> {
4176
+ artifactMetadata?: Readonly<Record<symbol, unknown>>,
4177
+ ): Record<string, TableRule>
4178
+ export function tableRules(
4179
+ source: TableProgram | CompactProgram,
4180
+ metadataOrEntry: Readonly<Record<symbol, unknown>> | string = {},
4181
+ ): Record<string, TableRule> | TableRule {
4182
+ const entry = typeof metadataOrEntry === 'string' ? metadataOrEntry : undefined
4183
+ const artifactMetadata = typeof metadataOrEntry === 'string' ? {} : metadataOrEntry
4084
4184
  const prog = expandCompact(source)
4085
4185
  const resolved = resolveTable(prog)
4086
4186
  const cache = new AssemblyCache(prog, resolved)
@@ -4105,7 +4205,7 @@ export function tableRules(
4105
4205
  * `runRule`, after the outer read.
4106
4206
  */
4107
4207
  let selected: Assembly | undefined
4108
- return stampRuleMap(prog, {
4208
+ const map = stampRuleMap(prog, {
4109
4209
  runRule: (ri, input, pos, ctx) => {
4110
4210
  const a = selected ?? cache.forCtx(ctx)
4111
4211
  selected = undefined
@@ -4128,4 +4228,8 @@ export function tableRules(
4128
4228
  return a.scanSkip[skipOf?.[ri] ?? -1]
4129
4229
  },
4130
4230
  }, artifactMetadata, resolved)
4231
+ if (entry === undefined) return map
4232
+ // Own properties only: the map inherits `toString`, `constructor` and the rest.
4233
+ if (!Object.hasOwn(map, entry)) throw new Error(`tableRules: the program has no rule ${JSON.stringify(entry)}`)
4234
+ return map[entry]!
4131
4235
  }
@@ -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
package/src/table/emit.ts CHANGED
@@ -352,12 +352,16 @@ export type ExpressionEmitOptions = EmitOptions & {
352
352
  *
353
353
  * `null` is what a `rules()` call site needs: its initialiser evaluates to the
354
354
  * `{ name: fn, … }` object, exactly what `tableRules(…)` already returns, so
355
- * the map form is the expression WITHOUT the trailing index — not a second
355
+ * the map form is the expression WITHOUT the entry argument — not a second
356
356
  * emitter. `compileRuleMap`'s replacement has the same shape and the same
357
357
  * splice point, which is what makes the two lowerings interchangeable there.
358
358
  */
359
359
  readonly entry?: string | null
360
- /** Static metadata object passed to `tableRules` as its second argument. */
360
+ /**
361
+ * Static metadata object passed to `tableRules` as its second argument. Only
362
+ * the rule-map form (`entry: null`) takes it; with an entry, that argument is
363
+ * the entry's name.
364
+ */
361
365
  readonly metadataSource?: string
362
366
  }
363
367
 
@@ -379,11 +383,18 @@ export function emitTableExpression(prog: TableProgram, opts: ExpressionEmitOpti
379
383
  const entry = opts.entry === undefined ? 'grammar' : opts.entry
380
384
  const ref = opts.runtimeRef ?? 'tableRules'
381
385
  const fns = opts.fnSources ?? prog.fns.map(() => '() => {}')
382
- const close = `}${opts.metadataSource === undefined ? '' : `,${opts.metadataSource}`})`
386
+ // The entry is an ARGUMENT, never a trailing `[entry]`: `@__PURE__` covers the
387
+ // call and not a property read after it, so `tableRules({…})["Entry"]` left an
388
+ // unreferenced terminal's whole table in every bundle.
389
+ const meta = opts.metadataSource
390
+ if (entry !== null && meta !== undefined) {
391
+ throw new Error('emitTableExpression: metadataSource is for the rule-map form (entry: null); a single entry carries none')
392
+ }
393
+ const tail = entry !== null ? `,${jsString(entry)}` : meta === undefined ? '' : `,${meta}`
383
394
  return [
384
395
  `/* @__PURE__ */ ${ref}({`,
385
396
  ...programFields(prog, fns, opts),
386
- entry === null ? close : `${close}[${jsString(entry)}]`,
397
+ `}${tail})`,
387
398
  ].join('\n')
388
399
  }
389
400
 
@@ -7,6 +7,7 @@ import { deriveExpected } from '../combinators/expect.ts'
7
7
  import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
8
8
  import { buildReadsRaw, buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
9
9
  import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
10
+ import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
10
11
  import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
11
12
  import {
12
13
  OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
@@ -555,6 +556,17 @@ class Encoder {
555
556
  return i
556
557
  }
557
558
 
559
+ private fieldSpecIndex = new Map<string, number>()
560
+ private fieldSpecSlot(name: string, bits: number): number {
561
+ const key = JSON.stringify([name, bits])
562
+ const hit = this.fieldSpecIndex.get(key)
563
+ if (hit !== undefined) return hit
564
+ const i = this.k.length
565
+ this.k.push([name, bits])
566
+ this.fieldSpecIndex.set(key, i)
567
+ return i
568
+ }
569
+
558
570
  /**
559
571
  * A `notAdjacent({ kinds })` filter, pooled by its CONTENTS.
560
572
  *
@@ -1518,8 +1530,12 @@ class Encoder {
1518
1530
  for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
1519
1531
  return head
1520
1532
  }
1521
- case 'field':
1522
- return this.emit(OP_FIELD, this.constant(d.name), this.node(d.parser).ip)
1533
+ case 'field': {
1534
+ // A trivia-attributing field carries `[name, bits]` in the name slot
1535
+ // (`FIELD_LEADING_TRIVIA`/`FIELD_TRAILING_TRIVIA`); a plain one the name.
1536
+ const bits = (d.leadingTrivia === true ? FIELD_LEADING_TRIVIA : 0) | (d.trailingTrivia === true ? FIELD_TRAILING_TRIVIA : 0)
1537
+ return this.emit(OP_FIELD, bits === 0 ? this.constant(d.name) : this.fieldSpecSlot(d.name, bits), this.node(d.parser).ip)
1538
+ }
1523
1539
  case 'lazy': {
1524
1540
  // A named reference is not a hop: it resolves to the target's row.
1525
1541
  // Emitting a trampoline here cost one dispatch per reference for nothing.
package/src/table/exec.ts CHANGED
@@ -72,6 +72,8 @@ import { scalarTerminalNotChild } from './scalar-terminal.ts'
72
72
  * a successful match to the other engine.
73
73
  */
74
74
  import { FAIL, newEndCell, type EndCell } from './cell.ts'
75
+ import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
76
+ import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
75
77
 
76
78
  /**
77
79
  * DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
@@ -645,11 +647,21 @@ function makeDriver(
645
647
  }
646
648
 
647
649
  case OP_FIELD: {
650
+ const spec = fieldSpec(k[code[ip + 1]!])
651
+ const leadFrom = LEAD.at
652
+ if (spec.bits !== 0) LEAD.at = -1
648
653
  const v = exec(code[ip + 2]!, input, pos, ctx)
649
654
  if (v === FAIL) return FAIL
650
655
  // Conditional on a live sink, exactly as src/combinators/map.ts has it:
651
656
  // a `field()` outside any field-reading node costs nothing.
652
- ctx._fields?.push({ name: k[code[ip + 1]!] as string, value: v, span: { start: pos, end: EC.e } })
657
+ if (spec.bits === 0) {
658
+ ctx._fields?.push({ name: spec.name, value: v, span: { start: pos, end: EC.e } })
659
+ return v
660
+ }
661
+ const end = EC.e
662
+ const trailEnd = (spec.bits & FIELD_TRAILING_TRIVIA) !== 0 && ctx.trivia !== undefined ? consumeTrivia(input, end, ctx) : end
663
+ ctx._fields?.push(triviaFieldCapture(spec.name, v, input, pos, end, (spec.bits & FIELD_LEADING_TRIVIA) !== 0 ? leadFrom : -1, trailEnd))
664
+ EC.e = trailEnd
653
665
  return v
654
666
  }
655
667
 
@@ -885,6 +897,7 @@ function makeDriver(
885
897
  const scanTl = need ? cstTlLen(ctx) : 0
886
898
  const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
887
899
  const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
900
+ if (leadingTriviaFieldAt(code, k, child)) LEAD.at = cur
888
901
  const v = exec(child, input, scanEnd, ctx)
889
902
  if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
890
903
  if (EC.e > scanEnd) cur = EC.e
@@ -1256,6 +1269,7 @@ function makeDriver(
1256
1269
  let sp = cur
1257
1270
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
1258
1271
  ctx._fc = false
1272
+ if (leadingTriviaFieldAt(code, k, sep)) LEAD.at = cur
1259
1273
  const sv = exec(sep, input, sp, ctx)
1260
1274
  if (sv === FAIL) {
1261
1275
  if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
@@ -0,0 +1,17 @@
1
+ import { OP_FIELD } from './ops.ts'
2
+ import { FIELD_LEADING_TRIVIA } from '../combinators/field-trivia.ts'
3
+
4
+ /**
5
+ * An `OP_FIELD` name slot: the name, or `[name, bits]` for a field that
6
+ * attributes trivia (`FIELD_LEADING_TRIVIA` / `FIELD_TRAILING_TRIVIA`). A
7
+ * program printed before those options existed carries only names, and reads
8
+ * as bits 0.
9
+ */
10
+ export function fieldSpec(slot: unknown): { readonly name: string; readonly bits: number } {
11
+ return Array.isArray(slot) ? { name: slot[0] as string, bits: slot[1] as number } : { name: slot as string, bits: 0 }
12
+ }
13
+
14
+ /** Is the instruction at `ip` a `field(…, { leadingTrivia: true })`? */
15
+ export function leadingTriviaFieldAt(code: ArrayLike<number>, k: readonly unknown[], ip: number): boolean {
16
+ return code[ip] === OP_FIELD && (fieldSpec(k[code[ip + 1]!]).bits & FIELD_LEADING_TRIVIA) !== 0
17
+ }
@@ -261,3 +261,28 @@ export function leadingLiteralFamily(
261
261
 
262
262
  return walk(ip, 0)
263
263
  }
264
+
265
+ /**
266
+ * The literal terminals an arm of a LARGE ordered choice (more than three arms)
267
+ * may be pretested on before it is entered, or `undefined` when it has none.
268
+ *
269
+ * A pretest runs again when the arm then matches, so a large choice only takes
270
+ * literals of two or more characters: one `startsWith` is the price of skipping an
271
+ * arm that first-char dispatch could not exclude (`@{` and `@@` beside `@name`).
272
+ * A one-character literal is exactly what the dispatch mask already decided.
273
+ * The emitted and the closure engines both select through this function.
274
+ */
275
+ export function largeChoiceLiteralPretest(
276
+ code: ArrayLike<number>, constants: readonly unknown[], armIp: number,
277
+ ): readonly number[] | undefined {
278
+ const child = leadingScalarTerminal(code, armIp, 2, true, true)
279
+ if (child >= 0) {
280
+ const spec = constants[code[child + 1]!]
281
+ if (code[child] === OP_LIT && typeof spec === 'string' && spec.length >= 2) return [child]
282
+ }
283
+ const family = leadingLiteralFamily(code, constants, armIp)
284
+ return family !== undefined && family.length >= 2 && family.every(terminal => {
285
+ const value = constants[code[terminal + 1]!]
286
+ return typeof value === 'string' && value.length >= 2
287
+ }) ? family : undefined
288
+ }
package/src/types.ts CHANGED
@@ -83,7 +83,7 @@ export type ParserDef =
83
83
  | { tag: 'routed'; fallback?: Combinator<unknown> }
84
84
  | { tag: 'leaf'; parser: Combinator<unknown>; fn: (v: unknown, span: { start: number; end: number }) => unknown; fnSrc?: string; cstValue?: 'source' }
85
85
  | { tag: 'label'; label: string; parser: Combinator<unknown> }
86
- | { tag: 'field'; name: string; parser: Combinator<unknown> }
86
+ | { tag: 'field'; name: string; parser: Combinator<unknown>; leadingTrivia?: true; trailingTrivia?: true }
87
87
  | { tag: 'grammar'; parser: Combinator<unknown>; triviaParser: Combinator<unknown> | undefined; clearTrivia?: boolean; captureTrivia?: boolean; rootCapture?: 'opaque'; trackLines: boolean; constructionTrackLines?: 'on' | 'off' | 'inherit'; constructionCaptureTriviaKinds?: readonly string[] }
88
88
  | { tag: 'lazy'; thunk: () => Combinator<unknown> }
89
89
  | { tag: 'not'; parser: Combinator<unknown> }
@@ -203,6 +203,16 @@ export type BuildHost = ((
203
203
  export type FieldCapture<T = unknown> = {
204
204
  value: T
205
205
  span: Span
206
+ /**
207
+ * `field(…, { leadingTrivia: true })` only: the source text of the trivia the
208
+ * enclosing sequence or separated list skipped right before this field, or `''`.
209
+ */
210
+ leading?: string
211
+ /**
212
+ * `field(…, { trailingTrivia: true })` only: the source text of the trivia the
213
+ * field consumed after its match, or `''`.
214
+ */
215
+ trailing?: string
206
216
  }
207
217
 
208
218
  export type FieldMap = Record<string, FieldCapture | FieldCapture[]>
@@ -382,7 +392,7 @@ export type ParseContext = {
382
392
  */
383
393
  _cstTriviaLog?: number[] | undefined
384
394
  /** Framework-internal: active node() field captures, enabled only when needed. */
385
- _fields?: Array<{ name: string; value: unknown; span: Span }> | undefined
395
+ _fields?: Array<{ name: string; value: unknown; span: Span; leading?: string; trailing?: string }> | undefined
386
396
  /** Framework-internal: lazy capture buffer for active node() parse. */
387
397
  _cstBuf?: CstCaptureBuf | undefined
388
398
  /** Framework-internal: value/span already consumed by an enclosing dispatch(). */
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.53.0'
16
+ export const PARSEMAN_VERSION = '0.54.0'