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.js CHANGED
@@ -27,6 +27,63 @@ function any() {
27
27
  function empty() {
28
28
  return { kind: "empty" };
29
29
  }
30
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
31
+ if (seen.has(p)) return true;
32
+ seen.add(p);
33
+ const me = (c) => matchesEmpty(c, seen);
34
+ const d = p._def;
35
+ switch (d.tag) {
36
+ case "literal":
37
+ return d.value.length === 0;
38
+ case "keywords":
39
+ return false;
40
+ case "regex":
41
+ try {
42
+ const m = new RegExp(d.source).exec("");
43
+ return m != null && m[0] === "";
44
+ } catch {
45
+ return true;
46
+ }
47
+ case "many":
48
+ case "optional":
49
+ case "not":
50
+ return true;
51
+ // zero repetitions / absent / lookahead
52
+ case "oneOrMore":
53
+ return me(d.parser);
54
+ case "sequence":
55
+ return d.parsers.every(me);
56
+ case "choice":
57
+ return d.parsers.some(me);
58
+ case "transform":
59
+ case "label":
60
+ case "trivia":
61
+ case "expect":
62
+ case "withCtx":
63
+ case "node":
64
+ case "grammar":
65
+ case "recover":
66
+ return me(d.parser);
67
+ case "skip":
68
+ return me(d.main);
69
+ case "lazy":
70
+ try {
71
+ return me(d.thunk());
72
+ } catch {
73
+ return true;
74
+ }
75
+ default:
76
+ return true;
77
+ }
78
+ }
79
+ function sequenceFirstSet(parsers) {
80
+ let fs = empty();
81
+ for (const p of parsers) {
82
+ fs = union(fs, p._meta.firstSet);
83
+ if (!matchesEmpty(p)) return fs;
84
+ }
85
+ return fs;
86
+ }
30
87
  function mergeRanges(ranges) {
31
88
  if (ranges.length === 0) return [];
32
89
  const sorted = [...ranges].sort((a, b) => a.lo - b.lo);
@@ -204,12 +261,16 @@ function pushTriviaLogEntry(ctx, start, end, kindIndex) {
204
261
  }
205
262
 
206
263
  // src/combinators/literal.ts
207
- var _collatorCache = null;
208
- function collator() {
209
- if (_collatorCache === null) {
210
- _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;
211
272
  }
212
- return _collatorCache;
273
+ return true;
213
274
  }
214
275
  function literal(value, opts = {}) {
215
276
  const caseInsensitive = opts.caseInsensitive ?? false;
@@ -224,8 +285,9 @@ function literal(value, opts = {}) {
224
285
  const lower = value.toLowerCase();
225
286
  const firstUpper = upper.codePointAt(0);
226
287
  const firstLower = lower.codePointAt(0);
227
- meta.firstSet = firstLower !== void 0 && firstUpper !== void 0 ? { kind: "ranges", ranges: [
228
- { 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 }
229
291
  ] } : firstSet;
230
292
  }
231
293
  return {
@@ -238,7 +300,7 @@ function literal(value, opts = {}) {
238
300
  return failAt(ctx, [JSON.stringify(value)], pos);
239
301
  }
240
302
  const slice = input.slice(pos, end);
241
- const matched = caseInsensitive ? collator().compare(slice, value) === 0 : slice === value;
303
+ const matched = caseInsensitive ? asciiFoldEq(slice, value) : slice === value;
242
304
  if (matched) {
243
305
  const span = { start: pos, end };
244
306
  const leaf = { _tag: "leaf", value: slice, span };
@@ -848,16 +910,19 @@ function scanTrivia(input, cur, ctx) {
848
910
  // src/combinators/sequence.ts
849
911
  function sequence(...parsers) {
850
912
  const meta = {
851
- firstSet: parsers[0]?._meta.firstSet ?? empty(),
913
+ // Union through the nullable prefix — a leading optional()/many() lets a later
914
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
915
+ firstSet: sequenceFirstSet(parsers),
852
916
  canMatchNewline: parsers.some((p) => p._meta.canMatchNewline),
853
917
  isTrivia: false
854
918
  };
919
+ const def = { tag: "sequence", parsers };
855
920
  return {
856
921
  _tag: "sequence",
857
922
  _meta: meta,
858
- _def: { tag: "sequence", parsers },
923
+ _def: def,
859
924
  parse(input, pos, ctx) {
860
- const values = [];
925
+ const values = def.valueUnused ? void 0 : [];
861
926
  let cur = pos;
862
927
  for (let i = 0; i < parsers.length; i++) {
863
928
  if (ctx.trivia && i > 0) {
@@ -877,17 +942,18 @@ function sequence(...parsers) {
877
942
  } else {
878
943
  rollbackTrivia(ctx, mark);
879
944
  }
880
- values.push(result2.value);
945
+ if (values !== void 0) values.push(result2.value);
881
946
  continue;
882
947
  }
883
948
  const result = parsers[i].parse(input, cur, ctx);
884
949
  if (!result.ok) return result;
885
- values.push(result.value);
950
+ if (values !== void 0) values.push(result.value);
886
951
  cur = result.span.end;
887
952
  }
888
953
  return {
889
954
  ok: true,
890
- value: values,
955
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
956
+ value: values ?? void 0,
891
957
  span: { start: pos, end: cur }
892
958
  };
893
959
  }
@@ -928,20 +994,21 @@ function many(combinator) {
928
994
  canMatchNewline: combinator._meta.canMatchNewline,
929
995
  isTrivia: false
930
996
  };
997
+ const def = { tag: "many", parser: combinator, min: 0 };
931
998
  return {
932
999
  _tag: "many",
933
1000
  _meta: meta,
934
- _def: { tag: "many", parser: combinator, min: 0 },
1001
+ _def: def,
935
1002
  parse(input, pos, ctx) {
936
- const values = [];
1003
+ const values = def.valueUnused ? void 0 : [];
937
1004
  let cur = pos;
938
1005
  while (cur < input.length) {
939
1006
  const item = repItem(combinator, input, cur, ctx);
940
1007
  if (item === "stop" || "fail" in item) break;
941
- values.push(item.value);
1008
+ if (values !== void 0) values.push(item.value);
942
1009
  cur = item.end;
943
1010
  }
944
- return { ok: true, value: values, span: { start: pos, end: cur } };
1011
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
945
1012
  }
946
1013
  };
947
1014
  }
@@ -951,22 +1018,23 @@ function oneOrMore(combinator) {
951
1018
  canMatchNewline: combinator._meta.canMatchNewline,
952
1019
  isTrivia: false
953
1020
  };
1021
+ const def = { tag: "oneOrMore", parser: combinator, min: 1 };
954
1022
  return {
955
1023
  _tag: "oneOrMore",
956
1024
  _meta: meta,
957
- _def: { tag: "oneOrMore", parser: combinator, min: 1 },
1025
+ _def: def,
958
1026
  parse(input, pos, ctx) {
959
1027
  const first = combinator.parse(input, pos, ctx);
960
1028
  if (!first.ok) return first;
961
- const values = [first.value];
1029
+ const values = def.valueUnused ? void 0 : [first.value];
962
1030
  let cur = first.span.end;
963
1031
  while (cur < input.length) {
964
1032
  const item = repItem(combinator, input, cur, ctx);
965
1033
  if (item === "stop" || "fail" in item) break;
966
- values.push(item.value);
1034
+ if (values !== void 0) values.push(item.value);
967
1035
  cur = item.end;
968
1036
  }
969
- return { ok: true, value: values, span: { start: pos, end: cur } };
1037
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
970
1038
  }
971
1039
  };
972
1040
  }
@@ -1074,6 +1142,90 @@ function ref() {
1074
1142
  return slot;
1075
1143
  }
1076
1144
 
1145
+ // src/compiler/value-usage.ts
1146
+ function markUnusedValues(root) {
1147
+ const seenConsumed = /* @__PURE__ */ new WeakSet();
1148
+ const seenUnconsumed = /* @__PURE__ */ new WeakSet();
1149
+ const mark = (def, consumed) => {
1150
+ if (consumed) def.valueUnused = false;
1151
+ else if (def.valueUnused !== false) def.valueUnused = true;
1152
+ };
1153
+ const visit = (c, consumed) => {
1154
+ const seen = consumed ? seenConsumed : seenUnconsumed;
1155
+ if (seen.has(c)) return;
1156
+ seen.add(c);
1157
+ const def = c._def;
1158
+ switch (def.tag) {
1159
+ case "many":
1160
+ case "oneOrMore":
1161
+ mark(def, consumed);
1162
+ visit(def.parser, consumed);
1163
+ return;
1164
+ case "optional":
1165
+ visit(def.parser, consumed);
1166
+ return;
1167
+ case "sequence":
1168
+ mark(def, consumed);
1169
+ for (const p of def.parsers) visit(p, consumed);
1170
+ return;
1171
+ case "choice":
1172
+ for (const p of def.parsers) visit(p, consumed);
1173
+ return;
1174
+ case "node":
1175
+ visit(def.parser, false);
1176
+ return;
1177
+ case "transform":
1178
+ visit(def.parser, true);
1179
+ return;
1180
+ case "skip":
1181
+ visit(def.main, consumed);
1182
+ visit(def.skipped, false);
1183
+ return;
1184
+ case "sepBy":
1185
+ visit(def.parser, true);
1186
+ visit(def.separator, false);
1187
+ return;
1188
+ case "not":
1189
+ visit(def.parser, false);
1190
+ return;
1191
+ case "grammar":
1192
+ visit(def.parser, consumed);
1193
+ if (def.triviaParser) visit(def.triviaParser, false);
1194
+ return;
1195
+ case "recover":
1196
+ visit(def.parser, consumed);
1197
+ visit(def.sentinel, false);
1198
+ return;
1199
+ case "label":
1200
+ case "trivia":
1201
+ case "withCtx":
1202
+ case "expect":
1203
+ visit(def.parser, consumed);
1204
+ return;
1205
+ case "lazy":
1206
+ return;
1207
+ default:
1208
+ return;
1209
+ }
1210
+ };
1211
+ const resolveRoot = (c) => {
1212
+ const guard2 = /* @__PURE__ */ new Set();
1213
+ let cur = c;
1214
+ while (!guard2.has(cur)) {
1215
+ const d = cur._def;
1216
+ if (d.tag !== "lazy") return cur;
1217
+ guard2.add(cur);
1218
+ try {
1219
+ cur = d.thunk();
1220
+ } catch {
1221
+ return c;
1222
+ }
1223
+ }
1224
+ return cur;
1225
+ };
1226
+ visit(resolveRoot(root), true);
1227
+ }
1228
+
1077
1229
  // src/combinators/parser.ts
1078
1230
  function rules(factory) {
1079
1231
  const cache = {};
@@ -1103,6 +1255,9 @@ function rules(factory) {
1103
1255
  cache[key] = parser2;
1104
1256
  }
1105
1257
  }
1258
+ for (const key of Object.keys(definitions)) {
1259
+ markUnusedValues(cache[key]);
1260
+ }
1106
1261
  return cache;
1107
1262
  }
1108
1263
 
@@ -1319,7 +1474,12 @@ function parser(opts, root) {
1319
1474
  function noTrivia(root) {
1320
1475
  return parser({ trivia: null }, root);
1321
1476
  }
1477
+ var _analyzed = /* @__PURE__ */ new WeakSet();
1322
1478
  function parse(combinator, input, opts = {}) {
1479
+ if (!_analyzed.has(combinator)) {
1480
+ _analyzed.add(combinator);
1481
+ markUnusedValues(combinator);
1482
+ }
1323
1483
  const trackLines = opts.trackLines ?? false;
1324
1484
  const _errors = opts.recover ? [] : void 0;
1325
1485
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
@@ -1474,8 +1634,8 @@ function classToRanges(cls) {
1474
1634
  return parseClassRanges(body);
1475
1635
  }
1476
1636
  var foldEq = (cVar, cp) => {
1477
- if (cp >= 65 && cp <= 90) return `(${cVar} === ${cp} || ${cVar} === ${cp + 32})`;
1478
- 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}`;
1479
1639
  return `${cVar} === ${cp}`;
1480
1640
  };
1481
1641
  function literalCodePoints(frag) {
@@ -1726,6 +1886,12 @@ function firstSetsDisjoint(a, b) {
1726
1886
  return classDisjoint(a, b);
1727
1887
  }
1728
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
+ }
1729
1895
  if (shape.kind !== "seq") return null;
1730
1896
  const out = [];
1731
1897
  for (const p of shape.parts) {
@@ -2147,12 +2313,34 @@ function parseScanShapeCore(source) {
2147
2313
  }
2148
2314
  function scanShapeFromRegex(source, flags) {
2149
2315
  if (/[msu]/.test(flags)) return null;
2150
- if (/i/.test(flags)) {
2151
- const open = literalCodePoints(source);
2152
- return open ? { kind: "litFold", open } : null;
2153
- }
2316
+ if (/i/.test(flags)) return caseFoldShape(source);
2154
2317
  return parseScanShape(source);
2155
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
+ }
2156
2344
  var classCond = (cVar, ranges) => ranges.map(([lo, hi]) => lo === hi ? `${cVar} === ${lo}` : `(${cVar} >= ${lo} && ${cVar} <= ${hi})`).join(" || ");
2157
2345
  var litCond = (base, cps, firstVar) => cps.map(
2158
2346
  (cp, k) => k === 0 && firstVar ? `${firstVar} === ${cp}` : `input.charCodeAt(${base}${k ? ` + ${k}` : ""}) === ${cp}`
@@ -2164,6 +2352,25 @@ function stringHardStop(shape) {
2164
2352
  );
2165
2353
  }
2166
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
+ }
2167
2374
  function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2168
2375
  if (shape.kind === "lookahead") {
2169
2376
  const inner = emitShapeMatch(shape.inner, start, mint, ind2, firstChar);
@@ -2188,18 +2395,57 @@ function emitShapeMatch(shape, start, mint, ind2, firstChar) {
2188
2395
  const okV = mint("_ok");
2189
2396
  const endV2 = mint("_end");
2190
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
+ }
2191
2421
  if (shape.disjoint) {
2192
- const c0 = codeAt(start, firstChar);
2193
- shape.arms.forEach((arm, k) => {
2194
- const fs = shape.firsts[k];
2195
- const cond = fs.negated ? `!(${classCond(c0, fs.ranges)})` : `(${classCond(c0, fs.ranges)})`;
2196
- const m = emitShapeMatch(arm, start, mint, `${ind2} `, firstChar);
2197
- lines.push(`${ind2}${k === 0 ? "if" : "else if"} (${start} < input.length && ${cond}) {`);
2198
- lines.push(...m.setup);
2199
- lines.push(`${ind2} ${okV} = ${m.ok}`);
2200
- 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
+ });
2201
2435
  lines.push(`${ind2}}`);
2202
- });
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
+ }
2203
2449
  } else {
2204
2450
  const lbl = mint("_alt");
2205
2451
  lines.push(`${ind2}${lbl}: {`);
@@ -2687,6 +2933,8 @@ function tryInlineDestructureTransform(src, valueVars) {
2687
2933
  }
2688
2934
 
2689
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;
2690
2938
  function v(ctx, prefix = "_v") {
2691
2939
  return `${prefix}${ctx.vars++}`;
2692
2940
  }
@@ -3085,13 +3333,14 @@ function emitLit(def, ctx, pos) {
3085
3333
  const vv = v(ctx);
3086
3334
  const expectedStr = JSON.stringify(JSON.stringify(value));
3087
3335
  const stmts = [];
3088
- if (caseInsensitive) {
3089
- 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(" && ");
3090
3341
  stmts.push(
3091
- ...emitIfFail(ctx, `${pos} + ${len} > input.length`, failBody(ctx, expectedStr, pos)),
3092
- `${ind(ctx)}const ${vv}_s = input.slice(${pos}, ${pos} + ${len})`,
3093
- ...emitIfFail(ctx, `_collator.compare(${vv}_s, ${JSON.stringify(value)}) !== 0`, failBody(ctx, expectedStr, pos)),
3094
- `${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})`
3095
3344
  );
3096
3345
  } else if (len === 0) {
3097
3346
  stmts.push(`${ind(ctx)}const ${vv} = ''`);
@@ -3212,6 +3461,7 @@ function emitRegex(def, ctx, pos) {
3212
3461
  return { stmts: stmts2, valueVar: vv2, endVar: scanned.endVar };
3213
3462
  }
3214
3463
  }
3464
+ _loweringSink?.add(`/${def.source}/${def.flags}`);
3215
3465
  const flags = "y" + def.flags.replace(/[gy]/g, "");
3216
3466
  const key = `${def.source}/${flags}`;
3217
3467
  let rName = ctx.regexMap.get(key);
@@ -3276,6 +3526,7 @@ function emitSeqValues(def, ctx, pos) {
3276
3526
  }
3277
3527
  function emitSeq(def, ctx, pos) {
3278
3528
  const { stmts, endVar, valueVars } = emitSeqValues(def, ctx, pos);
3529
+ if (def.valueUnused) return { stmts, valueVar: "undefined", endVar };
3279
3530
  const arrV = v(ctx, "_arr");
3280
3531
  stmts.push(`${ind(ctx)}const ${arrV} = [${valueVars.join(", ")}]`);
3281
3532
  return { stmts, valueVar: arrV, endVar };
@@ -3482,7 +3733,8 @@ function emitFirstMatch(def, ctx, pos) {
3482
3733
  const autoNot = def.autoNot[i];
3483
3734
  const atStart = failsAtStart(p);
3484
3735
  const staticFx = armStaticExpected(ctx, p);
3485
- const fsGuard = needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3736
+ const deferRuleName = ctx.deferFirstSetRefs ? p._ruleName : void 0;
3737
+ const fsGuard = deferRuleName !== void 0 ? `/*@FS:${deferRuleName}:${codeV}@*/true` : needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3486
3738
  let gateCond = null;
3487
3739
  if (gate) {
3488
3740
  const gateIdx = ctx.mapFns.length;
@@ -3573,19 +3825,18 @@ function emitLiteralCondition(litVal, pos) {
3573
3825
  return checks.join(" && ");
3574
3826
  }
3575
3827
  function emitMany(def, ctx, pos) {
3828
+ const wantValue = !def.valueUnused;
3576
3829
  const arrV = v(ctx, "_arr");
3577
3830
  const curV = v(ctx, "_cur");
3578
3831
  const stmts = [
3579
- `${ind(ctx)}const ${arrV} = []`,
3832
+ ...wantValue ? [`${ind(ctx)}const ${arrV} = []`] : [],
3580
3833
  `${ind(ctx)}let ${curV} = ${pos}`
3581
3834
  ];
3582
3835
  if (def.min === 1) {
3583
3836
  const firstR = emit(def.parser, ctx, curV);
3584
3837
  stmts.push(...firstR.stmts);
3585
- stmts.push(
3586
- `${ind(ctx)}${arrV}.push(${firstR.valueVar})`,
3587
- `${ind(ctx)}${curV} = ${firstR.endVar}`
3588
- );
3838
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
3839
+ stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
3589
3840
  }
3590
3841
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
3591
3842
  ctx.indent++;
@@ -3616,13 +3867,13 @@ function emitMany(def, ctx, pos) {
3616
3867
  const { stmts: iterStmts, okVar: iterOk, valVar: iterVal, endVar: iterEnd } = emitFallible(def.parser, ctx, itemPos, true);
3617
3868
  stmts.push(
3618
3869
  ...iterStmts,
3619
- `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`,
3620
- `${ind(ctx)}${arrV}.push(${iterVal})`,
3621
- `${ind(ctx)}${curV} = ${iterEnd}`
3870
+ `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`
3622
3871
  );
3872
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
3873
+ stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
3623
3874
  ctx.indent--;
3624
3875
  stmts.push(`${ind(ctx)}}`);
3625
- return { stmts, valueVar: arrV, endVar: curV };
3876
+ return { stmts, valueVar: wantValue ? arrV : "undefined", endVar: curV };
3626
3877
  }
3627
3878
  function emitOptional(def, ctx, pos) {
3628
3879
  const valV = v(ctx, "_opt");
@@ -3791,15 +4042,19 @@ function emitNode(def, ctx, pos) {
3791
4042
  ctx.buildSrcs.push(def.buildSrc ?? null);
3792
4043
  }
3793
4044
  const i = ind(ctx);
3794
- const capturesTrivia = mkType !== null || structural || buildReadsTrivia(def);
3795
- const clonesState = structural || buildReadsState(def);
4045
+ const capturesTrivia = mkType !== null || !structural && buildReadsTrivia(def);
4046
+ const clonesState = !structural && buildReadsState(def);
3796
4047
  const chV = v(ctx, "_ch");
3797
4048
  const rawV = v(ctx, "_raw");
3798
- 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";
3799
4052
  if (!capturesTrivia) ctx.needsEmptyTl = true;
3800
4053
  const sc = v(ctx, "_sc"), sl = v(ctx, "_sl"), sr = v(ctx, "_sr"), st = v(ctx, "_st"), stl = v(ctx, "_stl");
3801
- const allocStmt = capturesTrivia ? `${i}const ${chV} = [], ${rawV} = [], ${tlV} = []` : `${i}const ${chV} = [], ${rawV} = []`;
3802
- 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";
3803
4058
  const stmts = [
3804
4059
  allocStmt,
3805
4060
  `${i}const ${sc} = _ctx._cstChildren, ${sl} = _ctx._cstLeaves, ${sr} = _ctx._cstRawChildren, ${st} = _ctx.captureTrivia, ${stl} = _ctx._cstTriviaLog`,
@@ -3810,7 +4065,10 @@ function emitNode(def, ctx, pos) {
3810
4065
  stmts.push(`${i}_ctx._cstChildren = ${sc}; _ctx._cstLeaves = ${sl}; _ctx._cstRawChildren = ${sr}; _ctx.captureTrivia = ${st}; _ctx._cstTriviaLog = ${stl}`);
3811
4066
  stmts.push(...emitIfFail(ctx, `!${okVar}`, propagateFailBody(ctx)));
3812
4067
  let stV = "undefined";
3813
- 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) {
3814
4072
  stV = v(ctx, "_nst");
3815
4073
  stmts.push(`${i}const ${stV} = _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
3816
4074
  }
@@ -4232,6 +4490,7 @@ function ruleDependencies(ruleMap) {
4232
4490
  return deps;
4233
4491
  }
4234
4492
  function compile(combinator, mapFnSources) {
4493
+ markUnusedValues(combinator);
4235
4494
  const ctx = {
4236
4495
  vars: 0,
4237
4496
  indent: 1,
@@ -4245,7 +4504,6 @@ function compile(combinator, mapFnSources) {
4245
4504
  buildFns: [],
4246
4505
  buildSrcs: [],
4247
4506
  runtimeParsers: [],
4248
- needsCollator: false,
4249
4507
  namedParsers: /* @__PURE__ */ new Map(),
4250
4508
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4251
4509
  triviaFnNames: /* @__PURE__ */ new Map(),
@@ -4254,18 +4512,18 @@ function compile(combinator, mapFnSources) {
4254
4512
  lazyUsage: analyzeLazyUsage(combinator)
4255
4513
  };
4256
4514
  const r = emit(combinator, ctx, "_pos");
4257
- const collatorDecl = ctx.needsCollator ? `const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })
4258
- ` : "";
4259
4515
  const namedPrelude = ctx.namedFnDecls.length > 0 ? [...namedFnPrelude(), ""] : [];
4260
4516
  const emptyTlDecls = ctx.needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [];
4517
+ const hostReadsDecls = ctx.needsHostReads ? [HOST_READS_DECL] : [];
4261
4518
  const source = [
4262
4519
  ...emptyTlDecls,
4520
+ ...hostReadsDecls,
4263
4521
  ...ctx.regexDecls,
4264
4522
  ...ctx.expectedDecls,
4265
4523
  "",
4266
4524
  ...namedPrelude,
4267
4525
  ctx.namedFnDecls.join("\n\n"),
4268
- `${collatorDecl}function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4526
+ `function _parse(input, _pos, _rp, _mf, _build, _ctx) {`,
4269
4527
  ` let pos = _pos`,
4270
4528
  ...r.stmts,
4271
4529
  ` return { ok: true, value: ${r.valueVar}, span: { start: _pos, end: ${r.endVar} } }`,
@@ -4273,9 +4531,9 @@ function compile(combinator, mapFnSources) {
4273
4531
  ].join("\n");
4274
4532
  const fn = new Function("input", "_pos", "_rp", "_mf", "_build", "_ctx", [
4275
4533
  ...emptyTlDecls,
4534
+ ...hostReadsDecls,
4276
4535
  ...ctx.regexDecls,
4277
4536
  ...ctx.expectedDecls,
4278
- collatorDecl,
4279
4537
  ...namedPrelude,
4280
4538
  ...ctx.namedFnDecls.flatMap((decl, i) => i > 0 ? ["", decl] : [decl]),
4281
4539
  `let pos = _pos`,
@@ -4292,7 +4550,7 @@ function compile(combinator, mapFnSources) {
4292
4550
  const buildSources = ctx.buildFns.length === 0 ? void 0 : ctx.buildSrcs;
4293
4551
  const mfCovered = ctx.mapFns.length === 0 || effectiveSources !== void 0 && effectiveSources.length === ctx.mapFns.length;
4294
4552
  const canInline = ctx.runtimeParsers.length === 0 && mfCovered && buildCovered;
4295
- const inlineExpression = canInline ? buildInlineExpression(ctx, r, collatorDecl, effectiveSources, buildSources) : null;
4553
+ const inlineExpression = canInline ? buildInlineExpression(ctx, r, effectiveSources, buildSources) : null;
4296
4554
  return {
4297
4555
  source,
4298
4556
  inlineExpression,
@@ -4314,8 +4572,17 @@ function compile(combinator, mapFnSources) {
4314
4572
  }
4315
4573
  };
4316
4574
  }
4575
+ var RULE_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4576
+ function assertRuleName(name) {
4577
+ if (!RULE_NAME_RE.test(name)) {
4578
+ throw new Error(
4579
+ `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).`
4580
+ );
4581
+ }
4582
+ }
4317
4583
  function compileLinkable(ruleMapArg, ns) {
4318
4584
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
4585
+ for (const [, rule] of ruleMapArg) markUnusedValues(rule);
4319
4586
  const ruleMap = ruleMapArg.filter(([, val]) => {
4320
4587
  const d = val._def;
4321
4588
  if (d.tag !== "lazy") return true;
@@ -4339,20 +4606,21 @@ function compileLinkable(ruleMapArg, ns) {
4339
4606
  buildFns: [],
4340
4607
  buildSrcs: [],
4341
4608
  runtimeParsers: [],
4342
- needsCollator: false,
4343
4609
  namedParsers: /* @__PURE__ */ new Map(),
4344
4610
  triviaCaptureNames: /* @__PURE__ */ new Map(),
4345
4611
  triviaFnNames: /* @__PURE__ */ new Map(),
4346
4612
  namedFnDecls: [],
4347
4613
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
4348
4614
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
4349
- ns
4615
+ ns,
4616
+ deferFirstSetRefs: true
4350
4617
  };
4351
4618
  const seen = /* @__PURE__ */ new Map();
4352
4619
  const ruleNames = /* @__PURE__ */ new Map();
4353
4620
  const fnNameToKey = /* @__PURE__ */ new Map();
4354
4621
  for (const [key, rule] of ruleMap) {
4355
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4622
+ assertRuleName(key);
4623
+ const base = `_r_${key}`;
4356
4624
  const n = seen.get(base) ?? 0;
4357
4625
  seen.set(base, n + 1);
4358
4626
  const fn = n === 0 ? base : `${base}$${n}`;
@@ -4376,7 +4644,8 @@ function compileLinkable(ruleMapArg, ns) {
4376
4644
  if (resolved === void 0) {
4377
4645
  const name = p._ruleName;
4378
4646
  if (name && !ctx.namedParsers.has(p)) {
4379
- const fn = `_r_${name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4647
+ assertRuleName(name);
4648
+ const fn = `_r_${name}`;
4380
4649
  ruleNames.set(p, fn);
4381
4650
  ctx.namedParsers.set(p, fn);
4382
4651
  }
@@ -4445,20 +4714,34 @@ function compileLinkable(ruleMapArg, ns) {
4445
4714
  `}`
4446
4715
  ].join("\n"));
4447
4716
  }
4717
+ const firstSets = /* @__PURE__ */ new Map();
4718
+ for (const [key, rule] of ruleMap) {
4719
+ let resolved = rule;
4720
+ const d = rule._def;
4721
+ if (d.tag === "lazy") {
4722
+ try {
4723
+ resolved = d.thunk();
4724
+ } catch {
4725
+ resolved = rule;
4726
+ }
4727
+ }
4728
+ firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
4729
+ }
4448
4730
  return {
4449
4731
  ns,
4450
4732
  keys: perEntry.map((e) => e.key),
4451
4733
  prelude,
4452
4734
  ruleFns,
4453
4735
  wrappers,
4736
+ firstSets,
4454
4737
  deps: ruleDependencies(ruleMap),
4455
4738
  needsEmptyTl: !!ctx.needsEmptyTl,
4456
- needsCollator: ctx.needsCollator,
4739
+ needsHostReads: !!ctx.needsHostReads,
4457
4740
  mfFns: mfSrcs ? [] : ctx.mapFns,
4458
4741
  buildFns: buildSrcs ? [] : ctx.buildFns
4459
4742
  };
4460
4743
  }
4461
- function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources) {
4744
+ function buildInlineExpression(ctx, r, mapFnSources, buildSources) {
4462
4745
  const bodyLines = [
4463
4746
  ` let pos = _pos`,
4464
4747
  ...r.stmts,
@@ -4472,14 +4755,15 @@ function buildInlineExpression(ctx, r, collatorDecl, mapFnSources, buildSources)
4472
4755
  const mfDecl = mapFnSources?.length ? ` const _mf = [${mapFnSources.join(", ")}]` : "";
4473
4756
  const buildDecl = buildSources?.length ? ` const _build = [${buildSources.join(", ")}]` : "";
4474
4757
  const emptyTlDecl = ctx.needsEmptyTl ? ` const _EMPTY_TL = Object.freeze([])` : "";
4475
- 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;
4476
4760
  if (!needsWrapper) return innerFn;
4477
4761
  const namedPrelude = ctx.namedFnDecls.length > 0 ? namedFnPrelude() : [];
4478
4762
  const hoistedDecls = [
4479
4763
  emptyTlDecl,
4764
+ hostReadsDecl,
4480
4765
  ...ctx.regexDecls.map((d) => ` ${d}`),
4481
4766
  ...ctx.expectedDecls.map((d) => ` ${d}`),
4482
- collatorDecl ? ` ${collatorDecl.trim()}` : "",
4483
4767
  mfDecl,
4484
4768
  buildDecl,
4485
4769
  ...namedPrelude.map((l) => ` ${l}`),
@@ -4538,20 +4822,36 @@ function fusedBody(pieces) {
4538
4822
  }
4539
4823
  const contributing = new Set(winner.values());
4540
4824
  const needsEmptyTl = [...contributing].some((p) => p.needsEmptyTl);
4541
- const needsCollator = [...contributing].some((p) => p.needsCollator);
4825
+ const needsHostReads = [...contributing].some((p) => p.needsHostReads);
4542
4826
  const lines = [
4543
4827
  // Shared sentinel protocol (must match NAMED_FN_FAIL / NAMED_FN_END in codegen).
4544
4828
  "const _pfFail = {}",
4545
4829
  "let _pfEnd",
4546
4830
  ...needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [],
4547
- ...needsCollator ? ["const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })"] : [],
4831
+ ...needsHostReads ? [HOST_READS_DECL] : [],
4548
4832
  // Each contributing artifact's namespaced private prelude (regexes, _pf, …).
4549
4833
  ...[...contributing].flatMap((p) => p.prelude),
4550
4834
  // The winning `_r_<Name>` function for each rule (one per name → no redeclare).
4551
4835
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
4552
4836
  ];
4553
4837
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
4554
- const body = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4838
+ const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4839
+ const finalFS = /* @__PURE__ */ new Map();
4840
+ for (const [k, p] of winner) {
4841
+ const fs = p.firstSets?.get(k);
4842
+ if (fs) finalFS.set(k, fs);
4843
+ }
4844
+ const body = rawBody.replace(
4845
+ // Rule name + code-point var are both JS identifiers (rule names are validated
4846
+ // at compile time — see assertRuleName in codegen), so an identifier class
4847
+ // matches every well-formed placeholder.
4848
+ /\/\*@FS:([A-Za-z0-9_$]+):([A-Za-z0-9_$]+)@\*\/true/g,
4849
+ (_m, name, codevar) => {
4850
+ const fs = finalFS.get(name);
4851
+ if (!fs || fs.kind === "any" || fs.kind === "empty") return "true";
4852
+ return `(${firstSetCond(codevar, fs)})`;
4853
+ }
4854
+ );
4555
4855
  const env = {};
4556
4856
  for (const p of contributing) {
4557
4857
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;