parseman 0.52.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 (108) hide show
  1. package/dist/analysis/diagnostics.js +636 -126
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +6191 -6274
  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/parser.d.ts +34 -0
  10. package/dist/combinators/parser.d.ts.map +1 -1
  11. package/dist/combinators/ref.d.ts +0 -13
  12. package/dist/combinators/ref.d.ts.map +1 -1
  13. package/dist/combinators/repeat.d.ts.map +1 -1
  14. package/dist/combinators/sequence.d.ts.map +1 -1
  15. package/dist/compiler/borrowed-rules.d.ts +30 -0
  16. package/dist/compiler/borrowed-rules.d.ts.map +1 -0
  17. package/dist/compiler/compile-linkable-table.d.ts +1 -1
  18. package/dist/compiler/fields.d.ts +2 -0
  19. package/dist/compiler/fields.d.ts.map +1 -1
  20. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  21. package/dist/compiler/ir-serialize.d.ts +10 -4
  22. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  23. package/dist/compiler/linker.d.ts +48 -91
  24. package/dist/compiler/linker.d.ts.map +1 -1
  25. package/dist/coverage.d.ts +1 -1
  26. package/dist/coverage.d.ts.map +1 -1
  27. package/dist/cst/reflection.d.ts.map +1 -1
  28. package/dist/cst/trivia-charscan.d.ts +11 -3
  29. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  30. package/dist/cst/trivia-kinds.d.ts +11 -0
  31. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +764 -888
  35. package/dist/index.js.map +3 -3
  36. package/dist/language-service/index.js.map +1 -1
  37. package/dist/plugin/evaluator.d.ts +38 -2
  38. package/dist/plugin/evaluator.d.ts.map +1 -1
  39. package/dist/plugin/index.d.ts.map +1 -1
  40. package/dist/plugin/index.js +1443 -729
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/plugin/ir-eval.d.ts +21 -0
  43. package/dist/plugin/ir-eval.d.ts.map +1 -0
  44. package/dist/plugin/reducer-resolver.d.ts +6 -0
  45. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  46. package/dist/spec/index.js +555 -6109
  47. package/dist/spec/index.js.map +3 -3
  48. package/dist/spec/railroad-lib.d.ts +7 -2
  49. package/dist/spec/railroad-lib.d.ts.map +1 -1
  50. package/dist/spec/railroad.d.ts.map +1 -1
  51. package/dist/table/assemble.d.ts +9 -0
  52. package/dist/table/assemble.d.ts.map +1 -1
  53. package/dist/table/compile-rule-map.d.ts +11 -4
  54. package/dist/table/compile-rule-map.d.ts.map +1 -1
  55. package/dist/table/compile.d.ts +16 -0
  56. package/dist/table/compile.d.ts.map +1 -1
  57. package/dist/table/emit-assembly.d.ts +1 -1
  58. package/dist/table/emit-assembly.d.ts.map +1 -1
  59. package/dist/table/emit.d.ts +6 -2
  60. package/dist/table/emit.d.ts.map +1 -1
  61. package/dist/table/encode.d.ts.map +1 -1
  62. package/dist/table/exec.d.ts.map +1 -1
  63. package/dist/table/field-spec.d.ts +13 -0
  64. package/dist/table/field-spec.d.ts.map +1 -0
  65. package/dist/table/index.js +480 -199
  66. package/dist/table/index.js.map +2 -2
  67. package/dist/table/scalar-terminal.d.ts +11 -0
  68. package/dist/table/scalar-terminal.d.ts.map +1 -1
  69. package/dist/types.d.ts +14 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/version.d.ts +1 -1
  72. package/package.json +1 -1
  73. package/src/analysis/duplication.ts +1 -1
  74. package/src/analysis/rebuild.ts +1 -1
  75. package/src/combinators/field-trivia.ts +44 -0
  76. package/src/combinators/map.ts +48 -6
  77. package/src/combinators/parser.ts +133 -67
  78. package/src/combinators/ref.ts +10 -1
  79. package/src/combinators/repeat.ts +6 -1
  80. package/src/combinators/sequence.ts +9 -1
  81. package/src/compiler/borrowed-rules.ts +88 -0
  82. package/src/compiler/compile-linkable-table.ts +1 -1
  83. package/src/compiler/fields.ts +5 -2
  84. package/src/compiler/grammar-coverage-ids.ts +35 -6
  85. package/src/compiler/ir-serialize.ts +19 -119
  86. package/src/compiler/linker.ts +190 -666
  87. package/src/coverage.ts +7 -5
  88. package/src/cst/reflection.ts +6 -1
  89. package/src/cst/trivia-charscan.ts +17 -56
  90. package/src/cst/trivia-kinds.ts +24 -8
  91. package/src/index.ts +1 -0
  92. package/src/plugin/evaluator.ts +114 -14
  93. package/src/plugin/index.ts +588 -110
  94. package/src/plugin/ir-eval.ts +165 -0
  95. package/src/plugin/reducer-resolver.ts +20 -3
  96. package/src/spec/railroad-lib.ts +18 -8
  97. package/src/spec/railroad.ts +3 -4
  98. package/src/table/assemble.ts +126 -22
  99. package/src/table/compile-rule-map.ts +18 -8
  100. package/src/table/compile.ts +61 -0
  101. package/src/table/emit-assembly.ts +38 -18
  102. package/src/table/emit.ts +15 -4
  103. package/src/table/encode.ts +18 -2
  104. package/src/table/exec.ts +15 -1
  105. package/src/table/field-spec.ts +17 -0
  106. package/src/table/scalar-terminal.ts +25 -0
  107. package/src/types.ts +12 -2
  108. package/src/version.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  import type { Combinator } from '../types.ts'
2
2
  import { hasDirectBuildDef } from '../analysis/commitment.ts'
3
3
  import type { HostMode } from '../cst/host-mode.ts'
4
- import { collectGrammarReflection, type GrammarReflection } from '../cst/reflection.ts'
4
+ import { collectGrammarReflection, GRAMMAR_REFLECTION, type GrammarReflection } from '../cst/reflection.ts'
5
5
  import { encodeTableProgram, type TableSettings } from './encode.ts'
6
6
  import { defaultAssemblyCfgs, emitTableExpression } from './emit.ts'
7
7
  import { tableRules } from './assemble.ts'
@@ -54,6 +54,12 @@ const DEFAULT_PRECOMPILE_MIN_WORDS = 1_024
54
54
  export type TableRuleMapOptions = {
55
55
  /** Grammar-level ambient trivia, as `rules({ trivia }, …)` declares it. */
56
56
  readonly trivia?: Combinator<unknown>
57
+ /**
58
+ * `compileRuleMapRunnable` only: specialise the runnable table once with generated
59
+ * code (falling back to closures under CSP). `compile()` alone passes it — no other
60
+ * runtime path may construct code.
61
+ */
62
+ readonly specialise?: boolean
57
63
  /** Grammar-level ambient scan-skip, as `rules({ scanSkip }, …)` declares it. */
58
64
  readonly scanSkip?: Combinator<unknown>[]
59
65
  readonly recovery?: boolean
@@ -120,10 +126,11 @@ export type CompiledRuleMapTable = {
120
126
  * hands back text and nothing else, because its artifact only exists once the
121
127
  * emitted source is evaluated. A table exists as data before it is printed, so
122
128
  * the same call that produces the replacement can also hand back the parser.
123
- * It may use runtime assembly specialisation; `prog` and `replacement` remain
124
- * serialized closure artifacts. That is what makes a differential against the
125
- * interpreter possible without `eval`, and it is why the table-vs-interpreter
126
- * test can compare `expected` sets rather than only accept/reject.
129
+ * It runs the CLOSURE artifact — the engine printed output runs — so building
130
+ * it never constructs code: only `compile()` specialises (`specialise`), per
131
+ * docs/design/runtime-and-size-contract.md, rule 1. That is what makes a
132
+ * differential against the interpreter possible without `eval`, and it is why the
133
+ * table-vs-interpreter test can compare `expected` sets rather than only accept/reject.
127
134
  */
128
135
  rules: Record<string, TableRule>
129
136
  /** The closure-stamped wire program, for a caller that wants to fold or inspect it. */
@@ -232,13 +239,16 @@ export function compileRuleMapRunnable(
232
239
  if (encoded === null) return null
233
240
  const { prog, hostMode, plan } = encoded
234
241
  const artifact = closureArtifact(prog)
242
+ const reflection = collectGrammarReflection(ruleMap)
235
243
  return {
236
244
  keys: ruleMap.map(([key]) => key),
237
245
  hostMode,
238
246
  hostBranchElided: hostMode === 'ast' && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
239
- reflection: collectGrammarReflection(ruleMap),
247
+ reflection,
240
248
  ...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
241
- rules: tableRules(prog),
249
+ // Reflection rides on the map's metadata prototype from birth. Only `compile()`
250
+ // asks to specialise; every other caller runs the closure artifact.
251
+ rules: tableRules(opts.specialise === true ? prog : artifact, { [GRAMMAR_REFLECTION]: reflection }),
242
252
  prog: artifact,
243
253
  }
244
254
  }
@@ -349,7 +359,7 @@ export function compileRuleMap(
349
359
  hostBranchElided: hostMode === 'ast' && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
350
360
  reflection: collectGrammarReflection(ruleMap),
351
361
  ...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
352
- rules: tableRules(prog),
362
+ rules: tableRules(artifact),
353
363
  prog: artifact,
354
364
  }
355
365
  }
@@ -8,6 +8,9 @@ import { closureArtifact } from './program.ts'
8
8
  import { buildGrammarPlan } from '../compiler/grammar-coverage-ids.ts'
9
9
  import { runDuplicationDiagnostic, type DuplicationOption } from './duplication-hook.ts'
10
10
  import { beginCompileDegradationDrain } from '../compiler/degradation.ts'
11
+ import { compileRuleMapRunnable } from './compile-rule-map.ts'
12
+ import { borrowedRuleMessage, borrowedRuleReference } from '../compiler/borrowed-rules.ts'
13
+ import type { TableRule } from './program.ts'
11
14
 
12
15
  /**
13
16
  * `compile()` FOR THE TABLE LOWERING — same contract, different artifact.
@@ -110,7 +113,65 @@ export type TableCompileOptions = {
110
113
  readonly fnSources?: readonly string[]
111
114
  }
112
115
 
116
+ /**
117
+ * Compile a whole RULE MAP — a `rules()` grammar or a runtime `compose()` result —
118
+ * into a map of runnable rules that `run()` / `parseDoc()` take like any other.
119
+ *
120
+ * This is how a caller opts a runtime composition into speed: `compose()` only
121
+ * links an interpreter grammar, and `compile()` encodes it to one table and
122
+ * specialises that table once. Where the environment forbids generated code (a
123
+ * Content-Security-Policy without `'unsafe-eval'`) the specialisation throws
124
+ * `EvalError` and the same table runs on the closure assembler instead.
125
+ */
126
+ export function compile<T>(combinator: Combinator<T>, mapFnSources?: readonly string[], opts?: TableCompileOptions): CompiledParser<T>
127
+ export function compile(grammar: Record<string, unknown> & { readonly _def?: never }, opts?: { readonly hostMode?: HostMode }): Record<string, TableRule>
113
128
  export function compile<T>(
129
+ target: Combinator<T> | Record<string, unknown>,
130
+ second?: readonly string[] | { readonly hostMode?: HostMode },
131
+ third?: TableCompileOptions,
132
+ ): CompiledParser<T> | Record<string, TableRule> {
133
+ if (!isCombinator(target)) return compileMap(target, second as { readonly hostMode?: HostMode } | undefined)
134
+ return compileRoot(target as Combinator<T>, second as readonly string[] | undefined, third)
135
+ }
136
+
137
+ function isCombinator(x: unknown): x is Combinator<unknown> {
138
+ return typeof x === 'object' && x !== null
139
+ && typeof (x as { parse?: unknown }).parse === 'function' && '_def' in x
140
+ }
141
+
142
+ function compileMap(grammar: Record<string, unknown>, opts: { readonly hostMode?: HostMode } = {}): Record<string, TableRule> {
143
+ const entries: Array<[string, Combinator<unknown>]> = []
144
+ for (const [key, value] of Object.entries(grammar)) {
145
+ if (!isCombinator(value)) throw new TypeError(`compile: grammar entry "${key}" is not a parser`)
146
+ // A referenced-but-undefined `g.X` is a hole, not a rule: it has nothing to encode.
147
+ if (value._def.tag === 'lazy') {
148
+ try { value._def.thunk() } catch { continue }
149
+ }
150
+ entries.push([key, value])
151
+ }
152
+ // A runtime composition was already checked when it linked; a plain map is checked
153
+ // here, because this encoder binds references by name.
154
+ const borrowed = borrowedRuleReference(Object.fromEntries(entries))
155
+ if (borrowed !== undefined) throw new Error(borrowedRuleMessage('compile', borrowed))
156
+ // One aggregated degradation block per compile, exactly as `compileRoot` drains it.
157
+ const drain = beginCompileDegradationDrain()
158
+ let done = false
159
+ try {
160
+ const refusals: string[] = []
161
+ const compiled = compileRuleMapRunnable(entries, {
162
+ ...(opts.hostMode ? { hostMode: opts.hostMode } : {}),
163
+ refusals,
164
+ specialise: true,
165
+ })
166
+ if (compiled === null) {
167
+ throw new Error(`compile: this grammar could not be encoded to a table${refusals.length ? ` — ${refusals.join('; ')}` : ''}`)
168
+ }
169
+ done = true
170
+ return compiled.rules
171
+ } finally { drain(done) }
172
+ }
173
+
174
+ function compileRoot<T>(
114
175
  combinator: Combinator<T>,
115
176
  mapFnSources?: readonly string[],
116
177
  opts: TableCompileOptions = {},
@@ -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.52.0'
16
+ export const PARSEMAN_VERSION = '0.54.0'