parseman 0.59.3 → 0.60.0

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 (84) hide show
  1. package/dist/analysis/diagnostics.js +874 -188
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +874 -188
  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 +894 -197
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +901 -199
  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 +14 -0
  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.map +1 -1
  31. package/dist/table/emit-assembly.d.ts +8 -2
  32. package/dist/table/emit-assembly.d.ts.map +1 -1
  33. package/dist/table/emit.d.ts +4 -3
  34. package/dist/table/emit.d.ts.map +1 -1
  35. package/dist/table/encode.d.ts.map +1 -1
  36. package/dist/table/exec.d.ts.map +1 -1
  37. package/dist/table/factor-choice.d.ts +27 -0
  38. package/dist/table/factor-choice.d.ts.map +1 -1
  39. package/dist/table/gap-gate.d.ts +11 -0
  40. package/dist/table/gap-gate.d.ts.map +1 -0
  41. package/dist/table/index.js +924 -199
  42. package/dist/table/index.js.map +3 -3
  43. package/dist/table/lex-program.d.ts.map +1 -1
  44. package/dist/table/ops.d.ts +34 -6
  45. package/dist/table/ops.d.ts.map +1 -1
  46. package/dist/table/program.d.ts +6 -1
  47. package/dist/table/program.d.ts.map +1 -1
  48. package/dist/types.d.ts +16 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/version.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/analysis/duplication.ts +1 -1
  53. package/src/combinators/attempt.ts +1 -1
  54. package/src/combinators/dispatch.ts +30 -6
  55. package/src/combinators/first-set.ts +55 -12
  56. package/src/combinators/node.ts +1 -1
  57. package/src/combinators/regex.ts +9 -4
  58. package/src/combinators/repeat.ts +6 -6
  59. package/src/combinators/sequence.ts +1 -1
  60. package/src/combinators/trivia-boundary.ts +18 -0
  61. package/src/combinators/trivia-skip.ts +1 -1
  62. package/src/compiler/ir-serialize.ts +1 -1
  63. package/src/compiler/token-alphabet.ts +3 -1
  64. package/src/coverage.ts +1 -1
  65. package/src/index.ts +1 -1
  66. package/src/plugin/evaluator.ts +8 -3
  67. package/src/regex/first-set.ts +37 -3
  68. package/src/table/assemble.ts +171 -25
  69. package/src/table/child-slots.ts +8 -3
  70. package/src/table/compile-rule-map.ts +6 -3
  71. package/src/table/emit-assembly.ts +182 -31
  72. package/src/table/emit.ts +6 -4
  73. package/src/table/encode.ts +468 -73
  74. package/src/table/exec.ts +38 -13
  75. package/src/table/factor-choice.ts +62 -0
  76. package/src/table/fail-clean.ts +2 -2
  77. package/src/table/gap-gate.ts +55 -0
  78. package/src/table/hold-gap.ts +2 -2
  79. package/src/table/lex-program.ts +15 -6
  80. package/src/table/ops.ts +37 -6
  81. package/src/table/program.ts +7 -2
  82. package/src/table/select-hot.ts +7 -2
  83. package/src/types.ts +12 -5
  84. package/src/version.ts +1 -1
@@ -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
+ }
@@ -73,10 +73,11 @@ import { skipDeferredTrivia, triviaLeadTable } from './trivia-gap.ts'
73
73
  import { asciiFoldKey } from '../combinators/dispatch.ts'
74
74
  import { projectChild, unwrapChild } from '../combinators/node.ts'
75
75
  import { cstOutputHost } from '../compiler/build-arity.ts'
76
+ import { PARSEMAN_VERSION } from '../version.ts'
76
77
  import { consumeTrivia } from '../combinators/trivia-skip.ts'
77
78
  import {
78
- advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
79
- scanTrivia, scanTriviaCompact, skipTriviaScanned, type FastTriviaScanner,
79
+ advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTriviaAt,
80
+ skipTriviaScanned, type FastTriviaScanner,
80
81
  } from '../combinators/trivia-skip.ts'
81
82
  import {
82
83
  cstCaptureActive, cstLeavesLen, cstTlLen,
@@ -90,8 +91,8 @@ import {
90
91
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
91
92
  OP_LIVE,
92
93
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
93
- OP_COV,
94
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
94
+ OP_COV, OP_GAPGATE, OP_STRICT,
95
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_RGATE2,
95
96
  OP_LEX_BODY, OP_LEX_PROGRAM,
96
97
  } from './ops.ts'
97
98
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -110,6 +111,7 @@ import {
110
111
  } from './emit-assembly.ts'
111
112
  import { collapseEmpty, lead, rawEntry, spanLines } from './run-support.ts'
112
113
  import { holdsGap, rowNullable } from './hold-gap.ts'
114
+ import { gapRows, newGapSlot } from './gap-gate.ts'
113
115
  import { failCleanAnalysis } from './fail-clean.ts'
114
116
  /**
115
117
  * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
@@ -412,6 +414,28 @@ export type Assembly = {
412
414
  * ONE walk. Each site is lowered at most once and memoised by its code offset,
413
415
  * so a subtree shared by two parents is one piece with two references to it.
414
416
  */
417
+ /**
418
+ * A printed factory takes its helpers BY POSITION (`EMITTED_PARAMS`), so one printed
419
+ * against a different helper list would bind the wrong function to a name and run
420
+ * wrong rather than fail. A factory printed by 0.59.x guards only against FEWER
421
+ * helpers, and one printed before 0.59.0 has no guard at all. So the runtime checks
422
+ * too, before calling it: the assembly's printed `arity` must be exactly the list it
423
+ * passes. It reads that printed number, never `factory.length`, which a minifier that
424
+ * drops unused trailing parameters lowers on a valid artifact. An assembly printed
425
+ * before 0.60.0 carries no `arity` and is refused. A factory this version prints also
426
+ * checks the count it is called with (`FACTORY_ARITY_GUARD`, `emit.ts`).
427
+ */
428
+ export function assertFactoryArity(pre: { readonly arity?: number }): void {
429
+ if (pre.arity === EMITTED_PARAMS.length) return
430
+ const printed = pre.arity === undefined
431
+ ? 'carries no helper count, so it was printed before parseman 0.60.0'
432
+ : `takes ${pre.arity} helpers, but parseman ${PARSEMAN_VERSION} passes ${EMITTED_PARAMS.length}: it was printed by an older or newer parseman`
433
+ throw new Error(
434
+ `parseman: this parser's printed assembly ${printed}. Reprint the parser with `
435
+ + `parseman ${PARSEMAN_VERSION}, or run it on the parseman version that printed it.`,
436
+ )
437
+ }
438
+
415
439
  export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Assembly {
416
440
  const { code, k, fns, cc, fx, disp, dsp, trivia, triviaLabelled } = t
417
441
  // The leaf swap is only legal when line tracking is off, and that is a property
@@ -502,6 +526,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
502
526
  }
503
527
 
504
528
  const labelExtraIps = assemblyExtraIps(prog)
529
+ // The regex rows an optional repeat hands a scanned gap to (`gap-gate.ts`),
530
+ // and the one slot it hands it through, shared with the emitted pieces.
531
+ const GAP_ROWS = gapRows(code, [...Object.values(prog.rules), ...labelExtraIps])
532
+ const GAP = newGapSlot()
505
533
  const closureLabels = computeSiteLabels(
506
534
  code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
507
535
  )
@@ -772,6 +800,32 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
772
800
  }
773
801
  }
774
802
 
803
+ /**
804
+ * A regex row a repeat may hand a scanned gap to (`gap-gate.ts`): the end comes
805
+ * from the slot when the slot is this row's, at this position, in this input,
806
+ * and from the regex otherwise. Everything after the end is the row's own.
807
+ */
808
+ function gapRegexPiece(row: number, re: RegExp, xf: string[], track: boolean): Piece {
809
+ return (input, pos, ctx) => {
810
+ let e: number
811
+ if (GAP.row === row && GAP.at === pos && GAP.input === input) e = GAP.end
812
+ else {
813
+ re.lastIndex = pos
814
+ e = re.test(input) ? re.lastIndex : -1
815
+ }
816
+ if (e >= 0) {
817
+ const v = input.slice(pos, e)
818
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
819
+ if (track) trackLinesInto(ctx, input, e)
820
+ EC.e = e
821
+ return v
822
+ }
823
+ ctx._fe = pos; ctx._fx = xf
824
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
825
+ return FAIL
826
+ }
827
+ }
828
+
775
829
  /**
776
830
  * A collapse over one node is that node — `run-support.ts` `collapseEmpty` has
777
831
  * the contract, and the one case the node's value cannot stand for itself.
@@ -1353,7 +1407,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1353
1407
  }
1354
1408
  }
1355
1409
 
1356
- function dispatchBranch(child: Piece, usesRouted: boolean): DispatchBranch {
1410
+ function dispatchBranch(child: Piece, usesRouted: boolean, commits = true): DispatchBranch {
1357
1411
  if (!usesRouted) {
1358
1412
  // The arm runs after the selector's match, so it marks where it starts.
1359
1413
  return (input, _pos, selEnd, key, ctx) => {
@@ -1368,7 +1422,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1368
1422
  const v = child(input, selEnd, ctx)
1369
1423
  if (v === FAIL) {
1370
1424
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1371
- ctx._fc = true
1425
+ if (commits) ctx._fc = true
1372
1426
  return FAIL
1373
1427
  }
1374
1428
  return [key, v]
@@ -1388,7 +1442,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1388
1442
  }
1389
1443
  if (v === FAIL) {
1390
1444
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1391
- ctx._fc = true
1445
+ if (commits) ctx._fc = true
1392
1446
  return FAIL
1393
1447
  }
1394
1448
  return [key, v]
@@ -1662,6 +1716,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1662
1716
  case OP_RX: {
1663
1717
  const re = k[code[ip + 1]!] as RegExp
1664
1718
  const xf = fx[code[ip + 2]!] as string[]
1719
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, false)
1665
1720
  const shared = rawScalarSpecs.has(code[ip + 1]!) ? scalarFor(ip) : undefined
1666
1721
  if (shared !== undefined) {
1667
1722
  return (input, pos, ctx) => {
@@ -1700,6 +1755,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1700
1755
  case OP_RX_TRACK: {
1701
1756
  const re = k[code[ip + 1]!] as RegExp
1702
1757
  const xf = fx[code[ip + 2]!] as string[]
1758
+ if (GAP_ROWS.has(ip)) return gapRegexPiece(ip, re, xf, true)
1703
1759
  return (input, pos, ctx) => {
1704
1760
  re.lastIndex = pos
1705
1761
  if (re.test(input)) {
@@ -1877,6 +1933,14 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1877
1933
  }
1878
1934
  }
1879
1935
 
1936
+ // Transparent: only the repeat holding it reads its operands.
1937
+ case OP_GAPGATE:
1938
+ return link(code[ip + 1]!)
1939
+
1940
+ // One encoding per assembly: strict, or the construct as written.
1941
+ case OP_STRICT:
1942
+ return link(code[cfg.tolerant || cfg.probe ? ip + 2 : ip + 1]!)
1943
+
1880
1944
  case OP_RULE:
1881
1945
  // Pure indirection in the tape; in the assembled graph the parent simply
1882
1946
  // holds the target's piece. The trampoline row disappears entirely — this
@@ -2949,7 +3013,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2949
3013
  // `arms[]` merely to rediscover the same two or three children at
2950
3014
  // every dispatch. The classifier remains table data; only its result
2951
3015
  // is turned into a direct captured call.
2952
- if (n === 2) {
3016
+ if (n === 2 && !cfg.probe) {
2953
3017
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
2954
3018
  return (input, pos, ctx) => {
2955
3019
  const c = lead(input, pos)
@@ -2974,7 +3038,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2974
3038
  return FAIL
2975
3039
  }
2976
3040
  }
2977
- if (n === 3) {
3041
+ if (n === 3 && !cfg.probe) {
2978
3042
  const a0 = link(code[base]!), a1 = link(code[base + 1]!), a2 = link(code[base + 2]!)
2979
3043
  return (input, pos, ctx) => {
2980
3044
  const c = lead(input, pos)
@@ -3007,6 +3071,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3007
3071
  )
3008
3072
  }
3009
3073
  const selected = bindExclusive(0)
3074
+ // Under a completions probe the interpreter runs every arm on a miss
3075
+ // (`choice.ts`), so each opener reaches the probe.
3076
+ const probeArms = cfg.probe ? Array.from({ length: n }, (_, i) => link(code[base + i]!)) : undefined
3010
3077
  // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
3011
3078
  // ungated arm is one whose class is −1, and `resolveDispatch` clears
3012
3079
  // `exclusive` for exactly those arms (program.ts) — so `table.open` is
@@ -3038,6 +3105,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3038
3105
  // THE UNION IS REPORTED ONLY ON A DISPATCH MISS, where the interpreter's
3039
3106
  // `parsers.flatMap` over non-nullable, char-excluded arms yields exactly
3040
3107
  // each arm's own opener — this set, without running the arms.
3108
+ if (arm < 0 && probeArms !== undefined) for (const a of probeArms) a(input, pos, ctx)
3041
3109
  ctx._fe = pos; ctx._fx = choiceFx
3042
3110
  return FAIL
3043
3111
  }
@@ -3045,8 +3113,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3045
3113
 
3046
3114
  // THE PER-ARM GATE. Arms in source order, each skipped when its own class
3047
3115
  // excludes the char at `pos`. `null` means nullable or unmappable, and
3048
- // those arms are always entered.
3049
- const armCls = table.armCls
3116
+ // those arms are always entered. A probe assembly enters every arm, as
3117
+ // the interpreter's `firstMatch` does, so each opener reaches `failAt`.
3118
+ const armCls = cfg.probe ? table.armCls.map(() => null) : table.armCls
3050
3119
  if (n === 2 || n === 3) {
3051
3120
  const a0 = link(code[base]!), a1 = link(code[base + 1]!)
3052
3121
  const a2 = n === 3 ? link(code[base + 2]!) : undefined
@@ -3354,6 +3423,43 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3354
3423
  }
3355
3424
  }
3356
3425
 
3426
+ case OP_RGATE2: {
3427
+ const cls = cc[code[ip + 1]!]!
3428
+ const xf = fx[code[ip + 3]!] as string[]
3429
+ const first = cc[code[ip + 4]!]!
3430
+ const second = cc[code[ip + 5]!]!
3431
+ const child = link(code[ip + 2]!)
3432
+ if (cfg.tolerant || cfg.probe) return child
3433
+ // ASCII-only classes are read as code units against their tables.
3434
+ if (cls.hi.length === 0 && first.hi.length === 0 && second.hi.length === 0) {
3435
+ const a0 = cls.ascii, a1 = first.ascii, a2 = second.ascii
3436
+ return (input, pos, ctx) => {
3437
+ const item = ctx._routed
3438
+ if (item !== undefined && item.span.start === pos) {
3439
+ const e = item.span.end
3440
+ const c = input.charCodeAt(e)
3441
+ if (a0[c] !== 1 || (a1[c] === 1 && a2[input.charCodeAt(e + 1)] !== 1)) {
3442
+ ctx._fe = e; ctx._fx = xf
3443
+ return FAIL
3444
+ }
3445
+ }
3446
+ return child(input, pos, ctx)
3447
+ }
3448
+ }
3449
+ return (input, pos, ctx) => {
3450
+ const item = ctx._routed
3451
+ if (item !== undefined && item.span.start === pos) {
3452
+ const e = item.span.end
3453
+ const c = lead(input, e)
3454
+ if (!classHas(cls, c) || (classHas(first, c) && !classHas(second, lead(input, e + 1)))) {
3455
+ ctx._fe = e; ctx._fx = xf
3456
+ return FAIL
3457
+ }
3458
+ }
3459
+ return child(input, pos, ctx)
3460
+ }
3461
+ }
3462
+
3357
3463
  case OP_ARMGATE: {
3358
3464
  // Predicate, arm and expected set all bound HERE — the body reads no
3359
3465
  // opcode, decodes no operand and tests no setting.
@@ -3450,6 +3556,19 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3450
3556
  // `many()` — the min-0, separator-less repeat — is the only shape that runs
3451
3557
  // its FIRST item through `repItem` and so skips leading trivia. The shape
3452
3558
  // identifies itself, and it is table data, so it is decided here.
3559
+ // A separated list's ip+8 (flag bit 8) is its separator's first set where the encoder
3560
+ // found the test cheaper than entering it (`Encoder.sepGate`). Where the
3561
+ // separator cannot match empty and reads no pre-scan cursor, a lead
3562
+ // outside it stops the list at the separator, exactly as the strict
3563
+ // optional item stops at its own class above.
3564
+ const sepClassIndex = sepIp >= 0 && !sepLeads && (flags & 8) !== 0 ? code[ip + 8]! : -1
3565
+ const sepClass = !REC && sepClassIndex >= 0 && !rowNullable(code, k, sepIp, NULLABLE) ? cc[sepClassIndex]! : undefined
3566
+ // THE GAP GATE (`gap-gate.ts`): read the item's leading gap once, here.
3567
+ const gapIp = sepIp < 0 && min === 0 && !REC && code[code[ip + 1]!] === OP_GAPGATE ? code[ip + 1]! : -1
3568
+ const gapRow = gapIp >= 0 ? code[gapIp + 2]! : -1
3569
+ const gapRe = gapIp >= 0 ? k[code[gapRow + 1]!] as RegExp : undefined
3570
+ const gapFollow = gapIp >= 0 ? cc[code[gapIp + 3]!]! : undefined
3571
+ const gapOther = gapIp >= 0 && code[gapIp + 4]! >= 0 ? cc[code[gapIp + 4]!]! : undefined
3453
3572
  const skipBeforeFirst = sepIp < 0 && min === 0
3454
3573
  const itemClean = sepIp < 0 && CLEAN(code[ip + 1]!) && !rowNullable(code, k, code[ip + 1]!, NULLABLE)
3455
3574
 
@@ -3486,6 +3605,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3486
3605
  */
3487
3606
  if (REC) {
3488
3607
  const itemFx = fx[code[ip + 6]!] as string[]
3608
+ // A resync error carries the item's set as derived (bit 16: empty).
3609
+ const recFx = (flags & 16) !== 0 ? EMPTY_FX : itemFx
3489
3610
  const sepSent = sentinelFor(code[ip + 7]!)
3490
3611
  return (input, pos, ctx) => {
3491
3612
  const out: unknown[] | undefined = collect ? [] : undefined
@@ -3546,7 +3667,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3546
3667
  const rr = recoverScan(
3547
3668
  input, itemStart, ctx,
3548
3669
  sep === undefined ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
3549
- itemFx,
3670
+ recFx,
3550
3671
  )
3551
3672
  if (out !== undefined) out.push(rr.error)
3552
3673
  captureError(ctx, rr.error)
@@ -3586,6 +3707,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3586
3707
  // Keep the ordinary child path while one is active so this structural
3587
3708
  // fast stop cannot erase an item completion at the cursor.
3588
3709
  const gateItems = itemMayStart !== undefined && ctx._probe === undefined
3710
+ const gateSep = sepClass !== undefined && ctx._probe === undefined
3589
3711
  let cur = pos
3590
3712
  let count = 0
3591
3713
  for (;;) {
@@ -3617,6 +3739,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3617
3739
  const leavesBefore = cstLeavesLen(ctx)
3618
3740
  let sp = cur
3619
3741
  if (hasTrivia) sp = skipTrivia(input, sp, ctx)
3742
+ if (count >= min && gateSep && !classHas(sepClass!, lead(input, sp))) {
3743
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3744
+ break
3745
+ }
3620
3746
  ctx._fc = false
3621
3747
  if (sepLeads) LEAD.at = cur
3622
3748
  const sv = sep(input, sp, ctx)
@@ -3636,13 +3762,33 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3636
3762
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3637
3763
  break
3638
3764
  }
3639
- if (count >= min && gateItems && !itemMayStart!(input, itemStart)) {
3765
+ // Without a separator or trivia in front, the loop head already
3766
+ // tested this same position.
3767
+ if (count >= min && gateItems && (sep !== undefined || hasTrivia) && !itemMayStart!(input, itemStart)) {
3640
3768
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3641
3769
  if (trailingAllowed && sepEnd >= 0) cur = sepEnd
3642
3770
  break
3643
3771
  }
3772
+ let gapped = false
3773
+ if (gapRe !== undefined && ctx._probe === undefined) {
3774
+ gapRe.lastIndex = itemStart
3775
+ const q = gapRe.test(input) ? gapRe.lastIndex : -1
3776
+ // No arm with the gap can match where the gap fails, or where the
3777
+ // character after it opens nothing; an arm without it needs its
3778
+ // own lead at the item's start.
3779
+ if ((q < 0 || !classHas(gapFollow!, lead(input, q)))
3780
+ && (gapOther === undefined || !classHas(gapOther, lead(input, itemStart)))) {
3781
+ if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3782
+ break
3783
+ }
3784
+ if (q >= 0) {
3785
+ GAP.row = gapRow; GAP.at = itemStart; GAP.end = q; GAP.input = input
3786
+ gapped = true
3787
+ }
3788
+ }
3644
3789
  ctx._fc = false
3645
3790
  const v = child(input, itemStart, ctx)
3791
+ if (gapped) GAP.at = -1
3646
3792
  if (v === FAIL) {
3647
3793
  if (markIt) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
3648
3794
  if (committed(ctx)) return FAIL
@@ -3675,7 +3821,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3675
3821
  const selector = link(code[ip + 1]!)
3676
3822
  const otherIp = code[ip + 3]!
3677
3823
  const other = otherIp >= 0 ? link(otherIp) : undefined
3678
- const otherRouted = code[ip + 4]! === 1
3824
+ const otherRouted = (code[ip + 4]! & 1) === 1
3825
+ const otherCommits = (code[ip + 4]! & 2) === 0
3679
3826
  const n = code[ip + 5]!
3680
3827
  const armBase = ip + 6
3681
3828
  const byKey = spec.byKey
@@ -3722,7 +3869,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3722
3869
  )
3723
3870
  }
3724
3871
  const runArm = n === 0 ? undefined : bindArms(0)
3725
- const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted)
3872
+ const runOther = other === undefined ? undefined : dispatchBranch(other, otherRouted, otherCommits)
3726
3873
 
3727
3874
  return (input, pos, ctx) => {
3728
3875
  const need = markCst(ctx)
@@ -4099,22 +4246,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4099
4246
  ? prog.asm?.find(a => a.key === cfgKey(cfg))
4100
4247
  : undefined
4101
4248
  if (pre !== undefined) {
4249
+ assertFactoryArity(pre)
4102
4250
  const pools = rebuildPools(t.cc, t.fx, t.disp, pre.plan)
4103
4251
  emitted = pre.factory(
4104
- EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
4105
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4252
+ EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia, triviaScan,
4106
4253
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4107
4254
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4108
4255
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4109
4256
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4110
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4257
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4111
4258
  asciiFoldKey, ROUTED_FX,
4112
4259
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4113
4260
  matchesAt, recoverScan, orSentinel, captureError,
4114
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4261
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4115
4262
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4116
4263
  collapseEmpty,
4117
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4264
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4118
4265
  )
4119
4266
  emitReached = new Set(pre.reached)
4120
4267
  if (emitted.ext !== undefined) HYBRID = emitted
@@ -4181,13 +4328,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4181
4328
  throw new Unemittable(`the Function constructor (${String(e)})`)
4182
4329
  }
4183
4330
  emitted = factory(
4184
- EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
4185
- trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
4331
+ EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia, triviaScan,
4186
4332
  scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
4187
4333
  cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
4188
4334
  classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
4189
4335
  demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
4190
- scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4336
+ advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
4191
4337
  asciiFoldKey, ROUTED_FX,
4192
4338
  // THE SENTINEL POOL, dense over char-class indices so the emitted text can
4193
4339
  // index it the way it indexes `SCANS`. Built only for a recovery assembly
@@ -4196,10 +4342,10 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
4196
4342
  // object rather than to two separately constructed ones.
4197
4343
  REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
4198
4344
  matchesAt, recoverScan, orSentinel, captureError,
4199
- scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
4345
+ scalarRecognizers, t.lex, adjacencyHolds, t.lexPrograms,
4200
4346
  LEAD, triviaFieldCapture, skipDeferredTrivia,
4201
4347
  collapseEmpty,
4202
- PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf,
4348
+ PlainSpan, PlainLeaf, PlainNode, PlainField, PlainBuf, GAP,
4203
4349
  )
4204
4350
  emitReached = em.reached
4205
4351
  } catch (e) {