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
package/src/table/encode.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scanRefs } from './program.ts'
|
|
2
|
+
import { factorSharedEnds } from './factor-choice.ts'
|
|
2
3
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
3
4
|
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
4
5
|
import { childrenOf } from '../analysis/gating.ts'
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
17
18
|
OP_LIVE, OP_ATTEMPT, OP_LABEL,
|
|
18
19
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
|
|
19
|
-
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
|
|
20
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV,
|
|
20
21
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
21
22
|
} from './ops.ts'
|
|
22
23
|
import { adjacencyExpected } from '../combinators/adjacency.ts'
|
|
@@ -27,12 +28,12 @@ import type { BalancedSpec } from '../combinators/scanTo.ts'
|
|
|
27
28
|
import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
|
|
28
29
|
import {
|
|
29
30
|
lexProgramDigest,
|
|
30
|
-
|
|
31
|
-
LEX_NODE_NOT, LEX_NODE_OPTIONAL,
|
|
31
|
+
LEX_NODE_CHOICE,
|
|
32
|
+
LEX_NODE_NOT, LEX_NODE_OPTIONAL, LEX_NODE_REPEAT,
|
|
32
33
|
LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
|
|
33
34
|
LEX_NODE_TERMINAL,
|
|
34
35
|
} from './lex-program.ts'
|
|
35
|
-
import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
36
|
+
import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
36
37
|
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
37
38
|
import { directArrayProjection } from '../compiler/direct-projection.ts'
|
|
38
39
|
import {
|
|
@@ -298,15 +299,18 @@ class Encoder {
|
|
|
298
299
|
}
|
|
299
300
|
case 'choice': {
|
|
300
301
|
for (const arm of node.arms) if (!emitNode(arm)) return false
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
// The arm classes and the exclusive (one-arm) decision are the
|
|
303
|
+
// character choice's own (`case 'choice'`), under the trivia the
|
|
304
|
+
// encoder would hold at that row, so both lowerings rank the same
|
|
305
|
+
// failures.
|
|
306
|
+
const final = classifyFinalChoice(
|
|
307
|
+
node.parsers, this.refResolver(), node.cleared ? undefined : this.activeTrivia,
|
|
308
|
+
)
|
|
309
|
+
const classes = final.firstSets.map((first, i) => final.nullable[i] ? -1 : this.charClass(first))
|
|
310
|
+
const exclusive = final.exclusive && classes.every(cls => cls >= 0)
|
|
311
|
+
words.push(LEX_NODE_CHOICE, node.arms.length, exclusive ? 1 : 0, this.expected(node.expected))
|
|
306
312
|
for (let i = 0; i < node.arms.length; i++) {
|
|
307
|
-
|
|
308
|
-
words.push(first.kind === 'ranges' ? this.charClass(first) : -1)
|
|
309
|
-
words.push(this.expected(node.armExpected[i]!))
|
|
313
|
+
words.push(classes[i]!, this.expected(node.armExpected[i]!))
|
|
310
314
|
}
|
|
311
315
|
return true
|
|
312
316
|
}
|
|
@@ -318,6 +322,10 @@ class Encoder {
|
|
|
318
322
|
if (!emitNode(node.body)) return false
|
|
319
323
|
words.push(LEX_NODE_OPTIONAL)
|
|
320
324
|
return true
|
|
325
|
+
case 'repeat':
|
|
326
|
+
if (!emitNode(node.body)) return false
|
|
327
|
+
words.push(LEX_NODE_REPEAT, node.min, node.max)
|
|
328
|
+
return true
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
if (!emitNode(body.root)) return undefined
|
|
@@ -699,6 +707,38 @@ class Encoder {
|
|
|
699
707
|
}
|
|
700
708
|
}
|
|
701
709
|
|
|
710
|
+
/**
|
|
711
|
+
* THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
|
|
712
|
+
*
|
|
713
|
+
* A non-nullable child cannot match at a character outside its first set, so
|
|
714
|
+
* the wrapper's outcome there is already decided: `optional` yields `null`,
|
|
715
|
+
* `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
|
|
716
|
+
* makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
|
|
717
|
+
* child — a rollback mark, the call, the child's own failure — to learn it.
|
|
718
|
+
*
|
|
719
|
+
* The gate is an `OP_GATE` row around the child, so every engine runs it
|
|
720
|
+
* unchanged, a probe or tolerant parse forwards through it as for a node's
|
|
721
|
+
* gate, and the wrapper's row keeps its layout. The engines test the class
|
|
722
|
+
* before taking the wrapper's mark. The first set is resolved through
|
|
723
|
+
* `winners`, the analysis a choice arm's gate uses, so a composed rule's set
|
|
724
|
+
* is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
|
|
725
|
+
* already reduces it to one recognizer call.
|
|
726
|
+
*/
|
|
727
|
+
private lookaheadGate(p: Combinator<unknown>, child: number): number {
|
|
728
|
+
// By opcode FAMILY: a line-tracked variant spells the same terminal
|
|
729
|
+
// `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
|
|
730
|
+
// down the same words (`foldPrograms`).
|
|
731
|
+
const op = this.code[child]
|
|
732
|
+
if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
|
|
733
|
+
|| op === OP_RX || op === OP_RX_TRACK) return child
|
|
734
|
+
const rr = this.refResolver()
|
|
735
|
+
if (matchesEmpty(p, new Set(), rr)) return child
|
|
736
|
+
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
|
|
737
|
+
if (cls < 0) return child
|
|
738
|
+
const e = deriveExpected(p)
|
|
739
|
+
return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
|
|
740
|
+
}
|
|
741
|
+
|
|
702
742
|
/** A first set becomes a char class string, or −1 for `any`. */
|
|
703
743
|
private charClass(fs: FirstSet): number {
|
|
704
744
|
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
@@ -715,14 +755,21 @@ class Encoder {
|
|
|
715
755
|
return i
|
|
716
756
|
}
|
|
717
757
|
|
|
718
|
-
/**
|
|
719
|
-
*
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
758
|
+
/**
|
|
759
|
+
* The first set a node's or an attempt's fail-fast gate tests. A bare
|
|
760
|
+
* cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
|
|
761
|
+
* it). Any other body keeps its construction-time `_meta.firstSet`, except
|
|
762
|
+
* where that is `any`: under `compose()` a body that reaches a rule of
|
|
763
|
+
* another piece is built before that rule exists, so its cached set is
|
|
764
|
+
* `any` and the node got NO gate (jess's `CompoundSelector` was entered at
|
|
765
|
+
* `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
|
|
766
|
+
* over the final merged map.
|
|
767
|
+
*/
|
|
768
|
+
private gateFirstSet(p: Combinator<unknown>): FirstSet {
|
|
769
|
+
const cached = p._meta.firstSet
|
|
770
|
+
return p._def.tag === 'lazy' || cached.kind === 'any'
|
|
771
|
+
? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
|
|
772
|
+
: cached
|
|
726
773
|
}
|
|
727
774
|
|
|
728
775
|
/**
|
|
@@ -901,6 +948,54 @@ class Encoder {
|
|
|
901
948
|
return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
|
|
902
949
|
}
|
|
903
950
|
|
|
951
|
+
/**
|
|
952
|
+
* The `OP_RGATE` operands of a choice arm that opens with `routed()`, or
|
|
953
|
+
* `undefined`. Through `node()`/scope wrappers and first sequence terms, the
|
|
954
|
+
* arm's next term after the routed token is found; the gate is that term's
|
|
955
|
+
* first characters, with the trivia in force at the boundary before it, so
|
|
956
|
+
* the arm is entered only where it can get past the token. The term must be
|
|
957
|
+
* unable to match empty or to commit, so a miss is exactly that term failing
|
|
958
|
+
* at the token's end with its own set.
|
|
959
|
+
*/
|
|
960
|
+
private routedArmGate(arm: Combinator<unknown>): { cls: number; fx: number } | undefined {
|
|
961
|
+
type Follow = 'end' | { next: Combinator<unknown>; boundary: Combinator<unknown> | undefined } | undefined
|
|
962
|
+
const walk = (p: Combinator<unknown>, trivia: Combinator<unknown> | undefined, depth: number): Follow => {
|
|
963
|
+
if (depth > 16) return undefined
|
|
964
|
+
const d = p._def as ParserDef
|
|
965
|
+
switch (d.tag) {
|
|
966
|
+
case 'routed': return 'end'
|
|
967
|
+
case 'node': return walk(d.parser, trivia, depth + 1)
|
|
968
|
+
case 'grammar':
|
|
969
|
+
return walk(d.parser, d.clearTrivia === true ? undefined : d.triviaParser ?? trivia, depth + 1)
|
|
970
|
+
case 'sequence': {
|
|
971
|
+
const head = d.parsers[0]
|
|
972
|
+
if (head === undefined) return undefined
|
|
973
|
+
const r = walk(head, trivia, depth + 1)
|
|
974
|
+
if (r !== 'end') return r
|
|
975
|
+
const next = d.parsers[1]
|
|
976
|
+
return next === undefined ? 'end' : { next, boundary: trivia }
|
|
977
|
+
}
|
|
978
|
+
default: return undefined
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const follow = walk(arm, this.activeTrivia, 0)
|
|
982
|
+
if (follow === undefined || follow === 'end') return undefined
|
|
983
|
+
const rr = this.refResolver()
|
|
984
|
+
const { next, boundary } = follow
|
|
985
|
+
if (matchesEmpty(next, new Set(), rr) || mayCommitFailure(next, rr, this.commitFailureMemo)) return undefined
|
|
986
|
+
// A miss reports `deriveExpected(next)`, a static set. Only a terminal or a
|
|
987
|
+
// label fails with exactly that set: a sequence over an `optional` prefix
|
|
988
|
+
// reports the prefix's labels statically but not when it runs, and a choice
|
|
989
|
+
// whose arms share an opener reports that opener once statically, once per
|
|
990
|
+
// arm when it runs.
|
|
991
|
+
const nextTag = (next._def as ParserDef).tag
|
|
992
|
+
if (nextTag !== 'literal' && nextTag !== 'regex' && nextTag !== 'keywords' && nextTag !== 'label') return undefined
|
|
993
|
+
const first = firstSetOf(next, new Set(), rr, boundary)
|
|
994
|
+
const cls = this.charClass(boundary === undefined ? first : union(first, firstSetOf(boundary, new Set(), rr)))
|
|
995
|
+
if (cls < 0) return undefined
|
|
996
|
+
return { cls, fx: this.expected(deriveExpected(next)) }
|
|
997
|
+
}
|
|
998
|
+
|
|
904
999
|
node(p: Combinator<unknown>): Emitted {
|
|
905
1000
|
// See `refMemo`: a reference is memoised per REFERENCING trivia scope, every
|
|
906
1001
|
// other construct once for the program.
|
|
@@ -1062,6 +1157,12 @@ class Encoder {
|
|
|
1062
1157
|
return head
|
|
1063
1158
|
}
|
|
1064
1159
|
case 'choice': {
|
|
1160
|
+
// Arms that differ in one term are that term's choice (`factor-choice.ts`).
|
|
1161
|
+
// Not under a coverage plan, which credits the arms as written.
|
|
1162
|
+
if (this.plan === undefined) {
|
|
1163
|
+
const factored = factorSharedEnds(d)
|
|
1164
|
+
if (factored !== undefined) return this.node(factored).ip
|
|
1165
|
+
}
|
|
1065
1166
|
// `greedyClassify` is NOT a choice at all — one arm runs and another arm
|
|
1066
1167
|
// is credited — so it gets its own row rather than an arm ordering.
|
|
1067
1168
|
if (d.strategy.tag === 'greedyClassify') {
|
|
@@ -1130,11 +1231,15 @@ class Encoder {
|
|
|
1130
1231
|
const kids = arms.map((c, k) => {
|
|
1131
1232
|
const src = order[k]!
|
|
1132
1233
|
const checks = d.autoNot[src]
|
|
1133
|
-
const
|
|
1234
|
+
const gate = d.gates[src]
|
|
1235
|
+
const plain = (checks === null || checks === undefined || checks.length === 0)
|
|
1236
|
+
&& (gate === null || gate === undefined)
|
|
1237
|
+
const routedGate = plain ? this.routedArmGate(c) : undefined
|
|
1238
|
+
const body = this.node(c).ip
|
|
1239
|
+
const inner = routedGate === undefined ? body : this.emit(OP_RGATE, routedGate.cls, body, routedGate.fx)
|
|
1134
1240
|
const ip = checks === null || checks === undefined || checks.length === 0
|
|
1135
1241
|
? inner
|
|
1136
1242
|
: this.reject(inner, checks)
|
|
1137
|
-
const gate = d.gates[src]
|
|
1138
1243
|
const gated = gate === null || gate === undefined
|
|
1139
1244
|
? ip
|
|
1140
1245
|
: this.emit(OP_ARMGATE, this.fn(gate, d.gateSrcs?.[src] ?? null), ip, this.expected(deriveExpected(c)))
|
|
@@ -1202,6 +1307,19 @@ class Encoder {
|
|
|
1202
1307
|
if (transactionalFirst[i] !== undefined) classes[i] = -1
|
|
1203
1308
|
}
|
|
1204
1309
|
}
|
|
1310
|
+
// A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
|
|
1311
|
+
// skips a gated-off arm and contributes nothing for it (choice.ts
|
|
1312
|
+
// firstMatch), and gates it on only by running it. A char-gated skip would
|
|
1313
|
+
// instead add the arm's static opener set whatever the gate says, so an
|
|
1314
|
+
// ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
|
|
1315
|
+
// its classes: there the dispatched arm's gate failing IS the choice
|
|
1316
|
+
// failing, with that arm's set, as in the interpreter's disjoint path.
|
|
1317
|
+
if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
|
|
1318
|
+
for (let k = 0; k < classes.length; k++) {
|
|
1319
|
+
const gate = d.gates[order[k]!]
|
|
1320
|
+
if (gate !== null && gate !== undefined) classes[k] = -1
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1205
1323
|
const dispIdx = this.disp.length
|
|
1206
1324
|
this.disp.push(classes)
|
|
1207
1325
|
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
|
|
@@ -1245,7 +1363,7 @@ class Encoder {
|
|
|
1245
1363
|
const rr = this.refResolver()
|
|
1246
1364
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1247
1365
|
? -1
|
|
1248
|
-
: this.
|
|
1366
|
+
: this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
|
|
1249
1367
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1250
1368
|
const min = d.tag === 'many' ? 0 : d.min
|
|
1251
1369
|
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
@@ -1288,7 +1406,7 @@ class Encoder {
|
|
|
1288
1406
|
: this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
|
|
1289
1407
|
}
|
|
1290
1408
|
case 'optional': {
|
|
1291
|
-
const head = this.emit(OP_OPT, this.node(d.parser).ip)
|
|
1409
|
+
const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
|
|
1292
1410
|
if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
|
|
1293
1411
|
return head
|
|
1294
1412
|
}
|
|
@@ -1409,16 +1527,14 @@ class Encoder {
|
|
|
1409
1527
|
// `deriveExpected(body)`, which for a body with nullable leading terms
|
|
1410
1528
|
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1411
1529
|
// running the body reports `['"x"']` alone.
|
|
1412
|
-
if (matchesEmpty(d.parser)) return body
|
|
1530
|
+
if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
|
|
1413
1531
|
// A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
|
|
1414
1532
|
// first set from the ref's base binding. Under a compose OVERRIDE the winner
|
|
1415
1533
|
// for `Y` may be WIDER, so the cached set gates out input the merged rule
|
|
1416
1534
|
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1417
1535
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1418
1536
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1419
|
-
const fs = d.parser
|
|
1420
|
-
? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
|
|
1421
|
-
: d.parser._meta.firstSet
|
|
1537
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1422
1538
|
if (fs.kind === 'any') return body
|
|
1423
1539
|
const cls = this.charClass(fs)
|
|
1424
1540
|
if (cls < 0) return body
|
|
@@ -1558,7 +1674,7 @@ class Encoder {
|
|
|
1558
1674
|
// other engine reported `['not(literal)']`. It hid because `expected` is a
|
|
1559
1675
|
// TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
|
|
1560
1676
|
case 'not':
|
|
1561
|
-
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1677
|
+
return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1562
1678
|
case 'guard':
|
|
1563
1679
|
// `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
|
|
1564
1680
|
return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
|
|
@@ -1603,7 +1719,7 @@ class Encoder {
|
|
|
1603
1719
|
// lookahead's failure is "the guard did not hold", and naming the body's
|
|
1604
1720
|
// internals offers a token the parse never asked the author for. The
|
|
1605
1721
|
// table propagated the body's set instead.
|
|
1606
|
-
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1722
|
+
return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1607
1723
|
// A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
|
|
1608
1724
|
// lowering was correct only for a choice arm.
|
|
1609
1725
|
case 'attempt': {
|
|
@@ -1619,10 +1735,8 @@ class Encoder {
|
|
|
1619
1735
|
// not a slower equivalent.
|
|
1620
1736
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1621
1737
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1622
|
-
const fs = d.parser
|
|
1623
|
-
|
|
1624
|
-
: d.parser._meta.firstSet
|
|
1625
|
-
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1738
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1739
|
+
if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
|
|
1626
1740
|
const cls = this.charClass(fs)
|
|
1627
1741
|
if (cls < 0) return inner
|
|
1628
1742
|
const e = deriveExpected(d.parser)
|
|
@@ -1746,7 +1860,7 @@ class Encoder {
|
|
|
1746
1860
|
}
|
|
1747
1861
|
const slots = (ip: number): number[] => {
|
|
1748
1862
|
switch (this.code[ip]) {
|
|
1749
|
-
case OP_GATE: return [ip + 2]
|
|
1863
|
+
case OP_GATE: case OP_RGATE: return [ip + 2]
|
|
1750
1864
|
case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
|
|
1751
1865
|
case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_SOURCE_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
|
|
1752
1866
|
case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
|
|
@@ -487,9 +487,7 @@ function makeDriver(
|
|
|
487
487
|
children: readonly unknown[], fields: undefined, span: { start: number; end: number },
|
|
488
488
|
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
489
489
|
) => unknown
|
|
490
|
-
const st = (flags & 8) !== 0
|
|
491
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
492
|
-
: undefined
|
|
490
|
+
const st = (flags & 8) !== 0 ? ctx.state : undefined
|
|
493
491
|
const nd = build(
|
|
494
492
|
cap.children, undefined,
|
|
495
493
|
code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end },
|
package/src/table/exec.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
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
24
|
OP_COV,
|
|
25
|
-
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
|
|
25
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
|
|
26
26
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
27
27
|
} from './ops.ts'
|
|
28
28
|
import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
type CompactProgram, type ResolvedClass, type ResolvedDispatch, type ResolvedDispatchSpec, type ResolvedTable,
|
|
50
50
|
type SubtreeRef, type TableProgram, type TableRule,
|
|
51
51
|
} from './program.ts'
|
|
52
|
-
import { scalarTerminalNotChild } from './scalar-terminal.ts'
|
|
52
|
+
import { scalarTerminalNotChild, scalarTerminalPeekChild } from './scalar-terminal.ts'
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* THE SHARED DRIVER.
|
|
@@ -923,6 +923,7 @@ function makeDriver(
|
|
|
923
923
|
const lit = k[code[child + 1]!] as string
|
|
924
924
|
if (!input.startsWith(lit, cur)) {
|
|
925
925
|
ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
|
|
926
|
+
if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
|
|
926
927
|
{ if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
927
928
|
}
|
|
928
929
|
const e = cur + lit.length
|
|
@@ -937,6 +938,7 @@ function makeDriver(
|
|
|
937
938
|
const m = re.exec(input)
|
|
938
939
|
if (m === null) {
|
|
939
940
|
ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
|
|
941
|
+
if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
|
|
940
942
|
{ if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
941
943
|
}
|
|
942
944
|
const mv = m[0]
|
|
@@ -1118,12 +1120,25 @@ function makeDriver(
|
|
|
1118
1120
|
return sup
|
|
1119
1121
|
}
|
|
1120
1122
|
|
|
1123
|
+
case OP_RGATE: {
|
|
1124
|
+
const item = ctx._routed
|
|
1125
|
+
if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
|
|
1126
|
+
&& !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
|
|
1127
|
+
ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1128
|
+
return FAIL
|
|
1129
|
+
}
|
|
1130
|
+
return exec(code[ip + 2]!, input, pos, ctx)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1121
1133
|
case OP_ARMGATE: {
|
|
1122
1134
|
if (!(fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
|
|
1123
1135
|
// choice.ts:150 is a `continue`, not a failure — the arm is treated as
|
|
1124
1136
|
// never entered, so a cut it might have raised must not cut the choice.
|
|
1125
1137
|
ctx._fc = false
|
|
1126
|
-
|
|
1138
|
+
// No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
|
|
1139
|
+
// arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
|
|
1140
|
+
// set is still the answer when it is the one selected.
|
|
1141
|
+
ctx._fe = -1
|
|
1127
1142
|
ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1128
1143
|
return FAIL
|
|
1129
1144
|
}
|
|
@@ -1479,9 +1494,7 @@ function makeDriver(
|
|
|
1479
1494
|
if (v === FAIL) return FAIL
|
|
1480
1495
|
const end = EC.e
|
|
1481
1496
|
const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
1482
|
-
const st = (flags & 8) !== 0
|
|
1483
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
1484
|
-
: undefined
|
|
1497
|
+
const st = (flags & 8) !== 0 ? ctx.state : undefined
|
|
1485
1498
|
const tagIdx = code[ip + 6]!
|
|
1486
1499
|
const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
|
|
1487
1500
|
|
|
@@ -1518,13 +1531,13 @@ function makeDriver(
|
|
|
1518
1531
|
nd = kids[0]
|
|
1519
1532
|
} else if (proj >= 0) {
|
|
1520
1533
|
nd = hostCst && host !== undefined
|
|
1521
|
-
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1534
|
+
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1522
1535
|
: projectChild(kids, proj, type)
|
|
1523
1536
|
} else if (buildIdx >= 0) {
|
|
1524
1537
|
if (hostCst && host !== undefined) {
|
|
1525
1538
|
// A direct builder is bypassed under a CST host: the host must never
|
|
1526
1539
|
// receive an arbitrary AST object as a child of a CST node.
|
|
1527
|
-
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1540
|
+
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1528
1541
|
} else {
|
|
1529
1542
|
const build = fns[buildIdx] as (
|
|
1530
1543
|
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
@@ -1573,15 +1586,14 @@ function makeDriver(
|
|
|
1573
1586
|
}
|
|
1574
1587
|
|
|
1575
1588
|
case OP_NOT: {
|
|
1576
|
-
// The
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
// needs the reference oracle to hide the terminal's diagnostic writes.
|
|
1589
|
+
// The generic closure/emitted NOT bodies leave the child's failure
|
|
1590
|
+
// registers; only the direct-terminal shape hides them. Every NOT hides
|
|
1591
|
+
// the child's completions from the probe, as `not.ts` does.
|
|
1580
1592
|
const scalarChild = scalarTerminalNotChild(code, ip) >= 0
|
|
1581
1593
|
const savedFc = scalarChild ? ctx._fc : false
|
|
1582
1594
|
const savedFe = scalarChild ? ctx._fe : -1
|
|
1583
1595
|
const savedFx = scalarChild ? ctx._fx : undefined
|
|
1584
|
-
const savedProbe =
|
|
1596
|
+
const savedProbe = ctx._probe?.best
|
|
1585
1597
|
const need = rollbackNeeded(ctx)
|
|
1586
1598
|
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1587
1599
|
const mTl = need ? cstTlLen(ctx) : 0
|
|
@@ -1596,8 +1608,8 @@ function makeDriver(
|
|
|
1596
1608
|
ctx._fc = savedFc
|
|
1597
1609
|
ctx._fe = savedFe
|
|
1598
1610
|
ctx._fx = savedFx
|
|
1599
|
-
if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
|
|
1600
1611
|
}
|
|
1612
|
+
if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
|
|
1601
1613
|
if (v === FAIL) { EC.e = pos; return null }
|
|
1602
1614
|
// `not.ts:50` — the ASSERTION's own set, at the assertion's position.
|
|
1603
1615
|
ctx._fe = pos
|
|
@@ -1606,6 +1618,12 @@ function makeDriver(
|
|
|
1606
1618
|
}
|
|
1607
1619
|
|
|
1608
1620
|
case OP_PEEK: {
|
|
1621
|
+
// A direct terminal is reduced to its recognizer in both assemblies
|
|
1622
|
+
// (`scalarTerminalPeekChild`), as under `not()`: hide its own diagnostic
|
|
1623
|
+
// writes here too.
|
|
1624
|
+
const scalarChild = scalarTerminalPeekChild(code, ip) >= 0
|
|
1625
|
+
const savedFc = scalarChild ? ctx._fc : false
|
|
1626
|
+
const savedProbe = ctx._probe?.best
|
|
1609
1627
|
const need = rollbackNeeded(ctx)
|
|
1610
1628
|
const mRaw = need ? cstRawLen(ctx) : 0
|
|
1611
1629
|
const mTl = need ? cstTlLen(ctx) : 0
|
|
@@ -1616,6 +1634,8 @@ function makeDriver(
|
|
|
1616
1634
|
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
1617
1635
|
const v = exec(code[ip + 1]!, input, pos, ctx)
|
|
1618
1636
|
if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
1637
|
+
if (scalarChild) ctx._fc = savedFc
|
|
1638
|
+
if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
|
|
1619
1639
|
// `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
|
|
1620
1640
|
if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
|
|
1621
1641
|
EC.e = pos
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED ENDS, FACTORED — a choice whose arms differ in one term.
|
|
3
|
+
*
|
|
4
|
+
* `choice(leaf(noTrivia(sequence(gap, '*', gap))), leaf(noTrivia(sequence(gap, '+', gap))))`
|
|
5
|
+
* scans `gap` once per arm, and on a value with no operator (`red red`) both
|
|
6
|
+
* arms scan it and then fail. Every arm is the same wrappers over a sequence of
|
|
7
|
+
* the same terms except one, so the choice is that one term:
|
|
8
|
+
*
|
|
9
|
+
* leaf(noTrivia(sequence(gap, choice('*', '+'), gap)))
|
|
10
|
+
*
|
|
11
|
+
* The gap is scanned once, and the inner choice dispatches on the character
|
|
12
|
+
* after it.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT IS THE SAME PARSE. Each arm is tried at the same position with the same
|
|
15
|
+
* context, so its terms before the differing one match the same text with the
|
|
16
|
+
* same value in every arm: running them once is what each arm's retry did. The
|
|
17
|
+
* inner choice tries the differing terms in arm order, as the outer one tried
|
|
18
|
+
* the arms. The terms after it may differ only as `optional`/`many`, which
|
|
19
|
+
* cannot fail, so the arm that matched its differing term is the arm the outer
|
|
20
|
+
* choice took. The sequence's values, the wrappers' spans and the leaf or node a
|
|
21
|
+
* wrapper publishes are what that arm produced.
|
|
22
|
+
*
|
|
23
|
+
* A choice that fails rolls its last arm back and reports the position it started
|
|
24
|
+
* at, so the factored form is an `attempt`, which does both. An arm that may
|
|
25
|
+
* commit is left as written: a committed choice reports without rolling back.
|
|
26
|
+
*
|
|
27
|
+
* WHAT "THE SAME" MEANS. Two terms are the same when they are one combinator, or
|
|
28
|
+
* their definitions are, field for field, down to shared combinators. A reducer
|
|
29
|
+
* is the same function, or the same printed source: a table artifact carries a
|
|
30
|
+
* reducer AS its source, so two reducers printed alike are one function there.
|
|
31
|
+
*/
|
|
32
|
+
import type { Combinator, ParserDef } from '../types.ts'
|
|
33
|
+
import { choice } from '../combinators/choice.ts'
|
|
34
|
+
import { sequence } from '../combinators/sequence.ts'
|
|
35
|
+
import { leaf, sourceLeaf } from '../combinators/token.ts'
|
|
36
|
+
import { noTrivia, parser } from '../combinators/grammar.ts'
|
|
37
|
+
import { attempt } from '../combinators/attempt.ts'
|
|
38
|
+
import { mayCommitFailure } from '../analysis/commitment.ts'
|
|
39
|
+
|
|
40
|
+
type ChoiceDef = Extract<ParserDef, { tag: 'choice' }>
|
|
41
|
+
|
|
42
|
+
/** Own keys every combinator may carry beside its definition; all derived from it. */
|
|
43
|
+
const DERIVED = new Set(['_tag', '_meta', '_def', '_parseScalar', '_stickyRegex', 'parse'])
|
|
44
|
+
|
|
45
|
+
function isCombinator(v: unknown): v is Combinator<unknown> {
|
|
46
|
+
return typeof v === 'object' && v !== null && '_def' in v && '_meta' in v
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sameComb(a: Combinator<unknown>, b: Combinator<unknown>, depth: number): boolean {
|
|
50
|
+
if (a === b) return true
|
|
51
|
+
if (depth > 32 || a._tag !== b._tag) return false
|
|
52
|
+
for (const k of Object.keys(a)) if (!DERIVED.has(k)) return false
|
|
53
|
+
for (const k of Object.keys(b)) if (!DERIVED.has(k)) return false
|
|
54
|
+
return sameFields(a._def as Record<string, unknown>, b._def as Record<string, unknown>, depth, undefined)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sameValue(u: unknown, v: unknown, depth: number): boolean {
|
|
58
|
+
if (u === v) return true
|
|
59
|
+
if (isCombinator(u) && isCombinator(v)) return sameComb(u, v, depth + 1)
|
|
60
|
+
if (Array.isArray(u) && Array.isArray(v)) return u.length === v.length && u.every((x, i) => sameValue(x, v[i], depth))
|
|
61
|
+
if (typeof u === 'object' && u !== null && typeof v === 'object' && v !== null
|
|
62
|
+
&& Object.getPrototypeOf(u) === Object.prototype && Object.getPrototypeOf(v) === Object.prototype) {
|
|
63
|
+
return sameFields(u as Record<string, unknown>, v as Record<string, unknown>, depth, undefined)
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every field of `x` and `y` the same, `skip` aside. A reducer matches its twin by printed source. */
|
|
69
|
+
function sameFields(x: Record<string, unknown>, y: Record<string, unknown>, depth: number, skip: string | undefined): boolean {
|
|
70
|
+
const kx = Object.keys(x).filter(k => k !== skip && x[k] !== undefined)
|
|
71
|
+
const ky = Object.keys(y).filter(k => k !== skip && y[k] !== undefined)
|
|
72
|
+
if (kx.length !== ky.length) return false
|
|
73
|
+
for (const k of kx) {
|
|
74
|
+
if (!(k in y)) return false
|
|
75
|
+
const u = x[k]
|
|
76
|
+
const v = y[k]
|
|
77
|
+
if (k === 'fn' && typeof u === 'function' && typeof v === 'function' && u !== v) {
|
|
78
|
+
if (typeof x.fnSrc !== 'string' || x.fnSrc === '' || x.fnSrc !== y.fnSrc) return false
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (!sameValue(u, v, depth)) return false
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A wrapper the factoring can rebuild around a new body: a leaf, or a trivia scope. */
|
|
87
|
+
function rewrap(w: Combinator<unknown>, body: Combinator<unknown>): Combinator<unknown> | undefined {
|
|
88
|
+
const d = w._def
|
|
89
|
+
let out: Combinator<unknown>
|
|
90
|
+
if (d.tag === 'leaf') {
|
|
91
|
+
out = d.cstValue === 'source' ? sourceLeaf(body, d.fn) : leaf(body, d.fn)
|
|
92
|
+
if (d.fnSrc !== undefined) (out._def as { fnSrc?: string }).fnSrc = d.fnSrc
|
|
93
|
+
} else if (d.tag === 'grammar') {
|
|
94
|
+
out = d.clearTrivia === true ? noTrivia(body) : parser({ trivia: d.triviaParser ?? null }, body)
|
|
95
|
+
} else return undefined
|
|
96
|
+
// The rebuilt wrapper must be the original in everything but its body.
|
|
97
|
+
return sameFields(d as Record<string, unknown>, out._def as Record<string, unknown>, 0, 'parser') ? out : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function unusedOf(seq: Combinator<unknown>): boolean {
|
|
101
|
+
return seq._def.tag === 'sequence' && seq._def.valueUnused === true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function never(c: Combinator<unknown>): boolean {
|
|
105
|
+
const t = c._def.tag
|
|
106
|
+
return t === 'optional' || t === 'many'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The factored form of `d`'s choice, or `undefined` when its arms are not one
|
|
111
|
+
* shape that differs in one term.
|
|
112
|
+
*/
|
|
113
|
+
export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined {
|
|
114
|
+
const arms = d.parsers
|
|
115
|
+
if (arms.length < 2) return undefined
|
|
116
|
+
if (d.strategy.tag !== 'firstMatch' && d.strategy.tag !== 'sharedPrefix') return undefined
|
|
117
|
+
if (d.gates.some(g => g !== null) || d.autoNot.some(a => a !== null)) return undefined
|
|
118
|
+
// Each arm: its wrappers, outermost first, and the sequence they end in.
|
|
119
|
+
const shapes: { wrappers: Combinator<unknown>[]; terms: Combinator<unknown>[]; seq: Combinator<unknown> }[] = []
|
|
120
|
+
for (const arm of arms) {
|
|
121
|
+
const wrappers: Combinator<unknown>[] = []
|
|
122
|
+
let c = arm
|
|
123
|
+
while ((c._def.tag === 'leaf' || c._def.tag === 'grammar') && wrappers.length < 4) {
|
|
124
|
+
wrappers.push(c)
|
|
125
|
+
c = c._def.parser
|
|
126
|
+
}
|
|
127
|
+
if (c._def.tag !== 'sequence' || c._def.parsers.length < 2) return undefined
|
|
128
|
+
shapes.push({ wrappers, terms: c._def.parsers, seq: c })
|
|
129
|
+
}
|
|
130
|
+
const first = shapes[0]!
|
|
131
|
+
const n = first.terms.length
|
|
132
|
+
for (const s of shapes) {
|
|
133
|
+
if (s.wrappers.length !== first.wrappers.length || s.terms.length !== n) return undefined
|
|
134
|
+
if (unusedOf(s.seq) !== unusedOf(first.seq)) return undefined
|
|
135
|
+
for (let j = 0; j < s.wrappers.length; j++) {
|
|
136
|
+
const a = first.wrappers[j]!._def as Record<string, unknown>
|
|
137
|
+
const b = s.wrappers[j]!._def as Record<string, unknown>
|
|
138
|
+
if (a.tag !== b.tag || !sameFields(a, b, 0, 'parser')) return undefined
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Exactly one term position differs, and nothing after it can fail.
|
|
142
|
+
let at = -1
|
|
143
|
+
for (let i = 0; i < n; i++) {
|
|
144
|
+
if (shapes.every(s => sameComb(s.terms[i]!, first.terms[i]!, 0))) continue
|
|
145
|
+
if (at >= 0) return undefined
|
|
146
|
+
at = i
|
|
147
|
+
}
|
|
148
|
+
if (at < 0) return undefined
|
|
149
|
+
for (let i = at + 1; i < n; i++) if (!never(first.terms[i]!)) return undefined
|
|
150
|
+
// A committed failure leaves a choice without rolling its arm back. Asked last:
|
|
151
|
+
// it walks everything the arms reach.
|
|
152
|
+
if (arms.some(a => mayCommitFailure(a))) return undefined
|
|
153
|
+
const middle = choice(...(shapes.map(s => s.terms[at]!) as [Combinator<unknown>, ...Combinator<unknown>[]]))
|
|
154
|
+
// The inner choice must keep arm order: a strategy that reorders (longest
|
|
155
|
+
// literal first) or classifies would pick a different arm. Nor may it add an
|
|
156
|
+
// automatic lookahead the arms as written do not have: `'start'` before
|
|
157
|
+
// `'started'` gets a `not('ed')` as one choice, which rejects the `!start` the
|
|
158
|
+
// written arms take on `!started`.
|
|
159
|
+
if (middle._def.tag === 'choice' && middle._def.autoNot.some(a => a !== null)) return undefined
|
|
160
|
+
const st = middle._def.tag === 'choice' ? middle._def.strategy : undefined
|
|
161
|
+
if (st === undefined || (st.tag !== 'firstMatch' && st.tag !== 'sharedPrefix'
|
|
162
|
+
&& !(st.tag === 'literalsLongestFirst' && st.sortedIndices.every((x, i) => x === i)))) return undefined
|
|
163
|
+
let out: Combinator<unknown> | undefined = sequence(
|
|
164
|
+
...([...first.terms.slice(0, at), middle, ...first.terms.slice(at + 1)] as [Combinator<unknown>, ...Combinator<unknown>[]]),
|
|
165
|
+
)
|
|
166
|
+
if (unusedOf(first.seq)) {
|
|
167
|
+
(out._def as { valueUnused?: boolean }).valueUnused = true
|
|
168
|
+
}
|
|
169
|
+
for (let j = first.wrappers.length - 1; j >= 0 && out !== undefined; j--) out = rewrap(first.wrappers[j]!, out)
|
|
170
|
+
// A choice that fails is rolled back and reports where it started; so does an
|
|
171
|
+
// `attempt`. The sequence alone would report where its inner choice failed.
|
|
172
|
+
return out === undefined ? undefined : attempt(out)
|
|
173
|
+
}
|