parseman 0.55.1 → 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/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.1";
16
+ export declare const PARSEMAN_VERSION = "0.55.2";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.55.1",
3
+ "version": "0.55.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -441,24 +441,41 @@ export function hasDirectBuildDef(p: Combinator<unknown>, seen: Set<Combinator<u
441
441
  }
442
442
  }
443
443
 
444
- /** True when `p` can report a committed failure through emitFallible's failure channel. */
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
- if (seen.has(p)) return false
451
- seen.add(p)
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.some(x => mayCommitFailure(x, seen, resolve))
475
+ return d.parsers
459
476
  // A contained transaction reports every failure uncommitted.
460
477
  case 'attempt':
461
- return d.contain !== true && mayCommitFailure(d.parser, seen, resolve)
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 mayCommitFailure(d.parser, seen, resolve)
490
+ return [d.parser]
491
+ case 'sepBy':
492
+ return [d.parser, d.separator]
478
493
  case 'scanTo':
479
- return mayCommitFailure(d.sentinel, seen, resolve)
480
- || d.skip.some(x => mayCommitFailure(x, seen, resolve))
481
- || (d.recoverAt !== undefined && mayCommitFailure(d.recoverAt, seen, resolve))
482
- || (d.stopAt !== undefined && mayCommitFailure(d.stopAt, seen, resolve))
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 mayCommitFailure(d.thunk(), seen, resolve) }
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 : mayCommitFailure(target, seen, resolve)
505
+ return target === undefined ? true : [target]
489
506
  }
490
507
  }
491
- case 'unknown':
492
- return true
493
508
  default:
494
- return false
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
@@ -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'
@@ -154,6 +154,13 @@ class Encoder {
154
154
  * attempt sites reach it.
155
155
  */
156
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>()
157
164
  labels: readonly string[] | undefined = undefined
158
165
  /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
159
166
  activeTrivia: Combinator<unknown> | undefined = undefined
@@ -661,65 +668,11 @@ class Encoder {
661
668
 
662
669
  /**
663
670
  * Whether a field, recovered error, interpreted scanner or unresolvable shape
664
- * is reachable from `root` — the writer census of `failureNeedsRollback`.
665
- *
666
- * Reachability over a cyclic graph, decided with Tarjan's strongly connected
667
- * components so that every node the walk enters leaves with its exact answer
668
- * memoized: the members of one component reach each other, so they share one
669
- * answer, settled when the walk returns to the component's first node. A DFS
670
- * that instead cuts a cycle at its own ancestor path learns nothing it can
671
- * keep about a node whose walk hit the cut, and re-walks that node along
672
- * every path that reaches it — exponential over recursive value rules (linking
673
- * jess's SCSS grammar cost ~290G instructions). This walk enters each node
674
- * once per `Encoder`.
671
+ * is reachable from `root` — the writer census of `failureNeedsRollback`,
672
+ * entering each combinator once per `Encoder` (see `reachesMarked`).
675
673
  */
676
674
  private reachesSideWriter(root: Combinator<unknown>): boolean {
677
- const memo = this.sideWriterMemo
678
- const known = memo.get(root)
679
- if (known !== undefined) return known
680
- const order = new Map<Combinator<unknown>, number>()
681
- const low: number[] = []
682
- const found: boolean[] = []
683
- const stack: Combinator<unknown>[] = []
684
- const visit = (node: Combinator<unknown>): number => {
685
- const at = low.length
686
- order.set(node, at)
687
- low.push(at)
688
- found.push(false)
689
- stack.push(node)
690
- const next = this.sideWriterSuccessors(node)
691
- if (next === true) found[at] = true
692
- else {
693
- for (const child of next) {
694
- const hit = memo.get(child)
695
- if (hit !== undefined) {
696
- if (hit) found[at] = true
697
- continue
698
- }
699
- const seen = order.get(child)
700
- if (seen !== undefined) {
701
- // Unmemoized but entered: still on the stack, so in this node's
702
- // component. Its answer reaches the component's first node by the
703
- // tree edges, not through here.
704
- if (seen < low[at]!) low[at] = seen
705
- continue
706
- }
707
- const k = visit(child)
708
- if (low[k]! < low[at]!) low[at] = low[k]!
709
- if (found[k]) found[at] = true
710
- }
711
- }
712
- if (low[at] === at) {
713
- const answer = found[at]!
714
- let member: Combinator<unknown>
715
- do {
716
- member = stack.pop()!
717
- memo.set(member, answer)
718
- } while (member !== node)
719
- }
720
- return at
721
- }
722
- return found[visit(root)]!
675
+ return reachesMarked(root, node => this.sideWriterSuccessors(node), this.sideWriterMemo)
723
676
  }
724
677
 
725
678
  /** The census's edges out of `node`, or `true` when `node` is itself a writer. */
@@ -1268,7 +1221,7 @@ class Encoder {
1268
1221
  this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
1269
1222
  }
1270
1223
  this.choiceRollbackMasks.set(head, rollbackMask)
1271
- if (arms.every(arm => !mayCommitFailure(arm, new Set(), rr))) {
1224
+ if (arms.every(arm => !mayCommitFailure(arm, rr, this.commitFailureMemo))) {
1272
1225
  this.nonCommittingChoiceSites.add(head)
1273
1226
  }
1274
1227
  return head
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
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 const PARSEMAN_VERSION = '0.55.1'
16
+ export const PARSEMAN_VERSION = '0.55.2'