parseman 0.55.0 → 0.55.2
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/commitment.d.ts +27 -2
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +110 -80
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +110 -80
- package/dist/cli/index.js.map +2 -2
- package/dist/index.js +111 -81
- package/dist/index.js.map +2 -2
- package/dist/plugin/index.js +111 -81
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +110 -80
- package/dist/table/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/commitment.ts +107 -20
- package/src/table/encode.ts +47 -70
- package/src/version.ts +1 -1
package/dist/version.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
|
|
14
14
|
* the build if this constant drifts from the published version.
|
|
15
15
|
*/
|
|
16
|
-
export declare const PARSEMAN_VERSION = "0.55.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.55.2";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -441,24 +441,41 @@ export function hasDirectBuildDef(p: Combinator<unknown>, seen: Set<Combinator<u
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* True when `p` can report a committed failure through emitFallible's failure
|
|
446
|
+
* channel: a `dispatch`, or a shape this analysis cannot see into, is reachable
|
|
447
|
+
* along `commitFailureSuccessors`' edges.
|
|
448
|
+
*
|
|
449
|
+
* `memo` carries exact answers between calls over one graph and one `resolve`;
|
|
450
|
+
* a caller asking at many sites passes the same map to every call, so the graph
|
|
451
|
+
* is walked once rather than once per site.
|
|
452
|
+
*/
|
|
445
453
|
export function mayCommitFailure(
|
|
446
454
|
p: Combinator<unknown>,
|
|
447
|
-
seen: Set<Combinator<unknown>> = new Set(),
|
|
448
455
|
resolve?: RefResolver,
|
|
456
|
+
memo: Map<Combinator<unknown>, boolean> = new Map(),
|
|
449
457
|
): boolean {
|
|
450
|
-
|
|
451
|
-
|
|
458
|
+
return reachesMarked(p, node => commitFailureSuccessors(node, resolve), memo)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const NO_EDGES: readonly Combinator<unknown>[] = []
|
|
462
|
+
|
|
463
|
+
/** `mayCommitFailure`'s edges out of `p`, or `true` when `p` itself can commit. */
|
|
464
|
+
function commitFailureSuccessors(
|
|
465
|
+
p: Combinator<unknown>,
|
|
466
|
+
resolve: RefResolver | undefined,
|
|
467
|
+
): readonly Combinator<unknown>[] | true {
|
|
452
468
|
const d = p._def
|
|
453
469
|
switch (d.tag) {
|
|
454
470
|
case 'dispatch':
|
|
471
|
+
case 'unknown':
|
|
455
472
|
return true
|
|
456
473
|
case 'choice':
|
|
457
474
|
case 'sequence':
|
|
458
|
-
return d.parsers
|
|
475
|
+
return d.parsers
|
|
459
476
|
// A contained transaction reports every failure uncommitted.
|
|
460
477
|
case 'attempt':
|
|
461
|
-
return d.contain
|
|
478
|
+
return d.contain === true ? NO_EDGES : [d.parser]
|
|
462
479
|
case 'many':
|
|
463
480
|
case 'oneOrMore':
|
|
464
481
|
case 'optional':
|
|
@@ -467,32 +484,102 @@ export function mayCommitFailure(
|
|
|
467
484
|
case 'field':
|
|
468
485
|
case 'grammar':
|
|
469
486
|
case 'node':
|
|
470
|
-
return mayCommitFailure(d.parser, seen, resolve)
|
|
471
|
-
case 'sepBy':
|
|
472
|
-
return mayCommitFailure(d.parser, seen, resolve) || mayCommitFailure(d.separator, seen, resolve)
|
|
473
487
|
case 'token':
|
|
474
488
|
case 'leaf':
|
|
475
|
-
return mayCommitFailure(d.parser, seen, resolve)
|
|
476
489
|
case 'withCtx':
|
|
477
|
-
return
|
|
490
|
+
return [d.parser]
|
|
491
|
+
case 'sepBy':
|
|
492
|
+
return [d.parser, d.separator]
|
|
478
493
|
case 'scanTo':
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
494
|
+
return [
|
|
495
|
+
d.sentinel,
|
|
496
|
+
...d.skip,
|
|
497
|
+
...(d.recoverAt === undefined ? NO_EDGES : [d.recoverAt]),
|
|
498
|
+
...(d.stopAt === undefined ? NO_EDGES : [d.stopAt]),
|
|
499
|
+
]
|
|
483
500
|
case 'lazy': {
|
|
484
|
-
try { return
|
|
501
|
+
try { return [d.thunk()] }
|
|
485
502
|
catch {
|
|
486
503
|
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
487
504
|
const target = name === undefined ? undefined : resolve?.(name)
|
|
488
|
-
return target === undefined ? true :
|
|
505
|
+
return target === undefined ? true : [target]
|
|
489
506
|
}
|
|
490
507
|
}
|
|
491
|
-
case 'unknown':
|
|
492
|
-
return true
|
|
493
508
|
default:
|
|
494
|
-
return
|
|
509
|
+
return NO_EDGES
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Whether a MARKED node — one `successors` answers `true` for — is reachable
|
|
515
|
+
* from `root`.
|
|
516
|
+
*
|
|
517
|
+
* Reachability over a cyclic graph, decided with Tarjan's strongly connected
|
|
518
|
+
* components so that every node the walk enters leaves with its exact answer in
|
|
519
|
+
* `memo`: the members of one component reach each other, so they share one
|
|
520
|
+
* answer, settled when the walk returns to the component's first node. A DFS
|
|
521
|
+
* that instead cuts a cycle at its own ancestor path learns nothing it can keep
|
|
522
|
+
* about a node whose walk hit the cut; one that keeps a visited set per call
|
|
523
|
+
* keeps nothing past the call. Shared across calls, `memo` makes every node
|
|
524
|
+
* entered once however many roots are asked about.
|
|
525
|
+
*
|
|
526
|
+
* `memo` must only ever be used with one `successors`: its entries are answers
|
|
527
|
+
* about that graph.
|
|
528
|
+
*/
|
|
529
|
+
export function reachesMarked<T>(
|
|
530
|
+
root: T,
|
|
531
|
+
successors: (node: T) => readonly T[] | true,
|
|
532
|
+
memo: Map<T, boolean>,
|
|
533
|
+
): boolean {
|
|
534
|
+
const known = memo.get(root)
|
|
535
|
+
if (known !== undefined) return known
|
|
536
|
+
const order = new Map<T, number>()
|
|
537
|
+
const low: number[] = []
|
|
538
|
+
const found: boolean[] = []
|
|
539
|
+
const stack: T[] = []
|
|
540
|
+
const visit = (node: T): number => {
|
|
541
|
+
const at = low.length
|
|
542
|
+
order.set(node, at)
|
|
543
|
+
low.push(at)
|
|
544
|
+
found.push(false)
|
|
545
|
+
stack.push(node)
|
|
546
|
+
const next = successors(node)
|
|
547
|
+
if (next === true) found[at] = true
|
|
548
|
+
else {
|
|
549
|
+
for (const child of next) {
|
|
550
|
+
// Settled: a node that reaches a marked node is answered, and so is
|
|
551
|
+
// every node still above it on the stack, since each reaches it. The
|
|
552
|
+
// children left unentered stay unmemoized for a later walk to settle.
|
|
553
|
+
if (found[at]) break
|
|
554
|
+
const hit = memo.get(child)
|
|
555
|
+
if (hit !== undefined) {
|
|
556
|
+
if (hit) found[at] = true
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
const seen = order.get(child)
|
|
560
|
+
if (seen !== undefined) {
|
|
561
|
+
// Unmemoized but entered: still on the stack, so in this node's
|
|
562
|
+
// component. Its answer reaches the component's first node by the
|
|
563
|
+
// tree edges, not through here.
|
|
564
|
+
if (seen < low[at]!) low[at] = seen
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
const k = visit(child)
|
|
568
|
+
if (low[k]! < low[at]!) low[at] = low[k]!
|
|
569
|
+
if (found[k]) found[at] = true
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (low[at] === at) {
|
|
573
|
+
const answer = found[at]!
|
|
574
|
+
let member: T
|
|
575
|
+
do {
|
|
576
|
+
member = stack.pop()!
|
|
577
|
+
memo.set(member, answer)
|
|
578
|
+
} while (member !== node)
|
|
579
|
+
}
|
|
580
|
+
return at
|
|
495
581
|
}
|
|
582
|
+
return found[visit(root)]!
|
|
496
583
|
}
|
|
497
584
|
|
|
498
585
|
/** A leaf-composed imported piece may carry Parseman's own structural balanced
|
package/src/table/encode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
2
|
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
3
|
import { childrenOf } from '../analysis/gating.ts'
|
|
4
|
-
import { capturesLeaf, mayCommitFailure, mayLeavePartialCapture } from '../analysis/commitment.ts'
|
|
4
|
+
import { capturesLeaf, mayCommitFailure, mayLeavePartialCapture, reachesMarked } from '../analysis/commitment.ts'
|
|
5
5
|
import { getCoreLiteralValue } from '../combinators/choice.ts'
|
|
6
6
|
import { deriveExpected } from '../combinators/expect.ts'
|
|
7
7
|
import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
|
|
@@ -147,21 +147,20 @@ class Encoder {
|
|
|
147
147
|
private nonCommittingChoiceSites = new Set<number>()
|
|
148
148
|
private failureRollbackCleanSites = new Set<number>()
|
|
149
149
|
/**
|
|
150
|
-
* `
|
|
151
|
-
* for the lifetime of this `Encoder`.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* (as a local inside `failureNeedsRollback`) was correct but discarded that
|
|
156
|
-
* fact after every arm, so a grammar with many ordered-choice arms / optional
|
|
157
|
-
* / attempt sites re-walked the SAME shared rule graph from scratch for each
|
|
158
|
-
* site — O(sites × graph size) instead of O(graph size). Measured on the
|
|
159
|
-
* scss-parser grammar (composed CSS-base superset, the deepest/largest of the
|
|
160
|
-
* four grammars): 47-50s for one `compileRuleMap` with the per-call memo,
|
|
161
|
-
* versus the whole macro transform completing in under a second once this
|
|
162
|
-
* memo is hoisted here.
|
|
150
|
+
* `reachesSideWriter`'s answers, shared by every `failureNeedsRollback` call
|
|
151
|
+
* for the lifetime of this `Encoder`. Every entry is exact — it depends only on
|
|
152
|
+
* the node's own reachable subgraph, never on which root's walk asked — so each
|
|
153
|
+
* combinator is walked once per encode however many choice arms, optionals and
|
|
154
|
+
* attempt sites reach it.
|
|
163
155
|
*/
|
|
164
156
|
private sideWriterMemo = new Map<Combinator<unknown>, boolean>()
|
|
157
|
+
/**
|
|
158
|
+
* `mayCommitFailure`'s answers, shared by every choice site for the lifetime
|
|
159
|
+
* of this `Encoder` — exact for the graph and the `winners` resolver it is
|
|
160
|
+
* encoding, so the rule graph is walked once per encode rather than once per
|
|
161
|
+
* choice site.
|
|
162
|
+
*/
|
|
163
|
+
private commitFailureMemo = new Map<Combinator<unknown>, boolean>()
|
|
165
164
|
labels: readonly string[] | undefined = undefined
|
|
166
165
|
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
167
166
|
activeTrivia: Combinator<unknown> | undefined = undefined
|
|
@@ -653,8 +652,7 @@ class Encoder {
|
|
|
653
652
|
* writer census covers the side sinks that proof deliberately does not model:
|
|
654
653
|
* fields, recovered errors, and interpreted scanners. Strict assemblies do
|
|
655
654
|
* not run repeat recovery, but these rows can still publish through a caller's
|
|
656
|
-
* context.
|
|
657
|
-
* discover a writer the first traversal did not already reach.
|
|
655
|
+
* context.
|
|
658
656
|
*/
|
|
659
657
|
private failureNeedsRollback(
|
|
660
658
|
p: Combinator<unknown>,
|
|
@@ -663,63 +661,42 @@ class Encoder {
|
|
|
663
661
|
const refName = (p as unknown as { _ruleName?: string })._ruleName
|
|
664
662
|
const winner = refName === undefined ? undefined : this.winners?.[refName]
|
|
665
663
|
const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
|
|
666
|
-
const sideWriterMemo = this.sideWriterMemo
|
|
667
|
-
// A single mutable `seen` set threaded through the whole call, rather than
|
|
668
|
-
// copied at every recursion level: this DFS visits children strictly
|
|
669
|
-
// sequentially, so marking `current` in progress before recursing and
|
|
670
|
-
// clearing it once its subtree is fully explored reproduces the exact same
|
|
671
|
-
// ancestor-path membership an immutable copy-per-level would have computed,
|
|
672
|
-
// without the O(depth) allocation on every node.
|
|
673
|
-
const seen = new Set<Combinator<unknown>>()
|
|
674
|
-
const hasSideWriter = (current: Combinator<unknown>): { found: boolean; complete: boolean } => {
|
|
675
|
-
const cached = sideWriterMemo.get(current)
|
|
676
|
-
if (cached !== undefined) return { found: cached, complete: true }
|
|
677
|
-
if (seen.has(current)) return { found: false, complete: false }
|
|
678
|
-
seen.add(current)
|
|
679
|
-
const def = current._def
|
|
680
|
-
let result: { found: boolean; complete: boolean }
|
|
681
|
-
switch (def.tag) {
|
|
682
|
-
case 'field':
|
|
683
|
-
case 'expect':
|
|
684
|
-
case 'recover':
|
|
685
|
-
case 'scanTo':
|
|
686
|
-
case 'unknown':
|
|
687
|
-
result = { found: true, complete: true }
|
|
688
|
-
break
|
|
689
|
-
case 'lazy': {
|
|
690
|
-
const name = (current as unknown as { _ruleName?: string })._ruleName
|
|
691
|
-
const resolvedWinner = name === undefined ? undefined : this.winners?.[name]
|
|
692
|
-
if (resolvedWinner !== undefined && !winnerWrapsReference(resolvedWinner, current)) {
|
|
693
|
-
result = hasSideWriter(resolvedWinner)
|
|
694
|
-
break
|
|
695
|
-
}
|
|
696
|
-
try { result = hasSideWriter(def.thunk()) }
|
|
697
|
-
catch { result = { found: true, complete: true } }
|
|
698
|
-
break
|
|
699
|
-
}
|
|
700
|
-
default: {
|
|
701
|
-
let complete = true
|
|
702
|
-
result = { found: false, complete: true }
|
|
703
|
-
for (const child of childrenOf(def)) {
|
|
704
|
-
const childResult = hasSideWriter(child)
|
|
705
|
-
if (childResult.found) {
|
|
706
|
-
result = { found: true, complete: true }
|
|
707
|
-
break
|
|
708
|
-
}
|
|
709
|
-
complete &&= childResult.complete
|
|
710
|
-
}
|
|
711
|
-
if (!result.found) result = { found: false, complete }
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
seen.delete(current)
|
|
715
|
-
if (result.found || result.complete) sideWriterMemo.set(current, result.found)
|
|
716
|
-
return result
|
|
717
|
-
}
|
|
718
664
|
return mayLeavePartialCapture(root, new Set(), true)
|
|
719
|
-
||
|
|
665
|
+
|| this.reachesSideWriter(root)
|
|
720
666
|
|| ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
|
|
721
667
|
}
|
|
722
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Whether a field, recovered error, interpreted scanner or unresolvable shape
|
|
671
|
+
* is reachable from `root` — the writer census of `failureNeedsRollback`,
|
|
672
|
+
* entering each combinator once per `Encoder` (see `reachesMarked`).
|
|
673
|
+
*/
|
|
674
|
+
private reachesSideWriter(root: Combinator<unknown>): boolean {
|
|
675
|
+
return reachesMarked(root, node => this.sideWriterSuccessors(node), this.sideWriterMemo)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** The census's edges out of `node`, or `true` when `node` is itself a writer. */
|
|
679
|
+
private sideWriterSuccessors(node: Combinator<unknown>): readonly Combinator<unknown>[] | true {
|
|
680
|
+
const def = node._def
|
|
681
|
+
switch (def.tag) {
|
|
682
|
+
case 'field':
|
|
683
|
+
case 'expect':
|
|
684
|
+
case 'recover':
|
|
685
|
+
case 'scanTo':
|
|
686
|
+
case 'unknown':
|
|
687
|
+
return true
|
|
688
|
+
case 'lazy': {
|
|
689
|
+
const name = (node as unknown as { _ruleName?: string })._ruleName
|
|
690
|
+
const winner = name === undefined ? undefined : this.winners?.[name]
|
|
691
|
+
if (winner !== undefined && !winnerWrapsReference(winner, node)) return [winner]
|
|
692
|
+
try { return [def.thunk()] }
|
|
693
|
+
catch { return true }
|
|
694
|
+
}
|
|
695
|
+
default:
|
|
696
|
+
return childrenOf(def)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
723
700
|
/** A first set becomes a char class string, or −1 for `any`. */
|
|
724
701
|
private charClass(fs: FirstSet): number {
|
|
725
702
|
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
@@ -1244,7 +1221,7 @@ class Encoder {
|
|
|
1244
1221
|
this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
|
|
1245
1222
|
}
|
|
1246
1223
|
this.choiceRollbackMasks.set(head, rollbackMask)
|
|
1247
|
-
if (arms.every(arm => !mayCommitFailure(arm,
|
|
1224
|
+
if (arms.every(arm => !mayCommitFailure(arm, rr, this.commitFailureMemo))) {
|
|
1248
1225
|
this.nonCommittingChoiceSites.add(head)
|
|
1249
1226
|
}
|
|
1250
1227
|
return head
|
package/src/version.ts
CHANGED