parseman 0.39.0 → 0.39.1

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.
@@ -94,6 +94,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve2) {
94
94
  return d.value.length === 0;
95
95
  case "keywords":
96
96
  return false;
97
+ case "routed":
98
+ return false;
97
99
  case "regex":
98
100
  try {
99
101
  const m = new RegExp(d.source).exec("");
@@ -1186,6 +1188,8 @@ function derive(c, seen) {
1186
1188
  case "not":
1187
1189
  case "peek":
1188
1190
  return deriveExpected2(def.parser);
1191
+ case "routed":
1192
+ return ["routed()"];
1189
1193
  case "lazy": {
1190
1194
  const name = c._ruleName;
1191
1195
  if (seen.has(c)) return name ? [name] : [];
@@ -2007,42 +2011,195 @@ function sequence(...parsers) {
2007
2011
  }
2008
2012
 
2009
2013
  // src/combinators/dispatch.ts
2010
- function when(key, parser2) {
2014
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2015
+ if (seen.has(parser2)) return false;
2016
+ seen.add(parser2);
2017
+ const def = parser2._def;
2018
+ switch (def.tag) {
2019
+ case "routed":
2020
+ return true;
2021
+ case "sequence":
2022
+ case "choice":
2023
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2024
+ case "dispatch":
2025
+ return false;
2026
+ case "sepBy":
2027
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2028
+ case "skip":
2029
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2030
+ case "grammar":
2031
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2032
+ case "scanTo":
2033
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2034
+ case "recover":
2035
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2036
+ case "lazy":
2037
+ try {
2038
+ return parserUsesRouted(def.thunk(), seen);
2039
+ } catch {
2040
+ return false;
2041
+ }
2042
+ case "many":
2043
+ case "oneOrMore":
2044
+ case "optional":
2045
+ case "attempt":
2046
+ case "transform":
2047
+ case "trivia":
2048
+ case "token":
2049
+ case "leaf":
2050
+ case "label":
2051
+ case "field":
2052
+ case "not":
2053
+ case "peek":
2054
+ case "node":
2055
+ case "withCtx":
2056
+ case "expect":
2057
+ return parserUsesRouted(def.parser, seen);
2058
+ default:
2059
+ return false;
2060
+ }
2061
+ }
2062
+ function branchUsesRouted(branch) {
2063
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2064
+ }
2065
+ function asciiFoldKey(key) {
2066
+ let out = "";
2067
+ for (let i = 0; i < key.length; i++) {
2068
+ const c = key.charCodeAt(i);
2069
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2070
+ }
2071
+ return out;
2072
+ }
2073
+ function matchesDispatchMatcher(value, matcher) {
2074
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2075
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2076
+ switch (matcher.kind) {
2077
+ case "startsWith":
2078
+ return candidate.startsWith(expected);
2079
+ case "endsWith":
2080
+ return candidate.endsWith(expected);
2081
+ case "matches": {
2082
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2083
+ return new RegExp(matcher.value, flags).test(value);
2084
+ }
2085
+ }
2086
+ }
2087
+ function isStringMatcher(value) {
2088
+ if (typeof value !== "object" || value === null) return false;
2089
+ const kind = value.kind;
2090
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2091
+ }
2092
+ function fixedMatcher(kind, value) {
2093
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2094
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2095
+ return { kind, value };
2096
+ }
2097
+ function startsWith(prefix) {
2098
+ return fixedMatcher("startsWith", prefix);
2099
+ }
2100
+ function endsWith(suffix) {
2101
+ return fixedMatcher("endsWith", suffix);
2102
+ }
2103
+ function matches(pattern) {
2104
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2105
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2106
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2107
+ }
2108
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2109
+ }
2110
+ function when(key, parser2, opts = {}) {
2111
+ const usesRouted = parserUsesRouted(parser2);
2112
+ if (isStringMatcher(key)) {
2113
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2114
+ }
2011
2115
  const keys = Array.isArray(key) ? [...key] : [key];
2012
2116
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2013
2117
  for (const item of keys) {
2014
2118
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2015
2119
  }
2016
- return { kind: "when", keys, parser: parser2 };
2120
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2017
2121
  }
2018
2122
  function otherwise(parser2) {
2019
- return { kind: "otherwise", parser: parser2 };
2123
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2124
+ }
2125
+ function routed() {
2126
+ return {
2127
+ _tag: "routed",
2128
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2129
+ _def: { tag: "routed" },
2130
+ parse(_input, pos, ctx) {
2131
+ const item = ctx._routed;
2132
+ if (item === void 0) {
2133
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2134
+ }
2135
+ if (pos !== item.span.start) {
2136
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2137
+ }
2138
+ if (cstCaptureActive(ctx)) {
2139
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2140
+ }
2141
+ return { ok: true, value: item.value, span: item.span };
2142
+ }
2143
+ };
2020
2144
  }
2021
2145
  function dispatch(selector, ...arms) {
2146
+ if (parserUsesRouted(selector)) {
2147
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2148
+ }
2022
2149
  let fallback;
2150
+ let fallbackUsesRouted = false;
2151
+ const matchers = [];
2023
2152
  const cases = [];
2024
- const seen = /* @__PURE__ */ new Set();
2153
+ const seen = [];
2025
2154
  for (let i = 0; i < arms.length; i++) {
2026
2155
  const arm = arms[i];
2027
2156
  if (arm.kind === "otherwise") {
2028
2157
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2029
2158
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2030
2159
  fallback = arm.parser;
2160
+ fallbackUsesRouted = arm.usesRouted === true;
2031
2161
  continue;
2032
2162
  }
2033
- for (const key of arm.keys) {
2034
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2035
- seen.add(key);
2163
+ if (arm.kind === "whenMatcher") {
2164
+ const matcher = arm.matcher;
2165
+ matchers.push({
2166
+ kind: matcher.kind,
2167
+ value: matcher.value,
2168
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2169
+ parser: arm.parser,
2170
+ caseInsensitive: arm.caseInsensitive,
2171
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2172
+ });
2173
+ continue;
2036
2174
  }
2037
- cases.push({ keys: arm.keys, parser: arm.parser });
2175
+ for (const key of arm.keys) {
2176
+ const folded = asciiFoldKey(key);
2177
+ for (const prior of seen) {
2178
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2179
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2180
+ }
2181
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2182
+ }
2183
+ cases.push({
2184
+ keys: arm.keys,
2185
+ parser: arm.parser,
2186
+ caseInsensitive: arm.caseInsensitive,
2187
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2188
+ });
2038
2189
  }
2039
2190
  const meta = {
2040
2191
  firstSet: selector._meta.firstSet,
2041
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2192
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2042
2193
  isTrivia: false
2043
2194
  };
2044
2195
  const byKey = /* @__PURE__ */ new Map();
2045
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2196
+ const byFoldedKey = /* @__PURE__ */ new Map();
2197
+ for (const entry of cases) {
2198
+ for (const key of entry.keys) {
2199
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2200
+ else byKey.set(key, entry);
2201
+ }
2202
+ }
2046
2203
  return {
2047
2204
  _tag: "dispatch",
2048
2205
  _meta: meta,
@@ -2050,18 +2207,35 @@ function dispatch(selector, ...arms) {
2050
2207
  tag: "dispatch",
2051
2208
  selector,
2052
2209
  cases,
2053
- ...fallback === void 0 ? {} : { otherwise: fallback }
2210
+ ...matchers.length === 0 ? {} : { matchers },
2211
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2054
2212
  },
2055
2213
  parse(input, pos, ctx) {
2214
+ if (parserUsesRouted(selector)) {
2215
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2216
+ }
2217
+ const selectorMark = saveTriviaMark(ctx);
2056
2218
  const selected = selector.parse(input, pos, ctx);
2057
2219
  if (!selected.ok) return selected;
2058
- const tail = byKey.get(selected.value) ?? fallback;
2059
- if (tail === void 0) {
2220
+ const branch = byKey.get(selected.value) ?? (byFoldedKey.size === 0 ? void 0 : byFoldedKey.get(asciiFoldKey(selected.value))) ?? matchers.find((matcher) => matchesDispatchMatcher(selected.value, matcher)) ?? (fallback === void 0 ? void 0 : { keys: [], parser: fallback, caseInsensitive: false, usesRouted: fallbackUsesRouted });
2221
+ if (branch === void 0) {
2060
2222
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2061
2223
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2062
2224
  }
2063
- const mark = saveTriviaMark(ctx);
2064
- const result = tail.parse(input, selected.span.end, ctx);
2225
+ const savedRouted = ctx._routed;
2226
+ const usesRouted = branchUsesRouted(branch);
2227
+ let mark = saveTriviaMark(ctx);
2228
+ if (usesRouted) {
2229
+ rollbackTrivia(ctx, selectorMark);
2230
+ mark = saveTriviaMark(ctx);
2231
+ ctx._routed = { value: selected.value, span: selected.span };
2232
+ }
2233
+ let result;
2234
+ try {
2235
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2236
+ } finally {
2237
+ if (usesRouted) ctx._routed = savedRouted;
2238
+ }
2065
2239
  if (!result.ok) {
2066
2240
  rollbackTrivia(ctx, mark);
2067
2241
  return { ...result, committed: true };
@@ -2411,6 +2585,7 @@ function markUnusedValues(root) {
2411
2585
  case "dispatch":
2412
2586
  visit(def.selector, consumed);
2413
2587
  for (const entry of def.cases) visit(entry.parser, consumed);
2588
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
2414
2589
  if (def.otherwise) visit(def.otherwise, consumed);
2415
2590
  return;
2416
2591
  case "node":
@@ -2689,7 +2864,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2689
2864
  case "choice":
2690
2865
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2691
2866
  case "dispatch":
2692
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2867
+ return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => parserHasOwnFields(entry.parser, seen)) : false) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2693
2868
  case "sepBy":
2694
2869
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2695
2870
  case "skip":
@@ -2758,7 +2933,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2758
2933
  case "choice":
2759
2934
  return d.parsers.some((x) => parserHasTriviaSite(x, seen));
2760
2935
  case "dispatch":
2761
- return parserHasTriviaSite(d.selector, seen) || d.cases.some((x) => parserHasTriviaSite(x.parser, seen)) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false);
2936
+ return parserHasTriviaSite(d.selector, seen) || d.cases.some((x) => parserHasTriviaSite(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => parserHasTriviaSite(entry.parser, seen)) : false) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false);
2762
2937
  case "grammar":
2763
2938
  return parserHasTriviaSite(d.parser, seen);
2764
2939
  case "lazy": {
@@ -2785,7 +2960,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2785
2960
  case "choice":
2786
2961
  return d.parsers.some((x) => parserEnablesTriviaCapture(x, seen));
2787
2962
  case "dispatch":
2788
- return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some((x) => parserEnablesTriviaCapture(x.parser, seen)) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false);
2963
+ return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some((x) => parserEnablesTriviaCapture(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => parserEnablesTriviaCapture(entry.parser, seen)) : false) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false);
2789
2964
  case "sepBy":
2790
2965
  return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen);
2791
2966
  case "skip":
@@ -3160,7 +3335,7 @@ function leaf(root, fn) {
3160
3335
  }
3161
3336
 
3162
3337
  // src/version.ts
3163
- var PARSEMAN_VERSION = "0.39.0";
3338
+ var PARSEMAN_VERSION = "0.39.1";
3164
3339
 
3165
3340
  // src/cst/host-mode.ts
3166
3341
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3420,6 +3595,7 @@ function analyzeGatingRules(ruleMap, opts) {
3420
3595
  if (d.tag === "dispatch") {
3421
3596
  kids.push(d.selector);
3422
3597
  for (const c of d.cases) kids.push(c.parser);
3598
+ if (d.matchers) for (const c of d.matchers) kids.push(c.parser);
3423
3599
  if (d.otherwise !== void 0) kids.push(d.otherwise);
3424
3600
  }
3425
3601
  if (Array.isArray(rec.skip)) kids.push(...rec.skip);
@@ -4943,13 +5119,25 @@ function escapeClassChar(ch2) {
4943
5119
  }
4944
5120
 
4945
5121
  // src/compiler/grammar-coverage-ids.ts
5122
+ function idPart(value) {
5123
+ return encodeURIComponent(value);
5124
+ }
5125
+ function dispatchArmIds(def, path2) {
5126
+ const ids = [];
5127
+ for (const entry of def.cases) ids.push(`dispatch:${path2}/when:${entry.keys.map(idPart).join("|")}`);
5128
+ for (const entry of def.matchers ?? []) {
5129
+ ids.push(`dispatch:${path2}/matcher:${entry.kind}:${idPart(entry.value)}${entry.kind === "matches" && entry.flags ? `:${idPart(entry.flags)}` : ""}`);
5130
+ }
5131
+ if (def.otherwise) ids.push(`dispatch:${path2}/otherwise`);
5132
+ return ids;
5133
+ }
4946
5134
  function children(def, winners) {
4947
5135
  switch (def.tag) {
4948
5136
  case "sequence":
4949
5137
  case "choice":
4950
5138
  return def.parsers;
4951
5139
  case "dispatch":
4952
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
5140
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
4953
5141
  case "many":
4954
5142
  case "oneOrMore":
4955
5143
  case "optional":
@@ -4992,6 +5180,7 @@ function children(def, winners) {
4992
5180
  function buildGrammarPlan(entry, winners) {
4993
5181
  const definitions = /* @__PURE__ */ new Map();
4994
5182
  const choices = /* @__PURE__ */ new WeakMap();
5183
+ const dispatches = /* @__PURE__ */ new WeakMap();
4995
5184
  const attempts = /* @__PURE__ */ new WeakMap();
4996
5185
  const labels = /* @__PURE__ */ new WeakMap();
4997
5186
  const rules2 = /* @__PURE__ */ new WeakMap();
@@ -5022,6 +5211,11 @@ function buildGrammarPlan(entry, winners) {
5022
5211
  choices.set(parser2, ids);
5023
5212
  ids.forEach((id) => definitions.set(id, { id, kind: "choice-arm" }));
5024
5213
  }
5214
+ if (parser2._def.tag === "dispatch") {
5215
+ const ids = dispatchArmIds(parser2._def, path2);
5216
+ dispatches.set(parser2, ids);
5217
+ ids.forEach((id) => definitions.set(id, { id, kind: "dispatch-arm" }));
5218
+ }
5025
5219
  if (parser2._def.tag === "attempt") attempts.set(parser2, `attempt:${path2}`);
5026
5220
  if (parser2._def.tag === "label") {
5027
5221
  const id = `label:${path2}`;
@@ -5034,7 +5228,7 @@ function buildGrammarPlan(entry, winners) {
5034
5228
  for (const root of roots) {
5035
5229
  visit(root, winnerNames.get(root) ?? root._ruleName ?? "entry");
5036
5230
  }
5037
- return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, attempts, labels, rules: rules2 };
5231
+ return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules: rules2 };
5038
5232
  }
5039
5233
 
5040
5234
  // src/compiler/scannable-run.ts
@@ -6408,6 +6602,7 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
6408
6602
  case "literal":
6409
6603
  case "regex":
6410
6604
  case "keywords":
6605
+ case "routed":
6411
6606
  return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
6412
6607
  case "choice": {
6413
6608
  const alts = [];
@@ -6766,7 +6961,7 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
6766
6961
  case "choice":
6767
6962
  return false;
6768
6963
  case "dispatch":
6769
- return capturesLeaf(d.selector, seen) || d.cases.some((x) => mayLeavePartialCapture(x.parser, seen) || capturesLeaf(x.parser, seen)) || (d.otherwise ? mayLeavePartialCapture(d.otherwise, seen) || capturesLeaf(d.otherwise, seen) : false);
6964
+ return capturesLeaf(d.selector, seen) || d.cases.some((x) => mayLeavePartialCapture(x.parser, seen) || capturesLeaf(x.parser, seen)) || (d.matchers ? d.matchers.some((x) => mayLeavePartialCapture(x.parser, seen) || capturesLeaf(x.parser, seen)) : false) || (d.otherwise ? mayLeavePartialCapture(d.otherwise, seen) || capturesLeaf(d.otherwise, seen) : false);
6770
6965
  case "attempt":
6771
6966
  return false;
6772
6967
  // optional never fails; many/oneOrMore only "fail" with zero captured items.
@@ -6865,6 +7060,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6865
7060
  case "keywords":
6866
7061
  case "node":
6867
7062
  case "token":
7063
+ case "routed":
6868
7064
  case "leaf":
6869
7065
  return true;
6870
7066
  case "not":
@@ -6877,7 +7073,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6877
7073
  case "choice":
6878
7074
  return d.parsers.some((x) => capturesLeaf(x, seen));
6879
7075
  case "dispatch":
6880
- return capturesLeaf(d.selector, seen) || d.cases.some((x) => capturesLeaf(x.parser, seen)) || (d.otherwise ? capturesLeaf(d.otherwise, seen) : false);
7076
+ return capturesLeaf(d.selector, seen) || d.cases.some((x) => capturesLeaf(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => capturesLeaf(entry.parser, seen)) : false) || (d.otherwise ? capturesLeaf(d.otherwise, seen) : false);
6881
7077
  case "sepBy":
6882
7078
  return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen);
6883
7079
  case "many":
@@ -7355,6 +7551,7 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7355
7551
  for (const item of d.cases) {
7356
7552
  if (mayRecordRecoveryError(item.parser, recovery, seen)) return true;
7357
7553
  }
7554
+ if (d.matchers?.some((item) => mayRecordRecoveryError(item.parser, recovery, seen))) return true;
7358
7555
  return d.otherwise ? mayRecordRecoveryError(d.otherwise, recovery, seen) : false;
7359
7556
  }
7360
7557
  case "attempt":
@@ -7389,53 +7586,120 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7389
7586
  return false;
7390
7587
  }
7391
7588
  }
7392
- function emitDispatchCombinator(def, ctx, pos) {
7589
+ function emitDispatchCombinator(parser2, def, ctx, pos) {
7590
+ if (parserUsesRouted2(def.selector)) {
7591
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
7592
+ }
7593
+ const coverageIds = ctx.coverage?.plan.dispatches.get(parser2) ?? [];
7594
+ const branchUsesRouted2 = (entry) => entry.usesRouted === true || parserUsesRouted2(entry.parser);
7595
+ const hasRoutedBranch = def.cases.some(branchUsesRouted2) || (def.matchers ? def.matchers.some(branchUsesRouted2) : false) || (def.otherwise ? def.otherwiseUsesRouted === true || parserUsesRouted2(def.otherwise) : false);
7596
+ const selectorLeafMark = hasRoutedBranch ? v(ctx, "_dsl") : null;
7597
+ const selectorRawMark = hasRoutedBranch ? v(ctx, "_dsr") : null;
7598
+ const selectorTriviaMark = hasRoutedBranch ? v(ctx, "_dst") : null;
7599
+ const selectorLogMark = hasRoutedBranch ? v(ctx, "_dsg") : null;
7600
+ const selectorFieldMark = hasRoutedBranch ? v(ctx, "_dsf") : null;
7601
+ const selectorErrorMark = hasRoutedBranch ? v(ctx, "_dse") : null;
7393
7602
  const selector = emit(def.selector, ctx, pos);
7394
7603
  const outV = v(ctx, "_dval");
7395
7604
  const outE = v(ctx, "_dend");
7396
7605
  const keyV = v(ctx, "_dkey");
7397
7606
  const stmts = [
7607
+ ...selectorLeafMark ? [
7608
+ `${ind(ctx)}const ${selectorLeafMark} = _ctx._cstLeaves?.length ?? 0`,
7609
+ `${ind(ctx)}const ${selectorRawMark} = _ctx._cstRawChildren?.length ?? 0`,
7610
+ `${ind(ctx)}const ${selectorTriviaMark} = _ctx._cstTriviaLog?.length ?? 0`,
7611
+ `${ind(ctx)}const ${selectorLogMark} = _ctx._triviaLog?.length ?? 0`,
7612
+ `${ind(ctx)}const ${selectorFieldMark} = _ctx._fields?.length ?? 0`,
7613
+ `${ind(ctx)}const ${selectorErrorMark} = _ctx._errors?.length ?? 0`
7614
+ ] : [],
7398
7615
  ...selector.stmts,
7399
7616
  `${ind(ctx)}const ${keyV} = ${selector.valueVar}`,
7400
7617
  `${ind(ctx)}let ${outV}, ${outE} = ${selector.endVar}`
7401
7618
  ];
7402
7619
  let wroteBranch = false;
7403
- const emitSelectedTail = (parser2, keyword, condition) => {
7620
+ const emitSelectedTail = (entry, keyword, condition, coverageId) => {
7621
+ const parser3 = entry.parser;
7404
7622
  const head = keyword === "else" ? `${ind(ctx)}else {` : `${ind(ctx)}${keyword} (${condition}) {`;
7405
7623
  stmts.push(head);
7406
7624
  ctx.indent++;
7407
- const mayError = mayRecordRecoveryError(parser2, !!ctx.recovery);
7625
+ const usesRouted = branchUsesRouted2(entry);
7626
+ const routedV = usesRouted ? v(ctx, "_drt") : null;
7627
+ const mayError = mayRecordRecoveryError(parser3, !!ctx.recovery);
7408
7628
  const errMark = mayError ? v(ctx, "_derr") : null;
7409
7629
  const logMark = ctx.activeTrivia ? v(ctx, "_dlog") : null;
7630
+ if (routedV) {
7631
+ stmts.push(
7632
+ `${ind(ctx)}const ${routedV} = _ctx._routed; _ctx._routed = { value: ${selector.valueVar}, span: { start: ${pos}, end: ${selector.endVar} } }`,
7633
+ ...selectorLeafMark ? [
7634
+ `${ind(ctx)}if (_ctx._cstLeaves && _ctx._cstLeaves.length !== ${selectorLeafMark}) _ctx._cstLeaves.length = ${selectorLeafMark}; if (_ctx._cstRawChildren && _ctx._cstRawChildren.length !== ${selectorRawMark}) _ctx._cstRawChildren.length = ${selectorRawMark}; if (_ctx._cstTriviaLog && _ctx._cstTriviaLog.length !== ${selectorTriviaMark}) _ctx._cstTriviaLog.length = ${selectorTriviaMark}; if (_ctx._triviaLog && _ctx._triviaLog.length !== ${selectorLogMark}) _ctx._triviaLog.length = ${selectorLogMark}; if (_ctx._fields && _ctx._fields.length !== ${selectorFieldMark}) _ctx._fields.length = ${selectorFieldMark}; if (_ctx._errors && _ctx._errors.length !== ${selectorErrorMark}) _ctx._errors.length = ${selectorErrorMark}`
7635
+ ] : []
7636
+ );
7637
+ }
7410
7638
  if (errMark) stmts.push(`${ind(ctx)}const ${errMark} = _ctx._errors?.length ?? 0`);
7411
7639
  if (logMark) stmts.push(`${ind(ctx)}const ${logMark} = _ctx._triviaLog?.length ?? 0`);
7412
- const tail = emitTailFallible(parser2, ctx, selector.endVar);
7640
+ const tail = emitTailFallible(parser3, ctx, usesRouted ? pos : selector.endVar);
7641
+ stmts.push(...coverageAttempt(ctx, coverageId, pos));
7413
7642
  const errorRollback = errMark ? `if (_ctx._errors && _ctx._errors.length !== ${errMark}) _ctx._errors.length = ${errMark}; ` : "";
7414
7643
  const logRollback = logMark ? `if (_ctx._triviaLog && _ctx._triviaLog.length !== ${logMark}) _ctx._triviaLog.length = ${logMark}; ` : "";
7644
+ const routedRollback = routedV ? `_ctx._routed = ${routedV}; ` : "";
7645
+ const coverageFailure = coverageId === void 0 ? "" : `_ctx._grammarTrace?.write({ id: ${JSON.stringify(coverageId)}, phase: 'failure', offset: _ctx._fe }); `;
7415
7646
  stmts.push(
7416
7647
  ...tail.stmts,
7417
- `${ind(ctx)}if (!${tail.okVar}) { ${errorRollback}${logRollback}${committedFailBody(ctx)} }`,
7648
+ `${ind(ctx)}if (!${tail.okVar}) { ${routedRollback}${errorRollback}${logRollback}${coverageFailure}${committedFailBody(ctx)} }`,
7649
+ ...routedV ? [`${ind(ctx)}_ctx._routed = ${routedV}`] : [],
7418
7650
  `${ind(ctx)}${outV} = [${selector.valueVar}, ${tail.valVar}]`,
7419
- `${ind(ctx)}${outE} = ${tail.endVar}`
7651
+ `${ind(ctx)}${outE} = ${tail.endVar}`,
7652
+ ...coverageHit(ctx, coverageId, pos, tail.endVar)
7420
7653
  );
7421
7654
  ctx.indent--;
7422
7655
  stmts.push(`${ind(ctx)}}`);
7423
7656
  };
7657
+ let coverageIndex = 0;
7424
7658
  for (const entry of def.cases) {
7425
- const condition = entry.keys.map((key) => `${keyV} === ${JSON.stringify(key)}`).join(" || ");
7426
- emitSelectedTail(entry.parser, wroteBranch ? "else if" : "if", condition);
7659
+ const condition = entry.keys.map((key) => emitDispatchKeyCondition(keyV, key, entry.caseInsensitive)).join(" || ");
7660
+ emitSelectedTail(entry, wroteBranch ? "else if" : "if", condition, coverageIds[coverageIndex++]);
7427
7661
  wroteBranch = true;
7428
7662
  }
7663
+ const emitMatcherTails = () => {
7664
+ for (const matcher of def.matchers ?? []) {
7665
+ emitSelectedTail(matcher, wroteBranch ? "else if" : "if", emitDispatchMatcherCondition(keyV, matcher), coverageIds[coverageIndex++]);
7666
+ wroteBranch = true;
7667
+ }
7668
+ };
7429
7669
  if (def.otherwise) {
7430
- if (wroteBranch) emitSelectedTail(def.otherwise, "else");
7431
- else emitSelectedTail(def.otherwise, "if", "true");
7670
+ emitMatcherTails();
7671
+ const otherwiseEntry = { parser: def.otherwise, usesRouted: def.otherwiseUsesRouted };
7672
+ if (wroteBranch) emitSelectedTail(otherwiseEntry, "else", void 0, coverageIds[coverageIndex++]);
7673
+ else emitSelectedTail(otherwiseEntry, "if", "true", coverageIds[coverageIndex++]);
7432
7674
  } else {
7675
+ emitMatcherTails();
7433
7676
  const expected = JSON.stringify(def.cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key))));
7434
7677
  const fail = failArrBody(ctx, expected, selector.endVar);
7435
7678
  stmts.push(wroteBranch ? `${ind(ctx)}else { ${fail} }` : `${ind(ctx)}${fail}`);
7436
7679
  }
7437
7680
  return { stmts, valueVar: outV, endVar: outE };
7438
7681
  }
7682
+ function emitDispatchKeyCondition(valueVar, key, caseInsensitive) {
7683
+ if (!caseInsensitive) return `${valueVar} === ${JSON.stringify(key)}`;
7684
+ const parts = [`${valueVar}.length === ${key.length}`];
7685
+ for (let i = 0; i < key.length; i++) {
7686
+ parts.push(foldEq(`${valueVar}.charCodeAt(${i})`, key.charCodeAt(i)));
7687
+ }
7688
+ return parts.join(" && ");
7689
+ }
7690
+ function emitDispatchMatcherCondition(valueVar, matcher) {
7691
+ if (matcher.kind === "matches") {
7692
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
7693
+ return `${new RegExp(matcher.value, flags).toString()}.test(${valueVar})`;
7694
+ }
7695
+ const text = matcher.value;
7696
+ const parts = [`${valueVar}.length >= ${text.length}`];
7697
+ const offset = matcher.kind === "endsWith" ? `${valueVar}.length - ${text.length}` : "0";
7698
+ for (let i = 0; i < text.length; i++) {
7699
+ parts.push(matcher.caseInsensitive ? foldEq(`${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`})`, text.charCodeAt(i)) : `${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`}) === ${text.charCodeAt(i)}`);
7700
+ }
7701
+ return parts.join(" && ");
7702
+ }
7439
7703
  function emitGreedyClassify(def, superIndex, ctx, pos, coverageBase) {
7440
7704
  const superParser = def.parsers[superIndex];
7441
7705
  const regexDef = getCoreRegexDef(superParser);
@@ -8090,6 +8354,18 @@ function emitPeek(def, ctx, pos) {
8090
8354
  endVar: pos
8091
8355
  };
8092
8356
  }
8357
+ function emitRouted(ctx, pos) {
8358
+ const item = v(ctx, "_rt");
8359
+ return {
8360
+ stmts: [
8361
+ `${ind(ctx)}const ${item} = _ctx._routed`,
8362
+ ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
8363
+ ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
8364
+ ],
8365
+ valueVar: `${item}.value`,
8366
+ endVar: `${item}.span.end`
8367
+ };
8368
+ }
8093
8369
  function escapeTokenLiteral(value) {
8094
8370
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
8095
8371
  }
@@ -8555,7 +8831,7 @@ function emitDispatch(p, ctx, pos) {
8555
8831
  case "choice":
8556
8832
  return emitChoice(p, def, ctx, pos);
8557
8833
  case "dispatch":
8558
- return emitDispatchCombinator(def, ctx, pos);
8834
+ return emitDispatchCombinator(p, def, ctx, pos);
8559
8835
  case "attempt":
8560
8836
  return emitAttempt(p, def, ctx, pos);
8561
8837
  case "many":
@@ -8680,6 +8956,8 @@ function emitDispatch(p, ctx, pos) {
8680
8956
  return emitNot(def, ctx, pos);
8681
8957
  case "peek":
8682
8958
  return emitPeek(def, ctx, pos);
8959
+ case "routed":
8960
+ return emitRouted(ctx, pos);
8683
8961
  case "node":
8684
8962
  return emitNode(def, ctx, pos);
8685
8963
  case "scanTo":
@@ -8777,7 +9055,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8777
9055
  case "choice":
8778
9056
  return d.parsers.some((x) => hasNodeDef(x, seen));
8779
9057
  case "dispatch":
8780
- return hasNodeDef(d.selector, seen) || d.cases.some((x) => hasNodeDef(x.parser, seen)) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false);
9058
+ return hasNodeDef(d.selector, seen) || d.cases.some((x) => hasNodeDef(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => hasNodeDef(entry.parser, seen)) : false) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false);
8781
9059
  case "sepBy":
8782
9060
  return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen);
8783
9061
  case "scanTo":
@@ -8792,6 +9070,26 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8792
9070
  return false;
8793
9071
  }
8794
9072
  }
9073
+ function parserUsesRouted2(p, seen = /* @__PURE__ */ new Set()) {
9074
+ if (seen.has(p)) return false;
9075
+ seen.add(p);
9076
+ const d = p._def;
9077
+ switch (d.tag) {
9078
+ case "routed":
9079
+ return true;
9080
+ case "lazy": {
9081
+ try {
9082
+ return parserUsesRouted2(d.thunk(), seen);
9083
+ } catch {
9084
+ return false;
9085
+ }
9086
+ }
9087
+ case "dispatch":
9088
+ return false;
9089
+ default:
9090
+ return childrenOf2(d).some((child) => parserUsesRouted2(child, seen));
9091
+ }
9092
+ }
8795
9093
  function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
8796
9094
  if (seen.has(p)) return false;
8797
9095
  seen.add(p);
@@ -8812,7 +9110,7 @@ function childrenOf2(def) {
8812
9110
  case "choice":
8813
9111
  return def.parsers;
8814
9112
  case "dispatch":
8815
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9113
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
8816
9114
  case "many":
8817
9115
  case "attempt":
8818
9116
  case "optional":
@@ -8849,6 +9147,7 @@ function childrenOf2(def) {
8849
9147
  case "regex":
8850
9148
  case "keywords":
8851
9149
  case "guard":
9150
+ case "routed":
8852
9151
  case "unknown":
8853
9152
  return [];
8854
9153
  }
@@ -9682,7 +9981,7 @@ function childrenOf3(def) {
9682
9981
  case "choice":
9683
9982
  return def.parsers;
9684
9983
  case "dispatch":
9685
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9984
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
9686
9985
  case "many":
9687
9986
  case "oneOrMore":
9688
9987
  case "optional":
@@ -9711,6 +10010,7 @@ function childrenOf3(def) {
9711
10010
  case "literal":
9712
10011
  case "regex":
9713
10012
  case "keywords":
10013
+ case "routed":
9714
10014
  case "guard":
9715
10015
  case "recover":
9716
10016
  case "unknown":
@@ -9735,6 +10035,16 @@ function repeatOpts(min, max, trailing) {
9735
10035
  if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
9736
10036
  return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
9737
10037
  }
10038
+ function matcherExpr(entry) {
10039
+ switch (entry.kind) {
10040
+ case "startsWith":
10041
+ return `startsWith(${JSON.stringify(entry.value)})`;
10042
+ case "endsWith":
10043
+ return `endsWith(${JSON.stringify(entry.value)})`;
10044
+ case "matches":
10045
+ return `matches(${new RegExp(entry.value, entry.flags ?? "").toString()})`;
10046
+ }
10047
+ }
9738
10048
  function serializeRuleMap(ruleMap, scanSkip) {
9739
10049
  try {
9740
10050
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -9834,7 +10144,11 @@ function evalRuleMapIR(ir) {
9834
10144
  "choice",
9835
10145
  "dispatch",
9836
10146
  "when",
10147
+ "startsWith",
10148
+ "endsWith",
10149
+ "matches",
9837
10150
  "otherwise",
10151
+ "routed",
9838
10152
  "attempt",
9839
10153
  "many",
9840
10154
  "oneOrMore",
@@ -9870,7 +10184,11 @@ function evalRuleMapIR(ir) {
9870
10184
  choice,
9871
10185
  dispatch,
9872
10186
  when,
10187
+ startsWith,
10188
+ endsWith,
10189
+ matches,
9873
10190
  otherwise,
10191
+ routed,
9874
10192
  attempt,
9875
10193
  many,
9876
10194
  oneOrMore,
@@ -10058,8 +10376,13 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10058
10376
  }
10059
10377
  case "dispatch": {
10060
10378
  const arms = def.cases.map(
10061
- (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)})`
10379
+ (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`
10062
10380
  );
10381
+ if (def.matchers) {
10382
+ for (const entry of def.matchers) {
10383
+ arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`);
10384
+ }
10385
+ }
10063
10386
  if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`);
10064
10387
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? "" : `, ${arms.join(", ")}`})`;
10065
10388
  }
@@ -10079,6 +10402,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10079
10402
  return `not(${kid(def.parser)})`;
10080
10403
  case "peek":
10081
10404
  return `peek(${kid(def.parser)})`;
10405
+ case "routed":
10406
+ return "routed()";
10082
10407
  case "trivia":
10083
10408
  return `trivia(${kid(def.parser)})`;
10084
10409
  case "token":
@@ -10548,6 +10873,9 @@ function stripTsFromSource(node2, code) {
10548
10873
  function isWordFactory(v2) {
10549
10874
  return !!v2 && typeof v2 === "object" && v2.tag === "wordFactory";
10550
10875
  }
10876
+ function isWhenFactory(v2) {
10877
+ return !!v2 && typeof v2 === "object" && v2.tag === "whenFactory";
10878
+ }
10551
10879
  function isStaticValueEntry(v2) {
10552
10880
  return !!v2 && typeof v2 === "object" && "value" in v2 && "mfSrcs" in v2;
10553
10881
  }
@@ -10570,6 +10898,23 @@ function wordFactoryFromArgs(args, scope, code, mfs) {
10570
10898
  if (typeof caseInsensitive !== "boolean") return null;
10571
10899
  return { tag: "wordFactory", boundary: "_0-9A-Za-z", caseInsensitive };
10572
10900
  }
10901
+ function dispatchWhenOptions(v2) {
10902
+ if (v2 === void 0) return {};
10903
+ if (typeof v2 !== "object" || v2 === null || Array.isArray(v2)) return null;
10904
+ for (const key of Object.keys(v2)) {
10905
+ if (key !== "caseInsensitive") return null;
10906
+ }
10907
+ const caseInsensitive = "caseInsensitive" in v2 ? v2.caseInsensitive : false;
10908
+ if (typeof caseInsensitive !== "boolean") return null;
10909
+ return { caseInsensitive };
10910
+ }
10911
+ function whenFactoryFromArgs(args, scope, code, mfs) {
10912
+ const [optsArg, extra] = args;
10913
+ if (optsArg?.type === "SpreadElement" || extra !== void 0) return null;
10914
+ const rawOpts = optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs);
10915
+ const opts = dispatchWhenOptions(rawOpts);
10916
+ return opts === null ? null : { tag: "whenFactory", caseInsensitive: opts.caseInsensitive ?? false };
10917
+ }
10573
10918
  var SUPPORTED = {
10574
10919
  literal: (...a) => literal(a[0], a[1]),
10575
10920
  regex: (...a) => regex(a[0], a[1]),
@@ -10585,6 +10930,7 @@ var SUPPORTED = {
10585
10930
  field: (...a) => field(a[0], a[1]),
10586
10931
  noTrivia: (...a) => noTrivia(a[0]),
10587
10932
  token: (...a) => token(a[0]),
10933
+ routed: () => routed(),
10588
10934
  leaf: (...a) => leaf(a[0], a[1]),
10589
10935
  expect: (...a) => expect(a[0], a[1])
10590
10936
  };
@@ -10628,6 +10974,18 @@ function dispatchArmValue(node2, scope, code, mfs) {
10628
10974
  if (node2.type !== "CallExpression") return null;
10629
10975
  const callee = node2.callee;
10630
10976
  if (callee.type !== "Identifier") return null;
10977
+ const factory = scope.get(callee.name);
10978
+ if (isWhenFactory(factory)) {
10979
+ const [keyArg, parserArg, extraArg] = node2.arguments;
10980
+ if (!keyArg || !parserArg || extraArg !== void 0 || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement") return null;
10981
+ const key = anyValue(keyArg, scope, code, mfs);
10982
+ const parserValue = anyValue(parserArg, scope, code, mfs);
10983
+ if (!isCombinator(parserValue)) return null;
10984
+ const opts = { caseInsensitive: factory.caseInsensitive };
10985
+ if (typeof key === "string") return when(key, parserValue, opts);
10986
+ if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue, opts);
10987
+ return null;
10988
+ }
10631
10989
  if (callee.name === "otherwise") {
10632
10990
  const [parserArg] = node2.arguments;
10633
10991
  if (!parserArg || parserArg.type === "SpreadElement") return null;
@@ -10636,13 +10994,16 @@ function dispatchArmValue(node2, scope, code, mfs) {
10636
10994
  return otherwise(parserValue);
10637
10995
  }
10638
10996
  if (callee.name === "when") {
10639
- const [keyArg, parserArg] = node2.arguments;
10640
- if (!keyArg || !parserArg || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement") return null;
10997
+ const [keyArg, parserArg, optsArg] = node2.arguments;
10998
+ if (!keyArg || !parserArg || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement" || optsArg?.type === "SpreadElement") return null;
10641
10999
  const key = anyValue(keyArg, scope, code, mfs);
10642
11000
  const parserValue = anyValue(parserArg, scope, code, mfs);
11001
+ const opts = dispatchWhenOptions(optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs));
11002
+ if (opts === null) return null;
10643
11003
  if (!isCombinator(parserValue)) return null;
10644
- if (typeof key === "string") return when(key, parserValue);
10645
- if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue);
11004
+ if (typeof key === "string") return when(key, parserValue, opts);
11005
+ if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue, opts);
11006
+ if (isDispatchMatcher(key)) return when(key, parserValue, opts);
10646
11007
  }
10647
11008
  return null;
10648
11009
  }
@@ -10653,7 +11014,16 @@ function isDispatchArm(v2) {
10653
11014
  if (!v2 || typeof v2 !== "object") return false;
10654
11015
  const rec = v2;
10655
11016
  if (rec.kind === "otherwise") return isCombinator(rec.parser);
10656
- return rec.kind === "when" && Array.isArray(rec.keys) && rec.keys.every((key) => typeof key === "string") && isCombinator(rec.parser);
11017
+ if (rec.kind === "whenMatcher") {
11018
+ return isDispatchMatcher(rec.matcher) && typeof rec.caseInsensitive === "boolean" && isCombinator(rec.parser);
11019
+ }
11020
+ return rec.kind === "when" && Array.isArray(rec.keys) && rec.keys.every((key) => typeof key === "string") && typeof rec.caseInsensitive === "boolean" && isCombinator(rec.parser);
11021
+ }
11022
+ function isDispatchMatcher(v2) {
11023
+ if (!v2 || typeof v2 !== "object") return false;
11024
+ const rec = v2;
11025
+ if ((rec.kind === "startsWith" || rec.kind === "endsWith") && typeof rec.value === "string") return true;
11026
+ return rec.kind === "matches" && typeof rec.value === "string" && typeof rec.flags === "string";
10657
11027
  }
10658
11028
  function scopeGet(scope, name, mfs) {
10659
11029
  const entry = scope.get(name);
@@ -11039,9 +11409,27 @@ function anyValue(node2, scope, code, mfs) {
11039
11409
  if (callee.type === "Identifier" && callee.name === "makeWord") {
11040
11410
  return wordFactoryFromArgs(node2.arguments, scope, code, mfs);
11041
11411
  }
11412
+ if (callee.type === "Identifier" && callee.name === "makeWhen") {
11413
+ return whenFactoryFromArgs(node2.arguments, scope, code, mfs);
11414
+ }
11415
+ if (callee.type === "Identifier" && (callee.name === "startsWith" || callee.name === "endsWith")) {
11416
+ if (node2.arguments.length !== 1 || node2.arguments[0]?.type === "SpreadElement") return null;
11417
+ const value = anyValue(node2.arguments[0], scope, code, mfs);
11418
+ if (typeof value !== "string") return null;
11419
+ return callee.name === "startsWith" ? startsWith(value) : endsWith(value);
11420
+ }
11421
+ if (callee.type === "Identifier" && callee.name === "matches") {
11422
+ if (node2.arguments.length !== 1 || node2.arguments[0]?.type === "SpreadElement") return null;
11423
+ const value = anyValue(node2.arguments[0], scope, code, mfs);
11424
+ if (!(value instanceof RegExp)) return null;
11425
+ return matches(value);
11426
+ }
11042
11427
  if (callee.type === "Identifier" && (callee.name === "when" || callee.name === "otherwise")) {
11043
11428
  return dispatchArmValue(node2, scope, code, mfs);
11044
11429
  }
11430
+ if (callee.type === "Identifier" && isWhenFactory(scope.get(callee.name))) {
11431
+ return dispatchArmValue(node2, scope, code, mfs);
11432
+ }
11045
11433
  return exprToCombi(node2, scope, code, mfs);
11046
11434
  }
11047
11435
  return null;
@@ -11052,6 +11440,12 @@ function evaluateWordFactory(node2, scope, code) {
11052
11440
  if (callee.type !== "Identifier" || callee.name !== "makeWord") return null;
11053
11441
  return wordFactoryFromArgs(node2.arguments, scope, code);
11054
11442
  }
11443
+ function evaluateWhenFactory(node2, scope, code) {
11444
+ if (node2.type !== "CallExpression") return null;
11445
+ const callee = node2.callee;
11446
+ if (callee.type !== "Identifier" || callee.name !== "makeWhen") return null;
11447
+ return whenFactoryFromArgs(node2.arguments, scope, code);
11448
+ }
11055
11449
  function evaluateExpr(node2, scope, code, mapFnSources) {
11056
11450
  return exprToCombi(node2, scope, code, mapFnSources);
11057
11451
  }
@@ -11632,7 +12026,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
11632
12026
  for (const match of source.matchAll(/id:\s*"([^"]+)"/g)) ids.add(match[1]);
11633
12027
  return [...ids].sort().map((id2) => ({
11634
12028
  id: id2,
11635
- kind: id2.startsWith("rule:") ? "rule" : id2.startsWith("label:") ? "label" : "choice-arm"
12029
+ kind: id2.startsWith("rule:") ? "rule" : id2.startsWith("label:") ? "label" : id2.startsWith("dispatch:") ? "dispatch-arm" : "choice-arm"
11636
12030
  }));
11637
12031
  };
11638
12032
  const localComposedCarried = /* @__PURE__ */ new Map();
@@ -12078,6 +12472,14 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
12078
12472
  if (wordFactory) {
12079
12473
  ;
12080
12474
  scope.set(varName, wordFactory);
12475
+ replacements.push({ start: init.start, end: init.end, replacement: "undefined" });
12476
+ continue;
12477
+ }
12478
+ const whenFactory = evaluateWhenFactory(init, scope, code);
12479
+ if (whenFactory) {
12480
+ ;
12481
+ scope.set(varName, whenFactory);
12482
+ replacements.push({ start: init.start, end: init.end, replacement: "undefined" });
12081
12483
  continue;
12082
12484
  }
12083
12485
  const combiArray = evaluateCombinatorArray(init, scope, code);