parseman 0.14.1 → 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.cjs CHANGED
@@ -124,6 +124,63 @@ function any() {
124
124
  function empty() {
125
125
  return { kind: "empty" };
126
126
  }
127
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
128
+ if (seen.has(p)) return true;
129
+ seen.add(p);
130
+ const me = (c) => matchesEmpty(c, seen);
131
+ const d = p._def;
132
+ switch (d.tag) {
133
+ case "literal":
134
+ return d.value.length === 0;
135
+ case "keywords":
136
+ return false;
137
+ case "regex":
138
+ try {
139
+ const m = new RegExp(d.source).exec("");
140
+ return m != null && m[0] === "";
141
+ } catch {
142
+ return true;
143
+ }
144
+ case "many":
145
+ case "optional":
146
+ case "not":
147
+ return true;
148
+ // zero repetitions / absent / lookahead
149
+ case "oneOrMore":
150
+ return me(d.parser);
151
+ case "sequence":
152
+ return d.parsers.every(me);
153
+ case "choice":
154
+ return d.parsers.some(me);
155
+ case "transform":
156
+ case "label":
157
+ case "trivia":
158
+ case "expect":
159
+ case "withCtx":
160
+ case "node":
161
+ case "grammar":
162
+ case "recover":
163
+ return me(d.parser);
164
+ case "skip":
165
+ return me(d.main);
166
+ case "lazy":
167
+ try {
168
+ return me(d.thunk());
169
+ } catch {
170
+ return true;
171
+ }
172
+ default:
173
+ return true;
174
+ }
175
+ }
176
+ function sequenceFirstSet(parsers) {
177
+ let fs = empty();
178
+ for (const p of parsers) {
179
+ fs = union(fs, p._meta.firstSet);
180
+ if (!matchesEmpty(p)) return fs;
181
+ }
182
+ return fs;
183
+ }
127
184
  function mergeRanges(ranges) {
128
185
  if (ranges.length === 0) return [];
129
186
  const sorted = [...ranges].sort((a, b) => a.lo - b.lo);
@@ -301,12 +358,16 @@ function pushTriviaLogEntry(ctx, start, end, kindIndex) {
301
358
  }
302
359
 
303
360
  // src/combinators/literal.ts
304
- var _collatorCache = null;
305
- function collator() {
306
- if (_collatorCache === null) {
307
- _collatorCache = new Intl.Collator(void 0, { sensitivity: "accent" });
361
+ function asciiFoldEq(a, b) {
362
+ if (a.length !== b.length) return false;
363
+ for (let i = 0; i < a.length; i++) {
364
+ let ca = a.charCodeAt(i);
365
+ let cb = b.charCodeAt(i);
366
+ if (ca >= 65 && ca <= 90) ca += 32;
367
+ if (cb >= 65 && cb <= 90) cb += 32;
368
+ if (ca !== cb) return false;
308
369
  }
309
- return _collatorCache;
370
+ return true;
310
371
  }
311
372
  function literal(value, opts = {}) {
312
373
  const caseInsensitive = opts.caseInsensitive ?? false;
@@ -321,8 +382,9 @@ function literal(value, opts = {}) {
321
382
  const lower = value.toLowerCase();
322
383
  const firstUpper = upper.codePointAt(0);
323
384
  const firstLower = lower.codePointAt(0);
324
- meta.firstSet = firstLower !== void 0 && firstUpper !== void 0 ? { kind: "ranges", ranges: [
325
- { lo: Math.min(firstLower, firstUpper), hi: Math.max(firstLower, firstUpper) }
385
+ meta.firstSet = firstLower !== void 0 && firstUpper !== void 0 ? firstLower === firstUpper ? { kind: "ranges", ranges: [{ lo: firstLower, hi: firstLower }] } : { kind: "ranges", ranges: [
386
+ { lo: firstLower, hi: firstLower },
387
+ { lo: firstUpper, hi: firstUpper }
326
388
  ] } : firstSet;
327
389
  }
328
390
  return {
@@ -335,7 +397,7 @@ function literal(value, opts = {}) {
335
397
  return failAt(ctx, [JSON.stringify(value)], pos);
336
398
  }
337
399
  const slice = input.slice(pos, end);
338
- const matched = caseInsensitive ? collator().compare(slice, value) === 0 : slice === value;
400
+ const matched = caseInsensitive ? asciiFoldEq(slice, value) : slice === value;
339
401
  if (matched) {
340
402
  const span = { start: pos, end };
341
403
  const leaf = { _tag: "leaf", value: slice, span };
@@ -945,16 +1007,19 @@ function scanTrivia(input, cur, ctx) {
945
1007
  // src/combinators/sequence.ts
946
1008
  function sequence(...parsers) {
947
1009
  const meta = {
948
- firstSet: parsers[0]?._meta.firstSet ?? empty(),
1010
+ // Union through the nullable prefix — a leading optional()/many() lets a later
1011
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
1012
+ firstSet: sequenceFirstSet(parsers),
949
1013
  canMatchNewline: parsers.some((p) => p._meta.canMatchNewline),
950
1014
  isTrivia: false
951
1015
  };
1016
+ const def = { tag: "sequence", parsers };
952
1017
  return {
953
1018
  _tag: "sequence",
954
1019
  _meta: meta,
955
- _def: { tag: "sequence", parsers },
1020
+ _def: def,
956
1021
  parse(input, pos, ctx) {
957
- const values = [];
1022
+ const values = def.valueUnused ? void 0 : [];
958
1023
  let cur = pos;
959
1024
  for (let i = 0; i < parsers.length; i++) {
960
1025
  if (ctx.trivia && i > 0) {
@@ -974,17 +1039,18 @@ function sequence(...parsers) {
974
1039
  } else {
975
1040
  rollbackTrivia(ctx, mark);
976
1041
  }
977
- values.push(result2.value);
1042
+ if (values !== void 0) values.push(result2.value);
978
1043
  continue;
979
1044
  }
980
1045
  const result = parsers[i].parse(input, cur, ctx);
981
1046
  if (!result.ok) return result;
982
- values.push(result.value);
1047
+ if (values !== void 0) values.push(result.value);
983
1048
  cur = result.span.end;
984
1049
  }
985
1050
  return {
986
1051
  ok: true,
987
- value: values,
1052
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
1053
+ value: values ?? void 0,
988
1054
  span: { start: pos, end: cur }
989
1055
  };
990
1056
  }
@@ -1025,20 +1091,21 @@ function many(combinator) {
1025
1091
  canMatchNewline: combinator._meta.canMatchNewline,
1026
1092
  isTrivia: false
1027
1093
  };
1094
+ const def = { tag: "many", parser: combinator, min: 0 };
1028
1095
  return {
1029
1096
  _tag: "many",
1030
1097
  _meta: meta,
1031
- _def: { tag: "many", parser: combinator, min: 0 },
1098
+ _def: def,
1032
1099
  parse(input, pos, ctx) {
1033
- const values = [];
1100
+ const values = def.valueUnused ? void 0 : [];
1034
1101
  let cur = pos;
1035
1102
  while (cur < input.length) {
1036
1103
  const item = repItem(combinator, input, cur, ctx);
1037
1104
  if (item === "stop" || "fail" in item) break;
1038
- values.push(item.value);
1105
+ if (values !== void 0) values.push(item.value);
1039
1106
  cur = item.end;
1040
1107
  }
1041
- return { ok: true, value: values, span: { start: pos, end: cur } };
1108
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1042
1109
  }
1043
1110
  };
1044
1111
  }
@@ -1048,22 +1115,23 @@ function oneOrMore(combinator) {
1048
1115
  canMatchNewline: combinator._meta.canMatchNewline,
1049
1116
  isTrivia: false
1050
1117
  };
1118
+ const def = { tag: "oneOrMore", parser: combinator, min: 1 };
1051
1119
  return {
1052
1120
  _tag: "oneOrMore",
1053
1121
  _meta: meta,
1054
- _def: { tag: "oneOrMore", parser: combinator, min: 1 },
1122
+ _def: def,
1055
1123
  parse(input, pos, ctx) {
1056
1124
  const first = combinator.parse(input, pos, ctx);
1057
1125
  if (!first.ok) return first;
1058
- const values = [first.value];
1126
+ const values = def.valueUnused ? void 0 : [first.value];
1059
1127
  let cur = first.span.end;
1060
1128
  while (cur < input.length) {
1061
1129
  const item = repItem(combinator, input, cur, ctx);
1062
1130
  if (item === "stop" || "fail" in item) break;
1063
- values.push(item.value);
1131
+ if (values !== void 0) values.push(item.value);
1064
1132
  cur = item.end;
1065
1133
  }
1066
- return { ok: true, value: values, span: { start: pos, end: cur } };
1134
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1067
1135
  }
1068
1136
  };
1069
1137
  }
@@ -1171,6 +1239,90 @@ function ref() {
1171
1239
  return slot;
1172
1240
  }
1173
1241
 
1242
+ // src/compiler/value-usage.ts
1243
+ function markUnusedValues(root) {
1244
+ const seenConsumed = /* @__PURE__ */ new WeakSet();
1245
+ const seenUnconsumed = /* @__PURE__ */ new WeakSet();
1246
+ const mark = (def, consumed) => {
1247
+ if (consumed) def.valueUnused = false;
1248
+ else if (def.valueUnused !== false) def.valueUnused = true;
1249
+ };
1250
+ const visit = (c, consumed) => {
1251
+ const seen = consumed ? seenConsumed : seenUnconsumed;
1252
+ if (seen.has(c)) return;
1253
+ seen.add(c);
1254
+ const def = c._def;
1255
+ switch (def.tag) {
1256
+ case "many":
1257
+ case "oneOrMore":
1258
+ mark(def, consumed);
1259
+ visit(def.parser, consumed);
1260
+ return;
1261
+ case "optional":
1262
+ visit(def.parser, consumed);
1263
+ return;
1264
+ case "sequence":
1265
+ mark(def, consumed);
1266
+ for (const p of def.parsers) visit(p, consumed);
1267
+ return;
1268
+ case "choice":
1269
+ for (const p of def.parsers) visit(p, consumed);
1270
+ return;
1271
+ case "node":
1272
+ visit(def.parser, false);
1273
+ return;
1274
+ case "transform":
1275
+ visit(def.parser, true);
1276
+ return;
1277
+ case "skip":
1278
+ visit(def.main, consumed);
1279
+ visit(def.skipped, false);
1280
+ return;
1281
+ case "sepBy":
1282
+ visit(def.parser, true);
1283
+ visit(def.separator, false);
1284
+ return;
1285
+ case "not":
1286
+ visit(def.parser, false);
1287
+ return;
1288
+ case "grammar":
1289
+ visit(def.parser, consumed);
1290
+ if (def.triviaParser) visit(def.triviaParser, false);
1291
+ return;
1292
+ case "recover":
1293
+ visit(def.parser, consumed);
1294
+ visit(def.sentinel, false);
1295
+ return;
1296
+ case "label":
1297
+ case "trivia":
1298
+ case "withCtx":
1299
+ case "expect":
1300
+ visit(def.parser, consumed);
1301
+ return;
1302
+ case "lazy":
1303
+ return;
1304
+ default:
1305
+ return;
1306
+ }
1307
+ };
1308
+ const resolveRoot = (c) => {
1309
+ const guard2 = /* @__PURE__ */ new Set();
1310
+ let cur = c;
1311
+ while (!guard2.has(cur)) {
1312
+ const d = cur._def;
1313
+ if (d.tag !== "lazy") return cur;
1314
+ guard2.add(cur);
1315
+ try {
1316
+ cur = d.thunk();
1317
+ } catch {
1318
+ return c;
1319
+ }
1320
+ }
1321
+ return cur;
1322
+ };
1323
+ visit(resolveRoot(root), true);
1324
+ }
1325
+
1174
1326
  // src/combinators/parser.ts
1175
1327
  function rules(factory) {
1176
1328
  const cache = {};
@@ -1200,6 +1352,9 @@ function rules(factory) {
1200
1352
  cache[key] = parser2;
1201
1353
  }
1202
1354
  }
1355
+ for (const key of Object.keys(definitions)) {
1356
+ markUnusedValues(cache[key]);
1357
+ }
1203
1358
  return cache;
1204
1359
  }
1205
1360
 
@@ -1416,7 +1571,12 @@ function parser(opts, root) {
1416
1571
  function noTrivia(root) {
1417
1572
  return parser({ trivia: null }, root);
1418
1573
  }
1574
+ var _analyzed = /* @__PURE__ */ new WeakSet();
1419
1575
  function parse(combinator, input, opts = {}) {
1576
+ if (!_analyzed.has(combinator)) {
1577
+ _analyzed.add(combinator);
1578
+ markUnusedValues(combinator);
1579
+ }
1420
1580
  const trackLines = opts.trackLines ?? false;
1421
1581
  const _errors = opts.recover ? [] : void 0;
1422
1582
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
@@ -1571,8 +1731,8 @@ function classToRanges(cls) {
1571
1731
  return parseClassRanges(body);
1572
1732
  }
1573
1733
  var foldEq = (cVar, cp) => {
1574
- if (cp >= 65 && cp <= 90) return `(${cVar} === ${cp} || ${cVar} === ${cp + 32})`;
1575
- if (cp >= 97 && cp <= 122) return `(${cVar} === ${cp} || ${cVar} === ${cp - 32})`;
1734
+ if (cp >= 65 && cp <= 90) return `(${cVar} | 32) === ${cp + 32}`;
1735
+ if (cp >= 97 && cp <= 122) return `(${cVar} | 32) === ${cp}`;
1576
1736
  return `${cVar} === ${cp}`;
1577
1737
  };
1578
1738
  function literalCodePoints(frag) {
@@ -1823,6 +1983,12 @@ function firstSetsDisjoint(a, b) {
1823
1983
  return classDisjoint(a, b);
1824
1984
  }
1825
1985
  function fixedClassSeq(shape) {
1986
+ if (shape.kind === "litFold") {
1987
+ return shape.open.map((cp) => {
1988
+ const other = cp >= 65 && cp <= 90 ? cp + 32 : cp >= 97 && cp <= 122 ? cp - 32 : cp;
1989
+ 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 };
1990
+ });
1991
+ }
1826
1992
  if (shape.kind !== "seq") return null;
1827
1993
  const out = [];
1828
1994
  for (const p of shape.parts) {
@@ -2244,12 +2410,34 @@ function parseScanShapeCore(source) {
2244
2410
  }
2245
2411
  function scanShapeFromRegex(source, flags) {
2246
2412
  if (/[msu]/.test(flags)) return null;
2247
- if (/i/.test(flags)) {
2248
- const open = literalCodePoints(source);
2249
- return open ? { kind: "litFold", open } : null;
2250
- }
2413
+ if (/i/.test(flags)) return caseFoldShape(source);
2251
2414
  return parseScanShape(source);
2252
2415
  }
2416
+ function caseFoldShape(source) {
2417
+ const la = stripTrailingLookahead(source);
2418
+ if (la) {
2419
+ const inner = caseFoldLiteralOrAlt(la.base);
2420
+ if (!inner) return null;
2421
+ const operand = { ranges: la.ranges, negated: la.classNegated };
2422
+ if (!lookaheadUnambiguous(inner, operand, la.negative)) return null;
2423
+ return { kind: "lookahead", inner, ranges: la.ranges, classNegated: la.classNegated, negative: la.negative };
2424
+ }
2425
+ return caseFoldLiteralOrAlt(source);
2426
+ }
2427
+ function caseFoldLiteralOrAlt(source) {
2428
+ const asciiOnly = (cps) => cps.every((cp) => cp <= 127);
2429
+ const open = literalCodePoints(source);
2430
+ if (open) return asciiOnly(open) ? { kind: "litFold", open } : null;
2431
+ const arms = splitTopLevelAlternation(source);
2432
+ if (!arms || arms.length < 2) return null;
2433
+ const litArms = [];
2434
+ for (const arm of arms) {
2435
+ const cp = literalCodePoints(arm);
2436
+ if (!cp || !asciiOnly(cp)) return null;
2437
+ litArms.push({ kind: "litFold", open: cp });
2438
+ }
2439
+ return { kind: "alt", arms: litArms, disjoint: false, firsts: litArms.map(() => null) };
2440
+ }
2253
2441
  var classCond = (cVar, ranges) => ranges.map(([lo, hi]) => lo === hi ? `${cVar} === ${lo}` : `(${cVar} >= ${lo} && ${cVar} <= ${hi})`).join(" || ");
2254
2442
  var litCond = (base, cps, firstVar) => cps.map(
2255
2443
  (cp, k) => k === 0 && firstVar ? `${firstVar} === ${cp}` : `input.charCodeAt(${base}${k ? ` + ${k}` : ""}) === ${cp}`
@@ -2261,6 +2449,25 @@ function stringHardStop(shape) {
2261
2449
  );
2262
2450
  }
2263
2451
  var codeAt = (start, firstChar) => firstChar ?? `input.charCodeAt(${start})`;
2452
+ var ALT_SWITCH_RANGE_LIMIT = 4;
2453
+ var ALT_SWITCH_MAX_CASES = 48;
2454
+ var ALT_SWITCH_MIN_CASES = 3;
2455
+ function planAltSwitch(firsts) {
2456
+ const cases = [];
2457
+ let total = 0;
2458
+ for (const fs of firsts) {
2459
+ if (!fs || fs.negated) return { kind: "if" };
2460
+ const pts = [];
2461
+ for (const [lo, hi] of fs.ranges) {
2462
+ if (hi - lo + 1 > ALT_SWITCH_RANGE_LIMIT) return { kind: "if" };
2463
+ for (let cp = lo; cp <= hi; cp++) pts.push(cp);
2464
+ }
2465
+ total += pts.length;
2466
+ if (total > ALT_SWITCH_MAX_CASES) return { kind: "if" };
2467
+ cases.push(pts);
2468
+ }
2469
+ return total >= ALT_SWITCH_MIN_CASES ? { kind: "switch", cases } : { kind: "if" };
2470
+ }
2264
2471
  function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2265
2472
  if (shape.kind === "lookahead") {
2266
2473
  const inner = emitShapeMatch(shape.inner, start, mint, ind2, firstChar);
@@ -2285,18 +2492,57 @@ function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2285
2492
  const okV = mint("_ok");
2286
2493
  const endV2 = mint("_end");
2287
2494
  const lines = [`${ind2}let ${okV} = false`, `${ind2}let ${endV2} = ${start}`];
2495
+ if (shape.arms.length >= 2 && shape.arms.every((a) => a.kind === "litFold" && a.open.length >= 1)) {
2496
+ const groups = /* @__PURE__ */ new Map();
2497
+ for (const a of shape.arms) {
2498
+ const open = a.open;
2499
+ const f = open[0];
2500
+ const lower = f >= 65 && f <= 90 ? f + 32 : f;
2501
+ (groups.get(lower) ?? groups.set(lower, []).get(lower)).push(open);
2502
+ }
2503
+ lines.push(`${ind2}if (${start} < input.length) switch (${codeAt(start, firstChar)}) {`);
2504
+ for (const [lower, arms] of groups) {
2505
+ const labels = lower >= 97 && lower <= 122 ? `case ${lower}: case ${lower - 32}:` : `case ${lower}:`;
2506
+ lines.push(`${ind2} ${labels} {`);
2507
+ for (const open of arms) {
2508
+ const tail = open.slice(1).map((cp, i) => foldEq(`input.charCodeAt(${start} + ${i + 1})`, cp));
2509
+ const cond = tail.length ? ` && ${tail.join(" && ")}` : "";
2510
+ lines.push(`${ind2} if (${start} + ${open.length} <= input.length${cond}) { ${okV} = true; ${endV2} = ${start} + ${open.length}; break }`);
2511
+ }
2512
+ lines.push(`${ind2} break`);
2513
+ lines.push(`${ind2} }`);
2514
+ }
2515
+ lines.push(`${ind2}}`);
2516
+ return { setup: lines, ok: okV, end: endV2 };
2517
+ }
2288
2518
  if (shape.disjoint) {
2289
- const c0 = codeAt(start, firstChar);
2290
- shape.arms.forEach((arm, k) => {
2291
- const fs = shape.firsts[k];
2292
- const cond = fs.negated ? `!(${classCond(c0, fs.ranges)})` : `(${classCond(c0, fs.ranges)})`;
2293
- const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2294
- lines.push(`${ind2}${k === 0 ? "if" : "else if"} (${start} < input.length && ${cond}) {`);
2295
- lines.push(...m.setup);
2296
- lines.push(`${ind2} ${okV} = ${m.ok}`);
2297
- lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2519
+ const plan = planAltSwitch(shape.firsts);
2520
+ if (plan.kind === "switch") {
2521
+ lines.push(`${ind2}if (${start} < input.length) switch (${codeAt(start, firstChar)}) {`);
2522
+ shape.arms.forEach((arm, k) => {
2523
+ const labels = plan.cases[k].map((cp) => `case ${cp}:`).join(" ");
2524
+ const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2525
+ lines.push(`${ind2} ${labels} {`);
2526
+ lines.push(...m.setup);
2527
+ lines.push(`${ind2} ${okV} = ${m.ok}`);
2528
+ lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2529
+ lines.push(`${ind2} break`);
2530
+ lines.push(`${ind2} }`);
2531
+ });
2298
2532
  lines.push(`${ind2}}`);
2299
- });
2533
+ } else {
2534
+ const c0 = codeAt(start, firstChar);
2535
+ shape.arms.forEach((arm, k) => {
2536
+ const fs = shape.firsts[k];
2537
+ const cond = fs.negated ? `!(${classCond(c0, fs.ranges)})` : `(${classCond(c0, fs.ranges)})`;
2538
+ const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2539
+ lines.push(`${ind2}${k === 0 ? "if" : "else if"} (${start} < input.length && ${cond}) {`);
2540
+ lines.push(...m.setup);
2541
+ lines.push(`${ind2} ${okV} = ${m.ok}`);
2542
+ lines.push(`${ind2} ${endV2} = ${m.ok} ? ${m.end} : ${start}`);
2543
+ lines.push(`${ind2}}`);
2544
+ });
2545
+ }
2300
2546
  } else {
2301
2547
  const lbl = mint("_alt");
2302
2548
  lines.push(`${ind2}${lbl}: {`);
@@ -2784,6 +3030,8 @@ function tryInlineDestructureTransform(src, valueVars) {
2784
3030
  }
2785
3031
 
2786
3032
  // src/compiler/codegen.ts
3033
+ 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 }";
3034
+ var _loweringSink = null;
2787
3035
  function v(ctx, prefix = "_v") {
2788
3036
  return `${prefix}${ctx.vars++}`;
2789
3037
  }
@@ -3182,13 +3430,14 @@ function emitLit(def, ctx, pos) {
3182
3430
  const vv = v(ctx);
3183
3431
  const expectedStr = JSON.stringify(JSON.stringify(value));
3184
3432
  const stmts = [];
3185
- if (caseInsensitive) {
3186
- ctx.needsCollator = true;
3433
+ if (caseInsensitive && len > 0) {
3434
+ const match = Array.from(
3435
+ { length: len },
3436
+ (_, i) => `(${foldEq(`input.charCodeAt(${pos}${i > 0 ? ` + ${i}` : ""})`, value.charCodeAt(i))})`
3437
+ ).join(" && ");
3187
3438
  stmts.push(
3188
- ...emitIfFail(ctx, `${pos} + ${len} > input.length`, failBody(ctx, expectedStr, pos)),
3189
- `${ind(ctx)}const ${vv}_s = input.slice(${pos}, ${pos} + ${len})`,
3190
- ...emitIfFail(ctx, `_collator.compare(${vv}_s, ${JSON.stringify(value)}) !== 0`, failBody(ctx, expectedStr, pos)),
3191
- `${ind(ctx)}const ${vv} = ${vv}_s`
3439
+ ...emitIfFail(ctx, `${pos} + ${len} > input.length || !(${match})`, failBody(ctx, expectedStr, pos)),
3440
+ `${ind(ctx)}const ${vv} = input.slice(${pos}, ${pos} + ${len})`
3192
3441
  );
3193
3442
  } else if (len === 0) {
3194
3443
  stmts.push(`${ind(ctx)}const ${vv} = ''`);
@@ -3309,6 +3558,7 @@ function emitRegex(def, ctx, pos) {
3309
3558
  return { stmts: stmts2, valueVar: vv2, endVar: scanned.endVar };
3310
3559
  }
3311
3560
  }
3561
+ _loweringSink?.add(`/${def.source}/${def.flags}`);
3312
3562
  const flags = "y" + def.flags.replace(/[gy]/g, "");
3313
3563
  const key = `${def.source}/${flags}`;
3314
3564
  let rName = ctx.regexMap.get(key);
@@ -3373,6 +3623,7 @@ function emitSeqValues(def, ctx, pos) {
3373
3623
  }
3374
3624
  function emitSeq(def, ctx, pos) {
3375
3625
  const { stmts, endVar, valueVars } = emitSeqValues(def, ctx, pos);
3626
+ if (def.valueUnused) return { stmts, valueVar: "undefined", endVar };
3376
3627
  const arrV = v(ctx, "_arr");
3377
3628
  stmts.push(`${ind(ctx)}const ${arrV} = [${valueVars.join(", ")}]`);
3378
3629
  return { stmts, valueVar: arrV, endVar };
@@ -3579,7 +3830,8 @@ function emitFirstMatch(def, ctx, pos) {
3579
3830
  const autoNot = def.autoNot[i];
3580
3831
  const atStart = failsAtStart(p);
3581
3832
  const staticFx = armStaticExpected(ctx, p);
3582
- const fsGuard = needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3833
+ const deferRuleName = ctx.deferFirstSetRefs ? p._ruleName : void 0;
3834
+ const fsGuard = deferRuleName !== void 0 ? `/*@FS:${deferRuleName}:${codeV}@*/true` : needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3583
3835
  let gateCond = null;
3584
3836
  if (gate) {
3585
3837
  const gateIdx = ctx.mapFns.length;
@@ -3670,19 +3922,18 @@ function emitLiteralCondition(litVal, pos) {
3670
3922
  return checks.join(" && ");
3671
3923
  }
3672
3924
  function emitMany(def, ctx, pos) {
3925
+ const wantValue = !def.valueUnused;
3673
3926
  const arrV = v(ctx, "_arr");
3674
3927
  const curV = v(ctx, "_cur");
3675
3928
  const stmts = [
3676
- `${ind(ctx)}const ${arrV} = []`,
3929
+ ...wantValue ? [`${ind(ctx)}const ${arrV} = []`] : [],
3677
3930
  `${ind(ctx)}let ${curV} = ${pos}`
3678
3931
  ];
3679
3932
  if (def.min === 1) {
3680
3933
  const firstR = emit(def.parser, ctx, curV);
3681
3934
  stmts.push(...firstR.stmts);
3682
- stmts.push(
3683
- `${ind(ctx)}${arrV}.push(${firstR.valueVar})`,
3684
- `${ind(ctx)}${curV} = ${firstR.endVar}`
3685
- );
3935
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
3936
+ stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
3686
3937
  }
3687
3938
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
3688
3939
  ctx.indent++;
@@ -3713,13 +3964,13 @@ function emitMany(def, ctx, pos) {
3713
3964
  const { stmts: iterStmts, okVar: iterOk, valVar: iterVal, endVar: iterEnd } = emitFallible(def.parser, ctx, itemPos, true);
3714
3965
  stmts.push(
3715
3966
  ...iterStmts,
3716
- `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`,
3717
- `${ind(ctx)}${arrV}.push(${iterVal})`,
3718
- `${ind(ctx)}${curV} = ${iterEnd}`
3967
+ `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`
3719
3968
  );
3969
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
3970
+ stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
3720
3971
  ctx.indent--;
3721
3972
  stmts.push(`${ind(ctx)}}`);
3722
- return { stmts, valueVar: arrV, endVar: curV };
3973
+ return { stmts, valueVar: wantValue ? arrV : "undefined", endVar: curV };
3723
3974
  }
3724
3975
  function emitOptional(def, ctx, pos) {
3725
3976
  const valV = v(ctx, "_opt");
@@ -3888,15 +4139,19 @@ function emitNode(def, ctx, pos) {
3888
4139
  ctx.buildSrcs.push(def.buildSrc ?? null);
3889
4140
  }
3890
4141
  const i = ind(ctx);
3891
- const capturesTrivia = mkType !== null || structural || buildReadsTrivia(def);
3892
- const clonesState = structural || buildReadsState(def);
4142
+ const capturesTrivia = mkType !== null || !structural && buildReadsTrivia(def);
4143
+ const clonesState = !structural && buildReadsState(def);
3893
4144
  const chV = v(ctx, "_ch");
3894
4145
  const rawV = v(ctx, "_raw");
3895
- const tlV = capturesTrivia ? v(ctx, "_tl") : "_EMPTY_TL";
4146
+ const capTLv = structural ? v(ctx, "_ctl") : null;
4147
+ const capSTv = structural ? v(ctx, "_cst") : null;
4148
+ const tlV = capturesTrivia || structural ? v(ctx, "_tl") : "_EMPTY_TL";
3896
4149
  if (!capturesTrivia) ctx.needsEmptyTl = true;
3897
4150
  const sc = v(ctx, "_sc"), sl = v(ctx, "_sl"), sr = v(ctx, "_sr"), st = v(ctx, "_st"), stl = v(ctx, "_stl");
3898
- const allocStmt = capturesTrivia ? `${i}const ${chV} = [], ${rawV} = [], ${tlV} = []` : `${i}const ${chV} = [], ${rawV} = []`;
3899
- const innerTl = capturesTrivia ? tlV : "undefined";
4151
+ if (structural) ctx.needsHostReads = true;
4152
+ const allocStmt = structural ? `${i}const ${capTLv} = _ctx._pmCapTL ??= _hostReads(_ctx.build, 4), ${capSTv} = _ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 5))
4153
+ ${i}const ${chV} = [], ${rawV} = [], ${tlV} = ${capTLv} ? [] : _EMPTY_TL` : capturesTrivia ? `${i}const ${chV} = [], ${rawV} = [], ${tlV} = []` : `${i}const ${chV} = [], ${rawV} = []`;
4154
+ const innerTl = structural ? `${capTLv} ? ${tlV} : undefined` : capturesTrivia ? tlV : "undefined";
3900
4155
  const stmts = [
3901
4156
  allocStmt,
3902
4157
  `${i}const ${sc} = _ctx._cstChildren, ${sl} = _ctx._cstLeaves, ${sr} = _ctx._cstRawChildren, ${st} = _ctx.captureTrivia, ${stl} = _ctx._cstTriviaLog`,
@@ -3907,7 +4162,10 @@ function emitNode(def, ctx, pos) {
3907
4162
  stmts.push(`${i}_ctx._cstChildren = ${sc}; _ctx._cstLeaves = ${sl}; _ctx._cstRawChildren = ${sr}; _ctx.captureTrivia = ${st}; _ctx._cstTriviaLog = ${stl}`);
3908
4163
  stmts.push(...emitIfFail(ctx, `!${okVar}`, propagateFailBody(ctx)));
3909
4164
  let stV = "undefined";
3910
- if (clonesState) {
4165
+ if (structural) {
4166
+ stV = v(ctx, "_nst");
4167
+ stmts.push(`${i}const ${stV} = ${capSTv} && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
4168
+ } else if (clonesState) {
3911
4169
  stV = v(ctx, "_nst");
3912
4170
  stmts.push(`${i}const ${stV} = _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
3913
4171
  }
@@ -4329,6 +4587,7 @@ function ruleDependencies(ruleMap) {
4329
4587
  return deps;
4330
4588
  }
4331
4589
  function compile(combinator, mapFnSources) {
4590
+ markUnusedValues(combinator);
4332
4591
  const ctx = {
4333
4592
  vars: 0,
4334
4593
  indent: 1,
@@ -4342,7 +4601,6 @@ function compile(combinator, mapFnSources) {
4342
4601
  buildFns: [],
4343
4602
  buildSrcs: [],
4344
4603
  runtimeParsers: [],
4345
- needsCollator: false,
4346
4604
  namedParsers: /* @__PURE__ */ new Map(),
4347
4605
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4348
4606
  triviaFnNames: /* @__PURE__ */ new Map(),
@@ -4351,18 +4609,18 @@ function compile(combinator, mapFnSources) {
4351
4609
  lazyUsage: analyzeLazyUsage(combinator)
4352
4610
  };
4353
4611
  const r = emit(combinator, ctx, "_pos");
4354
- const collatorDecl = ctx.needsCollator ? `const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })
4355
- ` : "";
4356
4612
  const namedPrelude = ctx.namedFnDecls.length > 0 ? [...namedFnPrelude(), ""] : [];
4357
4613
  const emptyTlDecls = ctx.needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [];
4614
+ const hostReadsDecls = ctx.needsHostReads ? [HOST_READS_DECL] : [];
4358
4615
  const source = [
4359
4616
  ...emptyTlDecls,
4617
+ ...hostReadsDecls,
4360
4618
  ...ctx.regexDecls,
4361
4619
  ...ctx.expectedDecls,
4362
4620
  "",
4363
4621
  ...namedPrelude,
4364
4622
  ctx.namedFnDecls.join("\n\n"),
4365
- `${collatorDecl}function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4623
+ `function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4366
4624
  ` let pos = _pos`,
4367
4625
  ...r.stmts,
4368
4626
  ` return { ok: true, value: ${r.valueVar}, span: { start: _pos, end: ${r.endVar} } }`,
@@ -4370,9 +4628,9 @@ function compile(combinator, mapFnSources) {
4370
4628
  ].join("\n");
4371
4629
  const fn = new Function("input", "_pos", "_rp", "_mf", "_build", "_ctx", [
4372
4630
  ...emptyTlDecls,
4631
+ ...hostReadsDecls,
4373
4632
  ...ctx.regexDecls,
4374
4633
  ...ctx.expectedDecls,
4375
- collatorDecl,
4376
4634
  ...namedPrelude,
4377
4635
  ...ctx.namedFnDecls.flatMap((decl, i) => i > 0 ? ["", decl] : [decl]),
4378
4636
  `let pos = _pos`,
@@ -4389,7 +4647,7 @@ function compile(combinator, mapFnSources) {
4389
4647
  const buildSources = ctx.buildFns.length === 0 ? void 0 : ctx.buildSrcs;
4390
4648
  const mfCovered = ctx.mapFns.length === 0 || effectiveSources !== void 0 && effectiveSources.length === ctx.mapFns.length;
4391
4649
  const canInline = ctx.runtimeParsers.length === 0 && mfCovered && buildCovered;
4392
- const inlineExpression = canInline ? buildInlineExpression(ctx, r, collatorDecl, effectiveSources, buildSources) : null;
4650
+ const inlineExpression = canInline ? buildInlineExpression(ctx, r, effectiveSources, buildSources) : null;
4393
4651
  return {
4394
4652
  source,
4395
4653
  inlineExpression,
@@ -4411,8 +4669,17 @@ function compile(combinator, mapFnSources) {
4411
4669
  }
4412
4670
  };
4413
4671
  }
4672
+ var RULE_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4673
+ function assertRuleName(name) {
4674
+ if (!RULE_NAME_RE.test(name)) {
4675
+ throw new Error(
4676
+ `parseman: grammar rule name ${JSON.stringify(name)} is not a valid JS identifier. Rule names compile to _r_<Name> functions and dispatch guards \u2014 use letters, digits, _ or $ (and don't start with a digit).`
4677
+ );
4678
+ }
4679
+ }
4414
4680
  function compileLinkable(ruleMapArg, ns) {
4415
4681
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
4682
+ for (const [, rule] of ruleMapArg) markUnusedValues(rule);
4416
4683
  const ruleMap = ruleMapArg.filter(([, val]) => {
4417
4684
  const d = val._def;
4418
4685
  if (d.tag !== "lazy") return true;
@@ -4436,20 +4703,21 @@ function compileLinkable(ruleMapArg, ns) {
4436
4703
  buildFns: [],
4437
4704
  buildSrcs: [],
4438
4705
  runtimeParsers: [],
4439
- needsCollator: false,
4440
4706
  namedParsers: /* @__PURE__ */ new Map(),
4441
4707
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4442
4708
  triviaFnNames: /* @__PURE__ */ new Map(),
4443
4709
  namedFnDecls: [],
4444
4710
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
4445
4711
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
4446
- ns
4712
+ ns,
4713
+ deferFirstSetRefs: true
4447
4714
  };
4448
4715
  const seen = /* @__PURE__ */ new Map();
4449
4716
  const ruleNames = /* @__PURE__ */ new Map();
4450
4717
  const fnNameToKey = /* @__PURE__ */ new Map();
4451
4718
  for (const [key, rule] of ruleMap) {
4452
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4719
+ assertRuleName(key);
4720
+ const base = `_r_${key}`;
4453
4721
  const n = seen.get(base) ?? 0;
4454
4722
  seen.set(base, n + 1);
4455
4723
  const fn = n === 0 ? base : `${base}$${n}`;
@@ -4473,7 +4741,8 @@ function compileLinkable(ruleMapArg, ns) {
4473
4741
  if (resolved === void 0) {
4474
4742
  const name = p._ruleName;
4475
4743
  if (name && !ctx.namedParsers.has(p)) {
4476
- const fn = `_r_${name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4744
+ assertRuleName(name);
4745
+ const fn = `_r_${name}`;
4477
4746
  ruleNames.set(p, fn);
4478
4747
  ctx.namedParsers.set(p, fn);
4479
4748
  }
@@ -4542,20 +4811,34 @@ function compileLinkable(ruleMapArg, ns) {
4542
4811
  `}`
4543
4812
  ].join("\n"));
4544
4813
  }
4814
+ const firstSets = /* @__PURE__ */ new Map();
4815
+ for (const [key, rule] of ruleMap) {
4816
+ let resolved = rule;
4817
+ const d = rule._def;
4818
+ if (d.tag === "lazy") {
4819
+ try {
4820
+ resolved = d.thunk();
4821
+ } catch {
4822
+ resolved = rule;
4823
+ }
4824
+ }
4825
+ firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
4826
+ }
4545
4827
  return {
4546
4828
  ns,
4547
4829
  keys: perEntry.map((e) => e.key),
4548
4830
  prelude,
4549
4831
  ruleFns,
4550
4832
  wrappers,
4833
+ firstSets,
4551
4834
  deps: ruleDependencies(ruleMap),
4552
4835
  needsEmptyTl: !!ctx.needsEmptyTl,
4553
- needsCollator: ctx.needsCollator,
4836
+ needsHostReads: !!ctx.needsHostReads,
4554
4837
  mfFns: mfSrcs ? [] : ctx.mapFns,
4555
4838
  buildFns: buildSrcs ? [] : ctx.buildFns
4556
4839
  };
4557
4840
  }
4558
- function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources) {
4841
+ function buildInlineExpression(ctx, r, mapFnSources, buildSources) {
4559
4842
  const bodyLines = [
4560
4843
  ` let pos = _pos`,
4561
4844
  ...r.stmts,
@@ -4569,14 +4852,15 @@ function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources)
4569
4852
  const mfDecl = mapFnSources?.length ? ` const _mf = [${mapFnSources.join(", ")}]` : "";
4570
4853
  const buildDecl = buildSources?.length ? ` const _build = [${buildSources.join(", ")}]` : "";
4571
4854
  const emptyTlDecl = ctx.needsEmptyTl ? ` const _EMPTY_TL = Object.freeze([])` : "";
4572
- const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || !!collatorDecl || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl;
4855
+ const hostReadsDecl = ctx.needsHostReads ? ` ${HOST_READS_DECL}` : "";
4856
+ const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl;
4573
4857
  if (!needsWrapper) return innerFn;
4574
4858
  const namedPrelude = ctx.namedFnDecls.length > 0 ? namedFnPrelude() : [];
4575
4859
  const hoistedDecls = [
4576
4860
  emptyTlDecl,
4861
+ hostReadsDecl,
4577
4862
  ...ctx.regexDecls.map((d) => ` ${d}`),
4578
4863
  ...ctx.expectedDecls.map((d) => ` ${d}`),
4579
- collatorDecl ? ` ${collatorDecl.trim()}` : "",
4580
4864
  mfDecl,
4581
4865
  buildDecl,
4582
4866
  ...namedPrelude.map((l) => ` ${l}`),
@@ -4635,20 +4919,36 @@ function fusedBody(pieces) {
4635
4919
  }
4636
4920
  const contributing = new Set(winner.values());
4637
4921
  const needsEmptyTl = [...contributing].some((p) => p.needsEmptyTl);
4638
- const needsCollator = [...contributing].some((p) => p.needsCollator);
4922
+ const needsHostReads = [...contributing].some((p) => p.needsHostReads);
4639
4923
  const lines = [
4640
4924
  // Shared sentinel protocol (must match NAMED_FN_FAIL / NAMED_FN_END in codegen).
4641
4925
  "const _pfFail = {}",
4642
4926
  "let _pfEnd",
4643
4927
  ...needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [],
4644
- ...needsCollator ? ["const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })"] : [],
4928
+ ...needsHostReads ? [HOST_READS_DECL] : [],
4645
4929
  // Each contributing artifact's namespaced private prelude (regexes, _pf, …).
4646
4930
  ...[...contributing].flatMap((p) => p.prelude),
4647
4931
  // The winning `_r_<Name>` function for each rule (one per name → no redeclare).
4648
4932
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
4649
4933
  ];
4650
4934
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
4651
- const body = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4935
+ const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4936
+ const finalFS = /* @__PURE__ */ new Map();
4937
+ for (const [k, p] of winner) {
4938
+ const fs = p.firstSets?.get(k);
4939
+ if (fs) finalFS.set(k, fs);
4940
+ }
4941
+ const body = rawBody.replace(
4942
+ // Rule name + code-point var are both JS identifiers (rule names are validated
4943
+ // at compile time — see assertRuleName in codegen), so an identifier class
4944
+ // matches every well-formed placeholder.
4945
+ /\/\*@FS:([A-Za-z0-9_$]+):([A-Za-z0-9_$]+)@\*\/true/g,
4946
+ (_m, name, codevar) => {
4947
+ const fs = finalFS.get(name);
4948
+ if (!fs || fs.kind === "any" || fs.kind === "empty") return "true";
4949
+ return `(${firstSetCond(codevar, fs)})`;
4950
+ }
4951
+ );
4652
4952
  const env = {};
4653
4953
  for (const p of contributing) {
4654
4954
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;