parseman 0.50.5 → 0.50.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/diagnostics.js +49 -46
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +49 -46
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/first-set.d.ts +2 -26
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +38 -27
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/index.js +50 -47
- package/dist/index.js.map +2 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +53 -47
- package/dist/plugin/index.js.map +2 -2
- package/dist/spec/index.js +37 -36
- package/dist/spec/index.js.map +2 -2
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +49 -46
- package/dist/table/index.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/first-set.ts +58 -36
- package/src/plugin/evaluator.ts +13 -0
- package/src/table/encode.ts +10 -8
- package/src/version.ts +1 -1
package/dist/version.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
|
|
14
14
|
* the build if this constant drifts from the published version.
|
|
15
15
|
*/
|
|
16
|
-
export declare const PARSEMAN_VERSION = "0.50.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.50.7";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ export type FinalChoiceClassification = {
|
|
|
25
25
|
export function classifyFinalChoice(
|
|
26
26
|
arms: readonly Combinator<unknown>[],
|
|
27
27
|
resolve?: RefResolver,
|
|
28
|
+
activeTrivia?: Combinator<unknown> | null,
|
|
28
29
|
): FinalChoiceClassification {
|
|
29
30
|
const firstSets: FirstSet[] = []
|
|
30
31
|
const nullable: boolean[] = []
|
|
@@ -36,7 +37,7 @@ export function classifyFinalChoice(
|
|
|
36
37
|
for (const arm of arms) {
|
|
37
38
|
const empty = matchesEmpty(arm, new Set(), resolve)
|
|
38
39
|
nullable.push(empty)
|
|
39
|
-
firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve))
|
|
40
|
+
firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve, activeTrivia))
|
|
40
41
|
if (empty) exclusive = false
|
|
41
42
|
}
|
|
42
43
|
if (exclusive) {
|
|
@@ -109,7 +110,7 @@ export function intersects(a: FirstSet, b: FirstSet): boolean {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
export function fromChar(code: number): FirstSet {
|
|
112
|
-
return
|
|
113
|
+
return fromRange(code, code)
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
@@ -238,11 +239,10 @@ function matchesEmptyBody(
|
|
|
238
239
|
* (see `isPositiveLookahead` and `sequenceFirstSet`).
|
|
239
240
|
*/
|
|
240
241
|
export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
|
|
241
|
-
const tag = (p._def as ParserDef).tag
|
|
242
242
|
// `adjacency` joins `not` for the same reason: `adjacent()`/`notAdjacent()` are
|
|
243
243
|
// zero-width tests of the gap BEHIND the cursor, so they constrain nothing about
|
|
244
244
|
// the first char ahead and must not contribute their `any` to the sequence.
|
|
245
|
-
return
|
|
245
|
+
return p._tag === 'not' || p._tag === 'adjacency'
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
@@ -259,8 +259,10 @@ export function isZeroWidthAssertion(p: Combinator<unknown>): boolean {
|
|
|
259
259
|
* therefore constrains nothing; `peek()` reports `any()` in that case, which the
|
|
260
260
|
* intersection treats as "no constraint".
|
|
261
261
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
function isPositiveLookahead(
|
|
263
|
+
p: Combinator<unknown>,
|
|
264
|
+
): p is Combinator<unknown> & { _def: Extract<ParserDef, { tag: 'peek' }> } {
|
|
265
|
+
return p._tag === 'peek'
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
/** Intersect a lookahead constraint into an accumulator; `any` = no constraint. */
|
|
@@ -285,9 +287,7 @@ function narrowBy(acc: FirstSet | null, constraint: FirstSet): FirstSet | null {
|
|
|
285
287
|
* only succeed — even zero-width — where X matches.
|
|
286
288
|
*/
|
|
287
289
|
function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
|
|
288
|
-
|
|
289
|
-
if (fs.kind === 'empty') return assertion
|
|
290
|
-
return narrowBy(fs, assertion) ?? fs
|
|
290
|
+
return assertion === null ? fs : fs.kind === 'empty' ? assertion : narrowBy(fs, assertion)!
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
/**
|
|
@@ -299,19 +299,36 @@ function applyAssertion(fs: FirstSet, assertion: FirstSet | null): FirstSet {
|
|
|
299
299
|
* A leading zero-width assertion (`not(…)`) is nullable but contributes NOTHING to
|
|
300
300
|
* the first-set (see `isZeroWidthAssertion`) — its `any` must not poison the union.
|
|
301
301
|
*/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
function sequenceSet(
|
|
303
|
+
parsers: readonly Combinator<unknown>[],
|
|
304
|
+
fs: (p: Combinator<unknown>) => FirstSet,
|
|
305
|
+
empties: (p: Combinator<unknown>) => boolean,
|
|
306
|
+
trivia?: Combinator<unknown> | null,
|
|
307
|
+
): FirstSet {
|
|
308
|
+
const afterBoundary = (i: number): FirstSet => {
|
|
309
|
+
const rest = at(i)
|
|
310
|
+
return trivia ? union(rest, fs(trivia)) : rest
|
|
311
|
+
}
|
|
312
|
+
const at = (i: number): FirstSet => {
|
|
313
|
+
const p = parsers[i]!
|
|
306
314
|
if (isPositiveLookahead(p)) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
315
|
+
const constraint = fs(p)
|
|
316
|
+
return i + 1 === parsers.length
|
|
317
|
+
? constraint
|
|
318
|
+
: applyAssertion(afterBoundary(i + 1), constraint)
|
|
319
|
+
}
|
|
320
|
+
if (isZeroWidthAssertion(p)) {
|
|
321
|
+
return i + 1 === parsers.length ? empty() : afterBoundary(i + 1)
|
|
310
322
|
}
|
|
311
|
-
|
|
312
|
-
if (!
|
|
323
|
+
const first = fs(p)
|
|
324
|
+
if (!empties(p) || i + 1 === parsers.length) return first
|
|
325
|
+
return union(first, afterBoundary(i + 1))
|
|
313
326
|
}
|
|
314
|
-
return
|
|
327
|
+
return at(0)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
|
|
331
|
+
return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty)
|
|
315
332
|
}
|
|
316
333
|
|
|
317
334
|
/**
|
|
@@ -335,6 +352,7 @@ export function firstSetOf(
|
|
|
335
352
|
p: Combinator<unknown>,
|
|
336
353
|
seen: Set<Combinator<unknown>> = new Set(),
|
|
337
354
|
resolve?: RefResolver,
|
|
355
|
+
activeTrivia?: Combinator<unknown> | null,
|
|
338
356
|
): FirstSet {
|
|
339
357
|
// `seen` is the current recursion path, not a global visited set. A completed
|
|
340
358
|
// shared child must be analysed again for a sibling; only a back-edge on this
|
|
@@ -342,7 +360,7 @@ export function firstSetOf(
|
|
|
342
360
|
if (seen.has(p)) return any() // cycle → any (safe over-approximation)
|
|
343
361
|
seen.add(p)
|
|
344
362
|
try {
|
|
345
|
-
return firstSetBody(p, seen, resolve)
|
|
363
|
+
return firstSetBody(p, seen, resolve, activeTrivia)
|
|
346
364
|
} finally {
|
|
347
365
|
seen.delete(p)
|
|
348
366
|
}
|
|
@@ -352,8 +370,9 @@ function firstSetBody(
|
|
|
352
370
|
p: Combinator<unknown>,
|
|
353
371
|
seen: Set<Combinator<unknown>>,
|
|
354
372
|
resolve: RefResolver | undefined,
|
|
373
|
+
activeTrivia: Combinator<unknown> | null | undefined,
|
|
355
374
|
): FirstSet {
|
|
356
|
-
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve)
|
|
375
|
+
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia)
|
|
357
376
|
const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
|
|
358
377
|
const d = p._def as ParserDef
|
|
359
378
|
switch (d.tag) {
|
|
@@ -373,19 +392,17 @@ function firstSetBody(
|
|
|
373
392
|
return out
|
|
374
393
|
}
|
|
375
394
|
case 'sequence': {
|
|
376
|
-
//
|
|
377
|
-
// term
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
return applyAssertion(out, assertion)
|
|
395
|
+
// A zero-width or nullable term leaves the cursor at its current position,
|
|
396
|
+
// while every later term first skips active trivia. The sequence can
|
|
397
|
+
// therefore start with that trivia or with the later term. A positive
|
|
398
|
+
// lookahead still constrains both possibilities at the cursor where it ran:
|
|
399
|
+
//
|
|
400
|
+
// peek ∩ (later ∪ trivia)
|
|
401
|
+
//
|
|
402
|
+
// Recurse at every term boundary because a lookahead after another
|
|
403
|
+
// zero-width term runs after trivia has advanced, not at the sequence's
|
|
404
|
+
// original cursor.
|
|
405
|
+
return sequenceSet(d.parsers, fs, empties, activeTrivia)
|
|
389
406
|
}
|
|
390
407
|
case 'peek': {
|
|
391
408
|
// Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
|
|
@@ -402,9 +419,14 @@ function firstSetBody(
|
|
|
402
419
|
case 'token':
|
|
403
420
|
case 'leaf':
|
|
404
421
|
case 'node':
|
|
405
|
-
case '
|
|
422
|
+
case 'sepBy':
|
|
406
423
|
case 'expect': return fs(d.parser)
|
|
407
|
-
case '
|
|
424
|
+
case 'grammar': return firstSetOf(
|
|
425
|
+
d.parser,
|
|
426
|
+
seen,
|
|
427
|
+
resolve,
|
|
428
|
+
d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
|
|
429
|
+
)
|
|
408
430
|
default: return p._meta.firstSet // not / scanTo / guard / withCtx / recover / unknown
|
|
409
431
|
}
|
|
410
432
|
}
|
package/src/plugin/evaluator.ts
CHANGED
|
@@ -756,6 +756,19 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
756
756
|
if (prov) carriedImports.push({ local: name, source: prov.source, imported: prov.imported })
|
|
757
757
|
else unresolved.push(name)
|
|
758
758
|
}
|
|
759
|
+
} else if (be!.type === 'Identifier' && typeof be!.name === 'string' && combi._def.buildSrc === be!.name) {
|
|
760
|
+
// A NAMED reducer that is a BARE IMPORTED identifier — `node(t, parser, mkRel)`
|
|
761
|
+
// where `mkRel` is `import { mkRel } from './astf'`. The table inlines the
|
|
762
|
+
// reducer as its NAME (`f:[mkRel]`), and a cross-package compose that re-lowers
|
|
763
|
+
// this carried piece re-emits that name verbatim into the consuming module —
|
|
764
|
+
// which does not import it, so the fused artifact throws ReferenceError. Carry
|
|
765
|
+
// the ONE import the emitted name needs so the downstream re-emit binds it. This
|
|
766
|
+
// is the reducer NAME's own import (unambiguous — it is THIS module's import),
|
|
767
|
+
// never the reducer BODY's internal free names (the `buildSigSrc` P1 trap): the
|
|
768
|
+
// guard `buildSrc === be.name` also excludes a foreign-factory reducer whose
|
|
769
|
+
// source was rewritten to a self-contained body above.
|
|
770
|
+
const prov = _builderImports?.(be!.name) ?? null
|
|
771
|
+
if (prov) carriedImports.push({ local: be!.name, source: prov.source, imported: prov.imported })
|
|
759
772
|
}
|
|
760
773
|
const staticError = [...report.structural, ...unresolved]
|
|
761
774
|
if (staticError.length > 0) combi._def.buildStaticError = staticError
|
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
|
|
|
@@ -1168,7 +1170,7 @@ class Encoder {
|
|
|
1168
1170
|
// the O(1) table (`exclusive`) or falls to the ordered per-arm path.
|
|
1169
1171
|
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
1170
1172
|
// change rather than a reordering.
|
|
1171
|
-
const finalChoice = classifyFinalChoice(arms, rr)
|
|
1173
|
+
const finalChoice = classifyFinalChoice(arms, rr, this.activeTrivia)
|
|
1172
1174
|
// attempt() preserves its child's success language, but the broad
|
|
1173
1175
|
// classifier deliberately treats the transaction as nullable/unknown so
|
|
1174
1176
|
// it can never by itself authorize exclusive one-arm selection. Recover
|
|
@@ -1180,7 +1182,7 @@ class Encoder {
|
|
|
1180
1182
|
if (!finalChoice.nullable[i]) return undefined
|
|
1181
1183
|
const def = arm._def as ParserDef
|
|
1182
1184
|
if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
|
|
1183
|
-
return firstSetOf(def.parser, new Set(), rr)
|
|
1185
|
+
return firstSetOf(def.parser, new Set(), rr, this.activeTrivia)
|
|
1184
1186
|
})
|
|
1185
1187
|
const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
|
|
1186
1188
|
const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
|
|
@@ -1239,7 +1241,7 @@ class Encoder {
|
|
|
1239
1241
|
const rr = this.refResolver()
|
|
1240
1242
|
const itemClass = matchesEmpty(d.parser, new Set(), rr)
|
|
1241
1243
|
? -1
|
|
1242
|
-
: this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
|
|
1244
|
+
: this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
|
|
1243
1245
|
const op = d.valueUnused ? OP_REPV : OP_REP
|
|
1244
1246
|
const min = d.tag === 'many' ? 0 : d.min
|
|
1245
1247
|
// ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
|
|
@@ -1274,7 +1276,7 @@ class Encoder {
|
|
|
1274
1276
|
if (this.rec) {
|
|
1275
1277
|
return this.emit(
|
|
1276
1278
|
OP_REP, child, d.min, d.max ?? -1, sep, flags,
|
|
1277
|
-
this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver())),
|
|
1279
|
+
this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
|
|
1278
1280
|
)
|
|
1279
1281
|
}
|
|
1280
1282
|
return d.min >= 2
|
|
@@ -1411,7 +1413,7 @@ class Encoder {
|
|
|
1411
1413
|
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1412
1414
|
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1413
1415
|
const fs = d.parser._def.tag === 'lazy'
|
|
1414
|
-
? firstSetOf(d.parser, new Set(), this.refResolver())
|
|
1416
|
+
? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
|
|
1415
1417
|
: d.parser._meta.firstSet
|
|
1416
1418
|
if (fs.kind === 'any') return body
|
|
1417
1419
|
const cls = this.charClass(fs)
|
|
@@ -1605,7 +1607,7 @@ class Encoder {
|
|
|
1605
1607
|
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1606
1608
|
// its gate through `winners` so a widened winner is not gated out.
|
|
1607
1609
|
const fs = d.parser._def.tag === 'lazy'
|
|
1608
|
-
? firstSetOf(d.parser, new Set(), this.refResolver())
|
|
1610
|
+
? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
|
|
1609
1611
|
: d.parser._meta.firstSet
|
|
1610
1612
|
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1611
1613
|
const cls = this.charClass(fs)
|
package/src/version.ts
CHANGED