parseman 0.51.1 → 0.52.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 (64) hide show
  1. package/dist/analysis/commitment.d.ts.map +1 -1
  2. package/dist/analysis/diagnostics.js +101 -35
  3. package/dist/analysis/diagnostics.js.map +1 -1
  4. package/dist/cli/index.js +103 -37
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/combinators/attempt.d.ts +11 -1
  7. package/dist/combinators/attempt.d.ts.map +1 -1
  8. package/dist/combinators/map.d.ts.map +1 -1
  9. package/dist/combinators/node.d.ts +6 -2
  10. package/dist/combinators/node.d.ts.map +1 -1
  11. package/dist/combinators/token.d.ts +8 -8
  12. package/dist/combinators/token.d.ts.map +1 -1
  13. package/dist/compiler/token-capability.js +4 -0
  14. package/dist/compiler/token-capability.js.map +1 -1
  15. package/dist/coverage.d.ts.map +1 -1
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +107 -40
  20. package/dist/index.js.map +1 -1
  21. package/dist/plugin/evaluator.d.ts.map +1 -1
  22. package/dist/plugin/index.js +107 -41
  23. package/dist/plugin/index.js.map +1 -1
  24. package/dist/spec/index.js +55 -10
  25. package/dist/spec/index.js.map +1 -1
  26. package/dist/table/assemble.d.ts.map +1 -1
  27. package/dist/table/emit-assembly.d.ts.map +1 -1
  28. package/dist/table/encode-baseline.d.ts.map +1 -1
  29. package/dist/table/exec-baseline.d.ts.map +1 -1
  30. package/dist/table/exec.d.ts.map +1 -1
  31. package/dist/table/index.js +404 -371
  32. package/dist/table/index.js.map +2 -2
  33. package/dist/table/ops.d.ts +9 -5
  34. package/dist/table/ops.d.ts.map +1 -1
  35. package/dist/table/site-labels.d.ts.map +1 -1
  36. package/dist/types.d.ts +9 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/src/analysis/commitment.ts +3 -1
  41. package/src/analysis/duplication.ts +1 -1
  42. package/src/analysis/rebuild.ts +3 -3
  43. package/src/combinators/attempt.ts +26 -3
  44. package/src/combinators/first-set.ts +5 -1
  45. package/src/combinators/map.ts +5 -1
  46. package/src/combinators/node.ts +12 -3
  47. package/src/combinators/token.ts +46 -3
  48. package/src/compiler/ir-serialize.ts +5 -5
  49. package/src/coverage.ts +3 -4
  50. package/src/cst/trivia-kinds.ts +6 -0
  51. package/src/index.ts +2 -1
  52. package/src/plugin/evaluator.ts +8 -5
  53. package/src/table/assemble.ts +30 -11
  54. package/src/table/child-slots.ts +2 -2
  55. package/src/table/emit-assembly.ts +13 -6
  56. package/src/table/encode-baseline.ts +8 -3
  57. package/src/table/encode.ts +16 -16
  58. package/src/table/exec-baseline.ts +30 -4
  59. package/src/table/exec.ts +14 -5
  60. package/src/table/ops.ts +10 -6
  61. package/src/table/scalar-terminal.ts +5 -5
  62. package/src/table/site-labels.ts +5 -3
  63. package/src/types.ts +9 -2
  64. package/src/version.ts +1 -1
@@ -80,7 +80,7 @@ import {
80
80
  type CstCaptureBuf,
81
81
  } from '../cst/capture-buffer.ts'
82
82
  import {
83
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
83
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
84
84
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
85
85
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
86
86
  OP_LIVE,
@@ -523,8 +523,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
523
523
  * exact AST workloads execute the three shapes below 42k/66k/205k times per
524
524
  * CSS/Less/generated parse. Keep their bodies scalar and keep every other
525
525
  * shape on the generic oracle below.
526
+ *
527
+ * `trailing` is the node's `trailingTrivia` bit: consume the active trivia
528
+ * after a successful body, inside the node's span, exactly where the generic
529
+ * body does. It opens no capture: these shapes are the reducers that read no
530
+ * trivia log.
526
531
  */
527
- function plainBuildNode(child: Piece, build: NodeBuilder): Piece {
532
+ function plainBuildNode(child: Piece, build: NodeBuilder, trailing: boolean): Piece {
528
533
  return (input, pos, ctx) => {
529
534
  const sCh = ctx._cstChildren
530
535
  const sLv = ctx._cstLeaves
@@ -542,6 +547,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
542
547
  ctx.captureTrivia = false
543
548
  ctx._fields = undefined
544
549
  const value = child(input, pos, ctx)
550
+ if (trailing && value !== FAIL && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
545
551
  const kids = buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
546
552
  const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
547
553
  ctx._fields = savedFields
@@ -578,7 +584,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
578
584
  /** Direct reducer whose declared arity proves `rawChildren` is unobservable.
579
585
  * Use the existing split children/leaves collector instead of opening a
580
586
  * duplicate raw capture buffer. The body shape is selected while linking. */
581
- function childrenOnlyBuildNode(child: Piece, build: NodeBuilder): Piece {
587
+ function childrenOnlyBuildNode(child: Piece, build: NodeBuilder, trailing: boolean): Piece {
582
588
  return (input, pos, ctx) => {
583
589
  const sCh = ctx._cstChildren
584
590
  const sLv = ctx._cstLeaves
@@ -596,6 +602,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
596
602
  ctx.captureTrivia = false
597
603
  ctx._fields = undefined
598
604
  const value = child(input, pos, ctx)
605
+ if (trailing && value !== FAIL && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
599
606
  const captured = capturedFlatChildren(kids)
600
607
  ctx._fields = savedFields
601
608
  ctx._cstBuf = sBuf
@@ -1912,6 +1919,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1912
1919
 
1913
1920
  case OP_ATTEMPT: {
1914
1921
  const child = link(code[ip + 1]!)
1922
+ const contain = code[ip + 2] === 1
1915
1923
  return (input, pos, ctx) => {
1916
1924
  const need = markCst(ctx)
1917
1925
  const mRaw = MRAW
@@ -1924,8 +1932,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1924
1932
  const v = child(input, pos, ctx)
1925
1933
  if (v !== FAIL) return v
1926
1934
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1927
- // A committed failure propagates VERBATIM — rolled back, not re-anchored.
1928
- if (committed(ctx)) return FAIL
1935
+ // A committed failure propagates VERBATIM — rolled back, not re-anchored —
1936
+ // unless the row contains commitment, where it is reported like any other.
1937
+ if (committed(ctx)) {
1938
+ if (!contain) return FAIL
1939
+ ctx._fc = false
1940
+ }
1929
1941
  ctx._fe = pos
1930
1942
  return FAIL
1931
1943
  }
@@ -2149,7 +2161,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2149
2161
  }
2150
2162
  }
2151
2163
 
2152
- case OP_LEAF: {
2164
+ case OP_LEAF:
2165
+ case OP_SOURCE_LEAF: {
2153
2166
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
2154
2167
  const child = link(code[ip + 2]!)
2155
2168
  return (input, pos, ctx) => {
@@ -2179,7 +2192,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2179
2192
  if (v === FAIL) return FAIL
2180
2193
  const end = EC.e
2181
2194
  const out = fn(v, { start: pos, end })
2182
- if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
2195
+ if (wasCapturing) pushCstLeaf(ctx, {
2196
+ _tag: 'leaf',
2197
+ value: op === OP_SOURCE_LEAF && hostCst ? input.slice(pos, end) : out,
2198
+ span: { start: pos, end },
2199
+ })
2183
2200
  EC.e = end
2184
2201
  return out
2185
2202
  }
@@ -3457,11 +3474,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3457
3474
  // data plus this assembly's fixed option set; direct/custom contexts and
3458
3475
  // every richer node shape retain the generic implementation below.
3459
3476
  if (!hostCst && !tracked) {
3460
- if (build !== undefined && proj < 0 && flags === 0) {
3461
- return plainBuildNode(child, build)
3477
+ // The trailing-trivia bit (128) rides on the two leanest shapes; it adds
3478
+ // a consume, not a capture.
3479
+ if (build !== undefined && proj < 0 && (flags & ~128) === 0) {
3480
+ return plainBuildNode(child, build, trailingTrivia)
3462
3481
  }
3463
- if (build !== undefined && proj < 0 && flags === 2) {
3464
- return childrenOnlyBuildNode(child, build)
3482
+ if (build !== undefined && proj < 0 && (flags & ~128) === 2) {
3483
+ return childrenOnlyBuildNode(child, build, trailingTrivia)
3465
3484
  }
3466
3485
  if (build !== undefined && proj < 0 && flags === 18) {
3467
3486
  return childrenOnlyFieldsBuildNode(child, build)
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  OP_ADJ, OP_ARMGATE, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT,
3
- OP_FIELD, OP_GATE, OP_GREEDY, OP_GUARD, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
3
+ OP_FIELD, OP_GATE, OP_GREEDY, OP_GUARD, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI,
4
4
  OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_LIVE, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT,
5
5
  OP_PEEK, OP_REJECT, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCAN,
6
6
  OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_WITHCTX, OP_XFORM,
@@ -55,7 +55,7 @@ export function childSlots(code: ArrayLike<number>, ip: number, out: number[]):
55
55
 
56
56
  // Single child at `ip + 2`.
57
57
  case OP_GATE: case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM:
58
- case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_ARMGATE:
58
+ case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE:
59
59
  out.push(code[ip + 2]!)
60
60
  return true
61
61
 
@@ -48,7 +48,7 @@ import type { ParseContext } from '../types.ts'
48
48
  import { regexCanMatchEmpty } from '../regex/first-set.ts'
49
49
  import {
50
50
  OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
51
- OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
51
+ OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
52
52
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
53
53
  OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
54
54
  OP_LEX_BODY, OP_LEX_PROGRAM,
@@ -1411,8 +1411,10 @@ ${rootCap ? 'ctx._rootTriviaCapture=sR\n' : ''}return v
1411
1411
  /* ── boundaries ──────────────────────────────────────────────────────── */
1412
1412
 
1413
1413
  case OP_TOKEN:
1414
- case OP_LEAF: {
1414
+ case OP_LEAF:
1415
+ case OP_SOURCE_LEAF: {
1415
1416
  const isToken = op === OP_TOKEN
1417
+ const capturesSource = isToken || (op === OP_SOURCE_LEAF && hostCst)
1416
1418
  const fn = isToken ? undefined : fnRef(code[ip + 1]!)
1417
1419
  const child = link(isToken ? code[ip + 1]! : code[ip + 2]!)
1418
1420
  // `try`/`finally`, AS `assemble.ts` HAS IT. `OP_SCOPE` restores linearly
@@ -1460,7 +1462,7 @@ ctx._triviaLog=sOtl
1460
1462
  if(v===FAIL)return FAIL
1461
1463
  const e=EC.e
1462
1464
  const out=${isToken ? 'input.slice(pos,e)' : `${fn}(v,{start:pos,end:e})`}
1463
- if(wasCap)pushCstLeaf(ctx,{_tag:'leaf',value:out,span:{start:pos,end:e}})
1465
+ if(wasCap)pushCstLeaf(ctx,{_tag:'leaf',value:${capturesSource ? 'input.slice(pos,e)' : 'out'},span:{start:pos,end:e}})
1464
1466
  EC.e=e
1465
1467
  return out
1466
1468
  }`
@@ -1550,7 +1552,7 @@ ${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1550
1552
  const v=${child}(input,pos,ctx)
1551
1553
  if(v!==FAIL)return v
1552
1554
  ${clean ? '' : emitRollback(p, L.buf, L.raw, sinks)}
1553
- if(ctx._fc===true)return FAIL
1555
+ ${code[ip + 2] === 1 ? 'ctx._fc=false' : 'if(ctx._fc===true)return FAIL'}
1554
1556
  ctx._fe=pos
1555
1557
  return FAIL
1556
1558
  }`
@@ -2332,8 +2334,11 @@ return nd
2332
2334
  // not closure-only semantics. Print the same body for a precompiled
2333
2335
  // assembly so a large composeLeaf artifact does not fall back to opening
2334
2336
  // the generic raw/trivia buffer that its reducer arity proved unread.
2337
+ // The trailing-trivia bit (128) rides on the children-only shape as it
2338
+ // does in the closure assembler: a consume after the body, no capture.
2339
+ const leanFlags = flags & ~128
2335
2340
  if (!hostCst && !tracked && build !== undefined && proj < 0
2336
- && (flags === 2 || flags === 18 || flags === 34)) {
2341
+ && (leanFlags === 2 || (flags === 18 || flags === 34))) {
2337
2342
  const fields = flags === 18
2338
2343
  const collapseChildren = flags === 34
2339
2344
  const publish = L.buf && L.raw === RAW_OMIT
@@ -2380,7 +2385,9 @@ ctx._cstTriviaLog=undefined
2380
2385
  ctx.captureTrivia=false
2381
2386
  ctx._fields=${fields ? '[]' : 'undefined'}
2382
2387
  const v=${child}(input,pos,ctx)
2383
- const captured=_capturedFlatChildren(flat)
2388
+ ${trailingTrivia && L.tri !== TRI_NONE
2389
+ ? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
2390
+ : ''}const captured=_capturedFlatChildren(flat)
2384
2391
  ${fields ? 'const fieldMap=buildFieldMap(ctx._fields)\n' : ''}ctx._fields=sFields
2385
2392
  ctx._cstBuf=sBuf
2386
2393
  ctx._cstChildren=sCh
@@ -3,7 +3,7 @@ import { firstSetOf, matchesEmpty } from '../combinators/first-set.ts'
3
3
  import { deriveExpected } from '../combinators/expect.ts'
4
4
  import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
5
5
  import {
6
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
6
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
7
7
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
8
8
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_EXPECT,
9
9
  } from './ops.ts'
@@ -265,7 +265,7 @@ class Encoder {
265
265
  }
266
266
  case 'leaf': {
267
267
  const child = this.node(d.parser).ip
268
- return this.emit(OP_LEAF, this.fn(d.fn), child)
268
+ return this.emit(d.cstValue === 'source' ? OP_SOURCE_LEAF : OP_LEAF, this.fn(d.fn), child)
269
269
  }
270
270
  case 'node': {
271
271
  if (d.unwrap || d.collapse || d.project !== undefined) throw new UnsupportedConstructBaseline('node(unwrap|collapse|project)')
@@ -295,9 +295,13 @@ class Encoder {
295
295
  return this.emit(OP_NOT, this.node(d.parser).ip)
296
296
  case 'peek':
297
297
  return this.emit(OP_PEEK, this.node(d.parser).ip)
298
+ // A contained transaction changes what an enclosing choice does next, so
299
+ // it cannot be transparent.
300
+ case 'attempt':
301
+ if (d.contain === true) throw new UnsupportedConstructBaseline('attempt(contain)')
302
+ return this.node(d.parser).ip
298
303
  // Transparent wrappers: no row of their own, no dispatch at run time.
299
304
  case 'token':
300
- case 'attempt':
301
305
  case 'label':
302
306
  case 'trivia':
303
307
  return this.node(d.parser).ip
@@ -319,6 +323,7 @@ class Encoder {
319
323
  code: this.code, k: this.k, fns: this.fns, cc: this.cc,
320
324
  fx: this.fx, disp: this.disp, dsp: [], rules: this.rules,
321
325
  lines: this.track ? 1 : 0,
326
+ ...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
322
327
  }
323
328
  }
324
329
  }
@@ -9,7 +9,7 @@ import { buildReadsRaw, buildReadsState, buildReadsTrivia } from '../compiler/bu
9
9
  import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
10
10
  import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
11
11
  import {
12
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
12
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
13
13
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
14
14
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
15
15
  OP_LIVE, OP_ATTEMPT, OP_LABEL,
@@ -1322,7 +1322,7 @@ class Encoder {
1322
1322
  }
1323
1323
  case 'leaf': {
1324
1324
  const child = this.node(d.parser).ip
1325
- return this.emit(OP_LEAF, this.fn(d.fn, d.fnSrc ?? null), child)
1325
+ return this.emit(d.cstValue === 'source' ? OP_SOURCE_LEAF : OP_LEAF, this.fn(d.fn, d.fnSrc ?? null), child)
1326
1326
  }
1327
1327
  case 'node': {
1328
1328
  // A node legally has NO builder when its value comes from a selection:
@@ -1344,24 +1344,24 @@ class Encoder {
1344
1344
  // 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
1345
1345
  // driver reads a bit; it re-derives nothing and sees no setting.
1346
1346
  const cstOut = this.settings.hostMode === 'cst'
1347
- // THE THREE DERIVED CAPTURE BITS, mirroring `node.ts:215` term for term:
1347
+ // THE THREE DERIVED CAPTURE BITS, mirroring `node.ts` term for term:
1348
1348
  //
1349
- // capturesTrivia = captureTrivia || trailingTrivia
1349
+ // capturesTrivia = captureTrivia
1350
1350
  // || (build ? buildReadsTrivia(def) : project === undefined)
1351
1351
  //
1352
1352
  // `cstOut` is the static stand-in for "a CST host is coming", which the
1353
1353
  // interpreter reaches dynamically off `ctx.build`.
1354
1354
  //
1355
- // Two of these terms were MISSING and each was a real divergence:
1355
+ // `captureTrivia` was once MISSING here and was a real divergence: an
1356
+ // explicit request the arity analysis cannot express (an author can ask
1357
+ // for capture on a 3-argument reducer). This used to REFUSE rather than
1358
+ // lower, so a documented option no grammar could use through the table.
1356
1359
  //
1357
- // `captureTrivia` — an explicit request the arity analysis cannot
1358
- // express (an author can ask for capture on a 3-argument reducer).
1359
- // This used to REFUSE rather than lower, so a documented option no
1360
- // grammar could use through the table.
1361
- // `trailingTrivia` — trivia consumed INSIDE the node's capture scope
1362
- // lands in THIS node's log. The interpreter counts it; the table did
1363
- // not, so a node with `trailingTrivia` and a non-trivia-reading
1364
- // reducer captured under the interpreter and dropped under the table.
1360
+ // `trailingTrivia` is NOT a capture term (bit 128 only). It moves the
1361
+ // node's end past the trivia after the body; that trivia reaches the root
1362
+ // table with or without a node log, and only a log the reducer reads is
1363
+ // worth opening. Forcing bit 4 for it put every match of a
1364
+ // non-trivia-reading node on the generic capture path.
1365
1365
  //
1366
1366
  // The `build ? … : project === undefined` split matters now that a
1367
1367
  // STRUCTURAL node lowers: with no builder the interpreter captures unless
@@ -1374,7 +1374,7 @@ class Encoder {
1374
1374
  // Field capture additionally requires the body to CONTAIN `field()`
1375
1375
  // captures: a node that reads fields but has none allocates nothing.
1376
1376
  const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
1377
- const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
1377
+ const flags = (cstOut || d.captureTrivia === true || derivedTrivia ? 4 : 0)
1378
1378
  | (!cstOut && omitsRaw ? 2 : 0)
1379
1379
  | (cstOut || derivedState ? 8 : 0)
1380
1380
  | (wantsFields ? 16 : 0)
@@ -1596,7 +1596,7 @@ class Encoder {
1596
1596
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1597
1597
  // lowering was correct only for a choice arm.
1598
1598
  case 'attempt': {
1599
- const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
1599
+ const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip, d.contain === true ? 1 : 0)
1600
1600
  if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
1601
1601
  // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
1602
1602
  // case already uses — it is the same guard, written twice in the
@@ -1737,7 +1737,7 @@ class Encoder {
1737
1737
  switch (this.code[ip]) {
1738
1738
  case OP_GATE: return [ip + 2]
1739
1739
  case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
1740
- case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1740
+ case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_SOURCE_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1741
1741
  case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
1742
1742
  case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
1743
1743
  // ARMS START AT ip+4. `ip+3` is the choice's own EXPECTED-SET index, and
@@ -5,7 +5,7 @@ import {
5
5
  pushCstLeaf, rollbackCstCapture, saveCstMark, type CstRollbackMark,
6
6
  } from '../cst/capture-buffer.ts'
7
7
  import {
8
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
8
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
9
9
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
10
10
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_EXPECT,
11
11
  } from './ops.ts'
@@ -94,6 +94,7 @@ function makeDriver(
94
94
  cc: readonly ResolvedClass[],
95
95
  fx: readonly (readonly string[])[],
96
96
  disp: readonly ResolvedDispatch[],
97
+ hostCst: boolean,
97
98
  ): Driver {
98
99
  /** Shared end-position out-parameter (`_pfEnd` in emitted code). */
99
100
  let END = 0
@@ -434,12 +435,37 @@ function makeDriver(
434
435
  return fn(v, { start: pos, end: END })
435
436
  }
436
437
 
437
- case OP_LEAF: {
438
- const v = exec(code[ip + 2]!, input, pos, ctx)
438
+ case OP_LEAF:
439
+ case OP_SOURCE_LEAF: {
440
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
441
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
442
+ const sOuterTl = ctx._triviaLog
443
+ const wasCapturing = cstCaptureActive(ctx)
444
+ ctx._cstBuf = undefined
445
+ ctx._cstChildren = undefined
446
+ ctx._cstLeaves = undefined
447
+ ctx._cstRawChildren = undefined
448
+ ctx._cstTriviaLog = undefined
449
+ ctx._triviaLog = undefined
450
+ let v: unknown
451
+ try { v = exec(code[ip + 2]!, input, pos, ctx) }
452
+ finally {
453
+ ctx._cstBuf = sBuf
454
+ ctx._cstChildren = sChildren
455
+ ctx._cstLeaves = sLeaves
456
+ ctx._cstRawChildren = sRaw
457
+ ctx._cstTriviaLog = sTl
458
+ ctx._triviaLog = sOuterTl
459
+ }
439
460
  if (v === FAIL) return FAIL
440
461
  const end = END
441
462
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
442
463
  const out = fn(v, { start: pos, end })
464
+ if (wasCapturing) pushCstLeaf(ctx, {
465
+ _tag: 'leaf',
466
+ value: code[ip] === OP_SOURCE_LEAF && hostCst ? input.slice(pos, end) : out,
467
+ span: { start: pos, end },
468
+ })
443
469
  END = end
444
470
  return out
445
471
  }
@@ -509,7 +535,7 @@ function makeDriver(
509
535
  export function execRulesBaseline(source: TableProgram | CompactProgram): Record<string, TableRule> {
510
536
  const prog = expandCompact(source)
511
537
  const t = resolveTable(prog)
512
- const d = makeDriver(t.code, t.k, t.fns, t.cc, t.fx, t.disp)
538
+ const d = makeDriver(t.code, t.k, t.fns, t.cc, t.fx, t.disp, prog.hostMode === 'cst')
513
539
  const out: Record<string, TableRule> = {}
514
540
  // Chosen ONCE, from table data, at rule-map construction. Not a per-parse
515
541
  // branch on an option: a plain table never has this wrapper at all.
package/src/table/exec.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  demoteCapturedToRaw, endCstNodeCapture, pushCstChild, pushCstLeaf,
16
16
  } from '../cst/capture-buffer.ts'
17
17
  import {
18
- OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
18
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
19
19
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
20
20
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
21
21
  OP_LIVE,
@@ -1371,7 +1371,8 @@ function makeDriver(
1371
1371
  return fn(v, { start: pos, end: EC.e })
1372
1372
  }
1373
1373
 
1374
- case OP_LEAF: {
1374
+ case OP_LEAF:
1375
+ case OP_SOURCE_LEAF: {
1375
1376
  // Mirrors src/combinators/token.ts:89-127. `leaf()` is a CAPTURE
1376
1377
  // BOUNDARY: it suppresses the interior's own CST captures and exposes
1377
1378
  // exactly ONE leaf carrying the reducer's value. Running the interior
@@ -1410,7 +1411,11 @@ function makeDriver(
1410
1411
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1411
1412
  if (COUNT) siteFn('LEAF fn()', fn)
1412
1413
  const out = fn(v, { start: pos, end })
1413
- if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
1414
+ if (wasCapturing) pushCstLeaf(ctx, {
1415
+ _tag: 'leaf',
1416
+ value: code[ip] === OP_SOURCE_LEAF && HOSTCST ? input.slice(pos, end) : out,
1417
+ span: { start: pos, end },
1418
+ })
1414
1419
  EC.e = end
1415
1420
  return out
1416
1421
  }
@@ -1530,7 +1535,8 @@ function makeDriver(
1530
1535
  // `attempt()` verbatim: mark, run, and on failure restore every capture
1531
1536
  // sink and re-anchor the report at the transaction's entry. A COMMITTED
1532
1537
  // failure is still rolled back and then propagated untouched — the
1533
- // interpreter returns `result` itself on that branch.
1538
+ // interpreter returns `result` itself on that branch — unless the row
1539
+ // contains commitment (`k`), where it is reported like any other failure.
1534
1540
  const need = rollbackNeeded(ctx)
1535
1541
  const mRaw = need ? cstRawLen(ctx) : 0
1536
1542
  const mTl = need ? cstTlLen(ctx) : 0
@@ -1542,7 +1548,10 @@ function makeDriver(
1542
1548
  const v = exec(code[ip + 1]!, input, pos, ctx)
1543
1549
  if (v !== FAIL) return v
1544
1550
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1545
- if (ctx._fc === true) return FAIL
1551
+ if (ctx._fc === true) {
1552
+ if (code[ip + 2] !== 1) return FAIL
1553
+ ctx._fc = false
1554
+ }
1546
1555
  ctx._fe = pos
1547
1556
  return FAIL
1548
1557
  }
package/src/table/ops.ts CHANGED
@@ -378,7 +378,9 @@ export const OP_ARMGATE = 36
378
378
  */
379
379
  export const OP_LIVE = 37
380
380
  /**
381
- * `ATTEMPT c` — `attempt()`, the transactional ordered-choice arm.
381
+ * `ATTEMPT c k` — `attempt()`, the transactional ordered-choice arm. `k` is 1
382
+ * for `attempt(p, { contain: true })`: a committed inner failure is reported
383
+ * uncommitted — `_fc` cleared and `_fe` re-anchored, like any other failure.
382
384
  *
383
385
  * WAS A TRANSPARENT WRAPPER (`case 'attempt': return this.node(d.parser).ip`),
384
386
  * which is correct for exactly one placement — an arm of an `OP_CHOICE`, whose
@@ -389,10 +391,10 @@ export const OP_LIVE = 37
389
391
  * re-anchored at the transaction's entry. Both are `attempt()`'s whole contract
390
392
  * (`combinators/attempt.ts`), not a choice-arm detail.
391
393
  *
392
- * One child operand and no expected-set operand: on a non-committed inner
393
- * failure the row keeps the inner's `_fx` VERBATIM and only re-anchors `_fe` to
394
- * `pos`, exactly as `attempt.ts` returns `{ expected: result.expected, span:
395
- * { start: pos, end: pos } }`. A committed failure propagates untouched — the
394
+ * No expected-set operand: on a non-committed inner failure the row keeps the
395
+ * inner's `_fx` VERBATIM and only re-anchors `_fe` to `pos`, exactly as
396
+ * `attempt.ts` returns `{ expected: result.expected, span: { start: pos, end:
397
+ * pos } }`. A committed failure propagates untouched when `k` is 0 — the
396
398
  * rollback still happens, the re-anchor does not.
397
399
  *
398
400
  * The interpreter's first-set fail-fast guard is NOT lowered. It is an
@@ -469,6 +471,8 @@ export const OP_SCOPE_PLAIN = 41
469
471
  export const OP_LEX_BODY = 42
470
472
  /** `LEX_PROGRAM p` — selected fixed composite lexical body, childless. */
471
473
  export const OP_LEX_PROGRAM = 43
474
+ /** `SOURCE_LEAF f c` — `sourceLeaf()`: reduce semantically, capture source text. */
475
+ export const OP_SOURCE_LEAF = 44
472
476
  /**
473
477
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
474
478
  *
@@ -492,7 +496,7 @@ export const OP_NAMES: Record<number, string> = {
492
496
  [OP_LIT]: 'LIT', [OP_RX]: 'RX', [OP_SEQ]: 'SEQ', [OP_SEQV]: 'SEQV',
493
497
  [OP_CHOICE]: 'CHOICE', [OP_REP]: 'REP', [OP_REPV]: 'REPV', [OP_OPT]: 'OPT',
494
498
  [OP_XFORM]: 'XFORM', [OP_NODE]: 'NODE', [OP_RULE]: 'RULE', [OP_GATE]: 'GATE',
495
- [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_EMPTY]: 'EMPTY',
499
+ [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_EMPTY]: 'EMPTY',
496
500
  [OP_LIT_TRACK]: 'LIT_TRACK', [OP_RX_TRACK]: 'RX_TRACK', [OP_NODE_TRACK]: 'NODE_TRACK',
497
501
  [OP_SCOPE]: 'SCOPE', [OP_EXPECT]: 'EXPECT', [OP_SEQX]: 'SEQX', [OP_SCAN]: 'SCAN',
498
502
  [OP_FIELD]: 'FIELD', [OP_LIT_CI]: 'LIT_CI', [OP_LIT_CI_TRACK]: 'LIT_CI_TRACK', [OP_DISPATCH]: 'DISPATCH', [OP_ROUTED]: 'ROUTED',
@@ -1,5 +1,5 @@
1
1
  import {
2
- OP_ATTEMPT, OP_CHOICE, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_NODE, OP_NOT,
2
+ OP_ATTEMPT, OP_CHOICE, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT,
3
3
  OP_PEEK, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV,
4
4
  OP_SEQX, OP_TOKEN, OP_XFORM,
5
5
  } from './ops.ts'
@@ -86,7 +86,7 @@ export function leadingScalarTerminal(
86
86
  continue
87
87
  }
88
88
  if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
89
- || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
89
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || (op === OP_LEAF || op === OP_SOURCE_LEAF)) {
90
90
  at = code[at + 2]!
91
91
  depth++
92
92
  continue
@@ -139,7 +139,7 @@ export function leadingScalarSequence(
139
139
  continue
140
140
  }
141
141
  if (op === OP_GATE || op === OP_SCOPE_PLAIN
142
- || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
142
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || (op === OP_LEAF || op === OP_SOURCE_LEAF)) {
143
143
  at = code[at + 2]!
144
144
  continue
145
145
  }
@@ -173,7 +173,7 @@ export function leadingScalarSequence(
173
173
  at = code[at + 2]!
174
174
  continue
175
175
  }
176
- if (op === OP_GATE || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
176
+ if (op === OP_GATE || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || (op === OP_LEAF || op === OP_SOURCE_LEAF)) {
177
177
  at = code[at + 2]!
178
178
  continue
179
179
  }
@@ -220,7 +220,7 @@ export function leadingLiteralFamily(
220
220
  return walk(code[at + 1]!, depth + 1)
221
221
  }
222
222
  if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
223
- || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
223
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || (op === OP_LEAF || op === OP_SOURCE_LEAF)) {
224
224
  return walk(code[at + 2]!, depth + 1)
225
225
  }
226
226
  if (op === OP_SEQ || op === OP_SEQV) {
@@ -60,7 +60,7 @@
60
60
  * label licenses at a leaf is dropping the TEST — never the capture.
61
61
  */
62
62
  import {
63
- OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF,
63
+ OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF,
64
64
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE,
65
65
  OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
66
66
  } from './ops.ts'
@@ -159,8 +159,10 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
159
159
  }
160
160
  if (op === OP_NODE || op === OP_NODE_TRACK) {
161
161
  const flags = code[ip + 3]!
162
+ // The same selection `emit-assembly.ts` makes: the trailing-trivia bit (128)
163
+ // keeps the children-only shape.
162
164
  const directChildren = !hostCst && op === OP_NODE && code[ip + 1]! >= 0 && code[ip + 4]! < 0
163
- && (flags === 2 || flags === 18 || flags === 34)
165
+ && ((flags & ~128) === 2 || flags === 18 || flags === 34)
164
166
  if (directChildren) {
165
167
  // The confirmed low-arity builder projection installs split child/leaf
166
168
  // arrays instead of a CstCaptureBuf. Descendants still capture, but they
@@ -193,7 +195,7 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
193
195
  if (at.tri === TRI_NONE && !at.buf) return at
194
196
  return { tri: TRI_NONE, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
195
197
  }
196
- if (op === OP_LEAF) {
198
+ if (op === OP_LEAF || op === OP_SOURCE_LEAF) {
197
199
  if (!at.buf) return at
198
200
  return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
199
201
  }
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { LabeledTriviaSpec } from './cst/trivia-kinds.ts'
2
+
1
3
  export type Span = {
2
4
  start: number
3
5
  end: number
@@ -56,7 +58,7 @@ export type ParserDef =
56
58
  // closures live in `gates`.
57
59
  | { tag: 'choice'; parsers: Combinator<unknown>[]; gates: (((state: unknown) => boolean) | null)[]; gateSrcs?: (string | null)[]; disjoint: boolean; strategy: ChoiceStrategy; autoNot: (AutoNotCheck[] | null)[] }
58
60
  | { tag: 'dispatch'; selector: Combinator<string>; cases: readonly DispatchCase[]; matchers?: readonly DispatchMatcherCase[] | undefined; otherwise?: Combinator<unknown> | undefined; otherwiseUsesRouted?: boolean | undefined }
59
- | { tag: 'attempt'; parser: Combinator<unknown> }
61
+ | { tag: 'attempt'; parser: Combinator<unknown>; /** `attempt(p, { contain: true })`: a committed failure inside is reported uncommitted. */ contain?: true }
60
62
  // The TAG carries NULLABILITY (what every downstream switch keys on): `many` is
61
63
  // the nullable min-0 repeat, `oneOrMore` the non-nullable min>=1 one. `min`/`max`
62
64
  // carry the actual ITEM bounds (`many(x, { min: 3, max: 8 })` is a `oneOrMore`
@@ -79,7 +81,7 @@ export type ParserDef =
79
81
  // selector matched). It exists so ONE production can serve both contexts instead of
80
82
  // being spelled twice — a `routed()` twin and a concrete-lead original.
81
83
  | { tag: 'routed'; fallback?: Combinator<unknown> }
82
- | { tag: 'leaf'; parser: Combinator<unknown>; fn: (v: unknown, span: { start: number; end: number }) => unknown; fnSrc?: string }
84
+ | { tag: 'leaf'; parser: Combinator<unknown>; fn: (v: unknown, span: { start: number; end: number }) => unknown; fnSrc?: string; cstValue?: 'source' }
83
85
  | { tag: 'label'; label: string; parser: Combinator<unknown> }
84
86
  | { tag: 'field'; name: string; parser: Combinator<unknown> }
85
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[] }
@@ -433,6 +435,11 @@ export type ParserMeta = {
433
435
  isTrivia: boolean
434
436
  /** User-defined labels for labeled trivia arms (`label(name, parser)`). */
435
437
  triviaKindLabels?: readonly string[]
438
+ /**
439
+ * Set only by `trivia()`: the labeled-arm spec of this trivia (`null` when it is
440
+ * not labeled), built once so a labeled scan never re-derives it.
441
+ */
442
+ labeledTriviaSpec?: LabeledTriviaSpec | null
436
443
  /** Preclassified lightweight trivia scanner for the ordinary skip path. */
437
444
  triviaScanner?: ((input: string, cur: number) => number) | null
438
445
  /** Set only by `classifiedTrivia()`: each root-visible category is a separate
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.51.1'
16
+ export const PARSEMAN_VERSION = '0.52.0'