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
package/src/table/exec.ts CHANGED
@@ -21,8 +21,8 @@ import {
21
21
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
22
22
  OP_LIVE,
23
23
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
24
- OP_COV,
25
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
24
+ OP_COV, OP_GAPGATE, OP_STRICT,
25
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_RGATE2,
26
26
  OP_LEX_BODY, OP_LEX_PROGRAM,
27
27
  } from './ops.ts'
28
28
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -75,60 +75,6 @@ import { FAIL, newEndCell, type EndCell } from './cell.ts'
75
75
  import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
76
76
  import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
77
77
 
78
- /**
79
- * DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
80
- *
81
- * Read ONCE, at module load, into a module const, so this is not the per-parse
82
- * option branch G5 forbids: with the variable false the whole thing folds away
83
- * after tier-up. It exists because "too many rows" and "each row too slow" are
84
- * different defects with the same symptom, and only a count separates them.
85
- * No timing run in this repo may set it — see `bench/jess/table-rows.ts`.
86
- */
87
- export const tableCounters: {
88
- rows: number
89
- byOp: Int32Array
90
- /** Distinct reducer/host functions actually reaching each shared call site. */
91
- sites: Map<string, Set<unknown>>
92
- /**
93
- * SPECULATION AT UNGATED CHOICES. `encode.ts:368-376` gives a choice its
94
- * first-char dispatch only when NO arm is nullable, ALL arms are pairwise
95
- * disjoint and ALL map to a char class; any one failure ungates every arm of
96
- * the site, which then runs the linear loop below. Codegen has no such
97
- * all-or-nothing rule — `emitFirstMatch` guards each arm on its OWN first set
98
- * — so these counts are work the table does and the compiled engine does not.
99
- *
100
- * `ungatedEntries` is arms entered there; `ungatedFails` is those that then
101
- * failed, which is pure waste by construction: a failing arm consumed nothing
102
- * the winning arm will not scan again.
103
- */
104
- ungatedEntries: number
105
- ungatedFails: number
106
- /** Rows executed INSIDE failed ungated arms — the subtree each wasted entry drags. */
107
- ungatedFailRows: number
108
- gatedEntries: number
109
- /** Arm entries DECLINED by the per-arm class gate — the work this change removes. */
110
- armGateSkips: number
111
- } = { rows: 0, byOp: new Int32Array(64), sites: new Map(), ungatedEntries: 0, ungatedFails: 0, ungatedFailRows: 0, gatedEntries: 0, armGateSkips: 0 }
112
-
113
- const COUNT = process.env.PM_TABLE_COUNT === '1'
114
-
115
- function siteFn(site: string, fn: unknown): void {
116
- let s = tableCounters.sites.get(site)
117
- if (s === undefined) { s = new Set(); tableCounters.sites.set(site, s) }
118
- s.add(fn)
119
- }
120
-
121
- export function resetTableCounters(): void {
122
- tableCounters.rows = 0
123
- tableCounters.byOp = new Int32Array(64)
124
- tableCounters.sites = new Map()
125
- tableCounters.ungatedEntries = 0
126
- tableCounters.ungatedFails = 0
127
- tableCounters.ungatedFailRows = 0
128
- tableCounters.gatedEntries = 0
129
- tableCounters.armGateSkips = 0
130
- }
131
-
132
78
  const EMPTY_TL: readonly number[] = Object.freeze([])
133
79
  const EMPTY_FX: string[] = []
134
80
  const ROUTED_FX: string[] = ['routed()']
@@ -375,7 +321,11 @@ function makeDriver(
375
321
  }
376
322
 
377
323
  function exec(ip: number, input: string, pos: number, ctx: ParseContext): unknown {
378
- if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
324
+ // A row that only hands on to one child at the same position (a gate that
325
+ // passes, a rule trampoline, `OP_STRICT`) continues here rather than
326
+ // recursing, so the reference driver's stack grows with the grammar's
327
+ // nesting, not with its gate rows.
328
+ rows: for (;;) {
379
329
  switch (code[ip]) {
380
330
  case OP_LIT: {
381
331
  const s = k[code[ip + 1]!] as string
@@ -492,7 +442,7 @@ function makeDriver(
492
442
  ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
493
443
  return FAIL
494
444
  }
495
- return exec(code[ip + 2]!, input, pos, ctx)
445
+ ip = code[ip + 2]!; continue rows
496
446
  }
497
447
 
498
448
  case OP_LABEL: {
@@ -502,7 +452,7 @@ function makeDriver(
502
452
  return v
503
453
  }
504
454
  case OP_RULE:
505
- return exec(code[ip + 1]!, input, pos, ctx)
455
+ ip = code[ip + 1]!; continue rows
506
456
 
507
457
  /**
508
458
  * A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
@@ -518,8 +468,14 @@ function makeDriver(
518
468
  * a second implementation of it here would be a second place for the phase
519
469
  * semantics (entry for a rule, success for an arm) to drift.
520
470
  */
471
+ // The reference runs a gap-gated item as written: the gate is the repeat's,
472
+ // in the shipped engines, and this driver is the leg it is compared with.
521
473
  case OP_COV:
522
- return exec(code[ip + 1]!, input, pos, ctx)
474
+ case OP_GAPGATE:
475
+ ip = code[ip + 1]!; continue rows
476
+
477
+ case OP_STRICT:
478
+ ip = code[ctx._tolerant || ctx._probe !== undefined ? ip + 2 : ip + 1]!; continue rows
523
479
 
524
480
  case OP_LIVE: {
525
481
  // A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
@@ -601,6 +557,7 @@ function makeDriver(
601
557
 
602
558
  let target: number
603
559
  let usesRouted: boolean
560
+ let commits = true
604
561
  if (arm === undefined) {
605
562
  const other = code[ip + 3]!
606
563
  if (other < 0) {
@@ -610,7 +567,8 @@ function makeDriver(
610
567
  return FAIL
611
568
  }
612
569
  target = other
613
- usesRouted = code[ip + 4]! === 1
570
+ usesRouted = (code[ip + 4]! & 1) === 1
571
+ commits = (code[ip + 4]! & 2) === 0
614
572
  } else {
615
573
  target = code[armBase + arm]!
616
574
  usesRouted = spec.routed[arm] === 1
@@ -637,8 +595,8 @@ function makeDriver(
637
595
  rollbackTrivia(ctx, mark)
638
596
  // The interpreter marks a failed dispatch branch COMMITTED: the
639
597
  // selector already matched, so an enclosing choice must not treat this
640
- // as "try the next arm".
641
- ctx._fc = true
598
+ // as "try the next arm". An uncommitted fallback keeps its own flag.
599
+ if (commits) ctx._fc = true
642
600
  return FAIL
643
601
  }
644
602
  // The end cell already holds the branch's end — dispatch's span runs from `pos`
@@ -878,7 +836,6 @@ function makeDriver(
878
836
  // Running it through `exec` at the POST-scan position would find the gap
879
837
  // already consumed and answer "adjacent" unconditionally.
880
838
  if (code[child] === OP_ADJ) {
881
- if (COUNT) { tableCounters.rows++; tableCounters.byOp[OP_ADJ]!++ }
882
839
  const ki = code[child + 2]!
883
840
  if (!adjacencyHolds(input, cur, ctx, code[child + 1] === 1, ki < 0 ? undefined : k[ki] as readonly string[])) {
884
841
  ctx._fe = cur; ctx._fx = fx[code[child + 3]!] as string[]
@@ -914,10 +871,6 @@ function makeDriver(
914
871
  // LIT/RX in place removes both. The duplication is in the DRIVER,
915
872
  // which ships once for every grammar — the cost this design trades on.
916
873
  const cop = code[child]
917
- // The two inline terminals below execute a ROW without going through
918
- // `exec`, so the counter has to see them or the row count understates
919
- // exactly where the driver already won.
920
- if (COUNT && (cop === OP_LIT || cop === OP_RX)) { tableCounters.rows++; tableCounters.byOp[cop]!++ }
921
874
  if (cop === OP_LIT) {
922
875
  const lit = k[code[child + 1]!] as string
923
876
  if (!input.startsWith(lit, cur)) {
@@ -958,7 +911,6 @@ function makeDriver(
958
911
  const reducer = code[ip + 1]!
959
912
  if (reducer < 0) return values![~reducer]
960
913
  const fn = fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
961
- if (COUNT) siteFn('SEQX fn()', fn)
962
914
  return fn(values, { start: pos, end: cur })
963
915
  }
964
916
  return values
@@ -990,7 +942,6 @@ function makeDriver(
990
942
  }
991
943
  if (arm >= 0) {
992
944
  ctx._fc = false
993
- if (COUNT) tableCounters.gatedEntries++
994
945
  const v = exec(code[base + arm]!, input, pos, ctx)
995
946
  if (v !== FAIL) return v
996
947
  // THE CUT. `dispatch()` is the library's one true cut: once its
@@ -1029,6 +980,12 @@ function makeDriver(
1029
980
  // its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
1030
981
  // `encode.ts` recomputes around), so they firstMatch all seven arms where
1031
982
  // this driver dispatches to one. The arm sets differ, not the rule.
983
+ //
984
+ // Under a completions probe the interpreter runs every arm on a miss
985
+ // (`choice.ts`), so each opener reaches the probe; so does this.
986
+ if (arm < 0 && ctx._probe !== undefined) {
987
+ for (let i = 0; i < code[ip + 2]!; i++) exec(code[base + i]!, input, pos, ctx)
988
+ }
1032
989
  return failChoice()
1033
990
  }
1034
991
  // THE PER-ARM GATE. Arms in source order, each skipped when its own
@@ -1063,8 +1020,9 @@ function makeDriver(
1063
1020
  const armFx = base + n
1064
1021
  for (let i = 0; i < n; i++) {
1065
1022
  const cls = armCls[i]
1066
- if (cls !== undefined && cls !== null && !classHas(cls, c)) {
1067
- if (COUNT) tableCounters.armGateSkips++
1023
+ // Under a completion probe every arm is entered, as the interpreter's
1024
+ // `firstMatch` enters it: its opener then reaches the probe (`failAt`).
1025
+ if (ctx._probe === undefined && cls !== undefined && cls !== null && !classHas(cls, c)) {
1068
1026
  // THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
1069
1027
  // enters the arm, the arm's opener fails, and its static set is what
1070
1028
  // lands in the accumulation. Declining to enter must not also decline
@@ -1073,11 +1031,8 @@ function makeDriver(
1073
1031
  continue
1074
1032
  }
1075
1033
  ctx._fc = false
1076
- if (COUNT) tableCounters.ungatedEntries++
1077
- const rows0 = COUNT ? tableCounters.rows : 0
1078
1034
  const v = exec(code[base + i]!, input, pos, ctx)
1079
1035
  if (v !== FAIL) return v
1080
- if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
1081
1036
  const at = ctx._fe ?? pos
1082
1037
  if (at > best) { best = at; acc = undefined }
1083
1038
  if (at === best) push(ctx._fx)
@@ -1119,14 +1074,17 @@ function makeDriver(
1119
1074
  return sup
1120
1075
  }
1121
1076
 
1122
- case OP_RGATE: {
1077
+ // The reference tests the first character only: the second is the shipped
1078
+ // engines' (`OP_RGATE2`), and this driver is the leg they are compared with.
1079
+ case OP_RGATE:
1080
+ case OP_RGATE2: {
1123
1081
  const item = ctx._routed
1124
1082
  if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
1125
1083
  && !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
1126
1084
  ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
1127
1085
  return FAIL
1128
1086
  }
1129
- return exec(code[ip + 2]!, input, pos, ctx)
1087
+ ip = code[ip + 2]!; continue rows
1130
1088
  }
1131
1089
 
1132
1090
  case OP_ARMGATE: {
@@ -1141,7 +1099,7 @@ function makeDriver(
1141
1099
  ctx._fx = fx[code[ip + 3]!] as string[]
1142
1100
  return FAIL
1143
1101
  }
1144
- return exec(code[ip + 2]!, input, pos, ctx)
1102
+ ip = code[ip + 2]!; continue rows
1145
1103
  }
1146
1104
 
1147
1105
  case OP_REJECT: {
@@ -1225,7 +1183,7 @@ function makeDriver(
1225
1183
  // own sync. The item's expected set is at ip + 6 and the separator's
1226
1184
  // sentinel class at ip + 7, both laid down only for a recovery table.
1227
1185
  const mySync = REC ? ctx._sync : undefined
1228
- const recFx = REC ? fx[code[ip + 6]!] as string[] : undefined
1186
+ const recFx = !REC ? undefined : (code[ip + 5]! & 16) !== 0 ? [] : fx[code[ip + 6]!] as string[]
1229
1187
  const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
1230
1188
  const itemClassIndex = sep < 0 ? code[ip + 7]! : -1
1231
1189
  const itemClass = REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
@@ -1397,7 +1355,6 @@ function makeDriver(
1397
1355
  const v = exec(code[ip + 2]!, input, pos, ctx)
1398
1356
  if (v === FAIL) return FAIL
1399
1357
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1400
- if (COUNT) siteFn('XFORM fn()', fn)
1401
1358
  return fn(v, { start: pos, end: EC.e })
1402
1359
  }
1403
1360
 
@@ -1439,7 +1396,6 @@ function makeDriver(
1439
1396
  if (v === FAIL) return FAIL
1440
1397
  const end = EC.e
1441
1398
  const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1442
- if (COUNT) siteFn('LEAF fn()', fn)
1443
1399
  const out = fn(v, { start: pos, end })
1444
1400
  if (wasCapturing) pushCstLeaf(ctx, {
1445
1401
  _tag: 'leaf',
@@ -1542,7 +1498,6 @@ function makeDriver(
1542
1498
  children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
1543
1499
  rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
1544
1500
  ) => unknown
1545
- if (COUNT) siteFn('NODE build()', build)
1546
1501
  nd = build(kids, fieldMap, span, cap.rawChildren, (flags & 4) !== 0 || hostCst ? cap.triviaLog : EMPTY_TL, st)
1547
1502
  }
1548
1503
  } else if (host !== undefined) {
@@ -1644,6 +1599,7 @@ function makeDriver(
1644
1599
  default:
1645
1600
  throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
1646
1601
  }
1602
+ }
1647
1603
  }
1648
1604
 
1649
1605
  /**
@@ -35,8 +35,16 @@ import { sequence } from '../combinators/sequence.ts'
35
35
  import { leaf, sourceLeaf } from '../combinators/token.ts'
36
36
  import { noTrivia, parser } from '../combinators/grammar.ts'
37
37
  import { attempt } from '../combinators/attempt.ts'
38
+ import { optional } from '../combinators/repeat.ts'
38
39
  import { mayCommitFailure } from '../analysis/commitment.ts'
39
40
 
41
+ /**
42
+ * The choices this module builds. The interpreter never runs them: it runs the
43
+ * authored choice, so a factored one's `disjoint` says nothing about which arms
44
+ * the interpreter lets in (`Encoder` classifies them by their derived sets).
45
+ */
46
+ export const factoredChoices = new WeakSet<Combinator<unknown>>()
47
+
40
48
  type ChoiceDef = Extract<ParserDef, { tag: 'choice' }>
41
49
 
42
50
  /** Own keys every combinator may carry beside its definition; all derived from it. */
@@ -151,6 +159,7 @@ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined
151
159
  // it walks everything the arms reach.
152
160
  if (arms.some(a => mayCommitFailure(a))) return undefined
153
161
  const middle = choice(...(shapes.map(s => s.terms[at]!) as [Combinator<unknown>, ...Combinator<unknown>[]]))
162
+ factoredChoices.add(middle)
154
163
  // The inner choice must keep arm order: a strategy that reorders (longest
155
164
  // literal first) or classifies would pick a different arm. Nor may it add an
156
165
  // automatic lookahead the arms as written do not have: `'start'` before
@@ -171,3 +180,56 @@ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined
171
180
  // `attempt`. The sequence alone would report where its inner choice failed.
172
181
  return out === undefined ? undefined : attempt(out)
173
182
  }
183
+
184
+ /**
185
+ * AN ARM WHOSE NEXT SIBLING IS ITS OWN PREFIX — `choice(sequence(X, a), X)`.
186
+ *
187
+ * On input where `X` matches and `a` does not, the choice parses `X`, fails `a`,
188
+ * rolls back, and parses `X` again for the next arm. Where the choice's value is
189
+ * unused (its sequence arms are `valueUnused`, so only their captures reach the
190
+ * enclosing node), the first arm may be `sequence(X, optional(a))`:
191
+ *
192
+ * - `X` then `a` matches: the arm as it was.
193
+ * - `X` matches, `a` fails uncommitted: the optional matches empty, rolls `a`'s
194
+ * captures back and leaves the gap before it unrecorded, so the arm ends where
195
+ * `X` did with `X`'s captures, which is what the retried next arm produced.
196
+ * - `a` fails committed: the arm fails committed, as it did.
197
+ * - `X` fails: the arm fails as before, and the next arm, kept as it is, fails the
198
+ * same way, so the choice merges the same sets in the same order.
199
+ *
200
+ * `X` is the next arm's sequence of terms, or that arm itself as a single term,
201
+ * and "the same" is `sameComb`. Gated arms, `autoNot` and strategies that reorder
202
+ * are left alone.
203
+ */
204
+ export function factorPrefixArms(d: ChoiceDef): Combinator<unknown> | undefined {
205
+ const arms = d.parsers
206
+ if (arms.length < 2) return undefined
207
+ if (d.strategy.tag !== 'firstMatch' && d.strategy.tag !== 'sharedPrefix') return undefined
208
+ if (d.gates.some(g => g !== null) || d.autoNot.some(a => a !== null)) return undefined
209
+ const out = [...arms]
210
+ let changed = false
211
+ for (let i = 0; i + 1 < arms.length; i++) {
212
+ const arm = arms[i]!
213
+ if (arm._def.tag !== 'sequence' || arm._def.valueUnused !== true) continue
214
+ const terms = arm._def.parsers
215
+ const next = arms[i + 1]!
216
+ const prefix = next._def.tag === 'sequence' && next._def.valueUnused === true ? next._def.parsers : [next]
217
+ if (prefix.length >= terms.length) continue
218
+ if (!prefix.every((t, j) => sameComb(t, terms[j]!, 0))) continue
219
+ const rest = terms.slice(prefix.length)
220
+ // A rest that cannot fail (already `optional`, or `many`) has nothing to factor.
221
+ if (rest.every(never)) continue
222
+ const tail = rest.length === 1 ? rest[0]! : sequence(...(rest as [Combinator<unknown>, ...Combinator<unknown>[]]))
223
+ if (tail !== rest[0]) (tail._def as { valueUnused?: boolean }).valueUnused = true
224
+ const parts: Combinator<unknown>[] = [...terms.slice(0, prefix.length), optional(tail)]
225
+ const seq = sequence(...(parts as [Combinator<unknown>, ...Combinator<unknown>[]]))
226
+ ;(seq._def as { valueUnused?: boolean }).valueUnused = true
227
+ out[i] = seq
228
+ changed = true
229
+ }
230
+ if (!changed) return undefined
231
+ const rebuilt = choice(...(out as [Combinator<unknown>, ...Combinator<unknown>[]]))
232
+ factoredChoices.add(rebuilt)
233
+ const st = rebuilt._def.tag === 'choice' ? rebuilt._def.strategy : undefined
234
+ return st !== undefined && (st.tag === 'firstMatch' || st.tag === 'sharedPrefix') ? rebuilt : undefined
235
+ }
@@ -24,7 +24,7 @@ import {
24
24
  OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE, OP_GUARD, OP_LABEL,
25
25
  OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK,
26
26
  OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
27
- OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
27
+ OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
28
28
  } from './ops.ts'
29
29
 
30
30
  export type FailClean = (ip: number) => boolean
@@ -35,7 +35,7 @@ export type FailClean = (ip: number) => boolean
35
35
  */
36
36
  function passChild(code: ArrayLike<number>, ip: number): number {
37
37
  switch (code[ip]) {
38
- case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN:
38
+ case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN: case OP_GAPGATE:
39
39
  return code[ip + 1]!
40
40
  case OP_GATE: case OP_NODE: case OP_NODE_TRACK: case OP_XFORM: case OP_FIELD: case OP_SCOPE:
41
41
  case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_LEAF: case OP_SOURCE_LEAF:
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AN OPTIONAL REPEAT ITEM THAT OPENS WITH A GAP (`OP_GAPGATE`).
3
+ *
4
+ * SCSS writes its operators as `sequence(pad, '*', pad)` with `pad` a regex that
5
+ * may match empty, and a value chain has a repeat of them at every level. After
6
+ * a value followed by a space, each level entered its operator, scanned the
7
+ * space and failed on the character after it: the same gap read once per level,
8
+ * and an operator node's frame opened each time.
9
+ *
10
+ * The repeat now reads the gap itself, ONCE, with the item's own regex, and
11
+ * looks at the character after it: an item that opens with the gap must
12
+ * continue with a character of `cls` there (and an arm without the gap must
13
+ * start with one of `other` where the gap starts). Where neither holds the item
14
+ * cannot match, so the repeat ends, exactly as it ends on a strict item whose
15
+ * class excludes the lead. Otherwise it enters the item and leaves the gap's end
16
+ * in the GAP SLOT, from which the regex row takes it instead of scanning again.
17
+ *
18
+ * The slot is a memo of one pure function: the end of regex row `row` matched
19
+ * at `at` in `input`. A row reads it only when all three are its own, so a
20
+ * different row, position or input cannot take it, and a slot left set can
21
+ * only ever give a row the end it would have computed. Only rows the table
22
+ * names as a gap (`gapRows`) test it at all. It is set for the item and cleared
23
+ * when the item returns.
24
+ *
25
+ * Not under tolerant recovery or a completions probe, which observe an item's
26
+ * failure: there the repeat enters the item as written. The reference driver
27
+ * (`exec.ts`) always does, which is what makes it the ungated leg.
28
+ */
29
+ import { childSlots } from './child-slots.ts'
30
+ import { OP_GAPGATE } from './ops.ts'
31
+
32
+ /** The gap slot one assembly shares between its closure and emitted pieces. */
33
+ export type GapSlot = { row: number; at: number; end: number; input: string | undefined }
34
+
35
+ export function newGapSlot(): GapSlot {
36
+ return { row: -1, at: -1, end: -1, input: undefined }
37
+ }
38
+
39
+ /** The regex rows some reachable `OP_GAPGATE` names as its gap. */
40
+ export function gapRows(code: ArrayLike<number>, roots: Iterable<number>): Set<number> {
41
+ const rows = new Set<number>()
42
+ const seen = new Set<number>()
43
+ const stack = [...roots]
44
+ const kids: number[] = []
45
+ while (stack.length > 0) {
46
+ const ip = stack.pop()!
47
+ if (seen.has(ip)) continue
48
+ seen.add(ip)
49
+ if (code[ip] === OP_GAPGATE) rows.add(code[ip + 2]!)
50
+ kids.length = 0
51
+ childSlots(code, ip, kids)
52
+ for (const c of kids) stack.push(c)
53
+ }
54
+ return rows
55
+ }
@@ -17,14 +17,14 @@
17
17
  import {
18
18
  OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
19
19
  OP_NODE, OP_NOT, OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
20
- OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
20
+ OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
21
21
  } from './ops.ts'
22
22
  import { childSlots } from './child-slots.ts'
23
23
  import { leadingTriviaFieldAt } from './field-spec.ts'
24
24
  import { regexCanMatchEmpty } from '../regex/first-set.ts'
25
25
 
26
26
  const WRAPPERS = new Set([OP_NODE, OP_XFORM, OP_LEAF, OP_SOURCE_LEAF, OP_TOKEN, OP_SCOPE, OP_SCOPE_CAP,
27
- OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_EXPECT, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV])
27
+ OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_EXPECT, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV, OP_GAPGATE])
28
28
 
29
29
  /**
30
30
  * Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
@@ -187,7 +187,7 @@ function sequence5(
187
187
 
188
188
  type ChoiceTail = (
189
189
  input: string, pos: number, ctx: ParseContext, lead: number,
190
- acc: string[] | undefined, best: number,
190
+ acc: string[] | undefined, best: number, entered: boolean,
191
191
  ) => number
192
192
 
193
193
  function appendExpected(acc: string[] | undefined, values: readonly string[] | undefined): string[] | undefined {
@@ -211,19 +211,22 @@ function choiceNode(
211
211
  return end < 0 ? next(input, pos) : end
212
212
  }
213
213
  }
214
- let tail: ChoiceTail = (_input, pos, ctx, _lead, acc) => {
214
+ let tail: ChoiceTail = (input, pos, ctx, _lead, acc, _best, entered) => {
215
+ // No arm was entered. A completion probe still sees each one fail on its
216
+ // opener, as the interpreter's entered arms leave it.
217
+ if (!entered && ctx._probe !== undefined) for (const arm of arms) arm.run(input, pos, ctx)
215
218
  ctx._fe = pos
216
219
  ctx._fx = acc ?? choiceExpected as string[]
217
220
  return -1
218
221
  }
219
222
  for (let i = arms.length - 1; i >= 0; i--) {
220
223
  const armRun = arms[i]!.run, cls = classes[i], staticExpected = armExpected[i]!, next = tail
221
- tail = (input, pos, ctx, lead, acc, best) => {
224
+ tail = (input, pos, ctx, lead, acc, best, entered) => {
222
225
  if (cls !== undefined && !classHas(cls, lead)) {
223
226
  return next(
224
227
  input, pos, ctx, lead,
225
228
  best === pos ? appendExpected(acc, staticExpected) : acc,
226
- best,
229
+ best, entered,
227
230
  )
228
231
  }
229
232
  ctx._fc = false
@@ -236,13 +239,13 @@ function choiceNode(
236
239
  if (nextAcc !== undefined) ctx._fx = nextAcc
237
240
  return -1
238
241
  }
239
- return next(input, pos, ctx, lead, nextAcc, best)
242
+ return next(input, pos, ctx, lead, nextAcc, best, true)
240
243
  }
241
244
  }
242
245
  return {
243
246
  match: matchTail,
244
247
  run: (input, pos, ctx) => tail(
245
- input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined, pos,
248
+ input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined, pos, false,
246
249
  ),
247
250
  }
248
251
  }
@@ -290,6 +293,9 @@ function exclusiveChoiceNode(
290
293
  const failed = ctx._fx
291
294
  if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return -1 }
292
295
  }
296
+ // A miss under a completions probe: each arm fails on its opener, as the
297
+ // interpreter's arms do when it runs them all (`choice.ts`).
298
+ if (arm < 0 && ctx._probe !== undefined) for (const a of arms) a.run(input, pos, ctx)
293
299
  ctx._fe = pos
294
300
  ctx._fx = choiceFx as string[]
295
301
  return -1
@@ -547,6 +553,9 @@ export function buildLexProgramRunner(
547
553
  if (!classHas(cls, lead)) {
548
554
  ctx._fe = pos
549
555
  ctx._fx = choiceFx.slice()
556
+ // Each arm fails here; under a probe their openers are recorded as the
557
+ // interpreter's entered arms record them.
558
+ if (ctx._probe !== undefined) failAt(ctx, [...x0, ...x1, ...x2, ...x3], pos)
550
559
  return -1
551
560
  }
552
561
  let acc: string[] | undefined
package/src/table/ops.ts CHANGED
@@ -38,11 +38,15 @@ export const OP_SEQV = 4
38
38
  */
39
39
  export const OP_CHOICE = 5
40
40
  /**
41
- * `REP c min max sep flags fx sepClass` — `sep` is a child offset or −1; `max`
42
- * −1 = ∞. `fx` is recovery metadata. The last class is the separator sentinel
43
- * for a separated list, otherwise the finite/non-nullable optional-item class
44
- * (or −1). `flags` bit 0 = trailing separator allowed, bit 1 =
45
- * `keepSeparators`, bit 2 = item expected reporting.
41
+ * `REP c min max sep flags fx sepClass [sepGate]` — EIGHT words, or NINE with
42
+ * flag bit 3. `sep` is a child offset or −1; `max` −1 = ∞. `fx` is the item's
43
+ * expected set. `sepClass` is the separator sentinel for a separated list,
44
+ * otherwise the finite/non-nullable optional-item class (or −1). `flags`: bit 0
45
+ * (`&1`) trailing separator allowed; bit 1 (`&2`) `keepSeparators`; bit 2 (`&4`)
46
+ * item expected reporting; bit 3 (`&8`) a ninth word at `ip + 8`, the class of
47
+ * characters the separator can start, so a strict list past its minimum ends
48
+ * where it cannot (`sepGate`); bit 4 (`&16`) the item's derived set is empty, so
49
+ * `fx` holds the item's tag for the minimum check and a resync error carries [].
46
50
  */
47
51
  export const OP_REP = 6
48
52
  /** `REPV …` — `REP` with `valueUnused`. */
@@ -70,13 +74,7 @@ export const OP_XFORM = 9
70
74
  * from the reducer's declared arity by the same analysis codegen runs, and
71
75
  * forced on under `hostMode: 'cst'`. The driver reads the bits and re-derives
72
76
  * nothing.
73
- *
74
- * TWO ENCODERS SHARE THIS OPCODE and they do NOT agree on its length. The layout
75
- * above is `encode.ts` / `exec.ts`. `encode-baseline.ts` / `exec-baseline.ts` emit
76
- * the FOUR-word form `NODE b c flags` with bits 2 and 3 only, and refuse
77
- * `collapse` / `unwrap` / `project` outright. Neither driver walks the stream by
78
- * instruction length, so the divergence is contained — but read the pair you are
79
- * in, not this comment alone. */
77
+ */
80
78
  export const OP_NODE = 10
81
79
  /** `RULE r` — `r` indexes `prog.rules`. */
82
80
  export const OP_RULE = 11
@@ -89,9 +87,6 @@ export const OP_NOT = 13
89
87
  * `['peek(<child tag>)']`, because `peek.ts:60` DISCARDS the body's expectation:
90
88
  * a lookahead's failure is "the guard did not hold", not a request for whatever
91
89
  * token the body happened to stop on.
92
- *
93
- * `encode-baseline.ts` / `exec-baseline.ts` emit and read the two-word form and
94
- * are internally consistent; read the pair you are in.
95
90
  */
96
91
  export const OP_PEEK = 14
97
92
  /** `LEAF f c` — `leaf()`: reduce, then record as a spanned leaf. */
@@ -486,11 +481,35 @@ export const OP_SOURCE_LEAF = 44
486
481
  * at this position, a completions probe, or tolerant recovery enters the arm.
487
482
  */
488
483
  export const OP_RGATE = 45
484
+ /**
485
+ * `GAPGATE c p cls other` — an optional repeat ITEM that opens with a gap: the
486
+ * pure regex row `p`, which may match empty, and then a term whose first set is
487
+ * the class `cls` (`other`: the class of any choice arm that does not open with
488
+ * `p`, or −1). `c` is the item. The row is TRANSPARENT wherever it runs: only
489
+ * the repeat that holds it reads its operands (`gap-gate.ts`).
490
+ */
491
+ export const OP_GAPGATE = 46
492
+ /**
493
+ * `RGATE2 cls child fx first second` — `OP_RGATE` with a SECOND character: where
494
+ * the character after the routed token is in `first`, the one after it must be
495
+ * in `second` too (the next term reads both, `regexFirstTwo`), or the arm fails
496
+ * there with `fx`, as entering it would.
497
+ */
498
+ export const OP_RGATE2 = 47
499
+ /**
500
+ * `STRICT strict tolerant` — one construct encoded two ways: `strict` for a
501
+ * strict parse, `tolerant` for tolerant recovery and a completions probe, which
502
+ * must see the construct as written (`factor-choice.ts` `factorPrefixArms`). An
503
+ * assembly resolves it to one child; it has no body of its own.
504
+ */
505
+ export const OP_STRICT = 48
489
506
  /**
490
507
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
491
508
  *
492
509
  * `sel` is the selector's offset, `d` indexes a dispatch table in `prog.dsp`,
493
- * `other` is the `otherwise()` offset (or −1), `otherRouted` is 1 when the
510
+ * `other` is the `otherwise()` offset (or −1), `otherRouted` bit 1 (`&2`) is set
511
+ * when the fallback's failure is NOT committed (`otherwise(p, { commit: false })`)
512
+ * and bit 0 when the
494
513
  * fallback consumes the routed token, and `a1…an` are the arm offsets. Arm
495
514
  * `usesRouted` bits live in the `dsp` entry beside the key maps.
496
515
  *
@@ -520,4 +539,7 @@ export const OP_NAMES: Record<number, string> = {
520
539
  [OP_SCOPE_PLAIN]: 'SCOPE_PLAIN',
521
540
  [OP_LEX_BODY]: 'LEX_BODY',
522
541
  [OP_LEX_PROGRAM]: 'LEX_PROGRAM',
542
+ [OP_GAPGATE]: 'GAPGATE',
543
+ [OP_RGATE2]: 'RGATE2',
544
+ [OP_STRICT]: 'STRICT',
523
545
  }
@@ -17,10 +17,15 @@ import { buildLexProgramRunner, type LexProgramRunner } from './lex-program.ts'
17
17
  * `EMITTED_PARAMS` in order. `plan` rebuilds the three data pools the factory
18
18
  * expects (see `PoolPlan`), and `reached` is the emitter's site set, which
19
19
  * `Assembly.reached` publishes and `test/unit/table-assemble.test.ts`
20
- * asserts on.
20
+ * asserts on. `arity` is the number of helpers the factory was printed against
21
+ * (`EMITTED_PARAMS.length` of the printing parseman); the runtime compares it with
22
+ * its own list before calling the factory. It is printed data, not
23
+ * `factory.length`, which a minifier that drops unused trailing parameters lowers.
24
+ * An assembly printed before 0.60.0 has none.
21
25
  */
22
26
  export type PrecompiledAssembly = {
23
27
  readonly key: number
28
+ readonly arity?: number
24
29
  readonly factory: EmittedFactory
25
30
  readonly plan: PoolPlan
26
31
  readonly reached: readonly number[]
@@ -433,7 +438,7 @@ export function validateDispatchSpec(
433
438
  return false
434
439
  }
435
440
  if (spec === undefined || !Number.isInteger(arity) || arity < 0
436
- || (fallbackRouted !== 0 && fallbackRouted !== 1)
441
+ || !Number.isInteger(fallbackRouted) || fallbackRouted < 0 || fallbackRouted > 3
437
442
  || spec.routed.length !== arity
438
443
  || spec.routed.some(value => value !== 0 && value !== 1)
439
444
  || invalid(spec.byKey.values()) || invalid(spec.byFold.values())
@@ -15,7 +15,7 @@
15
15
  * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
16
16
  */
17
17
  import { childSlots } from './child-slots.ts'
18
- import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
18
+ import { OP_CHOICE, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
19
19
 
20
20
  const LOOP = 8
21
21
  const ROUNDS = 60
@@ -62,7 +62,12 @@ function staticWeights(code: readonly number[], entries: readonly number[]): Map
62
62
  const scale = op === OP_REP || op === OP_REPV ? LOOP
63
63
  : op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
64
64
  : 1
65
- for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
65
+ for (const k of kids) {
66
+ // A gap gate has no body: weigh its item as the repeat's own child, so
67
+ // adding the row moves no other site's estimate (`gap-gate.ts`).
68
+ const kid = code[k] === OP_GAPGATE ? code[k + 1]! : k
69
+ next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
70
+ }
66
71
  }
67
72
  weight = next
68
73
  }