parseman 0.50.6 → 0.50.7

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.6";
16
+ export declare const PARSEMAN_VERSION = "0.50.7";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.50.6",
3
+ "version": "0.50.7",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -25,6 +25,7 @@ export type FinalChoiceClassification = {
25
25
  export function classifyFinalChoice(
26
26
  arms: readonly Combinator<unknown>[],
27
27
  resolve?: RefResolver,
28
+ activeTrivia?: Combinator<unknown> | null,
28
29
  ): FinalChoiceClassification {
29
30
  const firstSets: FirstSet[] = []
30
31
  const nullable: boolean[] = []
@@ -36,7 +37,7 @@ export function classifyFinalChoice(
36
37
  for (const arm of arms) {
37
38
  const empty = matchesEmpty(arm, new Set(), resolve)
38
39
  nullable.push(empty)
39
- firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve))
40
+ firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve, activeTrivia))
40
41
  if (empty) exclusive = false
41
42
  }
42
43
  if (exclusive) {
@@ -109,7 +110,7 @@ export function intersects(a: FirstSet, b: FirstSet): boolean {
109
110
  }
110
111
 
111
112
  export function fromChar(code: number): FirstSet {
112
- return { kind: 'ranges', ranges: [{ lo: code, hi: code }] }
113
+ return fromRange(code, code)
113
114
  }
114
115
 
115
116
  /**
@@ -238,11 +239,10 @@ function matchesEmptyBody(
238
239
  * (see `isPositiveLookahead` and `sequenceFirstSet`).
239
240
  */
240
241
  export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
241
- const tag = (p._def as ParserDef).tag
242
242
  // `adjacency` joins `not` for the same reason: `adjacent()`/`notAdjacent()` are
243
243
  // zero-width tests of the gap BEHIND the cursor, so they constrain nothing about
244
244
  // the first char ahead and must not contribute their `any` to the sequence.
245
- return tag === 'not' || tag === 'adjacency'
245
+ return p._tag === 'not' || p._tag === 'adjacency'
246
246
  }
247
247
 
248
248
  /**
@@ -259,8 +259,10 @@ export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
259
259
  * therefore constrains nothing; `peek()` reports `any()` in that case, which the
260
260
  * intersection treats as "no constraint".
261
261
  */
262
- export function isPositiveLookahead(p: Combinator<unknown>): boolean {
263
- return (p._def as ParserDef).tag === 'peek'
262
+ function isPositiveLookahead(
263
+ p: Combinator<unknown>,
264
+ ): p is Combinator<unknown> & { _def: Extract<ParserDef, { tag: 'peek' }> } {
265
+ return p._tag === 'peek'
264
266
  }
265
267
 
266
268
  /** Intersect a lookahead constraint into an accumulator; `any` = no constraint. */
@@ -285,9 +287,7 @@ function narrowBy(acc: FirstSet | null, constraint: FirstSet): FirstSet | null {
285
287
  * only succeed — even zero-width — where X matches.
286
288
  */
287
289
  function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
288
- if (assertion === null) return fs
289
- if (fs.kind === 'empty') return assertion
290
- return narrowBy(fs, assertion) ?? fs
290
+ return assertion === null ? fs : fs.kind === 'empty' ? assertion : narrowBy(fs, assertion)!
291
291
  }
292
292
 
293
293
  /**
@@ -299,19 +299,36 @@ function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
299
299
  * A leading zero-width assertion (`not(…)`) is nullable but contributes NOTHING to
300
300
  * the first-set (see `isZeroWidthAssertion`) — its `any` must not poison the union.
301
301
  */
302
- export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
303
- let fs: FirstSet = empty()
304
- let assertion: FirstSet | null = null
305
- for (const p of parsers) {
302
+ function sequenceSet(
303
+ parsers: readonly Combinator<unknown>[],
304
+ fs: (p: Combinator<unknown>) => FirstSet,
305
+ empties: (p: Combinator<unknown>) => boolean,
306
+ trivia?: Combinator<unknown> | null,
307
+ ): FirstSet {
308
+ const afterBoundary = (i: number): FirstSet => {
309
+ const rest = at(i)
310
+ return trivia ? union(rest, fs(trivia)) : rest
311
+ }
312
+ const at = (i: number): FirstSet => {
313
+ const p = parsers[i]!
306
314
  if (isPositiveLookahead(p)) {
307
- // Zero-width but CONSTRAINING: intersect, keep scanning (it consumes nothing).
308
- assertion = narrowBy(assertion, p._meta.firstSet)
309
- continue
315
+ const constraint = fs(p)
316
+ return i + 1 === parsers.length
317
+ ? constraint
318
+ : applyAssertion(afterBoundary(i + 1), constraint)
319
+ }
320
+ if (isZeroWidthAssertion(p)) {
321
+ return i + 1 === parsers.length ? empty() : afterBoundary(i + 1)
310
322
  }
311
- if (!isZeroWidthAssertion(p)) fs = union(fs, p._meta.firstSet)
312
- if (!matchesEmpty(p)) return applyAssertion(fs, assertion)
323
+ const first = fs(p)
324
+ if (!empties(p) || i + 1 === parsers.length) return first
325
+ return union(first, afterBoundary(i + 1))
313
326
  }
314
- return applyAssertion(fs, assertion)
327
+ return at(0)
328
+ }
329
+
330
+ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
331
+ return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty)
315
332
  }
316
333
 
317
334
  /**
@@ -335,6 +352,7 @@ export function firstSetOf(
335
352
  p: Combinator<unknown>,
336
353
  seen: Set<Combinator<unknown>> = new Set(),
337
354
  resolve?: RefResolver,
355
+ activeTrivia?: Combinator<unknown> | null,
338
356
  ): FirstSet {
339
357
  // `seen` is the current recursion path, not a global visited set. A completed
340
358
  // shared child must be analysed again for a sibling; only a back-edge on this
@@ -342,7 +360,7 @@ export function firstSetOf(
342
360
  if (seen.has(p)) return any() // cycle → any (safe over-approximation)
343
361
  seen.add(p)
344
362
  try {
345
- return firstSetBody(p, seen, resolve)
363
+ return firstSetBody(p, seen, resolve, activeTrivia)
346
364
  } finally {
347
365
  seen.delete(p)
348
366
  }
@@ -352,8 +370,9 @@ function firstSetBody(
352
370
  p: Combinator<unknown>,
353
371
  seen: Set<Combinator<unknown>>,
354
372
  resolve: RefResolver | undefined,
373
+ activeTrivia: Combinator<unknown> | null | undefined,
355
374
  ): FirstSet {
356
- const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve)
375
+ const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia)
357
376
  const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
358
377
  const d = p._def as ParserDef
359
378
  switch (d.tag) {
@@ -373,19 +392,17 @@ function firstSetBody(
373
392
  return out
374
393
  }
375
394
  case 'sequence': {
376
- // Union through the nullable prefix (a leading nullable term lets a later
377
- // term's first chars start the sequence) — ref-resolving `sequenceFirstSet`.
378
- // A leading zero-width assertion (`not`) contributes nothing (its `any` would
379
- // poison the union) but is still nullable, so keep scanning past it. A
380
- // positive `peek()` is zero-width AND constraining → intersect.
381
- let out: FirstSet = empty()
382
- let assertion: FirstSet | null = null
383
- for (const term of d.parsers) {
384
- if (isPositiveLookahead(term)) { assertion = narrowBy(assertion, fs(term)); continue }
385
- if (!isZeroWidthAssertion(term)) out = union(out, fs(term))
386
- if (!empties(term)) return applyAssertion(out, assertion)
387
- }
388
- return applyAssertion(out, assertion)
395
+ // A zero-width or nullable term leaves the cursor at its current position,
396
+ // while every later term first skips active trivia. The sequence can
397
+ // therefore start with that trivia or with the later term. A positive
398
+ // lookahead still constrains both possibilities at the cursor where it ran:
399
+ //
400
+ // peek ∩ (later ∪ trivia)
401
+ //
402
+ // Recurse at every term boundary because a lookahead after another
403
+ // zero-width term runs after trivia has advanced, not at the sequence's
404
+ // original cursor.
405
+ return sequenceSet(d.parsers, fs, empties, activeTrivia)
389
406
  }
390
407
  case 'peek': {
391
408
  // Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
@@ -402,9 +419,14 @@ function firstSetBody(
402
419
  case 'token':
403
420
  case 'leaf':
404
421
  case 'node':
405
- case 'grammar':
422
+ case 'sepBy':
406
423
  case 'expect': return fs(d.parser)
407
- case 'sepBy': return fs(d.parser) // both min 0 and min 1 start with the item
424
+ case 'grammar': return firstSetOf(
425
+ d.parser,
426
+ seen,
427
+ resolve,
428
+ d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
429
+ )
408
430
  default: return p._meta.firstSet // not / scanTo / guard / withCtx / recover / unknown
409
431
  }
410
432
  }
@@ -539,7 +539,9 @@ class Encoder {
539
539
  private followClass(parsers: readonly Combinator<unknown>[], i: number): number {
540
540
  let fs: FirstSet = { kind: 'empty' }
541
541
  const rr = this.refResolver()
542
- for (let j = i + 1; j < parsers.length; j++) fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr))
542
+ for (let j = i + 1; j < parsers.length; j++) {
543
+ fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, this.activeTrivia))
544
+ }
543
545
  return this.charClass(fs)
544
546
  }
545
547
 
@@ -779,7 +781,7 @@ class Encoder {
779
781
  */
780
782
  private subtree(c: Combinator<unknown>): SubtreeRef {
781
783
  const ip = this.node(c).ip
782
- const fs = firstSetOf(c, new Set(), this.refResolver())
784
+ const fs = firstSetOf(c, new Set(), this.refResolver(), this.activeTrivia)
783
785
  return [ip, fs.kind === 'empty' ? -2 : this.charClass(fs)]
784
786
  }
785
787
 
@@ -1168,7 +1170,7 @@ class Encoder {
1168
1170
  // the O(1) table (`exclusive`) or falls to the ordered per-arm path.
1169
1171
  // Arm ORDER is preserved on both, which is what makes this a PEG-safe
1170
1172
  // change rather than a reordering.
1171
- const finalChoice = classifyFinalChoice(arms, rr)
1173
+ const finalChoice = classifyFinalChoice(arms, rr, this.activeTrivia)
1172
1174
  // attempt() preserves its child's success language, but the broad
1173
1175
  // classifier deliberately treats the transaction as nullable/unknown so
1174
1176
  // it can never by itself authorize exclusive one-arm selection. Recover
@@ -1180,7 +1182,7 @@ class Encoder {
1180
1182
  if (!finalChoice.nullable[i]) return undefined
1181
1183
  const def = arm._def as ParserDef
1182
1184
  if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
1183
- return firstSetOf(def.parser, new Set(), rr)
1185
+ return firstSetOf(def.parser, new Set(), rr, this.activeTrivia)
1184
1186
  })
1185
1187
  const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
1186
1188
  const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
@@ -1239,7 +1241,7 @@ class Encoder {
1239
1241
  const rr = this.refResolver()
1240
1242
  const itemClass = matchesEmpty(d.parser, new Set(), rr)
1241
1243
  ? -1
1242
- : this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
1244
+ : this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1243
1245
  const op = d.valueUnused ? OP_REPV : OP_REP
1244
1246
  const min = d.tag === 'many' ? 0 : d.min
1245
1247
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -1274,7 +1276,7 @@ class Encoder {
1274
1276
  if (this.rec) {
1275
1277
  return this.emit(
1276
1278
  OP_REP, child, d.min, d.max ?? -1, sep, flags,
1277
- this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver())),
1279
+ this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1278
1280
  )
1279
1281
  }
1280
1282
  return d.min >= 2
@@ -1411,7 +1413,7 @@ class Encoder {
1411
1413
  // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1412
1414
  // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1413
1415
  const fs = d.parser._def.tag === 'lazy'
1414
- ? firstSetOf(d.parser, new Set(), this.refResolver())
1416
+ ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1415
1417
  : d.parser._meta.firstSet
1416
1418
  if (fs.kind === 'any') return body
1417
1419
  const cls = this.charClass(fs)
@@ -1605,7 +1607,7 @@ class Encoder {
1605
1607
  // Same override-aware resolution as the node gate: a bare-ref inner resolves
1606
1608
  // its gate through `winners` so a widened winner is not gated out.
1607
1609
  const fs = d.parser._def.tag === 'lazy'
1608
- ? firstSetOf(d.parser, new Set(), this.refResolver())
1610
+ ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1609
1611
  : d.parser._meta.firstSet
1610
1612
  if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1611
1613
  const cls = this.charClass(fs)
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.6'
16
+ export const PARSEMAN_VERSION = '0.50.7'