parseman 0.59.3 → 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 (84) hide show
  1. package/dist/analysis/diagnostics.js +874 -188
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +874 -188
  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 +894 -197
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +901 -199
  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 +14 -0
  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.map +1 -1
  31. package/dist/table/emit-assembly.d.ts +8 -2
  32. package/dist/table/emit-assembly.d.ts.map +1 -1
  33. package/dist/table/emit.d.ts +4 -3
  34. package/dist/table/emit.d.ts.map +1 -1
  35. package/dist/table/encode.d.ts.map +1 -1
  36. package/dist/table/exec.d.ts.map +1 -1
  37. package/dist/table/factor-choice.d.ts +27 -0
  38. package/dist/table/factor-choice.d.ts.map +1 -1
  39. package/dist/table/gap-gate.d.ts +11 -0
  40. package/dist/table/gap-gate.d.ts.map +1 -0
  41. package/dist/table/index.js +924 -199
  42. package/dist/table/index.js.map +3 -3
  43. package/dist/table/lex-program.d.ts.map +1 -1
  44. package/dist/table/ops.d.ts +34 -6
  45. package/dist/table/ops.d.ts.map +1 -1
  46. package/dist/table/program.d.ts +6 -1
  47. package/dist/table/program.d.ts.map +1 -1
  48. package/dist/types.d.ts +16 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/analysis/duplication.ts +1 -1
  53. package/src/combinators/attempt.ts +1 -1
  54. package/src/combinators/dispatch.ts +30 -6
  55. package/src/combinators/first-set.ts +55 -12
  56. package/src/combinators/node.ts +1 -1
  57. package/src/combinators/regex.ts +9 -4
  58. package/src/combinators/repeat.ts +6 -6
  59. package/src/combinators/sequence.ts +1 -1
  60. package/src/combinators/trivia-boundary.ts +18 -0
  61. package/src/combinators/trivia-skip.ts +1 -1
  62. package/src/compiler/ir-serialize.ts +1 -1
  63. package/src/compiler/token-alphabet.ts +3 -1
  64. package/src/coverage.ts +1 -1
  65. package/src/index.ts +1 -1
  66. package/src/plugin/evaluator.ts +8 -3
  67. package/src/regex/first-set.ts +37 -3
  68. package/src/table/assemble.ts +171 -25
  69. package/src/table/child-slots.ts +8 -3
  70. package/src/table/compile-rule-map.ts +6 -3
  71. package/src/table/emit-assembly.ts +182 -31
  72. package/src/table/emit.ts +6 -4
  73. package/src/table/encode.ts +468 -73
  74. package/src/table/exec.ts +38 -13
  75. package/src/table/factor-choice.ts +62 -0
  76. package/src/table/fail-clean.ts +2 -2
  77. package/src/table/gap-gate.ts +55 -0
  78. package/src/table/hold-gap.ts +2 -2
  79. package/src/table/lex-program.ts +15 -6
  80. package/src/table/ops.ts +37 -6
  81. package/src/table/program.ts +7 -2
  82. package/src/table/select-hot.ts +7 -2
  83. package/src/types.ts +12 -5
  84. package/src/version.ts +1 -1
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'
@@ -375,6 +375,11 @@ function makeDriver(
375
375
  }
376
376
 
377
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 (;;) {
378
383
  if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
379
384
  switch (code[ip]) {
380
385
  case OP_LIT: {
@@ -492,7 +497,7 @@ function makeDriver(
492
497
  ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
493
498
  return FAIL
494
499
  }
495
- return exec(code[ip + 2]!, input, pos, ctx)
500
+ ip = code[ip + 2]!; continue rows
496
501
  }
497
502
 
498
503
  case OP_LABEL: {
@@ -502,7 +507,7 @@ function makeDriver(
502
507
  return v
503
508
  }
504
509
  case OP_RULE:
505
- return exec(code[ip + 1]!, input, pos, ctx)
510
+ ip = code[ip + 1]!; continue rows
506
511
 
507
512
  /**
508
513
  * A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
@@ -518,8 +523,14 @@ function makeDriver(
518
523
  * a second implementation of it here would be a second place for the phase
519
524
  * semantics (entry for a rule, success for an arm) to drift.
520
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.
521
528
  case OP_COV:
522
- 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
523
534
 
524
535
  case OP_LIVE: {
525
536
  // A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
@@ -601,6 +612,7 @@ function makeDriver(
601
612
 
602
613
  let target: number
603
614
  let usesRouted: boolean
615
+ let commits = true
604
616
  if (arm === undefined) {
605
617
  const other = code[ip + 3]!
606
618
  if (other < 0) {
@@ -610,7 +622,8 @@ function makeDriver(
610
622
  return FAIL
611
623
  }
612
624
  target = other
613
- usesRouted = code[ip + 4]! === 1
625
+ usesRouted = (code[ip + 4]! & 1) === 1
626
+ commits = (code[ip + 4]! & 2) === 0
614
627
  } else {
615
628
  target = code[armBase + arm]!
616
629
  usesRouted = spec.routed[arm] === 1
@@ -637,8 +650,8 @@ function makeDriver(
637
650
  rollbackTrivia(ctx, mark)
638
651
  // The interpreter marks a failed dispatch branch COMMITTED: the
639
652
  // selector already matched, so an enclosing choice must not treat this
640
- // as "try the next arm".
641
- ctx._fc = true
653
+ // as "try the next arm". An uncommitted fallback keeps its own flag.
654
+ if (commits) ctx._fc = true
642
655
  return FAIL
643
656
  }
644
657
  // The end cell already holds the branch's end — dispatch's span runs from `pos`
@@ -1029,6 +1042,12 @@ function makeDriver(
1029
1042
  // its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
1030
1043
  // `encode.ts` recomputes around), so they firstMatch all seven arms where
1031
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
+ }
1032
1051
  return failChoice()
1033
1052
  }
1034
1053
  // THE PER-ARM GATE. Arms in source order, each skipped when its own
@@ -1063,7 +1082,9 @@ function makeDriver(
1063
1082
  const armFx = base + n
1064
1083
  for (let i = 0; i < n; i++) {
1065
1084
  const cls = armCls[i]
1066
- 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)) {
1067
1088
  if (COUNT) tableCounters.armGateSkips++
1068
1089
  // THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
1069
1090
  // enters the arm, the arm's opener fails, and its static set is what
@@ -1119,14 +1140,17 @@ function makeDriver(
1119
1140
  return sup
1120
1141
  }
1121
1142
 
1122
- 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: {
1123
1147
  const item = ctx._routed
1124
1148
  if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
1125
1149
  && !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
1126
1150
  ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
1127
1151
  return FAIL
1128
1152
  }
1129
- return exec(code[ip + 2]!, input, pos, ctx)
1153
+ ip = code[ip + 2]!; continue rows
1130
1154
  }
1131
1155
 
1132
1156
  case OP_ARMGATE: {
@@ -1141,7 +1165,7 @@ function makeDriver(
1141
1165
  ctx._fx = fx[code[ip + 3]!] as string[]
1142
1166
  return FAIL
1143
1167
  }
1144
- return exec(code[ip + 2]!, input, pos, ctx)
1168
+ ip = code[ip + 2]!; continue rows
1145
1169
  }
1146
1170
 
1147
1171
  case OP_REJECT: {
@@ -1225,7 +1249,7 @@ function makeDriver(
1225
1249
  // own sync. The item's expected set is at ip + 6 and the separator's
1226
1250
  // sentinel class at ip + 7, both laid down only for a recovery table.
1227
1251
  const mySync = REC ? ctx._sync : undefined
1228
- 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[]
1229
1253
  const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
1230
1254
  const itemClassIndex = sep < 0 ? code[ip + 7]! : -1
1231
1255
  const itemClass = REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
@@ -1644,6 +1668,7 @@ function makeDriver(
1644
1668
  default:
1645
1669
  throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
1646
1670
  }
1671
+ }
1647
1672
  }
1648
1673
 
1649
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
@@ -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`. */
@@ -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[]
@@ -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
  }
package/src/types.ts CHANGED
@@ -58,14 +58,14 @@ export type ParserDef =
58
58
  // force interpreter fallback). Absent under runtime compile() — the real gate
59
59
  // closures live in `gates`.
60
60
  | { tag: 'choice'; parsers: Combinator<unknown>[]; gates: (((state: unknown) => boolean) | null)[]; gateSrcs?: (string | null)[]; disjoint: boolean; strategy: ChoiceStrategy; autoNot: (AutoNotCheck[] | null)[] }
61
- | { tag: 'dispatch'; selector: Combinator<string>; cases: readonly DispatchCase[]; matchers?: readonly DispatchMatcherCase[] | undefined; otherwise?: Combinator<unknown> | undefined; otherwiseUsesRouted?: boolean | undefined }
62
- | { tag: 'attempt'; parser: Combinator<unknown>; /** `attempt(p, { contain: true })`: a committed failure inside is reported uncommitted. */ contain?: true }
61
+ | { tag: 'dispatch'; selector: Combinator<string>; cases: readonly DispatchCase[]; matchers?: readonly DispatchMatcherCase[] | undefined; otherwise?: Combinator<unknown> | undefined; otherwiseUsesRouted?: boolean | undefined; otherwiseCommit?: false | undefined }
62
+ | { tag: 'attempt'; parser: Combinator<unknown>; /** `attempt(p, { contain: true })`: a committed failure inside is reported uncommitted. */ contain?: true; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
63
63
  // The TAG carries NULLABILITY (what every downstream switch keys on): `many` is
64
64
  // the nullable min-0 repeat, `oneOrMore` the non-nullable min>=1 one. `min`/`max`
65
65
  // carry the actual ITEM bounds (`many(x, { min: 3, max: 8 })` is a `oneOrMore`
66
66
  // def with `min: 3, max: 8`). `max` absent = unbounded.
67
- | { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean }
68
- | { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean }
67
+ | { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
68
+ | { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
69
69
  | { tag: 'optional'; parser: Combinator<unknown> }
70
70
  // `min`/`max` count ITEMS, not separators. `min: 0` (the default) is
71
71
  // `(item (sep item)*)?` — NULLABLE, matches the empty string. `min >= 1` is
@@ -92,7 +92,7 @@ export type ParserDef =
92
92
  // it requires, so it carries `parser`'s first-set and a leading `peek()` gates
93
93
  // its choice arm (see `isPositiveLookahead` / `sequenceFirstSet`).
94
94
  | { tag: 'peek'; parser: Combinator<unknown> }
95
- | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
95
+ | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[]; /** The interpreter tests the child's live `_meta.firstSet` before entering it: its set was finite when this was built. */ guarded?: true }
96
96
  // `predSrc`/`extraSrc` (set by the macro evaluator): SOURCE TEXT of the guard
97
97
  // predicate / the withCtx `extra` value, so codegen inlines them into `_mf`
98
98
  // rather than pushing a `null` source. Absent under runtime compile() (the real
@@ -461,6 +461,13 @@ export type ParseError = {
461
461
  export type ParserMeta = {
462
462
  /** Character codes / ranges that can start this parser (for choice dispatch) */
463
463
  firstSet: FirstSet
464
+ /**
465
+ * Set only by `regex()` for a pattern that can match empty: the first chars
466
+ * of its NON-EMPTY matches. `firstSet` stays `any` for such a pattern, because
467
+ * it may match empty anywhere; a sequence, which tracks nullability itself,
468
+ * unions this set with the next term's instead (`sequenceSet`).
469
+ */
470
+ consumingFirstSet?: FirstSet
464
471
  /** Whether this parser can consume a newline character */
465
472
  canMatchNewline: boolean
466
473
  /** Whether this parser is marked as trivia (auto-skip) */
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.59.3'
16
+ export const PARSEMAN_VERSION = '0.60.0'