parseman 0.59.3 → 0.60.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.
Files changed (94) hide show
  1. package/dist/analysis/diagnostics.js +876 -190
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +876 -190
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts +13 -1
  6. package/dist/combinators/dispatch.d.ts.map +1 -1
  7. package/dist/combinators/first-set.d.ts +35 -2
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/regex.d.ts.map +1 -1
  10. package/dist/combinators/trivia-boundary.d.ts +11 -0
  11. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  12. package/dist/combinators/trivia-skip.d.ts +1 -1
  13. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  14. package/dist/compiler/token-alphabet.d.ts +2 -0
  15. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  16. package/dist/compiler/token-capability.js +65 -53
  17. package/dist/compiler/token-capability.js.map +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +896 -199
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +903 -201
  24. package/dist/plugin/index.js.map +3 -3
  25. package/dist/regex/first-set.d.ts +19 -0
  26. package/dist/regex/first-set.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts +15 -1
  28. package/dist/table/assemble.d.ts.map +1 -1
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +1 -1
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/compile.d.ts +1 -1
  33. package/dist/table/emit-assembly.d.ts +8 -2
  34. package/dist/table/emit-assembly.d.ts.map +1 -1
  35. package/dist/table/emit.d.ts +4 -3
  36. package/dist/table/emit.d.ts.map +1 -1
  37. package/dist/table/encode.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts +0 -35
  39. package/dist/table/exec.d.ts.map +1 -1
  40. package/dist/table/factor-choice.d.ts +27 -0
  41. package/dist/table/factor-choice.d.ts.map +1 -1
  42. package/dist/table/gap-gate.d.ts +11 -0
  43. package/dist/table/gap-gate.d.ts.map +1 -0
  44. package/dist/table/index.js +926 -201
  45. package/dist/table/index.js.map +3 -3
  46. package/dist/table/lex-program.d.ts.map +1 -1
  47. package/dist/table/ops.d.ts +35 -16
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +6 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/types.d.ts +16 -4
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +2 -4
  55. package/src/analysis/duplication.ts +1 -1
  56. package/src/combinators/attempt.ts +1 -1
  57. package/src/combinators/dispatch.ts +30 -6
  58. package/src/combinators/first-set.ts +55 -12
  59. package/src/combinators/node.ts +1 -1
  60. package/src/combinators/regex.ts +9 -4
  61. package/src/combinators/repeat.ts +6 -6
  62. package/src/combinators/sequence.ts +1 -1
  63. package/src/combinators/trivia-boundary.ts +18 -0
  64. package/src/combinators/trivia-skip.ts +1 -1
  65. package/src/compiler/ir-serialize.ts +1 -1
  66. package/src/compiler/token-alphabet.ts +3 -1
  67. package/src/coverage.ts +1 -1
  68. package/src/index.ts +1 -1
  69. package/src/plugin/evaluator.ts +8 -3
  70. package/src/regex/first-set.ts +37 -3
  71. package/src/table/assemble.ts +176 -30
  72. package/src/table/child-slots.ts +8 -3
  73. package/src/table/compile-rule-map.ts +7 -4
  74. package/src/table/compile.ts +1 -1
  75. package/src/table/emit-assembly.ts +184 -33
  76. package/src/table/emit.ts +6 -4
  77. package/src/table/encode.ts +469 -74
  78. package/src/table/exec.ts +38 -82
  79. package/src/table/factor-choice.ts +62 -0
  80. package/src/table/fail-clean.ts +2 -2
  81. package/src/table/gap-gate.ts +55 -0
  82. package/src/table/hold-gap.ts +2 -2
  83. package/src/table/lex-program.ts +15 -6
  84. package/src/table/ops.ts +38 -16
  85. package/src/table/program.ts +7 -2
  86. package/src/table/select-hot.ts +7 -2
  87. package/src/types.ts +12 -5
  88. package/src/version.ts +1 -1
  89. package/dist/table/encode-baseline.d.ts +0 -16
  90. package/dist/table/encode-baseline.d.ts.map +0 -1
  91. package/dist/table/exec-baseline.d.ts +0 -9
  92. package/dist/table/exec-baseline.d.ts.map +0 -1
  93. package/src/table/encode-baseline.ts +0 -346
  94. package/src/table/exec-baseline.ts +0 -557
@@ -4,7 +4,7 @@ import { failAt } from './probe.ts'
4
4
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
5
  import { recordLineRangeFromContext } from '../line-index.ts'
6
6
  import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
7
- import { firstSetFromRegex } from '../regex/first-set.ts'
7
+ import { consumingFirstSetFromRegex, firstSetFromRegex, regexCanMatchEmpty } from '../regex/first-set.ts'
8
8
  import { directTerminalFailureExpected } from './expected.ts'
9
9
  import type { ScalarParser } from './scalar.ts'
10
10
  import { PlainLeaf, PlainSpan } from '../cst/plain.js'
@@ -156,13 +156,18 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
156
156
  // pairs, so gating on the ASCII set would false-exclude them. Fall back to `any()`
157
157
  // (always-try) — sound, and only forfeits gating for the rare Unicode-mode `/i`
158
158
  // recognizer, never the ASCII-only `/i` at-keywords the fix targets.
159
- const firstSet = !resolvedFlags.includes('i')
160
- ? raw.firstSet
159
+ const forFlags = (fs: FirstSet): FirstSet => !resolvedFlags.includes('i')
160
+ ? fs
161
161
  : (resolvedFlags.includes('u') || resolvedFlags.includes('v'))
162
162
  ? any()
163
- : asciiCaseFold(raw.firstSet)
163
+ : asciiCaseFold(fs)
164
+ const firstSet = forFlags(raw.firstSet)
164
165
  const canMatchNewline = raw.canMatchNewline
165
166
  const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
167
+ if (firstSet.kind === 'any' && regexCanMatchEmpty(source)) {
168
+ const consuming = forFlags(consumingFirstSetFromRegex(source, resolvedFlags))
169
+ if (consuming.kind !== 'any') meta.consumingFirstSet = consuming
170
+ }
166
171
  const def = { tag: 'regex', source, flags: resolvedFlags } as const
167
172
  const expected = directTerminalFailureExpected(def)
168
173
  const parseScalar: ScalarParser = (input, pos, ctx) => {
@@ -154,11 +154,11 @@ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Co
154
154
  canMatchNewline: combinator._meta.canMatchNewline,
155
155
  isTrivia: false,
156
156
  }
157
- const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean } =
158
- { tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }) }
159
- let expected: string[] | undefined
160
157
  // A non-nullable body can be first-set-gated per loop iteration (see repItem).
161
158
  const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
159
+ const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean; guarded?: true } =
160
+ { tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
161
+ let expected: string[] | undefined
162
162
  const scalarChild = scalarOf(combinator)
163
163
  const parseScalar = (input: string, pos: number, ctx: ParseContext): number =>
164
164
  repeatScalarTail(combinator, scalarChild, input, pos, ctx, guardable, def.valueUnused ? undefined : [], max)
@@ -229,11 +229,11 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
229
229
  canMatchNewline: combinator._meta.canMatchNewline,
230
230
  isTrivia: false,
231
231
  }
232
- const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean } =
233
- { tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }) }
234
- let expected: string[] | undefined
235
232
  // A non-nullable body can be first-set-gated per loop iteration (see repItem).
236
233
  const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
234
+ const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean; guarded?: true } =
235
+ { tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
236
+ let expected: string[] | undefined
237
237
  const scalarChild = scalarOf(combinator)
238
238
  const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
239
239
  let cur = scalarChild(input, pos, ctx)
@@ -108,7 +108,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
108
108
  if (tolerant) {
109
109
  followSentinels ??= parsers.map((_, i) => firstSetSentinel(
110
110
  parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
111
- (set, parser) => union(set, firstSetOf(parser)), { kind: 'empty' },
111
+ (set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false)), { kind: 'empty' },
112
112
  ),
113
113
  ))
114
114
  }
@@ -46,3 +46,21 @@ export function hasOwnTriviaBoundary(p: Combinator<unknown>, depth: number = MAX
46
46
  default: return false
47
47
  }
48
48
  }
49
+
50
+ /**
51
+ * The trivia `target` runs under when it is reached through the reference `ref`
52
+ * from a scope whose live trivia is `trivia` (`null` or `undefined`: none).
53
+ *
54
+ * A reference made from a region that cleared trivia re-installs the rule's own
55
+ * ambient trivia, where the rule has a boundary for it to repair (`ref.ts`,
56
+ * `Encoder.scopedRef`). Anything derived statically through a reference — a
57
+ * first set, a gate, a gap — must take the same scope, or it describes the
58
+ * caller's scope where the body runs under its own.
59
+ */
60
+ export function triviaThroughRef(
61
+ ref: Combinator<unknown>, target: Combinator<unknown>, trivia: Combinator<unknown> | null | undefined,
62
+ ): Combinator<unknown> | null | undefined {
63
+ if (trivia !== undefined && trivia !== null) return trivia
64
+ const amb = ref._meta.grammarTrivia ?? target._meta.grammarTrivia
65
+ return amb !== undefined && hasOwnTriviaBoundary(target) ? amb : trivia
66
+ }
@@ -413,7 +413,7 @@ export function scanTriviaCompact(input: string, cur: number, ctx: ParseContext)
413
413
  return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
414
414
  }
415
415
 
416
- /** Legacy object-shaped contract used by previously emitted assembly factories. */
416
+ /** The object-shaped form of `scanTriviaCompact`. No engine calls it; the trivia-skip tests compare the two. */
417
417
  export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
418
418
  const scan = scanTriviaCompact(input, cur, ctx)
419
419
  return typeof scan === 'number' ? { end: scan, commit: NOOP_COMMIT } : scan
@@ -461,7 +461,7 @@ class Serializer {
461
461
  arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`)
462
462
  }
463
463
  }
464
- if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`)
464
+ if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)}${def.otherwiseCommit === false ? ', { commit: false }' : ''})`)
465
465
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? '' : `, ${arms.join(', ')}`})`
466
466
  }
467
467
  case 'many': return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
@@ -140,6 +140,8 @@ export type ExecutableLexNode =
140
140
  readonly parsers: readonly Combinator<unknown>[]
141
141
  /** Inside a no-trivia scope of the token, where the encoder's trivia is none. */
142
142
  readonly cleared: boolean
143
+ /** The authored choice's own `disjoint`, which decides how the encoder classifies its arms. */
144
+ readonly disjoint: boolean
143
145
  readonly armExpected: readonly (readonly string[])[]
144
146
  readonly expected: readonly string[]
145
147
  }
@@ -509,7 +511,7 @@ function directFixedLexNode(
509
511
  if (arms.some(arm => arm === undefined)) return undefined
510
512
  return {
511
513
  kind: 'choice', arms: arms as ExecutableLexNode[],
512
- parsers: def.parsers, cleared,
514
+ parsers: def.parsers, cleared, disjoint: def.disjoint,
513
515
  armExpected: def.parsers.map(deriveExpected),
514
516
  expected: def.parsers.flatMap(deriveExpected),
515
517
  }
package/src/coverage.ts CHANGED
@@ -309,7 +309,7 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
309
309
  arms.push(when(dispatchMatcher(entry), wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined))
310
310
  }
311
311
  }
312
- if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])))
312
+ if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++]), def.otherwiseCommit === false ? { commit: false } : undefined))
313
313
  const unsafeDispatch = dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
314
314
  const base = unsafeDispatch(build(def.selector) as Combinator<string>, ...arms)
315
315
  return {
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export type { KeywordsOptions, WordOptions } from './combinators/keywords.ts'
17
17
  export { sequence } from './combinators/sequence.ts'
18
18
  export { choice } from './combinators/choice.ts'
19
19
  export { dispatch, endsWith, makeWhen, matches, otherwise, routed, startsWith, when } from './combinators/dispatch.ts'
20
- export type { DispatchArm, DispatchOtherwise, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
20
+ export type { DispatchArm, DispatchOtherwise, DispatchOtherwiseOptions, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
21
21
  export { attempt } from './combinators/attempt.ts'
22
22
  export type { AttemptOptions } from './combinators/attempt.ts'
23
23
  export { many, oneOrMore, optional, sepBy, oneOrMoreSep, keepSeparator } from './combinators/repeat.ts'
@@ -431,11 +431,16 @@ function dispatchArmValue(node: Expression, scope: XScope, code?: string, mfs?:
431
431
  }
432
432
 
433
433
  if (callee.name === 'otherwise') {
434
- const [parserArg] = node.arguments
435
- if (!parserArg || parserArg.type === 'SpreadElement') return null
434
+ const [parserArg, optsArg] = node.arguments
435
+ if (!parserArg || parserArg.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
436
436
  const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
437
437
  if (!isCombinator(parserValue)) return null
438
- return parseman.otherwise(parserValue)
438
+ if (optsArg === undefined) return parseman.otherwise(parserValue)
439
+ const opts = anyValue(optsArg as Expression, scope, code, mfs)
440
+ if (typeof opts !== 'object' || opts === null) return null
441
+ const commit = (opts as { commit?: unknown }).commit
442
+ if (commit !== undefined && typeof commit !== 'boolean') return null
443
+ return parseman.otherwise(parserValue, commit === undefined ? {} : { commit })
439
444
  }
440
445
 
441
446
  if (callee.name === 'when') {
@@ -32,7 +32,7 @@ import { shorthandRanges, parseClassRanges, readUnicodeEscape } from './classes.
32
32
  type Node =
33
33
  | { t: 'char'; set: FirstSet; nl: boolean } // matches exactly one char from `set`
34
34
  | { t: 'any' } // unknown 1-char-ish construct: could start with anything
35
- | { t: 'empty' } // zero-width: anchors, lookaround, word boundary
35
+ | { t: 'empty'; look?: Node } // zero-width: anchors, lookaround, word boundary (`look`: a positive lookahead's body)
36
36
  | { t: 'seq'; parts: Node[] }
37
37
  | { t: 'alt'; arms: Node[] }
38
38
  | { t: 'rep'; node: Node; min: number } // quantified; min 0 ⇒ nullable
@@ -142,10 +142,11 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
142
142
  i++ // consume '('
143
143
  // Group prefixes: (?:…) (?=…) (?!…) (?<=…) (?<!…) (?<name>…)
144
144
  let lookaround = false
145
+ let lookahead = false
145
146
  if (src[i] === '?') {
146
147
  const c1 = src[i + 1]
147
148
  if (c1 === ':') i += 2
148
- else if (c1 === '=' || c1 === '!') { i += 2; lookaround = true }
149
+ else if (c1 === '=' || c1 === '!') { lookahead = c1 === '='; i += 2; lookaround = true }
149
150
  else if (c1 === '<') {
150
151
  const c2 = src[i + 2]
151
152
  if (c2 === '=' || c2 === '!') { i += 3; lookaround = true } // lookbehind
@@ -159,7 +160,10 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
159
160
  const inner = parseAlt()
160
161
  if (src[i] !== ')') throw BAIL
161
162
  i++ // consume ')'
162
- return lookaround ? EMPTY : inner // lookaround is zero-width
163
+ // Lookaround is zero-width. A positive LOOKAHEAD keeps its body for
164
+ // `regexFirstTwo`; every other walker reads it as `EMPTY`.
165
+ if (lookaround) return lookahead ? { t: 'empty', look: inner } : EMPTY
166
+ return inner
163
167
  }
164
168
 
165
169
  const parseClass = (): Node => {
@@ -348,6 +352,17 @@ export function regexCanMatchEmpty(source: string): boolean {
348
352
  try { return nullable(parseRegex(source)) } catch { return true }
349
353
  }
350
354
 
355
+ /**
356
+ * The first chars of a NON-EMPTY match of `source`, nullable or not: the set
357
+ * `firstSetFromRegex` reports for a pattern that must consume, without its
358
+ * widening of a nullable one to `any()`. Only a caller that accounts for the
359
+ * empty match itself may use it (`sequenceSet`). `any()` when unparseable.
360
+ * `flags` reads atoms as `firstSetFromRegex` does.
361
+ */
362
+ export function consumingFirstSetFromRegex(source: string, flags = ''): FirstSet {
363
+ try { return firstSet(parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v'))) } catch { return any() }
364
+ }
365
+
351
366
  /**
352
367
  * `flags` decides how atoms read: under `u`/`v` each is one code point, else one
353
368
  * code unit, so a non-`u` set holds a high surrogate where an astral character
@@ -369,3 +384,22 @@ export function firstSetFromRegex(source: string, flags = ''): { firstSet: First
369
384
  return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
370
385
  }
371
386
 
387
+ /**
388
+ * The first TWO characters every match of `source` begins with, where the
389
+ * pattern says them outright: a one-character class and then another, or a
390
+ * positive lookahead over one (`:(?=[ \t])`). `undefined` otherwise. Every
391
+ * match that starts with a character of `first` has a second character, and it
392
+ * is in `second`. `flags` reads atoms as `firstSetFromRegex` does.
393
+ */
394
+ export function regexFirstTwo(source: string, flags = ''): { first: FirstSet; second: FirstSet } | undefined {
395
+ let ast: Node
396
+ try { ast = parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v')) } catch { return undefined }
397
+ const parts = ast.t === 'seq' ? ast.parts : [ast]
398
+ const a = parts[0]
399
+ const b = parts[1]
400
+ if (a === undefined || a.t !== 'char' || a.set.kind !== 'ranges' || b === undefined) return undefined
401
+ const second = b.t === 'char'
402
+ ? b.set
403
+ : b.t === 'empty' && b.look !== undefined && !nullable(b.look) ? firstSet(b.look) : undefined
404
+ return second !== undefined && second.kind === 'ranges' ? { first: a.set, second } : undefined
405
+ }