parseman 0.50.3 → 0.50.5
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 +24 -10
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +24 -10
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +22 -8
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/index.js +55 -13
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.js +55 -13
- package/dist/plugin/index.js.map +1 -1
- package/dist/spec/index.js +22 -8
- package/dist/spec/index.js.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +24 -10
- package/dist/table/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/combinators/first-set.ts +33 -7
- package/src/compiler/ir-serialize.ts +41 -2
- package/src/table/encode.ts +14 -2
- 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.5";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parseman",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.5",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^20.19.0 || >=22.13.0"
|
|
7
7
|
},
|
|
8
|
-
"description": "Parser combinators that compile to optimized JavaScript
|
|
8
|
+
"description": "Parser combinators that compile to optimized JavaScript — use as a library or as a build-time macro",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"author": "Matthew Dean <matthew-dean@users.noreply.github.com>",
|
|
@@ -63,11 +63,31 @@ export function classifyFinalChoice(
|
|
|
63
63
|
return { exclusive, firstSets, nullable }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** The
|
|
67
|
-
|
|
66
|
+
/** The compose WINNER a named lazy ref resolves to — the same row the encoder's
|
|
67
|
+
* `lazy` case calls via `winners`/`scopedRef`. Winner-FIRST resolution is what makes
|
|
68
|
+
* first-set / nullability analysis AGREE with emission: a base piece's thunk still
|
|
69
|
+
* closes over its own binding, so a compose OVERRIDE (and any undefined cross-piece
|
|
70
|
+
* ref) would otherwise be invisible to the gate — the reducer reroutes, the recognizer
|
|
71
|
+
* does not. `undefined` → no winner; fall back to the thunk. Monolithic: the winner IS
|
|
72
|
+
* the thunk's target, a no-op. A winner that recurses back to `p` (recursive or
|
|
73
|
+
* self-alias rule) is caught by the shared `seen` guard → `any`, a sound over-approx. */
|
|
74
|
+
function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
|
|
68
75
|
if (resolve === undefined) return undefined
|
|
69
76
|
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
70
|
-
|
|
77
|
+
const w = name === undefined ? undefined : resolve(name)
|
|
78
|
+
// Skip a winner that merely wraps THIS reference (a transparent rules()/parser()
|
|
79
|
+
// alias): resolving to it is a self-hop, not an override — matches the encoder's
|
|
80
|
+
// own `winnerWrapsReference` guard so first-set analysis agrees with emission.
|
|
81
|
+
if (w === undefined || w === p) return undefined
|
|
82
|
+
let cur = w
|
|
83
|
+
for (const seen = new Set<Combinator<unknown>>(); !seen.has(cur); ) {
|
|
84
|
+
if (cur === p) return undefined
|
|
85
|
+
seen.add(cur)
|
|
86
|
+
const cd = cur._def as ParserDef
|
|
87
|
+
if (cd.tag !== 'grammar' && cd.tag !== 'trivia') return w
|
|
88
|
+
cur = cd.parser
|
|
89
|
+
}
|
|
90
|
+
return w
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
export function union(a: FirstSet, b: FirstSet): FirstSet {
|
|
@@ -190,9 +210,12 @@ function matchesEmptyBody(
|
|
|
190
210
|
case 'node':
|
|
191
211
|
case 'grammar':
|
|
192
212
|
case 'recover': return me(d.parser)
|
|
193
|
-
case 'lazy':
|
|
213
|
+
case 'lazy': {
|
|
214
|
+
const w = winnerForRef(p, resolve)
|
|
215
|
+
if (w !== undefined) return me(w)
|
|
194
216
|
try { return me(d.thunk()) }
|
|
195
|
-
catch {
|
|
217
|
+
catch { return true }
|
|
218
|
+
}
|
|
196
219
|
default: return true // scanTo / guard / unknown → assume nullable (safe)
|
|
197
220
|
}
|
|
198
221
|
}
|
|
@@ -338,9 +361,12 @@ function firstSetBody(
|
|
|
338
361
|
case 'regex':
|
|
339
362
|
case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
|
|
340
363
|
case 'dispatch': return fs(d.selector)
|
|
341
|
-
case 'lazy':
|
|
364
|
+
case 'lazy': {
|
|
365
|
+
const w = winnerForRef(p, resolve)
|
|
366
|
+
if (w !== undefined) return fs(w)
|
|
342
367
|
try { return fs(d.thunk()) }
|
|
343
|
-
catch {
|
|
368
|
+
catch { return any() }
|
|
369
|
+
}
|
|
344
370
|
case 'choice': {
|
|
345
371
|
let out: FirstSet = empty()
|
|
346
372
|
for (const arm of d.parsers) out = union(out, fs(arm))
|
|
@@ -321,6 +321,33 @@ class Serializer {
|
|
|
321
321
|
return c._def.tag === 'lazy' && ruleNameOf(c) !== undefined ? (lazyTarget(c) ?? c) : c
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The child a rule-entry grammar scope should serialize as its body, when that
|
|
326
|
+
* scope merely WRAPS the rule's own named `lazy` proxy.
|
|
327
|
+
*
|
|
328
|
+
* `rules({ trackLines: true }, …)` wraps every rule VALUE — which is that rule's
|
|
329
|
+
* own named lazy proxy — in a `grammarParser({ trackLines: true })` scope
|
|
330
|
+
* (`parser.ts:229-242`). `body()` unwraps a BARE lazy entry to its defined target;
|
|
331
|
+
* a lazy wrapped one level deep by that scope must be unwrapped INSIDE the scope
|
|
332
|
+
* too. Left wrapped, the inner proxy serializes as a `g[name]` SELF-reference and
|
|
333
|
+
* the rule's real body is dropped, so it re-lowers to `Name: parser(opts, g.Name)`
|
|
334
|
+
* — a bodyless self-cycle the table encoder rejects with `alias cycle — lazy (rule
|
|
335
|
+
* 'Name') encodes to its own recursion trampoline`. This is why the four
|
|
336
|
+
* `*PositionsGrammar` exports fell back to a runtime `compose()` under trackLines.
|
|
337
|
+
*
|
|
338
|
+
* STRICT: only a scope that IS a rule entry (in `localRuleNames`) AND wraps ITS
|
|
339
|
+
* OWN name unwraps. A scope wrapping another rule's ref (a genuine cross-rule
|
|
340
|
+
* alias, `Name: parser(opts, g.Other)`) or a real body is untouched, so the
|
|
341
|
+
* non-trackLines and non-self-wrap output stays byte-identical.
|
|
342
|
+
*/
|
|
343
|
+
private scopeSelfBody(c: Comb): Comb | undefined {
|
|
344
|
+
const name = this.localRuleNames.get(c)
|
|
345
|
+
if (name === undefined || c._def.tag !== 'grammar') return undefined
|
|
346
|
+
const inner = c._def.parser
|
|
347
|
+
if (inner._def.tag !== 'lazy' || ruleNameOf(inner) !== name) return undefined
|
|
348
|
+
return lazyTarget(inner) ?? undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
324
351
|
run(): string {
|
|
325
352
|
for (const [, c] of this.ruleMap) this.analyze(this.body(c), new Set())
|
|
326
353
|
for (const c of this.scanSkip) { this.assertNoRuleRef(c, new Set()); this.analyze(c, new Set()) }
|
|
@@ -415,7 +442,19 @@ class Serializer {
|
|
|
415
442
|
return
|
|
416
443
|
}
|
|
417
444
|
active.add(c)
|
|
418
|
-
|
|
445
|
+
// When a rule-entry scope wraps its own named lazy, substitute the lazy's
|
|
446
|
+
// unwrapped body for `def.parser` — but keep the scope's OTHER children (its
|
|
447
|
+
// `triviaParser`) in the analysis, exactly as `childrenOf('grammar')` returns
|
|
448
|
+
// both. Dropping the trivia parser here would leave a recursive/shared trivia
|
|
449
|
+
// unanalysed, so its const/self-ref hoisting would be missing when `expr` emits
|
|
450
|
+
// it via `kid(def.triviaParser)`. selfBody is non-undefined only for `'grammar'`.
|
|
451
|
+
const selfBody = def.tag === 'grammar' ? this.scopeSelfBody(c) : undefined
|
|
452
|
+
const kids = selfBody === undefined
|
|
453
|
+
? childrenOf(def)
|
|
454
|
+
: def.tag === 'grammar' && def.triviaParser
|
|
455
|
+
? [selfBody, def.triviaParser]
|
|
456
|
+
: [selfBody]
|
|
457
|
+
for (const child of kids) this.analyze(child, active)
|
|
419
458
|
active.delete(c)
|
|
420
459
|
}
|
|
421
460
|
|
|
@@ -602,7 +641,7 @@ class Serializer {
|
|
|
602
641
|
: spec
|
|
603
642
|
? `classifiedTrivia({ ${spec.arms.map(arm => `${JSON.stringify(arm.label)}: ${kid(arm.parser)}`).join(', ')} })`
|
|
604
643
|
: def.triviaParser ? kid(def.triviaParser) : 'undefined'
|
|
605
|
-
return `parser({ trivia: ${trivia}${def.captureTrivia ? ', captureTrivia: true' : ''}${def.rootCapture ? `, rootCapture: ${JSON.stringify(def.rootCapture)}` : ''}${def.trackLines ? ', trackLines: true' : ''} }, ${kid(def.parser)})`
|
|
644
|
+
return `parser({ trivia: ${trivia}${def.captureTrivia ? ', captureTrivia: true' : ''}${def.rootCapture ? `, rootCapture: ${JSON.stringify(def.rootCapture)}` : ''}${def.trackLines ? ', trackLines: true' : ''} }, ${kid(this.scopeSelfBody(c) ?? def.parser)})`
|
|
606
645
|
}
|
|
607
646
|
case 'withCtx': {
|
|
608
647
|
// A `withCtx` round-trips through `_wc`, which rebuilds it AND re-attaches
|
package/src/table/encode.ts
CHANGED
|
@@ -1404,7 +1404,15 @@ class Encoder {
|
|
|
1404
1404
|
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1405
1405
|
// running the body reports `['"x"']` alone.
|
|
1406
1406
|
if (matchesEmpty(d.parser)) return body
|
|
1407
|
-
|
|
1407
|
+
// A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
|
|
1408
|
+
// first set from the ref's base binding. Under a compose OVERRIDE the winner
|
|
1409
|
+
// for `Y` may be WIDER, so the cached set gates out input the merged rule
|
|
1410
|
+
// accepts (the reducer reroutes via `winners`, but this recognizer gate did
|
|
1411
|
+
// not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
|
|
1412
|
+
// resolves the call. Non-ref bodies keep the cached set (byte-identical).
|
|
1413
|
+
const fs = d.parser._def.tag === 'lazy'
|
|
1414
|
+
? firstSetOf(d.parser, new Set(), this.refResolver())
|
|
1415
|
+
: d.parser._meta.firstSet
|
|
1408
1416
|
if (fs.kind === 'any') return body
|
|
1409
1417
|
const cls = this.charClass(fs)
|
|
1410
1418
|
if (cls < 0) return body
|
|
@@ -1594,7 +1602,11 @@ class Encoder {
|
|
|
1594
1602
|
// run itself would have skipped past. Running the inner instead reported a
|
|
1595
1603
|
// strictly smaller set, so omitting the guard was an observable divergence,
|
|
1596
1604
|
// not a slower equivalent.
|
|
1597
|
-
|
|
1605
|
+
// Same override-aware resolution as the node gate: a bare-ref inner resolves
|
|
1606
|
+
// its gate through `winners` so a widened winner is not gated out.
|
|
1607
|
+
const fs = d.parser._def.tag === 'lazy'
|
|
1608
|
+
? firstSetOf(d.parser, new Set(), this.refResolver())
|
|
1609
|
+
: d.parser._meta.firstSet
|
|
1598
1610
|
if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
|
|
1599
1611
|
const cls = this.charClass(fs)
|
|
1600
1612
|
if (cls < 0) return inner
|
package/src/version.ts
CHANGED