parseman 0.59.3 → 0.60.1

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 (94) hide show
  1. package/dist/analysis/diagnostics.js +876 -190
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +876 -190
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts +13 -1
  6. package/dist/combinators/dispatch.d.ts.map +1 -1
  7. package/dist/combinators/first-set.d.ts +35 -2
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/regex.d.ts.map +1 -1
  10. package/dist/combinators/trivia-boundary.d.ts +11 -0
  11. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  12. package/dist/combinators/trivia-skip.d.ts +1 -1
  13. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  14. package/dist/compiler/token-alphabet.d.ts +2 -0
  15. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  16. package/dist/compiler/token-capability.js +65 -53
  17. package/dist/compiler/token-capability.js.map +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +896 -199
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +903 -201
  24. package/dist/plugin/index.js.map +3 -3
  25. package/dist/regex/first-set.d.ts +19 -0
  26. package/dist/regex/first-set.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts +15 -1
  28. package/dist/table/assemble.d.ts.map +1 -1
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +1 -1
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/compile.d.ts +1 -1
  33. package/dist/table/emit-assembly.d.ts +8 -2
  34. package/dist/table/emit-assembly.d.ts.map +1 -1
  35. package/dist/table/emit.d.ts +4 -3
  36. package/dist/table/emit.d.ts.map +1 -1
  37. package/dist/table/encode.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts +0 -35
  39. package/dist/table/exec.d.ts.map +1 -1
  40. package/dist/table/factor-choice.d.ts +27 -0
  41. package/dist/table/factor-choice.d.ts.map +1 -1
  42. package/dist/table/gap-gate.d.ts +11 -0
  43. package/dist/table/gap-gate.d.ts.map +1 -0
  44. package/dist/table/index.js +926 -201
  45. package/dist/table/index.js.map +3 -3
  46. package/dist/table/lex-program.d.ts.map +1 -1
  47. package/dist/table/ops.d.ts +35 -16
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +6 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/types.d.ts +16 -4
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +2 -4
  55. package/src/analysis/duplication.ts +1 -1
  56. package/src/combinators/attempt.ts +1 -1
  57. package/src/combinators/dispatch.ts +30 -6
  58. package/src/combinators/first-set.ts +55 -12
  59. package/src/combinators/node.ts +1 -1
  60. package/src/combinators/regex.ts +9 -4
  61. package/src/combinators/repeat.ts +6 -6
  62. package/src/combinators/sequence.ts +1 -1
  63. package/src/combinators/trivia-boundary.ts +18 -0
  64. package/src/combinators/trivia-skip.ts +1 -1
  65. package/src/compiler/ir-serialize.ts +1 -1
  66. package/src/compiler/token-alphabet.ts +3 -1
  67. package/src/coverage.ts +1 -1
  68. package/src/index.ts +1 -1
  69. package/src/plugin/evaluator.ts +8 -3
  70. package/src/regex/first-set.ts +37 -3
  71. package/src/table/assemble.ts +176 -30
  72. package/src/table/child-slots.ts +8 -3
  73. package/src/table/compile-rule-map.ts +7 -4
  74. package/src/table/compile.ts +1 -1
  75. package/src/table/emit-assembly.ts +184 -33
  76. package/src/table/emit.ts +6 -4
  77. package/src/table/encode.ts +469 -74
  78. package/src/table/exec.ts +38 -82
  79. package/src/table/factor-choice.ts +62 -0
  80. package/src/table/fail-clean.ts +2 -2
  81. package/src/table/gap-gate.ts +55 -0
  82. package/src/table/hold-gap.ts +2 -2
  83. package/src/table/lex-program.ts +15 -6
  84. package/src/table/ops.ts +38 -16
  85. package/src/table/program.ts +7 -2
  86. package/src/table/select-hot.ts +7 -2
  87. package/src/types.ts +12 -5
  88. package/src/version.ts +1 -1
  89. package/dist/table/encode-baseline.d.ts +0 -16
  90. package/dist/table/encode-baseline.d.ts.map +0 -1
  91. package/dist/table/exec-baseline.d.ts +0 -9
  92. package/dist/table/exec-baseline.d.ts.map +0 -1
  93. package/src/table/encode-baseline.ts +0 -346
  94. package/src/table/exec-baseline.ts +0 -557
@@ -5,7 +5,7 @@
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
7
  * then interprets it: one `switch (code[ip])` over every opcode (29 then), executed ONCE PER
8
- * ROW — 497,360 rows for `benchmark.less` (`bench/jess/g5-rows.ts`) — each
8
+ * ROW — 497,360 rows for `benchmark.less` (measured with a one-off `g5-rows` bench, last at `4a5d0341`) — 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
11
11
  * design exists to remove.
@@ -20,13 +20,13 @@
20
20
  *
21
21
  * The numbers that make this obviously the right trade, measured not assumed:
22
22
  *
23
- * 2,241 distinct reachable sites in the less table (`bench/jess/g5-sites.ts`)
23
+ * 2,241 distinct reachable sites in the less table (`g5-sites`)
24
24
  * 497,360 rows executed for one parse of `benchmark.less`
25
25
  *
26
26
  * Pieces are GRAMMAR-sized; rows are INPUT-sized. Assembly allocates ~2.2k
27
27
  * closures once per process and removes a dispatch plus an operand decode from
28
28
  * ~497k executions — a 222x ratio. Assembly cost is paid once and is not the
29
- * metric; it is measured anyway (`bench/jess/g5-ms.ts`, `bench/jess/g5-profile.ts`).
29
+ * metric; it is measured anyway (`g5-ms`, `g5-profile`).
30
30
  *
31
31
  * ## Why this is not "just tuning the switch"
32
32
  *
@@ -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,6 +111,7 @@ 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.
@@ -412,6 +414,28 @@ export type Assembly = {
412
414
  * ONE walk. Each site is lowered at most once and memoised by its code offset,
413
415
  * so a subtree shared by two parents is one piece with two references to it.
414
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
+
415
439
  export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Assembly {
416
440
  const { code, k, fns, cc, fx, disp, dsp, trivia, triviaLabelled } = t
417
441
  // The leaf swap is only legal when line tracking is off, and that is a property
@@ -502,6 +526,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
502
526
  }
503
527
 
504
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()
505
533
  const closureLabels = computeSiteLabels(
506
534
  code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
507
535
  )
@@ -772,6 +800,32 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
772
800
  }
773
801
  }
774
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
+
775
829
  /**
776
830
  * A collapse over one node is that node — `run-support.ts` `collapseEmpty` has
777
831
  * the contract, and the one case the node's value cannot stand for itself.
@@ -1353,7 +1407,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1353
1407
  }
1354
1408
  }
1355
1409
 
1356
- function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1410
+ function dispatchBranch(child: Piece, usesRouted: boolean, commits = true): DispatchBranch {
1357
1411
  if (!usesRouted) {
1358
1412
  // The arm runs after the selector's match, so it marks where it starts.
1359
1413
  return (input, _pos, selEnd, key, ctx) => {
@@ -1368,7 +1422,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1368
1422
  const v = child(input, selEnd, ctx)
1369
1423
  if (v === FAIL) {
1370
1424
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1371
- ctx._fc = true
1425
+ if (commits) ctx._fc = true
1372
1426
  return FAIL
1373
1427
  }
1374
1428
  return [key, v]
@@ -1388,7 +1442,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1388
1442
  }
1389
1443
  if (v === FAIL) {
1390
1444
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1391
- ctx._fc = true
1445
+ if (commits) ctx._fc = true
1392
1446
  return FAIL
1393
1447
  }
1394
1448
  return [key, v]
@@ -1662,6 +1716,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1662
1716
  case OP_RX: {
1663
1717
  const re = k[code[ip + 1]!] as RegExp
1664
1718
  const xf = fx[code[ip + 2]!] as string[]
1719
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, false)
1665
1720
  const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1666
1721
  if (shared !== undefined) {
1667
1722
  return (input, pos, ctx) => {
@@ -1700,6 +1755,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1700
1755
  case OP_RX_TRACK: {
1701
1756
  const re = k[code[ip + 1]!] as RegExp
1702
1757
  const xf = fx[code[ip + 2]!] as string[]
1758
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, true)
1703
1759
  return (input, pos, ctx) => {
1704
1760
  re.lastIndex = pos
1705
1761
  if (re.test(input)) {
@@ -1877,6 +1933,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1877
1933
  }
1878
1934
  }
1879
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
+
1880
1944
  case OP_RULE:
1881
1945
  // Pure indirection in the tape; in the assembled graph the parent simply
1882
1946
  // holds the target's piece. The trampoline row disappears entirely — this
@@ -2949,7 +3013,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2949
3013
  // `arms[]` merely to rediscover the same two or three children at
2950
3014
  // every dispatch. The classifier remains table data; only its result
2951
3015
  // is turned into a direct captured call.
2952
- if (n === 2) {
3016
+ if (n === 2 && !cfg.probe) {
2953
3017
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2954
3018
  return (input, pos, ctx) => {
2955
3019
  const c = lead(input, pos)
@@ -2974,7 +3038,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2974
3038
  return FAIL
2975
3039
  }
2976
3040
  }
2977
- if (n === 3) {
3041
+ if (n === 3 && !cfg.probe) {
2978
3042
  const a0 = link(code[base]!), a1 = link(code[base + 1]!), a2 = link(code[base + 2]!)
2979
3043
  return (input, pos, ctx) => {
2980
3044
  const c = lead(input, pos)
@@ -3007,6 +3071,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3007
3071
  )
3008
3072
  }
3009
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
3010
3077
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
3011
3078
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
3012
3079
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -3038,6 +3105,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3038
3105
  // THE UNION IS REPORTED ONLY ON A DISPATCH MISS, where the interpreter's
3039
3106
  // `parsers.flatMap` over non-nullable, char-excluded arms yields exactly
3040
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)
3041
3109
  ctx._fe = pos; ctx._fx = choiceFx
3042
3110
  return FAIL
3043
3111
  }
@@ -3045,8 +3113,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3045
3113
 
3046
3114
  // THE PER-ARM GATE. Arms in source order, each skipped when its own class
3047
3115
  // excludes the char at `pos`. `null` means nullable or unmappable, and
3048
- // those arms are always entered.
3049
- 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
3050
3119
  if (n === 2 || n === 3) {
3051
3120
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
3052
3121
  const a2 = n === 3 ? link(code[base + 2]!) : undefined
@@ -3354,6 +3423,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3354
3423
  }
3355
3424
  }
3356
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
+
3357
3463
  case OP_ARMGATE: {
3358
3464
  // Predicate, arm and expected set all bound HERE — the body reads no
3359
3465
  // opcode, decodes no operand and tests no setting.
@@ -3450,6 +3556,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3450
3556
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
3451
3557
  // its FIRST item through `repItem` and so skips leading trivia. The shape
3452
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
3453
3572
  const skipBeforeFirst = sepIp < 0 && min === 0
3454
3573
  const itemClean = sepIp < 0 && CLEAN(code[ip + 1]!) && !rowNullable(code, k, code[ip + 1]!, NULLABLE)
3455
3574
 
@@ -3486,6 +3605,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3486
3605
  */
3487
3606
  if (REC) {
3488
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
3489
3610
  const sepSent = sentinelFor(code[ip + 7]!)
3490
3611
  return (input, pos, ctx) => {
3491
3612
  const out: unknown[] | undefined = collect ? [] : undefined
@@ -3546,7 +3667,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3546
3667
  const rr = recoverScan(
3547
3668
  input, itemStart, ctx,
3548
3669
  sep === undefined ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
3549
- itemFx,
3670
+ recFx,
3550
3671
  )
3551
3672
  if (out !== undefined) out.push(rr.error)
3552
3673
  captureError(ctx, rr.error)
@@ -3586,6 +3707,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3586
3707
  // Keep the ordinary child path while one is active so this structural
3587
3708
  // fast stop cannot erase an item completion at the cursor.
3588
3709
  const gateItems = itemMayStart !== undefined && ctx._probe === undefined
3710
+ const gateSep = sepClass !== undefined && ctx._probe === undefined
3589
3711
  let cur = pos
3590
3712
  let count = 0
3591
3713
  for (;;) {
@@ -3617,6 +3739,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3617
3739
  const leavesBefore = cstLeavesLen(ctx)
3618
3740
  let sp = cur
3619
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
+ }
3620
3746
  ctx._fc = false
3621
3747
  if (sepLeads) LEAD.at = cur
3622
3748
  const sv = sep(input, sp, ctx)
@@ -3636,13 +3762,33 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3636
3762
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3637
3763
  break
3638
3764
  }
3639
- 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)) {
3640
3768
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3641
3769
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3642
3770
  break
3643
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
+ }
3644
3789
  ctx._fc = false
3645
3790
  const v = child(input, itemStart, ctx)
3791
+ if (gapped) GAP.at = -1
3646
3792
  if (v === FAIL) {
3647
3793
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3648
3794
  if (committed(ctx)) return FAIL
@@ -3675,7 +3821,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3675
3821
  const selector = link(code[ip + 1]!)
3676
3822
  const otherIp = code[ip + 3]!
3677
3823
  const other = otherIp >= 0 ? link(otherIp) : undefined
3678
- const otherRouted = code[ip + 4]! === 1
3824
+ const otherRouted = (code[ip + 4]! & 1) === 1
3825
+ const otherCommits = (code[ip + 4]! & 2) === 0
3679
3826
  const n = code[ip + 5]!
3680
3827
  const armBase = ip + 6
3681
3828
  const byKey = spec.byKey
@@ -3722,7 +3869,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3722
3869
  )
3723
3870
  }
3724
3871
  const runArm = n === 0 ? undefined : bindArms(0)
3725
- const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3872
+ const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted, otherCommits)
3726
3873
 
3727
3874
  return (input, pos, ctx) => {
3728
3875
  const need = markCst(ctx)
@@ -4099,22 +4246,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4099
4246
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
4100
4247
  : undefined
4101
4248
  if (pre !== undefined) {
4249
+ assertFactoryArity(pre)
4102
4250
  const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
4103
4251
  emitted = pre.factory(
4104
- EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
4105
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4252
+ EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia, triviaScan,
4106
4253
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4107
4254
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4108
4255
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4109
4256
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4110
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4257
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4111
4258
  asciiFoldKey, ROUTED_FX,
4112
4259
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4113
4260
  matchesAt, recoverScan, orSentinel, captureError,
4114
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4261
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4115
4262
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4116
4263
  collapseEmpty,
4117
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4264
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4118
4265
  )
4119
4266
  emitReached = new Set(pre.reached)
4120
4267
  if (emitted.ext !== undefined) HYBRID = emitted
@@ -4151,7 +4298,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4151
4298
  // Two engines that can only be compared across two checkouts cannot be
4152
4299
  // compared at all: the bench harness's own guidance is that a
4153
4300
  // cross-worktree measurement carries a bias no repetition removes. Same
4154
- // spelling as `PM_TABLE_COUNT` (`bench/table-opcode-gaps.ts`).
4301
+ // spelling as the removed `PM_TABLE_COUNT` counters.
4155
4302
  if (!EMIT_ENABLED) throw new Unemittable('PM_TABLE_EMIT=0 (measurement toggle)')
4156
4303
  const em = emitAssemblySource(t, prog, cfg, extraIps)
4157
4304
  /**
@@ -4181,13 +4328,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4181
4328
  throw new Unemittable(`the Function constructor (${String(e)})`)
4182
4329
  }
4183
4330
  emitted = factory(
4184
- EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
4185
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4331
+ EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia, triviaScan,
4186
4332
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4187
4333
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4188
4334
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4189
4335
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4190
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4336
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4191
4337
  asciiFoldKey, ROUTED_FX,
4192
4338
  // THE SENTINEL POOL, dense over char-class indices so the emitted text can
4193
4339
  // index it the way it indexes `SCANS`. Built only for a recovery assembly
@@ -4196,10 +4342,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4196
4342
  // object rather than to two separately constructed ones.
4197
4343
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4198
4344
  matchesAt, recoverScan, orSentinel, captureError,
4199
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4345
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4200
4346
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4201
4347
  collapseEmpty,
4202
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4348
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4203
4349
  )
4204
4350
  emitReached = em.reached
4205
4351
  } catch (e) {
@@ -4423,7 +4569,7 @@ export class AssemblyCache {
4423
4569
  * That is not a hole in G5, it is G5's own first clause: "quickly building the
4424
4570
  * grammar reference ON RUN START, making some swaps on rules or sub-rules, and
4425
4571
  * then the run actually runs with no logic branching for that option input"
4426
- * (`notes/TABLE-DRIVER.md`). This IS the run-start step. What the criterion
4572
+ * (`notes/history/TABLE-DRIVER.md`). This IS the run-start step. What the criterion
4427
4573
  * forbids is the second sentence, and past this call there is no option read
4428
4574
  * anywhere — `scripts/check-invariants.mjs` INV-6 decides that mechanically.
4429
4575
  *
@@ -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.
@@ -68,7 +68,7 @@ export type TableRuleMapOptions = {
68
68
  * `coverage: grammarCoverage` here for every `rules()` grammar — so this is what
69
69
  * makes `{ grammarCoverage: true }` a working build under the table lowering.
70
70
  *
71
- * COUNTERS ONLY, per the owner ruling recorded in `notes/RELEASE-0.48-TARGET.md`
71
+ * COUNTERS ONLY, per the owner ruling recorded in `notes/history/RELEASE-0.48-TARGET.md`
72
72
  * §1; `_grammarTrace` parity is 0.48 work. The plugin's coverage path needs the
73
73
  * DEFINITIONS STAMP and the HITS, and both are here.
74
74
  */
@@ -356,9 +356,12 @@ export function compileRuleMap(
356
356
  const roots = assemblyExtraIps(artifact)
357
357
  let siteBytes, fixed
358
358
  try {
359
- // Priced without the `_errors` marks a lexical program over a scan adds
360
- // (`scanSinkNeutral`), so they move no site in or out of the budget.
361
- 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
362
365
  // What any selective assembly costs before its first site body: the shared
363
366
  // prelude and the factory's tail. A budget that cannot cover it buys nothing.
364
367
  fixed = emitAssemblySource(table, artifact, cfg, roots, true, new Set(), true).source.length
@@ -90,7 +90,7 @@ export type TableCompileOptions = {
90
90
  *
91
91
  * COUNTERS ONLY — owner ruling. `_grammarTrace`'s six phases are emitted at
92
92
  * ~40 fine-grained sites by codegen and are deferred to 0.48
93
- * (`notes/RELEASE-0.48-TARGET.md` §1). A table entry rejects a context carrying
93
+ * (`notes/history/RELEASE-0.48-TARGET.md` §1). A table entry rejects a context carrying
94
94
  * a trace sink before parsing: silently returning an empty trace would look
95
95
  * exactly like a real trace whose grammar sites were never visited. The
96
96
  * plugin's coverage path reads `_grammarCoverage` and the definitions stamp,