parseman 0.50.6 → 0.51.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 (59) hide show
  1. package/dist/analysis/choice-cost.d.ts.map +1 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +214 -67
  4. package/dist/analysis/diagnostics.js.map +2 -2
  5. package/dist/analysis/duplication.d.ts.map +1 -1
  6. package/dist/analysis/gating.d.ts.map +1 -1
  7. package/dist/cli/index.js +220 -68
  8. package/dist/cli/index.js.map +2 -2
  9. package/dist/combinators/dispatch.d.ts.map +1 -1
  10. package/dist/combinators/first-set.d.ts +2 -26
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/scanTo.d.ts +23 -1
  13. package/dist/combinators/scanTo.d.ts.map +1 -1
  14. package/dist/compiler/fields.d.ts.map +1 -1
  15. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  16. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  17. package/dist/compiler/token-capability.js +40 -29
  18. package/dist/compiler/token-capability.js.map +1 -1
  19. package/dist/coverage.d.ts.map +1 -1
  20. package/dist/cst/reflection.d.ts.map +1 -1
  21. package/dist/index.js +241 -74
  22. package/dist/index.js.map +2 -2
  23. package/dist/plugin/index.js +228 -71
  24. package/dist/plugin/index.js.map +2 -2
  25. package/dist/spec/index.js +164 -48
  26. package/dist/spec/index.js.map +2 -2
  27. package/dist/table/assemble.d.ts.map +1 -1
  28. package/dist/table/emit.d.ts.map +1 -1
  29. package/dist/table/encode.d.ts.map +1 -1
  30. package/dist/table/exec.d.ts.map +1 -1
  31. package/dist/table/index.js +220 -68
  32. package/dist/table/index.js.map +2 -2
  33. package/dist/table/program.d.ts +2 -0
  34. package/dist/table/program.d.ts.map +1 -1
  35. package/dist/types.d.ts +2 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis/choice-cost.ts +6 -1
  40. package/src/analysis/commitment.ts +10 -2
  41. package/src/analysis/duplication.ts +7 -2
  42. package/src/analysis/gating.ts +6 -1
  43. package/src/analysis/rebuild.ts +1 -1
  44. package/src/combinators/dispatch.ts +4 -1
  45. package/src/combinators/first-set.ts +58 -36
  46. package/src/combinators/scanTo.ts +163 -9
  47. package/src/compiler/fields.ts +8 -2
  48. package/src/compiler/grammar-coverage-ids.ts +6 -1
  49. package/src/compiler/ir-serialize.ts +7 -2
  50. package/src/compiler/token-alphabet.ts +1 -1
  51. package/src/coverage.ts +7 -1
  52. package/src/cst/reflection.ts +6 -1
  53. package/src/table/assemble.ts +11 -1
  54. package/src/table/emit.ts +4 -0
  55. package/src/table/encode.ts +16 -8
  56. package/src/table/exec.ts +7 -1
  57. package/src/table/program.ts +2 -0
  58. package/src/types.ts +1 -1
  59. package/src/version.ts +1 -1
@@ -32,6 +32,28 @@ export type ScanToOptions = {
32
32
  * everything consumed so far. Default false (fail at EOF).
33
33
  */
34
34
  orEOF?: boolean
35
+ /**
36
+ * A delimiter owned by the surrounding grammar that may terminate an
37
+ * unfinished scan. Unlike putting that delimiter in `sentinel`, `scanTo`
38
+ * keeps looking for a real sentinel so the same delimiter may appear in a
39
+ * complete payload.
40
+ *
41
+ * This mode is intended for paired sentinels such as quotes. Once a recovery
42
+ * point has been seen, later sentinels are counted as alternating close/open
43
+ * delimiters. At the next outer recovery point (or EOF), an odd count accepts
44
+ * the first sentinel; an even count returns the first recovery point. This
45
+ * preserves complete payloads such as `` `let x = 1; x` `` without pairing an
46
+ * unfinished payload with a later construct's opening delimiter.
47
+ */
48
+ recoverAt?: Combinator<unknown>
49
+ /**
50
+ * An unconditional recovery boundary. The scan resolves at this position
51
+ * using the same paired-sentinel decision as EOF. A hard stop is checked at
52
+ * the current scan position before opaque skippers. A skipper that began at an
53
+ * earlier position remains opaque throughout its matched span. Use this for
54
+ * boundaries such as a line end after an escape character.
55
+ */
56
+ stopAt?: Combinator<unknown>
35
57
  /**
36
58
  * `balanced()` only. Make an unmatched close a genuine FAILURE instead of a
37
59
  * recovered one.
@@ -97,7 +119,7 @@ export function resolveScanSkip(
97
119
  */
98
120
  export function scanTo(
99
121
  sentinel: Combinator<unknown>,
100
- { skip, raw = false, orEOF = false }: ScanToOptions = {},
122
+ { skip, raw = false, orEOF = false, recoverAt, stopAt }: ScanToOptions = {},
101
123
  ): Combinator<string> {
102
124
  const meta: ParserMeta = {
103
125
  firstSet: any(),
@@ -109,7 +131,11 @@ export function scanTo(
109
131
  return {
110
132
  _tag: 'scanTo',
111
133
  _meta: meta,
112
- _def: { tag: 'scanTo', sentinel, skip: explicitSkip, raw, orEOF },
134
+ _def: {
135
+ tag: 'scanTo', sentinel, skip: explicitSkip, raw, orEOF,
136
+ ...(recoverAt === undefined ? {} : { recoverAt }),
137
+ ...(stopAt === undefined ? {} : { stopAt }),
138
+ },
113
139
  parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
114
140
  let cur = pos
115
141
  // Fold grammar-level ambient trivia + scanSkip into the effective skippers.
@@ -133,35 +159,163 @@ export function scanTo(
133
159
  }
134
160
  }
135
161
 
162
+ // Keep ordinary scanTo on its established hot path. Recovery adds probes,
163
+ // parity, and diagnostic checkpoints inside the byte loop; selecting that
164
+ // machinery at construction scope would tax every CSS prelude scan even
165
+ // though almost all scanTo sites use neither recovery option.
166
+ if (recoverAt === undefined && stopAt === undefined) {
167
+ while (cur < input.length) {
168
+ const s = sentinel.parse(input, cur, probeCtx)
169
+ if (s.ok) {
170
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
171
+ emit(cur)
172
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
173
+ }
174
+
175
+ let advanced = false
176
+ for (const skipper of skip) {
177
+ const r = skipper.parse(input, cur, probeCtx)
178
+ if (r.ok && r.span.end > cur) {
179
+ cur = r.span.end
180
+ advanced = true
181
+ break
182
+ }
183
+ }
184
+ if (!advanced) cur++
185
+ }
186
+
187
+ if (orEOF) {
188
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
189
+ emit(cur)
190
+ return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
191
+ }
192
+ const sentDef = sentinel._def
193
+ const expected = sentDef.tag === 'literal' ? [JSON.stringify(sentDef.value)] : ['sentinel']
194
+ return { ok: false, expected, span: { start: pos, end: cur } }
195
+ }
196
+
197
+ let recovery = -1
198
+ let firstSentinel = -1
199
+ let sentinelCount = 0
200
+ let recoveryErrorCount = -1
201
+ let sentinelErrorCount = -1
202
+
203
+ const succeed = (end: number): ParseResult<string> => {
204
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, end)
205
+ emit(end)
206
+ return { ok: true, value: input.slice(pos, end), span: { start: pos, end } }
207
+ }
208
+
209
+ const recoveredEnd = () => {
210
+ const acceptSentinel = firstSentinel >= 0 && (sentinelCount & 1) === 1
211
+ const errors = ctx._errors
212
+ if (errors !== undefined) {
213
+ const keep = acceptSentinel ? sentinelErrorCount : recoveryErrorCount
214
+ if (keep >= 0 && errors.length > keep) errors.length = keep
215
+ }
216
+ return acceptSentinel ? firstSentinel : recovery
217
+ }
218
+
136
219
  while (cur < input.length) {
137
220
  // Check sentinel — if it matches here, stop and return consumed text.
221
+ const sentinelProbeErrorCount = ctx._errors?.length ?? -1
138
222
  const s = sentinel.parse(input, cur, probeCtx)
223
+ const sentinelProbeErrors = ctx._errors
224
+ if (sentinelProbeErrors !== undefined
225
+ && sentinelProbeErrorCount >= 0
226
+ && sentinelProbeErrors.length > sentinelProbeErrorCount) {
227
+ sentinelProbeErrors.length = sentinelProbeErrorCount
228
+ }
139
229
  if (s.ok) {
140
- if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
141
- emit(cur)
142
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
230
+ if (recovery < 0) return succeed(cur)
231
+ if (s.span.end <= cur) {
232
+ throw new TypeError('scanTo recovery requires a sentinel that consumes input')
233
+ }
234
+ if (firstSentinel < 0) {
235
+ firstSentinel = cur
236
+ sentinelErrorCount = sentinelProbeErrorCount
237
+ }
238
+ sentinelCount++
239
+ cur = s.span.end
240
+ continue
241
+ }
242
+
243
+ // A hard stop is stronger than opacity: when both begin here, the stop
244
+ // must win so a skipper cannot carry the scan across a boundary that the
245
+ // caller declared uncrossable. The sentinel still has first priority.
246
+ if (stopAt !== undefined) {
247
+ const errorCount = ctx._errors?.length ?? -1
248
+ const stop = stopAt.parse(input, cur, probeCtx)
249
+ const errors = ctx._errors
250
+ if (errors !== undefined && errorCount >= 0 && errors.length > errorCount) {
251
+ errors.length = errorCount
252
+ }
253
+ if (stop.ok) {
254
+ if (recovery < 0) {
255
+ recovery = cur
256
+ recoveryErrorCount = errorCount
257
+ }
258
+ return succeed(recoveredEnd())
259
+ }
143
260
  }
144
261
 
145
262
  // Try each skipper in order; take first that advances.
146
263
  let advanced = false
147
264
  for (const skipper of skip) {
265
+ const errorCount = ctx._errors?.length ?? -1
148
266
  const r = skipper.parse(input, cur, probeCtx)
149
267
  if (r.ok && r.span.end > cur) {
150
268
  cur = r.span.end
151
269
  advanced = true
152
270
  break
153
271
  }
272
+ // A skipper is a speculative opaque-region probe. If it recognizes
273
+ // an opener but cannot finish (for example strict balanced('(', ')')
274
+ // at an outer recovery boundary), its committed diagnostic belongs
275
+ // only to that failed probe. Letting it escape makes an otherwise
276
+ // successful recovery look malformed to the enclosing tolerant list.
277
+ const errors = ctx._errors
278
+ if (errors !== undefined && errorCount >= 0 && errors.length > errorCount) {
279
+ errors.length = errorCount
280
+ }
281
+ }
282
+
283
+ // Opaque units protect recovery boundaries just as they protect the
284
+ // sentinel. A `{` owned by balanced('{', '}') is payload, not an outer
285
+ // recovery point, even when recoverAt can also begin with `{`.
286
+ if (advanced) continue
287
+
288
+ if (recoverAt !== undefined) {
289
+ const errorCount = ctx._errors?.length ?? -1
290
+ const recover = recoverAt.parse(input, cur, probeCtx)
291
+ const errors = ctx._errors
292
+ if (errors !== undefined && errorCount >= 0 && errors.length > errorCount) {
293
+ errors.length = errorCount
294
+ }
295
+ if (recover.ok) {
296
+ if (recover.span.end <= cur) {
297
+ throw new TypeError('scanTo recoverAt must consume input')
298
+ }
299
+ if (recovery < 0) {
300
+ recovery = cur
301
+ recoveryErrorCount = errorCount
302
+ }
303
+ if (firstSentinel >= 0 && (sentinelCount & 1) === 1) {
304
+ return succeed(recoveredEnd())
305
+ }
306
+ cur = recover.span.end
307
+ continue
308
+ }
154
309
  }
155
310
 
156
311
  // Nothing matched — consume one character and continue.
157
- if (!advanced) cur++
312
+ cur++
158
313
  }
159
314
 
160
315
  // Reached EOF without finding sentinel.
316
+ if (recovery >= 0) return succeed(recoveredEnd())
161
317
  if (orEOF) {
162
- if (ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, cur)
163
- emit(cur)
164
- return { ok: true, value: input.slice(pos, cur), span: { start: pos, end: cur } }
318
+ return succeed(cur)
165
319
  }
166
320
  const sentDef = sentinel._def
167
321
  const expected = sentDef.tag === 'literal' ? [JSON.stringify(sentDef.value)] : ['sentinel']
@@ -25,7 +25,10 @@ export function parserHasOwnFields(p: Combinator<unknown>, seen: Set<Combinator<
25
25
  case 'dispatch': return parserHasOwnFields(d.selector, seen) || d.cases.some(x => parserHasOwnFields(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => parserHasOwnFields(entry.parser, seen)) : false) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false)
26
26
  case 'sepBy': return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen)
27
27
  case 'grammar': return parserHasOwnFields(d.parser, seen) || (d.triviaParser ? parserHasOwnFields(d.triviaParser, seen) : false)
28
- case 'scanTo': return parserHasOwnFields(d.sentinel, seen) || d.skip.some(x => parserHasOwnFields(x, seen))
28
+ case 'scanTo': return parserHasOwnFields(d.sentinel, seen)
29
+ || d.skip.some(x => parserHasOwnFields(x, seen))
30
+ || (d.recoverAt !== undefined && parserHasOwnFields(d.recoverAt, seen))
31
+ || (d.stopAt !== undefined && parserHasOwnFields(d.stopAt, seen))
29
32
  case 'recover': return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.sentinel, seen)
30
33
  case 'routed': return d.fallback ? parserHasOwnFields(d.fallback, seen) : false
31
34
  case 'many':
@@ -151,7 +154,10 @@ export function parserEnablesTriviaCapture(p: Combinator<unknown>, seen: Set<Com
151
154
  case 'choice': return d.parsers.some(x => parserEnablesTriviaCapture(x, seen))
152
155
  case 'dispatch': return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some(x => parserEnablesTriviaCapture(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => parserEnablesTriviaCapture(entry.parser, seen)) : false) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false)
153
156
  case 'sepBy': return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen)
154
- case 'scanTo': return parserEnablesTriviaCapture(d.sentinel, seen) || d.skip.some(x => parserEnablesTriviaCapture(x, seen))
157
+ case 'scanTo': return parserEnablesTriviaCapture(d.sentinel, seen)
158
+ || d.skip.some(x => parserEnablesTriviaCapture(x, seen))
159
+ || (d.recoverAt !== undefined && parserEnablesTriviaCapture(d.recoverAt, seen))
160
+ || (d.stopAt !== undefined && parserEnablesTriviaCapture(d.stopAt, seen))
155
161
  case 'recover': return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.sentinel, seen)
156
162
  case 'routed': return d.fallback ? parserEnablesTriviaCapture(d.fallback, seen) : false
157
163
  case 'many':
@@ -33,7 +33,12 @@ function children(def: ParserDef, winners?: Record<string, Combinator<unknown>>)
33
33
  case 'dispatch': return [def.selector, ...def.cases.map(entry => entry.parser), ...(def.matchers ? def.matchers.map(entry => entry.parser) : []), ...(def.otherwise ? [def.otherwise] : [])]
34
34
  case 'many': case 'oneOrMore': case 'optional': case 'attempt': case 'transform': case 'trivia': case 'token': case 'leaf': case 'label': case 'field': case 'grammar': case 'not': case 'peek': case 'node': case 'guard': case 'adjacency': case 'withCtx': case 'recover': case 'expect': return 'parser' in def ? [def.parser] : []
35
35
  case 'sepBy': return [def.parser, def.separator]
36
- case 'scanTo': return [def.sentinel, ...def.skip]
36
+ case 'scanTo': return [
37
+ def.sentinel,
38
+ ...def.skip,
39
+ ...(def.recoverAt === undefined ? [] : [def.recoverAt]),
40
+ ...(def.stopAt === undefined ? [] : [def.stopAt]),
41
+ ]
37
42
  case 'lazy': {
38
43
  let resolved: Combinator<unknown>
39
44
  try { resolved = def.thunk() } catch (e) {
@@ -65,7 +65,12 @@ function childrenOf(def: ParserDef): Comb[] {
65
65
  case 'expect': return [def.parser]
66
66
  case 'grammar': return def.triviaParser ? [def.parser, def.triviaParser] : [def.parser]
67
67
  case 'sepBy': return [def.parser, def.separator]
68
- case 'scanTo': return [def.sentinel, ...def.skip]
68
+ case 'scanTo': return [
69
+ def.sentinel,
70
+ ...def.skip,
71
+ ...(def.recoverAt === undefined ? [] : [def.recoverAt]),
72
+ ...(def.stopAt === undefined ? [] : [def.stopAt]),
73
+ ]
69
74
  case 'routed': return def.fallback ? [def.fallback] : []
70
75
  case 'lazy':
71
76
  case 'literal':
@@ -584,7 +589,7 @@ class Serializer {
584
589
  case 'field': return `field(${JSON.stringify(def.name)}, ${kid(def.parser)})`
585
590
  case 'expect': return `expect(${kid(def.parser)}${def.label !== undefined ? `, ${JSON.stringify(def.label)}` : ''})`
586
591
  case 'scanTo':
587
- return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF} })`
592
+ return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF}${def.recoverAt === undefined ? '' : `, recoverAt: ${kid(def.recoverAt)}`}${def.stopAt === undefined ? '' : `, stopAt: ${kid(def.stopAt)}`} })`
588
593
  case 'transform': {
589
594
  if (def.fnSrc === undefined) throw new Unserializable('transform without fnSrc')
590
595
  // `_tf` sets `_def.fnSrc` so re-lowering INLINES the callback (a plain
@@ -1319,7 +1319,7 @@ export function tokenChildren(
1319
1319
  const out: Combinator<unknown>[] = []
1320
1320
  const push = (v: Combinator<unknown> | undefined): void => { if (v !== undefined) out.push(v) }
1321
1321
  const rec = d as unknown as Record<string, unknown>
1322
- for (const k of ['parser', 'main', 'skipped', 'separator', 'selector', 'sentinel', 'fallback', 'otherwise', 'triviaParser']) {
1322
+ for (const k of ['parser', 'main', 'skipped', 'separator', 'selector', 'sentinel', 'recoverAt', 'stopAt', 'fallback', 'otherwise', 'triviaParser']) {
1323
1323
  const v = rec[k]
1324
1324
  if (v !== null && typeof v === 'object' && '_def' in (v as object)) push(v as Combinator<unknown>)
1325
1325
  }
package/src/coverage.ts CHANGED
@@ -359,7 +359,13 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
359
359
  case 'guard': return gate(def.predicate)
360
360
  case 'withCtx': return withCtx(def.extra, build(def.parser))
361
361
  case 'expect': return expect(build(def.parser), def.label)
362
- case 'scanTo': return scanTo(build(def.sentinel), { skip: def.skip.map(build), orEOF: def.orEOF })
362
+ case 'scanTo': return scanTo(build(def.sentinel), {
363
+ skip: def.skip.map(build),
364
+ raw: def.raw,
365
+ orEOF: def.orEOF,
366
+ ...(def.recoverAt === undefined ? {} : { recoverAt: build(def.recoverAt) }),
367
+ ...(def.stopAt === undefined ? {} : { stopAt: build(def.stopAt) }),
368
+ })
363
369
  // Zero-width assertion with no coverage-bearing child; reuse it verbatim so
364
370
  // the rebuilt sequence still recognises the boundary marker.
365
371
  case 'adjacency': return parser
@@ -58,7 +58,12 @@ function childrenOf(def: ParserDef): readonly Combinator<unknown>[] {
58
58
  case 'sepBy':
59
59
  return [def.parser, def.separator]
60
60
  case 'scanTo':
61
- return [def.sentinel, ...def.skip]
61
+ return [
62
+ def.sentinel,
63
+ ...def.skip,
64
+ ...(def.recoverAt === undefined ? [] : [def.recoverAt]),
65
+ ...(def.stopAt === undefined ? [] : [def.stopAt]),
66
+ ]
62
67
  case 'routed':
63
68
  return def.fallback ? [def.fallback] : []
64
69
  case 'literal':
@@ -487,6 +487,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
487
487
  for (const s of prog.scans ?? []) {
488
488
  for (const r of s.skip) labelExtraIps.push(r[0])
489
489
  if (s.sentinel !== undefined) labelExtraIps.push(s.sentinel[0])
490
+ if (s.recoverAt !== undefined) labelExtraIps.push(s.recoverAt[0])
491
+ if (s.stopAt !== undefined) labelExtraIps.push(s.stopAt[0])
490
492
  }
491
493
  for (const set of prog.scanSkip ?? []) for (const r of set) labelExtraIps.push(r[0])
492
494
  const closureLabels = computeSiteLabels(
@@ -3659,6 +3661,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3659
3661
  for (const s of prog.scans ?? []) {
3660
3662
  for (const r of s.skip) extraIps.push(r[0])
3661
3663
  if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
3664
+ if (s.recoverAt !== undefined) extraIps.push(s.recoverAt[0])
3665
+ if (s.stopAt !== undefined) extraIps.push(s.stopAt[0])
3662
3666
  }
3663
3667
  for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
3664
3668
  const roots = [...Object.values(prog.rules), ...extraIps]
@@ -3855,7 +3859,13 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
3855
3859
  const sentDef: ParserDef | undefined = typeof s.sent === 'string'
3856
3860
  ? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
3857
3861
  : undefined
3858
- scansArr.push(scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>)
3862
+ scansArr.push(scanTo(subtreeComb(s.sentinel!, sentDef), {
3863
+ skip,
3864
+ raw,
3865
+ orEOF: (s.flags & 2) !== 0,
3866
+ ...(s.recoverAt === undefined ? {} : { recoverAt: subtreeComb(s.recoverAt) }),
3867
+ ...(s.stopAt === undefined ? {} : { stopAt: subtreeComb(s.stopAt) }),
3868
+ }) as Combinator<unknown>)
3859
3869
  }
3860
3870
  const scans: readonly Combinator<unknown>[] = scansArr
3861
3871
 
package/src/table/emit.ts CHANGED
@@ -104,6 +104,8 @@ function emitRef(r: import('./program.ts').SubtreeRef): string {
104
104
  function emitScanSpec(s: import('./program.ts').ScanSpec): string {
105
105
  const parts = [`kind:${s.kind}`, `flags:${s.flags}`, `skip:[${s.skip.map(emitRef).join(',')}]`]
106
106
  if (s.sentinel !== undefined) parts.push(`sentinel:${emitRef(s.sentinel)}`)
107
+ if (s.recoverAt !== undefined) parts.push(`recoverAt:${emitRef(s.recoverAt)}`)
108
+ if (s.stopAt !== undefined) parts.push(`stopAt:${emitRef(s.stopAt)}`)
107
109
  if (s.sent !== undefined) parts.push(`sent:${s.sent === null ? 'null' : jsString(s.sent)}`)
108
110
  if (s.open !== undefined) parts.push(`open:${jsString(s.open)}`)
109
111
  if (s.close !== undefined) parts.push(`close:${jsString(s.close)}`)
@@ -197,6 +199,8 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
197
199
  for (const s of prog.scans ?? []) {
198
200
  for (const r of s.skip) extraIps.push(r[0])
199
201
  if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
202
+ if (s.recoverAt !== undefined) extraIps.push(s.recoverAt[0])
203
+ if (s.stopAt !== undefined) extraIps.push(s.stopAt[0])
200
204
  }
201
205
  for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
202
206
 
@@ -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
 
@@ -1003,6 +1005,8 @@ class Encoder {
1003
1005
  flags: (d.raw ? 1 : 0) | (d.orEOF ? 2 : 0),
1004
1006
  skip: d.skip.map(c => this.subtree(c)),
1005
1007
  sentinel: this.subtree(d.sentinel),
1008
+ ...(d.recoverAt === undefined ? {} : { recoverAt: this.subtree(d.recoverAt) }),
1009
+ ...(d.stopAt === undefined ? {} : { stopAt: this.subtree(d.stopAt) }),
1006
1010
  sent: sentDef.tag === 'literal' ? sentDef.value : null,
1007
1011
  }))
1008
1012
  }
@@ -1168,7 +1172,7 @@ class Encoder {
1168
1172
  // the O(1) table (`exclusive`) or falls to the ordered per-arm path.
1169
1173
  // Arm ORDER is preserved on both, which is what makes this a PEG-safe
1170
1174
  // change rather than a reordering.
1171
- const finalChoice = classifyFinalChoice(arms, rr)
1175
+ const finalChoice = classifyFinalChoice(arms, rr, this.activeTrivia)
1172
1176
  // attempt() preserves its child's success language, but the broad
1173
1177
  // classifier deliberately treats the transaction as nullable/unknown so
1174
1178
  // it can never by itself authorize exclusive one-arm selection. Recover
@@ -1180,7 +1184,7 @@ class Encoder {
1180
1184
  if (!finalChoice.nullable[i]) return undefined
1181
1185
  const def = arm._def as ParserDef
1182
1186
  if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
1183
- return firstSetOf(def.parser, new Set(), rr)
1187
+ return firstSetOf(def.parser, new Set(), rr, this.activeTrivia)
1184
1188
  })
1185
1189
  const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
1186
1190
  const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
@@ -1239,7 +1243,7 @@ class Encoder {
1239
1243
  const rr = this.refResolver()
1240
1244
  const itemClass = matchesEmpty(d.parser, new Set(), rr)
1241
1245
  ? -1
1242
- : this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
1246
+ : this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1243
1247
  const op = d.valueUnused ? OP_REPV : OP_REP
1244
1248
  const min = d.tag === 'many' ? 0 : d.min
1245
1249
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -1274,7 +1278,7 @@ class Encoder {
1274
1278
  if (this.rec) {
1275
1279
  return this.emit(
1276
1280
  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())),
1281
+ this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1278
1282
  )
1279
1283
  }
1280
1284
  return d.min >= 2
@@ -1411,7 +1415,7 @@ class Encoder {
1411
1415
  // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1412
1416
  // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1413
1417
  const fs = d.parser._def.tag === 'lazy'
1414
- ? firstSetOf(d.parser, new Set(), this.refResolver())
1418
+ ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1415
1419
  : d.parser._meta.firstSet
1416
1420
  if (fs.kind === 'any') return body
1417
1421
  const cls = this.charClass(fs)
@@ -1605,7 +1609,7 @@ class Encoder {
1605
1609
  // Same override-aware resolution as the node gate: a bare-ref inner resolves
1606
1610
  // its gate through `winners` so a widened winner is not gated out.
1607
1611
  const fs = d.parser._def.tag === 'lazy'
1608
- ? firstSetOf(d.parser, new Set(), this.refResolver())
1612
+ ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1609
1613
  : d.parser._meta.firstSet
1610
1614
  if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1611
1615
  const cls = this.charClass(fs)
@@ -1762,6 +1766,8 @@ class Encoder {
1762
1766
  const refs: SubtreeRef[] = []
1763
1767
  for (const s of this.scans) {
1764
1768
  if (s.sentinel !== undefined) refs.push(s.sentinel)
1769
+ if (s.recoverAt !== undefined) refs.push(s.recoverAt)
1770
+ if (s.stopAt !== undefined) refs.push(s.stopAt)
1765
1771
  refs.push(...s.skip)
1766
1772
  }
1767
1773
  for (const set of this.scanSkipSets) refs.push(...set)
@@ -1782,6 +1788,8 @@ class Encoder {
1782
1788
  ...s,
1783
1789
  skip: s.skip.map(res),
1784
1790
  ...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
1791
+ ...(s.recoverAt === undefined ? {} : { recoverAt: res(s.recoverAt) }),
1792
+ ...(s.stopAt === undefined ? {} : { stopAt: res(s.stopAt) }),
1785
1793
  }))
1786
1794
  this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
1787
1795
  }
package/src/table/exec.ts CHANGED
@@ -1666,7 +1666,13 @@ function makeDriver(
1666
1666
  const sentDef: ParserDef | undefined = typeof s.sent === 'string'
1667
1667
  ? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
1668
1668
  : undefined
1669
- return scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>
1669
+ return scanTo(subtreeComb(s.sentinel!, sentDef), {
1670
+ skip,
1671
+ raw,
1672
+ orEOF: (s.flags & 2) !== 0,
1673
+ ...(s.recoverAt === undefined ? {} : { recoverAt: subtreeComb(s.recoverAt) }),
1674
+ ...(s.stopAt === undefined ? {} : { stopAt: subtreeComb(s.stopAt) }),
1675
+ }) as Combinator<unknown>
1670
1676
  })
1671
1677
 
1672
1678
  const scanSkip: readonly (readonly Combinator<unknown>[])[] =
@@ -251,6 +251,8 @@ export type ScanSpec = {
251
251
  readonly flags: number
252
252
  readonly skip: readonly SubtreeRef[]
253
253
  readonly sentinel?: SubtreeRef
254
+ readonly recoverAt?: SubtreeRef
255
+ readonly stopAt?: SubtreeRef
254
256
  readonly sent?: string | null
255
257
  readonly open?: string
256
258
  readonly close?: string
package/src/types.ts CHANGED
@@ -107,7 +107,7 @@ export type ParserDef =
107
107
  // `scanSkip` are PREPENDED at parse/compile time (explicit skip EXTENDS the
108
108
  // ambient default). `raw`: hard opt-out — skip nothing ambiently, restoring the
109
109
  // pre-ambient raw byte-walk.
110
- | { tag: 'scanTo'; sentinel: Combinator<unknown>; skip: Combinator<unknown>[]; raw: boolean; orEOF: boolean }
110
+ | { tag: 'scanTo'; sentinel: Combinator<unknown>; skip: Combinator<unknown>[]; raw: boolean; orEOF: boolean; recoverAt?: Combinator<unknown>; stopAt?: Combinator<unknown> }
111
111
  | { tag: 'keywords'; words: readonly string[]; caseInsensitive: boolean; boundary: string | undefined }
112
112
  | { tag: 'unknown' }
113
113
 
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.51.0'