parseman 0.52.0 → 0.53.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.
Files changed (62) hide show
  1. package/dist/analysis/diagnostics.js +358 -8
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +5929 -6172
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/parser.d.ts +34 -0
  6. package/dist/combinators/parser.d.ts.map +1 -1
  7. package/dist/combinators/ref.d.ts +0 -13
  8. package/dist/combinators/ref.d.ts.map +1 -1
  9. package/dist/compiler/borrowed-rules.d.ts +30 -0
  10. package/dist/compiler/borrowed-rules.d.ts.map +1 -0
  11. package/dist/compiler/compile-linkable-table.d.ts +1 -1
  12. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  13. package/dist/compiler/ir-serialize.d.ts +10 -4
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts +48 -91
  16. package/dist/compiler/linker.d.ts.map +1 -1
  17. package/dist/coverage.d.ts +1 -1
  18. package/dist/coverage.d.ts.map +1 -1
  19. package/dist/cst/reflection.d.ts.map +1 -1
  20. package/dist/index.js +416 -704
  21. package/dist/index.js.map +3 -3
  22. package/dist/language-service/index.js.map +1 -1
  23. package/dist/plugin/evaluator.d.ts +38 -2
  24. package/dist/plugin/evaluator.d.ts.map +1 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +1073 -523
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +21 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -0
  30. package/dist/plugin/reducer-resolver.d.ts +6 -0
  31. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  32. package/dist/spec/index.js +555 -6109
  33. package/dist/spec/index.js.map +3 -3
  34. package/dist/spec/railroad-lib.d.ts +7 -2
  35. package/dist/spec/railroad-lib.d.ts.map +1 -1
  36. package/dist/spec/railroad.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +11 -4
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/compile.d.ts +16 -0
  40. package/dist/table/compile.d.ts.map +1 -1
  41. package/dist/table/index.js +235 -87
  42. package/dist/table/index.js.map +2 -2
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/parser.ts +133 -67
  46. package/src/combinators/ref.ts +10 -1
  47. package/src/compiler/borrowed-rules.ts +88 -0
  48. package/src/compiler/compile-linkable-table.ts +1 -1
  49. package/src/compiler/grammar-coverage-ids.ts +35 -6
  50. package/src/compiler/ir-serialize.ts +12 -118
  51. package/src/compiler/linker.ts +190 -666
  52. package/src/coverage.ts +6 -4
  53. package/src/cst/reflection.ts +6 -1
  54. package/src/plugin/evaluator.ts +113 -13
  55. package/src/plugin/index.ts +588 -110
  56. package/src/plugin/ir-eval.ts +165 -0
  57. package/src/plugin/reducer-resolver.ts +20 -3
  58. package/src/spec/railroad-lib.ts +18 -8
  59. package/src/spec/railroad.ts +3 -4
  60. package/src/table/compile-rule-map.ts +18 -8
  61. package/src/table/compile.ts +61 -0
  62. package/src/version.ts +1 -1
@@ -3392,6 +3392,7 @@ function withRuleTrivia(trivia2, ctx, run) {
3392
3392
  ctx.triviaKindLabels = savedLabels;
3393
3393
  }
3394
3394
  }
3395
+ var UNDEFINED_REF = new Error("ref<T>() used before .define() was called");
3395
3396
  function ref() {
3396
3397
  let resolved = null;
3397
3398
  let resolvedScalar = null;
@@ -3407,7 +3408,7 @@ function ref() {
3407
3408
  _def: {
3408
3409
  tag: "lazy",
3409
3410
  thunk: () => {
3410
- if (!resolved) throw new Error("ref<T>() used before .define() was called");
3411
+ if (!resolved) throw UNDEFINED_REF;
3411
3412
  return resolved;
3412
3413
  }
3413
3414
  },
@@ -13801,7 +13802,8 @@ function children(def, winners) {
13801
13802
  return [];
13802
13803
  }
13803
13804
  const name = resolved._ruleName;
13804
- return name && winners?.[name] ? [winners[name]] : [resolved];
13805
+ const winner = name ? winners?.[name] : void 0;
13806
+ return winner !== void 0 && winner._def !== def ? [winner] : [resolved];
13805
13807
  }
13806
13808
  default:
13807
13809
  return [];
@@ -13826,16 +13828,39 @@ function buildGrammarPlan(entry, winners) {
13826
13828
  }
13827
13829
  }
13828
13830
  }
13831
+ const selfWinnerBody = (parser) => {
13832
+ if (parser._def.tag !== "lazy" || winners === void 0) return void 0;
13833
+ const name = winnerNames.get(parser);
13834
+ if (name === void 0 || winners[name] !== parser) return void 0;
13835
+ try {
13836
+ return parser._def.thunk();
13837
+ } catch {
13838
+ return void 0;
13839
+ }
13840
+ };
13841
+ const slotsPerBody = /* @__PURE__ */ new Map();
13842
+ if (winners) {
13843
+ for (const winner of Object.values(winners)) {
13844
+ const body = selfWinnerBody(winner);
13845
+ if (body !== void 0) slotsPerBody.set(body, (slotsPerBody.get(body) ?? 0) + 1);
13846
+ }
13847
+ }
13848
+ const sharedBody = (parser) => (slotsPerBody.get(parser) ?? 0) > 1;
13849
+ const ownRule = (parser, rule) => {
13850
+ const id = `rule:${rule}`;
13851
+ definitions.set(id, { id, kind: "rule" });
13852
+ rules.set(parser, id);
13853
+ };
13829
13854
  const seen = /* @__PURE__ */ new Set();
13830
13855
  const visit = (parser, path) => {
13831
13856
  if (seen.has(parser)) return;
13832
13857
  seen.add(parser);
13833
13858
  const rule = winnerNames.get(parser) ?? parser._ruleName;
13834
- if (rule && (winnerNames.has(parser) || parser._def.tag !== "lazy")) {
13835
- const id = `rule:${rule}`;
13836
- definitions.set(id, { id, kind: "rule" });
13837
- rules.set(parser, id);
13859
+ const body = selfWinnerBody(parser);
13860
+ if (rule && body === void 0 && !sharedBody(parser) && (winnerNames.has(parser) || parser._def.tag !== "lazy")) {
13861
+ ownRule(parser, rule);
13838
13862
  }
13863
+ if (rule && body !== void 0 && sharedBody(body)) ownRule(parser, rule);
13839
13864
  if (parser._def.tag === "choice") {
13840
13865
  const ids = parser._def.parsers.map((_, index) => `choice:${path}/arm:${index}`);
13841
13866
  choices.set(parser, ids);
@@ -13856,7 +13881,13 @@ function buildGrammarPlan(entry, winners) {
13856
13881
  };
13857
13882
  const roots = Array.isArray(entry) ? entry : [entry];
13858
13883
  for (const root of roots) {
13859
- visit(root, winnerNames.get(root) ?? root._ruleName ?? "entry");
13884
+ const name = winnerNames.get(root) ?? root._ruleName ?? "entry";
13885
+ const body = selfWinnerBody(root);
13886
+ if (body !== void 0 && sharedBody(body) && !seen.has(root)) {
13887
+ seen.add(root);
13888
+ ownRule(root, name);
13889
+ }
13890
+ visit(body ?? root, name);
13860
13891
  }
13861
13892
  return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules };
13862
13893
  }
@@ -15162,84 +15193,8 @@ function runDuplicationDiagnosticRules(ruleMap, opt) {
15162
15193
  return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
15163
15194
  }
15164
15195
 
15165
- // src/table/compile.ts
15166
- var ENTRY = "Entry";
15167
- function ownSource(fn) {
15168
- if (typeof fn !== "function") return null;
15169
- const text = Function.prototype.toString.call(fn);
15170
- if (!text.includes("[native code]")) return text;
15171
- const name = fn.name;
15172
- return typeof name === "string" && name.length > 0 && globalThis[name] === fn ? name : null;
15173
- }
15174
- function compile(combinator, mapFnSources, opts = {}) {
15175
- runDuplicationDiagnostic(combinator, opts.duplication);
15176
- const drain = beginCompileDegradationDrain();
15177
- let compiled = false;
15178
- try {
15179
- const out = compileImpl(combinator, mapFnSources, opts);
15180
- compiled = true;
15181
- return out;
15182
- } finally {
15183
- drain(compiled);
15184
- }
15185
- }
15186
- function compileImpl(combinator, mapFnSources, opts = {}) {
15187
- const plan = opts.coverage === true ? buildGrammarPlan(combinator) : void 0;
15188
- const settings = {
15189
- ...opts.hostMode === void 0 ? {} : { hostMode: opts.hostMode },
15190
- ...opts.trackLines === void 0 ? {} : { trackLines: opts.trackLines },
15191
- ...plan === void 0 ? {} : { coverage: plan }
15192
- };
15193
- const { prog, fnSrcs } = encodeTableProgram({ [ENTRY]: combinator }, settings);
15194
- const artifact = closureArtifact(prog);
15195
- const entry = tableRules(prog)[ENTRY];
15196
- const offered = opts.fnSources ?? mapFnSources;
15197
- const supplied = offered !== void 0 && offered.length === fnSrcs.length ? offered : void 0;
15198
- const sources = fnSrcs.map((s, i) => s ?? supplied?.[i] ?? ownSource(prog.fns[i]));
15199
- const unsourced = sources.reduce((acc, s, i) => s === null ? [...acc, i] : acc, []);
15200
- const runtimeOnly = [
15201
- ...prog.runtimeOnly ?? [],
15202
- ...unsourced.length === 0 ? [] : [`author reducer source missing for prog.fns[${unsourced.join(", ")}] \u2014 printing would emit \`() => {}\` and the parse would return no value`]
15203
- ];
15204
- const printable = runtimeOnly.length === 0;
15205
- const emitOpts = { fnSources: sources };
15206
- const source = printable ? emitTableModule(artifact, { name: "grammar", ...emitOpts }) : "";
15207
- const inlineExpression = printable ? emitTableExpression(artifact, { entry: ENTRY, runtimeRef: opts.runtimeRef ?? "tableRules", ...emitOpts }) : null;
15208
- const runOnce = (input, pos, ctx) => entry(input, pos, ctx);
15209
- return {
15210
- source,
15211
- inlineExpression,
15212
- ...printable ? {} : { runtimeOnly },
15213
- // The DENOMINATOR, handed back rather than scraped out of the emitted text.
15214
- // `plugin/index.ts` prefers a compiler-supplied `coverageDefinitions` and only
15215
- // falls back to regex-scanning generated hooks; a table has no hooks to scan,
15216
- // so this is the channel.
15217
- ...plan === void 0 ? {} : { coverageDefinitions: plan.definitions },
15218
- parse(input, pos = 0) {
15219
- return runOnce(input, pos, createParseContext());
15220
- },
15221
- parseWithContext(input, parseCtx, pos = 0) {
15222
- return runOnce(input, pos, parseCtx);
15223
- },
15224
- /**
15225
- * NO LONGER REFUSES. It used to throw unless the caller passed
15226
- * `{ recovery: true }`, because setting `_tolerant` on a table with no
15227
- * recovery rows would report a clean parse with an empty error list whatever
15228
- * the input — refusing was the right answer to that, and OPTIONAL RECOVERY
15229
- * was the wrong question. `compile()` requires no such flag, so the refusal
15230
- * was a `CompiledParser` contract break; recovery is now always lowered and
15231
- * there is nothing left to refuse.
15232
- */
15233
- parseWithErrors(input, pos = 0) {
15234
- const ctx = createParseContext();
15235
- const errors = [];
15236
- ctx._errors = errors;
15237
- ctx._tolerant = true;
15238
- const r = runOnce(input, pos, ctx);
15239
- return { ...r, errors };
15240
- }
15241
- };
15242
- }
15196
+ // src/cst/reflection-symbols.ts
15197
+ var GRAMMAR_REFLECTION = /* @__PURE__ */ Symbol.for("parseman.grammarReflection");
15243
15198
 
15244
15199
  // src/cst/reflection.ts
15245
15200
  function childrenOf2(def) {
@@ -15326,6 +15281,7 @@ function collectFromCombinator(parser, out, seen, followLazy = true) {
15326
15281
  function collectGrammarReflection(ruleMap, opts) {
15327
15282
  const byType = /* @__PURE__ */ new Map();
15328
15283
  const followLazy = opts?.followLazy !== false;
15284
+ const seen = /* @__PURE__ */ new Set();
15329
15285
  for (const [, rule] of ruleMap) {
15330
15286
  if (!followLazy && rule._def.tag === "lazy") {
15331
15287
  try {
@@ -15335,7 +15291,7 @@ function collectGrammarReflection(ruleMap, opts) {
15335
15291
  }
15336
15292
  continue;
15337
15293
  }
15338
- collectFromCombinator(rule, byType, /* @__PURE__ */ new Set(), followLazy);
15294
+ collectFromCombinator(rule, byType, seen, followLazy);
15339
15295
  }
15340
15296
  return { nodes: [...byType].map(([type, tags]) => ({ type, tags })) };
15341
15297
  }
@@ -15371,6 +15327,24 @@ function withAmbient(ruleMap, opts) {
15371
15327
  return [name, wrapped];
15372
15328
  });
15373
15329
  }
15330
+ function compileRuleMapRunnable(ruleMap, opts = {}) {
15331
+ const encoded = encodeForRun(ruleMap, opts);
15332
+ if (encoded === null) return null;
15333
+ const { prog, hostMode, plan } = encoded;
15334
+ const artifact = closureArtifact(prog);
15335
+ const reflection = collectGrammarReflection(ruleMap);
15336
+ return {
15337
+ keys: ruleMap.map(([key]) => key),
15338
+ hostMode,
15339
+ hostBranchElided: hostMode === "ast" && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
15340
+ reflection,
15341
+ ...plan === void 0 ? {} : { coverageDefinitions: plan.definitions },
15342
+ // Reflection rides on the map's metadata prototype from birth. Only `compile()`
15343
+ // asks to specialise; every other caller runs the closure artifact.
15344
+ rules: tableRules(opts.specialise === true ? prog : artifact, { [GRAMMAR_REFLECTION]: reflection }),
15345
+ prog: artifact
15346
+ };
15347
+ }
15374
15348
  function encodeForRun(ruleMap, opts) {
15375
15349
  runDuplicationDiagnosticRules(ruleMap, opts.duplication);
15376
15350
  const encodeMap = withAmbient(ruleMap, opts);
@@ -15439,11 +15413,185 @@ function compileRuleMap(ruleMap, opts = {}) {
15439
15413
  hostBranchElided: hostMode === "ast" && ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
15440
15414
  reflection: collectGrammarReflection(ruleMap),
15441
15415
  ...plan === void 0 ? {} : { coverageDefinitions: plan.definitions },
15442
- rules: tableRules(prog),
15416
+ rules: tableRules(artifact),
15443
15417
  prog: artifact
15444
15418
  };
15445
15419
  }
15446
15420
 
15421
+ // src/compiler/borrowed-rules.ts
15422
+ var nameOf = (c) => c._ruleName;
15423
+ function deref(c) {
15424
+ const seen = /* @__PURE__ */ new Set();
15425
+ let current = c;
15426
+ while (current._def.tag === "lazy" && !seen.has(current)) {
15427
+ seen.add(current);
15428
+ try {
15429
+ current = current._def.thunk();
15430
+ } catch {
15431
+ return void 0;
15432
+ }
15433
+ }
15434
+ return current;
15435
+ }
15436
+ function borrowedRuleReference(map) {
15437
+ const seen = /* @__PURE__ */ new Set();
15438
+ for (const [rule, self] of Object.entries(map)) {
15439
+ let found;
15440
+ const visit = (c) => {
15441
+ if (found !== void 0) return;
15442
+ if (c._def.tag === "lazy") {
15443
+ if (!winnerWrapsReference(self, c)) {
15444
+ const name = nameOf(c);
15445
+ const winner = name !== void 0 && Object.hasOwn(map, name) ? map[name] : void 0;
15446
+ if (winner !== void 0) {
15447
+ if (winnerWrapsReference(winner, c) || deref(c) === deref(winner)) return;
15448
+ found = name;
15449
+ return;
15450
+ }
15451
+ }
15452
+ if (seen.has(c)) return;
15453
+ seen.add(c);
15454
+ let body;
15455
+ try {
15456
+ body = c._def.thunk();
15457
+ } catch {
15458
+ return;
15459
+ }
15460
+ visit(body);
15461
+ return;
15462
+ }
15463
+ if (seen.has(c)) return;
15464
+ seen.add(c);
15465
+ for (const child of childrenOf(c._def)) visit(child);
15466
+ };
15467
+ visit(self);
15468
+ if (found !== void 0) return { rule, reference: found };
15469
+ }
15470
+ return void 0;
15471
+ }
15472
+ function borrowedRuleMessage(api, hit) {
15473
+ return `${api}: rule "${hit.rule}" is a rule object borrowed from another grammar, and its graph references "${hit.reference}", which this grammar also defines. The interpreter would keep the other grammar's "${hit.reference}" while a compiled table binds this grammar's, so the two would accept different input. Compose the grammar that defines "${hit.rule}" instead of borrowing its rule object, so its references resolve to this grammar's rules.`;
15474
+ }
15475
+
15476
+ // src/table/compile.ts
15477
+ var ENTRY = "Entry";
15478
+ function ownSource(fn) {
15479
+ if (typeof fn !== "function") return null;
15480
+ const text = Function.prototype.toString.call(fn);
15481
+ if (!text.includes("[native code]")) return text;
15482
+ const name = fn.name;
15483
+ return typeof name === "string" && name.length > 0 && globalThis[name] === fn ? name : null;
15484
+ }
15485
+ function compile(target, second, third) {
15486
+ if (!isCombinator(target)) return compileMap(target, second);
15487
+ return compileRoot(target, second, third);
15488
+ }
15489
+ function isCombinator(x) {
15490
+ return typeof x === "object" && x !== null && typeof x.parse === "function" && "_def" in x;
15491
+ }
15492
+ function compileMap(grammar, opts = {}) {
15493
+ const entries = [];
15494
+ for (const [key, value] of Object.entries(grammar)) {
15495
+ if (!isCombinator(value)) throw new TypeError(`compile: grammar entry "${key}" is not a parser`);
15496
+ if (value._def.tag === "lazy") {
15497
+ try {
15498
+ value._def.thunk();
15499
+ } catch {
15500
+ continue;
15501
+ }
15502
+ }
15503
+ entries.push([key, value]);
15504
+ }
15505
+ const borrowed = borrowedRuleReference(Object.fromEntries(entries));
15506
+ if (borrowed !== void 0) throw new Error(borrowedRuleMessage("compile", borrowed));
15507
+ const drain = beginCompileDegradationDrain();
15508
+ let done = false;
15509
+ try {
15510
+ const refusals = [];
15511
+ const compiled = compileRuleMapRunnable(entries, {
15512
+ ...opts.hostMode ? { hostMode: opts.hostMode } : {},
15513
+ refusals,
15514
+ specialise: true
15515
+ });
15516
+ if (compiled === null) {
15517
+ throw new Error(`compile: this grammar could not be encoded to a table${refusals.length ? ` \u2014 ${refusals.join("; ")}` : ""}`);
15518
+ }
15519
+ done = true;
15520
+ return compiled.rules;
15521
+ } finally {
15522
+ drain(done);
15523
+ }
15524
+ }
15525
+ function compileRoot(combinator, mapFnSources, opts = {}) {
15526
+ runDuplicationDiagnostic(combinator, opts.duplication);
15527
+ const drain = beginCompileDegradationDrain();
15528
+ let compiled = false;
15529
+ try {
15530
+ const out = compileImpl(combinator, mapFnSources, opts);
15531
+ compiled = true;
15532
+ return out;
15533
+ } finally {
15534
+ drain(compiled);
15535
+ }
15536
+ }
15537
+ function compileImpl(combinator, mapFnSources, opts = {}) {
15538
+ const plan = opts.coverage === true ? buildGrammarPlan(combinator) : void 0;
15539
+ const settings = {
15540
+ ...opts.hostMode === void 0 ? {} : { hostMode: opts.hostMode },
15541
+ ...opts.trackLines === void 0 ? {} : { trackLines: opts.trackLines },
15542
+ ...plan === void 0 ? {} : { coverage: plan }
15543
+ };
15544
+ const { prog, fnSrcs } = encodeTableProgram({ [ENTRY]: combinator }, settings);
15545
+ const artifact = closureArtifact(prog);
15546
+ const entry = tableRules(prog)[ENTRY];
15547
+ const offered = opts.fnSources ?? mapFnSources;
15548
+ const supplied = offered !== void 0 && offered.length === fnSrcs.length ? offered : void 0;
15549
+ const sources = fnSrcs.map((s, i) => s ?? supplied?.[i] ?? ownSource(prog.fns[i]));
15550
+ const unsourced = sources.reduce((acc, s, i) => s === null ? [...acc, i] : acc, []);
15551
+ const runtimeOnly = [
15552
+ ...prog.runtimeOnly ?? [],
15553
+ ...unsourced.length === 0 ? [] : [`author reducer source missing for prog.fns[${unsourced.join(", ")}] \u2014 printing would emit \`() => {}\` and the parse would return no value`]
15554
+ ];
15555
+ const printable = runtimeOnly.length === 0;
15556
+ const emitOpts = { fnSources: sources };
15557
+ const source = printable ? emitTableModule(artifact, { name: "grammar", ...emitOpts }) : "";
15558
+ const inlineExpression = printable ? emitTableExpression(artifact, { entry: ENTRY, runtimeRef: opts.runtimeRef ?? "tableRules", ...emitOpts }) : null;
15559
+ const runOnce = (input, pos, ctx) => entry(input, pos, ctx);
15560
+ return {
15561
+ source,
15562
+ inlineExpression,
15563
+ ...printable ? {} : { runtimeOnly },
15564
+ // The DENOMINATOR, handed back rather than scraped out of the emitted text.
15565
+ // `plugin/index.ts` prefers a compiler-supplied `coverageDefinitions` and only
15566
+ // falls back to regex-scanning generated hooks; a table has no hooks to scan,
15567
+ // so this is the channel.
15568
+ ...plan === void 0 ? {} : { coverageDefinitions: plan.definitions },
15569
+ parse(input, pos = 0) {
15570
+ return runOnce(input, pos, createParseContext());
15571
+ },
15572
+ parseWithContext(input, parseCtx, pos = 0) {
15573
+ return runOnce(input, pos, parseCtx);
15574
+ },
15575
+ /**
15576
+ * NO LONGER REFUSES. It used to throw unless the caller passed
15577
+ * `{ recovery: true }`, because setting `_tolerant` on a table with no
15578
+ * recovery rows would report a clean parse with an empty error list whatever
15579
+ * the input — refusing was the right answer to that, and OPTIONAL RECOVERY
15580
+ * was the wrong question. `compile()` requires no such flag, so the refusal
15581
+ * was a `CompiledParser` contract break; recovery is now always lowered and
15582
+ * there is nothing left to refuse.
15583
+ */
15584
+ parseWithErrors(input, pos = 0) {
15585
+ const ctx = createParseContext();
15586
+ const errors = [];
15587
+ ctx._errors = errors;
15588
+ ctx._tolerant = true;
15589
+ const r = runOnce(input, pos, ctx);
15590
+ return { ...r, errors };
15591
+ }
15592
+ };
15593
+ }
15594
+
15447
15595
  // src/table/fold.ts
15448
15596
  function tableVariants(source, name) {
15449
15597
  const folded = "base" in source ? source : expandCompactFolded(source);