parseman 0.55.0 → 0.55.1

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.0";
16
+ export declare const PARSEMAN_VERSION = "0.55.1";
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.0",
3
+ "version": "0.55.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -147,19 +147,11 @@ class Encoder {
147
147
  private nonCommittingChoiceSites = new Set<number>()
148
148
  private failureRollbackCleanSites = new Set<number>()
149
149
  /**
150
- * `hasSideWriter` memo for `failureNeedsRollback`, shared across every call
151
- * for the lifetime of this `Encoder`. A cached entry is written only when
152
- * `complete` is true, which means the answer is a path-independent fact
153
- * about that combinator node — it does not depend on which root's traversal
154
- * asked, only on the node's own reachable subgraph. Scoping the memo per call
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>()
165
157
  labels: readonly string[] | undefined = undefined
@@ -653,8 +645,7 @@ class Encoder {
653
645
  * writer census covers the side sinks that proof deliberately does not model:
654
646
  * fields, recovered errors, and interpreted scanners. Strict assemblies do
655
647
  * not run repeat recovery, but these rows can still publish through a caller's
656
- * context. Cycles are safe to stop: revisiting the same combinator cannot
657
- * discover a writer the first traversal did not already reach.
648
+ * context.
658
649
  */
659
650
  private failureNeedsRollback(
660
651
  p: Combinator<unknown>,
@@ -663,61 +654,94 @@ class Encoder {
663
654
  const refName = (p as unknown as { _ruleName?: string })._ruleName
664
655
  const winner = refName === undefined ? undefined : this.winners?.[refName]
665
656
  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
657
+ return mayLeavePartialCapture(root, new Set(), true)
658
+ || this.reachesSideWriter(root)
659
+ || ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
660
+ }
661
+
662
+ /**
663
+ * 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`.
675
+ */
676
+ 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
695
698
  }
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
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
710
706
  }
711
- if (!result.found) result = { found: false, complete }
707
+ const k = visit(child)
708
+ if (low[k]! < low[at]!) low[at] = low[k]!
709
+ if (found[k]) found[at] = true
712
710
  }
713
711
  }
714
- seen.delete(current)
715
- if (result.found || result.complete) sideWriterMemo.set(current, result.found)
716
- return result
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)]!
723
+ }
724
+
725
+ /** The census's edges out of `node`, or `true` when `node` is itself a writer. */
726
+ private sideWriterSuccessors(node: Combinator<unknown>): readonly Combinator<unknown>[] | true {
727
+ const def = node._def
728
+ switch (def.tag) {
729
+ case 'field':
730
+ case 'expect':
731
+ case 'recover':
732
+ case 'scanTo':
733
+ case 'unknown':
734
+ return true
735
+ case 'lazy': {
736
+ const name = (node as unknown as { _ruleName?: string })._ruleName
737
+ const winner = name === undefined ? undefined : this.winners?.[name]
738
+ if (winner !== undefined && !winnerWrapsReference(winner, node)) return [winner]
739
+ try { return [def.thunk()] }
740
+ catch { return true }
741
+ }
742
+ default:
743
+ return childrenOf(def)
717
744
  }
718
- return mayLeavePartialCapture(root, new Set(), true)
719
- || hasSideWriter(root).found
720
- || ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
721
745
  }
722
746
 
723
747
  /** A first set becomes a char class string, or −1 for `any`. */
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.0'
16
+ export const PARSEMAN_VERSION = '0.55.1'