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.
- package/dist/analysis/diagnostics.js +2232 -1088
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +2247 -1103
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +11 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +69 -16
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/capture-buffer.d.ts +6 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +9 -1
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.js +1446 -236
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +1 -0
- package/dist/language-service/index.js.map +1 -1
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +51 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2126 -380
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts +9 -0
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts +4 -6
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +14 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +17 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +5 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec-baseline.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/factor-choice.d.ts +42 -0
- package/dist/table/factor-choice.d.ts.map +1 -0
- package/dist/table/fail-clean.d.ts +3 -0
- package/dist/table/fail-clean.d.ts.map +1 -0
- package/dist/table/hold-gap.d.ts +8 -0
- package/dist/table/hold-gap.d.ts.map +1 -0
- package/dist/table/index.js +1798 -316
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts +7 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +17 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -0
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/run-support.d.ts +19 -0
- package/dist/table/run-support.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +2 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/select-hot.d.ts +6 -0
- package/dist/table/select-hot.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +66 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/table/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/combinators/dispatch.ts +12 -3
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +44 -1
- package/src/compiler/build-arity.ts +3 -3
- package/src/compiler/token-alphabet.ts +57 -17
- package/src/cst/capture-buffer.ts +25 -0
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/parse-context.ts +1 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/index.ts +362 -42
- package/src/plugin/ir-eval.ts +7 -1
- package/src/regex/classes.ts +3 -0
- package/src/table/assemble.ts +371 -83
- package/src/table/child-slots.ts +6 -8
- package/src/table/compile-rule-map.ts +64 -15
- package/src/table/emit-assembly.ts +244 -42
- package/src/table/emit.ts +11 -8
- package/src/table/encode.ts +149 -35
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +34 -14
- package/src/table/factor-choice.ts +173 -0
- package/src/table/fail-clean.ts +130 -0
- package/src/table/hold-gap.ts +68 -0
- package/src/table/lex-program.ts +197 -43
- package/src/table/ops.ts +18 -7
- package/src/table/program.ts +24 -0
- package/src/table/run-support.ts +35 -0
- package/src/table/scalar-terminal.ts +8 -0
- package/src/table/select-hot.ts +70 -0
- package/src/table/site-labels.ts +135 -4
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +18 -0
- 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
|
+
}
|
package/src/table/lex-program.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
71
|
-
if (
|
|
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 =
|
|
87
|
-
return e0 < 0 ? -1 :
|
|
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 =
|
|
98
|
+
const e0 = ar(input, pos, ctx)
|
|
91
99
|
if (e0 < 0) return -1
|
|
92
100
|
ctx._fc = false
|
|
93
|
-
return
|
|
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 =
|
|
110
|
+
const e0 = am(input, pos)
|
|
102
111
|
if (e0 < 0) return -1
|
|
103
|
-
const e1 =
|
|
104
|
-
return e1 < 0 ? -1 :
|
|
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 =
|
|
116
|
+
const e0 = ar(input, pos, ctx)
|
|
108
117
|
if (e0 < 0) return -1
|
|
109
118
|
ctx._fc = false
|
|
110
|
-
const e1 =
|
|
119
|
+
const e1 = br(input, e0, ctx)
|
|
111
120
|
if (e1 < 0) return -1
|
|
112
121
|
ctx._fc = false
|
|
113
|
-
return
|
|
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
|
|
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
|
|
123
|
-
|
|
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
|
|
127
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
141
|
-
|
|
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
|
|
145
|
-
if (
|
|
171
|
+
const e0 = ar(input, pos, ctx)
|
|
172
|
+
if (e0 < 0) return -1
|
|
146
173
|
ctx._fc = false
|
|
147
|
-
|
|
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
|
|
208
|
+
const armMatch = arms[i]!.match, next = matchTail
|
|
173
209
|
matchTail = (input, pos) => {
|
|
174
|
-
const end =
|
|
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
|
|
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 =
|
|
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) =>
|
|
309
|
+
match: (input, pos) => bodyMatch(input, pos) < 0 ? pos : -1,
|
|
217
310
|
run: (input, pos, ctx) => {
|
|
218
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
273
|
-
const count =
|
|
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(
|
|
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
|
|
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
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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',
|
package/src/table/program.ts
CHANGED
|
@@ -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[] = []
|