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.
@@ -59,6 +59,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve2) {
59
59
  return d.value.length === 0;
60
60
  case "keywords":
61
61
  return false;
62
+ case "routed":
63
+ return false;
62
64
  case "regex":
63
65
  try {
64
66
  const m = new RegExp(d.source).exec("");
@@ -1151,6 +1153,8 @@ function derive(c, seen) {
1151
1153
  case "not":
1152
1154
  case "peek":
1153
1155
  return deriveExpected2(def.parser);
1156
+ case "routed":
1157
+ return ["routed()"];
1154
1158
  case "lazy": {
1155
1159
  const name = c._ruleName;
1156
1160
  if (seen.has(c)) return name ? [name] : [];
@@ -1972,42 +1976,195 @@ function sequence(...parsers) {
1972
1976
  }
1973
1977
 
1974
1978
  // src/combinators/dispatch.ts
1975
- function when(key, parser2) {
1979
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
1980
+ if (seen.has(parser2)) return false;
1981
+ seen.add(parser2);
1982
+ const def = parser2._def;
1983
+ switch (def.tag) {
1984
+ case "routed":
1985
+ return true;
1986
+ case "sequence":
1987
+ case "choice":
1988
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
1989
+ case "dispatch":
1990
+ return false;
1991
+ case "sepBy":
1992
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
1993
+ case "skip":
1994
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
1995
+ case "grammar":
1996
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
1997
+ case "scanTo":
1998
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
1999
+ case "recover":
2000
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2001
+ case "lazy":
2002
+ try {
2003
+ return parserUsesRouted(def.thunk(), seen);
2004
+ } catch {
2005
+ return false;
2006
+ }
2007
+ case "many":
2008
+ case "oneOrMore":
2009
+ case "optional":
2010
+ case "attempt":
2011
+ case "transform":
2012
+ case "trivia":
2013
+ case "token":
2014
+ case "leaf":
2015
+ case "label":
2016
+ case "field":
2017
+ case "not":
2018
+ case "peek":
2019
+ case "node":
2020
+ case "withCtx":
2021
+ case "expect":
2022
+ return parserUsesRouted(def.parser, seen);
2023
+ default:
2024
+ return false;
2025
+ }
2026
+ }
2027
+ function branchUsesRouted(branch) {
2028
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2029
+ }
2030
+ function asciiFoldKey(key) {
2031
+ let out = "";
2032
+ for (let i = 0; i < key.length; i++) {
2033
+ const c = key.charCodeAt(i);
2034
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2035
+ }
2036
+ return out;
2037
+ }
2038
+ function matchesDispatchMatcher(value, matcher) {
2039
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2040
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2041
+ switch (matcher.kind) {
2042
+ case "startsWith":
2043
+ return candidate.startsWith(expected);
2044
+ case "endsWith":
2045
+ return candidate.endsWith(expected);
2046
+ case "matches": {
2047
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2048
+ return new RegExp(matcher.value, flags).test(value);
2049
+ }
2050
+ }
2051
+ }
2052
+ function isStringMatcher(value) {
2053
+ if (typeof value !== "object" || value === null) return false;
2054
+ const kind = value.kind;
2055
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2056
+ }
2057
+ function fixedMatcher(kind, value) {
2058
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2059
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2060
+ return { kind, value };
2061
+ }
2062
+ function startsWith(prefix) {
2063
+ return fixedMatcher("startsWith", prefix);
2064
+ }
2065
+ function endsWith(suffix) {
2066
+ return fixedMatcher("endsWith", suffix);
2067
+ }
2068
+ function matches(pattern) {
2069
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2070
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2071
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2072
+ }
2073
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2074
+ }
2075
+ function when(key, parser2, opts = {}) {
2076
+ const usesRouted = parserUsesRouted(parser2);
2077
+ if (isStringMatcher(key)) {
2078
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2079
+ }
1976
2080
  const keys = Array.isArray(key) ? [...key] : [key];
1977
2081
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
1978
2082
  for (const item of keys) {
1979
2083
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
1980
2084
  }
1981
- return { kind: "when", keys, parser: parser2 };
2085
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
1982
2086
  }
1983
2087
  function otherwise(parser2) {
1984
- return { kind: "otherwise", parser: parser2 };
2088
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2089
+ }
2090
+ function routed() {
2091
+ return {
2092
+ _tag: "routed",
2093
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2094
+ _def: { tag: "routed" },
2095
+ parse(_input, pos, ctx) {
2096
+ const item = ctx._routed;
2097
+ if (item === void 0) {
2098
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2099
+ }
2100
+ if (pos !== item.span.start) {
2101
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2102
+ }
2103
+ if (cstCaptureActive(ctx)) {
2104
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2105
+ }
2106
+ return { ok: true, value: item.value, span: item.span };
2107
+ }
2108
+ };
1985
2109
  }
1986
2110
  function dispatch(selector, ...arms) {
2111
+ if (parserUsesRouted(selector)) {
2112
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2113
+ }
1987
2114
  let fallback;
2115
+ let fallbackUsesRouted = false;
2116
+ const matchers = [];
1988
2117
  const cases = [];
1989
- const seen = /* @__PURE__ */ new Set();
2118
+ const seen = [];
1990
2119
  for (let i = 0; i < arms.length; i++) {
1991
2120
  const arm = arms[i];
1992
2121
  if (arm.kind === "otherwise") {
1993
2122
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
1994
2123
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
1995
2124
  fallback = arm.parser;
2125
+ fallbackUsesRouted = arm.usesRouted === true;
1996
2126
  continue;
1997
2127
  }
1998
- for (const key of arm.keys) {
1999
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2000
- seen.add(key);
2128
+ if (arm.kind === "whenMatcher") {
2129
+ const matcher = arm.matcher;
2130
+ matchers.push({
2131
+ kind: matcher.kind,
2132
+ value: matcher.value,
2133
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2134
+ parser: arm.parser,
2135
+ caseInsensitive: arm.caseInsensitive,
2136
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2137
+ });
2138
+ continue;
2001
2139
  }
2002
- cases.push({ keys: arm.keys, parser: arm.parser });
2140
+ for (const key of arm.keys) {
2141
+ const folded = asciiFoldKey(key);
2142
+ for (const prior of seen) {
2143
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2144
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2145
+ }
2146
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2147
+ }
2148
+ cases.push({
2149
+ keys: arm.keys,
2150
+ parser: arm.parser,
2151
+ caseInsensitive: arm.caseInsensitive,
2152
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2153
+ });
2003
2154
  }
2004
2155
  const meta = {
2005
2156
  firstSet: selector._meta.firstSet,
2006
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2157
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2007
2158
  isTrivia: false
2008
2159
  };
2009
2160
  const byKey = /* @__PURE__ */ new Map();
2010
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2161
+ const byFoldedKey = /* @__PURE__ */ new Map();
2162
+ for (const entry of cases) {
2163
+ for (const key of entry.keys) {
2164
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2165
+ else byKey.set(key, entry);
2166
+ }
2167
+ }
2011
2168
  return {
2012
2169
  _tag: "dispatch",
2013
2170
  _meta: meta,
@@ -2015,18 +2172,35 @@ function dispatch(selector, ...arms) {
2015
2172
  tag: "dispatch",
2016
2173
  selector,
2017
2174
  cases,
2018
- ...fallback === void 0 ? {} : { otherwise: fallback }
2175
+ ...matchers.length === 0 ? {} : { matchers },
2176
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2019
2177
  },
2020
2178
  parse(input, pos, ctx) {
2179
+ if (parserUsesRouted(selector)) {
2180
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2181
+ }
2182
+ const selectorMark = saveTriviaMark(ctx);
2021
2183
  const selected = selector.parse(input, pos, ctx);
2022
2184
  if (!selected.ok) return selected;
2023
- const tail = byKey.get(selected.value) ?? fallback;
2024
- if (tail === void 0) {
2185
+ 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 });
2186
+ if (branch === void 0) {
2025
2187
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2026
2188
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2027
2189
  }
2028
- const mark = saveTriviaMark(ctx);
2029
- const result = tail.parse(input, selected.span.end, ctx);
2190
+ const savedRouted = ctx._routed;
2191
+ const usesRouted = branchUsesRouted(branch);
2192
+ let mark = saveTriviaMark(ctx);
2193
+ if (usesRouted) {
2194
+ rollbackTrivia(ctx, selectorMark);
2195
+ mark = saveTriviaMark(ctx);
2196
+ ctx._routed = { value: selected.value, span: selected.span };
2197
+ }
2198
+ let result;
2199
+ try {
2200
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2201
+ } finally {
2202
+ if (usesRouted) ctx._routed = savedRouted;
2203
+ }
2030
2204
  if (!result.ok) {
2031
2205
  rollbackTrivia(ctx, mark);
2032
2206
  return { ...result, committed: true };
@@ -2376,6 +2550,7 @@ function markUnusedValues(root) {
2376
2550
  case "dispatch":
2377
2551
  visit(def.selector, consumed);
2378
2552
  for (const entry of def.cases) visit(entry.parser, consumed);
2553
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
2379
2554
  if (def.otherwise) visit(def.otherwise, consumed);
2380
2555
  return;
2381
2556
  case "node":
@@ -2654,7 +2829,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2654
2829
  case "choice":
2655
2830
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2656
2831
  case "dispatch":
2657
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2832
+ 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);
2658
2833
  case "sepBy":
2659
2834
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2660
2835
  case "skip":
@@ -2723,7 +2898,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2723
2898
  case "choice":
2724
2899
  return d.parsers.some((x) => parserHasTriviaSite(x, seen));
2725
2900
  case "dispatch":
2726
- return parserHasTriviaSite(d.selector, seen) || d.cases.some((x) => parserHasTriviaSite(x.parser, seen)) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false);
2901
+ 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);
2727
2902
  case "grammar":
2728
2903
  return parserHasTriviaSite(d.parser, seen);
2729
2904
  case "lazy": {
@@ -2750,7 +2925,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2750
2925
  case "choice":
2751
2926
  return d.parsers.some((x) => parserEnablesTriviaCapture(x, seen));
2752
2927
  case "dispatch":
2753
- return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some((x) => parserEnablesTriviaCapture(x.parser, seen)) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false);
2928
+ 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);
2754
2929
  case "sepBy":
2755
2930
  return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen);
2756
2931
  case "skip":
@@ -3125,7 +3300,7 @@ function leaf(root, fn) {
3125
3300
  }
3126
3301
 
3127
3302
  // src/version.ts
3128
- var PARSEMAN_VERSION = "0.39.0";
3303
+ var PARSEMAN_VERSION = "0.39.1";
3129
3304
 
3130
3305
  // src/cst/host-mode.ts
3131
3306
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3385,6 +3560,7 @@ function analyzeGatingRules(ruleMap, opts) {
3385
3560
  if (d.tag === "dispatch") {
3386
3561
  kids.push(d.selector);
3387
3562
  for (const c of d.cases) kids.push(c.parser);
3563
+ if (d.matchers) for (const c of d.matchers) kids.push(c.parser);
3388
3564
  if (d.otherwise !== void 0) kids.push(d.otherwise);
3389
3565
  }
3390
3566
  if (Array.isArray(rec.skip)) kids.push(...rec.skip);
@@ -4908,13 +5084,25 @@ function escapeClassChar(ch2) {
4908
5084
  }
4909
5085
 
4910
5086
  // src/compiler/grammar-coverage-ids.ts
5087
+ function idPart(value) {
5088
+ return encodeURIComponent(value);
5089
+ }
5090
+ function dispatchArmIds(def, path2) {
5091
+ const ids = [];
5092
+ for (const entry of def.cases) ids.push(`dispatch:${path2}/when:${entry.keys.map(idPart).join("|")}`);
5093
+ for (const entry of def.matchers ?? []) {
5094
+ ids.push(`dispatch:${path2}/matcher:${entry.kind}:${idPart(entry.value)}${entry.kind === "matches" && entry.flags ? `:${idPart(entry.flags)}` : ""}`);
5095
+ }
5096
+ if (def.otherwise) ids.push(`dispatch:${path2}/otherwise`);
5097
+ return ids;
5098
+ }
4911
5099
  function children(def, winners) {
4912
5100
  switch (def.tag) {
4913
5101
  case "sequence":
4914
5102
  case "choice":
4915
5103
  return def.parsers;
4916
5104
  case "dispatch":
4917
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
5105
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
4918
5106
  case "many":
4919
5107
  case "oneOrMore":
4920
5108
  case "optional":
@@ -4957,6 +5145,7 @@ function children(def, winners) {
4957
5145
  function buildGrammarPlan(entry, winners) {
4958
5146
  const definitions = /* @__PURE__ */ new Map();
4959
5147
  const choices = /* @__PURE__ */ new WeakMap();
5148
+ const dispatches = /* @__PURE__ */ new WeakMap();
4960
5149
  const attempts = /* @__PURE__ */ new WeakMap();
4961
5150
  const labels = /* @__PURE__ */ new WeakMap();
4962
5151
  const rules2 = /* @__PURE__ */ new WeakMap();
@@ -4987,6 +5176,11 @@ function buildGrammarPlan(entry, winners) {
4987
5176
  choices.set(parser2, ids);
4988
5177
  ids.forEach((id) => definitions.set(id, { id, kind: "choice-arm" }));
4989
5178
  }
5179
+ if (parser2._def.tag === "dispatch") {
5180
+ const ids = dispatchArmIds(parser2._def, path2);
5181
+ dispatches.set(parser2, ids);
5182
+ ids.forEach((id) => definitions.set(id, { id, kind: "dispatch-arm" }));
5183
+ }
4990
5184
  if (parser2._def.tag === "attempt") attempts.set(parser2, `attempt:${path2}`);
4991
5185
  if (parser2._def.tag === "label") {
4992
5186
  const id = `label:${path2}`;
@@ -4999,7 +5193,7 @@ function buildGrammarPlan(entry, winners) {
4999
5193
  for (const root of roots) {
5000
5194
  visit(root, winnerNames.get(root) ?? root._ruleName ?? "entry");
5001
5195
  }
5002
- return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, attempts, labels, rules: rules2 };
5196
+ return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules: rules2 };
5003
5197
  }
5004
5198
 
5005
5199
  // src/compiler/scannable-run.ts
@@ -6373,6 +6567,7 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
6373
6567
  case "literal":
6374
6568
  case "regex":
6375
6569
  case "keywords":
6570
+ case "routed":
6376
6571
  return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
6377
6572
  case "choice": {
6378
6573
  const alts = [];
@@ -6731,7 +6926,7 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
6731
6926
  case "choice":
6732
6927
  return false;
6733
6928
  case "dispatch":
6734
- 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);
6929
+ 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);
6735
6930
  case "attempt":
6736
6931
  return false;
6737
6932
  // optional never fails; many/oneOrMore only "fail" with zero captured items.
@@ -6830,6 +7025,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6830
7025
  case "keywords":
6831
7026
  case "node":
6832
7027
  case "token":
7028
+ case "routed":
6833
7029
  case "leaf":
6834
7030
  return true;
6835
7031
  case "not":
@@ -6842,7 +7038,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6842
7038
  case "choice":
6843
7039
  return d.parsers.some((x) => capturesLeaf(x, seen));
6844
7040
  case "dispatch":
6845
- return capturesLeaf(d.selector, seen) || d.cases.some((x) => capturesLeaf(x.parser, seen)) || (d.otherwise ? capturesLeaf(d.otherwise, seen) : false);
7041
+ 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);
6846
7042
  case "sepBy":
6847
7043
  return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen);
6848
7044
  case "many":
@@ -7320,6 +7516,7 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7320
7516
  for (const item of d.cases) {
7321
7517
  if (mayRecordRecoveryError(item.parser, recovery, seen)) return true;
7322
7518
  }
7519
+ if (d.matchers?.some((item) => mayRecordRecoveryError(item.parser, recovery, seen))) return true;
7323
7520
  return d.otherwise ? mayRecordRecoveryError(d.otherwise, recovery, seen) : false;
7324
7521
  }
7325
7522
  case "attempt":
@@ -7354,53 +7551,120 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7354
7551
  return false;
7355
7552
  }
7356
7553
  }
7357
- function emitDispatchCombinator(def, ctx, pos) {
7554
+ function emitDispatchCombinator(parser2, def, ctx, pos) {
7555
+ if (parserUsesRouted2(def.selector)) {
7556
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
7557
+ }
7558
+ const coverageIds = ctx.coverage?.plan.dispatches.get(parser2) ?? [];
7559
+ const branchUsesRouted2 = (entry) => entry.usesRouted === true || parserUsesRouted2(entry.parser);
7560
+ const hasRoutedBranch = def.cases.some(branchUsesRouted2) || (def.matchers ? def.matchers.some(branchUsesRouted2) : false) || (def.otherwise ? def.otherwiseUsesRouted === true || parserUsesRouted2(def.otherwise) : false);
7561
+ const selectorLeafMark = hasRoutedBranch ? v(ctx, "_dsl") : null;
7562
+ const selectorRawMark = hasRoutedBranch ? v(ctx, "_dsr") : null;
7563
+ const selectorTriviaMark = hasRoutedBranch ? v(ctx, "_dst") : null;
7564
+ const selectorLogMark = hasRoutedBranch ? v(ctx, "_dsg") : null;
7565
+ const selectorFieldMark = hasRoutedBranch ? v(ctx, "_dsf") : null;
7566
+ const selectorErrorMark = hasRoutedBranch ? v(ctx, "_dse") : null;
7358
7567
  const selector = emit(def.selector, ctx, pos);
7359
7568
  const outV = v(ctx, "_dval");
7360
7569
  const outE = v(ctx, "_dend");
7361
7570
  const keyV = v(ctx, "_dkey");
7362
7571
  const stmts = [
7572
+ ...selectorLeafMark ? [
7573
+ `${ind(ctx)}const ${selectorLeafMark} = _ctx._cstLeaves?.length ?? 0`,
7574
+ `${ind(ctx)}const ${selectorRawMark} = _ctx._cstRawChildren?.length ?? 0`,
7575
+ `${ind(ctx)}const ${selectorTriviaMark} = _ctx._cstTriviaLog?.length ?? 0`,
7576
+ `${ind(ctx)}const ${selectorLogMark} = _ctx._triviaLog?.length ?? 0`,
7577
+ `${ind(ctx)}const ${selectorFieldMark} = _ctx._fields?.length ?? 0`,
7578
+ `${ind(ctx)}const ${selectorErrorMark} = _ctx._errors?.length ?? 0`
7579
+ ] : [],
7363
7580
  ...selector.stmts,
7364
7581
  `${ind(ctx)}const ${keyV} = ${selector.valueVar}`,
7365
7582
  `${ind(ctx)}let ${outV}, ${outE} = ${selector.endVar}`
7366
7583
  ];
7367
7584
  let wroteBranch = false;
7368
- const emitSelectedTail = (parser2, keyword, condition) => {
7585
+ const emitSelectedTail = (entry, keyword, condition, coverageId) => {
7586
+ const parser3 = entry.parser;
7369
7587
  const head = keyword === "else" ? `${ind(ctx)}else {` : `${ind(ctx)}${keyword} (${condition}) {`;
7370
7588
  stmts.push(head);
7371
7589
  ctx.indent++;
7372
- const mayError = mayRecordRecoveryError(parser2, !!ctx.recovery);
7590
+ const usesRouted = branchUsesRouted2(entry);
7591
+ const routedV = usesRouted ? v(ctx, "_drt") : null;
7592
+ const mayError = mayRecordRecoveryError(parser3, !!ctx.recovery);
7373
7593
  const errMark = mayError ? v(ctx, "_derr") : null;
7374
7594
  const logMark = ctx.activeTrivia ? v(ctx, "_dlog") : null;
7595
+ if (routedV) {
7596
+ stmts.push(
7597
+ `${ind(ctx)}const ${routedV} = _ctx._routed; _ctx._routed = { value: ${selector.valueVar}, span: { start: ${pos}, end: ${selector.endVar} } }`,
7598
+ ...selectorLeafMark ? [
7599
+ `${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}`
7600
+ ] : []
7601
+ );
7602
+ }
7375
7603
  if (errMark) stmts.push(`${ind(ctx)}const ${errMark} = _ctx._errors?.length ?? 0`);
7376
7604
  if (logMark) stmts.push(`${ind(ctx)}const ${logMark} = _ctx._triviaLog?.length ?? 0`);
7377
- const tail = emitTailFallible(parser2, ctx, selector.endVar);
7605
+ const tail = emitTailFallible(parser3, ctx, usesRouted ? pos : selector.endVar);
7606
+ stmts.push(...coverageAttempt(ctx, coverageId, pos));
7378
7607
  const errorRollback = errMark ? `if (_ctx._errors && _ctx._errors.length !== ${errMark}) _ctx._errors.length = ${errMark}; ` : "";
7379
7608
  const logRollback = logMark ? `if (_ctx._triviaLog && _ctx._triviaLog.length !== ${logMark}) _ctx._triviaLog.length = ${logMark}; ` : "";
7609
+ const routedRollback = routedV ? `_ctx._routed = ${routedV}; ` : "";
7610
+ const coverageFailure = coverageId === void 0 ? "" : `_ctx._grammarTrace?.write({ id: ${JSON.stringify(coverageId)}, phase: 'failure', offset: _ctx._fe }); `;
7380
7611
  stmts.push(
7381
7612
  ...tail.stmts,
7382
- `${ind(ctx)}if (!${tail.okVar}) { ${errorRollback}${logRollback}${committedFailBody(ctx)} }`,
7613
+ `${ind(ctx)}if (!${tail.okVar}) { ${routedRollback}${errorRollback}${logRollback}${coverageFailure}${committedFailBody(ctx)} }`,
7614
+ ...routedV ? [`${ind(ctx)}_ctx._routed = ${routedV}`] : [],
7383
7615
  `${ind(ctx)}${outV} = [${selector.valueVar}, ${tail.valVar}]`,
7384
- `${ind(ctx)}${outE} = ${tail.endVar}`
7616
+ `${ind(ctx)}${outE} = ${tail.endVar}`,
7617
+ ...coverageHit(ctx, coverageId, pos, tail.endVar)
7385
7618
  );
7386
7619
  ctx.indent--;
7387
7620
  stmts.push(`${ind(ctx)}}`);
7388
7621
  };
7622
+ let coverageIndex = 0;
7389
7623
  for (const entry of def.cases) {
7390
- const condition = entry.keys.map((key) => `${keyV} === ${JSON.stringify(key)}`).join(" || ");
7391
- emitSelectedTail(entry.parser, wroteBranch ? "else if" : "if", condition);
7624
+ const condition = entry.keys.map((key) => emitDispatchKeyCondition(keyV, key, entry.caseInsensitive)).join(" || ");
7625
+ emitSelectedTail(entry, wroteBranch ? "else if" : "if", condition, coverageIds[coverageIndex++]);
7392
7626
  wroteBranch = true;
7393
7627
  }
7628
+ const emitMatcherTails = () => {
7629
+ for (const matcher of def.matchers ?? []) {
7630
+ emitSelectedTail(matcher, wroteBranch ? "else if" : "if", emitDispatchMatcherCondition(keyV, matcher), coverageIds[coverageIndex++]);
7631
+ wroteBranch = true;
7632
+ }
7633
+ };
7394
7634
  if (def.otherwise) {
7395
- if (wroteBranch) emitSelectedTail(def.otherwise, "else");
7396
- else emitSelectedTail(def.otherwise, "if", "true");
7635
+ emitMatcherTails();
7636
+ const otherwiseEntry = { parser: def.otherwise, usesRouted: def.otherwiseUsesRouted };
7637
+ if (wroteBranch) emitSelectedTail(otherwiseEntry, "else", void 0, coverageIds[coverageIndex++]);
7638
+ else emitSelectedTail(otherwiseEntry, "if", "true", coverageIds[coverageIndex++]);
7397
7639
  } else {
7640
+ emitMatcherTails();
7398
7641
  const expected = JSON.stringify(def.cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key))));
7399
7642
  const fail = failArrBody(ctx, expected, selector.endVar);
7400
7643
  stmts.push(wroteBranch ? `${ind(ctx)}else { ${fail} }` : `${ind(ctx)}${fail}`);
7401
7644
  }
7402
7645
  return { stmts, valueVar: outV, endVar: outE };
7403
7646
  }
7647
+ function emitDispatchKeyCondition(valueVar, key, caseInsensitive) {
7648
+ if (!caseInsensitive) return `${valueVar} === ${JSON.stringify(key)}`;
7649
+ const parts = [`${valueVar}.length === ${key.length}`];
7650
+ for (let i = 0; i < key.length; i++) {
7651
+ parts.push(foldEq(`${valueVar}.charCodeAt(${i})`, key.charCodeAt(i)));
7652
+ }
7653
+ return parts.join(" && ");
7654
+ }
7655
+ function emitDispatchMatcherCondition(valueVar, matcher) {
7656
+ if (matcher.kind === "matches") {
7657
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
7658
+ return `${new RegExp(matcher.value, flags).toString()}.test(${valueVar})`;
7659
+ }
7660
+ const text = matcher.value;
7661
+ const parts = [`${valueVar}.length >= ${text.length}`];
7662
+ const offset = matcher.kind === "endsWith" ? `${valueVar}.length - ${text.length}` : "0";
7663
+ for (let i = 0; i < text.length; i++) {
7664
+ parts.push(matcher.caseInsensitive ? foldEq(`${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`})`, text.charCodeAt(i)) : `${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`}) === ${text.charCodeAt(i)}`);
7665
+ }
7666
+ return parts.join(" && ");
7667
+ }
7404
7668
  function emitGreedyClassify(def, superIndex, ctx, pos, coverageBase) {
7405
7669
  const superParser = def.parsers[superIndex];
7406
7670
  const regexDef = getCoreRegexDef(superParser);
@@ -8055,6 +8319,18 @@ function emitPeek(def, ctx, pos) {
8055
8319
  endVar: pos
8056
8320
  };
8057
8321
  }
8322
+ function emitRouted(ctx, pos) {
8323
+ const item = v(ctx, "_rt");
8324
+ return {
8325
+ stmts: [
8326
+ `${ind(ctx)}const ${item} = _ctx._routed`,
8327
+ ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
8328
+ ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
8329
+ ],
8330
+ valueVar: `${item}.value`,
8331
+ endVar: `${item}.span.end`
8332
+ };
8333
+ }
8058
8334
  function escapeTokenLiteral(value) {
8059
8335
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
8060
8336
  }
@@ -8520,7 +8796,7 @@ function emitDispatch(p, ctx, pos) {
8520
8796
  case "choice":
8521
8797
  return emitChoice(p, def, ctx, pos);
8522
8798
  case "dispatch":
8523
- return emitDispatchCombinator(def, ctx, pos);
8799
+ return emitDispatchCombinator(p, def, ctx, pos);
8524
8800
  case "attempt":
8525
8801
  return emitAttempt(p, def, ctx, pos);
8526
8802
  case "many":
@@ -8645,6 +8921,8 @@ function emitDispatch(p, ctx, pos) {
8645
8921
  return emitNot(def, ctx, pos);
8646
8922
  case "peek":
8647
8923
  return emitPeek(def, ctx, pos);
8924
+ case "routed":
8925
+ return emitRouted(ctx, pos);
8648
8926
  case "node":
8649
8927
  return emitNode(def, ctx, pos);
8650
8928
  case "scanTo":
@@ -8742,7 +9020,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8742
9020
  case "choice":
8743
9021
  return d.parsers.some((x) => hasNodeDef(x, seen));
8744
9022
  case "dispatch":
8745
- return hasNodeDef(d.selector, seen) || d.cases.some((x) => hasNodeDef(x.parser, seen)) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false);
9023
+ 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);
8746
9024
  case "sepBy":
8747
9025
  return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen);
8748
9026
  case "scanTo":
@@ -8757,6 +9035,26 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8757
9035
  return false;
8758
9036
  }
8759
9037
  }
9038
+ function parserUsesRouted2(p, seen = /* @__PURE__ */ new Set()) {
9039
+ if (seen.has(p)) return false;
9040
+ seen.add(p);
9041
+ const d = p._def;
9042
+ switch (d.tag) {
9043
+ case "routed":
9044
+ return true;
9045
+ case "lazy": {
9046
+ try {
9047
+ return parserUsesRouted2(d.thunk(), seen);
9048
+ } catch {
9049
+ return false;
9050
+ }
9051
+ }
9052
+ case "dispatch":
9053
+ return false;
9054
+ default:
9055
+ return childrenOf2(d).some((child) => parserUsesRouted2(child, seen));
9056
+ }
9057
+ }
8760
9058
  function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
8761
9059
  if (seen.has(p)) return false;
8762
9060
  seen.add(p);
@@ -8777,7 +9075,7 @@ function childrenOf2(def) {
8777
9075
  case "choice":
8778
9076
  return def.parsers;
8779
9077
  case "dispatch":
8780
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9078
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
8781
9079
  case "many":
8782
9080
  case "attempt":
8783
9081
  case "optional":
@@ -8814,6 +9112,7 @@ function childrenOf2(def) {
8814
9112
  case "regex":
8815
9113
  case "keywords":
8816
9114
  case "guard":
9115
+ case "routed":
8817
9116
  case "unknown":
8818
9117
  return [];
8819
9118
  }
@@ -9647,7 +9946,7 @@ function childrenOf3(def) {
9647
9946
  case "choice":
9648
9947
  return def.parsers;
9649
9948
  case "dispatch":
9650
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9949
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
9651
9950
  case "many":
9652
9951
  case "oneOrMore":
9653
9952
  case "optional":
@@ -9676,6 +9975,7 @@ function childrenOf3(def) {
9676
9975
  case "literal":
9677
9976
  case "regex":
9678
9977
  case "keywords":
9978
+ case "routed":
9679
9979
  case "guard":
9680
9980
  case "recover":
9681
9981
  case "unknown":
@@ -9700,6 +10000,16 @@ function repeatOpts(min, max, trailing) {
9700
10000
  if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
9701
10001
  return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
9702
10002
  }
10003
+ function matcherExpr(entry) {
10004
+ switch (entry.kind) {
10005
+ case "startsWith":
10006
+ return `startsWith(${JSON.stringify(entry.value)})`;
10007
+ case "endsWith":
10008
+ return `endsWith(${JSON.stringify(entry.value)})`;
10009
+ case "matches":
10010
+ return `matches(${new RegExp(entry.value, entry.flags ?? "").toString()})`;
10011
+ }
10012
+ }
9703
10013
  function serializeRuleMap(ruleMap, scanSkip) {
9704
10014
  try {
9705
10015
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -9799,7 +10109,11 @@ function evalRuleMapIR(ir) {
9799
10109
  "choice",
9800
10110
  "dispatch",
9801
10111
  "when",
10112
+ "startsWith",
10113
+ "endsWith",
10114
+ "matches",
9802
10115
  "otherwise",
10116
+ "routed",
9803
10117
  "attempt",
9804
10118
  "many",
9805
10119
  "oneOrMore",
@@ -9835,7 +10149,11 @@ function evalRuleMapIR(ir) {
9835
10149
  choice,
9836
10150
  dispatch,
9837
10151
  when,
10152
+ startsWith,
10153
+ endsWith,
10154
+ matches,
9838
10155
  otherwise,
10156
+ routed,
9839
10157
  attempt,
9840
10158
  many,
9841
10159
  oneOrMore,
@@ -10023,8 +10341,13 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10023
10341
  }
10024
10342
  case "dispatch": {
10025
10343
  const arms = def.cases.map(
10026
- (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)})`
10344
+ (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`
10027
10345
  );
10346
+ if (def.matchers) {
10347
+ for (const entry of def.matchers) {
10348
+ arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`);
10349
+ }
10350
+ }
10028
10351
  if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`);
10029
10352
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? "" : `, ${arms.join(", ")}`})`;
10030
10353
  }
@@ -10044,6 +10367,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10044
10367
  return `not(${kid(def.parser)})`;
10045
10368
  case "peek":
10046
10369
  return `peek(${kid(def.parser)})`;
10370
+ case "routed":
10371
+ return "routed()";
10047
10372
  case "trivia":
10048
10373
  return `trivia(${kid(def.parser)})`;
10049
10374
  case "token":
@@ -10513,6 +10838,9 @@ function stripTsFromSource(node2, code) {
10513
10838
  function isWordFactory(v2) {
10514
10839
  return !!v2 && typeof v2 === "object" && v2.tag === "wordFactory";
10515
10840
  }
10841
+ function isWhenFactory(v2) {
10842
+ return !!v2 && typeof v2 === "object" && v2.tag === "whenFactory";
10843
+ }
10516
10844
  function isStaticValueEntry(v2) {
10517
10845
  return !!v2 && typeof v2 === "object" && "value" in v2 && "mfSrcs" in v2;
10518
10846
  }
@@ -10535,6 +10863,23 @@ function wordFactoryFromArgs(args, scope, code, mfs) {
10535
10863
  if (typeof caseInsensitive !== "boolean") return null;
10536
10864
  return { tag: "wordFactory", boundary: "_0-9A-Za-z", caseInsensitive };
10537
10865
  }
10866
+ function dispatchWhenOptions(v2) {
10867
+ if (v2 === void 0) return {};
10868
+ if (typeof v2 !== "object" || v2 === null || Array.isArray(v2)) return null;
10869
+ for (const key of Object.keys(v2)) {
10870
+ if (key !== "caseInsensitive") return null;
10871
+ }
10872
+ const caseInsensitive = "caseInsensitive" in v2 ? v2.caseInsensitive : false;
10873
+ if (typeof caseInsensitive !== "boolean") return null;
10874
+ return { caseInsensitive };
10875
+ }
10876
+ function whenFactoryFromArgs(args, scope, code, mfs) {
10877
+ const [optsArg, extra] = args;
10878
+ if (optsArg?.type === "SpreadElement" || extra !== void 0) return null;
10879
+ const rawOpts = optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs);
10880
+ const opts = dispatchWhenOptions(rawOpts);
10881
+ return opts === null ? null : { tag: "whenFactory", caseInsensitive: opts.caseInsensitive ?? false };
10882
+ }
10538
10883
  var SUPPORTED = {
10539
10884
  literal: (...a) => literal(a[0], a[1]),
10540
10885
  regex: (...a) => regex(a[0], a[1]),
@@ -10550,6 +10895,7 @@ var SUPPORTED = {
10550
10895
  field: (...a) => field(a[0], a[1]),
10551
10896
  noTrivia: (...a) => noTrivia(a[0]),
10552
10897
  token: (...a) => token(a[0]),
10898
+ routed: () => routed(),
10553
10899
  leaf: (...a) => leaf(a[0], a[1]),
10554
10900
  expect: (...a) => expect(a[0], a[1])
10555
10901
  };
@@ -10593,6 +10939,18 @@ function dispatchArmValue(node2, scope, code, mfs) {
10593
10939
  if (node2.type !== "CallExpression") return null;
10594
10940
  const callee = node2.callee;
10595
10941
  if (callee.type !== "Identifier") return null;
10942
+ const factory = scope.get(callee.name);
10943
+ if (isWhenFactory(factory)) {
10944
+ const [keyArg, parserArg, extraArg] = node2.arguments;
10945
+ if (!keyArg || !parserArg || extraArg !== void 0 || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement") return null;
10946
+ const key = anyValue(keyArg, scope, code, mfs);
10947
+ const parserValue = anyValue(parserArg, scope, code, mfs);
10948
+ if (!isCombinator(parserValue)) return null;
10949
+ const opts = { caseInsensitive: factory.caseInsensitive };
10950
+ if (typeof key === "string") return when(key, parserValue, opts);
10951
+ if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue, opts);
10952
+ return null;
10953
+ }
10596
10954
  if (callee.name === "otherwise") {
10597
10955
  const [parserArg] = node2.arguments;
10598
10956
  if (!parserArg || parserArg.type === "SpreadElement") return null;
@@ -10601,13 +10959,16 @@ function dispatchArmValue(node2, scope, code, mfs) {
10601
10959
  return otherwise(parserValue);
10602
10960
  }
10603
10961
  if (callee.name === "when") {
10604
- const [keyArg, parserArg] = node2.arguments;
10605
- if (!keyArg || !parserArg || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement") return null;
10962
+ const [keyArg, parserArg, optsArg] = node2.arguments;
10963
+ if (!keyArg || !parserArg || keyArg.type === "SpreadElement" || parserArg.type === "SpreadElement" || optsArg?.type === "SpreadElement") return null;
10606
10964
  const key = anyValue(keyArg, scope, code, mfs);
10607
10965
  const parserValue = anyValue(parserArg, scope, code, mfs);
10966
+ const opts = dispatchWhenOptions(optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs));
10967
+ if (opts === null) return null;
10608
10968
  if (!isCombinator(parserValue)) return null;
10609
- if (typeof key === "string") return when(key, parserValue);
10610
- if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue);
10969
+ if (typeof key === "string") return when(key, parserValue, opts);
10970
+ if (Array.isArray(key) && key.every((item) => typeof item === "string")) return when(key, parserValue, opts);
10971
+ if (isDispatchMatcher(key)) return when(key, parserValue, opts);
10611
10972
  }
10612
10973
  return null;
10613
10974
  }
@@ -10618,7 +10979,16 @@ function isDispatchArm(v2) {
10618
10979
  if (!v2 || typeof v2 !== "object") return false;
10619
10980
  const rec = v2;
10620
10981
  if (rec.kind === "otherwise") return isCombinator(rec.parser);
10621
- return rec.kind === "when" && Array.isArray(rec.keys) && rec.keys.every((key) => typeof key === "string") && isCombinator(rec.parser);
10982
+ if (rec.kind === "whenMatcher") {
10983
+ return isDispatchMatcher(rec.matcher) && typeof rec.caseInsensitive === "boolean" && isCombinator(rec.parser);
10984
+ }
10985
+ return rec.kind === "when" && Array.isArray(rec.keys) && rec.keys.every((key) => typeof key === "string") && typeof rec.caseInsensitive === "boolean" && isCombinator(rec.parser);
10986
+ }
10987
+ function isDispatchMatcher(v2) {
10988
+ if (!v2 || typeof v2 !== "object") return false;
10989
+ const rec = v2;
10990
+ if ((rec.kind === "startsWith" || rec.kind === "endsWith") && typeof rec.value === "string") return true;
10991
+ return rec.kind === "matches" && typeof rec.value === "string" && typeof rec.flags === "string";
10622
10992
  }
10623
10993
  function scopeGet(scope, name, mfs) {
10624
10994
  const entry = scope.get(name);
@@ -11004,9 +11374,27 @@ function anyValue(node2, scope, code, mfs) {
11004
11374
  if (callee.type === "Identifier" && callee.name === "makeWord") {
11005
11375
  return wordFactoryFromArgs(node2.arguments, scope, code, mfs);
11006
11376
  }
11377
+ if (callee.type === "Identifier" && callee.name === "makeWhen") {
11378
+ return whenFactoryFromArgs(node2.arguments, scope, code, mfs);
11379
+ }
11380
+ if (callee.type === "Identifier" && (callee.name === "startsWith" || callee.name === "endsWith")) {
11381
+ if (node2.arguments.length !== 1 || node2.arguments[0]?.type === "SpreadElement") return null;
11382
+ const value = anyValue(node2.arguments[0], scope, code, mfs);
11383
+ if (typeof value !== "string") return null;
11384
+ return callee.name === "startsWith" ? startsWith(value) : endsWith(value);
11385
+ }
11386
+ if (callee.type === "Identifier" && callee.name === "matches") {
11387
+ if (node2.arguments.length !== 1 || node2.arguments[0]?.type === "SpreadElement") return null;
11388
+ const value = anyValue(node2.arguments[0], scope, code, mfs);
11389
+ if (!(value instanceof RegExp)) return null;
11390
+ return matches(value);
11391
+ }
11007
11392
  if (callee.type === "Identifier" && (callee.name === "when" || callee.name === "otherwise")) {
11008
11393
  return dispatchArmValue(node2, scope, code, mfs);
11009
11394
  }
11395
+ if (callee.type === "Identifier" && isWhenFactory(scope.get(callee.name))) {
11396
+ return dispatchArmValue(node2, scope, code, mfs);
11397
+ }
11010
11398
  return exprToCombi(node2, scope, code, mfs);
11011
11399
  }
11012
11400
  return null;
@@ -11017,6 +11405,12 @@ function evaluateWordFactory(node2, scope, code) {
11017
11405
  if (callee.type !== "Identifier" || callee.name !== "makeWord") return null;
11018
11406
  return wordFactoryFromArgs(node2.arguments, scope, code);
11019
11407
  }
11408
+ function evaluateWhenFactory(node2, scope, code) {
11409
+ if (node2.type !== "CallExpression") return null;
11410
+ const callee = node2.callee;
11411
+ if (callee.type !== "Identifier" || callee.name !== "makeWhen") return null;
11412
+ return whenFactoryFromArgs(node2.arguments, scope, code);
11413
+ }
11020
11414
  function evaluateExpr(node2, scope, code, mapFnSources) {
11021
11415
  return exprToCombi(node2, scope, code, mapFnSources);
11022
11416
  }
@@ -11597,7 +11991,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
11597
11991
  for (const match of source.matchAll(/id:\s*"([^"]+)"/g)) ids.add(match[1]);
11598
11992
  return [...ids].sort().map((id2) => ({
11599
11993
  id: id2,
11600
- kind: id2.startsWith("rule:") ? "rule" : id2.startsWith("label:") ? "label" : "choice-arm"
11994
+ kind: id2.startsWith("rule:") ? "rule" : id2.startsWith("label:") ? "label" : id2.startsWith("dispatch:") ? "dispatch-arm" : "choice-arm"
11601
11995
  }));
11602
11996
  };
11603
11997
  const localComposedCarried = /* @__PURE__ */ new Map();
@@ -12043,6 +12437,14 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
12043
12437
  if (wordFactory) {
12044
12438
  ;
12045
12439
  scope.set(varName, wordFactory);
12440
+ replacements.push({ start: init.start, end: init.end, replacement: "undefined" });
12441
+ continue;
12442
+ }
12443
+ const whenFactory = evaluateWhenFactory(init, scope, code);
12444
+ if (whenFactory) {
12445
+ ;
12446
+ scope.set(varName, whenFactory);
12447
+ replacements.push({ start: init.start, end: init.end, replacement: "undefined" });
12046
12448
  continue;
12047
12449
  }
12048
12450
  const combiArray = evaluateCombinatorArray(init, scope, code);