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
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'
@@ -30,9 +30,8 @@ import { failAt } from '../combinators/probe.ts'
30
30
  /**
31
31
  * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
32
32
  *
33
- * `failAt` (combinators/probe.ts) is called from exactly three places in the
34
- * interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
35
- * mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
33
+ * `failAt` (combinators/probe.ts) is called at the interpreter's terminal fail
34
+ * sites — `literal.ts`, `regex.ts`, `keywords.ts`, `lines.ts`. The table recorded
36
35
  * NOTHING, so `completionsAt` on a table artifact saw only the top-level
37
36
  * failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
38
37
  * `{ decl; }` grammar) never reached the probe and the item's opener vanished
@@ -376,6 +375,11 @@ function makeDriver(
376
375
  }
377
376
 
378
377
  function exec(ip: number, input: string, pos: number, ctx: ParseContext): unknown {
378
+ // A row that only hands on to one child at the same position (a gate that
379
+ // passes, a rule trampoline, `OP_STRICT`) continues here rather than
380
+ // recursing, so the reference driver's stack grows with the grammar's
381
+ // nesting, not with its gate rows.
382
+ rows: for (;;) {
379
383
  if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
380
384
  switch (code[ip]) {
381
385
  case OP_LIT: {
@@ -493,7 +497,7 @@ function makeDriver(
493
497
  ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
494
498
  return FAIL
495
499
  }
496
- return exec(code[ip + 2]!, input, pos, ctx)
500
+ ip = code[ip + 2]!; continue rows
497
501
  }
498
502
 
499
503
  case OP_LABEL: {
@@ -503,7 +507,7 @@ function makeDriver(
503
507
  return v
504
508
  }
505
509
  case OP_RULE:
506
- return exec(code[ip + 1]!, input, pos, ctx)
510
+ ip = code[ip + 1]!; continue rows
507
511
 
508
512
  /**
509
513
  * A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
@@ -519,8 +523,14 @@ function makeDriver(
519
523
  * a second implementation of it here would be a second place for the phase
520
524
  * semantics (entry for a rule, success for an arm) to drift.
521
525
  */
526
+ // The reference runs a gap-gated item as written: the gate is the repeat's,
527
+ // in the shipped engines, and this driver is the leg it is compared with.
522
528
  case OP_COV:
523
- return exec(code[ip + 1]!, input, pos, ctx)
529
+ case OP_GAPGATE:
530
+ ip = code[ip + 1]!; continue rows
531
+
532
+ case OP_STRICT:
533
+ ip = code[ctx._tolerant || ctx._probe !== undefined ? ip + 2 : ip + 1]!; continue rows
524
534
 
525
535
  case OP_LIVE: {
526
536
  // A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
@@ -552,7 +562,7 @@ function makeDriver(
552
562
  const err = { _tag: 'parseError' as const, span, expected: fx[code[ip + 2]!] as string[] }
553
563
  ctx._errors?.push(err)
554
564
  // A TOLERANT `expect()` also embeds the error in the TREE (expect.ts:150,
555
- // codegen.ts:4470), so a tree walk finds every diagnostic and it survives
565
+ // as the source lowering did), so a tree walk finds every diagnostic and it survives
556
566
  // incremental subtree reuse — the flat sink alone is not the contract.
557
567
  if (REC && ctx._tolerant === true) captureError(ctx, err)
558
568
  // See assemble.ts OP_EXPECT: the recovered failure is a SUCCESS, so the
@@ -602,6 +612,7 @@ function makeDriver(
602
612
 
603
613
  let target: number
604
614
  let usesRouted: boolean
615
+ let commits = true
605
616
  if (arm === undefined) {
606
617
  const other = code[ip + 3]!
607
618
  if (other < 0) {
@@ -611,7 +622,8 @@ function makeDriver(
611
622
  return FAIL
612
623
  }
613
624
  target = other
614
- usesRouted = code[ip + 4]! === 1
625
+ usesRouted = (code[ip + 4]! & 1) === 1
626
+ commits = (code[ip + 4]! & 2) === 0
615
627
  } else {
616
628
  target = code[armBase + arm]!
617
629
  usesRouted = spec.routed[arm] === 1
@@ -638,8 +650,8 @@ function makeDriver(
638
650
  rollbackTrivia(ctx, mark)
639
651
  // The interpreter marks a failed dispatch branch COMMITTED: the
640
652
  // selector already matched, so an enclosing choice must not treat this
641
- // as "try the next arm".
642
- ctx._fc = true
653
+ // as "try the next arm". An uncommitted fallback keeps its own flag.
654
+ if (commits) ctx._fc = true
643
655
  return FAIL
644
656
  }
645
657
  // The end cell already holds the branch's end — dispatch's span runs from `pos`
@@ -1030,6 +1042,12 @@ function makeDriver(
1030
1042
  // its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
1031
1043
  // `encode.ts` recomputes around), so they firstMatch all seven arms where
1032
1044
  // this driver dispatches to one. The arm sets differ, not the rule.
1045
+ //
1046
+ // Under a completions probe the interpreter runs every arm on a miss
1047
+ // (`choice.ts`), so each opener reaches the probe; so does this.
1048
+ if (arm < 0 && ctx._probe !== undefined) {
1049
+ for (let i = 0; i < code[ip + 2]!; i++) exec(code[base + i]!, input, pos, ctx)
1050
+ }
1033
1051
  return failChoice()
1034
1052
  }
1035
1053
  // THE PER-ARM GATE. Arms in source order, each skipped when its own
@@ -1064,7 +1082,9 @@ function makeDriver(
1064
1082
  const armFx = base + n
1065
1083
  for (let i = 0; i < n; i++) {
1066
1084
  const cls = armCls[i]
1067
- if (cls !== undefined && cls !== null && !classHas(cls, c)) {
1085
+ // Under a completion probe every arm is entered, as the interpreter's
1086
+ // `firstMatch` enters it: its opener then reaches the probe (`failAt`).
1087
+ if (ctx._probe === undefined && cls !== undefined && cls !== null && !classHas(cls, c)) {
1068
1088
  if (COUNT) tableCounters.armGateSkips++
1069
1089
  // THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
1070
1090
  // enters the arm, the arm's opener fails, and its static set is what
@@ -1120,14 +1140,17 @@ function makeDriver(
1120
1140
  return sup
1121
1141
  }
1122
1142
 
1123
- case OP_RGATE: {
1143
+ // The reference tests the first character only: the second is the shipped
1144
+ // engines' (`OP_RGATE2`), and this driver is the leg they are compared with.
1145
+ case OP_RGATE:
1146
+ case OP_RGATE2: {
1124
1147
  const item = ctx._routed
1125
1148
  if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
1126
1149
  && !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
1127
1150
  ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
1128
1151
  return FAIL
1129
1152
  }
1130
- return exec(code[ip + 2]!, input, pos, ctx)
1153
+ ip = code[ip + 2]!; continue rows
1131
1154
  }
1132
1155
 
1133
1156
  case OP_ARMGATE: {
@@ -1142,7 +1165,7 @@ function makeDriver(
1142
1165
  ctx._fx = fx[code[ip + 3]!] as string[]
1143
1166
  return FAIL
1144
1167
  }
1145
- return exec(code[ip + 2]!, input, pos, ctx)
1168
+ ip = code[ip + 2]!; continue rows
1146
1169
  }
1147
1170
 
1148
1171
  case OP_REJECT: {
@@ -1222,11 +1245,11 @@ function makeDriver(
1222
1245
  // TOLERANT RECOVERY, dormant unless `ctx._tolerant`. `mySync` is captured
1223
1246
  // at ENTRY because an element's own sequence publishes over `_sync` while
1224
1247
  // it runs — the interpreter restores in a `finally` (sequence.ts:105) and
1225
- // codegen saves at entry (codegen.ts:3032); either way this is the list's
1248
+ // the source lowering saved at entry; either way this is the list's
1226
1249
  // own sync. The item's expected set is at ip + 6 and the separator's
1227
1250
  // sentinel class at ip + 7, both laid down only for a recovery table.
1228
1251
  const mySync = REC ? ctx._sync : undefined
1229
- const recFx = REC ? fx[code[ip + 6]!] as string[] : undefined
1252
+ const recFx = !REC ? undefined : (code[ip + 5]! & 16) !== 0 ? [] : fx[code[ip + 6]!] as string[]
1230
1253
  const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
1231
1254
  const itemClassIndex = sep < 0 ? code[ip + 7]! : -1
1232
1255
  const itemClass = REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
@@ -1645,6 +1668,7 @@ function makeDriver(
1645
1668
  default:
1646
1669
  throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
1647
1670
  }
1671
+ }
1648
1672
  }
1649
1673
 
1650
1674
  /**
@@ -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
@@ -1,21 +1,21 @@
1
1
  /**
2
2
  * Opcodes for the TABLE lowering (design ledger row G5).
3
3
  *
4
- * The premise, stated so it can be falsified: today every rule's recognizer is
5
- * INLINED BESPOKE into its own emitted function, so an artifact pays the full
6
- * cost of the recognition machinery once PER RULE (measured: ~4.9 kB for a
7
- * `node()` rule; ~186 B for each additional call site). Emitting a rule as a
8
- * TABLE ROW interpreted by one shared driver moves that machinery out of the
9
- * artifact and into the runtime, where it is emitted ONCE for all grammars and
10
- * all variants.
4
+ * The source lowering this replaced (removed in 0.47.0) inlined every rule's
5
+ * recognizer into its own emitted function, so an artifact paid for the
6
+ * recognition machinery once PER RULE (measured then: ~4.9 kB for a `node()`
7
+ * rule; ~186 B for each additional call site). As TABLE ROWS run by one shared
8
+ * driver, that machinery lives in the runtime, once for all grammars and all
9
+ * variants.
11
10
  *
12
11
  * The instruction stream is a flat `number[]`: `code[ip]` is the opcode and the
13
12
  * operands follow it inline. Child instructions are referenced by ABSOLUTE code
14
- * offset, never by nesting, so the driver is a plain recursive read — no node
15
- * objects, no closures, no per-combinator allocation.
13
+ * offset, never by nesting.
16
14
  *
17
- * Operand layouts are documented per opcode below and are the contract between
18
- * `encode.ts` and `exec.ts`. Nothing else may read the stream.
15
+ * Operand layouts are documented per opcode below. `encode.ts` writes them; the
16
+ * engines (`assemble.ts`, `emit-assembly.ts`, the reference `exec.ts`) and the
17
+ * table analyses (`child-slots.ts`, `site-labels.ts`, `fail-clean.ts`, …) read
18
+ * them.
19
19
  */
20
20
 
21
21
  /** `LIT k fx` — `k` indexes a string in the const pool; `fx` the expected set on failure. */
@@ -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`. */
@@ -257,7 +261,7 @@ export const OP_GUARD = 32
257
261
  * would become a no-op and `notAdjacent()` a guaranteed failure. So the SEQ
258
262
  * pieces read this row at assembly and run the test themselves, exactly as
259
263
  * `sequence()` forks `parseAdjacent` (combinators/sequence.ts:118) and as
260
- * codegen lowers it at the boundary (codegen.ts:1765).
264
+ * the source lowering lowered it at the boundary.
261
265
  *
262
266
  * Reached as a row in its own right only where there IS no boundary — a bare
263
267
  * choice arm, a `node()` body, a repeat item. The interpreter throws there
@@ -367,7 +371,7 @@ export const OP_ARMGATE = 36
367
371
  * library never sees.
368
372
  *
369
373
  * It exists because codegen accepts exactly this and delegates at run time
370
- * (`compiler/codegen.ts`, `emitRuntimeFallback`'s `_rp[i].parse(...)` row). The
374
+ * (the source lowering's `emitRuntimeFallback`). The
371
375
  * encoder used to throw `UnsupportedConstruct`, so the table lowering REJECTED
372
376
  * grammars the source lowering compiles, and the two lowerings must accept the
373
377
  * same language.
@@ -436,7 +440,7 @@ export const OP_LABEL = 39
436
440
  * `on` is 0 for ENTRY and 1 for SUCCESS, and it is read at ASSEMBLY TIME, never
437
441
  * per parse: `assemble.ts` picks one of two closures from it, exactly as it picks
438
442
  * pieces from `RunCfg`. The two phases are not decoration — the source lowering
439
- * credits a RULE on entry (`codegen.ts:4529`) and a choice arm, dispatch arm or
443
+ * credited a RULE on entry and a choice arm, dispatch arm or
440
444
  * label only once it has SUCCEEDED, and a table that credited all four alike
441
445
  * would report a different denominator's worth of hits than the engine it is a
442
446
  * drop-in for.
@@ -486,11 +490,35 @@ export const OP_SOURCE_LEAF = 44
486
490
  * at this position, a completions probe, or tolerant recovery enters the arm.
487
491
  */
488
492
  export const OP_RGATE = 45
493
+ /**
494
+ * `GAPGATE c p cls other` — an optional repeat ITEM that opens with a gap: the
495
+ * pure regex row `p`, which may match empty, and then a term whose first set is
496
+ * the class `cls` (`other`: the class of any choice arm that does not open with
497
+ * `p`, or −1). `c` is the item. The row is TRANSPARENT wherever it runs: only
498
+ * the repeat that holds it reads its operands (`gap-gate.ts`).
499
+ */
500
+ export const OP_GAPGATE = 46
501
+ /**
502
+ * `RGATE2 cls child fx first second` — `OP_RGATE` with a SECOND character: where
503
+ * the character after the routed token is in `first`, the one after it must be
504
+ * in `second` too (the next term reads both, `regexFirstTwo`), or the arm fails
505
+ * there with `fx`, as entering it would.
506
+ */
507
+ export const OP_RGATE2 = 47
508
+ /**
509
+ * `STRICT strict tolerant` — one construct encoded two ways: `strict` for a
510
+ * strict parse, `tolerant` for tolerant recovery and a completions probe, which
511
+ * must see the construct as written (`factor-choice.ts` `factorPrefixArms`). An
512
+ * assembly resolves it to one child; it has no body of its own.
513
+ */
514
+ export const OP_STRICT = 48
489
515
  /**
490
516
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
491
517
  *
492
518
  * `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
519
+ * `other` is the `otherwise()` offset (or −1), `otherRouted` bit 1 (`&2`) is set
520
+ * when the fallback's failure is NOT committed (`otherwise(p, { commit: false })`)
521
+ * and bit 0 when the
494
522
  * fallback consumes the routed token, and `a1…an` are the arm offsets. Arm
495
523
  * `usesRouted` bits live in the `dsp` entry beside the key maps.
496
524
  *
@@ -520,4 +548,7 @@ export const OP_NAMES: Record<number, string> = {
520
548
  [OP_SCOPE_PLAIN]: 'SCOPE_PLAIN',
521
549
  [OP_LEX_BODY]: 'LEX_BODY',
522
550
  [OP_LEX_PROGRAM]: 'LEX_PROGRAM',
551
+ [OP_GAPGATE]: 'GAPGATE',
552
+ [OP_RGATE2]: 'RGATE2',
553
+ [OP_STRICT]: 'STRICT',
523
554
  }
@@ -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[]
@@ -29,10 +34,10 @@ export type PrecompiledAssembly = {
29
34
  /**
30
35
  * The emitted form of a grammar under the table lowering.
31
36
  *
32
- * This object is ALL a grammar contributes to a bundle. Every line of
33
- * recognition logic lives in `exec.ts`, which ships once in the runtime and is
34
- * shared by every grammar and every variant. A rule costs its row, not a
35
- * function.
37
+ * This object is ALL a grammar contributes to a bundle, apart from any
38
+ * build-time assemblies in `asm`. The recognition logic is the shared driver
39
+ * (`tableRules`, `assemble.ts`), which ships once in the runtime for every
40
+ * grammar and every variant. A rule costs its row, not a function.
36
41
  *
37
42
  * Serialized shape is deliberately JSON-ish (numbers, strings, and a pool of
38
43
  * author callbacks) so the emitter can print it as a literal.
@@ -145,9 +150,9 @@ export type TableProgram = {
145
150
  */
146
151
  readonly triviaSpecs?: readonly TriviaSpec[]
147
152
  /**
148
- * 1 when this table was encoded with `TableSettings.recovery` — the sequences
149
- * carry their inferred follow-set classes and the repetitions their item
150
- * expected-set and separator sentinel class.
153
+ * Always 1: every table carries recovery operands — the sequences their inferred
154
+ * follow-set classes, the repetitions their item expected set and separator
155
+ * sentinel class. (`TableSettings.recovery` is a deprecated no-op.)
151
156
  *
152
157
  * TABLE DATA, read once when the driver/assembly is built. It selects the
153
158
  * recovery pieces; it is not a per-parse option, and a recovery table still
@@ -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
  }
@@ -173,7 +173,7 @@ export function stampRuleMap(
173
173
  // `committed` IS PART OF THE FAILURE ENVELOPE, not a diagnostic extra.
174
174
  // The interpreter carries it on the result (`{ ok: false, …, committed: true }`)
175
175
  // and codegen re-derives it from the same bit this driver uses
176
- // (`codegen.ts`: `...(_ctx._fc ? { committed: true } : {})`). The table
176
+ // (`...(_ctx._fc ? { committed: true } : {})`). The table
177
177
  // dropped it, so a table entry embedded as a CHILD of another parser — the
178
178
  // one place the field is read rather than merely reported — lost the cut.
179
179
  return committed(ctx)