parseman 0.55.4 → 0.57.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/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +3745 -3180
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/cli/index.js +2182 -1617
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/attempt.d.ts.map +1 -1
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/lines.d.ts +79 -0
- package/dist/combinators/lines.d.ts.map +1 -0
- package/dist/combinators/node.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +25 -0
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.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.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2348 -1697
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +3 -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/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +27 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2360 -1314
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -4
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +3 -0
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +15 -0
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +9 -65
- 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/index.js +3884 -3262
- package/dist/table/index.js.map +3 -3
- package/dist/table/line-scan.d.ts +9 -0
- package/dist/table/line-scan.d.ts.map +1 -0
- package/dist/table/ops.d.ts +9 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +24 -2
- package/dist/table/program.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/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/analysis/choice-cost.ts +1 -0
- package/src/analysis/commitment.ts +13 -0
- package/src/analysis/duplication.ts +3 -0
- package/src/analysis/gating.ts +5 -0
- package/src/analysis/rebuild.ts +2 -2
- package/src/combinators/attempt.ts +2 -0
- package/src/combinators/choice.ts +3 -0
- package/src/combinators/dispatch.ts +3 -0
- package/src/combinators/expect.ts +5 -0
- package/src/combinators/first-set.ts +9 -1
- package/src/combinators/lines.ts +244 -0
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +48 -3
- package/src/compiler/build-arity.ts +3 -3
- package/src/compiler/fields.ts +3 -0
- package/src/compiler/grammar-coverage-ids.ts +1 -0
- package/src/compiler/ir-serialize.ts +10 -0
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +6 -0
- package/src/cst/reflection.ts +3 -0
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/index.ts +2 -0
- package/src/parse-context.ts +3 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/evaluator.ts +4 -0
- package/src/plugin/index.ts +138 -13
- package/src/plugin/ir-eval.ts +10 -1
- package/src/regex/classes.ts +3 -0
- package/src/spec/model.ts +15 -0
- package/src/table/assemble.ts +125 -33
- package/src/table/child-slots.ts +4 -6
- package/src/table/compile-rule-map.ts +55 -15
- package/src/table/emit-assembly.ts +139 -26
- package/src/table/emit.ts +13 -11
- package/src/table/encode.ts +87 -27
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +10 -6
- package/src/table/line-scan.ts +19 -0
- package/src/table/ops.ts +9 -6
- package/src/table/program.ts +41 -2
- package/src/table/select-hot.ts +70 -0
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +32 -0
- package/src/version.ts +1 -1
package/src/table/encode.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanRefs } from './program.ts'
|
|
1
2
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
3
|
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
4
|
import { childrenOf } from '../analysis/gating.ts'
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
|
|
32
33
|
LEX_NODE_TERMINAL,
|
|
33
34
|
} from './lex-program.ts'
|
|
34
|
-
import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
35
|
+
import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
35
36
|
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
36
37
|
import { directArrayProjection } from '../compiler/direct-projection.ts'
|
|
37
38
|
import {
|
|
@@ -683,6 +684,7 @@ class Encoder {
|
|
|
683
684
|
case 'expect':
|
|
684
685
|
case 'recover':
|
|
685
686
|
case 'scanTo':
|
|
687
|
+
case 'lines':
|
|
686
688
|
case 'unknown':
|
|
687
689
|
return true
|
|
688
690
|
case 'lazy': {
|
|
@@ -697,6 +699,38 @@ class Encoder {
|
|
|
697
699
|
}
|
|
698
700
|
}
|
|
699
701
|
|
|
702
|
+
/**
|
|
703
|
+
* THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
|
|
704
|
+
*
|
|
705
|
+
* A non-nullable child cannot match at a character outside its first set, so
|
|
706
|
+
* the wrapper's outcome there is already decided: `optional` yields `null`,
|
|
707
|
+
* `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
|
|
708
|
+
* makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
|
|
709
|
+
* child — a rollback mark, the call, the child's own failure — to learn it.
|
|
710
|
+
*
|
|
711
|
+
* The gate is an `OP_GATE` row around the child, so every engine runs it
|
|
712
|
+
* unchanged, a probe or tolerant parse forwards through it as for a node's
|
|
713
|
+
* gate, and the wrapper's row keeps its layout. The engines test the class
|
|
714
|
+
* before taking the wrapper's mark. The first set is resolved through
|
|
715
|
+
* `winners`, the analysis a choice arm's gate uses, so a composed rule's set
|
|
716
|
+
* is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
|
|
717
|
+
* already reduces it to one recognizer call.
|
|
718
|
+
*/
|
|
719
|
+
private lookaheadGate(p: Combinator<unknown>, child: number): number {
|
|
720
|
+
// By opcode FAMILY: a line-tracked variant spells the same terminal
|
|
721
|
+
// `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
|
|
722
|
+
// down the same words (`foldPrograms`).
|
|
723
|
+
const op = this.code[child]
|
|
724
|
+
if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
|
|
725
|
+
|| op === OP_RX || op === OP_RX_TRACK) return child
|
|
726
|
+
const rr = this.refResolver()
|
|
727
|
+
if (matchesEmpty(p, new Set(), rr)) return child
|
|
728
|
+
const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
|
|
729
|
+
if (cls < 0) return child
|
|
730
|
+
const e = deriveExpected(p)
|
|
731
|
+
return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
|
|
732
|
+
}
|
|
733
|
+
|
|
700
734
|
/** A first set becomes a char class string, or −1 for `any`. */
|
|
701
735
|
private charClass(fs: FirstSet): number {
|
|
702
736
|
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
@@ -713,14 +747,21 @@ class Encoder {
|
|
|
713
747
|
return i
|
|
714
748
|
}
|
|
715
749
|
|
|
716
|
-
/**
|
|
717
|
-
*
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
750
|
+
/**
|
|
751
|
+
* The first set a node's or an attempt's fail-fast gate tests. A bare
|
|
752
|
+
* cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
|
|
753
|
+
* it). Any other body keeps its construction-time `_meta.firstSet`, except
|
|
754
|
+
* where that is `any`: under `compose()` a body that reaches a rule of
|
|
755
|
+
* another piece is built before that rule exists, so its cached set is
|
|
756
|
+
* `any` and the node got NO gate (jess's `CompoundSelector` was entered at
|
|
757
|
+
* `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
|
|
758
|
+
* over the final merged map.
|
|
759
|
+
*/
|
|
760
|
+
private gateFirstSet(p: Combinator<unknown>): FirstSet {
|
|
761
|
+
const cached = p._meta.firstSet
|
|
762
|
+
return p._def.tag === 'lazy' || cached.kind === 'any'
|
|
763
|
+
? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
|
|
764
|
+
: cached
|
|
724
765
|
}
|
|
725
766
|
|
|
726
767
|
/**
|
|
@@ -987,6 +1028,17 @@ class Encoder {
|
|
|
987
1028
|
open: bal.open,
|
|
988
1029
|
close: bal.close,
|
|
989
1030
|
}))
|
|
1031
|
+
// `lines()` and `columns()` lower as their constructor arguments too: the
|
|
1032
|
+
// table rebuilds each and runs it, the body and prefixes being table subtrees.
|
|
1033
|
+
if (d.tag === 'lines') {
|
|
1034
|
+
return this.emit(OP_SCAN, this.scanSlot({
|
|
1035
|
+
kind: 2, flags: 0, skip: [],
|
|
1036
|
+
prefix: this.subtree(d.prefix), body: this.subtree(d.body),
|
|
1037
|
+
...(d.first === undefined ? {} : { first: this.subtree(d.first) }),
|
|
1038
|
+
...(d.lazy === undefined ? {} : { lazy: this.subtree(d.lazy) }),
|
|
1039
|
+
}))
|
|
1040
|
+
}
|
|
1041
|
+
if (d.tag === 'columns') return this.emit(OP_SCAN, this.scanSlot({ kind: 3, flags: 0, skip: [], n: d.n }))
|
|
990
1042
|
if (d.tag === 'scanTo') {
|
|
991
1043
|
const sentDef = d.sentinel._def
|
|
992
1044
|
return this.emit(OP_SCAN, this.scanSlot({
|
|
@@ -1189,6 +1241,19 @@ class Encoder {
|
|
|
1189
1241
|
if (transactionalFirst[i] !== undefined) classes[i] = -1
|
|
1190
1242
|
}
|
|
1191
1243
|
}
|
|
1244
|
+
// A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
|
|
1245
|
+
// skips a gated-off arm and contributes nothing for it (choice.ts
|
|
1246
|
+
// firstMatch), and gates it on only by running it. A char-gated skip would
|
|
1247
|
+
// instead add the arm's static opener set whatever the gate says, so an
|
|
1248
|
+
// ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
|
|
1249
|
+
// its classes: there the dispatched arm's gate failing IS the choice
|
|
1250
|
+
// failing, with that arm's set, as in the interpreter's disjoint path.
|
|
1251
|
+
if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
|
|
1252
|
+
for (let k = 0; k < classes.length; k++) {
|
|
1253
|
+
const gate = d.gates[order[k]!]
|
|
1254
|
+
if (gate !== null && gate !== undefined) classes[k] = -1
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1192
1257
|
const dispIdx = this.disp.length
|
|
1193
1258
|
this.disp.push(classes)
|
|
1194
1259
|
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
|
|
@@ -1232,7 +1297,7 @@ class Encoder {
|
|
|
1232
1297
|
const rr = this.refResolver()
|
|
1233
1298
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1234
1299
|
? -1
|
|
1235
|
-
: this.
|
|
1300
|
+
: this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
|
|
1236
1301
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1237
1302
|
const min = d.tag === 'many' ? 0 : d.min
|
|
1238
1303
|
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
@@ -1275,7 +1340,7 @@ class Encoder {
|
|
|
1275
1340
|
: this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
|
|
1276
1341
|
}
|
|
1277
1342
|
case 'optional': {
|
|
1278
|
-
const head = this.emit(OP_OPT, this.node(d.parser).ip)
|
|
1343
|
+
const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
|
|
1279
1344
|
if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
|
|
1280
1345
|
return head
|
|
1281
1346
|
}
|
|
@@ -1396,16 +1461,14 @@ class Encoder {
|
|
|
1396
1461
|
// `deriveExpected(body)`, which for a body with nullable leading terms
|
|
1397
1462
|
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1398
1463
|
// running the body reports `['"x"']` alone.
|
|
1399
|
-
if (matchesEmpty(d.parser)) return body
|
|
1464
|
+
if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
|
|
1400
1465
|
// A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
|
|
1401
1466
|
// first set from the ref's base binding. Under a compose OVERRIDE the winner
|
|
1402
1467
|
// for `Y` may be WIDER, so the cached set gates out input the merged rule
|
|
1403
1468
|
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1404
1469
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1405
1470
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1406
|
-
const fs = d.parser
|
|
1407
|
-
? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
|
|
1408
|
-
: d.parser._meta.firstSet
|
|
1471
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1409
1472
|
if (fs.kind === 'any') return body
|
|
1410
1473
|
const cls = this.charClass(fs)
|
|
1411
1474
|
if (cls < 0) return body
|
|
@@ -1545,7 +1608,7 @@ class Encoder {
|
|
|
1545
1608
|
// other engine reported `['not(literal)']`. It hid because `expected` is a
|
|
1546
1609
|
// TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
|
|
1547
1610
|
case 'not':
|
|
1548
|
-
return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1611
|
+
return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
|
|
1549
1612
|
case 'guard':
|
|
1550
1613
|
// `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
|
|
1551
1614
|
return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
|
|
@@ -1590,7 +1653,7 @@ class Encoder {
|
|
|
1590
1653
|
// lookahead's failure is "the guard did not hold", and naming the body's
|
|
1591
1654
|
// internals offers a token the parse never asked the author for. The
|
|
1592
1655
|
// table propagated the body's set instead.
|
|
1593
|
-
return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1656
|
+
return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
|
|
1594
1657
|
// A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
|
|
1595
1658
|
// lowering was correct only for a choice arm.
|
|
1596
1659
|
case 'attempt': {
|
|
@@ -1606,10 +1669,8 @@ class Encoder {
|
|
|
1606
1669
|
// not a slower equivalent.
|
|
1607
1670
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1608
1671
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1609
|
-
const fs = d.parser
|
|
1610
|
-
|
|
1611
|
-
: d.parser._meta.firstSet
|
|
1612
|
-
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1672
|
+
const fs = this.gateFirstSet(d.parser)
|
|
1673
|
+
if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
|
|
1613
1674
|
const cls = this.charClass(fs)
|
|
1614
1675
|
if (cls < 0) return inner
|
|
1615
1676
|
const e = deriveExpected(d.parser)
|
|
@@ -1762,12 +1823,7 @@ class Encoder {
|
|
|
1762
1823
|
// interiors uncollapsed and — worse — left the spec's own offset pointing at
|
|
1763
1824
|
// a trampoline whose target had been rewritten around it.
|
|
1764
1825
|
const refs: SubtreeRef[] = []
|
|
1765
|
-
for (const s of this.scans)
|
|
1766
|
-
if (s.sentinel !== undefined) refs.push(s.sentinel)
|
|
1767
|
-
if (s.recoverAt !== undefined) refs.push(s.recoverAt)
|
|
1768
|
-
if (s.stopAt !== undefined) refs.push(s.stopAt)
|
|
1769
|
-
refs.push(...s.skip)
|
|
1770
|
-
}
|
|
1826
|
+
for (const s of this.scans) refs.push(...scanRefs(s))
|
|
1771
1827
|
for (const set of this.scanSkipSets) refs.push(...set)
|
|
1772
1828
|
const stack = [...Object.values(this.rules), ...refs.map(r => r[0])]
|
|
1773
1829
|
while (stack.length > 0) {
|
|
@@ -1788,6 +1844,10 @@ class Encoder {
|
|
|
1788
1844
|
...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
|
|
1789
1845
|
...(s.recoverAt === undefined ? {} : { recoverAt: res(s.recoverAt) }),
|
|
1790
1846
|
...(s.stopAt === undefined ? {} : { stopAt: res(s.stopAt) }),
|
|
1847
|
+
...(s.prefix === undefined ? {} : { prefix: res(s.prefix) }),
|
|
1848
|
+
...(s.body === undefined ? {} : { body: res(s.body) }),
|
|
1849
|
+
...(s.first === undefined ? {} : { first: res(s.first) }),
|
|
1850
|
+
...(s.lazy === undefined ? {} : { lazy: res(s.lazy) }),
|
|
1791
1851
|
}))
|
|
1792
1852
|
this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
|
|
1793
1853
|
}
|
|
@@ -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rebuildLineScan } from './line-scan.ts'
|
|
1
2
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
2
3
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
3
4
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
@@ -1122,7 +1123,10 @@ function makeDriver(
|
|
|
1122
1123
|
// choice.ts:150 is a `continue`, not a failure — the arm is treated as
|
|
1123
1124
|
// never entered, so a cut it might have raised must not cut the choice.
|
|
1124
1125
|
ctx._fc = false
|
|
1125
|
-
|
|
1126
|
+
// No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
|
|
1127
|
+
// arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
|
|
1128
|
+
// set is still the answer when it is the one selected.
|
|
1129
|
+
ctx._fe = -1
|
|
1126
1130
|
ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1127
1131
|
return FAIL
|
|
1128
1132
|
}
|
|
@@ -1478,9 +1482,7 @@ function makeDriver(
|
|
|
1478
1482
|
if (v === FAIL) return FAIL
|
|
1479
1483
|
const end = EC.e
|
|
1480
1484
|
const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
|
|
1481
|
-
const st = (flags & 8) !== 0
|
|
1482
|
-
? Object.assign({}, ctx.state as Record<string, unknown>)
|
|
1483
|
-
: undefined
|
|
1485
|
+
const st = (flags & 8) !== 0 ? ctx.state : undefined
|
|
1484
1486
|
const tagIdx = code[ip + 6]!
|
|
1485
1487
|
const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
|
|
1486
1488
|
|
|
@@ -1517,13 +1519,13 @@ function makeDriver(
|
|
|
1517
1519
|
nd = kids[0]
|
|
1518
1520
|
} else if (proj >= 0) {
|
|
1519
1521
|
nd = hostCst && host !== undefined
|
|
1520
|
-
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1522
|
+
? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1521
1523
|
: projectChild(kids, proj, type)
|
|
1522
1524
|
} else if (buildIdx >= 0) {
|
|
1523
1525
|
if (hostCst && host !== undefined) {
|
|
1524
1526
|
// A direct builder is bypassed under a CST host: the host must never
|
|
1525
1527
|
// receive an arbitrary AST object as a child of a CST node.
|
|
1526
|
-
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog,
|
|
1528
|
+
nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
|
|
1527
1529
|
} else {
|
|
1528
1530
|
const build = fns[buildIdx] as (
|
|
1529
1531
|
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
@@ -1682,6 +1684,8 @@ function makeDriver(
|
|
|
1682
1684
|
* drift.
|
|
1683
1685
|
*/
|
|
1684
1686
|
const scans: readonly Combinator<unknown>[] = (prog.scans ?? []).map(s => {
|
|
1687
|
+
const rebuilt = rebuildLineScan(s, subtreeComb)
|
|
1688
|
+
if (rebuilt !== undefined) return rebuilt
|
|
1685
1689
|
const skip = s.skip.map(r => subtreeComb(r))
|
|
1686
1690
|
const raw = (s.flags & 1) !== 0
|
|
1687
1691
|
if (s.kind === 1) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Combinator } from '../types.ts'
|
|
2
|
+
import type { ScanSpec, SubtreeRef } from './program.ts'
|
|
3
|
+
import { columns, lines } from '../combinators/lines.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Rebuild a `lines()` (kind 2) or `columns()` (kind 3) scan spec as the
|
|
7
|
+
* combinator it was constructed as, its subtrees bound to the table's pieces.
|
|
8
|
+
* Both drivers run the result through its own `parse`, as they do `scanTo`.
|
|
9
|
+
*/
|
|
10
|
+
export function rebuildLineScan(s: ScanSpec, subtree: (r: SubtreeRef) => Combinator<unknown>): Combinator<unknown> | undefined {
|
|
11
|
+
if (s.kind === 2) {
|
|
12
|
+
return lines(subtree(s.prefix!), subtree(s.body!), {
|
|
13
|
+
...(s.first === undefined ? {} : { first: subtree(s.first) }),
|
|
14
|
+
...(s.lazy === undefined ? {} : { lazy: subtree(s.lazy) }),
|
|
15
|
+
}) as Combinator<unknown>
|
|
16
|
+
}
|
|
17
|
+
if (s.kind === 3) return columns(s.n!) as Combinator<unknown>
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
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.
|
package/src/table/program.ts
CHANGED
|
@@ -241,13 +241,13 @@ export type SubtreeRef = readonly [ip: number, cls: number]
|
|
|
241
241
|
/**
|
|
242
242
|
* A scanning construct as its CONSTRUCTOR ARGUMENTS.
|
|
243
243
|
*
|
|
244
|
-
* `kind` 0 is `scanTo`, 1 is `balanced`. `flags` bit 0 = `raw`, bit 1 = `orEOF`
|
|
244
|
+
* `kind` 0 is `scanTo`, 1 is `balanced`, 2 is `lines`, 3 is `columns`. `flags` bit 0 = `raw`, bit 1 = `orEOF`
|
|
245
245
|
* (scanTo), bit 2 = `strict` (balanced). `sent` is the sentinel's literal text
|
|
246
246
|
* when it is a `literal()`, so the rebuilt scan reports the same expected set as
|
|
247
247
|
* the interpreter, and `null` otherwise (which reports `"sentinel"`).
|
|
248
248
|
*/
|
|
249
249
|
export type ScanSpec = {
|
|
250
|
-
readonly kind: 0 | 1
|
|
250
|
+
readonly kind: 0 | 1 | 2 | 3
|
|
251
251
|
readonly flags: number
|
|
252
252
|
readonly skip: readonly SubtreeRef[]
|
|
253
253
|
readonly sentinel?: SubtreeRef
|
|
@@ -256,6 +256,34 @@ export type ScanSpec = {
|
|
|
256
256
|
readonly sent?: string | null
|
|
257
257
|
readonly open?: string
|
|
258
258
|
readonly close?: string
|
|
259
|
+
/** `lines()` (kind 2): the continuation prefix, the body, and the optional first-line prefix and lazy check. */
|
|
260
|
+
readonly prefix?: SubtreeRef
|
|
261
|
+
readonly body?: SubtreeRef
|
|
262
|
+
readonly first?: SubtreeRef
|
|
263
|
+
readonly lazy?: SubtreeRef
|
|
264
|
+
/** `columns()` (kind 3): the column count. */
|
|
265
|
+
readonly n?: number
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Every subtree a scan spec references, which the table links as roots. */
|
|
269
|
+
export function scanRefs(s: ScanSpec): SubtreeRef[] {
|
|
270
|
+
const refs: SubtreeRef[] = []
|
|
271
|
+
for (const r of [s.sentinel, s.recoverAt, s.stopAt, s.prefix, s.body, s.first, s.lazy]) if (r !== undefined) refs.push(r)
|
|
272
|
+
refs.push(...s.skip)
|
|
273
|
+
return refs
|
|
274
|
+
}
|
|
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
|
|
259
287
|
}
|
|
260
288
|
|
|
261
289
|
/** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
|
|
@@ -629,6 +657,17 @@ export function classHas(cls: ResolvedClass, code: number): boolean {
|
|
|
629
657
|
return false
|
|
630
658
|
}
|
|
631
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
|
+
|
|
632
671
|
function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
|
|
633
672
|
const ascii = new Uint8Array(128)
|
|
634
673
|
const hi: number[] = []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
|
|
3
|
+
*
|
|
4
|
+
* The emitted engine beats the closure engine per site, but its bytes are paid by
|
|
5
|
+
* every process that loads the module. Emitting every site made jess's Less AST
|
|
6
|
+
* artifact 20.8x its grammar source. So a build emits only the sites with the
|
|
7
|
+
* highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
|
|
8
|
+
* runs the rest (`assemble.ts`, the `HYBRID` path).
|
|
9
|
+
*
|
|
10
|
+
* Hotness is estimated statically, since a macro build has no workload to
|
|
11
|
+
* profile: every rule entry starts at 1, a repetition multiplies its body by
|
|
12
|
+
* `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
|
|
13
|
+
* weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
|
|
14
|
+
* instead of diverging. Sites are then taken greedily by weight per emitted byte.
|
|
15
|
+
* A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
|
|
16
|
+
*/
|
|
17
|
+
import { childSlots } from './child-slots.ts'
|
|
18
|
+
import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
|
|
19
|
+
|
|
20
|
+
const LOOP = 8
|
|
21
|
+
const ROUNDS = 60
|
|
22
|
+
const CAP = 1e12
|
|
23
|
+
|
|
24
|
+
/** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
|
|
25
|
+
export function selectHotSites(
|
|
26
|
+
code: readonly number[],
|
|
27
|
+
entries: readonly number[],
|
|
28
|
+
siteBytes: ReadonlyMap<number, number>,
|
|
29
|
+
budget: number,
|
|
30
|
+
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
31
|
+
* static estimate when given. */
|
|
32
|
+
profile?: ArrayLike<number>,
|
|
33
|
+
): Set<number> {
|
|
34
|
+
const selected = new Set<number>()
|
|
35
|
+
if (budget <= 0) return selected
|
|
36
|
+
const weight = profile === undefined ? staticWeights(code, entries) : undefined
|
|
37
|
+
const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
|
|
38
|
+
// Ties (saturated cycles) break by site offset, so a build is deterministic.
|
|
39
|
+
const ranked = [...siteBytes.keys()].sort((a, b) =>
|
|
40
|
+
w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
41
|
+
let spent = 0
|
|
42
|
+
for (const ip of ranked) {
|
|
43
|
+
if (w(ip) === 0) break
|
|
44
|
+
const bytes = siteBytes.get(ip)!
|
|
45
|
+
if (spent + bytes > budget) continue
|
|
46
|
+
spent += bytes
|
|
47
|
+
selected.add(ip)
|
|
48
|
+
}
|
|
49
|
+
return selected
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
53
|
+
let weight = new Map<number, number>()
|
|
54
|
+
const kids: number[] = []
|
|
55
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
56
|
+
const next = new Map<number, number>()
|
|
57
|
+
for (const ip of entries) next.set(ip, 1)
|
|
58
|
+
for (const [ip, w] of weight) {
|
|
59
|
+
kids.length = 0
|
|
60
|
+
childSlots(code, ip, kids)
|
|
61
|
+
const op = code[ip]
|
|
62
|
+
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
63
|
+
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
64
|
+
: 1
|
|
65
|
+
for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
66
|
+
}
|
|
67
|
+
weight = next
|
|
68
|
+
}
|
|
69
|
+
return weight
|
|
70
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CharRange, Combinator, ParseContext } from '../types.ts'
|
|
2
|
+
import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
|
|
3
|
+
import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
|
|
7
|
+
* the path every labelled trivia scan takes once a log is live, answered without
|
|
8
|
+
* the scan where the answer is already known. Both engines call it: the closure
|
|
9
|
+
* engine's `skipTrivia` and the emitted assembly's skips.
|
|
10
|
+
*
|
|
11
|
+
* ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
|
|
12
|
+
* and a run in which no character can start a selected arm holds none: its end
|
|
13
|
+
* is the whole answer, and the unlabelled scanner gives it without the
|
|
14
|
+
* chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
|
|
15
|
+
* selected leaders is free of selected chunks. jess parses with
|
|
16
|
+
* `rootTrivia: { select: [comment labels] }`, which is this case for every run
|
|
17
|
+
* of plain whitespace.
|
|
18
|
+
*
|
|
19
|
+
* A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
|
|
20
|
+
* trivia, so the next term asks for the same gap again: on `benchmark.less`,
|
|
21
|
+
* 21,741 of the closure engine's 53,607 trivia scans per parse started where
|
|
22
|
+
* the previous one did. A scan that recorded nothing records nothing again from
|
|
23
|
+
* the same position, in the same parse, under the same trivia, root selection,
|
|
24
|
+
* root-capture policy, trivia log and state, and with no CST capture, and it
|
|
25
|
+
* ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
|
|
26
|
+
* The state is part of the key because a trivia arm can read it (`gate`), and
|
|
27
|
+
* the log because a `leaf` sets it aside while its child runs. The memo lives
|
|
28
|
+
* on the context so it is released with the parse.
|
|
29
|
+
*
|
|
30
|
+
* Neither applies with `trackLines` (a scan records line ranges) or under CST
|
|
31
|
+
* capture (a scan records into the open buffer).
|
|
32
|
+
*/
|
|
33
|
+
export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
34
|
+
const triviaP = ctx.trivia
|
|
35
|
+
const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
|
|
36
|
+
const gap = ctx._gap
|
|
37
|
+
if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
|
|
38
|
+
&& ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
|
|
39
|
+
&& ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
|
|
40
|
+
const end = clearRunEnd(input, cur, ctx, triviaP, plain)
|
|
41
|
+
if (end >= 0) {
|
|
42
|
+
remember(cur, end, ctx, input, triviaP)
|
|
43
|
+
return end
|
|
44
|
+
}
|
|
45
|
+
const scan = scanTriviaCompact(input, cur, ctx)
|
|
46
|
+
if (typeof scan !== 'number') return commitTriviaScan(scan)
|
|
47
|
+
if (plain) remember(cur, scan, ctx, input, triviaP)
|
|
48
|
+
return scan
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
|
|
52
|
+
function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
|
|
53
|
+
const rootKinds = ctx._rootTriviaKindIndex
|
|
54
|
+
const spec = triviaP?._meta.labeledTriviaSpec
|
|
55
|
+
if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
|
|
56
|
+
if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
|
|
57
|
+
const fast = spec.rootScan
|
|
58
|
+
if (fast === null) return -1
|
|
59
|
+
const end = fast(input, cur)
|
|
60
|
+
return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
|
|
64
|
+
const gap = ctx._gap
|
|
65
|
+
if (gap === undefined) {
|
|
66
|
+
ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
gap.at = cur
|
|
70
|
+
gap.end = end
|
|
71
|
+
gap.input = input
|
|
72
|
+
gap.of = triviaP
|
|
73
|
+
gap.root = ctx._rootTriviaCapture
|
|
74
|
+
gap.rootKinds = ctx._rootTriviaKindIndex
|
|
75
|
+
gap.log = ctx._triviaLog
|
|
76
|
+
gap.state = ctx.state
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prime `spec` for the selection `rootKinds`: the first-character ranges of the
|
|
81
|
+
* arms it selects, and the scanner, which is `null` where one of those arms is
|
|
82
|
+
* unbounded or the trivia has no unlabelled scanner. `run()` builds one
|
|
83
|
+
* selection per parse, so this runs once per parse per trivia.
|
|
84
|
+
*/
|
|
85
|
+
function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
|
|
86
|
+
const ranges: CharRange[] = []
|
|
87
|
+
let bounded = true
|
|
88
|
+
for (const arm of spec.arms) {
|
|
89
|
+
if (rootKinds[arm.label] === undefined) continue
|
|
90
|
+
const fs = arm.parser._meta.firstSet
|
|
91
|
+
if (fs.kind === 'any') bounded = false
|
|
92
|
+
else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
|
|
93
|
+
}
|
|
94
|
+
const classified = triviaP._meta.triviaScanner
|
|
95
|
+
spec.rootKinds = rootKinds
|
|
96
|
+
spec.rootLeaders = ranges
|
|
97
|
+
spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
|
|
101
|
+
* the ranges are code points, and a code unit cannot be checked against them. */
|
|
102
|
+
function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
|
|
103
|
+
for (let i = from; i < to; i++) {
|
|
104
|
+
const c = input.charCodeAt(i)
|
|
105
|
+
if (c >= 0xD800 && c <= 0xDFFF) return false
|
|
106
|
+
for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LineView } from './combinators/lines.ts'
|
|
1
2
|
import type { LabeledTriviaSpec } from './cst/trivia-kinds.ts'
|
|
2
3
|
|
|
3
4
|
export type Span = {
|
|
@@ -113,6 +114,11 @@ export type ParserDef =
|
|
|
113
114
|
// pre-ambient raw byte-walk.
|
|
114
115
|
| { tag: 'scanTo'; sentinel: Combinator<unknown>; skip: Combinator<unknown>[]; raw: boolean; orEOF: boolean; recoverAt?: Combinator<unknown>; stopAt?: Combinator<unknown> }
|
|
115
116
|
| { tag: 'keywords'; words: readonly string[]; caseInsensitive: boolean; boundary: string | undefined }
|
|
117
|
+
// `lines()`: `body` parses a run of lines with `prefix` (`first` on the first
|
|
118
|
+
// line) removed from each; `lazy` admits a line the prefix does not match.
|
|
119
|
+
| { tag: 'lines'; prefix: Combinator<unknown>; body: Combinator<unknown>; first?: Combinator<unknown>; lazy?: Combinator<unknown> }
|
|
120
|
+
// `columns(n)`: n columns of indentation, a tab reaching the next stop of four.
|
|
121
|
+
| { tag: 'columns'; n: number }
|
|
116
122
|
| { tag: 'unknown' }
|
|
117
123
|
|
|
118
124
|
export type Combinator<T> = {
|
|
@@ -280,6 +286,14 @@ export type ParseContext = {
|
|
|
280
286
|
trackLines: boolean
|
|
281
287
|
/** Grammar-author-provided state, scoped with withCtx() and read in guard(). */
|
|
282
288
|
state?: unknown
|
|
289
|
+
/**
|
|
290
|
+
* Inside a `lines()` body, the run's lines: where each line's content sits in
|
|
291
|
+
* the body's input and in the run's own input, and the column it starts at.
|
|
292
|
+
* Undefined outside any run.
|
|
293
|
+
*/
|
|
294
|
+
lineView?: LineView | undefined
|
|
295
|
+
/** Framework-internal: where `columns()` last stopped inside a tab, for `lines()`. */
|
|
296
|
+
_columnsSplit?: { at: number; split: number } | undefined
|
|
283
297
|
/**
|
|
284
298
|
* Mode host (RULE_ABI_PLAN §7): when set, a linkable/fused grammar's `node()`
|
|
285
299
|
* rules build via `build(type, children, fields, span, rawChildren, triviaLog, state)`
|
|
@@ -384,6 +398,12 @@ export type ParseContext = {
|
|
|
384
398
|
* selected markers into the document-root capture.
|
|
385
399
|
*/
|
|
386
400
|
_rootTriviaCapture?: boolean | undefined
|
|
401
|
+
/**
|
|
402
|
+
* Framework-internal: the last deferred-commit trivia scan in this parse that
|
|
403
|
+
* recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
|
|
404
|
+
* with the parse.
|
|
405
|
+
*/
|
|
406
|
+
_gap?: TriviaGapMemo | undefined
|
|
387
407
|
/**
|
|
388
408
|
* Framework-internal: flat per-node trivia log for CST capture mode.
|
|
389
409
|
* When set alongside _cstRawChildren, each trivia entry is recorded as three
|
|
@@ -592,3 +612,15 @@ export type CompiledParser<T> = {
|
|
|
592
612
|
/** Present only when compiled with `{ coverage: true }`. */
|
|
593
613
|
coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
|
|
594
614
|
}
|
|
615
|
+
|
|
616
|
+
/** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
|
|
617
|
+
export interface TriviaGapMemo {
|
|
618
|
+
at: number
|
|
619
|
+
end: number
|
|
620
|
+
input: string
|
|
621
|
+
of: Combinator<unknown> | undefined
|
|
622
|
+
root: boolean | undefined
|
|
623
|
+
rootKinds: Readonly<Record<string, number>> | undefined
|
|
624
|
+
log: number[] | undefined
|
|
625
|
+
state: unknown
|
|
626
|
+
}
|
package/src/version.ts
CHANGED