parseman 0.35.0 → 0.37.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/README.md +34 -19
- package/dist/analysis/duplication.d.ts +421 -0
- package/dist/analysis/duplication.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +25 -0
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/grammar.d.ts +12 -0
- package/dist/analysis/grammar.d.ts.map +1 -0
- package/dist/combinators/expect.d.ts +12 -0
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +26 -0
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +12 -0
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/codegen.d.ts +43 -1
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +64 -7
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/cst/host-mode.d.ts +43 -0
- package/dist/cst/host-mode.d.ts.map +1 -0
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/index.cjs +1621 -55
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1621 -55
- package/dist/index.js.map +4 -4
- package/dist/language-service/index.cjs +37 -1
- package/dist/language-service/index.cjs.map +3 -3
- package/dist/language-service/index.js +37 -1
- package/dist/language-service/index.js.map +3 -3
- package/dist/oracle/corpus.d.ts +32 -0
- package/dist/oracle/corpus.d.ts.map +1 -0
- package/dist/oracle/digest.d.ts +21 -0
- package/dist/oracle/digest.d.ts.map +1 -0
- package/dist/oracle/identity.d.ts +117 -0
- package/dist/oracle/identity.d.ts.map +1 -0
- package/dist/oracle/index.cjs +423 -0
- package/dist/oracle/index.cjs.map +7 -0
- package/dist/oracle/index.d.ts +37 -0
- package/dist/oracle/index.d.ts.map +1 -0
- package/dist/oracle/index.js +400 -0
- package/dist/oracle/index.js.map +7 -0
- package/dist/plugin/index.cjs +1628 -62
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1628 -62
- package/dist/plugin/index.js.map +4 -4
- package/dist/run/index.cjs +23 -0
- package/dist/run/index.cjs.map +3 -3
- package/dist/run/index.js +23 -0
- package/dist/run/index.js.map +3 -3
- package/dist/spec/index.cjs +2977 -56
- package/dist/spec/index.cjs.map +4 -4
- package/dist/spec/index.js +2977 -56
- package/dist/spec/index.js.map +4 -4
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +10 -3
package/dist/plugin/index.js
CHANGED
|
@@ -1090,7 +1090,7 @@ function captureError(ctx, error) {
|
|
|
1090
1090
|
|
|
1091
1091
|
// src/combinators/expect.ts
|
|
1092
1092
|
function deriveExpected(c) {
|
|
1093
|
-
return derive(c, /* @__PURE__ */ new Set());
|
|
1093
|
+
return [...new Set(derive(c, /* @__PURE__ */ new Set()))];
|
|
1094
1094
|
}
|
|
1095
1095
|
function derive(c, seen) {
|
|
1096
1096
|
const deriveExpected2 = (p) => derive(p, seen);
|
|
@@ -2400,6 +2400,12 @@ function rules(a, b) {
|
|
|
2400
2400
|
if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
|
|
2401
2401
|
}
|
|
2402
2402
|
}
|
|
2403
|
+
if (options?.hostMode === "cst") {
|
|
2404
|
+
for (const key of Object.keys(definitions)) {
|
|
2405
|
+
const rule = cache[key];
|
|
2406
|
+
if (rule && !rule._meta.isTrivia) rule._meta.grammarHostMode = "cst";
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2403
2409
|
for (const key of Object.keys(definitions)) {
|
|
2404
2410
|
markUnusedValues(cache[key]);
|
|
2405
2411
|
}
|
|
@@ -2458,6 +2464,9 @@ function peek(combinator) {
|
|
|
2458
2464
|
}
|
|
2459
2465
|
|
|
2460
2466
|
// src/compiler/build-arity.ts
|
|
2467
|
+
function cstOutputHost(build) {
|
|
2468
|
+
return build?._parsemanCstOutput === true;
|
|
2469
|
+
}
|
|
2461
2470
|
var PARAM_LIST_RE = (
|
|
2462
2471
|
// (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
|
|
2463
2472
|
/^(?:function\b[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([A-Za-z_$][\w$]*)\s*=>)/
|
|
@@ -2707,7 +2716,8 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
|
|
|
2707
2716
|
const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
|
|
2708
2717
|
const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
|
|
2709
2718
|
const clonesState = build ? buildReadsState(def) : true;
|
|
2710
|
-
const
|
|
2719
|
+
const hasOwnFields = parserHasOwnFields(combinator);
|
|
2720
|
+
const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
|
|
2711
2721
|
const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
|
|
2712
2722
|
let failExpected;
|
|
2713
2723
|
return {
|
|
@@ -2722,28 +2732,32 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
|
|
|
2722
2732
|
}
|
|
2723
2733
|
return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
|
|
2724
2734
|
}
|
|
2735
|
+
const hostCst = build !== void 0 && cstOutputHost(ctx.build);
|
|
2736
|
+
const effTrivia = capturesTrivia || hostCst;
|
|
2737
|
+
const effFields = capturesFields || hostCst && hasOwnFields;
|
|
2725
2738
|
const saved = beginCstNodeCapture(ctx);
|
|
2726
2739
|
const savedFields = ctx._fields;
|
|
2727
|
-
ctx._fields =
|
|
2740
|
+
ctx._fields = effFields ? [] : void 0;
|
|
2728
2741
|
const savedMask = ctx._triviaCaptureMask;
|
|
2729
2742
|
if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
|
|
2730
2743
|
ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
|
|
2731
2744
|
}
|
|
2732
|
-
if (!
|
|
2745
|
+
if (!effTrivia) ctx.captureTrivia = false;
|
|
2733
2746
|
let r = combinator.parse(input, pos, ctx);
|
|
2734
2747
|
if (r.ok && trailingTrivia && ctx.trivia) {
|
|
2735
2748
|
const end = consumeTrivia(input, r.span.end, ctx);
|
|
2736
2749
|
r = { ...r, span: { start: r.span.start, end } };
|
|
2737
2750
|
}
|
|
2738
|
-
const fields =
|
|
2751
|
+
const fields = effFields ? buildFieldMap(ctx._fields) : void 0;
|
|
2739
2752
|
ctx._fields = savedFields;
|
|
2740
2753
|
ctx._triviaCaptureMask = savedMask;
|
|
2741
2754
|
const { children: children2, rawChildren, triviaLog } = endCstNodeCapture(ctx, saved);
|
|
2742
2755
|
if (!r.ok) return r;
|
|
2743
2756
|
const st = clonesState && ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
|
|
2744
2757
|
const nodeType = def.type ?? missingInferredType();
|
|
2745
|
-
const cstOutput = ctx.build
|
|
2746
|
-
const
|
|
2758
|
+
const cstOutput = cstOutputHost(ctx.build);
|
|
2759
|
+
const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
|
|
2760
|
+
const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, hostState) : build(children2, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
|
|
2747
2761
|
const rawEntry = isCstChild(built) ? built : { _tag: "leaf", value: typeof built === "string" ? built : typeof built === "object" && built !== null ? input.slice(r.span.start, r.span.end) : "", span: r.span };
|
|
2748
2762
|
if (saved.buf !== void 0 || saved.ch !== void 0) {
|
|
2749
2763
|
pushCstChild(ctx, built, rawEntry);
|
|
@@ -2994,7 +3008,24 @@ function leaf(root, fn) {
|
|
|
2994
3008
|
}
|
|
2995
3009
|
|
|
2996
3010
|
// src/version.ts
|
|
2997
|
-
var PARSEMAN_VERSION = "0.
|
|
3011
|
+
var PARSEMAN_VERSION = "0.37.0";
|
|
3012
|
+
|
|
3013
|
+
// src/cst/host-mode.ts
|
|
3014
|
+
var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
|
|
3015
|
+
var FUSED_HOST_ELIDED = /* @__PURE__ */ Symbol.for("parseman.fusedHostElided");
|
|
3016
|
+
function assertHostModeCompatible(mode, build, hostBranchElided = true) {
|
|
3017
|
+
const isCstHost = build?._parsemanCstOutput === true;
|
|
3018
|
+
if (mode === "ast" && isCstHost && hostBranchElided) {
|
|
3019
|
+
throw new Error(
|
|
3020
|
+
"parseman: this parser was compiled for host mode \"ast\" (the default), so its nodes build through their own `build` callbacks and no positioned-CST branch was emitted. It cannot be driven with a positioned-CST host (cstBuildHost / a language-service host). Compile a second artifact from the SAME grammar with `compile(grammar, { hostMode: 'cst' })` \u2014 or, under the macro, a second `rules({ hostMode: 'cst' }, factory)` call site \u2014 and drive that one instead."
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
if (mode === "cst" && !isCstHost) {
|
|
3024
|
+
throw new Error(
|
|
3025
|
+
'parseman: this parser was compiled for host mode "cst", so every node builds through a positioned-CST `ctx.build` host. Pass one (e.g. `{ build: cstBuildHost }`), or use an artifact compiled with the default `hostMode: "ast"` to get the grammar\'s own AST.'
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
2998
3029
|
|
|
2999
3030
|
// src/analysis/gating.ts
|
|
3000
3031
|
var isAny = (fs2) => fs2.kind === "any";
|
|
@@ -3194,11 +3225,34 @@ function analyzeGating(entry, opts) {
|
|
|
3194
3225
|
function analyzeGatingRules(ruleMap, opts) {
|
|
3195
3226
|
const raw = [];
|
|
3196
3227
|
const antiPatterns = [];
|
|
3228
|
+
const unanalysable = [];
|
|
3229
|
+
const seenUnanalysable = /* @__PURE__ */ new Set();
|
|
3197
3230
|
const visited = /* @__PURE__ */ new Set();
|
|
3231
|
+
const noteUnanalysable = (u) => {
|
|
3232
|
+
const key = `${u.rule}\0${u.kind}`;
|
|
3233
|
+
if (seenUnanalysable.has(key)) return;
|
|
3234
|
+
seenUnanalysable.add(key);
|
|
3235
|
+
unanalysable.push(u);
|
|
3236
|
+
};
|
|
3198
3237
|
const visit = (p, enclosingRule) => {
|
|
3199
3238
|
if (visited.has(p)) return;
|
|
3200
3239
|
visited.add(p);
|
|
3201
|
-
const
|
|
3240
|
+
const def = p?._def;
|
|
3241
|
+
if (def === null || typeof def !== "object") {
|
|
3242
|
+
noteUnanalysable(
|
|
3243
|
+
typeof p === "function" ? {
|
|
3244
|
+
rule: enclosingRule,
|
|
3245
|
+
kind: "fused-rule",
|
|
3246
|
+
reason: "fused rule function \u2014 a compose()/fuse result has no combinator graph. Analyze the composed grammar itself (analyzeGrammarGating) so the carried IR is re-lowered first."
|
|
3247
|
+
} : {
|
|
3248
|
+
rule: enclosingRule,
|
|
3249
|
+
kind: "not-a-combinator",
|
|
3250
|
+
reason: `value of type ${p === null ? "null" : typeof p} is not a combinator and carries no _def descriptor.`
|
|
3251
|
+
}
|
|
3252
|
+
);
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
const d = def;
|
|
3202
3256
|
const rule = ruleNameOf(p) ?? enclosingRule;
|
|
3203
3257
|
if (d.tag === "choice") {
|
|
3204
3258
|
raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
|
|
@@ -3238,7 +3292,18 @@ function analyzeGatingRules(ruleMap, opts) {
|
|
|
3238
3292
|
const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
|
|
3239
3293
|
const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
|
|
3240
3294
|
const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
|
|
3241
|
-
return {
|
|
3295
|
+
return {
|
|
3296
|
+
totalChoices: choices.length,
|
|
3297
|
+
gated,
|
|
3298
|
+
recoverable,
|
|
3299
|
+
unanalysable,
|
|
3300
|
+
ungated,
|
|
3301
|
+
accepted,
|
|
3302
|
+
deferred,
|
|
3303
|
+
acceptedUnused,
|
|
3304
|
+
choices,
|
|
3305
|
+
antiPatterns
|
|
3306
|
+
};
|
|
3242
3307
|
}
|
|
3243
3308
|
function analyzeChoice(p, d, rule, resolve2) {
|
|
3244
3309
|
const arms = d.parsers;
|
|
@@ -3275,6 +3340,13 @@ function analyzeChoice(p, d, rule, resolve2) {
|
|
|
3275
3340
|
}
|
|
3276
3341
|
function formatGatingWarnings(report) {
|
|
3277
3342
|
const lines = [];
|
|
3343
|
+
if (report.unanalysable.length > 0) {
|
|
3344
|
+
lines.push(
|
|
3345
|
+
`parseman gating: ${report.unanalysable.length} rule(s) UNANALYSABLE \u2014 this report is PARTIAL (${report.totalChoices} choice(s) examined). An empty finding list below does NOT mean the grammar is clean.`
|
|
3346
|
+
);
|
|
3347
|
+
for (const u of report.unanalysable)
|
|
3348
|
+
lines.push(` \xB7 ${u.rule} [${u.kind}]: ${u.reason}`);
|
|
3349
|
+
}
|
|
3278
3350
|
for (const c of report.ungated) {
|
|
3279
3351
|
lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
|
|
3280
3352
|
for (const a of c.anyArms)
|
|
@@ -3290,6 +3362,1284 @@ function formatGatingWarnings(report) {
|
|
|
3290
3362
|
return lines;
|
|
3291
3363
|
}
|
|
3292
3364
|
|
|
3365
|
+
// src/analysis/duplication.ts
|
|
3366
|
+
var siteToString = (s) => s.path === "" ? s.rule : `${s.rule} \u203A ${s.path}`;
|
|
3367
|
+
function childrenOf(d) {
|
|
3368
|
+
switch (d.tag) {
|
|
3369
|
+
case "sequence":
|
|
3370
|
+
case "choice":
|
|
3371
|
+
return d.parsers;
|
|
3372
|
+
case "skip":
|
|
3373
|
+
return [d.main, d.skipped];
|
|
3374
|
+
case "sepBy":
|
|
3375
|
+
return [d.parser, d.separator];
|
|
3376
|
+
case "recover":
|
|
3377
|
+
return [d.parser, d.sentinel];
|
|
3378
|
+
case "scanTo":
|
|
3379
|
+
return [d.sentinel, ...d.skip];
|
|
3380
|
+
case "grammar":
|
|
3381
|
+
return d.triviaParser ? [d.parser, d.triviaParser] : [d.parser];
|
|
3382
|
+
// A `lazy` is a REFERENCE, not a subtree: hashing through it would make every
|
|
3383
|
+
// rule's shape include the whole reachable grammar. Treated as a leaf, keyed
|
|
3384
|
+
// by the rule name it refers to — which is also the right semantics, since two
|
|
3385
|
+
// productions referencing `g.Ident` really do fill that slot the same way.
|
|
3386
|
+
case "lazy":
|
|
3387
|
+
case "literal":
|
|
3388
|
+
case "regex":
|
|
3389
|
+
case "keywords":
|
|
3390
|
+
case "guard":
|
|
3391
|
+
case "unknown":
|
|
3392
|
+
return [];
|
|
3393
|
+
default: {
|
|
3394
|
+
const rec = d;
|
|
3395
|
+
return rec.parser ? [rec.parser] : [];
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
var ruleNameOf2 = (p) => p._ruleName;
|
|
3400
|
+
var fnKey = (src, fn) => src ?? (typeof fn === "function" ? String(fn) : "");
|
|
3401
|
+
function payloadKey(p, d) {
|
|
3402
|
+
switch (d.tag) {
|
|
3403
|
+
case "literal":
|
|
3404
|
+
return `literal\0${d.value}\0${d.caseInsensitive}`;
|
|
3405
|
+
case "regex":
|
|
3406
|
+
return `regex\0${d.source}\0${d.flags}`;
|
|
3407
|
+
case "keywords":
|
|
3408
|
+
return `keywords\0${[...d.words].join("")}\0${d.caseInsensitive}\0${d.boundary ?? ""}`;
|
|
3409
|
+
case "lazy": {
|
|
3410
|
+
const n = ruleNameOf2(p);
|
|
3411
|
+
return n === void 0 ? `ref\0#${anonId(p)}` : `ref\0${n}`;
|
|
3412
|
+
}
|
|
3413
|
+
case "label":
|
|
3414
|
+
return `label\0${d.label}`;
|
|
3415
|
+
case "field":
|
|
3416
|
+
return `field\0${d.name}`;
|
|
3417
|
+
case "node":
|
|
3418
|
+
return `node\0${d.type ?? ""}\0${d.unwrap === true}\0${d.collapse === true}\0${fnKey(d.buildSrc, d.build)}`;
|
|
3419
|
+
case "transform":
|
|
3420
|
+
return `transform\0${fnKey(d.fnSrc, d.fn)}\0${d.recognitionOnly === true}`;
|
|
3421
|
+
case "leaf":
|
|
3422
|
+
return `leaf\0${fnKey(d.fnSrc, d.fn)}`;
|
|
3423
|
+
case "many":
|
|
3424
|
+
case "oneOrMore":
|
|
3425
|
+
return `${d.tag}\0${d.min}\0${d.max ?? ""}`;
|
|
3426
|
+
case "sepBy":
|
|
3427
|
+
return `sepBy\0${d.min}\0${d.max ?? ""}\0${d.trailing ?? ""}`;
|
|
3428
|
+
case "expect":
|
|
3429
|
+
return `expect\0${d.label ?? ""}\0${d.expected.join("")}`;
|
|
3430
|
+
case "scanTo":
|
|
3431
|
+
return `scanTo\0${d.raw}\0${d.orEOF}`;
|
|
3432
|
+
case "guard":
|
|
3433
|
+
return `guard\0${fnKey(d.predSrc, d.predicate)}`;
|
|
3434
|
+
case "withCtx":
|
|
3435
|
+
return `withCtx\0${d.extraSrc ?? safeJson(d.extra)}`;
|
|
3436
|
+
case "grammar":
|
|
3437
|
+
return `grammar\0${d.clearTrivia === true}\0${d.captureTrivia === true}\0${d.trackLines}`;
|
|
3438
|
+
default:
|
|
3439
|
+
return d.tag;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function safeJson(v2) {
|
|
3443
|
+
try {
|
|
3444
|
+
return JSON.stringify(v2) ?? String(v2);
|
|
3445
|
+
} catch {
|
|
3446
|
+
return "?";
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
var anonCounter = 0;
|
|
3450
|
+
var anonIds = /* @__PURE__ */ new WeakMap();
|
|
3451
|
+
function anonId(p) {
|
|
3452
|
+
let id = anonIds.get(p);
|
|
3453
|
+
if (id === void 0) {
|
|
3454
|
+
id = ++anonCounter;
|
|
3455
|
+
anonIds.set(p, id);
|
|
3456
|
+
}
|
|
3457
|
+
return id;
|
|
3458
|
+
}
|
|
3459
|
+
var Interner = class {
|
|
3460
|
+
ids = /* @__PURE__ */ new Map();
|
|
3461
|
+
hole;
|
|
3462
|
+
constructor() {
|
|
3463
|
+
this.hole = this.of("\0HOLE\0");
|
|
3464
|
+
}
|
|
3465
|
+
of(key) {
|
|
3466
|
+
let id = this.ids.get(key);
|
|
3467
|
+
if (id === void 0) {
|
|
3468
|
+
id = this.ids.size;
|
|
3469
|
+
this.ids.set(key, id);
|
|
3470
|
+
}
|
|
3471
|
+
return id;
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
var MAX_RENDER = 160;
|
|
3475
|
+
function render(p, depth = 3) {
|
|
3476
|
+
const d = p._def;
|
|
3477
|
+
const name = ruleNameOf2(p);
|
|
3478
|
+
switch (d.tag) {
|
|
3479
|
+
case "literal":
|
|
3480
|
+
return `literal('${d.value}')`;
|
|
3481
|
+
case "regex":
|
|
3482
|
+
return `regex(/${d.source}/${d.flags})`;
|
|
3483
|
+
case "keywords":
|
|
3484
|
+
return `keywords([${[...d.words].slice(0, 4).map((w) => `'${w}'`).join(", ")}${d.words.length > 4 ? ", \u2026" : ""}])`;
|
|
3485
|
+
case "lazy":
|
|
3486
|
+
return `g.${name ?? "?"}`;
|
|
3487
|
+
case "guard":
|
|
3488
|
+
return "gate(\u2026)";
|
|
3489
|
+
default:
|
|
3490
|
+
break;
|
|
3491
|
+
}
|
|
3492
|
+
if (depth <= 0) return `${d.tag}(\u2026)`;
|
|
3493
|
+
const kids = childrenOf(d).map((k) => render(k, depth - 1));
|
|
3494
|
+
switch (d.tag) {
|
|
3495
|
+
case "sequence":
|
|
3496
|
+
return `sequence(${kids.join(", ")})`;
|
|
3497
|
+
case "choice":
|
|
3498
|
+
return `choice(${kids.join(", ")})`;
|
|
3499
|
+
case "node":
|
|
3500
|
+
return `node(${d.type ? `'${d.type}', ` : ""}${kids[0] ?? ""})`;
|
|
3501
|
+
case "label":
|
|
3502
|
+
return `label('${d.label}', ${kids[0] ?? ""})`;
|
|
3503
|
+
case "field":
|
|
3504
|
+
return `field('${d.name}', ${kids[0] ?? ""})`;
|
|
3505
|
+
case "many":
|
|
3506
|
+
return `many(${kids[0] ?? ""}${d.min === 0 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
|
|
3507
|
+
case "oneOrMore":
|
|
3508
|
+
return `oneOrMore(${kids[0] ?? ""}${d.min === 1 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
|
|
3509
|
+
case "sepBy":
|
|
3510
|
+
return `sepBy(${kids[0] ?? ""}, ${kids[1] ?? ""})`;
|
|
3511
|
+
default:
|
|
3512
|
+
return `${d.tag}(${kids.join(", ")})`;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
var clamp = (s) => s.length <= MAX_RENDER ? s : `${s.slice(0, MAX_RENDER - 1)}\u2026`;
|
|
3516
|
+
function segment(d, index) {
|
|
3517
|
+
switch (d.tag) {
|
|
3518
|
+
case "sequence":
|
|
3519
|
+
return `seq[${index}]`;
|
|
3520
|
+
case "choice":
|
|
3521
|
+
return `choice[${index}]`;
|
|
3522
|
+
case "node":
|
|
3523
|
+
return `node(${d.type ?? ""})`;
|
|
3524
|
+
case "label":
|
|
3525
|
+
return `label(${d.label})`;
|
|
3526
|
+
case "field":
|
|
3527
|
+
return `field(${d.name})`;
|
|
3528
|
+
case "sepBy":
|
|
3529
|
+
return index === 0 ? "sepBy.item" : "sepBy.sep";
|
|
3530
|
+
case "skip":
|
|
3531
|
+
return index === 0 ? "skip.main" : "skip.skipped";
|
|
3532
|
+
case "recover":
|
|
3533
|
+
return index === 0 ? "recover.body" : "recover.sentinel";
|
|
3534
|
+
case "scanTo":
|
|
3535
|
+
return index === 0 ? "scanTo.sentinel" : `scanTo.skip[${index - 1}]`;
|
|
3536
|
+
default:
|
|
3537
|
+
return d.tag;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function alternationGroups(src) {
|
|
3541
|
+
const out = [];
|
|
3542
|
+
const stack = [{ branches: [], last: 0 }];
|
|
3543
|
+
let i = 0;
|
|
3544
|
+
while (i < src.length) {
|
|
3545
|
+
const c = src[i];
|
|
3546
|
+
if (c === "\\") {
|
|
3547
|
+
i += 2;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
if (c === "[") {
|
|
3551
|
+
let j = i + 1;
|
|
3552
|
+
if (src[j] === "^") j++;
|
|
3553
|
+
if (src[j] === "]") j++;
|
|
3554
|
+
while (j < src.length && src[j] !== "]") {
|
|
3555
|
+
if (src[j] === "\\") j++;
|
|
3556
|
+
j++;
|
|
3557
|
+
}
|
|
3558
|
+
i = j + 1;
|
|
3559
|
+
continue;
|
|
3560
|
+
}
|
|
3561
|
+
if (c === "(") {
|
|
3562
|
+
let open = i + 1;
|
|
3563
|
+
if (src[open] === "?") {
|
|
3564
|
+
const rest = src.slice(open);
|
|
3565
|
+
const m = /^\?(?::|=|!|<=|<!|<[^>]*>)/.exec(rest);
|
|
3566
|
+
open += m ? m[0].length : 1;
|
|
3567
|
+
}
|
|
3568
|
+
stack.push({ branches: [], last: open });
|
|
3569
|
+
i = open;
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (c === ")") {
|
|
3573
|
+
const top = stack.pop();
|
|
3574
|
+
if (top !== void 0) {
|
|
3575
|
+
top.branches.push(src.slice(top.last, i));
|
|
3576
|
+
if (top.branches.length >= 2) out.push(top.branches);
|
|
3577
|
+
}
|
|
3578
|
+
i++;
|
|
3579
|
+
continue;
|
|
3580
|
+
}
|
|
3581
|
+
if (c === "|") {
|
|
3582
|
+
const top = stack[stack.length - 1];
|
|
3583
|
+
top.branches.push(src.slice(top.last, i));
|
|
3584
|
+
top.last = i + 1;
|
|
3585
|
+
i++;
|
|
3586
|
+
continue;
|
|
3587
|
+
}
|
|
3588
|
+
i++;
|
|
3589
|
+
}
|
|
3590
|
+
const root = stack[0];
|
|
3591
|
+
root.branches.push(src.slice(root.last));
|
|
3592
|
+
if (root.branches.length >= 2) out.push(root.branches);
|
|
3593
|
+
return out;
|
|
3594
|
+
}
|
|
3595
|
+
var WORD_RE_SRC = "-?[A-Za-z][A-Za-z0-9_-]*";
|
|
3596
|
+
var WORDS_RE = new RegExp(`^${WORD_RE_SRC}(?:\\|${WORD_RE_SRC})*$`);
|
|
3597
|
+
var VOCABULARY_MIN_WORDS = 3;
|
|
3598
|
+
function keywordRegexShape(source) {
|
|
3599
|
+
let s = source;
|
|
3600
|
+
if (s.startsWith("^")) s = s.slice(1);
|
|
3601
|
+
s = s.replace(/^\(\?<!\[[^\]]+\]\)/, "");
|
|
3602
|
+
let boundary = null;
|
|
3603
|
+
const tail = /(?:\(\?!\[([^\]]+)\]\)|\\b)$/.exec(s);
|
|
3604
|
+
if (tail !== null) {
|
|
3605
|
+
const cls = tail[1];
|
|
3606
|
+
if (cls !== void 0 && cls.startsWith("^")) return null;
|
|
3607
|
+
boundary = cls ?? "_0-9A-Za-z";
|
|
3608
|
+
s = s.slice(0, tail.index);
|
|
3609
|
+
}
|
|
3610
|
+
const grouped = /^\(\?:([^()]*)\)$/.exec(s);
|
|
3611
|
+
if (grouped !== null) s = grouped[1];
|
|
3612
|
+
if (s.endsWith("$")) s = s.slice(0, -1);
|
|
3613
|
+
if (!WORDS_RE.test(s)) return null;
|
|
3614
|
+
const words = s.split("|");
|
|
3615
|
+
if (boundary === null && words.length < VOCABULARY_MIN_WORDS) return null;
|
|
3616
|
+
return { words, boundary };
|
|
3617
|
+
}
|
|
3618
|
+
function boundaryMatches(boundary, ch2) {
|
|
3619
|
+
const cp = ch2.codePointAt(0);
|
|
3620
|
+
for (const m of charClassMembers(boundary)) {
|
|
3621
|
+
if (m === "\\w") {
|
|
3622
|
+
if (/[A-Za-z0-9_]/.test(ch2)) return true;
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
if (m === "\\d") {
|
|
3626
|
+
if (/[0-9]/.test(ch2)) return true;
|
|
3627
|
+
continue;
|
|
3628
|
+
}
|
|
3629
|
+
if (m === "\\s") {
|
|
3630
|
+
if (/\s/.test(ch2)) return true;
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
const range = /^(.|\\u[0-9a-f]{4})-(.|\\u[0-9a-f]{4})$/.exec(m);
|
|
3634
|
+
if (range !== null) {
|
|
3635
|
+
const dec = (t) => t.startsWith("\\u") ? parseInt(t.slice(2), 16) : t.codePointAt(0);
|
|
3636
|
+
if (cp >= dec(range[1]) && cp <= dec(range[2])) return true;
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
if (m === ch2) return true;
|
|
3640
|
+
}
|
|
3641
|
+
return false;
|
|
3642
|
+
}
|
|
3643
|
+
function keywordAlternationHazards(words, boundary) {
|
|
3644
|
+
const out = [];
|
|
3645
|
+
for (let i = 0; i < words.length; i++) {
|
|
3646
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
3647
|
+
const a = words[i], b = words[j];
|
|
3648
|
+
if (a.length >= b.length || !b.startsWith(a)) continue;
|
|
3649
|
+
const at = b[a.length];
|
|
3650
|
+
out.push({ shorter: a, longer: b, at, rescuedByBoundary: boundary !== null && boundaryMatches(boundary, at) });
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return out;
|
|
3654
|
+
}
|
|
3655
|
+
function analyzeDuplication(entry, opts) {
|
|
3656
|
+
return analyzeDuplicationRules([[ruleNameOf2(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
|
|
3657
|
+
}
|
|
3658
|
+
function assertAnalyzable(ruleMap) {
|
|
3659
|
+
for (const [name, rule] of ruleMap) {
|
|
3660
|
+
const def = rule?._def;
|
|
3661
|
+
if (def !== null && typeof def === "object" && typeof def.tag === "string") continue;
|
|
3662
|
+
throw new TypeError(
|
|
3663
|
+
`analyzeDuplicationRules: rule '${name}' is not a combinator (no _def). This analysis walks the COMBINATOR TREE; the value returned by compose()/composeLeaf() is a fused, already-compiled artifact whose entries are parse functions. Pass the rules() map itself (or the map given to compileRuleMap/compileLinkable) instead \u2014 for a composed grammar, analyze each piece's own rules() map.`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
function analyzeDuplicationRules(ruleMap, opts) {
|
|
3668
|
+
assertAnalyzable(ruleMap);
|
|
3669
|
+
const minSize = opts?.minSize ?? 3;
|
|
3670
|
+
const maxFindings = opts?.maxFindings ?? 25;
|
|
3671
|
+
const interner = new Interner();
|
|
3672
|
+
const shapeCache = /* @__PURE__ */ new Map();
|
|
3673
|
+
const sizeCache = /* @__PURE__ */ new Map();
|
|
3674
|
+
const shapeOf = (p) => {
|
|
3675
|
+
const hit = shapeCache.get(p);
|
|
3676
|
+
if (hit !== void 0) return hit;
|
|
3677
|
+
const d = p._def;
|
|
3678
|
+
shapeCache.set(p, interner.hole);
|
|
3679
|
+
const kids = childrenOf(d);
|
|
3680
|
+
const id = interner.of(`${payloadKey(p, d)}|${kids.map(shapeOf).join(",")}`);
|
|
3681
|
+
shapeCache.set(p, id);
|
|
3682
|
+
return id;
|
|
3683
|
+
};
|
|
3684
|
+
const sizeOf = (p) => {
|
|
3685
|
+
const hit = sizeCache.get(p);
|
|
3686
|
+
if (hit !== void 0) return hit;
|
|
3687
|
+
sizeCache.set(p, 1);
|
|
3688
|
+
let n = 1;
|
|
3689
|
+
for (const k of childrenOf(p._def)) n += sizeOf(k);
|
|
3690
|
+
sizeCache.set(p, n);
|
|
3691
|
+
return n;
|
|
3692
|
+
};
|
|
3693
|
+
const visited = /* @__PURE__ */ new Map();
|
|
3694
|
+
const visit = (p, rule, path2, enclosingNode, enclosingReducer) => {
|
|
3695
|
+
if (visited.has(p)) return;
|
|
3696
|
+
const d = p._def;
|
|
3697
|
+
const own = ruleNameOf2(p);
|
|
3698
|
+
const here = own !== void 0 && own !== rule ? { rule: own, path: "" } : { rule, path: path2 };
|
|
3699
|
+
visited.set(p, { p, d, site: here, shape: shapeOf(p), size: sizeOf(p), enclosingNode, enclosingReducer });
|
|
3700
|
+
const inner = d.tag === "node" ? d.type ?? "(anonymous)" : enclosingNode;
|
|
3701
|
+
const innerReducer = enclosingReducer || d.tag === "node" && (d.build !== void 0 || d.buildSrc !== void 0) || d.tag === "transform" || d.tag === "leaf";
|
|
3702
|
+
const kids = childrenOf(d);
|
|
3703
|
+
kids.forEach((k, i) => {
|
|
3704
|
+
const seg = segment(d, i);
|
|
3705
|
+
visit(k, here.rule, here.path === "" ? seg : `${here.path} \u203A ${seg}`, inner, innerReducer);
|
|
3706
|
+
});
|
|
3707
|
+
if (d.tag === "lazy") {
|
|
3708
|
+
try {
|
|
3709
|
+
const target = d.thunk();
|
|
3710
|
+
visit(target, ruleNameOf2(target) ?? ruleNameOf2(p) ?? here.rule, "", null, false);
|
|
3711
|
+
} catch {
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
for (const [name, root] of ruleMap) visit(root, ruleNameOf2(root) ?? name, "", null, false);
|
|
3716
|
+
const accept = new Set(opts?.accept ?? []);
|
|
3717
|
+
const used = /* @__PURE__ */ new Set();
|
|
3718
|
+
const keep = (f) => {
|
|
3719
|
+
if (!accept.has(f.id)) return true;
|
|
3720
|
+
used.add(f.id);
|
|
3721
|
+
return false;
|
|
3722
|
+
};
|
|
3723
|
+
const duplicates = findExactDuplicates(visited, minSize);
|
|
3724
|
+
const nearDuplicates = findNearDuplicates(visited, interner, shapeOf, minSize);
|
|
3725
|
+
const regexFragments = findRegexFragments(visited);
|
|
3726
|
+
const regexClasses = findRegexClasses(visited);
|
|
3727
|
+
const overlaps = findOverlaps(visited, shapeOf, opts?.resolveRef);
|
|
3728
|
+
const rewrites = findRewrites(visited, shapeOf);
|
|
3729
|
+
const divergentNodes = findDivergentNodes(visited, shapeOf);
|
|
3730
|
+
const structureLoss = findStructureLoss(visited, opts?.resolveRef);
|
|
3731
|
+
const keywordRegexes = findKeywordRegexes(visited);
|
|
3732
|
+
const subsume = (fs2) => {
|
|
3733
|
+
const covered = /* @__PURE__ */ new Set();
|
|
3734
|
+
const surviving = /* @__PURE__ */ new Set();
|
|
3735
|
+
for (const f of [...fs2].sort((a, b) => b.savings - a.savings)) {
|
|
3736
|
+
if (f._members.every((m) => covered.has(m))) continue;
|
|
3737
|
+
surviving.add(f.id);
|
|
3738
|
+
for (const m of f._members) markSubtree(m, covered);
|
|
3739
|
+
}
|
|
3740
|
+
return surviving;
|
|
3741
|
+
};
|
|
3742
|
+
const survivingExact = subsume(duplicates);
|
|
3743
|
+
const survivingNear = subsume(nearDuplicates);
|
|
3744
|
+
const report = {
|
|
3745
|
+
duplicates: duplicates.filter((f) => survivingExact.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
|
|
3746
|
+
nearDuplicates: nearDuplicates.filter((f) => survivingNear.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
|
|
3747
|
+
regexFragments: regexFragments.filter(keep).slice(0, maxFindings),
|
|
3748
|
+
regexClasses: regexClasses.filter(keep).slice(0, maxFindings),
|
|
3749
|
+
overlaps: overlaps.filter(keep).slice(0, maxFindings),
|
|
3750
|
+
rewrites: rewrites.filter(keep).slice(0, maxFindings),
|
|
3751
|
+
divergentNodes: divergentNodes.filter(keep).slice(0, maxFindings),
|
|
3752
|
+
structureLoss: structureLoss.filter(keep).slice(0, maxFindings),
|
|
3753
|
+
keywordRegexes: keywordRegexes.filter(keep).slice(0, maxFindings),
|
|
3754
|
+
acceptedUnused: [...accept].filter((id) => !used.has(id)),
|
|
3755
|
+
stats: { rules: ruleMap.length, nodes: visited.size, shapes: new Set([...visited.values()].map((v2) => v2.shape)).size }
|
|
3756
|
+
};
|
|
3757
|
+
return report;
|
|
3758
|
+
}
|
|
3759
|
+
function markSubtree(p, into) {
|
|
3760
|
+
if (into.has(p)) return;
|
|
3761
|
+
into.add(p);
|
|
3762
|
+
for (const k of childrenOf(p._def)) markSubtree(k, into);
|
|
3763
|
+
}
|
|
3764
|
+
var stripMembers = (f) => {
|
|
3765
|
+
const { _members, ...rest } = f;
|
|
3766
|
+
void _members;
|
|
3767
|
+
return rest;
|
|
3768
|
+
};
|
|
3769
|
+
function findExactDuplicates(visited, minSize) {
|
|
3770
|
+
const byShape = /* @__PURE__ */ new Map();
|
|
3771
|
+
for (const v2 of visited.values()) {
|
|
3772
|
+
if (v2.size < minSize) continue;
|
|
3773
|
+
const list = byShape.get(v2.shape);
|
|
3774
|
+
if (list === void 0) byShape.set(v2.shape, [v2]);
|
|
3775
|
+
else list.push(v2);
|
|
3776
|
+
}
|
|
3777
|
+
const out = [];
|
|
3778
|
+
for (const group of byShape.values()) {
|
|
3779
|
+
if (group.length < 2) continue;
|
|
3780
|
+
const size = group[0].size;
|
|
3781
|
+
out.push({
|
|
3782
|
+
kind: "exact-duplicate",
|
|
3783
|
+
id: `exact-duplicate:${siteToString(group[0].site)}`,
|
|
3784
|
+
size,
|
|
3785
|
+
count: group.length,
|
|
3786
|
+
savings: (group.length - 1) * size,
|
|
3787
|
+
shape: clamp(render(group[0].p)),
|
|
3788
|
+
sites: group.map((g) => g.site),
|
|
3789
|
+
suggestion: `${group.length} DISTINCT instances build this identical ${size}-node shape. Bind it to one const and reference that \u2014 hoisting removes ${(group.length - 1) * size} nodes and makes a future edit land in one place instead of ${group.length}.`,
|
|
3790
|
+
_members: group.map((g) => g.p)
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
return out.sort((a, b) => b.savings - a.savings);
|
|
3794
|
+
}
|
|
3795
|
+
function findNearDuplicates(visited, interner, shapeOf, minSize) {
|
|
3796
|
+
const holeCache = /* @__PURE__ */ new Map();
|
|
3797
|
+
const holesOf = (p) => {
|
|
3798
|
+
const hit = holeCache.get(p);
|
|
3799
|
+
if (hit !== void 0) return hit;
|
|
3800
|
+
holeCache.set(p, []);
|
|
3801
|
+
const d = p._def;
|
|
3802
|
+
const kids = childrenOf(d);
|
|
3803
|
+
const kidShapes = kids.map(shapeOf);
|
|
3804
|
+
const payload = payloadKey(p, d);
|
|
3805
|
+
const out2 = [];
|
|
3806
|
+
kids.forEach((k, i) => {
|
|
3807
|
+
for (const inner of [interner.hole, ...holesOf(k)]) {
|
|
3808
|
+
const arr = kidShapes.slice();
|
|
3809
|
+
arr[i] = inner;
|
|
3810
|
+
out2.push(interner.of(`${payload}|${arr.join(",")}`));
|
|
3811
|
+
}
|
|
3812
|
+
});
|
|
3813
|
+
holeCache.set(p, out2);
|
|
3814
|
+
return out2;
|
|
3815
|
+
};
|
|
3816
|
+
const byHole = /* @__PURE__ */ new Map();
|
|
3817
|
+
for (const v2 of visited.values()) {
|
|
3818
|
+
if (v2.size < minSize) continue;
|
|
3819
|
+
for (const h of holesOf(v2.p)) {
|
|
3820
|
+
const list = byHole.get(h);
|
|
3821
|
+
if (list === void 0) byHole.set(h, [v2]);
|
|
3822
|
+
else list.push(v2);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
const out = [];
|
|
3826
|
+
const seenGroups = /* @__PURE__ */ new Set();
|
|
3827
|
+
for (const group of byHole.values()) {
|
|
3828
|
+
if (group.length < 2) continue;
|
|
3829
|
+
const shapes = new Set(group.map((g) => g.shape));
|
|
3830
|
+
if (shapes.size < 2) continue;
|
|
3831
|
+
const perShape = /* @__PURE__ */ new Map();
|
|
3832
|
+
for (const g of group) if (!perShape.has(g.shape)) perShape.set(g.shape, g);
|
|
3833
|
+
const members = [...perShape.values()];
|
|
3834
|
+
const key = members.map((m) => m.shape).sort((a, b) => a - b).join(",");
|
|
3835
|
+
if (seenGroups.has(key)) continue;
|
|
3836
|
+
seenGroups.add(key);
|
|
3837
|
+
const slot = findDivergentSlot(members.map((m) => m.p), shapeOf);
|
|
3838
|
+
if (slot === null) continue;
|
|
3839
|
+
const sharedSize = Math.min(...members.map((m) => m.size)) - slot.slotSize;
|
|
3840
|
+
if (sharedSize < minSize) continue;
|
|
3841
|
+
out.push({
|
|
3842
|
+
kind: "near-duplicate",
|
|
3843
|
+
id: `near-duplicate:${siteToString(members[0].site)}`,
|
|
3844
|
+
sharedSize,
|
|
3845
|
+
count: members.length,
|
|
3846
|
+
savings: (members.length - 1) * sharedSize,
|
|
3847
|
+
shape: clamp(renderWithHole(members[0].p, slot.path)),
|
|
3848
|
+
slotPath: slot.path.map((i, k) => segment(slot.defs[k], i)).join(" \u203A "),
|
|
3849
|
+
variants: slot.variants.map((v2) => clamp(render(v2))),
|
|
3850
|
+
sites: members.map((m) => m.site),
|
|
3851
|
+
suggestion: `${members.length} productions share this ${sharedSize}-node scaffolding and differ ONLY at \`${slot.path.map((i, k) => segment(slot.defs[k], i)).join(" \u203A ")}\`. That is one production whose varying slot is a \`choice(${slot.variants.map((v2) => clamp(render(v2, 1))).join(", ")})\` \u2014 not ${members.length} copies of the scaffolding. Re-check the AST: a single production emits one node type where the clones may emit several.`,
|
|
3852
|
+
_members: members.map((m) => m.p)
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
return out.sort((a, b) => b.savings - a.savings);
|
|
3856
|
+
}
|
|
3857
|
+
function findDivergentSlot(members, shapeOf) {
|
|
3858
|
+
const path2 = [];
|
|
3859
|
+
const defs = [];
|
|
3860
|
+
let cur = [...members];
|
|
3861
|
+
for (let guard2 = 0; guard2 < 64; guard2++) {
|
|
3862
|
+
const d0 = cur[0]._def;
|
|
3863
|
+
const kidLists = cur.map((c) => childrenOf(c._def));
|
|
3864
|
+
const arity = kidLists[0].length;
|
|
3865
|
+
if (kidLists.some((k) => k.length !== arity) || arity === 0) break;
|
|
3866
|
+
const diff = [];
|
|
3867
|
+
for (let i2 = 0; i2 < arity; i2++) {
|
|
3868
|
+
const s = shapeOf(kidLists[0][i2]);
|
|
3869
|
+
if (kidLists.some((k) => shapeOf(k[i2]) !== s)) diff.push(i2);
|
|
3870
|
+
}
|
|
3871
|
+
if (diff.length !== 1) break;
|
|
3872
|
+
const i = diff[0];
|
|
3873
|
+
path2.push(i);
|
|
3874
|
+
defs.push(d0);
|
|
3875
|
+
cur = kidLists.map((k) => k[i]);
|
|
3876
|
+
const nextArity = childrenOf(cur[0]._def).length;
|
|
3877
|
+
const samePayload = cur.every((c) => payloadKey(c, c._def) === payloadKey(cur[0], cur[0]._def));
|
|
3878
|
+
if (!samePayload || nextArity === 0) break;
|
|
3879
|
+
}
|
|
3880
|
+
if (path2.length === 0) return null;
|
|
3881
|
+
let slotSize = 0;
|
|
3882
|
+
const sizeOf = (p) => {
|
|
3883
|
+
let n = 1;
|
|
3884
|
+
for (const k of childrenOf(p._def)) n += sizeOf(k);
|
|
3885
|
+
return n;
|
|
3886
|
+
};
|
|
3887
|
+
for (const c of cur) slotSize = Math.max(slotSize, sizeOf(c));
|
|
3888
|
+
return { path: path2, defs, variants: cur, slotSize };
|
|
3889
|
+
}
|
|
3890
|
+
function renderWithHole(p, path2, depth = 0) {
|
|
3891
|
+
if (depth === path2.length) return "\u2039slot\u203A";
|
|
3892
|
+
const d = p._def;
|
|
3893
|
+
const kids = childrenOf(d);
|
|
3894
|
+
const idx = path2[depth];
|
|
3895
|
+
const parts = kids.map((k, i) => i === idx ? renderWithHole(k, path2, depth + 1) : render(k, 2));
|
|
3896
|
+
switch (d.tag) {
|
|
3897
|
+
case "sequence":
|
|
3898
|
+
return `sequence(${parts.join(", ")})`;
|
|
3899
|
+
case "choice":
|
|
3900
|
+
return `choice(${parts.join(", ")})`;
|
|
3901
|
+
case "node":
|
|
3902
|
+
return `node(${d.type ? `'${d.type}', ` : ""}${parts[0] ?? ""})`;
|
|
3903
|
+
default:
|
|
3904
|
+
return `${d.tag}(${parts.join(", ")})`;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
var MAX_RUN = 32;
|
|
3908
|
+
function findRegexFragments(visited) {
|
|
3909
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
3910
|
+
for (const v2 of visited.values()) {
|
|
3911
|
+
const d = v2.d;
|
|
3912
|
+
if (d.tag !== "regex") continue;
|
|
3913
|
+
const runs = /* @__PURE__ */ new Set();
|
|
3914
|
+
for (const branches of alternationGroups(d.source)) {
|
|
3915
|
+
if (branches.length >= 2) runs.add(branches.join("|"));
|
|
3916
|
+
for (let i = 0; i < branches.length; i++) {
|
|
3917
|
+
for (let n = 2; n <= Math.min(MAX_RUN, branches.length - i); n++) {
|
|
3918
|
+
const run2 = branches.slice(i, i + n).join("|");
|
|
3919
|
+
if (run2.length < 4) continue;
|
|
3920
|
+
runs.add(run2);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
for (const run2 of runs) {
|
|
3925
|
+
let e = byRun.get(run2);
|
|
3926
|
+
if (e === void 0) {
|
|
3927
|
+
e = { occ: [], nodes: /* @__PURE__ */ new Set(), branches: run2.split("|").length };
|
|
3928
|
+
byRun.set(run2, e);
|
|
3929
|
+
}
|
|
3930
|
+
if (e.nodes.has(v2.p)) continue;
|
|
3931
|
+
e.nodes.add(v2.p);
|
|
3932
|
+
e.occ.push({ site: v2.site, source: d.source });
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
const candidates = [...byRun.entries()].filter(([, e]) => e.occ.length >= 2).map(([run2, e]) => ({ run: run2, e, savings: (e.occ.length - 1) * run2.length })).sort((a, b) => b.e.occ.length - a.e.occ.length || b.savings - a.savings || b.run.length - a.run.length);
|
|
3936
|
+
const kept = [];
|
|
3937
|
+
for (const c of candidates) {
|
|
3938
|
+
if (kept.some((k) => k.e.occ.length >= c.e.occ.length && `|${k.run}|`.includes(`|${c.run}|`))) continue;
|
|
3939
|
+
kept.push(c);
|
|
3940
|
+
}
|
|
3941
|
+
return kept.map(({ run: run2, e }) => ({
|
|
3942
|
+
kind: "regex-fragment",
|
|
3943
|
+
id: `regex-fragment:${run2}`,
|
|
3944
|
+
fragment: run2,
|
|
3945
|
+
branches: e.branches,
|
|
3946
|
+
count: e.occ.length,
|
|
3947
|
+
savings: (e.occ.length - 1) * run2.length,
|
|
3948
|
+
sources: e.occ.map((o) => `/${o.source}/`),
|
|
3949
|
+
sites: e.occ.map((o) => o.site),
|
|
3950
|
+
suggestion: `this ${e.branches}-branch alternation is re-spelled in ${e.occ.length} DISTINCT regex() terminals. Hoist it to one shared source string and interpolate it (\`new RegExp(\\\`\u2026\\\${OP}\u2026\\\`)\`) or lift the shared part into its own terminal \u2014 an edit to the operator set currently has to land in ${e.occ.length} places, and the analysis cannot tell you when one of them drifts.`
|
|
3951
|
+
}));
|
|
3952
|
+
}
|
|
3953
|
+
function extractCharClasses(src) {
|
|
3954
|
+
const out = [];
|
|
3955
|
+
let i = 0;
|
|
3956
|
+
while (i < src.length) {
|
|
3957
|
+
const c = src[i];
|
|
3958
|
+
if (c === "\\") {
|
|
3959
|
+
i += 2;
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
if (c !== "[") {
|
|
3963
|
+
i++;
|
|
3964
|
+
continue;
|
|
3965
|
+
}
|
|
3966
|
+
let j = i + 1;
|
|
3967
|
+
if (src[j] === "^") j++;
|
|
3968
|
+
if (src[j] === "]") j++;
|
|
3969
|
+
while (j < src.length && src[j] !== "]") {
|
|
3970
|
+
if (src[j] === "\\") j++;
|
|
3971
|
+
j++;
|
|
3972
|
+
}
|
|
3973
|
+
out.push(src.slice(i + 1, j));
|
|
3974
|
+
i = j + 1;
|
|
3975
|
+
}
|
|
3976
|
+
return out;
|
|
3977
|
+
}
|
|
3978
|
+
function charClassMembers(body) {
|
|
3979
|
+
const esc = (ch2) => {
|
|
3980
|
+
const cp = ch2.codePointAt(0);
|
|
3981
|
+
return cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : ch2;
|
|
3982
|
+
};
|
|
3983
|
+
const atoms = [];
|
|
3984
|
+
let i = 0;
|
|
3985
|
+
while (i < body.length) {
|
|
3986
|
+
if (body[i] === "\\") {
|
|
3987
|
+
const m = /^\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2}))/.exec(body.slice(i));
|
|
3988
|
+
if (m !== null) {
|
|
3989
|
+
const cp = parseInt(m[1] ?? m[2] ?? m[3], 16);
|
|
3990
|
+
atoms.push(cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : String.fromCodePoint(cp));
|
|
3991
|
+
i += m[0].length;
|
|
3992
|
+
continue;
|
|
3993
|
+
}
|
|
3994
|
+
atoms.push(body.slice(i, i + 2));
|
|
3995
|
+
i += 2;
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
atoms.push(esc(body[i]));
|
|
3999
|
+
i++;
|
|
4000
|
+
}
|
|
4001
|
+
const members = [];
|
|
4002
|
+
for (let k = 0; k < atoms.length; k++) {
|
|
4003
|
+
if (k + 2 < atoms.length && atoms[k + 1] === "-" && atoms[k + 2] !== void 0) {
|
|
4004
|
+
members.push(`${atoms[k]}-${atoms[k + 2]}`);
|
|
4005
|
+
k += 2;
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
members.push(atoms[k]);
|
|
4009
|
+
}
|
|
4010
|
+
return members;
|
|
4011
|
+
}
|
|
4012
|
+
var CLASS_MIN_MEMBERS = 2;
|
|
4013
|
+
function findRegexClasses(visited) {
|
|
4014
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
4015
|
+
for (const v2 of visited.values()) {
|
|
4016
|
+
if (v2.d.tag !== "regex") continue;
|
|
4017
|
+
for (const body of new Set(extractCharClasses(v2.d.source))) {
|
|
4018
|
+
const members = charClassMembers(body);
|
|
4019
|
+
if (members.length < CLASS_MIN_MEMBERS) continue;
|
|
4020
|
+
const key = [...new Set(members)].sort().join("");
|
|
4021
|
+
let s = spellings.get(key);
|
|
4022
|
+
if (s === void 0) {
|
|
4023
|
+
s = { source: body, members, key, nodes: /* @__PURE__ */ new Set(), sites: [] };
|
|
4024
|
+
spellings.set(key, s);
|
|
4025
|
+
}
|
|
4026
|
+
if (s.nodes.has(v2.p)) continue;
|
|
4027
|
+
s.nodes.add(v2.p);
|
|
4028
|
+
s.sites.push(v2.site);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
const MAX_DELTA = 3;
|
|
4032
|
+
const MIN_JACCARD = 0.6;
|
|
4033
|
+
const all = [...spellings.values()];
|
|
4034
|
+
const parent = all.map((_, i) => i);
|
|
4035
|
+
const find = (i) => {
|
|
4036
|
+
while (parent[i] !== i) {
|
|
4037
|
+
parent[i] = parent[parent[i]];
|
|
4038
|
+
i = parent[i];
|
|
4039
|
+
}
|
|
4040
|
+
return i;
|
|
4041
|
+
};
|
|
4042
|
+
const union2 = (a, b) => {
|
|
4043
|
+
const ra = find(a), rb = find(b);
|
|
4044
|
+
if (ra !== rb) parent[ra] = rb;
|
|
4045
|
+
};
|
|
4046
|
+
for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) {
|
|
4047
|
+
if (all[i].members.includes("^") !== all[j].members.includes("^")) continue;
|
|
4048
|
+
const A = new Set(all[i].members), B = new Set(all[j].members);
|
|
4049
|
+
let shared = 0;
|
|
4050
|
+
for (const m of A) if (B.has(m)) shared++;
|
|
4051
|
+
const delta = A.size - shared + (B.size - shared);
|
|
4052
|
+
const jaccard = shared / (shared + delta);
|
|
4053
|
+
if (shared >= 2 && delta > 0 && delta <= MAX_DELTA && jaccard >= MIN_JACCARD) union2(i, j);
|
|
4054
|
+
}
|
|
4055
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
4056
|
+
all.forEach((_, i) => {
|
|
4057
|
+
const r = find(i);
|
|
4058
|
+
const list = clusters.get(r);
|
|
4059
|
+
if (list === void 0) clusters.set(r, [i]);
|
|
4060
|
+
else list.push(i);
|
|
4061
|
+
});
|
|
4062
|
+
const out = [];
|
|
4063
|
+
for (const idxs of clusters.values()) {
|
|
4064
|
+
const members = idxs.map((i) => all[i]).sort((a, b) => b.nodes.size - a.nodes.size);
|
|
4065
|
+
const total = members.reduce((n, m) => n + m.nodes.size, 0);
|
|
4066
|
+
const drifted = members.length > 1;
|
|
4067
|
+
if (!drifted && total < 2) continue;
|
|
4068
|
+
const base = new Set(members[0].members);
|
|
4069
|
+
const variants = members.map((m) => {
|
|
4070
|
+
const mine = new Set(m.members);
|
|
4071
|
+
const plus = [...mine].filter((x) => !base.has(x));
|
|
4072
|
+
const minus = [...base].filter((x) => !mine.has(x));
|
|
4073
|
+
return {
|
|
4074
|
+
source: m.source,
|
|
4075
|
+
delta: m === members[0] ? "(reference)" : `${plus.map((p) => `+\`${p}\``).join(" ")}${plus.length > 0 && minus.length > 0 ? " " : ""}${minus.map((p) => `-\`${p}\``).join(" ")}` || "(same members, different spelling)",
|
|
4076
|
+
count: m.nodes.size,
|
|
4077
|
+
sites: m.sites
|
|
4078
|
+
};
|
|
4079
|
+
});
|
|
4080
|
+
const bmpCeiling = members.some((m) => m.members.some((x) => x.endsWith("-\\uffff")));
|
|
4081
|
+
out.push({
|
|
4082
|
+
kind: "regex-class",
|
|
4083
|
+
id: `regex-class:${members[0].key}`,
|
|
4084
|
+
canonical: members[0].source,
|
|
4085
|
+
variants,
|
|
4086
|
+
count: total,
|
|
4087
|
+
drifted,
|
|
4088
|
+
bmpCeiling,
|
|
4089
|
+
suggestion: [
|
|
4090
|
+
drifted ? `${members.length} NEARLY-identical spellings of one class, across ${total} regex() terminals. They differ by ${variants.slice(1).map((v2) => v2.delta).join("; ")} \u2014 which is drift, not intent, unless every difference is deliberate. Two classes this close cannot be told apart by reading, so one of them is probably wrong and the tests cannot say which.` : `this class is re-spelled in ${total} DISTINCT regex() terminals. Extract it once \u2014 a named sub-leaf, or an interpolated source constant \u2014 so a range fix lands in one place. Re-spelling is how the near-identical variants elsewhere in this report came to exist.`,
|
|
4091
|
+
`Prefer the shared recognition surface where one already covers this class rather than a local extraction.`,
|
|
4092
|
+
bmpCeiling ? `The class tops out at U+FFFF, so astral-plane characters (emoji, many CJK extensions) fall OUTSIDE it. Widening the ceiling is exactly the kind of edit that has to reach every spelling.` : "",
|
|
4093
|
+
`Single-regex hygiene (duplicate class members, obscure ranges, a missing \`u\` flag) is \`eslint-plugin-regexp\`'s job and is not duplicated here \u2014 it sees one regex at a time, which is why it cannot see this.`
|
|
4094
|
+
].filter((s) => s !== "").join(" ")
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
return out.sort((a, b) => Number(b.drifted) - Number(a.drifted) || b.count - a.count);
|
|
4098
|
+
}
|
|
4099
|
+
function intersects3(a, b) {
|
|
4100
|
+
if (a.kind === "any" || b.kind === "any") return true;
|
|
4101
|
+
if (a.kind === "empty" || b.kind === "empty") return false;
|
|
4102
|
+
for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
|
|
4103
|
+
return false;
|
|
4104
|
+
}
|
|
4105
|
+
function intersection2(a, b) {
|
|
4106
|
+
if (a.kind === "any") return b;
|
|
4107
|
+
if (b.kind === "any") return a;
|
|
4108
|
+
if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
|
|
4109
|
+
const ranges = [];
|
|
4110
|
+
for (const ra of a.ranges) for (const rb of b.ranges) {
|
|
4111
|
+
const lo = Math.max(ra.lo, rb.lo);
|
|
4112
|
+
const hi = Math.min(ra.hi, rb.hi);
|
|
4113
|
+
if (lo <= hi) ranges.push({ lo, hi });
|
|
4114
|
+
}
|
|
4115
|
+
return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
|
|
4116
|
+
}
|
|
4117
|
+
function termsOf(arm) {
|
|
4118
|
+
let cur = arm;
|
|
4119
|
+
for (; ; ) {
|
|
4120
|
+
const d2 = cur._def;
|
|
4121
|
+
if (d2.tag === "node" || d2.tag === "transform" || d2.tag === "label" || d2.tag === "attempt" || d2.tag === "leaf") {
|
|
4122
|
+
cur = d2.parser;
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
break;
|
|
4126
|
+
}
|
|
4127
|
+
const d = cur._def;
|
|
4128
|
+
return d.tag === "sequence" ? d.parsers : [cur];
|
|
4129
|
+
}
|
|
4130
|
+
function leadingRegex(arm) {
|
|
4131
|
+
const t = termsOf(arm)[0];
|
|
4132
|
+
return t !== void 0 && t._def.tag === "regex";
|
|
4133
|
+
}
|
|
4134
|
+
function findOverlaps(visited, shapeOf, resolve2) {
|
|
4135
|
+
const out = [];
|
|
4136
|
+
for (const v2 of visited.values()) {
|
|
4137
|
+
const d = v2.d;
|
|
4138
|
+
if (d.tag !== "choice") continue;
|
|
4139
|
+
const arms = d.parsers;
|
|
4140
|
+
const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
|
|
4141
|
+
const strategy = d.strategy;
|
|
4142
|
+
for (let i = 0; i < arms.length; i++) {
|
|
4143
|
+
for (let j = i + 1; j < arms.length; j++) {
|
|
4144
|
+
const A = fs2[i], B = fs2[j];
|
|
4145
|
+
if (A.kind === "any" || B.kind === "any") continue;
|
|
4146
|
+
if (!intersects3(A, B)) continue;
|
|
4147
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4148
|
+
let shared = 0;
|
|
4149
|
+
while (shared < ta.length && shared < tb.length && shapeOf(ta[shared]) === shapeOf(tb[shared])) shared++;
|
|
4150
|
+
const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
|
|
4151
|
+
const regexPair = leadingRegex(arms[i]) && leadingRegex(arms[j]);
|
|
4152
|
+
out.push({
|
|
4153
|
+
kind: "arm-overlap",
|
|
4154
|
+
id: `arm-overlap:${siteToString(v2.site)}:${i}-${j}`,
|
|
4155
|
+
site: v2.site,
|
|
4156
|
+
a: i,
|
|
4157
|
+
b: j,
|
|
4158
|
+
on: intersection2(A, B),
|
|
4159
|
+
sharedLeadingTerms: shared,
|
|
4160
|
+
sharedPrefix: shared > 0 ? clamp(ta.slice(0, shared).map((t) => render(t, 2)).join(", ")) : null,
|
|
4161
|
+
handledByStrategy: handled,
|
|
4162
|
+
regexPair,
|
|
4163
|
+
suggestion: overlapSuggestion(shared, handled, regexPair)
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
return out.sort((a, b) => b.sharedLeadingTerms - a.sharedLeadingTerms);
|
|
4169
|
+
}
|
|
4170
|
+
function overlapSuggestion(shared, handled, regexPair) {
|
|
4171
|
+
if (shared > 0 && handled)
|
|
4172
|
+
return `the arms spell ${shared} identical leading term(s). parseman's \`sharedPrefix\` strategy already recognizes that prefix ONCE at runtime, so this costs nothing at parse time \u2014 it is a READABILITY finding: left-factoring into \`sequence(prefix, choice(\u2026))\` says in the grammar what the compiler had to infer.`;
|
|
4173
|
+
if (shared > 0)
|
|
4174
|
+
return `the arms spell ${shared} identical leading term(s) but do NOT qualify for the \`sharedPrefix\` strategy (it needs every arm to be a bare \`sequence\` starting with the same concrete terminal). Left-factor into \`sequence(prefix, choice(\u2026))\` \u2014 that both removes the re-spelling and lets the inner choice dispatch on its own first char.`;
|
|
4175
|
+
if (regexPair)
|
|
4176
|
+
return `two regex() arms whose character classes intersect. Nothing dispatches between them: the first is tried, and on failure the second re-scans the same position. Two regexes that can start on the same char are usually one terminal \u2014 merge them, or make the distinction explicit with a leading literal.`;
|
|
4177
|
+
return `the arms can start on the same character, so the choice cannot dispatch between them: on those chars the first arm is entered speculatively and rolled back before the second is tried. Give the arms disjoint leading terminals, or left-factor the common part out.`;
|
|
4178
|
+
}
|
|
4179
|
+
var PERF_ARM = "removes a speculative arm: on every input position reaching this choice, the deleted arm no longer costs a ctx save, a child-array push and a rollback before the surviving arm is tried. The saving scales with how often the choice is reached and how much the doomed arm consumes before failing.";
|
|
4180
|
+
var PERF_NONE = "";
|
|
4181
|
+
function findRewrites(visited, shapeOf) {
|
|
4182
|
+
const out = [];
|
|
4183
|
+
const push = (f, at) => {
|
|
4184
|
+
const site = siteToString(f.site);
|
|
4185
|
+
out.push({ kind: "rewrite", id: `rewrite:${f.rewrite}:${site}${at === void 0 ? "" : `:${at}`}`, ...f });
|
|
4186
|
+
};
|
|
4187
|
+
for (const v2 of visited.values()) {
|
|
4188
|
+
const d = v2.d;
|
|
4189
|
+
const site = v2.site;
|
|
4190
|
+
if (d.tag === "optional" || d.tag === "many" || d.tag === "oneOrMore") {
|
|
4191
|
+
const inner = d.parser;
|
|
4192
|
+
const id = inner._def;
|
|
4193
|
+
const collapsed = collapseRepeat(d, id);
|
|
4194
|
+
if (collapsed !== null) {
|
|
4195
|
+
push({
|
|
4196
|
+
rewrite: "idempotent-nesting",
|
|
4197
|
+
site,
|
|
4198
|
+
from: clamp(render(v2.p, 2)),
|
|
4199
|
+
to: collapsed,
|
|
4200
|
+
astNeutral: false,
|
|
4201
|
+
perf: "the outer wrapper still runs its own bookkeeping (and, for `many(optional(X))`, guards a zero-width iteration on every pass) for a nullability the inner combinator already has.",
|
|
4202
|
+
bug: id.tag === "optional" && d.tag === "many",
|
|
4203
|
+
suggestion: `\`${clamp(render(v2.p, 2))}\` is exactly \`${collapsed}\`. ${id.tag === "optional" && d.tag === "many" ? "The nested form is also a zero-width-iteration hazard: the inner `optional` succeeds consuming nothing, which the repeat has to detect and break on every pass." : ""} Candidate \u2014 verify AST identity: the collapsed form produces ONE level of array/undefined where the nested form produces two.`
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
if ((d.tag === "choice" || d.tag === "sequence") && d.parsers.length === 1) {
|
|
4208
|
+
push({
|
|
4209
|
+
rewrite: "single-element",
|
|
4210
|
+
site,
|
|
4211
|
+
from: clamp(render(v2.p, 2)),
|
|
4212
|
+
to: d.tag === "choice" ? clamp(render(d.parsers[0], 2)) : `${clamp(render(d.parsers[0], 2))} (note: sequence() of one still wraps its value in a 1-tuple)`,
|
|
4213
|
+
astNeutral: d.tag === "choice",
|
|
4214
|
+
perf: d.tag === "choice" ? "a one-arm choice still emits its dispatch scaffolding." : PERF_NONE,
|
|
4215
|
+
bug: false,
|
|
4216
|
+
suggestion: d.tag === "choice" ? "a `choice` with one arm IS that arm \u2014 the wrapper only adds dispatch scaffolding. Safe to unwrap (choice forwards its arm value unchanged)." : "a `sequence` with one element only exists to build a 1-tuple. If nothing reads that tuple, drop the wrapper \u2014 CANDIDATE, verify AST identity first."
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
if (d.tag !== "choice") continue;
|
|
4220
|
+
const arms = d.parsers;
|
|
4221
|
+
const armShapes = arms.map(shapeOf);
|
|
4222
|
+
const strategy = d.strategy;
|
|
4223
|
+
for (let i = 0; i < arms.length; i++) for (let j = i + 1; j < arms.length; j++) {
|
|
4224
|
+
if (armShapes[i] !== armShapes[j]) continue;
|
|
4225
|
+
push({
|
|
4226
|
+
rewrite: "duplicate-arm",
|
|
4227
|
+
site,
|
|
4228
|
+
from: `choice(\u2026 arm[${i}] \u2026, arm[${j}] \u2026)`,
|
|
4229
|
+
to: `delete arm[${j}]`,
|
|
4230
|
+
astNeutral: true,
|
|
4231
|
+
perf: PERF_ARM,
|
|
4232
|
+
bug: true,
|
|
4233
|
+
suggestion: `arm[${i}] and arm[${j}] are the SAME shape (\`${clamp(render(arms[i], 2))}\`). Under ordered choice arm[${i}] always wins, so arm[${j}] is unreachable \u2014 delete it. This is AST-neutral by construction: an arm that can never be selected contributes nothing to any parse.`
|
|
4234
|
+
}, `${i}-${j}`);
|
|
4235
|
+
}
|
|
4236
|
+
for (let i = 0; i < arms.length; i++) for (let j = 0; j < arms.length; j++) {
|
|
4237
|
+
if (i >= j) continue;
|
|
4238
|
+
if (armShapes[i] === armShapes[j]) continue;
|
|
4239
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4240
|
+
if (ta.length === 0 || ta.length >= tb.length) continue;
|
|
4241
|
+
let k = 0;
|
|
4242
|
+
while (k < ta.length && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
|
|
4243
|
+
if (k !== ta.length) continue;
|
|
4244
|
+
push({
|
|
4245
|
+
rewrite: "shadowed-arm",
|
|
4246
|
+
site,
|
|
4247
|
+
from: `choice(\u2026 arm[${i}]=${clamp(render(arms[i], 2))}, \u2026 arm[${j}]=${clamp(render(arms[j], 2))} \u2026)`,
|
|
4248
|
+
to: `arm[${j}] is unreachable`,
|
|
4249
|
+
astNeutral: true,
|
|
4250
|
+
perf: PERF_ARM,
|
|
4251
|
+
bug: true,
|
|
4252
|
+
suggestion: `arm[${i}] is a strict prefix of arm[${j}] and comes FIRST. Whenever arm[${j}] would match, arm[${i}] also matches, and ordered choice commits to it \u2014 so arm[${j}] can never be selected. Either swap them (longest first) or fold arm[${j}]'s extra terms into arm[${i}] as \`optional(\u2026)\`. Verify by hand where a \`not()\`/\`gate()\` in arm[${i}] makes its success input-dependent in a way this static check cannot see.`
|
|
4253
|
+
}, `${i}-${j}`);
|
|
4254
|
+
}
|
|
4255
|
+
for (let i = 0; i + 1 < arms.length; i++) {
|
|
4256
|
+
const j = i + 1;
|
|
4257
|
+
const longer = termsOf(arms[i]), shorter = termsOf(arms[j]);
|
|
4258
|
+
const ordered = detectOptionalTerm(longer, shorter, shapeOf);
|
|
4259
|
+
const reversed = ordered === null ? detectOptionalTerm(shorter, longer, shapeOf) : null;
|
|
4260
|
+
const hit = ordered ?? reversed;
|
|
4261
|
+
if (hit === null) continue;
|
|
4262
|
+
const nodeWrapped = v2.enclosingNode ?? wrappingNodeType(arms[i]) ?? wrappingNodeType(arms[j]);
|
|
4263
|
+
const rest = hit.rest.map((t) => render(t, 2)).join(", ");
|
|
4264
|
+
const to = hit.where === "prefix" ? `sequence(optional(${render(hit.term, 2)}), ${rest})` : `sequence(${rest}, optional(${render(hit.term, 2)}))`;
|
|
4265
|
+
push({
|
|
4266
|
+
rewrite: hit.where === "prefix" ? "optional-prefix" : "optional-suffix",
|
|
4267
|
+
site,
|
|
4268
|
+
from: clamp(render(v2.p, 2)),
|
|
4269
|
+
to: clamp(to),
|
|
4270
|
+
astNeutral: false,
|
|
4271
|
+
perf: PERF_ARM,
|
|
4272
|
+
bug: false,
|
|
4273
|
+
suggestion: `these two arms differ only by a ${hit.where === "prefix" ? "leading" : "trailing"} \`${clamp(render(hit.term, 2))}\`, so the choice is exactly \`${clamp(to)}\` \u2014 same language, one arm instead of two.${reversed !== null ? ` NOTE THE ORDER: the shorter arm is FIRST, so under ordered choice the longer arm is only reachable when the shorter one fails \u2014 check that is what you meant.` : ""} CANDIDATE \u2014 verify AST identity before applying:${nodeWrapped === null ? "" : ` this sits under \`node('${nodeWrapped}')\`, and`} the two forms produce different child arrays (two children vs. one child plus an absent optional). If a build fn or a downstream consumer reads children positionally, the rewrite MOVES the tree.`
|
|
4274
|
+
}, `${i}-${j}`);
|
|
4275
|
+
}
|
|
4276
|
+
for (let i = 0; i + 1 < arms.length; i++) {
|
|
4277
|
+
const j = i + 1;
|
|
4278
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4279
|
+
if (ta.length < 2 || tb.length < 2) continue;
|
|
4280
|
+
let k = 0;
|
|
4281
|
+
while (k < ta.length - 1 && k < tb.length - 1 && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
|
|
4282
|
+
if (k === 0) continue;
|
|
4283
|
+
const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
|
|
4284
|
+
push({
|
|
4285
|
+
rewrite: "left-factor",
|
|
4286
|
+
site,
|
|
4287
|
+
from: `choice(sequence(${ta.map((t) => render(t, 1)).join(", ")}), sequence(${tb.map((t) => render(t, 1)).join(", ")}))`,
|
|
4288
|
+
to: clamp(`sequence(${ta.slice(0, k).map((t) => render(t, 2)).join(", ")}, choice(sequence(${ta.slice(k).map((t) => render(t, 1)).join(", ")}), sequence(${tb.slice(k).map((t) => render(t, 1)).join(", ")})))`),
|
|
4289
|
+
astNeutral: false,
|
|
4290
|
+
perf: handled ? "NONE \u2014 parseman's `sharedPrefix` choice strategy already recognizes this prefix once and replays it, so the runtime cost is identical. This is a readability finding." : PERF_ARM,
|
|
4291
|
+
bug: false,
|
|
4292
|
+
suggestion: `arm[${i}] and arm[${j}] spell the same ${k} leading term(s). ${handled ? "The `sharedPrefix` strategy already collapses this AT RUNTIME, so left-factoring buys readability, not speed \u2014 but writing it out means the next reader does not have to trust the optimizer." : "Left-factor to `sequence(prefix, choice(tailA, tailB))`: the prefix is then recognized once and the inner choice dispatches on its own first char."} CANDIDATE \u2014 verify AST identity: factoring nests the tails one level deeper in the value.`
|
|
4293
|
+
}, `${i}-${j}`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
for (const v2 of visited.values()) {
|
|
4297
|
+
if (v2.d.tag !== "sequence") continue;
|
|
4298
|
+
const hit = detectHandRolledSepBy(v2.d.parsers, shapeOf);
|
|
4299
|
+
if (hit === null) continue;
|
|
4300
|
+
const captured = v2.d.parsers.some((t) => capturesPositionally(t));
|
|
4301
|
+
const verdict = captured ? "blocked-by-capture" : v2.enclosingReducer ? "reducer-stride-review" : "convertible";
|
|
4302
|
+
const to = `sepBy(${clamp(render(hit.item, 2))}, ${clamp(render(hit.sep, 2))}${hit.trailing ? `, { trailing: 'allow' }` : ""}, { min: ${hit.min} })`;
|
|
4303
|
+
out.push({
|
|
4304
|
+
kind: "rewrite",
|
|
4305
|
+
id: `rewrite:hand-rolled-sepby:${siteToString(v2.site)}`,
|
|
4306
|
+
rewrite: "hand-rolled-sepby",
|
|
4307
|
+
site: v2.site,
|
|
4308
|
+
sepByVerdict: verdict,
|
|
4309
|
+
from: clamp(render(v2.p, 3)),
|
|
4310
|
+
to: verdict === "convertible" ? to : `${to} \u2014 NOT APPLICABLE HERE (${verdict})`,
|
|
4311
|
+
astNeutral: false,
|
|
4312
|
+
perf: verdict === "convertible" ? "the hand-rolled form allocates a tuple per repetition (`sequence(sep, item)`) and a wrapper array around them; `sepBy` builds the item list directly." : PERF_NONE,
|
|
4313
|
+
bug: false,
|
|
4314
|
+
suggestion: `${SEPBY_SPELLED_OUT(hit.trailing)} ${SEPBY_VERDICT_TEXT[verdict](hit.trailing)}`
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
return out;
|
|
4318
|
+
}
|
|
4319
|
+
function collapseRepeat(outer, inner) {
|
|
4320
|
+
const o = outer.tag, i = inner.tag;
|
|
4321
|
+
if (o === "optional" && i === "optional") return "optional(X)";
|
|
4322
|
+
if (o === "optional" && i === "many") return "many(X)";
|
|
4323
|
+
if (o === "many" && i === "many") return "many(X)";
|
|
4324
|
+
if (o === "many" && i === "optional") return "many(X)";
|
|
4325
|
+
if (o === "optional" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
|
|
4326
|
+
if (o === "many" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
|
|
4327
|
+
return null;
|
|
4328
|
+
}
|
|
4329
|
+
function wrappingNodeType(arm) {
|
|
4330
|
+
const d = arm._def;
|
|
4331
|
+
return d.tag === "node" ? d.type ?? "(anonymous)" : null;
|
|
4332
|
+
}
|
|
4333
|
+
function detectOptionalTerm(longer, shorter, shapeOf) {
|
|
4334
|
+
if (longer.length !== shorter.length + 1 || shorter.length === 0) return null;
|
|
4335
|
+
const same = (a, b) => a.length === b.length && a.every((x, i) => shapeOf(x) === shapeOf(b[i]));
|
|
4336
|
+
if (same(longer.slice(1), shorter)) return { where: "prefix", term: longer[0], rest: shorter };
|
|
4337
|
+
if (same(longer.slice(0, -1), shorter)) return { where: "suffix", term: longer[longer.length - 1], rest: shorter };
|
|
4338
|
+
return null;
|
|
4339
|
+
}
|
|
4340
|
+
var SEPBY_SPELLED_OUT = (trailing) => `this is \`sepBy(item, sep${trailing ? ", { trailing: 'allow' }" : ""})\` spelled out.`;
|
|
4341
|
+
var SEPBY_VERDICT_TEXT = {
|
|
4342
|
+
"blocked-by-capture": () => "BLOCKED: the repetition CAPTURES its separator (`field`/`label`), which `sepBy` cannot express \u2014 it yields items and discards separators, so byte-faithful layout replay is lost. Leave this site alone; read it as a parseman gap (a separator-capturing repeat), not as work.",
|
|
4343
|
+
"reducer-stride-review": () => "NEEDS REVIEW, not conversion: an enclosing `node()`/`transform()` reducer reads this repetition's children, and a left-associating reducer typically STRIDES BY TWO over `[item, sep, item, sep, \u2026]` and re-emits the operator text. `sepBy` yields a FLAT ITEM list with the separators gone, so the reducer has to be rewritten in the same change or the tree moves. Convert only together with the reducer.",
|
|
4344
|
+
convertible: (trailing) => `No separator capture, and no enclosing reducer reads these children \u2014 this site is convertible. Beyond the ${trailing ? "four" : "three"} combinators it replaces, \`sepBy\` carries the right NULLABILITY: \`min: 0\` (the default) matches empty and does NOT gate as a choice arm, \`min: 1\` does \u2014 a distinction the hand-rolled shape leaves implicit and gets wrong silently. Still check the consumer: \`sepBy\` yields a flat item list where the hand-rolled form yields \`[first, [[sep, item], \u2026]]\`.`
|
|
4345
|
+
};
|
|
4346
|
+
function capturesPositionally(p, seen = /* @__PURE__ */ new Set()) {
|
|
4347
|
+
if (seen.has(p)) return false;
|
|
4348
|
+
seen.add(p);
|
|
4349
|
+
const d = p._def;
|
|
4350
|
+
if (d.tag === "field" || d.tag === "label") return true;
|
|
4351
|
+
for (const k of childrenOf(d)) if (capturesPositionally(k, seen)) return true;
|
|
4352
|
+
return false;
|
|
4353
|
+
}
|
|
4354
|
+
function detectHandRolledSepBy(terms, shapeOf) {
|
|
4355
|
+
if (terms.length < 2 || terms.length > 3) return null;
|
|
4356
|
+
const item = terms[0];
|
|
4357
|
+
const rep = terms[1]._def;
|
|
4358
|
+
if (rep.tag !== "many" && rep.tag !== "oneOrMore") return null;
|
|
4359
|
+
if (rep.max !== void 0) return null;
|
|
4360
|
+
const innerDef = rep.parser._def;
|
|
4361
|
+
if (innerDef.tag !== "sequence" || innerDef.parsers.length !== 2) return null;
|
|
4362
|
+
const [sep2, repeated] = innerDef.parsers;
|
|
4363
|
+
if (shapeOf(repeated) !== shapeOf(item)) return null;
|
|
4364
|
+
let trailing = false;
|
|
4365
|
+
if (terms.length === 3) {
|
|
4366
|
+
const t = terms[2]._def;
|
|
4367
|
+
if (t.tag !== "optional" || shapeOf(t.parser) !== shapeOf(sep2)) return null;
|
|
4368
|
+
trailing = true;
|
|
4369
|
+
}
|
|
4370
|
+
return { item, sep: sep2, trailing, min: 1 + rep.min };
|
|
4371
|
+
}
|
|
4372
|
+
function bodyTermsOf(p) {
|
|
4373
|
+
let cur = p;
|
|
4374
|
+
for (; ; ) {
|
|
4375
|
+
const d2 = cur._def;
|
|
4376
|
+
if (d2.tag === "node" || d2.tag === "grammar" || d2.tag === "transform" || d2.tag === "label" || d2.tag === "attempt" || d2.tag === "leaf" || d2.tag === "token" || d2.tag === "expect" || d2.tag === "field") {
|
|
4377
|
+
cur = d2.parser;
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
break;
|
|
4381
|
+
}
|
|
4382
|
+
const d = cur._def;
|
|
4383
|
+
return d.tag === "sequence" ? d.parsers : [cur];
|
|
4384
|
+
}
|
|
4385
|
+
var MIN_SHARED_TERMS = 2;
|
|
4386
|
+
function findDivergentNodes(visited, shapeOf) {
|
|
4387
|
+
const byType = /* @__PURE__ */ new Map();
|
|
4388
|
+
for (const v2 of visited.values()) {
|
|
4389
|
+
if (v2.d.tag !== "node" || v2.d.type === void 0) continue;
|
|
4390
|
+
let m = byType.get(v2.d.type);
|
|
4391
|
+
if (m === void 0) {
|
|
4392
|
+
m = /* @__PURE__ */ new Map();
|
|
4393
|
+
byType.set(v2.d.type, m);
|
|
4394
|
+
}
|
|
4395
|
+
if (!m.has(v2.shape)) m.set(v2.shape, v2);
|
|
4396
|
+
}
|
|
4397
|
+
const out = [];
|
|
4398
|
+
for (const [type, m] of byType) {
|
|
4399
|
+
if (m.size < 2) continue;
|
|
4400
|
+
const members = [...m.values()];
|
|
4401
|
+
const termSets = members.map((v2) => bodyTermsOf(v2.p).map((t) => ({ id: shapeOf(t), text: clamp(render(t, 2)) })));
|
|
4402
|
+
const shared = termSets[0].filter((t) => termSets.every((set) => set.some((x) => x.id === t.id)));
|
|
4403
|
+
const uniqueShared = [...new Map(shared.map((t) => [t.id, t])).values()];
|
|
4404
|
+
if (uniqueShared.length < MIN_SHARED_TERMS) continue;
|
|
4405
|
+
const sharedIds = new Set(uniqueShared.map((t) => t.id));
|
|
4406
|
+
out.push({
|
|
4407
|
+
kind: "divergent-node",
|
|
4408
|
+
id: `divergent-node:${type}`,
|
|
4409
|
+
nodeType: type,
|
|
4410
|
+
count: members.length,
|
|
4411
|
+
sharedTerms: uniqueShared.map((t) => t.text),
|
|
4412
|
+
productions: members.map((v2, i) => ({
|
|
4413
|
+
shape: clamp(render(v2.p, 3)),
|
|
4414
|
+
site: v2.site,
|
|
4415
|
+
distinctTerms: termSets[i].filter((t) => !sharedIds.has(t.id)).map((t) => t.text)
|
|
4416
|
+
})),
|
|
4417
|
+
suggestion: `\`${type}\` is built by ${members.length} structurally different productions that nevertheless spell ${uniqueShared.length} identical term(s) (${uniqueShared.map((t) => `\`${t.text}\``).join(", ")}) \u2014 they are variants of ONE shape, not ${members.length} constructs. Nothing keeps them in sync: an edit to the ${type} shape has to land in all ${members.length}, and no test fails when it lands in one. Fold them into a single production whose varying part is a \`choice\`, or, if the variants exist for a parse-order reason (a fast path tried first), say so at the definition \u2014 the cost of the split is that a reader cannot tell which it is.`
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
return out.sort((a, b) => b.sharedTerms.length * b.count - a.sharedTerms.length * a.count);
|
|
4421
|
+
}
|
|
4422
|
+
function resolveArm(p) {
|
|
4423
|
+
let cur = p;
|
|
4424
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4425
|
+
for (; ; ) {
|
|
4426
|
+
if (seen.has(cur)) return cur;
|
|
4427
|
+
seen.add(cur);
|
|
4428
|
+
const d = cur._def;
|
|
4429
|
+
if (d.tag === "label" || d.tag === "attempt") {
|
|
4430
|
+
cur = d.parser;
|
|
4431
|
+
continue;
|
|
4432
|
+
}
|
|
4433
|
+
if (d.tag === "lazy") {
|
|
4434
|
+
try {
|
|
4435
|
+
cur = d.thunk();
|
|
4436
|
+
continue;
|
|
4437
|
+
} catch {
|
|
4438
|
+
return cur;
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
return cur;
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
function nestedNodeTypes(root, out, seen, isRoot) {
|
|
4445
|
+
if (seen.has(root)) return;
|
|
4446
|
+
seen.add(root);
|
|
4447
|
+
const d = root._def;
|
|
4448
|
+
if (d.tag === "leaf" || d.tag === "token") return;
|
|
4449
|
+
if (d.tag === "node" && !isRoot) out.add(d.type ?? "(anonymous)");
|
|
4450
|
+
if (d.tag === "lazy") {
|
|
4451
|
+
try {
|
|
4452
|
+
nestedNodeTypes(d.thunk(), out, seen, false);
|
|
4453
|
+
} catch {
|
|
4454
|
+
}
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
for (const k of childrenOf(d)) nestedNodeTypes(k, out, seen, false);
|
|
4458
|
+
}
|
|
4459
|
+
var MAX_LOST_TYPES = 8;
|
|
4460
|
+
function findStructureLoss(visited, resolve2) {
|
|
4461
|
+
const nestedCache = /* @__PURE__ */ new Map();
|
|
4462
|
+
const nested = (p) => {
|
|
4463
|
+
const hit = nestedCache.get(p);
|
|
4464
|
+
if (hit !== void 0) return hit;
|
|
4465
|
+
const out = /* @__PURE__ */ new Set();
|
|
4466
|
+
nestedNodeTypes(p, out, /* @__PURE__ */ new Set(), true);
|
|
4467
|
+
nestedCache.set(p, out);
|
|
4468
|
+
return out;
|
|
4469
|
+
};
|
|
4470
|
+
const findings = [];
|
|
4471
|
+
for (const v2 of visited.values()) {
|
|
4472
|
+
const d = v2.d;
|
|
4473
|
+
if (d.tag !== "choice") continue;
|
|
4474
|
+
const arms = d.parsers;
|
|
4475
|
+
const resolved = arms.map(resolveArm);
|
|
4476
|
+
const nodeTypes = resolved.map(wrappingNodeType);
|
|
4477
|
+
const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
|
|
4478
|
+
for (let i = 0; i < arms.length; i++) {
|
|
4479
|
+
if (d.gates[i] != null) continue;
|
|
4480
|
+
const type = nodeTypes[i] ?? null;
|
|
4481
|
+
if (type === null) continue;
|
|
4482
|
+
if (nested(resolved[i]).size !== 0) continue;
|
|
4483
|
+
const A = fs2[i];
|
|
4484
|
+
if (A.kind === "any") continue;
|
|
4485
|
+
for (let j = i + 1; j < arms.length; j++) {
|
|
4486
|
+
if (nodeTypes[j] !== type) continue;
|
|
4487
|
+
const B = fs2[j];
|
|
4488
|
+
if (B.kind === "any" || !intersects3(A, B)) continue;
|
|
4489
|
+
const lost = nested(resolved[j]);
|
|
4490
|
+
if (lost.size === 0) continue;
|
|
4491
|
+
const lostNodeTypes = [...lost].sort();
|
|
4492
|
+
findings.push({
|
|
4493
|
+
kind: "structure-loss",
|
|
4494
|
+
id: `structure-loss:${siteToString(v2.site)}:${i}-${j}`,
|
|
4495
|
+
site: v2.site,
|
|
4496
|
+
nodeType: type,
|
|
4497
|
+
earlier: i,
|
|
4498
|
+
later: j,
|
|
4499
|
+
on: intersection2(A, B),
|
|
4500
|
+
lostNodeTypes: lostNodeTypes.slice(0, MAX_LOST_TYPES),
|
|
4501
|
+
earlierShape: clamp(render(resolved[i], 3)),
|
|
4502
|
+
laterShape: clamp(render(resolved[j], 3)),
|
|
4503
|
+
suggestion: structureLossSuggestion(type, i, j, lostNodeTypes)
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
return findings.sort((a, b) => b.lostNodeTypes.length - a.lostNodeTypes.length);
|
|
4509
|
+
}
|
|
4510
|
+
function structureLossSuggestion(type, i, j, lost) {
|
|
4511
|
+
const names = lost.slice(0, MAX_LOST_TYPES).map((t) => `\`${t}\``).join(", ");
|
|
4512
|
+
const more = lost.length > MAX_LOST_TYPES ? ` (and ${lost.length - MAX_LOST_TYPES} more)` : "";
|
|
4513
|
+
return `arm[${i}] and arm[${j}] both build \`${type}\` and can start on the same character, so on every input BOTH accept, arm[${i}] wins \u2014 and arm[${i}] contains no \`node()\`, so it yields a \`${type}\` over bare leaves. arm[${j}] would have built ${names}${more} there. The parse still succeeds and the span is still right; only the tree shape changes, and only on the inputs arm[${i}] happens to accept \u2014 so a suite that asserts "it parses" and an output diff both stay green while every consumer of the tree (language service, formatter, refactor) silently loses those nodes on the commonest inputs. Decide which tree is the contract: if the structured one is, DELETE arm[${i}] or move it after arm[${j}]; if the flat one is, it must be flat for ALL inputs of this shape, not just the ones arm[${i}] matches \u2014 a fast path that is not tree-neutral is not a fast path.`;
|
|
4514
|
+
}
|
|
4515
|
+
function findKeywordRegexes(visited) {
|
|
4516
|
+
const armIndexOf = /* @__PURE__ */ new Map();
|
|
4517
|
+
for (const v2 of visited.values()) {
|
|
4518
|
+
if (v2.d.tag !== "choice") continue;
|
|
4519
|
+
v2.d.parsers.forEach((a, i) => {
|
|
4520
|
+
let cur = a;
|
|
4521
|
+
for (; ; ) {
|
|
4522
|
+
const cd = cur._def;
|
|
4523
|
+
if (cd.tag === "node" || cd.tag === "label" || cd.tag === "transform" || cd.tag === "leaf" || cd.tag === "attempt") {
|
|
4524
|
+
cur = cd.parser;
|
|
4525
|
+
continue;
|
|
4526
|
+
}
|
|
4527
|
+
break;
|
|
4528
|
+
}
|
|
4529
|
+
armIndexOf.set(cur, { owner: v2.p, index: i });
|
|
4530
|
+
});
|
|
4531
|
+
}
|
|
4532
|
+
const keywordArms = /* @__PURE__ */ new Map();
|
|
4533
|
+
for (const [arm, { owner }] of armIndexOf) {
|
|
4534
|
+
const d = arm._def;
|
|
4535
|
+
if (d.tag !== "regex" || keywordRegexShape(d.source) === null) continue;
|
|
4536
|
+
const list = keywordArms.get(owner) ?? [];
|
|
4537
|
+
list.push(armIndexOf.get(arm).index);
|
|
4538
|
+
keywordArms.set(owner, list);
|
|
4539
|
+
}
|
|
4540
|
+
const out = [];
|
|
4541
|
+
for (const v2 of visited.values()) {
|
|
4542
|
+
const d = v2.d;
|
|
4543
|
+
if (d.tag !== "regex") continue;
|
|
4544
|
+
const shape = keywordRegexShape(d.source);
|
|
4545
|
+
if (shape === null) continue;
|
|
4546
|
+
const ci = d.flags.includes("i");
|
|
4547
|
+
const unicode = d.flags.includes("u") || d.flags.includes("v");
|
|
4548
|
+
const own = armIndexOf.get(v2.p);
|
|
4549
|
+
const siblings = own === void 0 ? [] : (keywordArms.get(own.owner) ?? []).filter((i) => i !== own.index);
|
|
4550
|
+
const bq = (s) => `'${s.replace(/\\/g, "\\\\")}'`;
|
|
4551
|
+
const vocabulary = shape.words.length >= VOCABULARY_MIN_WORDS;
|
|
4552
|
+
const hazards = keywordAlternationHazards(shape.words, shape.boundary);
|
|
4553
|
+
const unrescued = hazards.filter((h) => !h.rescuedByBoundary);
|
|
4554
|
+
const longestFirst = shape.words.every((w, i) => i === 0 || shape.words[i - 1].length >= w.length);
|
|
4555
|
+
const shown = shape.words.length > 8 ? [...shape.words.slice(0, 6), "/* \u2026 */", shape.words[shape.words.length - 1]] : shape.words;
|
|
4556
|
+
const call = shape.words.length === 1 ? `word(${bq(shape.words[0])}, ${bq(shape.boundary ?? "_0-9A-Za-z")}${ci ? ", { caseInsensitive: true }" : ""})` : `keywords([${shown.map((w) => w === "/* \u2026 */" ? w : bq(w)).join(", ")}], { boundary: ${bq(shape.boundary ?? "_0-9A-Za-z")}${ci ? ", caseInsensitive: true" : ""} })`;
|
|
4557
|
+
out.push({
|
|
4558
|
+
kind: "keyword-regex",
|
|
4559
|
+
id: `keyword-regex:${siteToString(v2.site)}`,
|
|
4560
|
+
site: v2.site,
|
|
4561
|
+
source: d.source,
|
|
4562
|
+
flags: d.flags,
|
|
4563
|
+
words: shape.words,
|
|
4564
|
+
boundary: shape.boundary,
|
|
4565
|
+
caseFoldRisk: ci && !unicode,
|
|
4566
|
+
siblingArms: siblings,
|
|
4567
|
+
vocabulary,
|
|
4568
|
+
longestFirst,
|
|
4569
|
+
hazards,
|
|
4570
|
+
bug: unrescued.length > 0,
|
|
4571
|
+
suggestion: [
|
|
4572
|
+
vocabulary ? `this regex enumerates a FIXED VOCABULARY of ${shape.words.length} literal words${shape.words.length > 20 ? " \u2014 the alternation is long enough that nobody re-reads it" : ""}. A regex enumerating a fixed vocabulary is a keyword set written the hard way. Use \`${call}\` \u2014 it exposes an EXACT first-set that \`choice\` can dispatch on (the alternation exposes none), it owns the boundary so it is not re-spelled per word, it sorts LONGEST-FIRST by construction, and it lowers to a single charCodeAt scan.` : `\`regex(/${d.source}/${d.flags})\` hand-rolls a keyword and its word boundary. Use \`${call}\` \u2014 it owns the boundary (so \`${shape.boundary ?? ""}\` is not re-spelled per keyword), exposes an EXACT first-set that \`choice\` can dispatch on, and lowers to the same charCodeAt scan.`,
|
|
4573
|
+
unrescued.length > 0 ? `LIVE BUG \u2014 the alternation is ordered so ${unrescued.length} later alternative(s) can NEVER match: ${unrescued.slice(0, 3).map((h) => `\`${h.shorter}\` precedes \`${h.longer}\` and nothing rejects the following \`${h.at}\``).join("; ")}${unrescued.length > 3 ? `, and ${unrescued.length - 3} more` : ""}. Regex alternation is FIRST-match, not longest-match, and with no boundary guard to force a backtrack the shorter branch wins outright. \`keywords()\` cannot express this bug: it sorts longest-first.` : hazards.length > 0 ? `ORDERING HAZARD (currently harmless): ${hazards.length} earlier alternative(s) are strict prefixes of later ones \u2014 e.g. \`${hazards[0].shorter}\` before \`${hazards[0].longer}\`. Regex alternation is first-match, so these work ONLY because the trailing boundary guard rejects the next character and the engine backtracks into the longer branch. That correctness rests on a hand-maintained order plus a guard that happens to cover the right characters; \`keywords()\` sorts longest-first by construction and does not depend on either.` : !longestFirst && vocabulary ? `The alternation is not in longest-first order. It is correct today${shape.boundary === null ? "" : " (the boundary guard forces a backtrack where it matters)"}, but the ordering is hand-maintained and nothing checks it \u2014 adding a word that extends an existing one is a silent bug. \`keywords()\` sorts longest-first by construction.` : "",
|
|
4574
|
+
ci && !unicode ? `CORRECTNESS, not style: \`/i\` without \`/u\` does NOT fold case the way \`{c, toUpperCase(c), toLowerCase(c)}\` suggests \u2014 67 BMP code points fold in ways those three miss (\u03C2/\u03C3, \xB5/\u03BC, the \u01C4\u01C5\u01C6 digraphs, combining iota subscript). parseman fixed exactly this INSIDE \`keywords()\` (see combinators/case-fold.ts); a hand-rolled copy never received the fix, so its first-set is unsound for those inputs.` : "",
|
|
4575
|
+
siblings.length > 0 ? `arm[${own?.index}] here sits alongside arm(s) [${siblings.join(", ")}] in the same \`choice\`, which are also hand-rolled keywords. A single \`keywords([\u2026])\` replaces the whole group and gives longest-match dispatch across it \u2014 which is what that combinator exists for.` : ""
|
|
4576
|
+
].filter((s) => s !== "").join(" ")
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
return out.sort((a, b) => Number(b.bug) - Number(a.bug) || b.hazards.length - a.hazards.length || b.words.length - a.words.length || b.siblingArms.length - a.siblingArms.length);
|
|
4580
|
+
}
|
|
4581
|
+
var sitesLine = (sites) => sites.slice(0, 6).map(siteToString).join("\n ") + (sites.length > 6 ? `
|
|
4582
|
+
\u2026 and ${sites.length - 6} more` : "");
|
|
4583
|
+
function formatDuplicationFindings(report) {
|
|
4584
|
+
const lines = [];
|
|
4585
|
+
for (const f of report.structureLoss) {
|
|
4586
|
+
lines.push(`parseman BUG [structure-loss] @ ${siteToString(f.site)}: arm[${f.earlier}] flattens \`${f.nodeType}\` where arm[${f.later}] structures it, on ${firstSetToString(f.on)}`);
|
|
4587
|
+
lines.push(` flat: ${f.earlierShape}`);
|
|
4588
|
+
lines.push(` rich: ${f.laterShape}`);
|
|
4589
|
+
lines.push(` lost: ${f.lostNodeTypes.map((t) => `\`${t}\``).join(", ")}`);
|
|
4590
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4591
|
+
}
|
|
4592
|
+
for (const f of report.rewrites) {
|
|
4593
|
+
lines.push(`parseman ${f.bug ? "BUG" : "rewrite"} [${f.rewrite}${f.sepByVerdict === void 0 ? "" : `/${f.sepByVerdict}`}] @ ${siteToString(f.site)}${f.astNeutral ? "" : " (candidate \u2014 verify AST identity)"}`);
|
|
4594
|
+
lines.push(` from: ${f.from}`);
|
|
4595
|
+
lines.push(` to: ${f.to}`);
|
|
4596
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4597
|
+
if (f.perf !== "") lines.push(` perf: ${f.perf}`);
|
|
4598
|
+
}
|
|
4599
|
+
for (const f of report.divergentNodes) {
|
|
4600
|
+
lines.push(`parseman divergent-node: \`${f.nodeType}\` is built by ${f.count} different productions sharing ${f.sharedTerms.length} term(s): ${f.sharedTerms.map((t) => `\`${t}\``).join(", ")}`);
|
|
4601
|
+
for (const p of f.productions) lines.push(` ${siteToString(p.site)}
|
|
4602
|
+
${p.shape}
|
|
4603
|
+
unique to it: ${p.distinctTerms.length === 0 ? "(none)" : p.distinctTerms.map((t) => `\`${t}\``).join(", ")}`);
|
|
4604
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4605
|
+
}
|
|
4606
|
+
for (const f of report.keywordRegexes) {
|
|
4607
|
+
const src = f.source.length > 90 ? `${f.source.slice(0, 88)}\u2026` : f.source;
|
|
4608
|
+
lines.push(`parseman ${f.bug ? "BUG " : ""}keyword-regex @ ${siteToString(f.site)}: /${src}/${f.flags}${f.vocabulary ? ` [${f.words.length} literal words${f.longestFirst ? "" : ", NOT longest-first"}]` : ""}${f.hazards.length > 0 ? ` [${f.hazards.length} prefix hazard(s)${f.bug ? ", UNRESCUED" : ""}]` : ""}${f.caseFoldRisk ? " [CASE-FOLD RISK]" : ""}`);
|
|
4609
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4610
|
+
}
|
|
4611
|
+
for (const f of report.duplicates) {
|
|
4612
|
+
lines.push(`parseman duplication: ${f.count}\xD7 identical ${f.size}-node shape (saves ${f.savings} nodes) \u2014 ${f.shape}`);
|
|
4613
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4614
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4615
|
+
}
|
|
4616
|
+
for (const f of report.nearDuplicates) {
|
|
4617
|
+
lines.push(`parseman near-duplication: ${f.count} clones of a ${f.sharedSize}-node shape differing at ONE slot (saves ${f.savings} nodes) \u2014 ${f.shape}`);
|
|
4618
|
+
lines.push(` slot: ${f.slotPath} \u2208 { ${f.variants.join(" | ")} }`);
|
|
4619
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4620
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4621
|
+
}
|
|
4622
|
+
for (const f of report.regexFragments) {
|
|
4623
|
+
lines.push(`parseman regex-fragment: \`${f.fragment}\` (${f.branches} branches) re-spelled in ${f.count} regex() terminals`);
|
|
4624
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4625
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4626
|
+
}
|
|
4627
|
+
for (const f of report.regexClasses) {
|
|
4628
|
+
lines.push(`parseman regex-class${f.drifted ? " DRIFT" : ""}: [${f.canonical}] across ${f.count} regex() terminals${f.bmpCeiling ? " [BMP ceiling]" : ""}`);
|
|
4629
|
+
for (const v2 of f.variants) lines.push(` [${v2.source}] \xD7${v2.count} ${v2.delta}
|
|
4630
|
+
${sitesLine(v2.sites)}`);
|
|
4631
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4632
|
+
}
|
|
4633
|
+
for (const f of report.overlaps) {
|
|
4634
|
+
lines.push(`parseman overlap @ ${siteToString(f.site)}: arm[${f.a}] \u2229 arm[${f.b}] on ${firstSetToString(f.on)}${f.sharedPrefix === null ? "" : ` \u2014 shared prefix: ${f.sharedPrefix}`}${f.handledByStrategy ? " [sharedPrefix strategy already handles this]" : ""}`);
|
|
4635
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4636
|
+
}
|
|
4637
|
+
return lines;
|
|
4638
|
+
}
|
|
4639
|
+
function duplicationFindingCount(report) {
|
|
4640
|
+
return report.duplicates.length + report.nearDuplicates.length + report.regexFragments.length + report.regexClasses.length + report.overlaps.length + report.rewrites.length + report.divergentNodes.length + report.structureLoss.length + report.keywordRegexes.length;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
3293
4643
|
// src/combinators/scanTo.ts
|
|
3294
4644
|
function resolveScanSkip(explicitSkip, raw, ctx) {
|
|
3295
4645
|
if (raw) return [];
|
|
@@ -6518,12 +7868,14 @@ function emitNode(def, ctx, pos) {
|
|
|
6518
7868
|
ctx.buildSrcs.push(def.buildSrc ?? null);
|
|
6519
7869
|
}
|
|
6520
7870
|
const i = ind(ctx);
|
|
6521
|
-
const
|
|
6522
|
-
|
|
6523
|
-
const
|
|
6524
|
-
const
|
|
7871
|
+
const cstOut = ctx.hostMode === "cst" && !structural;
|
|
7872
|
+
if (!structural && !cstOut) ctx.hostBranchElided = true;
|
|
7873
|
+
const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
|
|
7874
|
+
const clonesState = !structural && (cstOut || buildReadsState(def));
|
|
7875
|
+
const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
|
|
7876
|
+
const capturesRaw = !structural && (cstOut || mkType !== null || buildReadsRaw(def));
|
|
6525
7877
|
const hasFields = parserHasOwnFields(def.parser);
|
|
6526
|
-
const capturesFields = hasFields && !structural && buildReadsFields(def);
|
|
7878
|
+
const capturesFields = hasFields && !structural && (cstOut || buildReadsFields(def));
|
|
6527
7879
|
const innerEnablesTriviaCapture = parserEnablesTriviaCapture(def.parser);
|
|
6528
7880
|
const chV = v(ctx, "_ch");
|
|
6529
7881
|
const rawV = v(ctx, "_raw");
|
|
@@ -6540,8 +7892,9 @@ function emitNode(def, ctx, pos) {
|
|
|
6540
7892
|
const profHoist = `${i}const ${pmV} = _ctx._pmProfile, ${recV} = ${pmV}?.phase === 'recognizer', ${capV} = ${pmV}?.phase === 'capture'`;
|
|
6541
7893
|
const profileRecognizer = recV;
|
|
6542
7894
|
const profileCapture = capV;
|
|
6543
|
-
const
|
|
6544
|
-
const
|
|
7895
|
+
const cstMode = ctx.hostMode === "cst";
|
|
7896
|
+
const directCstV = !structural && !cstMode && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
|
|
7897
|
+
const directCstGate = directCstV === null ? "false" : profileCapture;
|
|
6545
7898
|
const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
|
|
6546
7899
|
const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
|
|
6547
7900
|
const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
|
|
@@ -6622,7 +7975,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
|
|
|
6622
7975
|
const ndV = v(ctx, "_nd");
|
|
6623
7976
|
const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: { start: ${pos}, end: ${endVar} }, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, pos, endVar, tlV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
|
|
6624
7977
|
const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${JSON.stringify(def.type)}, ${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
|
|
6625
|
-
const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` :
|
|
7978
|
+
const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
|
|
6626
7979
|
const hostCollapseExpr = structural ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
|
|
6627
7980
|
const unwrapExpr = `${chV}.length === 1 ? (${chV}[0] !== null && typeof ${chV}[0] === 'object' && ${chV}[0]._tag === 'leaf' ? ${chV}[0].value : ${chV}[0]) : (${ndExpr})`;
|
|
6628
7981
|
const collapseExpr = `${chV}.length === 1 ? ${chV}[0] : (${ndExpr})`;
|
|
@@ -7071,9 +8424,9 @@ function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
|
|
|
7071
8424
|
return false;
|
|
7072
8425
|
}
|
|
7073
8426
|
}
|
|
7074
|
-
return
|
|
8427
|
+
return childrenOf2(d).some((child) => hasDirectBuildDef(child, seen));
|
|
7075
8428
|
}
|
|
7076
|
-
function
|
|
8429
|
+
function childrenOf2(def) {
|
|
7077
8430
|
switch (def.tag) {
|
|
7078
8431
|
case "sequence":
|
|
7079
8432
|
case "choice":
|
|
@@ -7144,7 +8497,7 @@ function subtreeSizes(roots) {
|
|
|
7144
8497
|
const def = p._def;
|
|
7145
8498
|
let s = 1;
|
|
7146
8499
|
if (def.tag !== "lazy") {
|
|
7147
|
-
for (const child of
|
|
8500
|
+
for (const child of childrenOf2(def)) s += child._def.tag === "lazy" ? 1 : sz(child);
|
|
7148
8501
|
}
|
|
7149
8502
|
sizes.set(p, s);
|
|
7150
8503
|
return s;
|
|
@@ -7165,7 +8518,7 @@ function subtreeSizes(roots) {
|
|
|
7165
8518
|
return;
|
|
7166
8519
|
}
|
|
7167
8520
|
sz(p);
|
|
7168
|
-
for (const child of
|
|
8521
|
+
for (const child of childrenOf2(def)) visit(child);
|
|
7169
8522
|
};
|
|
7170
8523
|
for (const root of roots) visit(root);
|
|
7171
8524
|
return sizes;
|
|
@@ -7199,7 +8552,7 @@ function analyzeLazyUsageMulti(roots) {
|
|
|
7199
8552
|
if (isHoistableTag(def.tag)) {
|
|
7200
8553
|
counts.set(p, (counts.get(p) ?? 0) + 1);
|
|
7201
8554
|
}
|
|
7202
|
-
for (const child of
|
|
8555
|
+
for (const child of childrenOf2(def)) walk2(child);
|
|
7203
8556
|
}
|
|
7204
8557
|
const rootsArr = [...roots];
|
|
7205
8558
|
for (const root of rootsArr) walk2(root);
|
|
@@ -7231,7 +8584,7 @@ function ruleDependencies(ruleMap) {
|
|
|
7231
8584
|
walk2(resolved, false);
|
|
7232
8585
|
return;
|
|
7233
8586
|
}
|
|
7234
|
-
for (const child of
|
|
8587
|
+
for (const child of childrenOf2(def)) walk2(child, false);
|
|
7235
8588
|
};
|
|
7236
8589
|
walk2(comb, true);
|
|
7237
8590
|
deps.set(name, [...found]);
|
|
@@ -7251,10 +8604,30 @@ function runGatingDiagnostic(combinator, opt) {
|
|
|
7251
8604
|
function runGatingDiagnosticRules(ruleMap, opt) {
|
|
7252
8605
|
return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
|
|
7253
8606
|
}
|
|
7254
|
-
function runFusedGatingDiagnostic(getRuleMaps, opt) {
|
|
8607
|
+
function runFusedGatingDiagnostic(getRuleMaps, opt, getOpaque) {
|
|
7255
8608
|
if (resolveGatingLevel(opt) === "off") return void 0;
|
|
7256
8609
|
const ruleMaps = getRuleMaps();
|
|
7257
|
-
|
|
8610
|
+
const opaque = getOpaque?.() ?? [];
|
|
8611
|
+
const opaqueFindings = opaque.map((o) => ({
|
|
8612
|
+
rule: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
|
|
8613
|
+
kind: "opaque-artifact",
|
|
8614
|
+
reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices were not examined at this fuse.`
|
|
8615
|
+
}));
|
|
8616
|
+
if (!ruleMaps.some((map) => hasExternalRuleRef(map))) {
|
|
8617
|
+
if (opaqueFindings.length === 0) return void 0;
|
|
8618
|
+
return reportGating(opt, () => ({
|
|
8619
|
+
totalChoices: 0,
|
|
8620
|
+
gated: 0,
|
|
8621
|
+
recoverable: 0,
|
|
8622
|
+
unanalysable: opaqueFindings,
|
|
8623
|
+
ungated: [],
|
|
8624
|
+
accepted: [],
|
|
8625
|
+
deferred: [],
|
|
8626
|
+
acceptedUnused: [],
|
|
8627
|
+
choices: [],
|
|
8628
|
+
antiPatterns: []
|
|
8629
|
+
}));
|
|
8630
|
+
}
|
|
7258
8631
|
const winners = /* @__PURE__ */ new Map();
|
|
7259
8632
|
for (const map of ruleMaps) for (const [name, rule] of map) {
|
|
7260
8633
|
if (rule._def.tag === "lazy") {
|
|
@@ -7270,7 +8643,12 @@ function runFusedGatingDiagnostic(getRuleMaps, opt) {
|
|
|
7270
8643
|
return reportGating(opt, (o) => {
|
|
7271
8644
|
const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
|
|
7272
8645
|
const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
|
|
7273
|
-
return {
|
|
8646
|
+
return {
|
|
8647
|
+
...fused,
|
|
8648
|
+
ungated: fused.ungated.filter((c) => deferredHere.has(c.id)),
|
|
8649
|
+
antiPatterns: [],
|
|
8650
|
+
unanalysable: [...fused.unanalysable, ...opaqueFindings]
|
|
8651
|
+
};
|
|
7274
8652
|
});
|
|
7275
8653
|
}
|
|
7276
8654
|
function reportGating(opt, analyze) {
|
|
@@ -7281,22 +8659,82 @@ function reportGating(opt, analyze) {
|
|
|
7281
8659
|
let report;
|
|
7282
8660
|
try {
|
|
7283
8661
|
report = analyze(analyzeOpts);
|
|
7284
|
-
} catch {
|
|
7285
|
-
|
|
8662
|
+
} catch (e) {
|
|
8663
|
+
const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
8664
|
+
const failed = {
|
|
8665
|
+
totalChoices: 0,
|
|
8666
|
+
gated: 0,
|
|
8667
|
+
recoverable: 0,
|
|
8668
|
+
unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason: `gating analysis threw \u2014 ${reason}` }],
|
|
8669
|
+
ungated: [],
|
|
8670
|
+
accepted: [],
|
|
8671
|
+
deferred: [],
|
|
8672
|
+
acceptedUnused: [],
|
|
8673
|
+
choices: [],
|
|
8674
|
+
antiPatterns: []
|
|
8675
|
+
};
|
|
8676
|
+
const failLines = formatGatingWarnings(failed);
|
|
8677
|
+
if (level === "error") throw new Error(`parseman: gating analysis FAILED (no verdict available)
|
|
8678
|
+
${failLines.join("\n")}`);
|
|
8679
|
+
for (const l of failLines) console.warn(l);
|
|
8680
|
+
return failed;
|
|
7286
8681
|
}
|
|
7287
8682
|
const lines = formatGatingWarnings(report);
|
|
7288
8683
|
if (lines.length > 0) {
|
|
7289
|
-
if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
|
|
8684
|
+
if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s) / ${report.unanalysable.length} unanalysable rule(s)
|
|
7290
8685
|
${lines.join("\n")}`);
|
|
7291
8686
|
for (const l of lines) console.warn(l);
|
|
7292
8687
|
}
|
|
7293
8688
|
return report;
|
|
7294
8689
|
}
|
|
8690
|
+
function resolveDuplicationLevel(opt) {
|
|
8691
|
+
const explicit = typeof opt === "string" ? opt : opt?.level;
|
|
8692
|
+
if (explicit !== void 0) return explicit;
|
|
8693
|
+
const env = typeof process !== "undefined" ? process.env?.PARSEMAN_DUPLICATION : void 0;
|
|
8694
|
+
if (env === "off" || env === "warn" || env === "error") return env;
|
|
8695
|
+
return "off";
|
|
8696
|
+
}
|
|
8697
|
+
function reportDuplication(opt, analyze) {
|
|
8698
|
+
const level = resolveDuplicationLevel(opt);
|
|
8699
|
+
if (level === "off") return void 0;
|
|
8700
|
+
const obj = opt !== null && typeof opt === "object" ? opt : void 0;
|
|
8701
|
+
const analyzeOpts = obj === void 0 ? void 0 : {
|
|
8702
|
+
...obj.accept !== void 0 ? { accept: obj.accept } : {},
|
|
8703
|
+
...obj.minSize !== void 0 ? { minSize: obj.minSize } : {},
|
|
8704
|
+
...obj.maxFindings !== void 0 ? { maxFindings: obj.maxFindings } : {},
|
|
8705
|
+
...obj.entryName !== void 0 ? { entryName: obj.entryName } : {}
|
|
8706
|
+
};
|
|
8707
|
+
let report;
|
|
8708
|
+
try {
|
|
8709
|
+
report = analyze(analyzeOpts);
|
|
8710
|
+
} catch (err) {
|
|
8711
|
+
console.warn(
|
|
8712
|
+
`parseman: the duplication diagnostic could not run, so NOTHING was checked \u2014 this is not a clean result.
|
|
8713
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
8714
|
+
);
|
|
8715
|
+
return void 0;
|
|
8716
|
+
}
|
|
8717
|
+
const lines = formatDuplicationFindings(report);
|
|
8718
|
+
if (lines.length > 0) {
|
|
8719
|
+
if (level === "error") throw new Error(`parseman: ${duplicationFindingCount(report)} duplication/overlap finding(s)
|
|
8720
|
+
${lines.join("\n")}`);
|
|
8721
|
+
for (const l of lines) console.warn(l);
|
|
8722
|
+
}
|
|
8723
|
+
return report;
|
|
8724
|
+
}
|
|
8725
|
+
function runDuplicationDiagnostic(combinator, opt) {
|
|
8726
|
+
return reportDuplication(opt, (o) => analyzeDuplication(combinator, o));
|
|
8727
|
+
}
|
|
8728
|
+
function runDuplicationDiagnosticRules(ruleMap, opt) {
|
|
8729
|
+
return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
|
|
8730
|
+
}
|
|
7295
8731
|
function compile(combinator, mapFnSources, opts) {
|
|
7296
8732
|
const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
|
|
8733
|
+
runDuplicationDiagnostic(combinator, opts?.duplication);
|
|
7297
8734
|
markUnusedValues(combinator);
|
|
7298
8735
|
const grammarTrivia = combinator._meta.grammarTrivia;
|
|
7299
8736
|
const grammarScanSkip = combinator._meta.grammarScanSkip;
|
|
8737
|
+
const grammarHostMode = combinator._meta.grammarHostMode;
|
|
7300
8738
|
const ctx = {
|
|
7301
8739
|
vars: 0,
|
|
7302
8740
|
indent: 1,
|
|
@@ -7316,6 +8754,7 @@ function compile(combinator, mapFnSources, opts) {
|
|
|
7316
8754
|
namedFnDecls: [],
|
|
7317
8755
|
capturing: hasNodeDef(combinator),
|
|
7318
8756
|
recovery: opts?.recovery ?? false,
|
|
8757
|
+
hostMode: opts?.hostMode ?? grammarHostMode ?? "ast",
|
|
7319
8758
|
...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
|
|
7320
8759
|
lazyUsage: analyzeLazyUsage(combinator),
|
|
7321
8760
|
...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
|
|
@@ -7388,6 +8827,7 @@ function compile(combinator, mapFnSources, opts) {
|
|
|
7388
8827
|
return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
|
|
7389
8828
|
},
|
|
7390
8829
|
parseWithContext(input, parseCtx, pos = 0) {
|
|
8830
|
+
assertHostModeCompatible(ctx.hostMode ?? "ast", parseCtx.build, !!ctx.hostBranchElided);
|
|
7391
8831
|
return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, parseCtx);
|
|
7392
8832
|
},
|
|
7393
8833
|
// Note: collects expect() errors via _errors. Unlike interpreter
|
|
@@ -7417,10 +8857,12 @@ function publicRuleWrapperSource(rule, fnSource) {
|
|
|
7417
8857
|
}
|
|
7418
8858
|
function compileRuleMap(ruleMap, opts) {
|
|
7419
8859
|
runGatingDiagnosticRules(ruleMap, opts?.gating);
|
|
8860
|
+
runDuplicationDiagnosticRules(ruleMap, opts?.duplication);
|
|
7420
8861
|
for (const [, rule] of ruleMap) markUnusedValues(rule);
|
|
7421
8862
|
const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
|
|
7422
8863
|
const grammarTrivia = opts?.trivia;
|
|
7423
8864
|
const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
|
|
8865
|
+
const grammarHostMode = opts?.hostMode ?? ruleMap.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
|
|
7424
8866
|
const ctx = {
|
|
7425
8867
|
vars: 0,
|
|
7426
8868
|
indent: 1,
|
|
@@ -7440,6 +8882,7 @@ function compileRuleMap(ruleMap, opts) {
|
|
|
7440
8882
|
namedFnDecls: [],
|
|
7441
8883
|
capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
|
|
7442
8884
|
recovery: opts?.recovery ?? false,
|
|
8885
|
+
hostMode: grammarHostMode ?? "ast",
|
|
7443
8886
|
...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
|
|
7444
8887
|
lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
|
|
7445
8888
|
...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
|
|
@@ -7530,15 +8973,22 @@ function compileRuleMap(ruleMap, opts) {
|
|
|
7530
8973
|
return {
|
|
7531
8974
|
keys: perEntry.map((e) => e.key),
|
|
7532
8975
|
replacement,
|
|
8976
|
+
// Reported so the MACRO can stamp the emitted map, exactly as `fusedBody` stamps a
|
|
8977
|
+
// fused one. Without it a `rules()` artifact carries no mode and every driver-side
|
|
8978
|
+
// host check passes vacuously — see `withHostMode` in the plugin.
|
|
8979
|
+
hostMode: ctx.hostMode ?? "ast",
|
|
8980
|
+
hostBranchElided: !!ctx.hostBranchElided,
|
|
7533
8981
|
...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions }
|
|
7534
8982
|
};
|
|
7535
8983
|
}
|
|
7536
8984
|
function compileLinkable(ruleMapArg, ns, opts) {
|
|
7537
8985
|
if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
|
|
7538
8986
|
if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
|
|
8987
|
+
if (opts?.duplication !== void 0) runDuplicationDiagnosticRules(ruleMapArg, opts.duplication);
|
|
7539
8988
|
for (const [, rule] of ruleMapArg) markUnusedValues(rule);
|
|
7540
8989
|
const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
|
|
7541
8990
|
const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
|
|
8991
|
+
const grammarHostMode = opts?.hostMode ?? ruleMapArg.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
|
|
7542
8992
|
const ruleMap = ruleMapArg.filter(([, val]) => {
|
|
7543
8993
|
const d = val._def;
|
|
7544
8994
|
if (d.tag !== "lazy") return true;
|
|
@@ -7572,6 +9022,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7572
9022
|
// merely returning their scalar parse values.
|
|
7573
9023
|
capturing: opts?.captureTerminals === true || ruleMap.some(([, rule]) => hasNodeDef(rule)),
|
|
7574
9024
|
recovery: opts?.recovery ?? false,
|
|
9025
|
+
hostMode: grammarHostMode ?? "ast",
|
|
7575
9026
|
...opts?.coverage ? { coverage: { plan: opts.coverage } } : {},
|
|
7576
9027
|
lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
|
|
7577
9028
|
ns,
|
|
@@ -7622,7 +9073,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7622
9073
|
scanExternal(resolved);
|
|
7623
9074
|
return;
|
|
7624
9075
|
}
|
|
7625
|
-
for (const child of
|
|
9076
|
+
for (const child of childrenOf2(def)) scanExternal(child);
|
|
7626
9077
|
};
|
|
7627
9078
|
for (const [, rule] of ruleMap) scanExternal(rule);
|
|
7628
9079
|
}
|
|
@@ -7727,6 +9178,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7727
9178
|
deps: ruleDependencies(ruleMap),
|
|
7728
9179
|
needsEmptyTl: !!ctx.needsEmptyTl,
|
|
7729
9180
|
needsHostReads: !!ctx.needsHostReads,
|
|
9181
|
+
hostMode: ctx.hostMode ?? "ast",
|
|
9182
|
+
hostBranchElided: !!ctx.hostBranchElided,
|
|
7730
9183
|
hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
|
|
7731
9184
|
isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
|
|
7732
9185
|
mfFns: mfSrcs ? [] : ctx.mapFns,
|
|
@@ -7749,7 +9202,7 @@ function hasExternalRuleRef(ruleMap) {
|
|
|
7749
9202
|
if (resolved === void 0) return p._ruleName !== void 0;
|
|
7750
9203
|
return visit(resolved);
|
|
7751
9204
|
}
|
|
7752
|
-
return
|
|
9205
|
+
return childrenOf2(def).some(visit);
|
|
7753
9206
|
};
|
|
7754
9207
|
return ruleMap.some(([, rule]) => visit(rule));
|
|
7755
9208
|
}
|
|
@@ -7771,7 +9224,7 @@ function hasSemanticReduction(roots, externalRefs) {
|
|
|
7771
9224
|
return true;
|
|
7772
9225
|
}
|
|
7773
9226
|
}
|
|
7774
|
-
return
|
|
9227
|
+
return childrenOf2(def).some(visit);
|
|
7775
9228
|
};
|
|
7776
9229
|
return roots.some(visit);
|
|
7777
9230
|
}
|
|
@@ -7840,7 +9293,7 @@ function withCtx(extra, combinator) {
|
|
|
7840
9293
|
}
|
|
7841
9294
|
|
|
7842
9295
|
// src/compiler/ir-serialize.ts
|
|
7843
|
-
function
|
|
9296
|
+
function childrenOf3(def) {
|
|
7844
9297
|
switch (def.tag) {
|
|
7845
9298
|
case "sequence":
|
|
7846
9299
|
case "choice":
|
|
@@ -7879,7 +9332,7 @@ function childrenOf2(def) {
|
|
|
7879
9332
|
return [];
|
|
7880
9333
|
}
|
|
7881
9334
|
}
|
|
7882
|
-
var
|
|
9335
|
+
var ruleNameOf3 = (c) => c._ruleName;
|
|
7883
9336
|
function lazyTarget(c) {
|
|
7884
9337
|
if (c._def.tag !== "lazy") return null;
|
|
7885
9338
|
try {
|
|
@@ -8077,18 +9530,18 @@ var Serializer = class {
|
|
|
8077
9530
|
if (seen.has(c)) return;
|
|
8078
9531
|
seen.add(c);
|
|
8079
9532
|
if (c._def.tag === "lazy") {
|
|
8080
|
-
if (
|
|
9533
|
+
if (ruleNameOf3(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
|
|
8081
9534
|
const target = lazyTarget(c);
|
|
8082
9535
|
if (target) this.assertNoRuleRef(target, seen);
|
|
8083
9536
|
return;
|
|
8084
9537
|
}
|
|
8085
|
-
for (const child of
|
|
9538
|
+
for (const child of childrenOf3(c._def)) this.assertNoRuleRef(child, seen);
|
|
8086
9539
|
}
|
|
8087
9540
|
/** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
|
|
8088
9541
|
* its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
|
|
8089
9542
|
* `g[name]` ref — only the top-level entry is unwrapped. */
|
|
8090
9543
|
body(c) {
|
|
8091
|
-
return c._def.tag === "lazy" &&
|
|
9544
|
+
return c._def.tag === "lazy" && ruleNameOf3(c) !== void 0 ? lazyTarget(c) ?? c : c;
|
|
8092
9545
|
}
|
|
8093
9546
|
run() {
|
|
8094
9547
|
for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
|
|
@@ -8130,7 +9583,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8130
9583
|
analyze(c, active) {
|
|
8131
9584
|
const def = c._def;
|
|
8132
9585
|
if (def.tag === "lazy") {
|
|
8133
|
-
if (
|
|
9586
|
+
if (ruleNameOf3(c) !== void 0) return;
|
|
8134
9587
|
const target = lazyTarget(c);
|
|
8135
9588
|
if (!target) throw new Unserializable("unresolved unnamed ref");
|
|
8136
9589
|
if (active.has(target)) {
|
|
@@ -8146,7 +9599,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8146
9599
|
this.counts.set(c, (this.counts.get(c) ?? 0) + 1);
|
|
8147
9600
|
if ((this.counts.get(c) ?? 0) > 1) return;
|
|
8148
9601
|
active.add(c);
|
|
8149
|
-
for (const child of
|
|
9602
|
+
for (const child of childrenOf3(def)) this.analyze(child, active);
|
|
8150
9603
|
active.delete(c);
|
|
8151
9604
|
}
|
|
8152
9605
|
/** Reference `c`: a const var if it has one (emitting its decl first), else inline.
|
|
@@ -8182,7 +9635,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8182
9635
|
const kid = (x) => this.ref(x, selfOf);
|
|
8183
9636
|
switch (def.tag) {
|
|
8184
9637
|
case "lazy": {
|
|
8185
|
-
if (
|
|
9638
|
+
if (ruleNameOf3(c) !== void 0) return `g[${JSON.stringify(ruleNameOf3(c))}]`;
|
|
8186
9639
|
const target = lazyTarget(c);
|
|
8187
9640
|
if (selfOf && target === selfOf.comb) return selfOf.var;
|
|
8188
9641
|
return this.ref(target, selfOf);
|
|
@@ -8356,7 +9809,23 @@ function fusedBody(pieces) {
|
|
|
8356
9809
|
...[...winner].map(([k, p]) => p.ruleFns.get(k))
|
|
8357
9810
|
];
|
|
8358
9811
|
const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
|
|
8359
|
-
const
|
|
9812
|
+
const { mode, elided } = hostModeOfPieces(pieces);
|
|
9813
|
+
const rawBody = [
|
|
9814
|
+
...lines,
|
|
9815
|
+
"const _map = {",
|
|
9816
|
+
wrapperEntries.join(",\n"),
|
|
9817
|
+
"}",
|
|
9818
|
+
`Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
|
|
9819
|
+
`Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
|
|
9820
|
+
// …and on each rule FUNCTION, because `run(map.Rule, …)` is handed the rule, not the
|
|
9821
|
+
// map, and would otherwise have nothing to check against. Done once at fuse time,
|
|
9822
|
+
// before any rule has been called, so it costs nothing per parse.
|
|
9823
|
+
"for (const _k of Object.keys(_map)) {",
|
|
9824
|
+
` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
|
|
9825
|
+
` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
|
|
9826
|
+
"}",
|
|
9827
|
+
"return _map"
|
|
9828
|
+
].join("\n");
|
|
8360
9829
|
const ANY2 = { kind: "any" };
|
|
8361
9830
|
const EMPTY3 = { kind: "empty" };
|
|
8362
9831
|
const recipes = /* @__PURE__ */ new Map();
|
|
@@ -8415,6 +9884,16 @@ function fusedBody(pieces) {
|
|
|
8415
9884
|
}
|
|
8416
9885
|
return { body, env };
|
|
8417
9886
|
}
|
|
9887
|
+
function hostModeOfPieces(pieces) {
|
|
9888
|
+
const mode = pieces.find((p) => p.hostMode !== void 0 && p.hostMode !== "ast")?.hostMode ?? "ast";
|
|
9889
|
+
const elided = pieces.filter((p) => p.hostBranchElided === true);
|
|
9890
|
+
if (mode === "cst" && elided.length > 0) {
|
|
9891
|
+
throw new Error(
|
|
9892
|
+
`compose: cannot build a positioned-CST artifact from pieces that were compiled for host mode "ast" \u2014 ${elided.map((p) => `"${p.ns}"`).join(", ")} ${elided.length === 1 ? "dropped its" : "dropped their"} direct builders' CST branch, so ${elided.length === 1 ? "it" : "they"} would return AST objects inside the CST. An already-compiled piece keeps the mode it was built with; only pieces carried as IR are re-lowered by this compose. Re-compile ${elided.length === 1 ? "it" : "them"} with hostMode: 'cst', or pass the source grammar so it can be re-lowered.`
|
|
9893
|
+
);
|
|
9894
|
+
}
|
|
9895
|
+
return { mode, elided: elided.length > 0 };
|
|
9896
|
+
}
|
|
8418
9897
|
function emitFusedSource(pieces) {
|
|
8419
9898
|
const { body, env } = fusedBody(pieces);
|
|
8420
9899
|
if (Object.keys(env).length > 0) {
|
|
@@ -8427,11 +9906,23 @@ ${body}
|
|
|
8427
9906
|
function isIRPiece(p) {
|
|
8428
9907
|
return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
|
|
8429
9908
|
}
|
|
8430
|
-
function
|
|
9909
|
+
function once(fn) {
|
|
9910
|
+
let done = false;
|
|
9911
|
+
let value;
|
|
9912
|
+
return () => {
|
|
9913
|
+
if (!done) {
|
|
9914
|
+
value = fn();
|
|
9915
|
+
done = true;
|
|
9916
|
+
}
|
|
9917
|
+
return value;
|
|
9918
|
+
};
|
|
9919
|
+
}
|
|
9920
|
+
function materializePiece(p, trivia2, captureTerminals = false, hostMode) {
|
|
8431
9921
|
if (!isIRPiece(p)) return p;
|
|
8432
9922
|
const pieces = compileLinkable(evalRuleMapIR(p.ir), p.ns, {
|
|
8433
9923
|
...trivia2 ? { trivia: trivia2 } : {},
|
|
8434
|
-
...captureTerminals ? { captureTerminals: true } : {}
|
|
9924
|
+
...captureTerminals ? { captureTerminals: true } : {},
|
|
9925
|
+
...hostMode ? { hostMode } : {}
|
|
8435
9926
|
});
|
|
8436
9927
|
if (!pieces) throw new Error(`compose: carried IR for ns "${p.ns}" could not be re-lowered`);
|
|
8437
9928
|
return pieces;
|
|
@@ -9531,6 +11022,23 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9531
11022
|
for (const n of names) allNames.add(n);
|
|
9532
11023
|
}
|
|
9533
11024
|
if (macroImports.length === 0) return null;
|
|
11025
|
+
const factoryDecls = /* @__PURE__ */ new Map();
|
|
11026
|
+
for (const stmt of body) {
|
|
11027
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
11028
|
+
if (decl?.type !== "VariableDeclaration") continue;
|
|
11029
|
+
if (decl.kind !== "const") continue;
|
|
11030
|
+
for (const d of decl.declarations) {
|
|
11031
|
+
const id2 = d.id;
|
|
11032
|
+
const init = d.init;
|
|
11033
|
+
if (id2.type !== "Identifier" || !id2.name || !init) continue;
|
|
11034
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
11035
|
+
factoryDecls.set(id2.name, {
|
|
11036
|
+
fn: init,
|
|
11037
|
+
declaredAt: d.end
|
|
11038
|
+
});
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
9534
11042
|
const scope = /* @__PURE__ */ new Map();
|
|
9535
11043
|
const replacements = [];
|
|
9536
11044
|
const warnings = [];
|
|
@@ -9553,20 +11061,28 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9553
11061
|
const arg0 = args[0];
|
|
9554
11062
|
const arg1 = args[1];
|
|
9555
11063
|
const optionsFirst = arg0?.type === "ObjectExpression";
|
|
9556
|
-
const
|
|
9557
|
-
if (!
|
|
11064
|
+
const factoryArgRaw = optionsFirst ? arg1 : arg0;
|
|
11065
|
+
if (!factoryArgRaw) {
|
|
9558
11066
|
warn(init.start, `${label2}: rules() needs a factory argument`);
|
|
9559
11067
|
return null;
|
|
9560
11068
|
}
|
|
11069
|
+
const namedFactory = factoryArgRaw.type === "Identifier" ? factoryDecls.get(factoryArgRaw.name) : void 0;
|
|
11070
|
+
const factoryArg = namedFactory !== void 0 && namedFactory.declaredAt <= init.start ? namedFactory.fn : factoryArgRaw;
|
|
11071
|
+
const optionsArg = optionsFirst ? arg0 : arg1;
|
|
11072
|
+
const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
|
|
11073
|
+
(p) => p.key?.name === name
|
|
11074
|
+
)?.value : void 0;
|
|
11075
|
+
const hostModeValue = optionValue("hostMode");
|
|
11076
|
+
const gHostMode = hostModeValue?.type === "Literal" ? hostModeValue.value : void 0;
|
|
11077
|
+
if (hostModeValue !== void 0 && gHostMode !== "ast" && gHostMode !== "cst") {
|
|
11078
|
+
warn(init.start, `${label2}: rules({ hostMode }) must be the literal 'ast' or 'cst'`);
|
|
11079
|
+
return null;
|
|
11080
|
+
}
|
|
9561
11081
|
const ruleMap = evaluateParserFactory(factoryArg, scope, code, []);
|
|
9562
11082
|
if (!ruleMap) {
|
|
9563
11083
|
warn(init.start, `${label2}: rules(...) factory isn't statically evaluable`);
|
|
9564
11084
|
return null;
|
|
9565
11085
|
}
|
|
9566
|
-
const optionsArg = optionsFirst ? arg0 : arg1;
|
|
9567
|
-
const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
|
|
9568
|
-
(p) => p.key?.name === name
|
|
9569
|
-
)?.value : void 0;
|
|
9570
11086
|
const triviaValue = optionValue("trivia");
|
|
9571
11087
|
const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
|
|
9572
11088
|
const scanSkipValue = optionValue("scanSkip");
|
|
@@ -9579,13 +11095,29 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9579
11095
|
}
|
|
9580
11096
|
}
|
|
9581
11097
|
}
|
|
11098
|
+
if (gHostMode === "cst") {
|
|
11099
|
+
for (const rule of ruleMap.values()) {
|
|
11100
|
+
if (rule && !rule._meta.isTrivia) {
|
|
11101
|
+
;
|
|
11102
|
+
rule._meta.grammarHostMode = "cst";
|
|
11103
|
+
}
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
9582
11106
|
return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
|
|
9583
11107
|
};
|
|
9584
11108
|
const compileRulesFactory = (init, label2) => {
|
|
9585
11109
|
const evaluated = evaluateRulesFactory(init, label2);
|
|
9586
11110
|
if (!evaluated) return null;
|
|
9587
11111
|
const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
|
|
9588
|
-
return {
|
|
11112
|
+
return {
|
|
11113
|
+
replacement: compiled?.replacement ?? null,
|
|
11114
|
+
ruleMap: evaluated.ruleMap,
|
|
11115
|
+
hostMode: compiled?.hostMode ?? "ast",
|
|
11116
|
+
hostBranchElided: compiled?.hostBranchElided ?? false,
|
|
11117
|
+
...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {},
|
|
11118
|
+
...evaluated.trivia ? { trivia: evaluated.trivia } : {},
|
|
11119
|
+
...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}
|
|
11120
|
+
};
|
|
9589
11121
|
};
|
|
9590
11122
|
const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
|
|
9591
11123
|
const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
|
|
@@ -9600,7 +11132,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9600
11132
|
const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
|
|
9601
11133
|
([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
|
|
9602
11134
|
).join(", ")}])`;
|
|
9603
|
-
return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
|
|
11135
|
+
return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, hostMode: ${JSON.stringify(p.hostMode ?? "ast")}, hostBranchElided: ${p.hostBranchElided === true}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
|
|
9604
11136
|
};
|
|
9605
11137
|
const isSpread = (it) => "__spreadLocal" in it;
|
|
9606
11138
|
const isIR = (it) => "ir" in it && !("ruleFns" in it);
|
|
@@ -9608,6 +11140,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9608
11140
|
const serializeList = (list) => `[${list.map(serializeItem).join(", ")}]`;
|
|
9609
11141
|
const withCarriedPieces = (grammarExpr, list) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)}, enumerable: false })`;
|
|
9610
11142
|
const withLeafMarker = (grammarExpr) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.leafComposed'), { value: true, enumerable: false })`;
|
|
11143
|
+
const withHostMode = (grammarExpr, mode, elided) => `/* @__PURE__ */ (m => { for (const k of Object.keys(m)) { Object.defineProperty(m[k], Symbol.for('parseman.fusedHostMode'), { value: ${JSON.stringify(mode)}, enumerable: false }); Object.defineProperty(m[k], Symbol.for('parseman.fusedHostElided'), { value: ${JSON.stringify(elided)}, enumerable: false }) } Object.defineProperty(m, Symbol.for('parseman.fusedHostMode'), { value: ${JSON.stringify(mode)}, enumerable: false }); Object.defineProperty(m, Symbol.for('parseman.fusedHostElided'), { value: ${JSON.stringify(elided)}, enumerable: false }); return m })(${grammarExpr})`;
|
|
9611
11144
|
const withCoverageDefinitions = (grammarExpr, definitions) => !grammarCoverage ? grammarExpr : `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.grammarCoverageDefinitions'), { value: Object.freeze(${JSON.stringify(definitions)}.map(Object.freeze)), enumerable: false })`;
|
|
9612
11145
|
const emittedCoverageDefinitions = (source) => {
|
|
9613
11146
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -9667,29 +11200,38 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9667
11200
|
importedPiecesCache.set(localName, result2);
|
|
9668
11201
|
return result2;
|
|
9669
11202
|
};
|
|
9670
|
-
const materializeCarried = (items, composing, captureTerminals = false) => {
|
|
11203
|
+
const materializeCarried = (items, composing, captureTerminals = false, hostMode) => {
|
|
9671
11204
|
const out = [];
|
|
9672
11205
|
for (const it of items) {
|
|
9673
11206
|
if (isSpread(it)) {
|
|
9674
|
-
for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals));
|
|
11207
|
+
for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals, hostMode));
|
|
9675
11208
|
} else if (isIR(it)) {
|
|
9676
|
-
out.push(materializePiece(it, composing, captureTerminals));
|
|
11209
|
+
out.push(materializePiece(it, composing, captureTerminals, hostMode));
|
|
9677
11210
|
} else {
|
|
9678
11211
|
out.push(it);
|
|
9679
11212
|
}
|
|
9680
11213
|
}
|
|
9681
11214
|
return out;
|
|
9682
11215
|
};
|
|
9683
|
-
const
|
|
9684
|
-
const
|
|
11216
|
+
const carriedRuleMapsDetailed = (items) => {
|
|
11217
|
+
const maps = [];
|
|
11218
|
+
const opaque = [];
|
|
9685
11219
|
const add = (it) => {
|
|
9686
|
-
if (isIR(it))
|
|
11220
|
+
if (isIR(it)) {
|
|
11221
|
+
maps.push(evalRuleMapIR(it.ir));
|
|
11222
|
+
return;
|
|
11223
|
+
}
|
|
11224
|
+
const o = it;
|
|
11225
|
+
opaque.push({
|
|
11226
|
+
ns: o.ns ?? "<unknown>",
|
|
11227
|
+
ruleNames: o.ruleFns instanceof Map ? [...o.ruleFns.keys()] : []
|
|
11228
|
+
});
|
|
9687
11229
|
};
|
|
9688
11230
|
for (const it of items) {
|
|
9689
11231
|
if (isSpread(it)) for (const p of importedPieces(it.__spreadLocal) ?? []) add(p);
|
|
9690
11232
|
else add(it);
|
|
9691
11233
|
}
|
|
9692
|
-
return
|
|
11234
|
+
return { maps, opaque };
|
|
9693
11235
|
};
|
|
9694
11236
|
const materializeLeafCoverage = (items, localRules, localNs, composing, captureTerminals = false) => {
|
|
9695
11237
|
const sources = [];
|
|
@@ -9727,6 +11269,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9727
11269
|
const ns = nsFor(label2);
|
|
9728
11270
|
const ir = serializeRuleMap(entries, scanSkip);
|
|
9729
11271
|
if (ir) return { carried: [{ ns, ir }] };
|
|
11272
|
+
warn(0, `${label2}: rule map could not be serialized to IR; carrying FULL pieces instead. The artifact is correct but larger, and a downstream compose cannot re-lower it. Re-run with PARSEMAN_IR_DEBUG=1 to print the exact combinator that blocked serialization.`);
|
|
9730
11273
|
const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
|
|
9731
11274
|
return p ? { carried: [p] } : null;
|
|
9732
11275
|
};
|
|
@@ -9814,6 +11357,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9814
11357
|
return null;
|
|
9815
11358
|
}
|
|
9816
11359
|
const elements = arr.elements;
|
|
11360
|
+
const cOptions = init.arguments[1];
|
|
11361
|
+
const cHostModeValue = cOptions?.type === "ObjectExpression" ? (cOptions.properties ?? []).find(
|
|
11362
|
+
(p) => p.key?.name === "hostMode"
|
|
11363
|
+
)?.value : void 0;
|
|
11364
|
+
const cHostMode = cHostModeValue?.type === "Literal" ? cHostModeValue.value : void 0;
|
|
11365
|
+
if (cHostModeValue !== void 0 && cHostMode !== "ast" && cHostMode !== "cst") {
|
|
11366
|
+
warn(init.start, "compose({ hostMode }) must be the literal 'ast' or 'cst'");
|
|
11367
|
+
return null;
|
|
11368
|
+
}
|
|
9817
11369
|
const composing = composingTrivia(elements);
|
|
9818
11370
|
const carried = [];
|
|
9819
11371
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -9824,8 +11376,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9824
11376
|
}
|
|
9825
11377
|
carried.push(...r.carried);
|
|
9826
11378
|
}
|
|
9827
|
-
|
|
9828
|
-
|
|
11379
|
+
const detailed = once(() => carriedRuleMapsDetailed(carried));
|
|
11380
|
+
runFusedGatingDiagnostic(
|
|
11381
|
+
() => detailed().maps,
|
|
11382
|
+
void 0,
|
|
11383
|
+
() => detailed().opaque
|
|
11384
|
+
);
|
|
11385
|
+
const pieces = materializeCarried(carried, composing, false, cHostMode);
|
|
9829
11386
|
try {
|
|
9830
11387
|
return { replacement: emitFusedSource(pieces), carried, ...composing ? { trivia: composing } : {} };
|
|
9831
11388
|
} catch (e) {
|
|
@@ -9873,7 +11430,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9873
11430
|
}
|
|
9874
11431
|
try {
|
|
9875
11432
|
const localNs = nsFor(`composeLeaf${init.start}`);
|
|
9876
|
-
|
|
11433
|
+
const detailedLeaf = once(() => carriedRuleMapsDetailed(carried));
|
|
11434
|
+
runFusedGatingDiagnostic(
|
|
11435
|
+
() => [...detailedLeaf().maps, [...localRules]],
|
|
11436
|
+
void 0,
|
|
11437
|
+
() => detailedLeaf().opaque
|
|
11438
|
+
);
|
|
9877
11439
|
const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
|
|
9878
11440
|
if (!plainLocalPiece) {
|
|
9879
11441
|
warn(init.start, "composeLeaf(): local rules could not be statically compiled");
|
|
@@ -9994,6 +11556,9 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9994
11556
|
source,
|
|
9995
11557
|
compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(source)
|
|
9996
11558
|
);
|
|
11559
|
+
if (compiledRules.replacement !== null) {
|
|
11560
|
+
replacement = withHostMode(replacement, compiledRules.hostMode, compiledRules.hostBranchElided);
|
|
11561
|
+
}
|
|
9997
11562
|
if (exportPrefix && pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
|
|
9998
11563
|
const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
|
|
9999
11564
|
replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
|
|
@@ -10036,6 +11601,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
10036
11601
|
scope.set(varName, combiArray);
|
|
10037
11602
|
continue;
|
|
10038
11603
|
}
|
|
11604
|
+
if (factoryDecls.has(varName)) continue;
|
|
10039
11605
|
warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`);
|
|
10040
11606
|
continue;
|
|
10041
11607
|
}
|