parseman 0.36.0 → 0.38.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/keywords.d.ts +9 -8
- package/dist/combinators/keywords.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 +1657 -64
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1657 -64
- 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/evaluator.d.ts +2 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +1696 -85
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1696 -85
- 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 +3001 -60
- package/dist/spec/index.cjs.map +4 -4
- package/dist/spec/index.js +3001 -60
- 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.cjs
CHANGED
|
@@ -2393,6 +2393,16 @@ function tagRule(r, key) {
|
|
|
2393
2393
|
r._ruleName = key;
|
|
2394
2394
|
if (r._def.tag === "node" && r._def.type === void 0) r._def.type = key;
|
|
2395
2395
|
}
|
|
2396
|
+
function isDefinableRef(v2) {
|
|
2397
|
+
return !!v2 && typeof v2 === "object" && "_def" in v2 && v2._def.tag === "lazy" && typeof v2.define === "function";
|
|
2398
|
+
}
|
|
2399
|
+
function ruleNameOf(r) {
|
|
2400
|
+
return r._ruleName;
|
|
2401
|
+
}
|
|
2402
|
+
function isNamedRuleRefForAnotherRule(r, key) {
|
|
2403
|
+
const name = r._def.tag === "lazy" ? ruleNameOf(r) : void 0;
|
|
2404
|
+
return name !== void 0 && name !== key;
|
|
2405
|
+
}
|
|
2396
2406
|
function rules(a, b) {
|
|
2397
2407
|
const factory = typeof a === "function" ? a : b;
|
|
2398
2408
|
const options = typeof a === "function" ? b : a;
|
|
@@ -2413,11 +2423,21 @@ function rules(a, b) {
|
|
|
2413
2423
|
for (const key of Object.keys(definitions)) {
|
|
2414
2424
|
const placeholder = cache[key];
|
|
2415
2425
|
const parser2 = definitions[key];
|
|
2416
|
-
if (placeholder
|
|
2417
|
-
|
|
2426
|
+
if (placeholder === parser2) {
|
|
2427
|
+
throw new Error(`rules(): rule "${key}" cannot be a direct alias to itself`);
|
|
2428
|
+
}
|
|
2429
|
+
if (isDefinableRef(placeholder)) {
|
|
2430
|
+
if (!isNamedRuleRefForAnotherRule(parser2, key)) tagRule(parser2, key);
|
|
2418
2431
|
placeholder.define(parser2);
|
|
2419
2432
|
placeholder._meta.firstSet = parser2._meta.firstSet;
|
|
2420
2433
|
placeholder._meta.canMatchNewline = parser2._meta.canMatchNewline;
|
|
2434
|
+
} else if (isNamedRuleRefForAnotherRule(parser2, key)) {
|
|
2435
|
+
const alias = ref();
|
|
2436
|
+
tagRule(alias, key);
|
|
2437
|
+
alias.define(parser2);
|
|
2438
|
+
alias._meta.firstSet = parser2._meta.firstSet;
|
|
2439
|
+
alias._meta.canMatchNewline = parser2._meta.canMatchNewline;
|
|
2440
|
+
cache[key] = alias;
|
|
2421
2441
|
} else {
|
|
2422
2442
|
tagRule(parser2, key);
|
|
2423
2443
|
cache[key] = parser2;
|
|
@@ -2435,6 +2455,12 @@ function rules(a, b) {
|
|
|
2435
2455
|
if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
|
|
2436
2456
|
}
|
|
2437
2457
|
}
|
|
2458
|
+
if (options?.hostMode === "cst") {
|
|
2459
|
+
for (const key of Object.keys(definitions)) {
|
|
2460
|
+
const rule = cache[key];
|
|
2461
|
+
if (rule && !rule._meta.isTrivia) rule._meta.grammarHostMode = "cst";
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2438
2464
|
for (const key of Object.keys(definitions)) {
|
|
2439
2465
|
markUnusedValues(cache[key]);
|
|
2440
2466
|
}
|
|
@@ -2493,6 +2519,9 @@ function peek(combinator) {
|
|
|
2493
2519
|
}
|
|
2494
2520
|
|
|
2495
2521
|
// src/compiler/build-arity.ts
|
|
2522
|
+
function cstOutputHost(build) {
|
|
2523
|
+
return build?._parsemanCstOutput === true;
|
|
2524
|
+
}
|
|
2496
2525
|
var PARAM_LIST_RE = (
|
|
2497
2526
|
// (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
|
|
2498
2527
|
/^(?:function\b[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([A-Za-z_$][\w$]*)\s*=>)/
|
|
@@ -2742,7 +2771,8 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
|
|
|
2742
2771
|
const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
|
|
2743
2772
|
const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
|
|
2744
2773
|
const clonesState = build ? buildReadsState(def) : true;
|
|
2745
|
-
const
|
|
2774
|
+
const hasOwnFields = parserHasOwnFields(combinator);
|
|
2775
|
+
const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
|
|
2746
2776
|
const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
|
|
2747
2777
|
let failExpected;
|
|
2748
2778
|
return {
|
|
@@ -2757,28 +2787,32 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
|
|
|
2757
2787
|
}
|
|
2758
2788
|
return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
|
|
2759
2789
|
}
|
|
2790
|
+
const hostCst = build !== void 0 && cstOutputHost(ctx.build);
|
|
2791
|
+
const effTrivia = capturesTrivia || hostCst;
|
|
2792
|
+
const effFields = capturesFields || hostCst && hasOwnFields;
|
|
2760
2793
|
const saved = beginCstNodeCapture(ctx);
|
|
2761
2794
|
const savedFields = ctx._fields;
|
|
2762
|
-
ctx._fields =
|
|
2795
|
+
ctx._fields = effFields ? [] : void 0;
|
|
2763
2796
|
const savedMask = ctx._triviaCaptureMask;
|
|
2764
2797
|
if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
|
|
2765
2798
|
ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
|
|
2766
2799
|
}
|
|
2767
|
-
if (!
|
|
2800
|
+
if (!effTrivia) ctx.captureTrivia = false;
|
|
2768
2801
|
let r = combinator.parse(input, pos, ctx);
|
|
2769
2802
|
if (r.ok && trailingTrivia && ctx.trivia) {
|
|
2770
2803
|
const end = consumeTrivia(input, r.span.end, ctx);
|
|
2771
2804
|
r = { ...r, span: { start: r.span.start, end } };
|
|
2772
2805
|
}
|
|
2773
|
-
const fields =
|
|
2806
|
+
const fields = effFields ? buildFieldMap(ctx._fields) : void 0;
|
|
2774
2807
|
ctx._fields = savedFields;
|
|
2775
2808
|
ctx._triviaCaptureMask = savedMask;
|
|
2776
2809
|
const { children: children2, rawChildren, triviaLog } = endCstNodeCapture(ctx, saved);
|
|
2777
2810
|
if (!r.ok) return r;
|
|
2778
2811
|
const st = clonesState && ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
|
|
2779
2812
|
const nodeType = def.type ?? missingInferredType();
|
|
2780
|
-
const cstOutput = ctx.build
|
|
2781
|
-
const
|
|
2813
|
+
const cstOutput = cstOutputHost(ctx.build);
|
|
2814
|
+
const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
|
|
2815
|
+
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 };
|
|
2782
2816
|
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 };
|
|
2783
2817
|
if (saved.buf !== void 0 || saved.ch !== void 0) {
|
|
2784
2818
|
pushCstChild(ctx, built, rawEntry);
|
|
@@ -3029,7 +3063,24 @@ function leaf(root, fn) {
|
|
|
3029
3063
|
}
|
|
3030
3064
|
|
|
3031
3065
|
// src/version.ts
|
|
3032
|
-
var PARSEMAN_VERSION = "0.
|
|
3066
|
+
var PARSEMAN_VERSION = "0.38.0";
|
|
3067
|
+
|
|
3068
|
+
// src/cst/host-mode.ts
|
|
3069
|
+
var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
|
|
3070
|
+
var FUSED_HOST_ELIDED = /* @__PURE__ */ Symbol.for("parseman.fusedHostElided");
|
|
3071
|
+
function assertHostModeCompatible(mode, build, hostBranchElided = true) {
|
|
3072
|
+
const isCstHost = build?._parsemanCstOutput === true;
|
|
3073
|
+
if (mode === "ast" && isCstHost && hostBranchElided) {
|
|
3074
|
+
throw new Error(
|
|
3075
|
+
"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."
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
if (mode === "cst" && !isCstHost) {
|
|
3079
|
+
throw new Error(
|
|
3080
|
+
'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.'
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3033
3084
|
|
|
3034
3085
|
// src/analysis/gating.ts
|
|
3035
3086
|
var isAny = (fs2) => fs2.kind === "any";
|
|
@@ -3066,7 +3117,7 @@ function firstSetToString(fs2) {
|
|
|
3066
3117
|
if (fs2.kind === "empty") return "(empty)";
|
|
3067
3118
|
return fs2.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
|
|
3068
3119
|
}
|
|
3069
|
-
var
|
|
3120
|
+
var ruleNameOf2 = (p) => p._ruleName;
|
|
3070
3121
|
var SUGGESTIONS = {
|
|
3071
3122
|
"broad-recognizer": "if this arm leads with a keyword regex, use word('kw', boundary) / keywords([...]) \u2014 they expose an EXACT, gating first-set and lower to the same charCodeAt scan. A genuine scanTo fallback is fine; accept it in the gating snapshot if intentional.",
|
|
3072
3123
|
"leading-not": "let the arm lead with its actual consuming terminal (first-sets gate it automatically); keep not(...) only as a TRAILING boundary. Do not hand-roll not(not(...)) to fake gating \u2014 use peek(X), which is zero-width AND carries X first-set.",
|
|
@@ -3224,17 +3275,40 @@ function detectAntiPatterns(rule, arms) {
|
|
|
3224
3275
|
return out;
|
|
3225
3276
|
}
|
|
3226
3277
|
function analyzeGating(entry, opts) {
|
|
3227
|
-
return analyzeGatingRules([[
|
|
3278
|
+
return analyzeGatingRules([[ruleNameOf2(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
|
|
3228
3279
|
}
|
|
3229
3280
|
function analyzeGatingRules(ruleMap, opts) {
|
|
3230
3281
|
const raw = [];
|
|
3231
3282
|
const antiPatterns = [];
|
|
3283
|
+
const unanalysable = [];
|
|
3284
|
+
const seenUnanalysable = /* @__PURE__ */ new Set();
|
|
3232
3285
|
const visited = /* @__PURE__ */ new Set();
|
|
3286
|
+
const noteUnanalysable = (u) => {
|
|
3287
|
+
const key = `${u.rule}\0${u.kind}`;
|
|
3288
|
+
if (seenUnanalysable.has(key)) return;
|
|
3289
|
+
seenUnanalysable.add(key);
|
|
3290
|
+
unanalysable.push(u);
|
|
3291
|
+
};
|
|
3233
3292
|
const visit = (p, enclosingRule) => {
|
|
3234
3293
|
if (visited.has(p)) return;
|
|
3235
3294
|
visited.add(p);
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3295
|
+
const def = p?._def;
|
|
3296
|
+
if (def === null || typeof def !== "object") {
|
|
3297
|
+
noteUnanalysable(
|
|
3298
|
+
typeof p === "function" ? {
|
|
3299
|
+
rule: enclosingRule,
|
|
3300
|
+
kind: "fused-rule",
|
|
3301
|
+
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."
|
|
3302
|
+
} : {
|
|
3303
|
+
rule: enclosingRule,
|
|
3304
|
+
kind: "not-a-combinator",
|
|
3305
|
+
reason: `value of type ${p === null ? "null" : typeof p} is not a combinator and carries no _def descriptor.`
|
|
3306
|
+
}
|
|
3307
|
+
);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
const d = def;
|
|
3311
|
+
const rule = ruleNameOf2(p) ?? enclosingRule;
|
|
3238
3312
|
if (d.tag === "choice") {
|
|
3239
3313
|
raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
|
|
3240
3314
|
antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
|
|
@@ -3253,7 +3327,7 @@ function analyzeGatingRules(ruleMap, opts) {
|
|
|
3253
3327
|
}
|
|
3254
3328
|
for (const k of kids) visit(k, rule);
|
|
3255
3329
|
};
|
|
3256
|
-
for (const [name, root] of ruleMap) visit(root,
|
|
3330
|
+
for (const [name, root] of ruleMap) visit(root, ruleNameOf2(root) ?? name);
|
|
3257
3331
|
const perRule = /* @__PURE__ */ new Map();
|
|
3258
3332
|
for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
|
|
3259
3333
|
const seenInRule = /* @__PURE__ */ new Map();
|
|
@@ -3273,7 +3347,18 @@ function analyzeGatingRules(ruleMap, opts) {
|
|
|
3273
3347
|
const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
|
|
3274
3348
|
const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
|
|
3275
3349
|
const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
|
|
3276
|
-
return {
|
|
3350
|
+
return {
|
|
3351
|
+
totalChoices: choices.length,
|
|
3352
|
+
gated,
|
|
3353
|
+
recoverable,
|
|
3354
|
+
unanalysable,
|
|
3355
|
+
ungated,
|
|
3356
|
+
accepted,
|
|
3357
|
+
deferred,
|
|
3358
|
+
acceptedUnused,
|
|
3359
|
+
choices,
|
|
3360
|
+
antiPatterns
|
|
3361
|
+
};
|
|
3277
3362
|
}
|
|
3278
3363
|
function analyzeChoice(p, d, rule, resolve2) {
|
|
3279
3364
|
const arms = d.parsers;
|
|
@@ -3310,6 +3395,13 @@ function analyzeChoice(p, d, rule, resolve2) {
|
|
|
3310
3395
|
}
|
|
3311
3396
|
function formatGatingWarnings(report) {
|
|
3312
3397
|
const lines = [];
|
|
3398
|
+
if (report.unanalysable.length > 0) {
|
|
3399
|
+
lines.push(
|
|
3400
|
+
`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.`
|
|
3401
|
+
);
|
|
3402
|
+
for (const u of report.unanalysable)
|
|
3403
|
+
lines.push(` \xB7 ${u.rule} [${u.kind}]: ${u.reason}`);
|
|
3404
|
+
}
|
|
3313
3405
|
for (const c of report.ungated) {
|
|
3314
3406
|
lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
|
|
3315
3407
|
for (const a of c.anyArms)
|
|
@@ -3325,6 +3417,1284 @@ function formatGatingWarnings(report) {
|
|
|
3325
3417
|
return lines;
|
|
3326
3418
|
}
|
|
3327
3419
|
|
|
3420
|
+
// src/analysis/duplication.ts
|
|
3421
|
+
var siteToString = (s) => s.path === "" ? s.rule : `${s.rule} \u203A ${s.path}`;
|
|
3422
|
+
function childrenOf(d) {
|
|
3423
|
+
switch (d.tag) {
|
|
3424
|
+
case "sequence":
|
|
3425
|
+
case "choice":
|
|
3426
|
+
return d.parsers;
|
|
3427
|
+
case "skip":
|
|
3428
|
+
return [d.main, d.skipped];
|
|
3429
|
+
case "sepBy":
|
|
3430
|
+
return [d.parser, d.separator];
|
|
3431
|
+
case "recover":
|
|
3432
|
+
return [d.parser, d.sentinel];
|
|
3433
|
+
case "scanTo":
|
|
3434
|
+
return [d.sentinel, ...d.skip];
|
|
3435
|
+
case "grammar":
|
|
3436
|
+
return d.triviaParser ? [d.parser, d.triviaParser] : [d.parser];
|
|
3437
|
+
// A `lazy` is a REFERENCE, not a subtree: hashing through it would make every
|
|
3438
|
+
// rule's shape include the whole reachable grammar. Treated as a leaf, keyed
|
|
3439
|
+
// by the rule name it refers to — which is also the right semantics, since two
|
|
3440
|
+
// productions referencing `g.Ident` really do fill that slot the same way.
|
|
3441
|
+
case "lazy":
|
|
3442
|
+
case "literal":
|
|
3443
|
+
case "regex":
|
|
3444
|
+
case "keywords":
|
|
3445
|
+
case "guard":
|
|
3446
|
+
case "unknown":
|
|
3447
|
+
return [];
|
|
3448
|
+
default: {
|
|
3449
|
+
const rec = d;
|
|
3450
|
+
return rec.parser ? [rec.parser] : [];
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
var ruleNameOf3 = (p) => p._ruleName;
|
|
3455
|
+
var fnKey = (src, fn) => src ?? (typeof fn === "function" ? String(fn) : "");
|
|
3456
|
+
function payloadKey(p, d) {
|
|
3457
|
+
switch (d.tag) {
|
|
3458
|
+
case "literal":
|
|
3459
|
+
return `literal\0${d.value}\0${d.caseInsensitive}`;
|
|
3460
|
+
case "regex":
|
|
3461
|
+
return `regex\0${d.source}\0${d.flags}`;
|
|
3462
|
+
case "keywords":
|
|
3463
|
+
return `keywords\0${[...d.words].join("")}\0${d.caseInsensitive}\0${d.boundary ?? ""}`;
|
|
3464
|
+
case "lazy": {
|
|
3465
|
+
const n = ruleNameOf3(p);
|
|
3466
|
+
return n === void 0 ? `ref\0#${anonId(p)}` : `ref\0${n}`;
|
|
3467
|
+
}
|
|
3468
|
+
case "label":
|
|
3469
|
+
return `label\0${d.label}`;
|
|
3470
|
+
case "field":
|
|
3471
|
+
return `field\0${d.name}`;
|
|
3472
|
+
case "node":
|
|
3473
|
+
return `node\0${d.type ?? ""}\0${d.unwrap === true}\0${d.collapse === true}\0${fnKey(d.buildSrc, d.build)}`;
|
|
3474
|
+
case "transform":
|
|
3475
|
+
return `transform\0${fnKey(d.fnSrc, d.fn)}\0${d.recognitionOnly === true}`;
|
|
3476
|
+
case "leaf":
|
|
3477
|
+
return `leaf\0${fnKey(d.fnSrc, d.fn)}`;
|
|
3478
|
+
case "many":
|
|
3479
|
+
case "oneOrMore":
|
|
3480
|
+
return `${d.tag}\0${d.min}\0${d.max ?? ""}`;
|
|
3481
|
+
case "sepBy":
|
|
3482
|
+
return `sepBy\0${d.min}\0${d.max ?? ""}\0${d.trailing ?? ""}`;
|
|
3483
|
+
case "expect":
|
|
3484
|
+
return `expect\0${d.label ?? ""}\0${d.expected.join("")}`;
|
|
3485
|
+
case "scanTo":
|
|
3486
|
+
return `scanTo\0${d.raw}\0${d.orEOF}`;
|
|
3487
|
+
case "guard":
|
|
3488
|
+
return `guard\0${fnKey(d.predSrc, d.predicate)}`;
|
|
3489
|
+
case "withCtx":
|
|
3490
|
+
return `withCtx\0${d.extraSrc ?? safeJson(d.extra)}`;
|
|
3491
|
+
case "grammar":
|
|
3492
|
+
return `grammar\0${d.clearTrivia === true}\0${d.captureTrivia === true}\0${d.trackLines}`;
|
|
3493
|
+
default:
|
|
3494
|
+
return d.tag;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
function safeJson(v2) {
|
|
3498
|
+
try {
|
|
3499
|
+
return JSON.stringify(v2) ?? String(v2);
|
|
3500
|
+
} catch {
|
|
3501
|
+
return "?";
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
var anonCounter = 0;
|
|
3505
|
+
var anonIds = /* @__PURE__ */ new WeakMap();
|
|
3506
|
+
function anonId(p) {
|
|
3507
|
+
let id = anonIds.get(p);
|
|
3508
|
+
if (id === void 0) {
|
|
3509
|
+
id = ++anonCounter;
|
|
3510
|
+
anonIds.set(p, id);
|
|
3511
|
+
}
|
|
3512
|
+
return id;
|
|
3513
|
+
}
|
|
3514
|
+
var Interner = class {
|
|
3515
|
+
ids = /* @__PURE__ */ new Map();
|
|
3516
|
+
hole;
|
|
3517
|
+
constructor() {
|
|
3518
|
+
this.hole = this.of("\0HOLE\0");
|
|
3519
|
+
}
|
|
3520
|
+
of(key) {
|
|
3521
|
+
let id = this.ids.get(key);
|
|
3522
|
+
if (id === void 0) {
|
|
3523
|
+
id = this.ids.size;
|
|
3524
|
+
this.ids.set(key, id);
|
|
3525
|
+
}
|
|
3526
|
+
return id;
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
var MAX_RENDER = 160;
|
|
3530
|
+
function render(p, depth = 3) {
|
|
3531
|
+
const d = p._def;
|
|
3532
|
+
const name = ruleNameOf3(p);
|
|
3533
|
+
switch (d.tag) {
|
|
3534
|
+
case "literal":
|
|
3535
|
+
return `literal('${d.value}')`;
|
|
3536
|
+
case "regex":
|
|
3537
|
+
return `regex(/${d.source}/${d.flags})`;
|
|
3538
|
+
case "keywords":
|
|
3539
|
+
return `keywords([${[...d.words].slice(0, 4).map((w) => `'${w}'`).join(", ")}${d.words.length > 4 ? ", \u2026" : ""}])`;
|
|
3540
|
+
case "lazy":
|
|
3541
|
+
return `g.${name ?? "?"}`;
|
|
3542
|
+
case "guard":
|
|
3543
|
+
return "gate(\u2026)";
|
|
3544
|
+
default:
|
|
3545
|
+
break;
|
|
3546
|
+
}
|
|
3547
|
+
if (depth <= 0) return `${d.tag}(\u2026)`;
|
|
3548
|
+
const kids = childrenOf(d).map((k) => render(k, depth - 1));
|
|
3549
|
+
switch (d.tag) {
|
|
3550
|
+
case "sequence":
|
|
3551
|
+
return `sequence(${kids.join(", ")})`;
|
|
3552
|
+
case "choice":
|
|
3553
|
+
return `choice(${kids.join(", ")})`;
|
|
3554
|
+
case "node":
|
|
3555
|
+
return `node(${d.type ? `'${d.type}', ` : ""}${kids[0] ?? ""})`;
|
|
3556
|
+
case "label":
|
|
3557
|
+
return `label('${d.label}', ${kids[0] ?? ""})`;
|
|
3558
|
+
case "field":
|
|
3559
|
+
return `field('${d.name}', ${kids[0] ?? ""})`;
|
|
3560
|
+
case "many":
|
|
3561
|
+
return `many(${kids[0] ?? ""}${d.min === 0 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
|
|
3562
|
+
case "oneOrMore":
|
|
3563
|
+
return `oneOrMore(${kids[0] ?? ""}${d.min === 1 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
|
|
3564
|
+
case "sepBy":
|
|
3565
|
+
return `sepBy(${kids[0] ?? ""}, ${kids[1] ?? ""})`;
|
|
3566
|
+
default:
|
|
3567
|
+
return `${d.tag}(${kids.join(", ")})`;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
var clamp = (s) => s.length <= MAX_RENDER ? s : `${s.slice(0, MAX_RENDER - 1)}\u2026`;
|
|
3571
|
+
function segment(d, index) {
|
|
3572
|
+
switch (d.tag) {
|
|
3573
|
+
case "sequence":
|
|
3574
|
+
return `seq[${index}]`;
|
|
3575
|
+
case "choice":
|
|
3576
|
+
return `choice[${index}]`;
|
|
3577
|
+
case "node":
|
|
3578
|
+
return `node(${d.type ?? ""})`;
|
|
3579
|
+
case "label":
|
|
3580
|
+
return `label(${d.label})`;
|
|
3581
|
+
case "field":
|
|
3582
|
+
return `field(${d.name})`;
|
|
3583
|
+
case "sepBy":
|
|
3584
|
+
return index === 0 ? "sepBy.item" : "sepBy.sep";
|
|
3585
|
+
case "skip":
|
|
3586
|
+
return index === 0 ? "skip.main" : "skip.skipped";
|
|
3587
|
+
case "recover":
|
|
3588
|
+
return index === 0 ? "recover.body" : "recover.sentinel";
|
|
3589
|
+
case "scanTo":
|
|
3590
|
+
return index === 0 ? "scanTo.sentinel" : `scanTo.skip[${index - 1}]`;
|
|
3591
|
+
default:
|
|
3592
|
+
return d.tag;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
function alternationGroups(src) {
|
|
3596
|
+
const out = [];
|
|
3597
|
+
const stack = [{ branches: [], last: 0 }];
|
|
3598
|
+
let i = 0;
|
|
3599
|
+
while (i < src.length) {
|
|
3600
|
+
const c = src[i];
|
|
3601
|
+
if (c === "\\") {
|
|
3602
|
+
i += 2;
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (c === "[") {
|
|
3606
|
+
let j = i + 1;
|
|
3607
|
+
if (src[j] === "^") j++;
|
|
3608
|
+
if (src[j] === "]") j++;
|
|
3609
|
+
while (j < src.length && src[j] !== "]") {
|
|
3610
|
+
if (src[j] === "\\") j++;
|
|
3611
|
+
j++;
|
|
3612
|
+
}
|
|
3613
|
+
i = j + 1;
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
if (c === "(") {
|
|
3617
|
+
let open = i + 1;
|
|
3618
|
+
if (src[open] === "?") {
|
|
3619
|
+
const rest = src.slice(open);
|
|
3620
|
+
const m = /^\?(?::|=|!|<=|<!|<[^>]*>)/.exec(rest);
|
|
3621
|
+
open += m ? m[0].length : 1;
|
|
3622
|
+
}
|
|
3623
|
+
stack.push({ branches: [], last: open });
|
|
3624
|
+
i = open;
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
if (c === ")") {
|
|
3628
|
+
const top = stack.pop();
|
|
3629
|
+
if (top !== void 0) {
|
|
3630
|
+
top.branches.push(src.slice(top.last, i));
|
|
3631
|
+
if (top.branches.length >= 2) out.push(top.branches);
|
|
3632
|
+
}
|
|
3633
|
+
i++;
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
if (c === "|") {
|
|
3637
|
+
const top = stack[stack.length - 1];
|
|
3638
|
+
top.branches.push(src.slice(top.last, i));
|
|
3639
|
+
top.last = i + 1;
|
|
3640
|
+
i++;
|
|
3641
|
+
continue;
|
|
3642
|
+
}
|
|
3643
|
+
i++;
|
|
3644
|
+
}
|
|
3645
|
+
const root = stack[0];
|
|
3646
|
+
root.branches.push(src.slice(root.last));
|
|
3647
|
+
if (root.branches.length >= 2) out.push(root.branches);
|
|
3648
|
+
return out;
|
|
3649
|
+
}
|
|
3650
|
+
var WORD_RE_SRC = "-?[A-Za-z][A-Za-z0-9_-]*";
|
|
3651
|
+
var WORDS_RE = new RegExp(`^${WORD_RE_SRC}(?:\\|${WORD_RE_SRC})*$`);
|
|
3652
|
+
var VOCABULARY_MIN_WORDS = 3;
|
|
3653
|
+
function keywordRegexShape(source) {
|
|
3654
|
+
let s = source;
|
|
3655
|
+
if (s.startsWith("^")) s = s.slice(1);
|
|
3656
|
+
s = s.replace(/^\(\?<!\[[^\]]+\]\)/, "");
|
|
3657
|
+
let boundary = null;
|
|
3658
|
+
const tail = /(?:\(\?!\[([^\]]+)\]\)|\\b)$/.exec(s);
|
|
3659
|
+
if (tail !== null) {
|
|
3660
|
+
const cls = tail[1];
|
|
3661
|
+
if (cls !== void 0 && cls.startsWith("^")) return null;
|
|
3662
|
+
boundary = cls ?? "_0-9A-Za-z";
|
|
3663
|
+
s = s.slice(0, tail.index);
|
|
3664
|
+
}
|
|
3665
|
+
const grouped = /^\(\?:([^()]*)\)$/.exec(s);
|
|
3666
|
+
if (grouped !== null) s = grouped[1];
|
|
3667
|
+
if (s.endsWith("$")) s = s.slice(0, -1);
|
|
3668
|
+
if (!WORDS_RE.test(s)) return null;
|
|
3669
|
+
const words = s.split("|");
|
|
3670
|
+
if (boundary === null && words.length < VOCABULARY_MIN_WORDS) return null;
|
|
3671
|
+
return { words, boundary };
|
|
3672
|
+
}
|
|
3673
|
+
function boundaryMatches(boundary, ch2) {
|
|
3674
|
+
const cp = ch2.codePointAt(0);
|
|
3675
|
+
for (const m of charClassMembers(boundary)) {
|
|
3676
|
+
if (m === "\\w") {
|
|
3677
|
+
if (/[A-Za-z0-9_]/.test(ch2)) return true;
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
if (m === "\\d") {
|
|
3681
|
+
if (/[0-9]/.test(ch2)) return true;
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
if (m === "\\s") {
|
|
3685
|
+
if (/\s/.test(ch2)) return true;
|
|
3686
|
+
continue;
|
|
3687
|
+
}
|
|
3688
|
+
const range = /^(.|\\u[0-9a-f]{4})-(.|\\u[0-9a-f]{4})$/.exec(m);
|
|
3689
|
+
if (range !== null) {
|
|
3690
|
+
const dec = (t) => t.startsWith("\\u") ? parseInt(t.slice(2), 16) : t.codePointAt(0);
|
|
3691
|
+
if (cp >= dec(range[1]) && cp <= dec(range[2])) return true;
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
if (m === ch2) return true;
|
|
3695
|
+
}
|
|
3696
|
+
return false;
|
|
3697
|
+
}
|
|
3698
|
+
function keywordAlternationHazards(words, boundary) {
|
|
3699
|
+
const out = [];
|
|
3700
|
+
for (let i = 0; i < words.length; i++) {
|
|
3701
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
3702
|
+
const a = words[i], b = words[j];
|
|
3703
|
+
if (a.length >= b.length || !b.startsWith(a)) continue;
|
|
3704
|
+
const at = b[a.length];
|
|
3705
|
+
out.push({ shorter: a, longer: b, at, rescuedByBoundary: boundary !== null && boundaryMatches(boundary, at) });
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return out;
|
|
3709
|
+
}
|
|
3710
|
+
function analyzeDuplication(entry, opts) {
|
|
3711
|
+
return analyzeDuplicationRules([[ruleNameOf3(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
|
|
3712
|
+
}
|
|
3713
|
+
function assertAnalyzable(ruleMap) {
|
|
3714
|
+
for (const [name, rule] of ruleMap) {
|
|
3715
|
+
const def = rule?._def;
|
|
3716
|
+
if (def !== null && typeof def === "object" && typeof def.tag === "string") continue;
|
|
3717
|
+
throw new TypeError(
|
|
3718
|
+
`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.`
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
function analyzeDuplicationRules(ruleMap, opts) {
|
|
3723
|
+
assertAnalyzable(ruleMap);
|
|
3724
|
+
const minSize = opts?.minSize ?? 3;
|
|
3725
|
+
const maxFindings = opts?.maxFindings ?? 25;
|
|
3726
|
+
const interner = new Interner();
|
|
3727
|
+
const shapeCache = /* @__PURE__ */ new Map();
|
|
3728
|
+
const sizeCache = /* @__PURE__ */ new Map();
|
|
3729
|
+
const shapeOf = (p) => {
|
|
3730
|
+
const hit = shapeCache.get(p);
|
|
3731
|
+
if (hit !== void 0) return hit;
|
|
3732
|
+
const d = p._def;
|
|
3733
|
+
shapeCache.set(p, interner.hole);
|
|
3734
|
+
const kids = childrenOf(d);
|
|
3735
|
+
const id = interner.of(`${payloadKey(p, d)}|${kids.map(shapeOf).join(",")}`);
|
|
3736
|
+
shapeCache.set(p, id);
|
|
3737
|
+
return id;
|
|
3738
|
+
};
|
|
3739
|
+
const sizeOf = (p) => {
|
|
3740
|
+
const hit = sizeCache.get(p);
|
|
3741
|
+
if (hit !== void 0) return hit;
|
|
3742
|
+
sizeCache.set(p, 1);
|
|
3743
|
+
let n = 1;
|
|
3744
|
+
for (const k of childrenOf(p._def)) n += sizeOf(k);
|
|
3745
|
+
sizeCache.set(p, n);
|
|
3746
|
+
return n;
|
|
3747
|
+
};
|
|
3748
|
+
const visited = /* @__PURE__ */ new Map();
|
|
3749
|
+
const visit = (p, rule, path2, enclosingNode, enclosingReducer) => {
|
|
3750
|
+
if (visited.has(p)) return;
|
|
3751
|
+
const d = p._def;
|
|
3752
|
+
const own = ruleNameOf3(p);
|
|
3753
|
+
const here = own !== void 0 && own !== rule ? { rule: own, path: "" } : { rule, path: path2 };
|
|
3754
|
+
visited.set(p, { p, d, site: here, shape: shapeOf(p), size: sizeOf(p), enclosingNode, enclosingReducer });
|
|
3755
|
+
const inner = d.tag === "node" ? d.type ?? "(anonymous)" : enclosingNode;
|
|
3756
|
+
const innerReducer = enclosingReducer || d.tag === "node" && (d.build !== void 0 || d.buildSrc !== void 0) || d.tag === "transform" || d.tag === "leaf";
|
|
3757
|
+
const kids = childrenOf(d);
|
|
3758
|
+
kids.forEach((k, i) => {
|
|
3759
|
+
const seg = segment(d, i);
|
|
3760
|
+
visit(k, here.rule, here.path === "" ? seg : `${here.path} \u203A ${seg}`, inner, innerReducer);
|
|
3761
|
+
});
|
|
3762
|
+
if (d.tag === "lazy") {
|
|
3763
|
+
try {
|
|
3764
|
+
const target = d.thunk();
|
|
3765
|
+
visit(target, ruleNameOf3(target) ?? ruleNameOf3(p) ?? here.rule, "", null, false);
|
|
3766
|
+
} catch {
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
};
|
|
3770
|
+
for (const [name, root] of ruleMap) visit(root, ruleNameOf3(root) ?? name, "", null, false);
|
|
3771
|
+
const accept = new Set(opts?.accept ?? []);
|
|
3772
|
+
const used = /* @__PURE__ */ new Set();
|
|
3773
|
+
const keep = (f) => {
|
|
3774
|
+
if (!accept.has(f.id)) return true;
|
|
3775
|
+
used.add(f.id);
|
|
3776
|
+
return false;
|
|
3777
|
+
};
|
|
3778
|
+
const duplicates = findExactDuplicates(visited, minSize);
|
|
3779
|
+
const nearDuplicates = findNearDuplicates(visited, interner, shapeOf, minSize);
|
|
3780
|
+
const regexFragments = findRegexFragments(visited);
|
|
3781
|
+
const regexClasses = findRegexClasses(visited);
|
|
3782
|
+
const overlaps = findOverlaps(visited, shapeOf, opts?.resolveRef);
|
|
3783
|
+
const rewrites = findRewrites(visited, shapeOf);
|
|
3784
|
+
const divergentNodes = findDivergentNodes(visited, shapeOf);
|
|
3785
|
+
const structureLoss = findStructureLoss(visited, opts?.resolveRef);
|
|
3786
|
+
const keywordRegexes = findKeywordRegexes(visited);
|
|
3787
|
+
const subsume = (fs2) => {
|
|
3788
|
+
const covered = /* @__PURE__ */ new Set();
|
|
3789
|
+
const surviving = /* @__PURE__ */ new Set();
|
|
3790
|
+
for (const f of [...fs2].sort((a, b) => b.savings - a.savings)) {
|
|
3791
|
+
if (f._members.every((m) => covered.has(m))) continue;
|
|
3792
|
+
surviving.add(f.id);
|
|
3793
|
+
for (const m of f._members) markSubtree(m, covered);
|
|
3794
|
+
}
|
|
3795
|
+
return surviving;
|
|
3796
|
+
};
|
|
3797
|
+
const survivingExact = subsume(duplicates);
|
|
3798
|
+
const survivingNear = subsume(nearDuplicates);
|
|
3799
|
+
const report = {
|
|
3800
|
+
duplicates: duplicates.filter((f) => survivingExact.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
|
|
3801
|
+
nearDuplicates: nearDuplicates.filter((f) => survivingNear.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
|
|
3802
|
+
regexFragments: regexFragments.filter(keep).slice(0, maxFindings),
|
|
3803
|
+
regexClasses: regexClasses.filter(keep).slice(0, maxFindings),
|
|
3804
|
+
overlaps: overlaps.filter(keep).slice(0, maxFindings),
|
|
3805
|
+
rewrites: rewrites.filter(keep).slice(0, maxFindings),
|
|
3806
|
+
divergentNodes: divergentNodes.filter(keep).slice(0, maxFindings),
|
|
3807
|
+
structureLoss: structureLoss.filter(keep).slice(0, maxFindings),
|
|
3808
|
+
keywordRegexes: keywordRegexes.filter(keep).slice(0, maxFindings),
|
|
3809
|
+
acceptedUnused: [...accept].filter((id) => !used.has(id)),
|
|
3810
|
+
stats: { rules: ruleMap.length, nodes: visited.size, shapes: new Set([...visited.values()].map((v2) => v2.shape)).size }
|
|
3811
|
+
};
|
|
3812
|
+
return report;
|
|
3813
|
+
}
|
|
3814
|
+
function markSubtree(p, into) {
|
|
3815
|
+
if (into.has(p)) return;
|
|
3816
|
+
into.add(p);
|
|
3817
|
+
for (const k of childrenOf(p._def)) markSubtree(k, into);
|
|
3818
|
+
}
|
|
3819
|
+
var stripMembers = (f) => {
|
|
3820
|
+
const { _members, ...rest } = f;
|
|
3821
|
+
void _members;
|
|
3822
|
+
return rest;
|
|
3823
|
+
};
|
|
3824
|
+
function findExactDuplicates(visited, minSize) {
|
|
3825
|
+
const byShape = /* @__PURE__ */ new Map();
|
|
3826
|
+
for (const v2 of visited.values()) {
|
|
3827
|
+
if (v2.size < minSize) continue;
|
|
3828
|
+
const list = byShape.get(v2.shape);
|
|
3829
|
+
if (list === void 0) byShape.set(v2.shape, [v2]);
|
|
3830
|
+
else list.push(v2);
|
|
3831
|
+
}
|
|
3832
|
+
const out = [];
|
|
3833
|
+
for (const group of byShape.values()) {
|
|
3834
|
+
if (group.length < 2) continue;
|
|
3835
|
+
const size = group[0].size;
|
|
3836
|
+
out.push({
|
|
3837
|
+
kind: "exact-duplicate",
|
|
3838
|
+
id: `exact-duplicate:${siteToString(group[0].site)}`,
|
|
3839
|
+
size,
|
|
3840
|
+
count: group.length,
|
|
3841
|
+
savings: (group.length - 1) * size,
|
|
3842
|
+
shape: clamp(render(group[0].p)),
|
|
3843
|
+
sites: group.map((g) => g.site),
|
|
3844
|
+
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}.`,
|
|
3845
|
+
_members: group.map((g) => g.p)
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3848
|
+
return out.sort((a, b) => b.savings - a.savings);
|
|
3849
|
+
}
|
|
3850
|
+
function findNearDuplicates(visited, interner, shapeOf, minSize) {
|
|
3851
|
+
const holeCache = /* @__PURE__ */ new Map();
|
|
3852
|
+
const holesOf = (p) => {
|
|
3853
|
+
const hit = holeCache.get(p);
|
|
3854
|
+
if (hit !== void 0) return hit;
|
|
3855
|
+
holeCache.set(p, []);
|
|
3856
|
+
const d = p._def;
|
|
3857
|
+
const kids = childrenOf(d);
|
|
3858
|
+
const kidShapes = kids.map(shapeOf);
|
|
3859
|
+
const payload = payloadKey(p, d);
|
|
3860
|
+
const out2 = [];
|
|
3861
|
+
kids.forEach((k, i) => {
|
|
3862
|
+
for (const inner of [interner.hole, ...holesOf(k)]) {
|
|
3863
|
+
const arr = kidShapes.slice();
|
|
3864
|
+
arr[i] = inner;
|
|
3865
|
+
out2.push(interner.of(`${payload}|${arr.join(",")}`));
|
|
3866
|
+
}
|
|
3867
|
+
});
|
|
3868
|
+
holeCache.set(p, out2);
|
|
3869
|
+
return out2;
|
|
3870
|
+
};
|
|
3871
|
+
const byHole = /* @__PURE__ */ new Map();
|
|
3872
|
+
for (const v2 of visited.values()) {
|
|
3873
|
+
if (v2.size < minSize) continue;
|
|
3874
|
+
for (const h of holesOf(v2.p)) {
|
|
3875
|
+
const list = byHole.get(h);
|
|
3876
|
+
if (list === void 0) byHole.set(h, [v2]);
|
|
3877
|
+
else list.push(v2);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
const out = [];
|
|
3881
|
+
const seenGroups = /* @__PURE__ */ new Set();
|
|
3882
|
+
for (const group of byHole.values()) {
|
|
3883
|
+
if (group.length < 2) continue;
|
|
3884
|
+
const shapes = new Set(group.map((g) => g.shape));
|
|
3885
|
+
if (shapes.size < 2) continue;
|
|
3886
|
+
const perShape = /* @__PURE__ */ new Map();
|
|
3887
|
+
for (const g of group) if (!perShape.has(g.shape)) perShape.set(g.shape, g);
|
|
3888
|
+
const members = [...perShape.values()];
|
|
3889
|
+
const key = members.map((m) => m.shape).sort((a, b) => a - b).join(",");
|
|
3890
|
+
if (seenGroups.has(key)) continue;
|
|
3891
|
+
seenGroups.add(key);
|
|
3892
|
+
const slot = findDivergentSlot(members.map((m) => m.p), shapeOf);
|
|
3893
|
+
if (slot === null) continue;
|
|
3894
|
+
const sharedSize = Math.min(...members.map((m) => m.size)) - slot.slotSize;
|
|
3895
|
+
if (sharedSize < minSize) continue;
|
|
3896
|
+
out.push({
|
|
3897
|
+
kind: "near-duplicate",
|
|
3898
|
+
id: `near-duplicate:${siteToString(members[0].site)}`,
|
|
3899
|
+
sharedSize,
|
|
3900
|
+
count: members.length,
|
|
3901
|
+
savings: (members.length - 1) * sharedSize,
|
|
3902
|
+
shape: clamp(renderWithHole(members[0].p, slot.path)),
|
|
3903
|
+
slotPath: slot.path.map((i, k) => segment(slot.defs[k], i)).join(" \u203A "),
|
|
3904
|
+
variants: slot.variants.map((v2) => clamp(render(v2))),
|
|
3905
|
+
sites: members.map((m) => m.site),
|
|
3906
|
+
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.`,
|
|
3907
|
+
_members: members.map((m) => m.p)
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
return out.sort((a, b) => b.savings - a.savings);
|
|
3911
|
+
}
|
|
3912
|
+
function findDivergentSlot(members, shapeOf) {
|
|
3913
|
+
const path2 = [];
|
|
3914
|
+
const defs = [];
|
|
3915
|
+
let cur = [...members];
|
|
3916
|
+
for (let guard2 = 0; guard2 < 64; guard2++) {
|
|
3917
|
+
const d0 = cur[0]._def;
|
|
3918
|
+
const kidLists = cur.map((c) => childrenOf(c._def));
|
|
3919
|
+
const arity = kidLists[0].length;
|
|
3920
|
+
if (kidLists.some((k) => k.length !== arity) || arity === 0) break;
|
|
3921
|
+
const diff = [];
|
|
3922
|
+
for (let i2 = 0; i2 < arity; i2++) {
|
|
3923
|
+
const s = shapeOf(kidLists[0][i2]);
|
|
3924
|
+
if (kidLists.some((k) => shapeOf(k[i2]) !== s)) diff.push(i2);
|
|
3925
|
+
}
|
|
3926
|
+
if (diff.length !== 1) break;
|
|
3927
|
+
const i = diff[0];
|
|
3928
|
+
path2.push(i);
|
|
3929
|
+
defs.push(d0);
|
|
3930
|
+
cur = kidLists.map((k) => k[i]);
|
|
3931
|
+
const nextArity = childrenOf(cur[0]._def).length;
|
|
3932
|
+
const samePayload = cur.every((c) => payloadKey(c, c._def) === payloadKey(cur[0], cur[0]._def));
|
|
3933
|
+
if (!samePayload || nextArity === 0) break;
|
|
3934
|
+
}
|
|
3935
|
+
if (path2.length === 0) return null;
|
|
3936
|
+
let slotSize = 0;
|
|
3937
|
+
const sizeOf = (p) => {
|
|
3938
|
+
let n = 1;
|
|
3939
|
+
for (const k of childrenOf(p._def)) n += sizeOf(k);
|
|
3940
|
+
return n;
|
|
3941
|
+
};
|
|
3942
|
+
for (const c of cur) slotSize = Math.max(slotSize, sizeOf(c));
|
|
3943
|
+
return { path: path2, defs, variants: cur, slotSize };
|
|
3944
|
+
}
|
|
3945
|
+
function renderWithHole(p, path2, depth = 0) {
|
|
3946
|
+
if (depth === path2.length) return "\u2039slot\u203A";
|
|
3947
|
+
const d = p._def;
|
|
3948
|
+
const kids = childrenOf(d);
|
|
3949
|
+
const idx = path2[depth];
|
|
3950
|
+
const parts = kids.map((k, i) => i === idx ? renderWithHole(k, path2, depth + 1) : render(k, 2));
|
|
3951
|
+
switch (d.tag) {
|
|
3952
|
+
case "sequence":
|
|
3953
|
+
return `sequence(${parts.join(", ")})`;
|
|
3954
|
+
case "choice":
|
|
3955
|
+
return `choice(${parts.join(", ")})`;
|
|
3956
|
+
case "node":
|
|
3957
|
+
return `node(${d.type ? `'${d.type}', ` : ""}${parts[0] ?? ""})`;
|
|
3958
|
+
default:
|
|
3959
|
+
return `${d.tag}(${parts.join(", ")})`;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
var MAX_RUN = 32;
|
|
3963
|
+
function findRegexFragments(visited) {
|
|
3964
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
3965
|
+
for (const v2 of visited.values()) {
|
|
3966
|
+
const d = v2.d;
|
|
3967
|
+
if (d.tag !== "regex") continue;
|
|
3968
|
+
const runs = /* @__PURE__ */ new Set();
|
|
3969
|
+
for (const branches of alternationGroups(d.source)) {
|
|
3970
|
+
if (branches.length >= 2) runs.add(branches.join("|"));
|
|
3971
|
+
for (let i = 0; i < branches.length; i++) {
|
|
3972
|
+
for (let n = 2; n <= Math.min(MAX_RUN, branches.length - i); n++) {
|
|
3973
|
+
const run2 = branches.slice(i, i + n).join("|");
|
|
3974
|
+
if (run2.length < 4) continue;
|
|
3975
|
+
runs.add(run2);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
for (const run2 of runs) {
|
|
3980
|
+
let e = byRun.get(run2);
|
|
3981
|
+
if (e === void 0) {
|
|
3982
|
+
e = { occ: [], nodes: /* @__PURE__ */ new Set(), branches: run2.split("|").length };
|
|
3983
|
+
byRun.set(run2, e);
|
|
3984
|
+
}
|
|
3985
|
+
if (e.nodes.has(v2.p)) continue;
|
|
3986
|
+
e.nodes.add(v2.p);
|
|
3987
|
+
e.occ.push({ site: v2.site, source: d.source });
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
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);
|
|
3991
|
+
const kept = [];
|
|
3992
|
+
for (const c of candidates) {
|
|
3993
|
+
if (kept.some((k) => k.e.occ.length >= c.e.occ.length && `|${k.run}|`.includes(`|${c.run}|`))) continue;
|
|
3994
|
+
kept.push(c);
|
|
3995
|
+
}
|
|
3996
|
+
return kept.map(({ run: run2, e }) => ({
|
|
3997
|
+
kind: "regex-fragment",
|
|
3998
|
+
id: `regex-fragment:${run2}`,
|
|
3999
|
+
fragment: run2,
|
|
4000
|
+
branches: e.branches,
|
|
4001
|
+
count: e.occ.length,
|
|
4002
|
+
savings: (e.occ.length - 1) * run2.length,
|
|
4003
|
+
sources: e.occ.map((o) => `/${o.source}/`),
|
|
4004
|
+
sites: e.occ.map((o) => o.site),
|
|
4005
|
+
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.`
|
|
4006
|
+
}));
|
|
4007
|
+
}
|
|
4008
|
+
function extractCharClasses(src) {
|
|
4009
|
+
const out = [];
|
|
4010
|
+
let i = 0;
|
|
4011
|
+
while (i < src.length) {
|
|
4012
|
+
const c = src[i];
|
|
4013
|
+
if (c === "\\") {
|
|
4014
|
+
i += 2;
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
if (c !== "[") {
|
|
4018
|
+
i++;
|
|
4019
|
+
continue;
|
|
4020
|
+
}
|
|
4021
|
+
let j = i + 1;
|
|
4022
|
+
if (src[j] === "^") j++;
|
|
4023
|
+
if (src[j] === "]") j++;
|
|
4024
|
+
while (j < src.length && src[j] !== "]") {
|
|
4025
|
+
if (src[j] === "\\") j++;
|
|
4026
|
+
j++;
|
|
4027
|
+
}
|
|
4028
|
+
out.push(src.slice(i + 1, j));
|
|
4029
|
+
i = j + 1;
|
|
4030
|
+
}
|
|
4031
|
+
return out;
|
|
4032
|
+
}
|
|
4033
|
+
function charClassMembers(body) {
|
|
4034
|
+
const esc = (ch2) => {
|
|
4035
|
+
const cp = ch2.codePointAt(0);
|
|
4036
|
+
return cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : ch2;
|
|
4037
|
+
};
|
|
4038
|
+
const atoms = [];
|
|
4039
|
+
let i = 0;
|
|
4040
|
+
while (i < body.length) {
|
|
4041
|
+
if (body[i] === "\\") {
|
|
4042
|
+
const m = /^\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2}))/.exec(body.slice(i));
|
|
4043
|
+
if (m !== null) {
|
|
4044
|
+
const cp = parseInt(m[1] ?? m[2] ?? m[3], 16);
|
|
4045
|
+
atoms.push(cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : String.fromCodePoint(cp));
|
|
4046
|
+
i += m[0].length;
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
atoms.push(body.slice(i, i + 2));
|
|
4050
|
+
i += 2;
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
atoms.push(esc(body[i]));
|
|
4054
|
+
i++;
|
|
4055
|
+
}
|
|
4056
|
+
const members = [];
|
|
4057
|
+
for (let k = 0; k < atoms.length; k++) {
|
|
4058
|
+
if (k + 2 < atoms.length && atoms[k + 1] === "-" && atoms[k + 2] !== void 0) {
|
|
4059
|
+
members.push(`${atoms[k]}-${atoms[k + 2]}`);
|
|
4060
|
+
k += 2;
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
4063
|
+
members.push(atoms[k]);
|
|
4064
|
+
}
|
|
4065
|
+
return members;
|
|
4066
|
+
}
|
|
4067
|
+
var CLASS_MIN_MEMBERS = 2;
|
|
4068
|
+
function findRegexClasses(visited) {
|
|
4069
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
4070
|
+
for (const v2 of visited.values()) {
|
|
4071
|
+
if (v2.d.tag !== "regex") continue;
|
|
4072
|
+
for (const body of new Set(extractCharClasses(v2.d.source))) {
|
|
4073
|
+
const members = charClassMembers(body);
|
|
4074
|
+
if (members.length < CLASS_MIN_MEMBERS) continue;
|
|
4075
|
+
const key = [...new Set(members)].sort().join("");
|
|
4076
|
+
let s = spellings.get(key);
|
|
4077
|
+
if (s === void 0) {
|
|
4078
|
+
s = { source: body, members, key, nodes: /* @__PURE__ */ new Set(), sites: [] };
|
|
4079
|
+
spellings.set(key, s);
|
|
4080
|
+
}
|
|
4081
|
+
if (s.nodes.has(v2.p)) continue;
|
|
4082
|
+
s.nodes.add(v2.p);
|
|
4083
|
+
s.sites.push(v2.site);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
const MAX_DELTA = 3;
|
|
4087
|
+
const MIN_JACCARD = 0.6;
|
|
4088
|
+
const all = [...spellings.values()];
|
|
4089
|
+
const parent = all.map((_, i) => i);
|
|
4090
|
+
const find = (i) => {
|
|
4091
|
+
while (parent[i] !== i) {
|
|
4092
|
+
parent[i] = parent[parent[i]];
|
|
4093
|
+
i = parent[i];
|
|
4094
|
+
}
|
|
4095
|
+
return i;
|
|
4096
|
+
};
|
|
4097
|
+
const union2 = (a, b) => {
|
|
4098
|
+
const ra = find(a), rb = find(b);
|
|
4099
|
+
if (ra !== rb) parent[ra] = rb;
|
|
4100
|
+
};
|
|
4101
|
+
for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) {
|
|
4102
|
+
if (all[i].members.includes("^") !== all[j].members.includes("^")) continue;
|
|
4103
|
+
const A = new Set(all[i].members), B = new Set(all[j].members);
|
|
4104
|
+
let shared = 0;
|
|
4105
|
+
for (const m of A) if (B.has(m)) shared++;
|
|
4106
|
+
const delta = A.size - shared + (B.size - shared);
|
|
4107
|
+
const jaccard = shared / (shared + delta);
|
|
4108
|
+
if (shared >= 2 && delta > 0 && delta <= MAX_DELTA && jaccard >= MIN_JACCARD) union2(i, j);
|
|
4109
|
+
}
|
|
4110
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
4111
|
+
all.forEach((_, i) => {
|
|
4112
|
+
const r = find(i);
|
|
4113
|
+
const list = clusters.get(r);
|
|
4114
|
+
if (list === void 0) clusters.set(r, [i]);
|
|
4115
|
+
else list.push(i);
|
|
4116
|
+
});
|
|
4117
|
+
const out = [];
|
|
4118
|
+
for (const idxs of clusters.values()) {
|
|
4119
|
+
const members = idxs.map((i) => all[i]).sort((a, b) => b.nodes.size - a.nodes.size);
|
|
4120
|
+
const total = members.reduce((n, m) => n + m.nodes.size, 0);
|
|
4121
|
+
const drifted = members.length > 1;
|
|
4122
|
+
if (!drifted && total < 2) continue;
|
|
4123
|
+
const base = new Set(members[0].members);
|
|
4124
|
+
const variants = members.map((m) => {
|
|
4125
|
+
const mine = new Set(m.members);
|
|
4126
|
+
const plus = [...mine].filter((x) => !base.has(x));
|
|
4127
|
+
const minus = [...base].filter((x) => !mine.has(x));
|
|
4128
|
+
return {
|
|
4129
|
+
source: m.source,
|
|
4130
|
+
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)",
|
|
4131
|
+
count: m.nodes.size,
|
|
4132
|
+
sites: m.sites
|
|
4133
|
+
};
|
|
4134
|
+
});
|
|
4135
|
+
const bmpCeiling = members.some((m) => m.members.some((x) => x.endsWith("-\\uffff")));
|
|
4136
|
+
out.push({
|
|
4137
|
+
kind: "regex-class",
|
|
4138
|
+
id: `regex-class:${members[0].key}`,
|
|
4139
|
+
canonical: members[0].source,
|
|
4140
|
+
variants,
|
|
4141
|
+
count: total,
|
|
4142
|
+
drifted,
|
|
4143
|
+
bmpCeiling,
|
|
4144
|
+
suggestion: [
|
|
4145
|
+
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.`,
|
|
4146
|
+
`Prefer the shared recognition surface where one already covers this class rather than a local extraction.`,
|
|
4147
|
+
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.` : "",
|
|
4148
|
+
`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.`
|
|
4149
|
+
].filter((s) => s !== "").join(" ")
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
return out.sort((a, b) => Number(b.drifted) - Number(a.drifted) || b.count - a.count);
|
|
4153
|
+
}
|
|
4154
|
+
function intersects3(a, b) {
|
|
4155
|
+
if (a.kind === "any" || b.kind === "any") return true;
|
|
4156
|
+
if (a.kind === "empty" || b.kind === "empty") return false;
|
|
4157
|
+
for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
|
|
4158
|
+
return false;
|
|
4159
|
+
}
|
|
4160
|
+
function intersection2(a, b) {
|
|
4161
|
+
if (a.kind === "any") return b;
|
|
4162
|
+
if (b.kind === "any") return a;
|
|
4163
|
+
if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
|
|
4164
|
+
const ranges = [];
|
|
4165
|
+
for (const ra of a.ranges) for (const rb of b.ranges) {
|
|
4166
|
+
const lo = Math.max(ra.lo, rb.lo);
|
|
4167
|
+
const hi = Math.min(ra.hi, rb.hi);
|
|
4168
|
+
if (lo <= hi) ranges.push({ lo, hi });
|
|
4169
|
+
}
|
|
4170
|
+
return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
|
|
4171
|
+
}
|
|
4172
|
+
function termsOf(arm) {
|
|
4173
|
+
let cur = arm;
|
|
4174
|
+
for (; ; ) {
|
|
4175
|
+
const d2 = cur._def;
|
|
4176
|
+
if (d2.tag === "node" || d2.tag === "transform" || d2.tag === "label" || d2.tag === "attempt" || d2.tag === "leaf") {
|
|
4177
|
+
cur = d2.parser;
|
|
4178
|
+
continue;
|
|
4179
|
+
}
|
|
4180
|
+
break;
|
|
4181
|
+
}
|
|
4182
|
+
const d = cur._def;
|
|
4183
|
+
return d.tag === "sequence" ? d.parsers : [cur];
|
|
4184
|
+
}
|
|
4185
|
+
function leadingRegex(arm) {
|
|
4186
|
+
const t = termsOf(arm)[0];
|
|
4187
|
+
return t !== void 0 && t._def.tag === "regex";
|
|
4188
|
+
}
|
|
4189
|
+
function findOverlaps(visited, shapeOf, resolve2) {
|
|
4190
|
+
const out = [];
|
|
4191
|
+
for (const v2 of visited.values()) {
|
|
4192
|
+
const d = v2.d;
|
|
4193
|
+
if (d.tag !== "choice") continue;
|
|
4194
|
+
const arms = d.parsers;
|
|
4195
|
+
const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
|
|
4196
|
+
const strategy = d.strategy;
|
|
4197
|
+
for (let i = 0; i < arms.length; i++) {
|
|
4198
|
+
for (let j = i + 1; j < arms.length; j++) {
|
|
4199
|
+
const A = fs2[i], B = fs2[j];
|
|
4200
|
+
if (A.kind === "any" || B.kind === "any") continue;
|
|
4201
|
+
if (!intersects3(A, B)) continue;
|
|
4202
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4203
|
+
let shared = 0;
|
|
4204
|
+
while (shared < ta.length && shared < tb.length && shapeOf(ta[shared]) === shapeOf(tb[shared])) shared++;
|
|
4205
|
+
const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
|
|
4206
|
+
const regexPair = leadingRegex(arms[i]) && leadingRegex(arms[j]);
|
|
4207
|
+
out.push({
|
|
4208
|
+
kind: "arm-overlap",
|
|
4209
|
+
id: `arm-overlap:${siteToString(v2.site)}:${i}-${j}`,
|
|
4210
|
+
site: v2.site,
|
|
4211
|
+
a: i,
|
|
4212
|
+
b: j,
|
|
4213
|
+
on: intersection2(A, B),
|
|
4214
|
+
sharedLeadingTerms: shared,
|
|
4215
|
+
sharedPrefix: shared > 0 ? clamp(ta.slice(0, shared).map((t) => render(t, 2)).join(", ")) : null,
|
|
4216
|
+
handledByStrategy: handled,
|
|
4217
|
+
regexPair,
|
|
4218
|
+
suggestion: overlapSuggestion(shared, handled, regexPair)
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
return out.sort((a, b) => b.sharedLeadingTerms - a.sharedLeadingTerms);
|
|
4224
|
+
}
|
|
4225
|
+
function overlapSuggestion(shared, handled, regexPair) {
|
|
4226
|
+
if (shared > 0 && handled)
|
|
4227
|
+
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.`;
|
|
4228
|
+
if (shared > 0)
|
|
4229
|
+
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.`;
|
|
4230
|
+
if (regexPair)
|
|
4231
|
+
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.`;
|
|
4232
|
+
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.`;
|
|
4233
|
+
}
|
|
4234
|
+
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.";
|
|
4235
|
+
var PERF_NONE = "";
|
|
4236
|
+
function findRewrites(visited, shapeOf) {
|
|
4237
|
+
const out = [];
|
|
4238
|
+
const push = (f, at) => {
|
|
4239
|
+
const site = siteToString(f.site);
|
|
4240
|
+
out.push({ kind: "rewrite", id: `rewrite:${f.rewrite}:${site}${at === void 0 ? "" : `:${at}`}`, ...f });
|
|
4241
|
+
};
|
|
4242
|
+
for (const v2 of visited.values()) {
|
|
4243
|
+
const d = v2.d;
|
|
4244
|
+
const site = v2.site;
|
|
4245
|
+
if (d.tag === "optional" || d.tag === "many" || d.tag === "oneOrMore") {
|
|
4246
|
+
const inner = d.parser;
|
|
4247
|
+
const id = inner._def;
|
|
4248
|
+
const collapsed = collapseRepeat(d, id);
|
|
4249
|
+
if (collapsed !== null) {
|
|
4250
|
+
push({
|
|
4251
|
+
rewrite: "idempotent-nesting",
|
|
4252
|
+
site,
|
|
4253
|
+
from: clamp(render(v2.p, 2)),
|
|
4254
|
+
to: collapsed,
|
|
4255
|
+
astNeutral: false,
|
|
4256
|
+
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.",
|
|
4257
|
+
bug: id.tag === "optional" && d.tag === "many",
|
|
4258
|
+
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.`
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
if ((d.tag === "choice" || d.tag === "sequence") && d.parsers.length === 1) {
|
|
4263
|
+
push({
|
|
4264
|
+
rewrite: "single-element",
|
|
4265
|
+
site,
|
|
4266
|
+
from: clamp(render(v2.p, 2)),
|
|
4267
|
+
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)`,
|
|
4268
|
+
astNeutral: d.tag === "choice",
|
|
4269
|
+
perf: d.tag === "choice" ? "a one-arm choice still emits its dispatch scaffolding." : PERF_NONE,
|
|
4270
|
+
bug: false,
|
|
4271
|
+
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."
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
if (d.tag !== "choice") continue;
|
|
4275
|
+
const arms = d.parsers;
|
|
4276
|
+
const armShapes = arms.map(shapeOf);
|
|
4277
|
+
const strategy = d.strategy;
|
|
4278
|
+
for (let i = 0; i < arms.length; i++) for (let j = i + 1; j < arms.length; j++) {
|
|
4279
|
+
if (armShapes[i] !== armShapes[j]) continue;
|
|
4280
|
+
push({
|
|
4281
|
+
rewrite: "duplicate-arm",
|
|
4282
|
+
site,
|
|
4283
|
+
from: `choice(\u2026 arm[${i}] \u2026, arm[${j}] \u2026)`,
|
|
4284
|
+
to: `delete arm[${j}]`,
|
|
4285
|
+
astNeutral: true,
|
|
4286
|
+
perf: PERF_ARM,
|
|
4287
|
+
bug: true,
|
|
4288
|
+
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.`
|
|
4289
|
+
}, `${i}-${j}`);
|
|
4290
|
+
}
|
|
4291
|
+
for (let i = 0; i < arms.length; i++) for (let j = 0; j < arms.length; j++) {
|
|
4292
|
+
if (i >= j) continue;
|
|
4293
|
+
if (armShapes[i] === armShapes[j]) continue;
|
|
4294
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4295
|
+
if (ta.length === 0 || ta.length >= tb.length) continue;
|
|
4296
|
+
let k = 0;
|
|
4297
|
+
while (k < ta.length && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
|
|
4298
|
+
if (k !== ta.length) continue;
|
|
4299
|
+
push({
|
|
4300
|
+
rewrite: "shadowed-arm",
|
|
4301
|
+
site,
|
|
4302
|
+
from: `choice(\u2026 arm[${i}]=${clamp(render(arms[i], 2))}, \u2026 arm[${j}]=${clamp(render(arms[j], 2))} \u2026)`,
|
|
4303
|
+
to: `arm[${j}] is unreachable`,
|
|
4304
|
+
astNeutral: true,
|
|
4305
|
+
perf: PERF_ARM,
|
|
4306
|
+
bug: true,
|
|
4307
|
+
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.`
|
|
4308
|
+
}, `${i}-${j}`);
|
|
4309
|
+
}
|
|
4310
|
+
for (let i = 0; i + 1 < arms.length; i++) {
|
|
4311
|
+
const j = i + 1;
|
|
4312
|
+
const longer = termsOf(arms[i]), shorter = termsOf(arms[j]);
|
|
4313
|
+
const ordered = detectOptionalTerm(longer, shorter, shapeOf);
|
|
4314
|
+
const reversed = ordered === null ? detectOptionalTerm(shorter, longer, shapeOf) : null;
|
|
4315
|
+
const hit = ordered ?? reversed;
|
|
4316
|
+
if (hit === null) continue;
|
|
4317
|
+
const nodeWrapped = v2.enclosingNode ?? wrappingNodeType(arms[i]) ?? wrappingNodeType(arms[j]);
|
|
4318
|
+
const rest = hit.rest.map((t) => render(t, 2)).join(", ");
|
|
4319
|
+
const to = hit.where === "prefix" ? `sequence(optional(${render(hit.term, 2)}), ${rest})` : `sequence(${rest}, optional(${render(hit.term, 2)}))`;
|
|
4320
|
+
push({
|
|
4321
|
+
rewrite: hit.where === "prefix" ? "optional-prefix" : "optional-suffix",
|
|
4322
|
+
site,
|
|
4323
|
+
from: clamp(render(v2.p, 2)),
|
|
4324
|
+
to: clamp(to),
|
|
4325
|
+
astNeutral: false,
|
|
4326
|
+
perf: PERF_ARM,
|
|
4327
|
+
bug: false,
|
|
4328
|
+
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.`
|
|
4329
|
+
}, `${i}-${j}`);
|
|
4330
|
+
}
|
|
4331
|
+
for (let i = 0; i + 1 < arms.length; i++) {
|
|
4332
|
+
const j = i + 1;
|
|
4333
|
+
const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
|
|
4334
|
+
if (ta.length < 2 || tb.length < 2) continue;
|
|
4335
|
+
let k = 0;
|
|
4336
|
+
while (k < ta.length - 1 && k < tb.length - 1 && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
|
|
4337
|
+
if (k === 0) continue;
|
|
4338
|
+
const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
|
|
4339
|
+
push({
|
|
4340
|
+
rewrite: "left-factor",
|
|
4341
|
+
site,
|
|
4342
|
+
from: `choice(sequence(${ta.map((t) => render(t, 1)).join(", ")}), sequence(${tb.map((t) => render(t, 1)).join(", ")}))`,
|
|
4343
|
+
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(", ")})))`),
|
|
4344
|
+
astNeutral: false,
|
|
4345
|
+
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,
|
|
4346
|
+
bug: false,
|
|
4347
|
+
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.`
|
|
4348
|
+
}, `${i}-${j}`);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
for (const v2 of visited.values()) {
|
|
4352
|
+
if (v2.d.tag !== "sequence") continue;
|
|
4353
|
+
const hit = detectHandRolledSepBy(v2.d.parsers, shapeOf);
|
|
4354
|
+
if (hit === null) continue;
|
|
4355
|
+
const captured = v2.d.parsers.some((t) => capturesPositionally(t));
|
|
4356
|
+
const verdict = captured ? "blocked-by-capture" : v2.enclosingReducer ? "reducer-stride-review" : "convertible";
|
|
4357
|
+
const to = `sepBy(${clamp(render(hit.item, 2))}, ${clamp(render(hit.sep, 2))}${hit.trailing ? `, { trailing: 'allow' }` : ""}, { min: ${hit.min} })`;
|
|
4358
|
+
out.push({
|
|
4359
|
+
kind: "rewrite",
|
|
4360
|
+
id: `rewrite:hand-rolled-sepby:${siteToString(v2.site)}`,
|
|
4361
|
+
rewrite: "hand-rolled-sepby",
|
|
4362
|
+
site: v2.site,
|
|
4363
|
+
sepByVerdict: verdict,
|
|
4364
|
+
from: clamp(render(v2.p, 3)),
|
|
4365
|
+
to: verdict === "convertible" ? to : `${to} \u2014 NOT APPLICABLE HERE (${verdict})`,
|
|
4366
|
+
astNeutral: false,
|
|
4367
|
+
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,
|
|
4368
|
+
bug: false,
|
|
4369
|
+
suggestion: `${SEPBY_SPELLED_OUT(hit.trailing)} ${SEPBY_VERDICT_TEXT[verdict](hit.trailing)}`
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
return out;
|
|
4373
|
+
}
|
|
4374
|
+
function collapseRepeat(outer, inner) {
|
|
4375
|
+
const o = outer.tag, i = inner.tag;
|
|
4376
|
+
if (o === "optional" && i === "optional") return "optional(X)";
|
|
4377
|
+
if (o === "optional" && i === "many") return "many(X)";
|
|
4378
|
+
if (o === "many" && i === "many") return "many(X)";
|
|
4379
|
+
if (o === "many" && i === "optional") return "many(X)";
|
|
4380
|
+
if (o === "optional" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
|
|
4381
|
+
if (o === "many" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
function wrappingNodeType(arm) {
|
|
4385
|
+
const d = arm._def;
|
|
4386
|
+
return d.tag === "node" ? d.type ?? "(anonymous)" : null;
|
|
4387
|
+
}
|
|
4388
|
+
function detectOptionalTerm(longer, shorter, shapeOf) {
|
|
4389
|
+
if (longer.length !== shorter.length + 1 || shorter.length === 0) return null;
|
|
4390
|
+
const same = (a, b) => a.length === b.length && a.every((x, i) => shapeOf(x) === shapeOf(b[i]));
|
|
4391
|
+
if (same(longer.slice(1), shorter)) return { where: "prefix", term: longer[0], rest: shorter };
|
|
4392
|
+
if (same(longer.slice(0, -1), shorter)) return { where: "suffix", term: longer[longer.length - 1], rest: shorter };
|
|
4393
|
+
return null;
|
|
4394
|
+
}
|
|
4395
|
+
var SEPBY_SPELLED_OUT = (trailing) => `this is \`sepBy(item, sep${trailing ? ", { trailing: 'allow' }" : ""})\` spelled out.`;
|
|
4396
|
+
var SEPBY_VERDICT_TEXT = {
|
|
4397
|
+
"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.",
|
|
4398
|
+
"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.",
|
|
4399
|
+
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]]\`.`
|
|
4400
|
+
};
|
|
4401
|
+
function capturesPositionally(p, seen = /* @__PURE__ */ new Set()) {
|
|
4402
|
+
if (seen.has(p)) return false;
|
|
4403
|
+
seen.add(p);
|
|
4404
|
+
const d = p._def;
|
|
4405
|
+
if (d.tag === "field" || d.tag === "label") return true;
|
|
4406
|
+
for (const k of childrenOf(d)) if (capturesPositionally(k, seen)) return true;
|
|
4407
|
+
return false;
|
|
4408
|
+
}
|
|
4409
|
+
function detectHandRolledSepBy(terms, shapeOf) {
|
|
4410
|
+
if (terms.length < 2 || terms.length > 3) return null;
|
|
4411
|
+
const item = terms[0];
|
|
4412
|
+
const rep = terms[1]._def;
|
|
4413
|
+
if (rep.tag !== "many" && rep.tag !== "oneOrMore") return null;
|
|
4414
|
+
if (rep.max !== void 0) return null;
|
|
4415
|
+
const innerDef = rep.parser._def;
|
|
4416
|
+
if (innerDef.tag !== "sequence" || innerDef.parsers.length !== 2) return null;
|
|
4417
|
+
const [sep2, repeated] = innerDef.parsers;
|
|
4418
|
+
if (shapeOf(repeated) !== shapeOf(item)) return null;
|
|
4419
|
+
let trailing = false;
|
|
4420
|
+
if (terms.length === 3) {
|
|
4421
|
+
const t = terms[2]._def;
|
|
4422
|
+
if (t.tag !== "optional" || shapeOf(t.parser) !== shapeOf(sep2)) return null;
|
|
4423
|
+
trailing = true;
|
|
4424
|
+
}
|
|
4425
|
+
return { item, sep: sep2, trailing, min: 1 + rep.min };
|
|
4426
|
+
}
|
|
4427
|
+
function bodyTermsOf(p) {
|
|
4428
|
+
let cur = p;
|
|
4429
|
+
for (; ; ) {
|
|
4430
|
+
const d2 = cur._def;
|
|
4431
|
+
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") {
|
|
4432
|
+
cur = d2.parser;
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
break;
|
|
4436
|
+
}
|
|
4437
|
+
const d = cur._def;
|
|
4438
|
+
return d.tag === "sequence" ? d.parsers : [cur];
|
|
4439
|
+
}
|
|
4440
|
+
var MIN_SHARED_TERMS = 2;
|
|
4441
|
+
function findDivergentNodes(visited, shapeOf) {
|
|
4442
|
+
const byType = /* @__PURE__ */ new Map();
|
|
4443
|
+
for (const v2 of visited.values()) {
|
|
4444
|
+
if (v2.d.tag !== "node" || v2.d.type === void 0) continue;
|
|
4445
|
+
let m = byType.get(v2.d.type);
|
|
4446
|
+
if (m === void 0) {
|
|
4447
|
+
m = /* @__PURE__ */ new Map();
|
|
4448
|
+
byType.set(v2.d.type, m);
|
|
4449
|
+
}
|
|
4450
|
+
if (!m.has(v2.shape)) m.set(v2.shape, v2);
|
|
4451
|
+
}
|
|
4452
|
+
const out = [];
|
|
4453
|
+
for (const [type, m] of byType) {
|
|
4454
|
+
if (m.size < 2) continue;
|
|
4455
|
+
const members = [...m.values()];
|
|
4456
|
+
const termSets = members.map((v2) => bodyTermsOf(v2.p).map((t) => ({ id: shapeOf(t), text: clamp(render(t, 2)) })));
|
|
4457
|
+
const shared = termSets[0].filter((t) => termSets.every((set) => set.some((x) => x.id === t.id)));
|
|
4458
|
+
const uniqueShared = [...new Map(shared.map((t) => [t.id, t])).values()];
|
|
4459
|
+
if (uniqueShared.length < MIN_SHARED_TERMS) continue;
|
|
4460
|
+
const sharedIds = new Set(uniqueShared.map((t) => t.id));
|
|
4461
|
+
out.push({
|
|
4462
|
+
kind: "divergent-node",
|
|
4463
|
+
id: `divergent-node:${type}`,
|
|
4464
|
+
nodeType: type,
|
|
4465
|
+
count: members.length,
|
|
4466
|
+
sharedTerms: uniqueShared.map((t) => t.text),
|
|
4467
|
+
productions: members.map((v2, i) => ({
|
|
4468
|
+
shape: clamp(render(v2.p, 3)),
|
|
4469
|
+
site: v2.site,
|
|
4470
|
+
distinctTerms: termSets[i].filter((t) => !sharedIds.has(t.id)).map((t) => t.text)
|
|
4471
|
+
})),
|
|
4472
|
+
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.`
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
return out.sort((a, b) => b.sharedTerms.length * b.count - a.sharedTerms.length * a.count);
|
|
4476
|
+
}
|
|
4477
|
+
function resolveArm(p) {
|
|
4478
|
+
let cur = p;
|
|
4479
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4480
|
+
for (; ; ) {
|
|
4481
|
+
if (seen.has(cur)) return cur;
|
|
4482
|
+
seen.add(cur);
|
|
4483
|
+
const d = cur._def;
|
|
4484
|
+
if (d.tag === "label" || d.tag === "attempt") {
|
|
4485
|
+
cur = d.parser;
|
|
4486
|
+
continue;
|
|
4487
|
+
}
|
|
4488
|
+
if (d.tag === "lazy") {
|
|
4489
|
+
try {
|
|
4490
|
+
cur = d.thunk();
|
|
4491
|
+
continue;
|
|
4492
|
+
} catch {
|
|
4493
|
+
return cur;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
return cur;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function nestedNodeTypes(root, out, seen, isRoot) {
|
|
4500
|
+
if (seen.has(root)) return;
|
|
4501
|
+
seen.add(root);
|
|
4502
|
+
const d = root._def;
|
|
4503
|
+
if (d.tag === "leaf" || d.tag === "token") return;
|
|
4504
|
+
if (d.tag === "node" && !isRoot) out.add(d.type ?? "(anonymous)");
|
|
4505
|
+
if (d.tag === "lazy") {
|
|
4506
|
+
try {
|
|
4507
|
+
nestedNodeTypes(d.thunk(), out, seen, false);
|
|
4508
|
+
} catch {
|
|
4509
|
+
}
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
for (const k of childrenOf(d)) nestedNodeTypes(k, out, seen, false);
|
|
4513
|
+
}
|
|
4514
|
+
var MAX_LOST_TYPES = 8;
|
|
4515
|
+
function findStructureLoss(visited, resolve2) {
|
|
4516
|
+
const nestedCache = /* @__PURE__ */ new Map();
|
|
4517
|
+
const nested = (p) => {
|
|
4518
|
+
const hit = nestedCache.get(p);
|
|
4519
|
+
if (hit !== void 0) return hit;
|
|
4520
|
+
const out = /* @__PURE__ */ new Set();
|
|
4521
|
+
nestedNodeTypes(p, out, /* @__PURE__ */ new Set(), true);
|
|
4522
|
+
nestedCache.set(p, out);
|
|
4523
|
+
return out;
|
|
4524
|
+
};
|
|
4525
|
+
const findings = [];
|
|
4526
|
+
for (const v2 of visited.values()) {
|
|
4527
|
+
const d = v2.d;
|
|
4528
|
+
if (d.tag !== "choice") continue;
|
|
4529
|
+
const arms = d.parsers;
|
|
4530
|
+
const resolved = arms.map(resolveArm);
|
|
4531
|
+
const nodeTypes = resolved.map(wrappingNodeType);
|
|
4532
|
+
const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
|
|
4533
|
+
for (let i = 0; i < arms.length; i++) {
|
|
4534
|
+
if (d.gates[i] != null) continue;
|
|
4535
|
+
const type = nodeTypes[i] ?? null;
|
|
4536
|
+
if (type === null) continue;
|
|
4537
|
+
if (nested(resolved[i]).size !== 0) continue;
|
|
4538
|
+
const A = fs2[i];
|
|
4539
|
+
if (A.kind === "any") continue;
|
|
4540
|
+
for (let j = i + 1; j < arms.length; j++) {
|
|
4541
|
+
if (nodeTypes[j] !== type) continue;
|
|
4542
|
+
const B = fs2[j];
|
|
4543
|
+
if (B.kind === "any" || !intersects3(A, B)) continue;
|
|
4544
|
+
const lost = nested(resolved[j]);
|
|
4545
|
+
if (lost.size === 0) continue;
|
|
4546
|
+
const lostNodeTypes = [...lost].sort();
|
|
4547
|
+
findings.push({
|
|
4548
|
+
kind: "structure-loss",
|
|
4549
|
+
id: `structure-loss:${siteToString(v2.site)}:${i}-${j}`,
|
|
4550
|
+
site: v2.site,
|
|
4551
|
+
nodeType: type,
|
|
4552
|
+
earlier: i,
|
|
4553
|
+
later: j,
|
|
4554
|
+
on: intersection2(A, B),
|
|
4555
|
+
lostNodeTypes: lostNodeTypes.slice(0, MAX_LOST_TYPES),
|
|
4556
|
+
earlierShape: clamp(render(resolved[i], 3)),
|
|
4557
|
+
laterShape: clamp(render(resolved[j], 3)),
|
|
4558
|
+
suggestion: structureLossSuggestion(type, i, j, lostNodeTypes)
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
return findings.sort((a, b) => b.lostNodeTypes.length - a.lostNodeTypes.length);
|
|
4564
|
+
}
|
|
4565
|
+
function structureLossSuggestion(type, i, j, lost) {
|
|
4566
|
+
const names = lost.slice(0, MAX_LOST_TYPES).map((t) => `\`${t}\``).join(", ");
|
|
4567
|
+
const more = lost.length > MAX_LOST_TYPES ? ` (and ${lost.length - MAX_LOST_TYPES} more)` : "";
|
|
4568
|
+
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.`;
|
|
4569
|
+
}
|
|
4570
|
+
function findKeywordRegexes(visited) {
|
|
4571
|
+
const armIndexOf = /* @__PURE__ */ new Map();
|
|
4572
|
+
for (const v2 of visited.values()) {
|
|
4573
|
+
if (v2.d.tag !== "choice") continue;
|
|
4574
|
+
v2.d.parsers.forEach((a, i) => {
|
|
4575
|
+
let cur = a;
|
|
4576
|
+
for (; ; ) {
|
|
4577
|
+
const cd = cur._def;
|
|
4578
|
+
if (cd.tag === "node" || cd.tag === "label" || cd.tag === "transform" || cd.tag === "leaf" || cd.tag === "attempt") {
|
|
4579
|
+
cur = cd.parser;
|
|
4580
|
+
continue;
|
|
4581
|
+
}
|
|
4582
|
+
break;
|
|
4583
|
+
}
|
|
4584
|
+
armIndexOf.set(cur, { owner: v2.p, index: i });
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
const keywordArms = /* @__PURE__ */ new Map();
|
|
4588
|
+
for (const [arm, { owner }] of armIndexOf) {
|
|
4589
|
+
const d = arm._def;
|
|
4590
|
+
if (d.tag !== "regex" || keywordRegexShape(d.source) === null) continue;
|
|
4591
|
+
const list = keywordArms.get(owner) ?? [];
|
|
4592
|
+
list.push(armIndexOf.get(arm).index);
|
|
4593
|
+
keywordArms.set(owner, list);
|
|
4594
|
+
}
|
|
4595
|
+
const out = [];
|
|
4596
|
+
for (const v2 of visited.values()) {
|
|
4597
|
+
const d = v2.d;
|
|
4598
|
+
if (d.tag !== "regex") continue;
|
|
4599
|
+
const shape = keywordRegexShape(d.source);
|
|
4600
|
+
if (shape === null) continue;
|
|
4601
|
+
const ci = d.flags.includes("i");
|
|
4602
|
+
const unicode = d.flags.includes("u") || d.flags.includes("v");
|
|
4603
|
+
const own = armIndexOf.get(v2.p);
|
|
4604
|
+
const siblings = own === void 0 ? [] : (keywordArms.get(own.owner) ?? []).filter((i) => i !== own.index);
|
|
4605
|
+
const bq = (s) => `'${s.replace(/\\/g, "\\\\")}'`;
|
|
4606
|
+
const vocabulary = shape.words.length >= VOCABULARY_MIN_WORDS;
|
|
4607
|
+
const hazards = keywordAlternationHazards(shape.words, shape.boundary);
|
|
4608
|
+
const unrescued = hazards.filter((h) => !h.rescuedByBoundary);
|
|
4609
|
+
const longestFirst = shape.words.every((w, i) => i === 0 || shape.words[i - 1].length >= w.length);
|
|
4610
|
+
const shown = shape.words.length > 8 ? [...shape.words.slice(0, 6), "/* \u2026 */", shape.words[shape.words.length - 1]] : shape.words;
|
|
4611
|
+
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" : ""} })`;
|
|
4612
|
+
out.push({
|
|
4613
|
+
kind: "keyword-regex",
|
|
4614
|
+
id: `keyword-regex:${siteToString(v2.site)}`,
|
|
4615
|
+
site: v2.site,
|
|
4616
|
+
source: d.source,
|
|
4617
|
+
flags: d.flags,
|
|
4618
|
+
words: shape.words,
|
|
4619
|
+
boundary: shape.boundary,
|
|
4620
|
+
caseFoldRisk: ci && !unicode,
|
|
4621
|
+
siblingArms: siblings,
|
|
4622
|
+
vocabulary,
|
|
4623
|
+
longestFirst,
|
|
4624
|
+
hazards,
|
|
4625
|
+
bug: unrescued.length > 0,
|
|
4626
|
+
suggestion: [
|
|
4627
|
+
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.`,
|
|
4628
|
+
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.` : "",
|
|
4629
|
+
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.` : "",
|
|
4630
|
+
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.` : ""
|
|
4631
|
+
].filter((s) => s !== "").join(" ")
|
|
4632
|
+
});
|
|
4633
|
+
}
|
|
4634
|
+
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);
|
|
4635
|
+
}
|
|
4636
|
+
var sitesLine = (sites) => sites.slice(0, 6).map(siteToString).join("\n ") + (sites.length > 6 ? `
|
|
4637
|
+
\u2026 and ${sites.length - 6} more` : "");
|
|
4638
|
+
function formatDuplicationFindings(report) {
|
|
4639
|
+
const lines = [];
|
|
4640
|
+
for (const f of report.structureLoss) {
|
|
4641
|
+
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)}`);
|
|
4642
|
+
lines.push(` flat: ${f.earlierShape}`);
|
|
4643
|
+
lines.push(` rich: ${f.laterShape}`);
|
|
4644
|
+
lines.push(` lost: ${f.lostNodeTypes.map((t) => `\`${t}\``).join(", ")}`);
|
|
4645
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4646
|
+
}
|
|
4647
|
+
for (const f of report.rewrites) {
|
|
4648
|
+
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)"}`);
|
|
4649
|
+
lines.push(` from: ${f.from}`);
|
|
4650
|
+
lines.push(` to: ${f.to}`);
|
|
4651
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4652
|
+
if (f.perf !== "") lines.push(` perf: ${f.perf}`);
|
|
4653
|
+
}
|
|
4654
|
+
for (const f of report.divergentNodes) {
|
|
4655
|
+
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(", ")}`);
|
|
4656
|
+
for (const p of f.productions) lines.push(` ${siteToString(p.site)}
|
|
4657
|
+
${p.shape}
|
|
4658
|
+
unique to it: ${p.distinctTerms.length === 0 ? "(none)" : p.distinctTerms.map((t) => `\`${t}\``).join(", ")}`);
|
|
4659
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4660
|
+
}
|
|
4661
|
+
for (const f of report.keywordRegexes) {
|
|
4662
|
+
const src = f.source.length > 90 ? `${f.source.slice(0, 88)}\u2026` : f.source;
|
|
4663
|
+
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]" : ""}`);
|
|
4664
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4665
|
+
}
|
|
4666
|
+
for (const f of report.duplicates) {
|
|
4667
|
+
lines.push(`parseman duplication: ${f.count}\xD7 identical ${f.size}-node shape (saves ${f.savings} nodes) \u2014 ${f.shape}`);
|
|
4668
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4669
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4670
|
+
}
|
|
4671
|
+
for (const f of report.nearDuplicates) {
|
|
4672
|
+
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}`);
|
|
4673
|
+
lines.push(` slot: ${f.slotPath} \u2208 { ${f.variants.join(" | ")} }`);
|
|
4674
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4675
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4676
|
+
}
|
|
4677
|
+
for (const f of report.regexFragments) {
|
|
4678
|
+
lines.push(`parseman regex-fragment: \`${f.fragment}\` (${f.branches} branches) re-spelled in ${f.count} regex() terminals`);
|
|
4679
|
+
lines.push(` at: ${sitesLine(f.sites)}`);
|
|
4680
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4681
|
+
}
|
|
4682
|
+
for (const f of report.regexClasses) {
|
|
4683
|
+
lines.push(`parseman regex-class${f.drifted ? " DRIFT" : ""}: [${f.canonical}] across ${f.count} regex() terminals${f.bmpCeiling ? " [BMP ceiling]" : ""}`);
|
|
4684
|
+
for (const v2 of f.variants) lines.push(` [${v2.source}] \xD7${v2.count} ${v2.delta}
|
|
4685
|
+
${sitesLine(v2.sites)}`);
|
|
4686
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4687
|
+
}
|
|
4688
|
+
for (const f of report.overlaps) {
|
|
4689
|
+
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]" : ""}`);
|
|
4690
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
4691
|
+
}
|
|
4692
|
+
return lines;
|
|
4693
|
+
}
|
|
4694
|
+
function duplicationFindingCount(report) {
|
|
4695
|
+
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;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
3328
4698
|
// src/combinators/scanTo.ts
|
|
3329
4699
|
function resolveScanSkip(explicitSkip, raw, ctx) {
|
|
3330
4700
|
if (raw) return [];
|
|
@@ -6553,12 +7923,14 @@ function emitNode(def, ctx, pos) {
|
|
|
6553
7923
|
ctx.buildSrcs.push(def.buildSrc ?? null);
|
|
6554
7924
|
}
|
|
6555
7925
|
const i = ind(ctx);
|
|
6556
|
-
const
|
|
6557
|
-
|
|
6558
|
-
const
|
|
6559
|
-
const
|
|
7926
|
+
const cstOut = ctx.hostMode === "cst" && !structural;
|
|
7927
|
+
if (!structural && !cstOut) ctx.hostBranchElided = true;
|
|
7928
|
+
const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
|
|
7929
|
+
const clonesState = !structural && (cstOut || buildReadsState(def));
|
|
7930
|
+
const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
|
|
7931
|
+
const capturesRaw = !structural && (cstOut || mkType !== null || buildReadsRaw(def));
|
|
6560
7932
|
const hasFields = parserHasOwnFields(def.parser);
|
|
6561
|
-
const capturesFields = hasFields && !structural && buildReadsFields(def);
|
|
7933
|
+
const capturesFields = hasFields && !structural && (cstOut || buildReadsFields(def));
|
|
6562
7934
|
const innerEnablesTriviaCapture = parserEnablesTriviaCapture(def.parser);
|
|
6563
7935
|
const chV = v(ctx, "_ch");
|
|
6564
7936
|
const rawV = v(ctx, "_raw");
|
|
@@ -6575,8 +7947,9 @@ function emitNode(def, ctx, pos) {
|
|
|
6575
7947
|
const profHoist = `${i}const ${pmV} = _ctx._pmProfile, ${recV} = ${pmV}?.phase === 'recognizer', ${capV} = ${pmV}?.phase === 'capture'`;
|
|
6576
7948
|
const profileRecognizer = recV;
|
|
6577
7949
|
const profileCapture = capV;
|
|
6578
|
-
const
|
|
6579
|
-
const
|
|
7950
|
+
const cstMode = ctx.hostMode === "cst";
|
|
7951
|
+
const directCstV = !structural && !cstMode && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
|
|
7952
|
+
const directCstGate = directCstV === null ? "false" : profileCapture;
|
|
6580
7953
|
const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
|
|
6581
7954
|
const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
|
|
6582
7955
|
const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
|
|
@@ -6657,7 +8030,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
|
|
|
6657
8030
|
const ndV = v(ctx, "_nd");
|
|
6658
8031
|
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})`;
|
|
6659
8032
|
const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${JSON.stringify(def.type)}, ${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
|
|
6660
|
-
const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` :
|
|
8033
|
+
const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
|
|
6661
8034
|
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;
|
|
6662
8035
|
const unwrapExpr = `${chV}.length === 1 ? (${chV}[0] !== null && typeof ${chV}[0] === 'object' && ${chV}[0]._tag === 'leaf' ? ${chV}[0].value : ${chV}[0]) : (${ndExpr})`;
|
|
6663
8036
|
const collapseExpr = `${chV}.length === 1 ? ${chV}[0] : (${ndExpr})`;
|
|
@@ -7106,9 +8479,9 @@ function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
|
|
|
7106
8479
|
return false;
|
|
7107
8480
|
}
|
|
7108
8481
|
}
|
|
7109
|
-
return
|
|
8482
|
+
return childrenOf2(d).some((child) => hasDirectBuildDef(child, seen));
|
|
7110
8483
|
}
|
|
7111
|
-
function
|
|
8484
|
+
function childrenOf2(def) {
|
|
7112
8485
|
switch (def.tag) {
|
|
7113
8486
|
case "sequence":
|
|
7114
8487
|
case "choice":
|
|
@@ -7179,7 +8552,7 @@ function subtreeSizes(roots) {
|
|
|
7179
8552
|
const def = p._def;
|
|
7180
8553
|
let s = 1;
|
|
7181
8554
|
if (def.tag !== "lazy") {
|
|
7182
|
-
for (const child of
|
|
8555
|
+
for (const child of childrenOf2(def)) s += child._def.tag === "lazy" ? 1 : sz(child);
|
|
7183
8556
|
}
|
|
7184
8557
|
sizes.set(p, s);
|
|
7185
8558
|
return s;
|
|
@@ -7200,7 +8573,7 @@ function subtreeSizes(roots) {
|
|
|
7200
8573
|
return;
|
|
7201
8574
|
}
|
|
7202
8575
|
sz(p);
|
|
7203
|
-
for (const child of
|
|
8576
|
+
for (const child of childrenOf2(def)) visit(child);
|
|
7204
8577
|
};
|
|
7205
8578
|
for (const root of roots) visit(root);
|
|
7206
8579
|
return sizes;
|
|
@@ -7234,7 +8607,7 @@ function analyzeLazyUsageMulti(roots) {
|
|
|
7234
8607
|
if (isHoistableTag(def.tag)) {
|
|
7235
8608
|
counts.set(p, (counts.get(p) ?? 0) + 1);
|
|
7236
8609
|
}
|
|
7237
|
-
for (const child of
|
|
8610
|
+
for (const child of childrenOf2(def)) walk2(child);
|
|
7238
8611
|
}
|
|
7239
8612
|
const rootsArr = [...roots];
|
|
7240
8613
|
for (const root of rootsArr) walk2(root);
|
|
@@ -7249,7 +8622,7 @@ function ruleDependencies(ruleMap) {
|
|
|
7249
8622
|
const seen = /* @__PURE__ */ new Set();
|
|
7250
8623
|
const walk2 = (p, isRoot) => {
|
|
7251
8624
|
const def = p._def;
|
|
7252
|
-
const boundary = def.tag === "lazy" ?
|
|
8625
|
+
const boundary = def.tag === "lazy" ? nameOf.get(p) ?? p._ruleName : nameOf.get(p);
|
|
7253
8626
|
if (!isRoot && boundary !== void 0) {
|
|
7254
8627
|
found.add(boundary);
|
|
7255
8628
|
return;
|
|
@@ -7266,7 +8639,7 @@ function ruleDependencies(ruleMap) {
|
|
|
7266
8639
|
walk2(resolved, false);
|
|
7267
8640
|
return;
|
|
7268
8641
|
}
|
|
7269
|
-
for (const child of
|
|
8642
|
+
for (const child of childrenOf2(def)) walk2(child, false);
|
|
7270
8643
|
};
|
|
7271
8644
|
walk2(comb, true);
|
|
7272
8645
|
deps.set(name, [...found]);
|
|
@@ -7286,10 +8659,30 @@ function runGatingDiagnostic(combinator, opt) {
|
|
|
7286
8659
|
function runGatingDiagnosticRules(ruleMap, opt) {
|
|
7287
8660
|
return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
|
|
7288
8661
|
}
|
|
7289
|
-
function runFusedGatingDiagnostic(getRuleMaps, opt) {
|
|
8662
|
+
function runFusedGatingDiagnostic(getRuleMaps, opt, getOpaque) {
|
|
7290
8663
|
if (resolveGatingLevel(opt) === "off") return void 0;
|
|
7291
8664
|
const ruleMaps = getRuleMaps();
|
|
7292
|
-
|
|
8665
|
+
const opaque = getOpaque?.() ?? [];
|
|
8666
|
+
const opaqueFindings = opaque.map((o) => ({
|
|
8667
|
+
rule: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
|
|
8668
|
+
kind: "opaque-artifact",
|
|
8669
|
+
reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices were not examined at this fuse.`
|
|
8670
|
+
}));
|
|
8671
|
+
if (!ruleMaps.some((map) => hasExternalRuleRef(map))) {
|
|
8672
|
+
if (opaqueFindings.length === 0) return void 0;
|
|
8673
|
+
return reportGating(opt, () => ({
|
|
8674
|
+
totalChoices: 0,
|
|
8675
|
+
gated: 0,
|
|
8676
|
+
recoverable: 0,
|
|
8677
|
+
unanalysable: opaqueFindings,
|
|
8678
|
+
ungated: [],
|
|
8679
|
+
accepted: [],
|
|
8680
|
+
deferred: [],
|
|
8681
|
+
acceptedUnused: [],
|
|
8682
|
+
choices: [],
|
|
8683
|
+
antiPatterns: []
|
|
8684
|
+
}));
|
|
8685
|
+
}
|
|
7293
8686
|
const winners = /* @__PURE__ */ new Map();
|
|
7294
8687
|
for (const map of ruleMaps) for (const [name, rule] of map) {
|
|
7295
8688
|
if (rule._def.tag === "lazy") {
|
|
@@ -7305,7 +8698,12 @@ function runFusedGatingDiagnostic(getRuleMaps, opt) {
|
|
|
7305
8698
|
return reportGating(opt, (o) => {
|
|
7306
8699
|
const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
|
|
7307
8700
|
const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
|
|
7308
|
-
return {
|
|
8701
|
+
return {
|
|
8702
|
+
...fused,
|
|
8703
|
+
ungated: fused.ungated.filter((c) => deferredHere.has(c.id)),
|
|
8704
|
+
antiPatterns: [],
|
|
8705
|
+
unanalysable: [...fused.unanalysable, ...opaqueFindings]
|
|
8706
|
+
};
|
|
7309
8707
|
});
|
|
7310
8708
|
}
|
|
7311
8709
|
function reportGating(opt, analyze) {
|
|
@@ -7316,22 +8714,82 @@ function reportGating(opt, analyze) {
|
|
|
7316
8714
|
let report;
|
|
7317
8715
|
try {
|
|
7318
8716
|
report = analyze(analyzeOpts);
|
|
7319
|
-
} catch {
|
|
7320
|
-
|
|
8717
|
+
} catch (e) {
|
|
8718
|
+
const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
8719
|
+
const failed = {
|
|
8720
|
+
totalChoices: 0,
|
|
8721
|
+
gated: 0,
|
|
8722
|
+
recoverable: 0,
|
|
8723
|
+
unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason: `gating analysis threw \u2014 ${reason}` }],
|
|
8724
|
+
ungated: [],
|
|
8725
|
+
accepted: [],
|
|
8726
|
+
deferred: [],
|
|
8727
|
+
acceptedUnused: [],
|
|
8728
|
+
choices: [],
|
|
8729
|
+
antiPatterns: []
|
|
8730
|
+
};
|
|
8731
|
+
const failLines = formatGatingWarnings(failed);
|
|
8732
|
+
if (level === "error") throw new Error(`parseman: gating analysis FAILED (no verdict available)
|
|
8733
|
+
${failLines.join("\n")}`);
|
|
8734
|
+
for (const l of failLines) console.warn(l);
|
|
8735
|
+
return failed;
|
|
7321
8736
|
}
|
|
7322
8737
|
const lines = formatGatingWarnings(report);
|
|
7323
8738
|
if (lines.length > 0) {
|
|
7324
|
-
if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
|
|
8739
|
+
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)
|
|
8740
|
+
${lines.join("\n")}`);
|
|
8741
|
+
for (const l of lines) console.warn(l);
|
|
8742
|
+
}
|
|
8743
|
+
return report;
|
|
8744
|
+
}
|
|
8745
|
+
function resolveDuplicationLevel(opt) {
|
|
8746
|
+
const explicit = typeof opt === "string" ? opt : opt?.level;
|
|
8747
|
+
if (explicit !== void 0) return explicit;
|
|
8748
|
+
const env = typeof process !== "undefined" ? process.env?.PARSEMAN_DUPLICATION : void 0;
|
|
8749
|
+
if (env === "off" || env === "warn" || env === "error") return env;
|
|
8750
|
+
return "off";
|
|
8751
|
+
}
|
|
8752
|
+
function reportDuplication(opt, analyze) {
|
|
8753
|
+
const level = resolveDuplicationLevel(opt);
|
|
8754
|
+
if (level === "off") return void 0;
|
|
8755
|
+
const obj = opt !== null && typeof opt === "object" ? opt : void 0;
|
|
8756
|
+
const analyzeOpts = obj === void 0 ? void 0 : {
|
|
8757
|
+
...obj.accept !== void 0 ? { accept: obj.accept } : {},
|
|
8758
|
+
...obj.minSize !== void 0 ? { minSize: obj.minSize } : {},
|
|
8759
|
+
...obj.maxFindings !== void 0 ? { maxFindings: obj.maxFindings } : {},
|
|
8760
|
+
...obj.entryName !== void 0 ? { entryName: obj.entryName } : {}
|
|
8761
|
+
};
|
|
8762
|
+
let report;
|
|
8763
|
+
try {
|
|
8764
|
+
report = analyze(analyzeOpts);
|
|
8765
|
+
} catch (err) {
|
|
8766
|
+
console.warn(
|
|
8767
|
+
`parseman: the duplication diagnostic could not run, so NOTHING was checked \u2014 this is not a clean result.
|
|
8768
|
+
${err instanceof Error ? err.message : String(err)}`
|
|
8769
|
+
);
|
|
8770
|
+
return void 0;
|
|
8771
|
+
}
|
|
8772
|
+
const lines = formatDuplicationFindings(report);
|
|
8773
|
+
if (lines.length > 0) {
|
|
8774
|
+
if (level === "error") throw new Error(`parseman: ${duplicationFindingCount(report)} duplication/overlap finding(s)
|
|
7325
8775
|
${lines.join("\n")}`);
|
|
7326
8776
|
for (const l of lines) console.warn(l);
|
|
7327
8777
|
}
|
|
7328
8778
|
return report;
|
|
7329
8779
|
}
|
|
8780
|
+
function runDuplicationDiagnostic(combinator, opt) {
|
|
8781
|
+
return reportDuplication(opt, (o) => analyzeDuplication(combinator, o));
|
|
8782
|
+
}
|
|
8783
|
+
function runDuplicationDiagnosticRules(ruleMap, opt) {
|
|
8784
|
+
return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
|
|
8785
|
+
}
|
|
7330
8786
|
function compile(combinator, mapFnSources, opts) {
|
|
7331
8787
|
const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
|
|
8788
|
+
runDuplicationDiagnostic(combinator, opts?.duplication);
|
|
7332
8789
|
markUnusedValues(combinator);
|
|
7333
8790
|
const grammarTrivia = combinator._meta.grammarTrivia;
|
|
7334
8791
|
const grammarScanSkip = combinator._meta.grammarScanSkip;
|
|
8792
|
+
const grammarHostMode = combinator._meta.grammarHostMode;
|
|
7335
8793
|
const ctx = {
|
|
7336
8794
|
vars: 0,
|
|
7337
8795
|
indent: 1,
|
|
@@ -7351,6 +8809,7 @@ function compile(combinator, mapFnSources, opts) {
|
|
|
7351
8809
|
namedFnDecls: [],
|
|
7352
8810
|
capturing: hasNodeDef(combinator),
|
|
7353
8811
|
recovery: opts?.recovery ?? false,
|
|
8812
|
+
hostMode: opts?.hostMode ?? grammarHostMode ?? "ast",
|
|
7354
8813
|
...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
|
|
7355
8814
|
lazyUsage: analyzeLazyUsage(combinator),
|
|
7356
8815
|
...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
|
|
@@ -7423,6 +8882,7 @@ function compile(combinator, mapFnSources, opts) {
|
|
|
7423
8882
|
return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
|
|
7424
8883
|
},
|
|
7425
8884
|
parseWithContext(input, parseCtx, pos = 0) {
|
|
8885
|
+
assertHostModeCompatible(ctx.hostMode ?? "ast", parseCtx.build, !!ctx.hostBranchElided);
|
|
7426
8886
|
return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, parseCtx);
|
|
7427
8887
|
},
|
|
7428
8888
|
// Note: collects expect() errors via _errors. Unlike interpreter
|
|
@@ -7452,10 +8912,12 @@ function publicRuleWrapperSource(rule, fnSource) {
|
|
|
7452
8912
|
}
|
|
7453
8913
|
function compileRuleMap(ruleMap, opts) {
|
|
7454
8914
|
runGatingDiagnosticRules(ruleMap, opts?.gating);
|
|
8915
|
+
runDuplicationDiagnosticRules(ruleMap, opts?.duplication);
|
|
7455
8916
|
for (const [, rule] of ruleMap) markUnusedValues(rule);
|
|
7456
8917
|
const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
|
|
7457
8918
|
const grammarTrivia = opts?.trivia;
|
|
7458
8919
|
const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
|
|
8920
|
+
const grammarHostMode = opts?.hostMode ?? ruleMap.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
|
|
7459
8921
|
const ctx = {
|
|
7460
8922
|
vars: 0,
|
|
7461
8923
|
indent: 1,
|
|
@@ -7475,6 +8937,7 @@ function compileRuleMap(ruleMap, opts) {
|
|
|
7475
8937
|
namedFnDecls: [],
|
|
7476
8938
|
capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
|
|
7477
8939
|
recovery: opts?.recovery ?? false,
|
|
8940
|
+
hostMode: grammarHostMode ?? "ast",
|
|
7478
8941
|
...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
|
|
7479
8942
|
lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
|
|
7480
8943
|
...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
|
|
@@ -7565,15 +9028,22 @@ function compileRuleMap(ruleMap, opts) {
|
|
|
7565
9028
|
return {
|
|
7566
9029
|
keys: perEntry.map((e) => e.key),
|
|
7567
9030
|
replacement,
|
|
9031
|
+
// Reported so the MACRO can stamp the emitted map, exactly as `fusedBody` stamps a
|
|
9032
|
+
// fused one. Without it a `rules()` artifact carries no mode and every driver-side
|
|
9033
|
+
// host check passes vacuously — see `withHostMode` in the plugin.
|
|
9034
|
+
hostMode: ctx.hostMode ?? "ast",
|
|
9035
|
+
hostBranchElided: !!ctx.hostBranchElided,
|
|
7568
9036
|
...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions }
|
|
7569
9037
|
};
|
|
7570
9038
|
}
|
|
7571
9039
|
function compileLinkable(ruleMapArg, ns, opts) {
|
|
7572
9040
|
if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
|
|
7573
9041
|
if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
|
|
9042
|
+
if (opts?.duplication !== void 0) runDuplicationDiagnosticRules(ruleMapArg, opts.duplication);
|
|
7574
9043
|
for (const [, rule] of ruleMapArg) markUnusedValues(rule);
|
|
7575
9044
|
const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
|
|
7576
9045
|
const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
|
|
9046
|
+
const grammarHostMode = opts?.hostMode ?? ruleMapArg.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
|
|
7577
9047
|
const ruleMap = ruleMapArg.filter(([, val]) => {
|
|
7578
9048
|
const d = val._def;
|
|
7579
9049
|
if (d.tag !== "lazy") return true;
|
|
@@ -7607,6 +9077,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7607
9077
|
// merely returning their scalar parse values.
|
|
7608
9078
|
capturing: opts?.captureTerminals === true || ruleMap.some(([, rule]) => hasNodeDef(rule)),
|
|
7609
9079
|
recovery: opts?.recovery ?? false,
|
|
9080
|
+
hostMode: grammarHostMode ?? "ast",
|
|
7610
9081
|
...opts?.coverage ? { coverage: { plan: opts.coverage } } : {},
|
|
7611
9082
|
lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
|
|
7612
9083
|
ns,
|
|
@@ -7657,7 +9128,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7657
9128
|
scanExternal(resolved);
|
|
7658
9129
|
return;
|
|
7659
9130
|
}
|
|
7660
|
-
for (const child of
|
|
9131
|
+
for (const child of childrenOf2(def)) scanExternal(child);
|
|
7661
9132
|
};
|
|
7662
9133
|
for (const [, rule] of ruleMap) scanExternal(rule);
|
|
7663
9134
|
}
|
|
@@ -7762,6 +9233,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
|
|
|
7762
9233
|
deps: ruleDependencies(ruleMap),
|
|
7763
9234
|
needsEmptyTl: !!ctx.needsEmptyTl,
|
|
7764
9235
|
needsHostReads: !!ctx.needsHostReads,
|
|
9236
|
+
hostMode: ctx.hostMode ?? "ast",
|
|
9237
|
+
hostBranchElided: !!ctx.hostBranchElided,
|
|
7765
9238
|
hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
|
|
7766
9239
|
isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
|
|
7767
9240
|
mfFns: mfSrcs ? [] : ctx.mapFns,
|
|
@@ -7784,7 +9257,7 @@ function hasExternalRuleRef(ruleMap) {
|
|
|
7784
9257
|
if (resolved === void 0) return p._ruleName !== void 0;
|
|
7785
9258
|
return visit(resolved);
|
|
7786
9259
|
}
|
|
7787
|
-
return
|
|
9260
|
+
return childrenOf2(def).some(visit);
|
|
7788
9261
|
};
|
|
7789
9262
|
return ruleMap.some(([, rule]) => visit(rule));
|
|
7790
9263
|
}
|
|
@@ -7806,7 +9279,7 @@ function hasSemanticReduction(roots, externalRefs) {
|
|
|
7806
9279
|
return true;
|
|
7807
9280
|
}
|
|
7808
9281
|
}
|
|
7809
|
-
return
|
|
9282
|
+
return childrenOf2(def).some(visit);
|
|
7810
9283
|
};
|
|
7811
9284
|
return roots.some(visit);
|
|
7812
9285
|
}
|
|
@@ -7875,7 +9348,7 @@ function withCtx(extra, combinator) {
|
|
|
7875
9348
|
}
|
|
7876
9349
|
|
|
7877
9350
|
// src/compiler/ir-serialize.ts
|
|
7878
|
-
function
|
|
9351
|
+
function childrenOf3(def) {
|
|
7879
9352
|
switch (def.tag) {
|
|
7880
9353
|
case "sequence":
|
|
7881
9354
|
case "choice":
|
|
@@ -7914,7 +9387,7 @@ function childrenOf2(def) {
|
|
|
7914
9387
|
return [];
|
|
7915
9388
|
}
|
|
7916
9389
|
}
|
|
7917
|
-
var
|
|
9390
|
+
var ruleNameOf4 = (c) => c._ruleName;
|
|
7918
9391
|
function lazyTarget(c) {
|
|
7919
9392
|
if (c._def.tag !== "lazy") return null;
|
|
7920
9393
|
try {
|
|
@@ -8096,12 +9569,16 @@ var Serializer = class {
|
|
|
8096
9569
|
emitted = /* @__PURE__ */ new Set();
|
|
8097
9570
|
decls = [];
|
|
8098
9571
|
ruleValues = /* @__PURE__ */ new Set();
|
|
9572
|
+
localRuleNames = /* @__PURE__ */ new Map();
|
|
8099
9573
|
ruleMap;
|
|
8100
9574
|
scanSkip;
|
|
8101
9575
|
constructor(ruleMap, scanSkip = []) {
|
|
8102
9576
|
this.ruleMap = ruleMap;
|
|
8103
9577
|
this.scanSkip = scanSkip;
|
|
8104
|
-
for (const [, c] of ruleMap)
|
|
9578
|
+
for (const [name, c] of ruleMap) {
|
|
9579
|
+
this.localRuleNames.set(c, name);
|
|
9580
|
+
this.ruleValues.add(this.body(c));
|
|
9581
|
+
}
|
|
8105
9582
|
}
|
|
8106
9583
|
/** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
|
|
8107
9584
|
* bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
|
|
@@ -8112,18 +9589,18 @@ var Serializer = class {
|
|
|
8112
9589
|
if (seen.has(c)) return;
|
|
8113
9590
|
seen.add(c);
|
|
8114
9591
|
if (c._def.tag === "lazy") {
|
|
8115
|
-
if (
|
|
9592
|
+
if (ruleNameOf4(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
|
|
8116
9593
|
const target = lazyTarget(c);
|
|
8117
9594
|
if (target) this.assertNoRuleRef(target, seen);
|
|
8118
9595
|
return;
|
|
8119
9596
|
}
|
|
8120
|
-
for (const child of
|
|
9597
|
+
for (const child of childrenOf3(c._def)) this.assertNoRuleRef(child, seen);
|
|
8121
9598
|
}
|
|
8122
9599
|
/** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
|
|
8123
9600
|
* its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
|
|
8124
9601
|
* `g[name]` ref — only the top-level entry is unwrapped. */
|
|
8125
9602
|
body(c) {
|
|
8126
|
-
return c._def.tag === "lazy" &&
|
|
9603
|
+
return c._def.tag === "lazy" && ruleNameOf4(c) !== void 0 ? lazyTarget(c) ?? c : c;
|
|
8127
9604
|
}
|
|
8128
9605
|
run() {
|
|
8129
9606
|
for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
|
|
@@ -8165,7 +9642,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8165
9642
|
analyze(c, active) {
|
|
8166
9643
|
const def = c._def;
|
|
8167
9644
|
if (def.tag === "lazy") {
|
|
8168
|
-
if (
|
|
9645
|
+
if (ruleNameOf4(c) !== void 0) return;
|
|
8169
9646
|
const target = lazyTarget(c);
|
|
8170
9647
|
if (!target) throw new Unserializable("unresolved unnamed ref");
|
|
8171
9648
|
if (active.has(target)) {
|
|
@@ -8181,7 +9658,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8181
9658
|
this.counts.set(c, (this.counts.get(c) ?? 0) + 1);
|
|
8182
9659
|
if ((this.counts.get(c) ?? 0) > 1) return;
|
|
8183
9660
|
active.add(c);
|
|
8184
|
-
for (const child of
|
|
9661
|
+
for (const child of childrenOf3(def)) this.analyze(child, active);
|
|
8185
9662
|
active.delete(c);
|
|
8186
9663
|
}
|
|
8187
9664
|
/** Reference `c`: a const var if it has one (emitting its decl first), else inline.
|
|
@@ -8217,7 +9694,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
|
|
|
8217
9694
|
const kid = (x) => this.ref(x, selfOf);
|
|
8218
9695
|
switch (def.tag) {
|
|
8219
9696
|
case "lazy": {
|
|
8220
|
-
|
|
9697
|
+
const name = this.localRuleNames.get(c) ?? ruleNameOf4(c);
|
|
9698
|
+
if (name !== void 0) return `g[${JSON.stringify(name)}]`;
|
|
8221
9699
|
const target = lazyTarget(c);
|
|
8222
9700
|
if (selfOf && target === selfOf.comb) return selfOf.var;
|
|
8223
9701
|
return this.ref(target, selfOf);
|
|
@@ -8391,7 +9869,23 @@ function fusedBody(pieces) {
|
|
|
8391
9869
|
...[...winner].map(([k, p]) => p.ruleFns.get(k))
|
|
8392
9870
|
];
|
|
8393
9871
|
const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
|
|
8394
|
-
const
|
|
9872
|
+
const { mode, elided } = hostModeOfPieces(pieces);
|
|
9873
|
+
const rawBody = [
|
|
9874
|
+
...lines,
|
|
9875
|
+
"const _map = {",
|
|
9876
|
+
wrapperEntries.join(",\n"),
|
|
9877
|
+
"}",
|
|
9878
|
+
`Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
|
|
9879
|
+
`Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
|
|
9880
|
+
// …and on each rule FUNCTION, because `run(map.Rule, …)` is handed the rule, not the
|
|
9881
|
+
// map, and would otherwise have nothing to check against. Done once at fuse time,
|
|
9882
|
+
// before any rule has been called, so it costs nothing per parse.
|
|
9883
|
+
"for (const _k of Object.keys(_map)) {",
|
|
9884
|
+
` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
|
|
9885
|
+
` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
|
|
9886
|
+
"}",
|
|
9887
|
+
"return _map"
|
|
9888
|
+
].join("\n");
|
|
8395
9889
|
const ANY2 = { kind: "any" };
|
|
8396
9890
|
const EMPTY3 = { kind: "empty" };
|
|
8397
9891
|
const recipes = /* @__PURE__ */ new Map();
|
|
@@ -8450,6 +9944,16 @@ function fusedBody(pieces) {
|
|
|
8450
9944
|
}
|
|
8451
9945
|
return { body, env };
|
|
8452
9946
|
}
|
|
9947
|
+
function hostModeOfPieces(pieces) {
|
|
9948
|
+
const mode = pieces.find((p) => p.hostMode !== void 0 && p.hostMode !== "ast")?.hostMode ?? "ast";
|
|
9949
|
+
const elided = pieces.filter((p) => p.hostBranchElided === true);
|
|
9950
|
+
if (mode === "cst" && elided.length > 0) {
|
|
9951
|
+
throw new Error(
|
|
9952
|
+
`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.`
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9955
|
+
return { mode, elided: elided.length > 0 };
|
|
9956
|
+
}
|
|
8453
9957
|
function emitFusedSource(pieces) {
|
|
8454
9958
|
const { body, env } = fusedBody(pieces);
|
|
8455
9959
|
if (Object.keys(env).length > 0) {
|
|
@@ -8462,11 +9966,23 @@ ${body}
|
|
|
8462
9966
|
function isIRPiece(p) {
|
|
8463
9967
|
return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
|
|
8464
9968
|
}
|
|
8465
|
-
function
|
|
9969
|
+
function once(fn) {
|
|
9970
|
+
let done = false;
|
|
9971
|
+
let value;
|
|
9972
|
+
return () => {
|
|
9973
|
+
if (!done) {
|
|
9974
|
+
value = fn();
|
|
9975
|
+
done = true;
|
|
9976
|
+
}
|
|
9977
|
+
return value;
|
|
9978
|
+
};
|
|
9979
|
+
}
|
|
9980
|
+
function materializePiece(p, trivia2, captureTerminals = false, hostMode) {
|
|
8466
9981
|
if (!isIRPiece(p)) return p;
|
|
8467
9982
|
const pieces = compileLinkable(evalRuleMapIR(p.ir), p.ns, {
|
|
8468
9983
|
...trivia2 ? { trivia: trivia2 } : {},
|
|
8469
|
-
...captureTerminals ? { captureTerminals: true } : {}
|
|
9984
|
+
...captureTerminals ? { captureTerminals: true } : {},
|
|
9985
|
+
...hostMode ? { hostMode } : {}
|
|
8470
9986
|
});
|
|
8471
9987
|
if (!pieces) throw new Error(`compose: carried IR for ns "${p.ns}" could not be re-lowered`);
|
|
8472
9988
|
return pieces;
|
|
@@ -8689,6 +10205,25 @@ function stripTsFromSource(node2, code) {
|
|
|
8689
10205
|
function isWordFactory(v2) {
|
|
8690
10206
|
return !!v2 && typeof v2 === "object" && v2.tag === "wordFactory";
|
|
8691
10207
|
}
|
|
10208
|
+
function wordFactoryFromArgs(args, scope, code, mfs) {
|
|
10209
|
+
const [boundaryOrOptsArg, optsArg] = args;
|
|
10210
|
+
if (boundaryOrOptsArg?.type === "SpreadElement" || optsArg?.type === "SpreadElement") return null;
|
|
10211
|
+
const rawBoundaryOrOpts = boundaryOrOptsArg === void 0 ? void 0 : anyValue(boundaryOrOptsArg, scope, code, mfs);
|
|
10212
|
+
const boundaryOrOpts = rawBoundaryOrOpts === void 0 ? "_0-9A-Za-z" : rawBoundaryOrOpts;
|
|
10213
|
+
const opts = optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs);
|
|
10214
|
+
if (typeof boundaryOrOpts === "string") {
|
|
10215
|
+
if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) return null;
|
|
10216
|
+
const caseInsensitive2 = typeof opts === "object" && opts !== null && "caseInsensitive" in opts ? opts.caseInsensitive : false;
|
|
10217
|
+
if (typeof caseInsensitive2 !== "boolean") return null;
|
|
10218
|
+
return { tag: "wordFactory", boundary: boundaryOrOpts, caseInsensitive: caseInsensitive2 };
|
|
10219
|
+
}
|
|
10220
|
+
if (typeof boundaryOrOpts !== "object" || boundaryOrOpts === null || Array.isArray(boundaryOrOpts) || opts !== void 0) {
|
|
10221
|
+
return null;
|
|
10222
|
+
}
|
|
10223
|
+
const caseInsensitive = "caseInsensitive" in boundaryOrOpts ? boundaryOrOpts.caseInsensitive : false;
|
|
10224
|
+
if (typeof caseInsensitive !== "boolean") return null;
|
|
10225
|
+
return { tag: "wordFactory", boundary: "_0-9A-Za-z", caseInsensitive };
|
|
10226
|
+
}
|
|
8692
10227
|
var SUPPORTED = {
|
|
8693
10228
|
literal: (...a) => literal(a[0], a[1]),
|
|
8694
10229
|
regex: (...a) => regex(a[0], a[1]),
|
|
@@ -8772,14 +10307,13 @@ function exprToCombi(node2, scope, code, mfs) {
|
|
|
8772
10307
|
if (node2.type !== "CallExpression") return null;
|
|
8773
10308
|
const callee = node2.callee;
|
|
8774
10309
|
if (callee.type === "CallExpression" && callee.callee.type === "Identifier" && callee.callee.name === "makeWord") {
|
|
8775
|
-
const boundaryArg = callee.arguments[0];
|
|
8776
10310
|
const strArg = node2.arguments[0];
|
|
8777
10311
|
if (!strArg || strArg.type === "SpreadElement") return null;
|
|
8778
|
-
const
|
|
10312
|
+
const factory2 = wordFactoryFromArgs(callee.arguments, scope, code, mfs);
|
|
8779
10313
|
const str = anyValue(strArg, scope, code, mfs);
|
|
8780
|
-
if (
|
|
10314
|
+
if (factory2 === null || typeof str !== "string") return null;
|
|
8781
10315
|
try {
|
|
8782
|
-
return word(str, boundary);
|
|
10316
|
+
return word(str, factory2.boundary, { caseInsensitive: factory2.caseInsensitive });
|
|
8783
10317
|
} catch {
|
|
8784
10318
|
return null;
|
|
8785
10319
|
}
|
|
@@ -8792,7 +10326,7 @@ function exprToCombi(node2, scope, code, mfs) {
|
|
|
8792
10326
|
const str = anyValue(strArg, scope, code, mfs);
|
|
8793
10327
|
if (typeof str !== "string") return null;
|
|
8794
10328
|
try {
|
|
8795
|
-
return word(str, factory2.boundary);
|
|
10329
|
+
return word(str, factory2.boundary, { caseInsensitive: factory2.caseInsensitive });
|
|
8796
10330
|
} catch {
|
|
8797
10331
|
return null;
|
|
8798
10332
|
}
|
|
@@ -9102,10 +10636,7 @@ function anyValue(node2, scope, code, mfs) {
|
|
|
9102
10636
|
if (node2.type === "CallExpression") {
|
|
9103
10637
|
const callee = node2.callee;
|
|
9104
10638
|
if (callee.type === "Identifier" && callee.name === "makeWord") {
|
|
9105
|
-
|
|
9106
|
-
const boundary = boundaryArg && boundaryArg.type !== "SpreadElement" ? anyValue(boundaryArg, scope, code, mfs) : "_0-9A-Za-z";
|
|
9107
|
-
if (typeof boundary !== "string") return null;
|
|
9108
|
-
return { tag: "wordFactory", boundary };
|
|
10639
|
+
return wordFactoryFromArgs(node2.arguments, scope, code, mfs);
|
|
9109
10640
|
}
|
|
9110
10641
|
return exprToCombi(node2, scope, code, mfs);
|
|
9111
10642
|
}
|
|
@@ -9115,10 +10646,7 @@ function evaluateWordFactory(node2, scope, code) {
|
|
|
9115
10646
|
if (node2.type !== "CallExpression") return null;
|
|
9116
10647
|
const callee = node2.callee;
|
|
9117
10648
|
if (callee.type !== "Identifier" || callee.name !== "makeWord") return null;
|
|
9118
|
-
|
|
9119
|
-
const boundary = boundaryArg && boundaryArg.type !== "SpreadElement" ? anyValue(boundaryArg, scope, code) : "_0-9A-Za-z";
|
|
9120
|
-
if (typeof boundary !== "string") return null;
|
|
9121
|
-
return { tag: "wordFactory", boundary };
|
|
10649
|
+
return wordFactoryFromArgs(node2.arguments, scope, code);
|
|
9122
10650
|
}
|
|
9123
10651
|
function evaluateExpr(node2, scope, code, mapFnSources) {
|
|
9124
10652
|
return exprToCombi(node2, scope, code, mapFnSources);
|
|
@@ -9213,6 +10741,11 @@ function evaluateParserFactory(factoryNode, scope, code, mapFnSources) {
|
|
|
9213
10741
|
r._ruleName = name;
|
|
9214
10742
|
if (r._def.tag === "node" && r._def.type === void 0) r._def.type = name;
|
|
9215
10743
|
};
|
|
10744
|
+
const ruleNameOf5 = (r) => r._ruleName;
|
|
10745
|
+
const isNamedRuleRefForAnotherRule2 = (r, key) => {
|
|
10746
|
+
const name = r._def.tag === "lazy" ? ruleNameOf5(r) : void 0;
|
|
10747
|
+
return name !== void 0 && name !== key;
|
|
10748
|
+
};
|
|
9216
10749
|
for (const key of keys) {
|
|
9217
10750
|
if (!ruleRefs.has(key)) {
|
|
9218
10751
|
const r = ref();
|
|
@@ -9243,12 +10776,15 @@ function evaluateParserFactory(factoryNode, scope, code, mapFnSources) {
|
|
|
9243
10776
|
for (const [key, e] of finalByKey) {
|
|
9244
10777
|
const val = anyValue(e.value, e.scope, e.code, mapFnSources);
|
|
9245
10778
|
if (!isCombinator(val)) return null;
|
|
9246
|
-
|
|
10779
|
+
if (val === ruleRefs.get(key)) return null;
|
|
10780
|
+
if (!isNamedRuleRefForAnotherRule2(val, key)) {
|
|
10781
|
+
tagRef(val, key);
|
|
10782
|
+
}
|
|
9247
10783
|
ruleRefs.get(key).define(val);
|
|
9248
10784
|
}
|
|
9249
10785
|
return ruleRefs;
|
|
9250
10786
|
}
|
|
9251
|
-
function
|
|
10787
|
+
function isDefinableRef2(v2) {
|
|
9252
10788
|
return isCombinator(v2) && typeof v2.define === "function" && v2._def.tag === "lazy";
|
|
9253
10789
|
}
|
|
9254
10790
|
function evaluateRefDeclaration(init, name, scope) {
|
|
@@ -9270,7 +10806,7 @@ function applyDefineStatement(callExpr, scope, code) {
|
|
|
9270
10806
|
if (mem.object.type !== "Identifier" || !mem.object.name) return false;
|
|
9271
10807
|
const target = scope.get(mem.object.name);
|
|
9272
10808
|
const refCombi = isScopeEntry(target) ? target.combi : isCombinator(target) ? target : null;
|
|
9273
|
-
if (!refCombi || !
|
|
10809
|
+
if (!refCombi || !isDefinableRef2(refCombi)) return false;
|
|
9274
10810
|
const args = callExpr.arguments;
|
|
9275
10811
|
if (args.length !== 1 || args[0].type === "SpreadElement") return false;
|
|
9276
10812
|
const inner = anyValue(args[0], scope, code, []);
|
|
@@ -9566,6 +11102,23 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9566
11102
|
for (const n of names) allNames.add(n);
|
|
9567
11103
|
}
|
|
9568
11104
|
if (macroImports.length === 0) return null;
|
|
11105
|
+
const factoryDecls = /* @__PURE__ */ new Map();
|
|
11106
|
+
for (const stmt of body) {
|
|
11107
|
+
const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
11108
|
+
if (decl?.type !== "VariableDeclaration") continue;
|
|
11109
|
+
if (decl.kind !== "const") continue;
|
|
11110
|
+
for (const d of decl.declarations) {
|
|
11111
|
+
const id2 = d.id;
|
|
11112
|
+
const init = d.init;
|
|
11113
|
+
if (id2.type !== "Identifier" || !id2.name || !init) continue;
|
|
11114
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
11115
|
+
factoryDecls.set(id2.name, {
|
|
11116
|
+
fn: init,
|
|
11117
|
+
declaredAt: d.end
|
|
11118
|
+
});
|
|
11119
|
+
}
|
|
11120
|
+
}
|
|
11121
|
+
}
|
|
9569
11122
|
const scope = /* @__PURE__ */ new Map();
|
|
9570
11123
|
const replacements = [];
|
|
9571
11124
|
const warnings = [];
|
|
@@ -9588,20 +11141,28 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9588
11141
|
const arg0 = args[0];
|
|
9589
11142
|
const arg1 = args[1];
|
|
9590
11143
|
const optionsFirst = arg0?.type === "ObjectExpression";
|
|
9591
|
-
const
|
|
9592
|
-
if (!
|
|
11144
|
+
const factoryArgRaw = optionsFirst ? arg1 : arg0;
|
|
11145
|
+
if (!factoryArgRaw) {
|
|
9593
11146
|
warn(init.start, `${label2}: rules() needs a factory argument`);
|
|
9594
11147
|
return null;
|
|
9595
11148
|
}
|
|
11149
|
+
const namedFactory = factoryArgRaw.type === "Identifier" ? factoryDecls.get(factoryArgRaw.name) : void 0;
|
|
11150
|
+
const factoryArg = namedFactory !== void 0 && namedFactory.declaredAt <= init.start ? namedFactory.fn : factoryArgRaw;
|
|
11151
|
+
const optionsArg = optionsFirst ? arg0 : arg1;
|
|
11152
|
+
const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
|
|
11153
|
+
(p) => p.key?.name === name
|
|
11154
|
+
)?.value : void 0;
|
|
11155
|
+
const hostModeValue = optionValue("hostMode");
|
|
11156
|
+
const gHostMode = hostModeValue?.type === "Literal" ? hostModeValue.value : void 0;
|
|
11157
|
+
if (hostModeValue !== void 0 && gHostMode !== "ast" && gHostMode !== "cst") {
|
|
11158
|
+
warn(init.start, `${label2}: rules({ hostMode }) must be the literal 'ast' or 'cst'`);
|
|
11159
|
+
return null;
|
|
11160
|
+
}
|
|
9596
11161
|
const ruleMap = evaluateParserFactory(factoryArg, scope, code, []);
|
|
9597
11162
|
if (!ruleMap) {
|
|
9598
11163
|
warn(init.start, `${label2}: rules(...) factory isn't statically evaluable`);
|
|
9599
11164
|
return null;
|
|
9600
11165
|
}
|
|
9601
|
-
const optionsArg = optionsFirst ? arg0 : arg1;
|
|
9602
|
-
const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
|
|
9603
|
-
(p) => p.key?.name === name
|
|
9604
|
-
)?.value : void 0;
|
|
9605
11166
|
const triviaValue = optionValue("trivia");
|
|
9606
11167
|
const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
|
|
9607
11168
|
const scanSkipValue = optionValue("scanSkip");
|
|
@@ -9614,13 +11175,29 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9614
11175
|
}
|
|
9615
11176
|
}
|
|
9616
11177
|
}
|
|
11178
|
+
if (gHostMode === "cst") {
|
|
11179
|
+
for (const rule of ruleMap.values()) {
|
|
11180
|
+
if (rule && !rule._meta.isTrivia) {
|
|
11181
|
+
;
|
|
11182
|
+
rule._meta.grammarHostMode = "cst";
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
9617
11186
|
return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
|
|
9618
11187
|
};
|
|
9619
11188
|
const compileRulesFactory = (init, label2) => {
|
|
9620
11189
|
const evaluated = evaluateRulesFactory(init, label2);
|
|
9621
11190
|
if (!evaluated) return null;
|
|
9622
11191
|
const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
|
|
9623
|
-
return {
|
|
11192
|
+
return {
|
|
11193
|
+
replacement: compiled?.replacement ?? null,
|
|
11194
|
+
ruleMap: evaluated.ruleMap,
|
|
11195
|
+
hostMode: compiled?.hostMode ?? "ast",
|
|
11196
|
+
hostBranchElided: compiled?.hostBranchElided ?? false,
|
|
11197
|
+
...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {},
|
|
11198
|
+
...evaluated.trivia ? { trivia: evaluated.trivia } : {},
|
|
11199
|
+
...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}
|
|
11200
|
+
};
|
|
9624
11201
|
};
|
|
9625
11202
|
const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
|
|
9626
11203
|
const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
|
|
@@ -9635,7 +11212,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9635
11212
|
const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
|
|
9636
11213
|
([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
|
|
9637
11214
|
).join(", ")}])`;
|
|
9638
|
-
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: [] }`;
|
|
11215
|
+
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: [] }`;
|
|
9639
11216
|
};
|
|
9640
11217
|
const isSpread = (it) => "__spreadLocal" in it;
|
|
9641
11218
|
const isIR = (it) => "ir" in it && !("ruleFns" in it);
|
|
@@ -9643,6 +11220,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9643
11220
|
const serializeList = (list) => `[${list.map(serializeItem).join(", ")}]`;
|
|
9644
11221
|
const withCarriedPieces = (grammarExpr, list) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)}, enumerable: false })`;
|
|
9645
11222
|
const withLeafMarker = (grammarExpr) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.leafComposed'), { value: true, enumerable: false })`;
|
|
11223
|
+
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})`;
|
|
9646
11224
|
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 })`;
|
|
9647
11225
|
const emittedCoverageDefinitions = (source) => {
|
|
9648
11226
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -9702,29 +11280,38 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9702
11280
|
importedPiecesCache.set(localName, result2);
|
|
9703
11281
|
return result2;
|
|
9704
11282
|
};
|
|
9705
|
-
const materializeCarried = (items, composing, captureTerminals = false) => {
|
|
11283
|
+
const materializeCarried = (items, composing, captureTerminals = false, hostMode) => {
|
|
9706
11284
|
const out = [];
|
|
9707
11285
|
for (const it of items) {
|
|
9708
11286
|
if (isSpread(it)) {
|
|
9709
|
-
for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals));
|
|
11287
|
+
for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals, hostMode));
|
|
9710
11288
|
} else if (isIR(it)) {
|
|
9711
|
-
out.push(materializePiece(it, composing, captureTerminals));
|
|
11289
|
+
out.push(materializePiece(it, composing, captureTerminals, hostMode));
|
|
9712
11290
|
} else {
|
|
9713
11291
|
out.push(it);
|
|
9714
11292
|
}
|
|
9715
11293
|
}
|
|
9716
11294
|
return out;
|
|
9717
11295
|
};
|
|
9718
|
-
const
|
|
9719
|
-
const
|
|
11296
|
+
const carriedRuleMapsDetailed = (items) => {
|
|
11297
|
+
const maps = [];
|
|
11298
|
+
const opaque = [];
|
|
9720
11299
|
const add = (it) => {
|
|
9721
|
-
if (isIR(it))
|
|
11300
|
+
if (isIR(it)) {
|
|
11301
|
+
maps.push(evalRuleMapIR(it.ir));
|
|
11302
|
+
return;
|
|
11303
|
+
}
|
|
11304
|
+
const o = it;
|
|
11305
|
+
opaque.push({
|
|
11306
|
+
ns: o.ns ?? "<unknown>",
|
|
11307
|
+
ruleNames: o.ruleFns instanceof Map ? [...o.ruleFns.keys()] : []
|
|
11308
|
+
});
|
|
9722
11309
|
};
|
|
9723
11310
|
for (const it of items) {
|
|
9724
11311
|
if (isSpread(it)) for (const p of importedPieces(it.__spreadLocal) ?? []) add(p);
|
|
9725
11312
|
else add(it);
|
|
9726
11313
|
}
|
|
9727
|
-
return
|
|
11314
|
+
return { maps, opaque };
|
|
9728
11315
|
};
|
|
9729
11316
|
const materializeLeafCoverage = (items, localRules, localNs, composing, captureTerminals = false) => {
|
|
9730
11317
|
const sources = [];
|
|
@@ -9762,6 +11349,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9762
11349
|
const ns = nsFor(label2);
|
|
9763
11350
|
const ir = serializeRuleMap(entries, scanSkip);
|
|
9764
11351
|
if (ir) return { carried: [{ ns, ir }] };
|
|
11352
|
+
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.`);
|
|
9765
11353
|
const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
|
|
9766
11354
|
return p ? { carried: [p] } : null;
|
|
9767
11355
|
};
|
|
@@ -9849,6 +11437,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9849
11437
|
return null;
|
|
9850
11438
|
}
|
|
9851
11439
|
const elements = arr.elements;
|
|
11440
|
+
const cOptions = init.arguments[1];
|
|
11441
|
+
const cHostModeValue = cOptions?.type === "ObjectExpression" ? (cOptions.properties ?? []).find(
|
|
11442
|
+
(p) => p.key?.name === "hostMode"
|
|
11443
|
+
)?.value : void 0;
|
|
11444
|
+
const cHostMode = cHostModeValue?.type === "Literal" ? cHostModeValue.value : void 0;
|
|
11445
|
+
if (cHostModeValue !== void 0 && cHostMode !== "ast" && cHostMode !== "cst") {
|
|
11446
|
+
warn(init.start, "compose({ hostMode }) must be the literal 'ast' or 'cst'");
|
|
11447
|
+
return null;
|
|
11448
|
+
}
|
|
9852
11449
|
const composing = composingTrivia(elements);
|
|
9853
11450
|
const carried = [];
|
|
9854
11451
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -9859,8 +11456,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9859
11456
|
}
|
|
9860
11457
|
carried.push(...r.carried);
|
|
9861
11458
|
}
|
|
9862
|
-
|
|
9863
|
-
|
|
11459
|
+
const detailed = once(() => carriedRuleMapsDetailed(carried));
|
|
11460
|
+
runFusedGatingDiagnostic(
|
|
11461
|
+
() => detailed().maps,
|
|
11462
|
+
void 0,
|
|
11463
|
+
() => detailed().opaque
|
|
11464
|
+
);
|
|
11465
|
+
const pieces = materializeCarried(carried, composing, false, cHostMode);
|
|
9864
11466
|
try {
|
|
9865
11467
|
return { replacement: emitFusedSource(pieces), carried, ...composing ? { trivia: composing } : {} };
|
|
9866
11468
|
} catch (e) {
|
|
@@ -9908,7 +11510,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
9908
11510
|
}
|
|
9909
11511
|
try {
|
|
9910
11512
|
const localNs = nsFor(`composeLeaf${init.start}`);
|
|
9911
|
-
|
|
11513
|
+
const detailedLeaf = once(() => carriedRuleMapsDetailed(carried));
|
|
11514
|
+
runFusedGatingDiagnostic(
|
|
11515
|
+
() => [...detailedLeaf().maps, [...localRules]],
|
|
11516
|
+
void 0,
|
|
11517
|
+
() => detailedLeaf().opaque
|
|
11518
|
+
);
|
|
9912
11519
|
const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
|
|
9913
11520
|
if (!plainLocalPiece) {
|
|
9914
11521
|
warn(init.start, "composeLeaf(): local rules could not be statically compiled");
|
|
@@ -10029,6 +11636,9 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
10029
11636
|
source,
|
|
10030
11637
|
compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(source)
|
|
10031
11638
|
);
|
|
11639
|
+
if (compiledRules.replacement !== null) {
|
|
11640
|
+
replacement = withHostMode(replacement, compiledRules.hostMode, compiledRules.hostBranchElided);
|
|
11641
|
+
}
|
|
10032
11642
|
if (exportPrefix && pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
|
|
10033
11643
|
const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
|
|
10034
11644
|
replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
|
|
@@ -10071,6 +11681,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
|
|
|
10071
11681
|
scope.set(varName, combiArray);
|
|
10072
11682
|
continue;
|
|
10073
11683
|
}
|
|
11684
|
+
if (factoryDecls.has(varName)) continue;
|
|
10074
11685
|
warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`);
|
|
10075
11686
|
continue;
|
|
10076
11687
|
}
|