parseman 0.50.3 → 0.50.4

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.50.3";
16
+ export declare const PARSEMAN_VERSION = "0.50.4";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.50.3",
3
+ "version": "0.50.4",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
7
7
  },
8
- "description": "Parser combinators that compile to optimized JavaScript \u2014 use as a library or as a build-time macro",
8
+ "description": "Parser combinators that compile to optimized JavaScript — use as a library or as a build-time macro",
9
9
  "type": "module",
10
10
  "sideEffects": false,
11
11
  "author": "Matthew Dean <matthew-dean@users.noreply.github.com>",
@@ -63,11 +63,31 @@ export function classifyFinalChoice(
63
63
  return { exclusive, firstSets, nullable }
64
64
  }
65
65
 
66
- /** The `resolve`-supplied target for an unresolvable NAMED lazy, if any. */
67
- function resolveNamedRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
66
+ /** The compose WINNER a named lazy ref resolves to — the same row the encoder's
67
+ * `lazy` case calls via `winners`/`scopedRef`. Winner-FIRST resolution is what makes
68
+ * first-set / nullability analysis AGREE with emission: a base piece's thunk still
69
+ * closes over its own binding, so a compose OVERRIDE (and any undefined cross-piece
70
+ * ref) would otherwise be invisible to the gate — the reducer reroutes, the recognizer
71
+ * does not. `undefined` → no winner; fall back to the thunk. Monolithic: the winner IS
72
+ * the thunk's target, a no-op. A winner that recurses back to `p` (recursive or
73
+ * self-alias rule) is caught by the shared `seen` guard → `any`, a sound over-approx. */
74
+ function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
68
75
  if (resolve === undefined) return undefined
69
76
  const name = (p as unknown as { _ruleName?: string })._ruleName
70
- return name === undefined ? undefined : resolve(name)
77
+ const w = name === undefined ? undefined : resolve(name)
78
+ // Skip a winner that merely wraps THIS reference (a transparent rules()/parser()
79
+ // alias): resolving to it is a self-hop, not an override — matches the encoder's
80
+ // own `winnerWrapsReference` guard so first-set analysis agrees with emission.
81
+ if (w === undefined || w === p) return undefined
82
+ let cur = w
83
+ for (const seen = new Set<Combinator<unknown>>(); !seen.has(cur); ) {
84
+ if (cur === p) return undefined
85
+ seen.add(cur)
86
+ const cd = cur._def as ParserDef
87
+ if (cd.tag !== 'grammar' && cd.tag !== 'trivia') return w
88
+ cur = cd.parser
89
+ }
90
+ return w
71
91
  }
72
92
 
73
93
  export function union(a: FirstSet, b: FirstSet): FirstSet {
@@ -190,9 +210,12 @@ function matchesEmptyBody(
190
210
  case 'node':
191
211
  case 'grammar':
192
212
  case 'recover': return me(d.parser)
193
- case 'lazy':
213
+ case 'lazy': {
214
+ const w = winnerForRef(p, resolve)
215
+ if (w !== undefined) return me(w)
194
216
  try { return me(d.thunk()) }
195
- catch { const t = resolveNamedRef(p, resolve); return t ? me(t) : true }
217
+ catch { return true }
218
+ }
196
219
  default: return true // scanTo / guard / unknown → assume nullable (safe)
197
220
  }
198
221
  }
@@ -338,9 +361,12 @@ function firstSetBody(
338
361
  case 'regex':
339
362
  case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
340
363
  case 'dispatch': return fs(d.selector)
341
- case 'lazy':
364
+ case 'lazy': {
365
+ const w = winnerForRef(p, resolve)
366
+ if (w !== undefined) return fs(w)
342
367
  try { return fs(d.thunk()) }
343
- catch { const t = resolveNamedRef(p, resolve); return t ? fs(t) : any() }
368
+ catch { return any() }
369
+ }
344
370
  case 'choice': {
345
371
  let out: FirstSet = empty()
346
372
  for (const arm of d.parsers) out = union(out, fs(arm))
@@ -1404,7 +1404,15 @@ class Encoder {
1404
1404
  // names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
1405
1405
  // running the body reports `['"x"']` alone.
1406
1406
  if (matchesEmpty(d.parser)) return body
1407
- const fs = d.parser._meta.firstSet
1407
+ // A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
1408
+ // first set from the ref's base binding. Under a compose OVERRIDE the winner
1409
+ // for `Y` may be WIDER, so the cached set gates out input the merged rule
1410
+ // accepts (the reducer reroutes via `winners`, but this recognizer gate did
1411
+ // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1412
+ // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1413
+ const fs = d.parser._def.tag === 'lazy'
1414
+ ? firstSetOf(d.parser, new Set(), this.refResolver())
1415
+ : d.parser._meta.firstSet
1408
1416
  if (fs.kind === 'any') return body
1409
1417
  const cls = this.charClass(fs)
1410
1418
  if (cls < 0) return body
@@ -1594,7 +1602,11 @@ class Encoder {
1594
1602
  // run itself would have skipped past. Running the inner instead reported a
1595
1603
  // strictly smaller set, so omitting the guard was an observable divergence,
1596
1604
  // not a slower equivalent.
1597
- const fs = d.parser._meta.firstSet
1605
+ // Same override-aware resolution as the node gate: a bare-ref inner resolves
1606
+ // its gate through `winners` so a widened winner is not gated out.
1607
+ const fs = d.parser._def.tag === 'lazy'
1608
+ ? firstSetOf(d.parser, new Set(), this.refResolver())
1609
+ : d.parser._meta.firstSet
1598
1610
  if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1599
1611
  const cls = this.charClass(fs)
1600
1612
  if (cls < 0) return inner
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.50.3'
16
+ export const PARSEMAN_VERSION = '0.50.4'