parseman 0.15.0 → 0.16.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/dist/index.js CHANGED
@@ -261,12 +261,16 @@ function pushTriviaLogEntry(ctx, start, end, kindIndex) {
261
261
  }
262
262
 
263
263
  // src/combinators/literal.ts
264
- var _collatorCache = null;
265
- function collator() {
266
- if (_collatorCache === null) {
267
- _collatorCache = new Intl.Collator(void 0, { sensitivity: "accent" });
264
+ function asciiFoldEq(a, b) {
265
+ if (a.length !== b.length) return false;
266
+ for (let i = 0; i < a.length; i++) {
267
+ let ca = a.charCodeAt(i);
268
+ let cb = b.charCodeAt(i);
269
+ if (ca >= 65 && ca <= 90) ca += 32;
270
+ if (cb >= 65 && cb <= 90) cb += 32;
271
+ if (ca !== cb) return false;
268
272
  }
269
- return _collatorCache;
273
+ return true;
270
274
  }
271
275
  function literal(value, opts = {}) {
272
276
  const caseInsensitive = opts.caseInsensitive ?? false;
@@ -281,8 +285,9 @@ function literal(value, opts = {}) {
281
285
  const lower = value.toLowerCase();
282
286
  const firstUpper = upper.codePointAt(0);
283
287
  const firstLower = lower.codePointAt(0);
284
- meta.firstSet = firstLower !== void 0 && firstUpper !== void 0 ? { kind: "ranges", ranges: [
285
- { lo: Math.min(firstLower, firstUpper), hi: Math.max(firstLower, firstUpper) }
288
+ meta.firstSet = firstLower !== void 0 && firstUpper !== void 0 ? firstLower === firstUpper ? { kind: "ranges", ranges: [{ lo: firstLower, hi: firstLower }] } : { kind: "ranges", ranges: [
289
+ { lo: firstLower, hi: firstLower },
290
+ { lo: firstUpper, hi: firstUpper }
286
291
  ] } : firstSet;
287
292
  }
288
293
  return {
@@ -295,7 +300,7 @@ function literal(value, opts = {}) {
295
300
  return failAt(ctx, [JSON.stringify(value)], pos);
296
301
  }
297
302
  const slice = input.slice(pos, end);
298
- const matched = caseInsensitive ? collator().compare(slice, value) === 0 : slice === value;
303
+ const matched = caseInsensitive ? asciiFoldEq(slice, value) : slice === value;
299
304
  if (matched) {
300
305
  const span = { start: pos, end };
301
306
  const leaf = { _tag: "leaf", value: slice, span };
@@ -1629,8 +1634,8 @@ function classToRanges(cls) {
1629
1634
  return parseClassRanges(body);
1630
1635
  }
1631
1636
  var foldEq = (cVar, cp) => {
1632
- if (cp >= 65 && cp <= 90) return `(${cVar} === ${cp} || ${cVar} === ${cp + 32})`;
1633
- if (cp >= 97 && cp <= 122) return `(${cVar} === ${cp} || ${cVar} === ${cp - 32})`;
1637
+ if (cp >= 65 && cp <= 90) return `(${cVar} | 32) === ${cp + 32}`;
1638
+ if (cp >= 97 && cp <= 122) return `(${cVar} | 32) === ${cp}`;
1634
1639
  return `${cVar} === ${cp}`;
1635
1640
  };
1636
1641
  function literalCodePoints(frag) {
@@ -1881,6 +1886,12 @@ function firstSetsDisjoint(a, b) {
1881
1886
  return classDisjoint(a, b);
1882
1887
  }
1883
1888
  function fixedClassSeq(shape) {
1889
+ if (shape.kind === "litFold") {
1890
+ return shape.open.map((cp) => {
1891
+ const other = cp >= 65 && cp <= 90 ? cp + 32 : cp >= 97 && cp <= 122 ? cp - 32 : cp;
1892
+ return other === cp ? { ranges: [[cp, cp]], negated: false } : { ranges: [[Math.min(cp, other), Math.min(cp, other)], [Math.max(cp, other), Math.max(cp, other)]], negated: false };
1893
+ });
1894
+ }
1884
1895
  if (shape.kind !== "seq") return null;
1885
1896
  const out = [];
1886
1897
  for (const p of shape.parts) {
@@ -2302,12 +2313,34 @@ function parseScanShapeCore(source) {
2302
2313
  }
2303
2314
  function scanShapeFromRegex(source, flags) {
2304
2315
  if (/[msu]/.test(flags)) return null;
2305
- if (/i/.test(flags)) {
2306
- const open = literalCodePoints(source);
2307
- return open ? { kind: "litFold", open } : null;
2308
- }
2316
+ if (/i/.test(flags)) return caseFoldShape(source);
2309
2317
  return parseScanShape(source);
2310
2318
  }
2319
+ function caseFoldShape(source) {
2320
+ const la = stripTrailingLookahead(source);
2321
+ if (la) {
2322
+ const inner = caseFoldLiteralOrAlt(la.base);
2323
+ if (!inner) return null;
2324
+ const operand = { ranges: la.ranges, negated: la.classNegated };
2325
+ if (!lookaheadUnambiguous(inner, operand, la.negative)) return null;
2326
+ return { kind: "lookahead", inner, ranges: la.ranges, classNegated: la.classNegated, negative: la.negative };
2327
+ }
2328
+ return caseFoldLiteralOrAlt(source);
2329
+ }
2330
+ function caseFoldLiteralOrAlt(source) {
2331
+ const asciiOnly = (cps) => cps.every((cp) => cp <= 127);
2332
+ const open = literalCodePoints(source);
2333
+ if (open) return asciiOnly(open) ? { kind: "litFold", open } : null;
2334
+ const arms = splitTopLevelAlternation(source);
2335
+ if (!arms || arms.length < 2) return null;
2336
+ const litArms = [];
2337
+ for (const arm of arms) {
2338
+ const cp = literalCodePoints(arm);
2339
+ if (!cp || !asciiOnly(cp)) return null;
2340
+ litArms.push({ kind: "litFold", open: cp });
2341
+ }
2342
+ return { kind: "alt", arms: litArms, disjoint: false, firsts: litArms.map(() => null) };
2343
+ }
2311
2344
  var classCond = (cVar, ranges) => ranges.map(([lo, hi]) => lo === hi ? `${cVar} === ${lo}` : `(${cVar} >= ${lo} && ${cVar} <= ${hi})`).join(" || ");
2312
2345
  var litCond = (base, cps, firstVar) => cps.map(
2313
2346
  (cp, k) => k === 0 && firstVar ? `${firstVar} === ${cp}` : `input.charCodeAt(${base}${k ? ` + ${k}` : ""}) === ${cp}`
@@ -2319,6 +2352,25 @@ function stringHardStop(shape) {
2319
2352
  );
2320
2353
  }
2321
2354
  var codeAt = (start, firstChar) => firstChar ?? `input.charCodeAt(${start})`;
2355
+ var ALT_SWITCH_RANGE_LIMIT = 4;
2356
+ var ALT_SWITCH_MAX_CASES = 48;
2357
+ var ALT_SWITCH_MIN_CASES = 3;
2358
+ function planAltSwitch(firsts) {
2359
+ const cases = [];
2360
+ let total = 0;
2361
+ for (const fs of firsts) {
2362
+ if (!fs || fs.negated) return { kind: "if" };
2363
+ const pts = [];
2364
+ for (const [lo, hi] of fs.ranges) {
2365
+ if (hi - lo + 1 > ALT_SWITCH_RANGE_LIMIT) return { kind: "if" };
2366
+ for (let cp = lo; cp <= hi; cp++) pts.push(cp);
2367
+ }
2368
+ total += pts.length;
2369
+ if (total > ALT_SWITCH_MAX_CASES) return { kind: "if" };
2370
+ cases.push(pts);
2371
+ }
2372
+ return total >= ALT_SWITCH_MIN_CASES ? { kind: "switch", cases } : { kind: "if" };
2373
+ }
2322
2374
  function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2323
2375
  if (shape.kind === "lookahead") {
2324
2376
  const inner = emitShapeMatch(shape.inner, start, mint, ind2, firstChar);
@@ -2343,18 +2395,57 @@ function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2343
2395
  const okV = mint("_ok");
2344
2396
  const endV2 = mint("_end");
2345
2397
  const lines = [`${ind2}let ${okV} = false`, `${ind2}let ${endV2} = ${start}`];
2398
+ if (shape.arms.length >= 2 && shape.arms.every((a) => a.kind === "litFold" && a.open.length >= 1)) {
2399
+ const groups = /* @__PURE__ */ new Map();
2400
+ for (const a of shape.arms) {
2401
+ const open = a.open;
2402
+ const f = open[0];
2403
+ const lower = f >= 65 && f <= 90 ? f + 32 : f;
2404
+ (groups.get(lower) ?? groups.set(lower, []).get(lower)).push(open);
2405
+ }
2406
+ lines.push(`${ind2}if (${start} < input.length) switch (${codeAt(start, firstChar)}) {`);
2407
+ for (const [lower, arms] of groups) {
2408
+ const labels = lower >= 97 && lower <= 122 ? `case ${lower}: case ${lower - 32}:` : `case ${lower}:`;
2409
+ lines.push(`${ind2} ${labels} {`);
2410
+ for (const open of arms) {
2411
+ const tail = open.slice(1).map((cp, i) => foldEq(`input.charCodeAt(${start} + ${i + 1})`, cp));
2412
+ const cond = tail.length ? ` && ${tail.join(" && ")}` : "";
2413
+ lines.push(`${ind2} if (${start} + ${open.length} <= input.length${cond}) { ${okV} = true; ${endV2} = ${start} + ${open.length}; break }`);
2414
+ }
2415
+ lines.push(`${ind2} break`);
2416
+ lines.push(`${ind2} }`);
2417
+ }
2418
+ lines.push(`${ind2}}`);
2419
+ return { setup: lines, ok: okV, end: endV2 };
2420
+ }
2346
2421
  if (shape.disjoint) {
2347
- const c0 = codeAt(start, firstChar);
2348
- shape.arms.forEach((arm, k) => {
2349
- const fs = shape.firsts[k];
2350
- const cond = fs.negated ? `!(${classCond(c0, fs.ranges)})` : `(${classCond(c0, fs.ranges)})`;
2351
- const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2352
- lines.push(`${ind2}${k === 0 ? "if" : "else if"} (${start} < input.length && ${cond}) {`);
2353
- lines.push(...m.setup);
2354
- lines.push(`${ind2} ${okV} = ${m.ok}`);
2355
- lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2422
+ const plan = planAltSwitch(shape.firsts);
2423
+ if (plan.kind === "switch") {
2424
+ lines.push(`${ind2}if (${start} < input.length) switch (${codeAt(start, firstChar)}) {`);
2425
+ shape.arms.forEach((arm, k) => {
2426
+ const labels = plan.cases[k].map((cp) => `case ${cp}:`).join(" ");
2427
+ const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2428
+ lines.push(`${ind2} ${labels} {`);
2429
+ lines.push(...m.setup);
2430
+ lines.push(`${ind2} ${okV} = ${m.ok}`);
2431
+ lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2432
+ lines.push(`${ind2} break`);
2433
+ lines.push(`${ind2} }`);
2434
+ });
2356
2435
  lines.push(`${ind2}}`);
2357
- });
2436
+ } else {
2437
+ const c0 = codeAt(start, firstChar);
2438
+ shape.arms.forEach((arm, k) => {
2439
+ const fs = shape.firsts[k];
2440
+ const cond = fs.negated ? `!(${classCond(c0, fs.ranges)})` : `(${classCond(c0, fs.ranges)})`;
2441
+ const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2442
+ lines.push(`${ind2}${k === 0 ? "if" : "else if"} (${start} < input.length && ${cond}) {`);
2443
+ lines.push(...m.setup);
2444
+ lines.push(`${ind2} ${okV} = ${m.ok}`);
2445
+ lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2446
+ lines.push(`${ind2}}`);
2447
+ });
2448
+ }
2358
2449
  } else {
2359
2450
  const lbl = mint("_alt");
2360
2451
  lines.push(`${ind2}${lbl}: {`);
@@ -2842,6 +2933,8 @@ function tryInlineDestructureTransform(src, valueVars) {
2842
2933
  }
2843
2934
 
2844
2935
  // src/compiler/codegen.ts
2936
+ var HOST_READS_DECL = "const _hostReads = (b, n) => { if (b === undefined) return false; let s; try { s = Function.prototype.toString.call(b) } catch (e) { return true } if (/\\barguments\\b/.test(s)) return true; const m = /^[^(]*\\(([\\s\\S]*?)\\)/.exec(s); if (m && /\\.\\.\\.|=/.test(m[1])) return true; return b.length > n }";
2937
+ var _loweringSink = null;
2845
2938
  function v(ctx, prefix = "_v") {
2846
2939
  return `${prefix}${ctx.vars++}`;
2847
2940
  }
@@ -3240,13 +3333,14 @@ function emitLit(def, ctx, pos) {
3240
3333
  const vv = v(ctx);
3241
3334
  const expectedStr = JSON.stringify(JSON.stringify(value));
3242
3335
  const stmts = [];
3243
- if (caseInsensitive) {
3244
- ctx.needsCollator = true;
3336
+ if (caseInsensitive && len > 0) {
3337
+ const match = Array.from(
3338
+ { length: len },
3339
+ (_, i) => `(${foldEq(`input.charCodeAt(${pos}${i > 0 ? ` + ${i}` : ""})`, value.charCodeAt(i))})`
3340
+ ).join(" && ");
3245
3341
  stmts.push(
3246
- ...emitIfFail(ctx, `${pos} + ${len} > input.length`, failBody(ctx, expectedStr, pos)),
3247
- `${ind(ctx)}const ${vv}_s = input.slice(${pos}, ${pos} + ${len})`,
3248
- ...emitIfFail(ctx, `_collator.compare(${vv}_s, ${JSON.stringify(value)}) !== 0`, failBody(ctx, expectedStr, pos)),
3249
- `${ind(ctx)}const ${vv} = ${vv}_s`
3342
+ ...emitIfFail(ctx, `${pos} + ${len} > input.length || !(${match})`, failBody(ctx, expectedStr, pos)),
3343
+ `${ind(ctx)}const ${vv} = input.slice(${pos}, ${pos} + ${len})`
3250
3344
  );
3251
3345
  } else if (len === 0) {
3252
3346
  stmts.push(`${ind(ctx)}const ${vv} = ''`);
@@ -3367,6 +3461,7 @@ function emitRegex(def, ctx, pos) {
3367
3461
  return { stmts: stmts2, valueVar: vv2, endVar: scanned.endVar };
3368
3462
  }
3369
3463
  }
3464
+ _loweringSink?.add(`/${def.source}/${def.flags}`);
3370
3465
  const flags = "y" + def.flags.replace(/[gy]/g, "");
3371
3466
  const key = `${def.source}/${flags}`;
3372
3467
  let rName = ctx.regexMap.get(key);
@@ -3947,15 +4042,19 @@ function emitNode(def, ctx, pos) {
3947
4042
  ctx.buildSrcs.push(def.buildSrc ?? null);
3948
4043
  }
3949
4044
  const i = ind(ctx);
3950
- const capturesTrivia = mkType !== null || structural || buildReadsTrivia(def);
3951
- const clonesState = structural || buildReadsState(def);
4045
+ const capturesTrivia = mkType !== null || !structural && buildReadsTrivia(def);
4046
+ const clonesState = !structural && buildReadsState(def);
3952
4047
  const chV = v(ctx, "_ch");
3953
4048
  const rawV = v(ctx, "_raw");
3954
- const tlV = capturesTrivia ? v(ctx, "_tl") : "_EMPTY_TL";
4049
+ const capTLv = structural ? v(ctx, "_ctl") : null;
4050
+ const capSTv = structural ? v(ctx, "_cst") : null;
4051
+ const tlV = capturesTrivia || structural ? v(ctx, "_tl") : "_EMPTY_TL";
3955
4052
  if (!capturesTrivia) ctx.needsEmptyTl = true;
3956
4053
  const sc = v(ctx, "_sc"), sl = v(ctx, "_sl"), sr = v(ctx, "_sr"), st = v(ctx, "_st"), stl = v(ctx, "_stl");
3957
- const allocStmt = capturesTrivia ? `${i}const ${chV} = [], ${rawV} = [], ${tlV} = []` : `${i}const ${chV} = [], ${rawV} = []`;
3958
- const innerTl = capturesTrivia ? tlV : "undefined";
4054
+ if (structural) ctx.needsHostReads = true;
4055
+ const allocStmt = structural ? `${i}const ${capTLv} = _ctx._pmCapTL ??= _hostReads(_ctx.build, 4), ${capSTv} = _ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 5))
4056
+ ${i}const ${chV} = [], ${rawV} = [], ${tlV} = ${capTLv} ? [] : _EMPTY_TL` : capturesTrivia ? `${i}const ${chV} = [], ${rawV} = [], ${tlV} = []` : `${i}const ${chV} = [], ${rawV} = []`;
4057
+ const innerTl = structural ? `${capTLv} ? ${tlV} : undefined` : capturesTrivia ? tlV : "undefined";
3959
4058
  const stmts = [
3960
4059
  allocStmt,
3961
4060
  `${i}const ${sc} = _ctx._cstChildren, ${sl} = _ctx._cstLeaves, ${sr} = _ctx._cstRawChildren, ${st} = _ctx.captureTrivia, ${stl} = _ctx._cstTriviaLog`,
@@ -3966,7 +4065,10 @@ function emitNode(def, ctx, pos) {
3966
4065
  stmts.push(`${i}_ctx._cstChildren = ${sc}; _ctx._cstLeaves = ${sl}; _ctx._cstRawChildren = ${sr}; _ctx.captureTrivia = ${st}; _ctx._cstTriviaLog = ${stl}`);
3967
4066
  stmts.push(...emitIfFail(ctx, `!${okVar}`, propagateFailBody(ctx)));
3968
4067
  let stV = "undefined";
3969
- if (clonesState) {
4068
+ if (structural) {
4069
+ stV = v(ctx, "_nst");
4070
+ stmts.push(`${i}const ${stV} = ${capSTv} && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
4071
+ } else if (clonesState) {
3970
4072
  stV = v(ctx, "_nst");
3971
4073
  stmts.push(`${i}const ${stV} = _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
3972
4074
  }
@@ -4402,7 +4504,6 @@ function compile(combinator, mapFnSources) {
4402
4504
  buildFns: [],
4403
4505
  buildSrcs: [],
4404
4506
  runtimeParsers: [],
4405
- needsCollator: false,
4406
4507
  namedParsers: /* @__PURE__ */ new Map(),
4407
4508
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4408
4509
  triviaFnNames: /* @__PURE__ */ new Map(),
@@ -4411,18 +4512,18 @@ function compile(combinator, mapFnSources) {
4411
4512
  lazyUsage: analyzeLazyUsage(combinator)
4412
4513
  };
4413
4514
  const r = emit(combinator, ctx, "_pos");
4414
- const collatorDecl = ctx.needsCollator ? `const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })
4415
- ` : "";
4416
4515
  const namedPrelude = ctx.namedFnDecls.length > 0 ? [...namedFnPrelude(), ""] : [];
4417
4516
  const emptyTlDecls = ctx.needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [];
4517
+ const hostReadsDecls = ctx.needsHostReads ? [HOST_READS_DECL] : [];
4418
4518
  const source = [
4419
4519
  ...emptyTlDecls,
4520
+ ...hostReadsDecls,
4420
4521
  ...ctx.regexDecls,
4421
4522
  ...ctx.expectedDecls,
4422
4523
  "",
4423
4524
  ...namedPrelude,
4424
4525
  ctx.namedFnDecls.join("\n\n"),
4425
- `${collatorDecl}function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4526
+ `function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4426
4527
  ` let pos = _pos`,
4427
4528
  ...r.stmts,
4428
4529
  ` return { ok: true, value: ${r.valueVar}, span: { start: _pos, end: ${r.endVar} } }`,
@@ -4430,9 +4531,9 @@ function compile(combinator, mapFnSources) {
4430
4531
  ].join("\n");
4431
4532
  const fn = new Function("input", "_pos", "_rp", "_mf", "_build", "_ctx", [
4432
4533
  ...emptyTlDecls,
4534
+ ...hostReadsDecls,
4433
4535
  ...ctx.regexDecls,
4434
4536
  ...ctx.expectedDecls,
4435
- collatorDecl,
4436
4537
  ...namedPrelude,
4437
4538
  ...ctx.namedFnDecls.flatMap((decl, i) => i > 0 ? ["", decl] : [decl]),
4438
4539
  `let pos = _pos`,
@@ -4449,7 +4550,7 @@ function compile(combinator, mapFnSources) {
4449
4550
  const buildSources = ctx.buildFns.length === 0 ? void 0 : ctx.buildSrcs;
4450
4551
  const mfCovered = ctx.mapFns.length === 0 || effectiveSources !== void 0 && effectiveSources.length === ctx.mapFns.length;
4451
4552
  const canInline = ctx.runtimeParsers.length === 0 && mfCovered && buildCovered;
4452
- const inlineExpression = canInline ? buildInlineExpression(ctx, r, collatorDecl, effectiveSources, buildSources) : null;
4553
+ const inlineExpression = canInline ? buildInlineExpression(ctx, r, effectiveSources, buildSources) : null;
4453
4554
  return {
4454
4555
  source,
4455
4556
  inlineExpression,
@@ -4505,7 +4606,6 @@ function compileLinkable(ruleMapArg, ns) {
4505
4606
  buildFns: [],
4506
4607
  buildSrcs: [],
4507
4608
  runtimeParsers: [],
4508
- needsCollator: false,
4509
4609
  namedParsers: /* @__PURE__ */ new Map(),
4510
4610
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4511
4611
  triviaFnNames: /* @__PURE__ */ new Map(),
@@ -4636,12 +4736,12 @@ function compileLinkable(ruleMapArg, ns) {
4636
4736
  firstSets,
4637
4737
  deps: ruleDependencies(ruleMap),
4638
4738
  needsEmptyTl: !!ctx.needsEmptyTl,
4639
- needsCollator: ctx.needsCollator,
4739
+ needsHostReads: !!ctx.needsHostReads,
4640
4740
  mfFns: mfSrcs ? [] : ctx.mapFns,
4641
4741
  buildFns: buildSrcs ? [] : ctx.buildFns
4642
4742
  };
4643
4743
  }
4644
- function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources) {
4744
+ function buildInlineExpression(ctx, r, mapFnSources, buildSources) {
4645
4745
  const bodyLines = [
4646
4746
  ` let pos = _pos`,
4647
4747
  ...r.stmts,
@@ -4655,14 +4755,15 @@ function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources)
4655
4755
  const mfDecl = mapFnSources?.length ? ` const _mf = [${mapFnSources.join(", ")}]` : "";
4656
4756
  const buildDecl = buildSources?.length ? ` const _build = [${buildSources.join(", ")}]` : "";
4657
4757
  const emptyTlDecl = ctx.needsEmptyTl ? ` const _EMPTY_TL = Object.freeze([])` : "";
4658
- const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || !!collatorDecl || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl;
4758
+ const hostReadsDecl = ctx.needsHostReads ? ` ${HOST_READS_DECL}` : "";
4759
+ const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl;
4659
4760
  if (!needsWrapper) return innerFn;
4660
4761
  const namedPrelude = ctx.namedFnDecls.length > 0 ? namedFnPrelude() : [];
4661
4762
  const hoistedDecls = [
4662
4763
  emptyTlDecl,
4764
+ hostReadsDecl,
4663
4765
  ...ctx.regexDecls.map((d) => ` ${d}`),
4664
4766
  ...ctx.expectedDecls.map((d) => ` ${d}`),
4665
- collatorDecl ? ` ${collatorDecl.trim()}` : "",
4666
4767
  mfDecl,
4667
4768
  buildDecl,
4668
4769
  ...namedPrelude.map((l) => ` ${l}`),
@@ -4721,13 +4822,13 @@ function fusedBody(pieces) {
4721
4822
  }
4722
4823
  const contributing = new Set(winner.values());
4723
4824
  const needsEmptyTl = [...contributing].some((p) => p.needsEmptyTl);
4724
- const needsCollator = [...contributing].some((p) => p.needsCollator);
4825
+ const needsHostReads = [...contributing].some((p) => p.needsHostReads);
4725
4826
  const lines = [
4726
4827
  // Shared sentinel protocol (must match NAMED_FN_FAIL / NAMED_FN_END in codegen).
4727
4828
  "const _pfFail = {}",
4728
4829
  "let _pfEnd",
4729
4830
  ...needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [],
4730
- ...needsCollator ? ["const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })"] : [],
4831
+ ...needsHostReads ? [HOST_READS_DECL] : [],
4731
4832
  // Each contributing artifact's namespaced private prelude (regexes, _pf, …).
4732
4833
  ...[...contributing].flatMap((p) => p.prelude),
4733
4834
  // The winning `_r_<Name>` function for each rule (one per name → no redeclare).