parseman 0.56.0 → 0.58.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 (106) hide show
  1. package/dist/analysis/diagnostics.js +2232 -1088
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +2247 -1103
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/combinators/node.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts +1 -1
  8. package/dist/compiler/build-arity.d.ts.map +1 -1
  9. package/dist/compiler/token-alphabet.d.ts +11 -1
  10. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  11. package/dist/compiler/token-capability.js +69 -16
  12. package/dist/compiler/token-capability.js.map +2 -2
  13. package/dist/cst/capture-buffer.d.ts +6 -0
  14. package/dist/cst/capture-buffer.d.ts.map +1 -1
  15. package/dist/cst/trivia-kinds.d.ts +9 -1
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.js +1446 -236
  18. package/dist/index.js.map +3 -3
  19. package/dist/language-service/index.js +1 -0
  20. package/dist/language-service/index.js.map +1 -1
  21. package/dist/parse-context.d.ts.map +1 -1
  22. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  23. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  24. package/dist/plugin/index.d.ts +51 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +2126 -380
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +2 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -1
  30. package/dist/regex/classes.d.ts.map +1 -1
  31. package/dist/run/index.js +1 -0
  32. package/dist/run/index.js.map +1 -1
  33. package/dist/table/assemble.d.ts +9 -0
  34. package/dist/table/assemble.d.ts.map +1 -1
  35. package/dist/table/child-slots.d.ts +4 -6
  36. package/dist/table/child-slots.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +14 -2
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/emit-assembly.d.ts +17 -2
  40. package/dist/table/emit-assembly.d.ts.map +1 -1
  41. package/dist/table/emit.d.ts +5 -1
  42. package/dist/table/emit.d.ts.map +1 -1
  43. package/dist/table/encode.d.ts.map +1 -1
  44. package/dist/table/exec-baseline.d.ts.map +1 -1
  45. package/dist/table/exec.d.ts.map +1 -1
  46. package/dist/table/factor-choice.d.ts +42 -0
  47. package/dist/table/factor-choice.d.ts.map +1 -0
  48. package/dist/table/fail-clean.d.ts +3 -0
  49. package/dist/table/fail-clean.d.ts.map +1 -0
  50. package/dist/table/hold-gap.d.ts +8 -0
  51. package/dist/table/hold-gap.d.ts.map +1 -0
  52. package/dist/table/index.js +1798 -316
  53. package/dist/table/index.js.map +3 -3
  54. package/dist/table/lex-program.d.ts +7 -3
  55. package/dist/table/lex-program.d.ts.map +1 -1
  56. package/dist/table/ops.d.ts +17 -6
  57. package/dist/table/ops.d.ts.map +1 -1
  58. package/dist/table/program.d.ts +13 -0
  59. package/dist/table/program.d.ts.map +1 -1
  60. package/dist/table/run-support.d.ts +19 -0
  61. package/dist/table/run-support.d.ts.map +1 -1
  62. package/dist/table/scalar-terminal.d.ts +2 -0
  63. package/dist/table/scalar-terminal.d.ts.map +1 -1
  64. package/dist/table/select-hot.d.ts +6 -0
  65. package/dist/table/select-hot.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts +66 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/table/trivia-gap.d.ts +31 -0
  69. package/dist/table/trivia-gap.d.ts.map +1 -0
  70. package/dist/types.d.ts +17 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/version.d.ts +1 -1
  73. package/package.json +4 -1
  74. package/src/combinators/dispatch.ts +12 -3
  75. package/src/combinators/node.ts +6 -13
  76. package/src/combinators/trivia-skip.ts +44 -1
  77. package/src/compiler/build-arity.ts +3 -3
  78. package/src/compiler/token-alphabet.ts +57 -17
  79. package/src/cst/capture-buffer.ts +25 -0
  80. package/src/cst/trivia-kinds.ts +12 -1
  81. package/src/parse-context.ts +1 -0
  82. package/src/plugin/entry-supercompiler.ts +8 -9
  83. package/src/plugin/index.ts +362 -42
  84. package/src/plugin/ir-eval.ts +7 -1
  85. package/src/regex/classes.ts +3 -0
  86. package/src/table/assemble.ts +371 -83
  87. package/src/table/child-slots.ts +6 -8
  88. package/src/table/compile-rule-map.ts +64 -15
  89. package/src/table/emit-assembly.ts +244 -42
  90. package/src/table/emit.ts +11 -8
  91. package/src/table/encode.ts +149 -35
  92. package/src/table/exec-baseline.ts +1 -3
  93. package/src/table/exec.ts +34 -14
  94. package/src/table/factor-choice.ts +173 -0
  95. package/src/table/fail-clean.ts +130 -0
  96. package/src/table/hold-gap.ts +68 -0
  97. package/src/table/lex-program.ts +197 -43
  98. package/src/table/ops.ts +18 -7
  99. package/src/table/program.ts +24 -0
  100. package/src/table/run-support.ts +35 -0
  101. package/src/table/scalar-terminal.ts +8 -0
  102. package/src/table/select-hot.ts +70 -0
  103. package/src/table/site-labels.ts +135 -4
  104. package/src/table/trivia-gap.ts +109 -0
  105. package/src/types.ts +18 -0
  106. package/src/version.ts +1 -1
@@ -0,0 +1,130 @@
1
+ /**
2
+ * WHICH ROWS FAIL CLEAN.
3
+ *
4
+ * A choice, an `optional()` and a repeat item take a rollback mark before they
5
+ * try their child and restore it when the child fails, so a failed attempt leaves
6
+ * nothing in the capture sinks: the open frame's children, raw entries and trivia
7
+ * rows, the fields, the errors and the two trivia logs. Most children never write
8
+ * to a sink before they fail. A terminal fails before it pushes its leaf; a node
9
+ * that fails drops its own frame; a sequence whose later terms cannot fail can
10
+ * only fail in its first term, which has no trivia in front of it. For such a
11
+ * child the mark and the restore are work with nothing to undo.
12
+ *
13
+ * `failsClean(ip)` answers, from the table alone, whether every failure of the
14
+ * row at `ip` leaves every sink as the row found it. Only uncommitted failures
15
+ * count: a committed failure is propagated past every choice, optional and
16
+ * repeat without their restore, so the mark is never read for it.
17
+ *
18
+ * It is one memoised walk, run while the assembler links, so it is paid on the
19
+ * first parse. A rule reached again while it is still being answered counts as
20
+ * dirty, and so does a row whose behaviour is not modelled here: `false` keeps
21
+ * the mark, so every approximation errs towards the old cost, never a wrong tree.
22
+ */
23
+ import {
24
+ OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE, OP_GUARD, OP_LABEL,
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
+ 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,
28
+ } from './ops.ts'
29
+
30
+ export type FailClean = (ip: number) => boolean
31
+
32
+ /**
33
+ * The child offset of a row that fails exactly when its one child fails and
34
+ * writes nothing of its own first, or −1.
35
+ */
36
+ function passChild(code: ArrayLike<number>, ip: number): number {
37
+ switch (code[ip]) {
38
+ case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN:
39
+ return code[ip + 1]!
40
+ case OP_GATE: case OP_NODE: case OP_NODE_TRACK: case OP_XFORM: case OP_FIELD: case OP_SCOPE:
41
+ case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_LEAF: case OP_SOURCE_LEAF:
42
+ return code[ip + 2]!
43
+ default:
44
+ return -1
45
+ }
46
+ }
47
+
48
+ /** Sequence terms: `[first offset, count]` packed as the offset of the count word, or −1. */
49
+ function seqCount(code: ArrayLike<number>, ip: number): number {
50
+ const op = code[ip]
51
+ return op === OP_SEQ || op === OP_SEQV ? ip + 1 : op === OP_SEQX ? ip + 2 : -1
52
+ }
53
+
54
+ export function failCleanAnalysis(code: ArrayLike<number>): FailClean {
55
+ // Per row: 0 unknown, 1 yes, 2 no, 3 being answered (answered as no).
56
+ const cleanAt = new Uint8Array(code.length)
57
+ const neverAt = new Uint8Array(code.length)
58
+
59
+ /** No uncommitted failure at all. */
60
+ const neverFails = (ip: number): boolean => {
61
+ const hit = neverAt[ip]!
62
+ if (hit !== 0) return hit === 1
63
+ neverAt[ip] = 3
64
+ let out = false
65
+ const op = code[ip]
66
+ if (op === OP_OPT || op === OP_EMPTY || op === OP_EXPECT) out = true
67
+ else if (op === OP_REP || op === OP_REPV) out = code[ip + 2] === 0
68
+ else {
69
+ const at = seqCount(code, ip)
70
+ if (at >= 0) {
71
+ out = true
72
+ for (let i = 1, n = code[at]!; out && i <= n; i++) out = neverFails(code[at + i]!)
73
+ } else if (op !== OP_GATE) {
74
+ const c = passChild(code, ip)
75
+ out = c >= 0 && neverFails(c)
76
+ }
77
+ }
78
+ neverAt[ip] = out ? 1 : 2
79
+ return out
80
+ }
81
+
82
+ const clean = (ip: number): boolean => {
83
+ const hit = cleanAt[ip]!
84
+ if (hit !== 0) return hit === 1
85
+ cleanAt[ip] = 3
86
+ let out = false
87
+ switch (code[ip]) {
88
+ // A terminal pushes its leaf only on a match; NOT/PEEK/ATTEMPT restore
89
+ // their own mark; OPT, EMPTY and EXPECT do not fail uncommitted.
90
+ case OP_LIT: case OP_RX: case OP_LIT_TRACK: case OP_RX_TRACK: case OP_LIT_CI: case OP_LIT_CI_TRACK:
91
+ case OP_GUARD: case OP_NOT: case OP_PEEK: case OP_ATTEMPT: case OP_EMPTY: case OP_OPT: case OP_EXPECT:
92
+ out = true
93
+ break
94
+ // Min 0 does not fail uncommitted; min 1 fails only with no item kept, and
95
+ // restores the failed item itself.
96
+ case OP_REP: case OP_REPV:
97
+ out = code[ip + 2]! <= 1
98
+ break
99
+ // Only the first term may fail: a later one fails after the terms before it
100
+ // wrote, and after the trivia scanned in front of it.
101
+ case OP_SEQ: case OP_SEQV: case OP_SEQX: {
102
+ const at = seqCount(code, ip)
103
+ const n = code[at]!
104
+ out = n > 0 && clean(code[at + 1]!)
105
+ for (let i = 2; out && i <= n; i++) out = neverFails(code[at + i]!)
106
+ break
107
+ }
108
+ case OP_CHOICE: {
109
+ out = true
110
+ for (let i = 0, n = code[ip + 2]!; out && i < n; i++) out = clean(code[ip + 4 + i]!)
111
+ break
112
+ }
113
+ case OP_ROUTED:
114
+ out = code[ip + 1]! < 0 || clean(code[ip + 1]!)
115
+ break
116
+ // An arm's failure and the fallback's are committed; only the selector's is
117
+ // not, and without a fallback an unknown key fails after the selector wrote.
118
+ case OP_DISPATCH:
119
+ out = code[ip + 3]! >= 0 && clean(code[ip + 1]!)
120
+ break
121
+ default: {
122
+ const c = passChild(code, ip)
123
+ out = c >= 0 && clean(c)
124
+ }
125
+ }
126
+ cleanAt[ip] = out ? 1 : 2
127
+ return out
128
+ }
129
+ return clean
130
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A GAP HELD OVER A LOOKAHEAD.
3
+ *
4
+ * In a sequence, the trivia before each term is scanned and recorded, and when
5
+ * the term matches nothing it is unrecorded and the cursor goes back before it:
6
+ * the next term scans the same gap again. Where the term that matched nothing is
7
+ * a lookahead (`not`/`peek`, always zero-width) and the next term cannot match
8
+ * empty, that second scan reads the same characters from the same position under
9
+ * the same trivia and records the same rows, and the next term then either
10
+ * consumes past the gap or the sequence fails. So the gap is kept: the lookahead
11
+ * runs at the gap's end, the cursor stays there, and the next term starts there
12
+ * with nothing left to scan. One scan where there were two and an undo.
13
+ *
14
+ * Not before an adjacency assertion or a leading-trivia field, which read the
15
+ * cursor from before the gap.
16
+ */
17
+ import {
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
+ 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,
21
+ } from './ops.ts'
22
+ import { childSlots } from './child-slots.ts'
23
+ import { leadingTriviaFieldAt } from './field-spec.ts'
24
+ import { regexCanMatchEmpty } from '../regex/first-set.ts'
25
+
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])
28
+
29
+ /**
30
+ * Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
31
+ * gate row wraps only a child that cannot (`encode.ts` gates nothing nullable).
32
+ */
33
+ export function rowNullable(code: ArrayLike<number>, k: readonly unknown[], ip: number, memo: Map<number, boolean>): boolean {
34
+ const hit = memo.get(ip)
35
+ if (hit !== undefined) return hit
36
+ memo.set(ip, true) // a cycle is answered conservatively
37
+ const op = code[ip]!
38
+ const kids: number[] = []
39
+ let out = true
40
+ if (op === OP_GATE) out = false
41
+ else if (op === OP_LIT || op === OP_LIT_CI) out = (k[code[ip + 1]!] as string).length === 0
42
+ else if (op === OP_RX) { const re = k[code[ip + 1]!]; out = !(re instanceof RegExp) || regexCanMatchEmpty(re.source) }
43
+ else if (op === OP_SEQ || op === OP_SEQV || op === OP_SEQX) {
44
+ childSlots(code, ip, kids)
45
+ out = kids.every(c => rowNullable(code, k, c, memo))
46
+ } else if (op === OP_CHOICE) {
47
+ childSlots(code, ip, kids)
48
+ out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo))
49
+ } else if (op === OP_REP || op === OP_REPV) {
50
+ out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo)
51
+ } else if (WRAPPERS.has(op)) {
52
+ if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo)
53
+ }
54
+ memo.set(ip, out)
55
+ return out
56
+ }
57
+
58
+ /** Term `i` of the sequence at `seq` (terms from `base`, `n` of them) holds its gap. */
59
+ export function holdsGap(
60
+ code: ArrayLike<number>, k: readonly unknown[], base: number, n: number, i: number, memo: Map<number, boolean>,
61
+ ): boolean {
62
+ if (i < 1 || i + 1 >= n) return false
63
+ const term = code[base + i]!
64
+ if (code[term] !== OP_NOT && code[term] !== OP_PEEK) return false
65
+ const next = code[base + i + 1]!
66
+ if (code[next] === OP_ADJ || leadingTriviaFieldAt(code, k, next)) return false
67
+ return !rowNullable(code, k, next, memo)
68
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Combinator, ParseContext } from '../types.ts'
2
2
  import { failAt } from '../combinators/probe.ts'
3
+ import { lead } from './run-support.ts'
3
4
  import type { LexBodyRecognizer } from './lex-body.ts'
4
5
  import type { LexProgramSpec, ResolvedClass } from './program.ts'
5
6
 
@@ -16,11 +17,15 @@ export const LEX_NODE_SEQUENCE2 = 1
16
17
  export const LEX_NODE_SEQUENCE3 = 2
17
18
  export const LEX_NODE_SEQUENCE4 = 3
18
19
  export const LEX_NODE_SEQUENCE5 = 4
19
- export const LEX_NODE_CHOICE2 = 5
20
- export const LEX_NODE_CHOICE4 = 6
21
- export const LEX_NODE_CHOICE8 = 7
20
+ /**
21
+ * `[op, armCount, exclusive, choiceFx, (armClass, armFx) × armCount]`: any arity,
22
+ * no padding. `exclusive` is the character choice's one-arm dispatch decision.
23
+ */
24
+ export const LEX_NODE_CHOICE = 5
22
25
  export const LEX_NODE_NOT = 8
23
26
  export const LEX_NODE_OPTIONAL = 9
27
+ /** `[op, min, max]` after the item: a separator-less repeat, `max` −1 unbounded. */
28
+ export const LEX_NODE_REPEAT = 10
24
29
 
25
30
  export function lexProgramDigest(words: readonly number[]): number {
26
31
  let hash = 0x811C9DC5
@@ -60,73 +65,88 @@ function terminalNode(
60
65
  failure: readonly string[],
61
66
  tracksLines: boolean,
62
67
  ): BuiltLexNode {
63
- const match: PureLexRunner = (input, pos) => {
64
- const result = matcher(input, pos)
65
- return result < 0 ? -1 : result / 2
66
- }
67
68
  return {
68
- match,
69
+ match: (input, pos) => {
70
+ const result = matcher(input, pos)
71
+ return result < 0 ? -1 : result / 2
72
+ },
69
73
  run: (input, pos, ctx) => {
70
- const end = match(input, pos)
71
- if (end < 0) {
74
+ const result = matcher(input, pos)
75
+ if (result < 0) {
72
76
  ctx._fe = pos
73
77
  ctx._fx = failure as string[]
74
78
  if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
75
79
  return -1
76
80
  }
81
+ const end = result / 2
77
82
  if (tracksLines) trackLexLines(ctx, input, end)
78
83
  return end
79
84
  },
80
85
  }
81
86
  }
82
87
 
88
+ // Each sequence captures its parts' runners as scalars: a part is reached by a
89
+ // direct call, not a property read, and no arity nests another sequence.
83
90
  function sequence2(a: BuiltLexNode, b: BuiltLexNode): BuiltLexNode {
91
+ const am = a.match, bm = b.match, ar = a.run, br = b.run
84
92
  return {
85
93
  match: (input, pos) => {
86
- const e0 = a.match(input, pos)
87
- return e0 < 0 ? -1 : b.match(input, e0)
94
+ const e0 = am(input, pos)
95
+ return e0 < 0 ? -1 : bm(input, e0)
88
96
  },
89
97
  run: (input, pos, ctx) => {
90
- const e0 = a.run(input, pos, ctx)
98
+ const e0 = ar(input, pos, ctx)
91
99
  if (e0 < 0) return -1
92
100
  ctx._fc = false
93
- return b.run(input, e0, ctx)
101
+ return br(input, e0, ctx)
94
102
  },
95
103
  }
96
104
  }
97
105
 
98
106
  function sequence3(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode): BuiltLexNode {
107
+ const am = a.match, bm = b.match, cm = c.match, ar = a.run, br = b.run, cr = c.run
99
108
  return {
100
109
  match: (input, pos) => {
101
- const e0 = a.match(input, pos)
110
+ const e0 = am(input, pos)
102
111
  if (e0 < 0) return -1
103
- const e1 = b.match(input, e0)
104
- return e1 < 0 ? -1 : c.match(input, e1)
112
+ const e1 = bm(input, e0)
113
+ return e1 < 0 ? -1 : cm(input, e1)
105
114
  },
106
115
  run: (input, pos, ctx) => {
107
- const e0 = a.run(input, pos, ctx)
116
+ const e0 = ar(input, pos, ctx)
108
117
  if (e0 < 0) return -1
109
118
  ctx._fc = false
110
- const e1 = b.run(input, e0, ctx)
119
+ const e1 = br(input, e0, ctx)
111
120
  if (e1 < 0) return -1
112
121
  ctx._fc = false
113
- return c.run(input, e1, ctx)
122
+ return cr(input, e1, ctx)
114
123
  },
115
124
  }
116
125
  }
117
126
 
118
127
  function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode): BuiltLexNode {
119
- const abc = sequence3(a, b, c)
128
+ const am = a.match, bm = b.match, cm = c.match, dm = d.match
129
+ const ar = a.run, br = b.run, cr = c.run, dr = d.run
120
130
  return {
121
131
  match: (input, pos) => {
122
- const end = abc.match(input, pos)
123
- return end < 0 ? -1 : d.match(input, end)
132
+ const e0 = am(input, pos)
133
+ if (e0 < 0) return -1
134
+ const e1 = bm(input, e0)
135
+ if (e1 < 0) return -1
136
+ const e2 = cm(input, e1)
137
+ return e2 < 0 ? -1 : dm(input, e2)
124
138
  },
125
139
  run: (input, pos, ctx) => {
126
- const end = abc.run(input, pos, ctx)
127
- if (end < 0) return -1
140
+ const e0 = ar(input, pos, ctx)
141
+ if (e0 < 0) return -1
142
+ ctx._fc = false
143
+ const e1 = br(input, e0, ctx)
144
+ if (e1 < 0) return -1
145
+ ctx._fc = false
146
+ const e2 = cr(input, e1, ctx)
147
+ if (e2 < 0) return -1
128
148
  ctx._fc = false
129
- return d.run(input, end, ctx)
149
+ return dr(input, e2, ctx)
130
150
  },
131
151
  }
132
152
  }
@@ -134,17 +154,33 @@ function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLe
134
154
  function sequence5(
135
155
  a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode, e: BuiltLexNode,
136
156
  ): BuiltLexNode {
137
- const abcd = sequence4(a, b, c, d)
157
+ const am = a.match, bm = b.match, cm = c.match, dm = d.match, em = e.match
158
+ const ar = a.run, br = b.run, cr = c.run, dr = d.run, er = e.run
138
159
  return {
139
160
  match: (input, pos) => {
140
- const end = abcd.match(input, pos)
141
- return end < 0 ? -1 : e.match(input, end)
161
+ const e0 = am(input, pos)
162
+ if (e0 < 0) return -1
163
+ const e1 = bm(input, e0)
164
+ if (e1 < 0) return -1
165
+ const e2 = cm(input, e1)
166
+ if (e2 < 0) return -1
167
+ const e3 = dm(input, e2)
168
+ return e3 < 0 ? -1 : em(input, e3)
142
169
  },
143
170
  run: (input, pos, ctx) => {
144
- const end = abcd.run(input, pos, ctx)
145
- if (end < 0) return -1
171
+ const e0 = ar(input, pos, ctx)
172
+ if (e0 < 0) return -1
146
173
  ctx._fc = false
147
- return e.run(input, end, ctx)
174
+ const e1 = br(input, e0, ctx)
175
+ if (e1 < 0) return -1
176
+ ctx._fc = false
177
+ const e2 = cr(input, e1, ctx)
178
+ if (e2 < 0) return -1
179
+ ctx._fc = false
180
+ const e3 = dr(input, e2, ctx)
181
+ if (e3 < 0) return -1
182
+ ctx._fc = false
183
+ return er(input, e3, ctx)
148
184
  },
149
185
  }
150
186
  }
@@ -169,9 +205,9 @@ function choiceNode(
169
205
  ): BuiltLexNode {
170
206
  let matchTail: PureLexRunner = () => -1
171
207
  for (let i = arms.length - 1; i >= 0; i--) {
172
- const arm = arms[i]!, next = matchTail
208
+ const armMatch = arms[i]!.match, next = matchTail
173
209
  matchTail = (input, pos) => {
174
- const end = arm.match(input, pos)
210
+ const end = armMatch(input, pos)
175
211
  return end < 0 ? next(input, pos) : end
176
212
  }
177
213
  }
@@ -181,7 +217,7 @@ function choiceNode(
181
217
  return -1
182
218
  }
183
219
  for (let i = arms.length - 1; i >= 0; i--) {
184
- const arm = arms[i]!, cls = classes[i], staticExpected = armExpected[i]!, next = tail
220
+ const armRun = arms[i]!.run, cls = classes[i], staticExpected = armExpected[i]!, next = tail
185
221
  tail = (input, pos, ctx, lead, acc, best) => {
186
222
  if (cls !== undefined && !classHas(cls, lead)) {
187
223
  return next(
@@ -191,7 +227,7 @@ function choiceNode(
191
227
  )
192
228
  }
193
229
  ctx._fc = false
194
- const end = arm.run(input, pos, ctx)
230
+ const end = armRun(input, pos, ctx)
195
231
  if (end >= 0) return end
196
232
  const at = ctx._fe ?? pos
197
233
  if (at > best) { best = at; acc = undefined }
@@ -211,11 +247,68 @@ function choiceNode(
211
247
  }
212
248
  }
213
249
 
250
+ /**
251
+ * A choice whose arms' classes are disjoint, as the character choice's exclusive
252
+ * dispatch: the arm the lead selects runs alone, its failure is the choice's at
253
+ * the choice's position, and a lead no arm claims reports the arms' openers. The
254
+ * selected index reaches its arm through a fixed chain, never an array.
255
+ */
256
+ function exclusiveChoiceNode(
257
+ arms: readonly BuiltLexNode[],
258
+ classes: readonly ResolvedClass[],
259
+ choiceFx: readonly string[],
260
+ ): BuiltLexNode {
261
+ // A signed byte holds every arm index up to 127, which is every real grammar's
262
+ // choice; a wider choice takes a wider table so its 129th arm is not read as -128.
263
+ const ascii = (classes.length <= 128 ? new Int8Array(128) : new Int32Array(128)).fill(-1)
264
+ const hi: number[] = []
265
+ for (let i = 0; i < classes.length; i++) {
266
+ const cls = classes[i]!
267
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) ascii[c] = i
268
+ for (let r = 0; r < cls.hi.length; r += 2) hi.push(cls.hi[r]!, cls.hi[r + 1]!, i)
269
+ }
270
+ const select = (code: number): number => {
271
+ if (code < 0) return -1
272
+ if (code < 128) return ascii[code]!
273
+ for (let r = 0; r < hi.length; r += 3) if (code >= hi[r]! && code <= hi[r + 1]!) return hi[r + 2]!
274
+ return -1
275
+ }
276
+ let matchArm: (arm: number, input: string, pos: number) => number = () => -1
277
+ let selectRun: (arm: number, input: string, pos: number, ctx: ParseContext) => number = () => -1
278
+ for (let i = arms.length - 1; i >= 0; i--) {
279
+ const armMatch = arms[i]!.match, armRun = arms[i]!.run, nextMatch = matchArm, nextRun = selectRun
280
+ matchArm = (selected, input, pos) => selected === i ? armMatch(input, pos) : nextMatch(selected, input, pos)
281
+ selectRun = (selected, input, pos, ctx) => selected === i ? armRun(input, pos, ctx) : nextRun(selected, input, pos, ctx)
282
+ }
283
+ const runArm: LexProgramRunner = (input, pos, ctx) => {
284
+ const arm = select(lead(input, pos))
285
+ if (arm >= 0) {
286
+ ctx._fc = false
287
+ const end = selectRun(arm, input, pos, ctx)
288
+ if (end >= 0) return end
289
+ if (committed(ctx)) return -1
290
+ const failed = ctx._fx
291
+ if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return -1 }
292
+ }
293
+ ctx._fe = pos
294
+ ctx._fx = choiceFx as string[]
295
+ return -1
296
+ }
297
+ return {
298
+ match: (input, pos) => {
299
+ const arm = select(lead(input, pos))
300
+ return arm < 0 ? -1 : matchArm(arm, input, pos)
301
+ },
302
+ run: runArm,
303
+ }
304
+ }
305
+
214
306
  function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
307
+ const bodyMatch = body.match
215
308
  return {
216
- match: (input, pos) => body.match(input, pos) < 0 ? pos : -1,
309
+ match: (input, pos) => bodyMatch(input, pos) < 0 ? pos : -1,
217
310
  run: (input, pos, ctx) => {
218
- if (body.match(input, pos) < 0) return pos
311
+ if (bodyMatch(input, pos) < 0) return pos
219
312
  ctx._fe = pos
220
313
  ctx._fx = failure as string[]
221
314
  return -1
@@ -224,19 +317,65 @@ function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
224
317
  }
225
318
 
226
319
  function optionalNode(body: BuiltLexNode): BuiltLexNode {
320
+ const bodyMatch = body.match, bodyRun = body.run
227
321
  return {
228
322
  match: (input, pos) => {
229
- const end = body.match(input, pos)
323
+ const end = bodyMatch(input, pos)
230
324
  return end < 0 ? pos : end
231
325
  },
232
326
  run: (input, pos, ctx) => {
233
327
  ctx._fc = false
234
- const end = body.run(input, pos, ctx)
328
+ const end = bodyRun(input, pos, ctx)
235
329
  return end < 0 && !committed(ctx) ? pos : end
236
330
  },
237
331
  }
238
332
  }
239
333
 
334
+ /**
335
+ * The table's separator-less `OP_REP` under `token()`: no trivia, no separator,
336
+ * nothing to roll back. Only an optional item stops at end of input or on a
337
+ * zero-width match (`repItem`); a failed item ends the loop with its own
338
+ * failure published, and a committed one fails the repeat.
339
+ */
340
+ function repeatNode(item: BuiltLexNode, min: number, max: number): BuiltLexNode {
341
+ const itemMatch = item.match, itemRun = item.run
342
+ return {
343
+ match: (input, pos) => {
344
+ let cur = pos
345
+ let count = 0
346
+ for (;;) {
347
+ if (max >= 0 && count >= max) break
348
+ const optional = count >= min && (count > 0 || min === 0)
349
+ if (optional && cur >= input.length) break
350
+ const end = itemMatch(input, cur)
351
+ if (end < 0 || (optional && end === cur)) break
352
+ cur = end
353
+ count++
354
+ }
355
+ return count < min ? -1 : cur
356
+ },
357
+ run: (input, pos, ctx) => {
358
+ let cur = pos
359
+ let count = 0
360
+ for (;;) {
361
+ if (max >= 0 && count >= max) break
362
+ const optional = count >= min && (count > 0 || min === 0)
363
+ if (optional && cur >= input.length) break
364
+ ctx._fc = false
365
+ const end = itemRun(input, cur, ctx)
366
+ if (end < 0) {
367
+ if (committed(ctx)) return -1
368
+ break
369
+ }
370
+ if (optional && end === cur) break
371
+ cur = end
372
+ count++
373
+ }
374
+ return count < min ? -1 : cur
375
+ },
376
+ }
377
+ }
378
+
240
379
  function fixedTreeRunner(
241
380
  words: readonly number[],
242
381
  matchers: readonly LexBodyRecognizer[],
@@ -269,9 +408,14 @@ function fixedTreeRunner(
269
408
  else stack.push(sequence5(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!, nodes[4]!))
270
409
  continue
271
410
  }
272
- if (op >= LEX_NODE_CHOICE2 && op <= LEX_NODE_CHOICE8) {
273
- const count = op === LEX_NODE_CHOICE2 ? 2 : op === LEX_NODE_CHOICE4 ? 4 : 8
411
+ if (op === LEX_NODE_CHOICE) {
412
+ const count = words[at++]!
413
+ const exclusive = words[at++]!
414
+ if (exclusive !== 0 && exclusive !== 1) throw new TypeError('table lexical tree choice has an invalid dispatch mode')
274
415
  const choiceFx = expected[words[at++]!]
416
+ if (!Number.isInteger(count) || count < 2 || count > stack.length) {
417
+ throw new TypeError('table lexical tree choice has an invalid arm count')
418
+ }
275
419
  if (choiceFx === undefined) throw new TypeError('table lexical tree choice has an invalid expected set')
276
420
  const armClasses: Array<ResolvedClass | undefined> = []
277
421
  const armFx: Array<readonly string[]> = []
@@ -284,7 +428,9 @@ function fixedTreeRunner(
284
428
  armFx.push(fx)
285
429
  }
286
430
  const arms = Array.from({ length: count }, pop).reverse()
287
- stack.push(choiceNode(arms, armClasses, armFx, choiceFx))
431
+ stack.push(exclusive === 1
432
+ ? exclusiveChoiceNode(arms, armClasses as ResolvedClass[], choiceFx)
433
+ : choiceNode(arms, armClasses, armFx, choiceFx))
288
434
  continue
289
435
  }
290
436
  if (op === LEX_NODE_NOT) {
@@ -294,6 +440,14 @@ function fixedTreeRunner(
294
440
  continue
295
441
  }
296
442
  if (op === LEX_NODE_OPTIONAL) { stack.push(optionalNode(pop())); continue }
443
+ if (op === LEX_NODE_REPEAT) {
444
+ const min = words[at++]!, max = words[at++]!
445
+ if (!Number.isInteger(min) || min < 0 || !Number.isInteger(max) || (max >= 0 && max < min)) {
446
+ throw new TypeError('table lexical tree repeat has invalid bounds')
447
+ }
448
+ stack.push(repeatNode(pop(), min, max))
449
+ continue
450
+ }
297
451
  throw new TypeError('table lexical tree has an unknown fixed body')
298
452
  }
299
453
  if (stack.length !== 1) throw new TypeError('table lexical tree does not resolve to one root body')
package/src/table/ops.ts CHANGED
@@ -339,13 +339,16 @@ export const OP_REJECT = 35
339
339
  * arm INSTEAD of sitting inside it, so the encoder still reads the arm's own
340
340
  * first set for `disp` and the site keeps its dispatch slot.
341
341
  *
342
- * A blocked arm is SKIPPED, not failed — `choice.ts:150` is `continue`. The row
343
- * therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
342
+ * A blocked arm is SKIPPED, not failed — `choice.ts` firstMatch `continue`s. The
343
+ * row therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
344
344
  * "pretend this arm was never entered", so no cut it might have raised survives
345
- * to cut the choice. It still reports `e`, because the OTHER path a blocked arm
346
- * can be reached by is first-char dispatch, where skip-and-retry and
347
- * fail-the-choice are the same thing (choice.ts:23-34, :100-105) and the
348
- * interpreter answers with `deriveExpected(arm)` — this set.
345
+ * to cut the choice. It sets `_fe` to −1, so an ORDERED choice, which ranks arms
346
+ * by failure position, takes nothing from it, as the interpreter does. It still
347
+ * leaves `e` in `_fx`, because the other path a blocked arm can be reached by is
348
+ * exclusive first-char dispatch, where skip-and-retry and fail-the-choice are the
349
+ * same thing and the interpreter answers with `deriveExpected(arm)` — this set.
350
+ * On an ordered site the encoder gives a gated arm no char class, so it is always
351
+ * entered and this row, not a char gate, decides what it contributes.
349
352
  *
350
353
  * The predicate is a live function, like `OP_GUARD`'s, so a grammar using one is
351
354
  * runtime-only for `emitTableModule` unless `fnSources` are supplied.
@@ -475,6 +478,14 @@ export const OP_LEX_BODY = 42
475
478
  export const OP_LEX_PROGRAM = 43
476
479
  /** `SOURCE_LEAF f c` — `sourceLeaf()`: reduce semantically, capture source text. */
477
480
  export const OP_SOURCE_LEAF = 44
481
+ /**
482
+ * `RGATE cls child fx` — a choice arm that opens with the token its `dispatch()`
483
+ * already routed. `cls` is what may follow that token (the arm's next term,
484
+ * with the sequence's trivia), tested at the routed token's END; a miss fails
485
+ * there with the next term's set `fx`, as running the arm would. No routed token
486
+ * at this position, a completions probe, or tolerant recovery enters the arm.
487
+ */
488
+ export const OP_RGATE = 45
478
489
  /**
479
490
  * `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
480
491
  *
@@ -498,7 +509,7 @@ export const OP_NAMES: Record<number, string> = {
498
509
  [OP_LIT]: 'LIT', [OP_RX]: 'RX', [OP_SEQ]: 'SEQ', [OP_SEQV]: 'SEQV',
499
510
  [OP_CHOICE]: 'CHOICE', [OP_REP]: 'REP', [OP_REPV]: 'REPV', [OP_OPT]: 'OPT',
500
511
  [OP_XFORM]: 'XFORM', [OP_NODE]: 'NODE', [OP_RULE]: 'RULE', [OP_GATE]: 'GATE',
501
- [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_EMPTY]: 'EMPTY',
512
+ [OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_RGATE]: 'RGATE', [OP_EMPTY]: 'EMPTY',
502
513
  [OP_LIT_TRACK]: 'LIT_TRACK', [OP_RX_TRACK]: 'RX_TRACK', [OP_NODE_TRACK]: 'NODE_TRACK',
503
514
  [OP_SCOPE]: 'SCOPE', [OP_EXPECT]: 'EXPECT', [OP_SEQX]: 'SEQX', [OP_SCAN]: 'SCAN',
504
515
  [OP_FIELD]: 'FIELD', [OP_LIT_CI]: 'LIT_CI', [OP_LIT_CI_TRACK]: 'LIT_CI_TRACK', [OP_DISPATCH]: 'DISPATCH', [OP_ROUTED]: 'ROUTED',
@@ -273,6 +273,19 @@ export function scanRefs(s: ScanSpec): SubtreeRef[] {
273
273
  return refs
274
274
  }
275
275
 
276
+ /**
277
+ * The sites an assembly links from OUTSIDE its rule entries — the scan pool's
278
+ * subtrees, then the scan-skip sets — in the one order every assembler names
279
+ * them. The emitted source is keyed by this order, so a second copy of the list
280
+ * that disagrees prints different source for the same program.
281
+ */
282
+ export function assemblyExtraIps(prog: TableProgram): number[] {
283
+ const ips: number[] = []
284
+ for (const s of prog.scans ?? []) for (const r of scanRefs(s)) ips.push(r[0])
285
+ for (const set of prog.scanSkip ?? []) for (const r of set) ips.push(r[0])
286
+ return ips
287
+ }
288
+
276
289
  /** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
277
290
  export type TriviaSpec = {
278
291
  /** `[label, regexSource, regexFlags]` per classified arm. */
@@ -644,6 +657,17 @@ export function classHas(cls: ResolvedClass, code: number): boolean {
644
657
  return false
645
658
  }
646
659
 
660
+ /**
661
+ * Whether a choice with these arm classes (indices into the class-spec pool `cc`)
662
+ * takes the O(1) exclusive dispatch — the same decision `resolveDispatch` makes,
663
+ * for an encoder that has only the specs.
664
+ */
665
+ export function dispatchIsExclusive(arms: readonly number[], cc: readonly string[]): boolean {
666
+ const resolved: ResolvedClass[] = []
667
+ for (const ci of arms) if (ci >= 0) resolved[ci] = resolveClass(cc[ci]!)
668
+ return resolveDispatch(arms, resolved).exclusive
669
+ }
670
+
647
671
  function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
648
672
  const ascii = new Uint8Array(128)
649
673
  const hi: number[] = []