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.
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +214 -67
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/cli/index.js +220 -68
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +2 -26
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +23 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +40 -29
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/index.js +241 -74
- package/dist/index.js.map +2 -2
- package/dist/plugin/index.js +228 -71
- package/dist/plugin/index.js.map +2 -2
- package/dist/spec/index.js +164 -48
- package/dist/spec/index.js.map +2 -2
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +220 -68
- package/dist/table/index.js.map +2 -2
- package/dist/table/program.d.ts +2 -0
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/choice-cost.ts +6 -1
- package/src/analysis/commitment.ts +10 -2
- package/src/analysis/duplication.ts +7 -2
- package/src/analysis/gating.ts +6 -1
- package/src/analysis/rebuild.ts +1 -1
- package/src/combinators/dispatch.ts +4 -1
- package/src/combinators/first-set.ts +58 -36
- package/src/combinators/scanTo.ts +163 -9
- package/src/compiler/fields.ts +8 -2
- package/src/compiler/grammar-coverage-ids.ts +6 -1
- package/src/compiler/ir-serialize.ts +7 -2
- package/src/compiler/token-alphabet.ts +1 -1
- package/src/coverage.ts +7 -1
- package/src/cst/reflection.ts +6 -1
- package/src/table/assemble.ts +11 -1
- package/src/table/emit.ts +4 -0
- package/src/table/encode.ts +16 -8
- package/src/table/exec.ts +7 -1
- package/src/table/program.ts +2 -0
- package/src/types.ts +1 -1
- 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: {
|
|
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 (
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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']
|
package/src/compiler/fields.ts
CHANGED
|
@@ -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)
|
|
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)
|
|
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 [
|
|
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 [
|
|
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), {
|
|
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
|
package/src/cst/reflection.ts
CHANGED
|
@@ -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 [
|
|
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':
|
package/src/table/assemble.ts
CHANGED
|
@@ -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), {
|
|
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
|
|
package/src/table/encode.ts
CHANGED
|
@@ -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++)
|
|
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), {
|
|
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>[])[] =
|
package/src/table/program.ts
CHANGED
|
@@ -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