parseman 0.59.2 → 0.60.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 (118) hide show
  1. package/dist/analysis/commitment.d.ts +1 -1
  2. package/dist/analysis/diagnostics.js +877 -191
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +877 -191
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/completions.d.ts +3 -3
  7. package/dist/combinators/completions.d.ts.map +1 -1
  8. package/dist/combinators/dispatch.d.ts +13 -1
  9. package/dist/combinators/dispatch.d.ts.map +1 -1
  10. package/dist/combinators/first-set.d.ts +35 -2
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/gate.d.ts +1 -1
  13. package/dist/combinators/regex.d.ts.map +1 -1
  14. package/dist/combinators/trivia-boundary.d.ts +11 -0
  15. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  16. package/dist/combinators/trivia-skip.d.ts +1 -1
  17. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  18. package/dist/compiler/compile-linkable-table.d.ts +2 -3
  19. package/dist/compiler/compile-linkable-table.d.ts.map +1 -1
  20. package/dist/compiler/linker.d.ts.map +1 -1
  21. package/dist/compiler/token-alphabet.d.ts +2 -0
  22. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  23. package/dist/compiler/token-capability.js +65 -53
  24. package/dist/compiler/token-capability.js.map +2 -2
  25. package/dist/cst/host-mode.d.ts +1 -1
  26. package/dist/cst/root-trivia-scope.d.ts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +897 -200
  30. package/dist/index.js.map +3 -3
  31. package/dist/language-service/index.d.ts +1 -1
  32. package/dist/language-service/index.js.map +1 -1
  33. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  34. package/dist/plugin/evaluator.d.ts.map +1 -1
  35. package/dist/plugin/index.d.ts +3 -7
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/index.js +904 -202
  38. package/dist/plugin/index.js.map +3 -3
  39. package/dist/regex/first-set.d.ts +19 -0
  40. package/dist/regex/first-set.d.ts.map +1 -1
  41. package/dist/spec/index.js.map +1 -1
  42. package/dist/table/assemble.d.ts +16 -3
  43. package/dist/table/assemble.d.ts.map +1 -1
  44. package/dist/table/child-slots.d.ts.map +1 -1
  45. package/dist/table/compile-rule-map.d.ts +3 -2
  46. package/dist/table/compile-rule-map.d.ts.map +1 -1
  47. package/dist/table/compile.d.ts +2 -2
  48. package/dist/table/emit-assembly.d.ts +8 -2
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts +8 -8
  51. package/dist/table/emit.d.ts.map +1 -1
  52. package/dist/table/encode.d.ts +6 -7
  53. package/dist/table/encode.d.ts.map +1 -1
  54. package/dist/table/exec.d.ts.map +1 -1
  55. package/dist/table/factor-choice.d.ts +27 -0
  56. package/dist/table/factor-choice.d.ts.map +1 -1
  57. package/dist/table/gap-gate.d.ts +11 -0
  58. package/dist/table/gap-gate.d.ts.map +1 -0
  59. package/dist/table/index.js +927 -202
  60. package/dist/table/index.js.map +3 -3
  61. package/dist/table/lex-program.d.ts.map +1 -1
  62. package/dist/table/ops.d.ts +48 -20
  63. package/dist/table/ops.d.ts.map +1 -1
  64. package/dist/table/program.d.ts +13 -8
  65. package/dist/table/program.d.ts.map +1 -1
  66. package/dist/types.d.ts +17 -5
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/version.d.ts +6 -6
  69. package/package.json +1 -1
  70. package/src/analysis/commitment.ts +1 -1
  71. package/src/analysis/duplication.ts +1 -1
  72. package/src/analysis/terminal.ts +1 -1
  73. package/src/combinators/attempt.ts +1 -1
  74. package/src/combinators/choice.ts +1 -1
  75. package/src/combinators/completions.ts +3 -3
  76. package/src/combinators/dispatch.ts +30 -6
  77. package/src/combinators/first-set.ts +55 -12
  78. package/src/combinators/gate.ts +1 -1
  79. package/src/combinators/node.ts +1 -1
  80. package/src/combinators/regex.ts +9 -4
  81. package/src/combinators/repeat.ts +6 -6
  82. package/src/combinators/sequence.ts +1 -1
  83. package/src/combinators/trivia-boundary.ts +18 -0
  84. package/src/combinators/trivia-skip.ts +1 -1
  85. package/src/compiler/compile-linkable-table.ts +2 -3
  86. package/src/compiler/degradation.ts +2 -2
  87. package/src/compiler/ir-serialize.ts +1 -1
  88. package/src/compiler/linker.ts +5 -3
  89. package/src/compiler/token-alphabet.ts +3 -1
  90. package/src/coverage.ts +1 -1
  91. package/src/cst/capture-buffer.ts +2 -2
  92. package/src/cst/host-mode.ts +1 -1
  93. package/src/cst/root-trivia-scope.ts +1 -1
  94. package/src/index.ts +1 -1
  95. package/src/language-service/index.ts +1 -1
  96. package/src/plugin/entry-supercompiler.ts +2 -1
  97. package/src/plugin/evaluator.ts +8 -3
  98. package/src/plugin/index.ts +11 -16
  99. package/src/regex/first-set.ts +37 -3
  100. package/src/table/assemble.ts +180 -35
  101. package/src/table/child-slots.ts +8 -3
  102. package/src/table/compile-rule-map.ts +10 -6
  103. package/src/table/compile.ts +3 -3
  104. package/src/table/emit-assembly.ts +186 -35
  105. package/src/table/emit.ts +19 -18
  106. package/src/table/encode.ts +482 -88
  107. package/src/table/exec.ts +42 -18
  108. package/src/table/factor-choice.ts +62 -0
  109. package/src/table/fail-clean.ts +2 -2
  110. package/src/table/gap-gate.ts +55 -0
  111. package/src/table/hold-gap.ts +2 -2
  112. package/src/table/lex-program.ts +15 -6
  113. package/src/table/ops.ts +51 -20
  114. package/src/table/program.ts +14 -9
  115. package/src/table/select-hot.ts +7 -2
  116. package/src/table/stamp.ts +1 -1
  117. package/src/types.ts +13 -6
  118. package/src/version.ts +6 -6
@@ -4,7 +4,7 @@
4
4
  * This is ledger row G5 as stated: *"build the grammar reference at run start,
5
5
  * making the swaps on rules / sub-rules (leaves) at that point, then run with NO
6
6
  * logic branching for that option input"*. `exec.ts` builds the same table and
7
- * then interprets it: one `switch (code[ip])` over 29 opcodes, executed ONCE PER
7
+ * then interprets it: one `switch (code[ip])` over every opcode (29 then), executed ONCE PER
8
8
  * ROW — 497,360 rows for `benchmark.less` (`bench/jess/g5-rows.ts`) — each
9
9
  * re-reading its opcode and re-decoding its operands from the `Int32Array`, and
10
10
  * re-testing the same per-parse options. That is the per-node branching the
@@ -73,10 +73,11 @@ import { skipDeferredTrivia, triviaLeadTable } from './trivia-gap.ts'
73
73
  import { asciiFoldKey } from '../combinators/dispatch.ts'
74
74
  import { projectChild, unwrapChild } from '../combinators/node.ts'
75
75
  import { cstOutputHost } from '../compiler/build-arity.ts'
76
+ import { PARSEMAN_VERSION } from '../version.ts'
76
77
  import { consumeTrivia } from '../combinators/trivia-skip.ts'
77
78
  import {
78
- advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
79
- scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
79
+ advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
80
+ skipTriviaScanned, type FastTriviaScanner,
80
81
  } from '../combinators/trivia-skip.ts'
81
82
  import {
82
83
  cstCaptureActive, cstLeavesLen, cstTlLen,
@@ -90,8 +91,8 @@ import {
90
91
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
91
92
  OP_LIVE,
92
93
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
93
- OP_COV,
94
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
94
+ OP_COV, OP_GAPGATE, OP_STRICT,
95
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_RGATE2,
95
96
  OP_LEX_BODY, OP_LEX_PROGRAM,
96
97
  } from './ops.ts'
97
98
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -110,13 +111,13 @@ import {
110
111
  } from './emit-assembly.ts'
111
112
  import { collapseEmpty, lead, rawEntry, spanLines } from './run-support.ts'
112
113
  import { holdsGap, rowNullable } from './hold-gap.ts'
114
+ import { gapRows, newGapSlot } from './gap-gate.ts'
113
115
  import { failCleanAnalysis } from './fail-clean.ts'
114
116
  /**
115
117
  * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
116
118
  *
117
- * `failAt` (combinators/probe.ts) is called from exactly three places in the
118
- * interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
119
- * mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
119
+ * `failAt` (combinators/probe.ts) is called at the interpreter's terminal fail
120
+ * sites — `literal.ts`, `regex.ts`, `keywords.ts`, `lines.ts`. The table recorded
120
121
  * NOTHING, so `completionsAt` on a table artifact saw only the top-level
121
122
  * failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
122
123
  * `{ decl; }` grammar) never reached the probe and the item's opener vanished
@@ -413,6 +414,28 @@ export type Assembly = {
413
414
  * ONE walk. Each site is lowered at most once and memoised by its code offset,
414
415
  * so a subtree shared by two parents is one piece with two references to it.
415
416
  */
417
+ /**
418
+ * A printed factory takes its helpers BY POSITION (`EMITTED_PARAMS`), so one printed
419
+ * against a different helper list would bind the wrong function to a name and run
420
+ * wrong rather than fail. A factory printed by 0.59.x guards only against FEWER
421
+ * helpers, and one printed before 0.59.0 has no guard at all. So the runtime checks
422
+ * too, before calling it: the assembly's printed `arity` must be exactly the list it
423
+ * passes. It reads that printed number, never `factory.length`, which a minifier that
424
+ * drops unused trailing parameters lowers on a valid artifact. An assembly printed
425
+ * before 0.60.0 carries no `arity` and is refused. A factory this version prints also
426
+ * checks the count it is called with (`FACTORY_ARITY_GUARD`, `emit.ts`).
427
+ */
428
+ export function assertFactoryArity(pre: { readonly arity?: number }): void {
429
+ if (pre.arity === EMITTED_PARAMS.length) return
430
+ const printed = pre.arity === undefined
431
+ ? 'carries no helper count, so it was printed before parseman 0.60.0'
432
+ : `takes ${pre.arity} helpers, but parseman ${PARSEMAN_VERSION} passes ${EMITTED_PARAMS.length}: it was printed by an older or newer parseman`
433
+ throw new Error(
434
+ `parseman: this parser's printed assembly ${printed}. Reprint the parser with `
435
+ + `parseman ${PARSEMAN_VERSION}, or run it on the parseman version that printed it.`,
436
+ )
437
+ }
438
+
416
439
  export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Assembly {
417
440
  const { code, k, fns, cc, fx, disp, dsp, trivia, triviaLabelled } = t
418
441
  // The leaf swap is only legal when line tracking is off, and that is a property
@@ -438,7 +461,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
438
461
  * graph with no recovery in it anywhere.
439
462
  *
440
463
  * Dormancy is unchanged for the OTHER two engines: the source lowering
441
- * (`codegen.ts:3153`) and the interpreter (`repeat.ts:163`) still gate on
464
+ * (removed in 0.47.0) and the interpreter (`repeat.ts:163`) still gate on
442
465
  * `ctx._tolerant` at runtime, and the recovery pieces below keep their own
443
466
  * `_tolerant === true` failure-path gates so `exec.ts` stays the identity
444
467
  * reference.
@@ -503,6 +526,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
503
526
  }
504
527
 
505
528
  const labelExtraIps = assemblyExtraIps(prog)
529
+ // The regex rows an optional repeat hands a scanned gap to (`gap-gate.ts`),
530
+ // and the one slot it hands it through, shared with the emitted pieces.
531
+ const GAP_ROWS = gapRows(code, [...Object.values(prog.rules), ...labelExtraIps])
532
+ const GAP = newGapSlot()
506
533
  const closureLabels = computeSiteLabels(
507
534
  code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
508
535
  )
@@ -773,6 +800,32 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
773
800
  }
774
801
  }
775
802
 
803
+ /**
804
+ * A regex row a repeat may hand a scanned gap to (`gap-gate.ts`): the end comes
805
+ * from the slot when the slot is this row's, at this position, in this input,
806
+ * and from the regex otherwise. Everything after the end is the row's own.
807
+ */
808
+ function gapRegexPiece(row: number, re: RegExp, xf: string[], track: boolean): Piece {
809
+ return (input, pos, ctx) => {
810
+ let e: number
811
+ if (GAP.row === row && GAP.at === pos && GAP.input === input) e = GAP.end
812
+ else {
813
+ re.lastIndex = pos
814
+ e = re.test(input) ? re.lastIndex : -1
815
+ }
816
+ if (e >= 0) {
817
+ const v = input.slice(pos, e)
818
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
819
+ if (track) trackLinesInto(ctx, input, e)
820
+ EC.e = e
821
+ return v
822
+ }
823
+ ctx._fe = pos; ctx._fx = xf
824
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
825
+ return FAIL
826
+ }
827
+ }
828
+
776
829
  /**
777
830
  * A collapse over one node is that node — `run-support.ts` `collapseEmpty` has
778
831
  * the contract, and the one case the node's value cannot stand for itself.
@@ -1354,7 +1407,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1354
1407
  }
1355
1408
  }
1356
1409
 
1357
- function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1410
+ function dispatchBranch(child: Piece, usesRouted: boolean, commits = true): DispatchBranch {
1358
1411
  if (!usesRouted) {
1359
1412
  // The arm runs after the selector's match, so it marks where it starts.
1360
1413
  return (input, _pos, selEnd, key, ctx) => {
@@ -1369,7 +1422,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1369
1422
  const v = child(input, selEnd, ctx)
1370
1423
  if (v === FAIL) {
1371
1424
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1372
- ctx._fc = true
1425
+ if (commits) ctx._fc = true
1373
1426
  return FAIL
1374
1427
  }
1375
1428
  return [key, v]
@@ -1389,7 +1442,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1389
1442
  }
1390
1443
  if (v === FAIL) {
1391
1444
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1392
- ctx._fc = true
1445
+ if (commits) ctx._fc = true
1393
1446
  return FAIL
1394
1447
  }
1395
1448
  return [key, v]
@@ -1663,6 +1716,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1663
1716
  case OP_RX: {
1664
1717
  const re = k[code[ip + 1]!] as RegExp
1665
1718
  const xf = fx[code[ip + 2]!] as string[]
1719
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, false)
1666
1720
  const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1667
1721
  if (shared !== undefined) {
1668
1722
  return (input, pos, ctx) => {
@@ -1701,6 +1755,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1701
1755
  case OP_RX_TRACK: {
1702
1756
  const re = k[code[ip + 1]!] as RegExp
1703
1757
  const xf = fx[code[ip + 2]!] as string[]
1758
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, true)
1704
1759
  return (input, pos, ctx) => {
1705
1760
  re.lastIndex = pos
1706
1761
  if (re.test(input)) {
@@ -1878,6 +1933,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1878
1933
  }
1879
1934
  }
1880
1935
 
1936
+ // Transparent: only the repeat holding it reads its operands.
1937
+ case OP_GAPGATE:
1938
+ return link(code[ip + 1]!)
1939
+
1940
+ // One encoding per assembly: strict, or the construct as written.
1941
+ case OP_STRICT:
1942
+ return link(code[cfg.tolerant || cfg.probe ? ip + 2 : ip + 1]!)
1943
+
1881
1944
  case OP_RULE:
1882
1945
  // Pure indirection in the tape; in the assembled graph the parent simply
1883
1946
  // holds the target's piece. The trampoline row disappears entirely — this
@@ -1914,8 +1977,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1914
1977
  const expectSpan = (ctx: ParseContext, pos: number) =>
1915
1978
  prog.lines === 1 ? spanLines(ctx, pos, pos) : new PlainSpan(pos, pos)
1916
1979
  // TOLERANT `expect()` EMBEDS ITS ERROR IN THE TREE, not only in the flat
1917
- // `_errors` side-channel (`combinators/expect.ts:150`, and codegen's
1918
- // `_ctx._rec.capture` at codegen.ts:4470) — so a tree walk finds every
1980
+ // `_errors` side-channel (`combinators/expect.ts:150`, and the source
1981
+ // lowering's `_ctx._rec.capture`) — so a tree walk finds every
1919
1982
  // diagnostic and the node survives incremental subtree reuse. The table
1920
1983
  // pushed to `_errors` and stopped, so a tolerant CST parse was missing the
1921
1984
  // `parseError` child the other two engines both produce.
@@ -2536,7 +2599,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2536
2599
  *
2537
2600
  * The restore is `sequence()`'s `finally` (combinators/sequence.ts:105).
2538
2601
  * Codegen does not restore and compensates by having every list capture
2539
- * `_sync` at ENTRY (codegen.ts:3032); the table captures at entry too, so
2602
+ * `_sync` at ENTRY; the table captures at entry too, so
2540
2603
  * this is unobservable either way — it is here because leaving a stale
2541
2604
  * sentinel published is the kind of state a later reader will trust.
2542
2605
  */
@@ -2950,7 +3013,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2950
3013
  // `arms[]` merely to rediscover the same two or three children at
2951
3014
  // every dispatch. The classifier remains table data; only its result
2952
3015
  // is turned into a direct captured call.
2953
- if (n === 2) {
3016
+ if (n === 2 && !cfg.probe) {
2954
3017
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2955
3018
  return (input, pos, ctx) => {
2956
3019
  const c = lead(input, pos)
@@ -2975,7 +3038,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2975
3038
  return FAIL
2976
3039
  }
2977
3040
  }
2978
- if (n === 3) {
3041
+ if (n === 3 && !cfg.probe) {
2979
3042
  const a0 = link(code[base]!), a1 = link(code[base + 1]!), a2 = link(code[base + 2]!)
2980
3043
  return (input, pos, ctx) => {
2981
3044
  const c = lead(input, pos)
@@ -3008,6 +3071,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3008
3071
  )
3009
3072
  }
3010
3073
  const selected = bindExclusive(0)
3074
+ // Under a completions probe the interpreter runs every arm on a miss
3075
+ // (`choice.ts`), so each opener reaches the probe.
3076
+ const probeArms = cfg.probe ? Array.from({ length: n }, (_, i) => link(code[base + i]!)) : undefined
3011
3077
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
3012
3078
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
3013
3079
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -3039,6 +3105,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3039
3105
  // THE UNION IS REPORTED ONLY ON A DISPATCH MISS, where the interpreter's
3040
3106
  // `parsers.flatMap` over non-nullable, char-excluded arms yields exactly
3041
3107
  // each arm's own opener — this set, without running the arms.
3108
+ if (arm < 0 && probeArms !== undefined) for (const a of probeArms) a(input, pos, ctx)
3042
3109
  ctx._fe = pos; ctx._fx = choiceFx
3043
3110
  return FAIL
3044
3111
  }
@@ -3046,8 +3113,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3046
3113
 
3047
3114
  // THE PER-ARM GATE. Arms in source order, each skipped when its own class
3048
3115
  // excludes the char at `pos`. `null` means nullable or unmappable, and
3049
- // those arms are always entered.
3050
- const armCls = table.armCls
3116
+ // those arms are always entered. A probe assembly enters every arm, as
3117
+ // the interpreter's `firstMatch` does, so each opener reaches `failAt`.
3118
+ const armCls = cfg.probe ? table.armCls.map(() => null) : table.armCls
3051
3119
  if (n === 2 || n === 3) {
3052
3120
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
3053
3121
  const a2 = n === 3 ? link(code[base + 2]!) : undefined
@@ -3355,6 +3423,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3355
3423
  }
3356
3424
  }
3357
3425
 
3426
+ case OP_RGATE2: {
3427
+ const cls = cc[code[ip + 1]!]!
3428
+ const xf = fx[code[ip + 3]!] as string[]
3429
+ const first = cc[code[ip + 4]!]!
3430
+ const second = cc[code[ip + 5]!]!
3431
+ const child = link(code[ip + 2]!)
3432
+ if (cfg.tolerant || cfg.probe) return child
3433
+ // ASCII-only classes are read as code units against their tables.
3434
+ if (cls.hi.length === 0 && first.hi.length === 0 && second.hi.length === 0) {
3435
+ const a0 = cls.ascii, a1 = first.ascii, a2 = second.ascii
3436
+ return (input, pos, ctx) => {
3437
+ const item = ctx._routed
3438
+ if (item !== undefined && item.span.start === pos) {
3439
+ const e = item.span.end
3440
+ const c = input.charCodeAt(e)
3441
+ if (a0[c] !== 1 || (a1[c] === 1 && a2[input.charCodeAt(e + 1)] !== 1)) {
3442
+ ctx._fe = e; ctx._fx = xf
3443
+ return FAIL
3444
+ }
3445
+ }
3446
+ return child(input, pos, ctx)
3447
+ }
3448
+ }
3449
+ return (input, pos, ctx) => {
3450
+ const item = ctx._routed
3451
+ if (item !== undefined && item.span.start === pos) {
3452
+ const e = item.span.end
3453
+ const c = lead(input, e)
3454
+ if (!classHas(cls, c) || (classHas(first, c) && !classHas(second, lead(input, e + 1)))) {
3455
+ ctx._fe = e; ctx._fx = xf
3456
+ return FAIL
3457
+ }
3458
+ }
3459
+ return child(input, pos, ctx)
3460
+ }
3461
+ }
3462
+
3358
3463
  case OP_ARMGATE: {
3359
3464
  // Predicate, arm and expected set all bound HERE — the body reads no
3360
3465
  // opcode, decodes no operand and tests no setting.
@@ -3451,6 +3556,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3451
3556
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
3452
3557
  // its FIRST item through `repItem` and so skips leading trivia. The shape
3453
3558
  // identifies itself, and it is table data, so it is decided here.
3559
+ // A separated list's ip+8 (flag bit 8) is its separator's first set where the encoder
3560
+ // found the test cheaper than entering it (`Encoder.sepGate`). Where the
3561
+ // separator cannot match empty and reads no pre-scan cursor, a lead
3562
+ // outside it stops the list at the separator, exactly as the strict
3563
+ // optional item stops at its own class above.
3564
+ const sepClassIndex = sepIp >= 0 && !sepLeads && (flags & 8) !== 0 ? code[ip + 8]! : -1
3565
+ const sepClass = !REC && sepClassIndex >= 0 && !rowNullable(code, k, sepIp, NULLABLE) ? cc[sepClassIndex]! : undefined
3566
+ // THE GAP GATE (`gap-gate.ts`): read the item's leading gap once, here.
3567
+ const gapIp = sepIp < 0 && min === 0 && !REC && code[code[ip + 1]!] === OP_GAPGATE ? code[ip + 1]! : -1
3568
+ const gapRow = gapIp >= 0 ? code[gapIp + 2]! : -1
3569
+ const gapRe = gapIp >= 0 ? k[code[gapRow + 1]!] as RegExp : undefined
3570
+ const gapFollow = gapIp >= 0 ? cc[code[gapIp + 3]!]! : undefined
3571
+ const gapOther = gapIp >= 0 && code[gapIp + 4]! >= 0 ? cc[code[gapIp + 4]!]! : undefined
3454
3572
  const skipBeforeFirst = sepIp < 0 && min === 0
3455
3573
  const itemClean = sepIp < 0 && CLEAN(code[ip + 1]!) && !rowNullable(code, k, code[ip + 1]!, NULLABLE)
3456
3574
 
@@ -3479,7 +3597,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3479
3597
  * delimiter (`orSentinel`, repeat.ts:454).
3480
3598
  * - `mySync` is captured at ENTRY. An element's own sequence publishes
3481
3599
  * over `_sync` while it runs; the interpreter restores in a `finally`
3482
- * and codegen saves at entry (codegen.ts:3032) — same value, both ways.
3600
+ * and the source lowering saved at entry — same value, both ways.
3483
3601
  * - the separator-less path rolls the leading trivia back BEFORE
3484
3602
  * recovering (repeat.ts's `repItem`), and the separated path does NOT:
3485
3603
  * a consumed separator and the error after it both belong to the list
@@ -3487,6 +3605,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3487
3605
  */
3488
3606
  if (REC) {
3489
3607
  const itemFx = fx[code[ip + 6]!] as string[]
3608
+ // A resync error carries the item's set as derived (bit 16: empty).
3609
+ const recFx = (flags & 16) !== 0 ? EMPTY_FX : itemFx
3490
3610
  const sepSent = sentinelFor(code[ip + 7]!)
3491
3611
  return (input, pos, ctx) => {
3492
3612
  const out: unknown[] | undefined = collect ? [] : undefined
@@ -3547,7 +3667,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3547
3667
  const rr = recoverScan(
3548
3668
  input, itemStart, ctx,
3549
3669
  sep === undefined ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
3550
- itemFx,
3670
+ recFx,
3551
3671
  )
3552
3672
  if (out !== undefined) out.push(rr.error)
3553
3673
  captureError(ctx, rr.error)
@@ -3587,6 +3707,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3587
3707
  // Keep the ordinary child path while one is active so this structural
3588
3708
  // fast stop cannot erase an item completion at the cursor.
3589
3709
  const gateItems = itemMayStart !== undefined && ctx._probe === undefined
3710
+ const gateSep = sepClass !== undefined && ctx._probe === undefined
3590
3711
  let cur = pos
3591
3712
  let count = 0
3592
3713
  for (;;) {
@@ -3618,6 +3739,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3618
3739
  const leavesBefore = cstLeavesLen(ctx)
3619
3740
  let sp = cur
3620
3741
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3742
+ if (count >= min && gateSep && !classHas(sepClass!, lead(input, sp))) {
3743
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3744
+ break
3745
+ }
3621
3746
  ctx._fc = false
3622
3747
  if (sepLeads) LEAD.at = cur
3623
3748
  const sv = sep(input, sp, ctx)
@@ -3637,13 +3762,33 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3637
3762
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3638
3763
  break
3639
3764
  }
3640
- if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3765
+ // Without a separator or trivia in front, the loop head already
3766
+ // tested this same position.
3767
+ if (count >= min && gateItems && (sep !== undefined || hasTrivia) && !itemMayStart!(input, itemStart)) {
3641
3768
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3642
3769
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3643
3770
  break
3644
3771
  }
3772
+ let gapped = false
3773
+ if (gapRe !== undefined && ctx._probe === undefined) {
3774
+ gapRe.lastIndex = itemStart
3775
+ const q = gapRe.test(input) ? gapRe.lastIndex : -1
3776
+ // No arm with the gap can match where the gap fails, or where the
3777
+ // character after it opens nothing; an arm without it needs its
3778
+ // own lead at the item's start.
3779
+ if ((q < 0 || !classHas(gapFollow!, lead(input, q)))
3780
+ && (gapOther === undefined || !classHas(gapOther, lead(input, itemStart)))) {
3781
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3782
+ break
3783
+ }
3784
+ if (q >= 0) {
3785
+ GAP.row = gapRow; GAP.at = itemStart; GAP.end = q; GAP.input = input
3786
+ gapped = true
3787
+ }
3788
+ }
3645
3789
  ctx._fc = false
3646
3790
  const v = child(input, itemStart, ctx)
3791
+ if (gapped) GAP.at = -1
3647
3792
  if (v === FAIL) {
3648
3793
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3649
3794
  if (committed(ctx)) return FAIL
@@ -3676,7 +3821,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3676
3821
  const selector = link(code[ip + 1]!)
3677
3822
  const otherIp = code[ip + 3]!
3678
3823
  const other = otherIp >= 0 ? link(otherIp) : undefined
3679
- const otherRouted = code[ip + 4]! === 1
3824
+ const otherRouted = (code[ip + 4]! & 1) === 1
3825
+ const otherCommits = (code[ip + 4]! & 2) === 0
3680
3826
  const n = code[ip + 5]!
3681
3827
  const armBase = ip + 6
3682
3828
  const byKey = spec.byKey
@@ -3723,7 +3869,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3723
3869
  )
3724
3870
  }
3725
3871
  const runArm = n === 0 ? undefined : bindArms(0)
3726
- const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3872
+ const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted, otherCommits)
3727
3873
 
3728
3874
  return (input, pos, ctx) => {
3729
3875
  const need = markCst(ctx)
@@ -3903,7 +4049,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3903
4049
  ctx._fields = wantFields ? [] : undefined
3904
4050
  // PER-NODE-TYPE TRIVIA-KIND MASK, scoped to the body and restored —
3905
4051
  // `node.ts:259-273`, and codegen's `_triviaCaptureMask` save/install/
3906
- // restore around the same scope (`codegen.ts:4130-4136`). The table
4052
+ // restore around the same scope. The table
3907
4053
  // installed nothing, so a host that asked `Outer` for comments only got
3908
4054
  // every kind the scanner saw.
3909
4055
  const savedMask = structural ? ctx._triviaCaptureMask : undefined
@@ -4100,22 +4246,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4100
4246
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
4101
4247
  : undefined
4102
4248
  if (pre !== undefined) {
4249
+ assertFactoryArity(pre)
4103
4250
  const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
4104
4251
  emitted = pre.factory(
4105
- EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
4106
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4252
+ EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia, triviaScan,
4107
4253
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4108
4254
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4109
4255
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4110
4256
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4111
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4257
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4112
4258
  asciiFoldKey, ROUTED_FX,
4113
4259
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4114
4260
  matchesAt, recoverScan, orSentinel, captureError,
4115
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4261
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4116
4262
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4117
4263
  collapseEmpty,
4118
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4264
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4119
4265
  )
4120
4266
  emitReached = new Set(pre.reached)
4121
4267
  if (emitted.ext !== undefined) HYBRID = emitted
@@ -4182,13 +4328,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4182
4328
  throw new Unemittable(`the Function constructor (${String(e)})`)
4183
4329
  }
4184
4330
  emitted = factory(
4185
- EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
4186
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4331
+ EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia, triviaScan,
4187
4332
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4188
4333
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4189
4334
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4190
4335
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4191
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4336
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4192
4337
  asciiFoldKey, ROUTED_FX,
4193
4338
  // THE SENTINEL POOL, dense over char-class indices so the emitted text can
4194
4339
  // index it the way it indexes `SCANS`. Built only for a recovery assembly
@@ -4197,10 +4342,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4197
4342
  // object rather than to two separately constructed ones.
4198
4343
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4199
4344
  matchesAt, recoverScan, orSentinel, captureError,
4200
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4345
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4201
4346
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4202
4347
  collapseEmpty,
4203
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4348
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4204
4349
  )
4205
4350
  emitReached = em.reached
4206
4351
  } catch (e) {
@@ -4,7 +4,7 @@ import {
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,
7
- OP_LEX_BODY, OP_LEX_PROGRAM,
7
+ OP_LEX_BODY, OP_LEX_PROGRAM, OP_GAPGATE, OP_RGATE2, OP_STRICT,
8
8
  } from './ops.ts'
9
9
 
10
10
  /**
@@ -47,16 +47,21 @@ export function childSlots(code: ArrayLike<number>, ip: number, out: number[]):
47
47
  // rest — a coverage-encoded program is a program, and omitting it made
48
48
  // `opHistogram` throw on every table the coverage build produces.
49
49
  case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT:
50
- case OP_TOKEN: case OP_ATTEMPT: case OP_LABEL: case OP_COV: case OP_REJECT:
50
+ case OP_TOKEN: case OP_ATTEMPT: case OP_LABEL: case OP_COV: case OP_REJECT: case OP_GAPGATE:
51
51
  out.push(code[ip + 1]!)
52
52
  return true
53
53
 
54
54
  // Single child at `ip + 2`.
55
55
  case OP_GATE: case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM:
56
- case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE: case OP_RGATE:
56
+ case OP_NODE: case OP_NODE_TRACK: case OP_FIELD: case OP_LEAF: case OP_SOURCE_LEAF: case OP_ARMGATE: case OP_RGATE: case OP_RGATE2:
57
57
  out.push(code[ip + 2]!)
58
58
  return true
59
59
 
60
+ // `STRICT strict tolerant` — both encodings are reachable; an assembly keeps one.
61
+ case OP_STRICT:
62
+ out.push(code[ip + 1]!, code[ip + 2]!)
63
+ return true
64
+
60
65
  // `ROUTED fallback` — the operand is an offset or −1. The routed token itself is
61
66
  // data the enclosing `dispatch()` already consumed, so the only edge is the
62
67
  // fallback.
@@ -19,7 +19,7 @@ import { runDuplicationDiagnosticRules, type DuplicationOption } from './duplica
19
19
  * `compileRuleMap()` FOR THE TABLE LOWERING — the counterpart that did not exist.
20
20
  *
21
21
  * `compile` covered `compile()`, the SINGLE-ROOT entry point, which the
22
- * plugin uses only for standalone combinators (`plugin/index.ts:1693`, `:1870`).
22
+ * plugin uses only for standalone combinators.
23
23
  * The main path — every `rules()` grammar — goes through `compileRuleMap`, and
24
24
  * with no table counterpart there was nothing for the macro build to point at.
25
25
  * This is that function, and it is the EASY direction: `encodeTable` takes a
@@ -29,7 +29,7 @@ import { runDuplicationDiagnosticRules, type DuplicationOption } from './duplica
29
29
  * ── THE CONTRACT IT MATCHES ──────────────────────────────────────────────────
30
30
  * `compileRuleMap` returns ONE `replacement` string: a self-contained expression
31
31
  * that the plugin splices over the whole `rules(factory)` call-expression
32
- * (`plugin/index.ts:1753`, and as an initialiser at `:1941`), evaluating ONCE to
32
+ * (`evaluateRulesFactory` in `plugin/index.ts`), evaluating ONCE to
33
33
  * the `{ name: fn, … }` map. `keys` is the entry list, for the caller to check
34
34
  * against the source's own keys. `hostMode` / `hostBranchElided` are what the
35
35
  * macro stamps the emitted map with. `reflection` is per-grammar CST node
@@ -59,6 +59,7 @@ export type TableRuleMapOptions = {
59
59
  readonly specialise?: boolean
60
60
  /** Grammar-level ambient scan-skip, as `rules({ scanSkip }, …)` declares it. */
61
61
  readonly scanSkip?: Combinator<unknown>[]
62
+ /** @deprecated Has no effect (see `TableSettings.recovery`). Removed in the next minor release. */
62
63
  readonly recovery?: boolean
63
64
  readonly hostMode?: HostMode
64
65
  readonly trackLines?: boolean
@@ -264,7 +265,7 @@ function encodeForRun(
264
265
  ): { prog: TableProgram; fnSrcs: (string | null)[]; hostMode: HostMode; plan: ReturnType<typeof buildGrammarPlan> | undefined } | null {
265
266
  runDuplicationDiagnosticRules(ruleMap, opts.duplication)
266
267
  const encodeMap = withAmbient(ruleMap, opts)
267
- // WINNERS ARE THE NON-`lazy` ENTRIES, exactly as `codegen.ts:5639` picks them.
268
+ // WINNERS ARE THE NON-`lazy` ENTRIES, exactly as the source lowering picked them.
268
269
  // A `rules()` map stores named lazy proxies beside resolved bodies, and the plan
269
270
  // uses the winner map to give a shared subtree ONE owner; filtering differently
270
271
  // here would mint `rule:` ids the source lowering does not, so a consumer
@@ -355,9 +356,12 @@ export function compileRuleMap(
355
356
  const roots = assemblyExtraIps(artifact)
356
357
  let siteBytes, fixed
357
358
  try {
358
- // Priced without the `_errors` marks a lexical program over a scan adds
359
- // (`scanSinkNeutral`), so they move no site in or out of the budget.
360
- siteBytes = emitAssemblySource(table, artifact, cfg, roots, true, undefined, true).siteBytes
359
+ // RANKED with every gap gate lowered as its item (`gapNeutral`), so the
360
+ // gates move no site's place in the order; the fit below is judged by the
361
+ // source that ships, gates included. Priced throughout without the
362
+ // `_errors` marks a lexical program over a scan adds (`scanSinkNeutral`),
363
+ // so they move no site in or out of the budget.
364
+ siteBytes = emitAssemblySource(table, artifact, cfg, roots, true, undefined, true, true).siteBytes
361
365
  // What any selective assembly costs before its first site body: the shared
362
366
  // prelude and the factory's tail. A budget that cannot cover it buys nothing.
363
367
  fixed = emitAssemblySource(table, artifact, cfg, roots, true, new Set(), true).source.length
@@ -79,8 +79,8 @@ export type TableCompileOptions = {
79
79
  readonly hostMode?: HostMode
80
80
  readonly trackLines?: boolean
81
81
  /**
82
- * ACCEPTED AND IGNORED — recovery is ALWAYS lowered (see `TableSettings.recovery`).
83
- * Kept so this stays a drop-in for `compile(g, …, { recovery: true })`.
82
+ * @deprecated ACCEPTED AND IGNORED — recovery is ALWAYS lowered (see
83
+ * `TableSettings.recovery`). Removed in the next minor release.
84
84
  */
85
85
  readonly recovery?: boolean
86
86
  /**
@@ -178,7 +178,7 @@ function compileRoot<T>(
178
178
  ): CompiledParser<T> {
179
179
  // Argument order mirrors `compile(combinator, mapFnSources?, opts?)` so this is
180
180
  // a drop-in, not a second API with a different shape.
181
- // THE SAME WALK CODEGEN USES FOR A SINGLE ROOT (`codegen.ts:5377`): no winner
181
+ // THE SAME WALK THE SOURCE LOWERING USED FOR A SINGLE ROOT: no winner
182
182
  // map, so the paths are rooted at `entry` and the ids are the ones a caller
183
183
  // switching lowerings already has. Building it differently here would produce a
184
184
  // set that is internally consistent and lines up with nothing.