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
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.
@@ -1576,6 +1592,11 @@ class Encoder {
1576
1592
  this.expected([adjacencyExpected(d)]),
1577
1593
  )
1578
1594
  case 'withCtx': {
1595
+ // The DERIVE form is an author callback, parked in `fns` like a gate
1596
+ // predicate and told apart from a const-pool index by its sign.
1597
+ if (typeof d.extra === 'function') {
1598
+ return this.emit(OP_WITHCTX, ~this.fn(d.extra, d.extraSrc ?? null), this.node(d.parser).ip)
1599
+ }
1579
1600
  // `extra` is arbitrary user data in the const pool. `emitConst` takes
1580
1601
  // scalars, arrays of scalars, and plain objects of those — anything
1581
1602
  // richer (a class instance, a function value) RUNS fine but cannot be
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
 
@@ -791,7 +803,8 @@ function makeDriver(
791
803
 
792
804
  case OP_WITHCTX: {
793
805
  const saved = ctx.state
794
- ctx.state = k[code[ip + 1]!]
806
+ const ki = code[ip + 1]!
807
+ ctx.state = ki >= 0 ? k[ki] : (fns[~ki] as (s: unknown) => unknown)(saved)
795
808
  try { return exec(code[ip + 2]!, input, pos, ctx) }
796
809
  finally { ctx.state = saved }
797
810
  }
@@ -885,6 +898,7 @@ function makeDriver(
885
898
  const scanTl = need ? cstTlLen(ctx) : 0
886
899
  const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
887
900
  const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
901
+ if (leadingTriviaFieldAt(code, k, child)) LEAD.at = cur
888
902
  const v = exec(child, input, scanEnd, ctx)
889
903
  if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
890
904
  if (EC.e > scanEnd) cur = EC.e
@@ -1256,6 +1270,7 @@ function makeDriver(
1256
1270
  let sp = cur
1257
1271
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
1258
1272
  ctx._fc = false
1273
+ if (leadingTriviaFieldAt(code, k, sep)) LEAD.at = cur
1259
1274
  const sv = exec(sep, input, sp, ctx)
1260
1275
  if (sv === FAIL) {
1261
1276
  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
+ }
package/src/table/ops.ts CHANGED
@@ -217,7 +217,9 @@ export const OP_TOKEN = 29
217
217
  */
218
218
  export const OP_SCOPE_CAP = 30
219
219
  /**
220
- * `WITHCTX k c` — `withCtx(extra, c)`. `k` is `extra` in the const pool.
220
+ * `WITHCTX k c` — `withCtx(extra, c)`. `k >= 0` is `extra` in the const pool
221
+ * (the replace form); `k < 0` is the derive form, `~k` indexing the author's
222
+ * `(state) => state` in `prog.fns`, called once per entry with the outer state.
221
223
  *
222
224
  * SAVE / RESTORE, matching `withCtx.ts`. Both used to CLONE the context, which
223
225
  * scoped far more than the state: every scalar the child wrote on `ctx` landed
@@ -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> }
@@ -102,6 +102,8 @@ export type ParserDef =
102
102
  // asserts that something did. `kinds` (notAdjacent only) narrows the assertion to
103
103
  // trivia CATEGORIES from `classifiedTrivia({...})`. See combinators/adjacency.ts.
104
104
  | { tag: 'adjacency'; polarity: 'adjacent' | 'notAdjacent'; kinds?: readonly string[] }
105
+ // `extra` is the subtree's state (replace form) or, when it is a function, the
106
+ // `(state) => state` that derives it from the state in force at entry.
105
107
  | { tag: 'withCtx'; extra: unknown; parser: Combinator<unknown>; extraSrc?: string }
106
108
  | { tag: 'recover'; parser: Combinator<unknown>; sentinel: Combinator<unknown> }
107
109
  | { tag: 'expect'; parser: Combinator<unknown>; label: string | undefined; expected: string[] }
@@ -203,6 +205,16 @@ export type BuildHost = ((
203
205
  export type FieldCapture<T = unknown> = {
204
206
  value: T
205
207
  span: Span
208
+ /**
209
+ * `field(…, { leadingTrivia: true })` only: the source text of the trivia the
210
+ * enclosing sequence or separated list skipped right before this field, or `''`.
211
+ */
212
+ leading?: string
213
+ /**
214
+ * `field(…, { trailingTrivia: true })` only: the source text of the trivia the
215
+ * field consumed after its match, or `''`.
216
+ */
217
+ trailing?: string
206
218
  }
207
219
 
208
220
  export type FieldMap = Record<string, FieldCapture | FieldCapture[]>
@@ -382,7 +394,7 @@ export type ParseContext = {
382
394
  */
383
395
  _cstTriviaLog?: number[] | undefined
384
396
  /** Framework-internal: active node() field captures, enabled only when needed. */
385
- _fields?: Array<{ name: string; value: unknown; span: Span }> | undefined
397
+ _fields?: Array<{ name: string; value: unknown; span: Span; leading?: string; trailing?: string }> | undefined
386
398
  /** Framework-internal: lazy capture buffer for active node() parse. */
387
399
  _cstBuf?: CstCaptureBuf | undefined
388
400
  /** 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.55.0'