parseman 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -57,6 +57,7 @@ __export(index_exports, {
57
57
  cstBuildHost: () => cstBuildHost,
58
58
  dispatch: () => dispatch,
59
59
  duplicationFindingCount: () => duplicationFindingCount,
60
+ endsWith: () => endsWith,
60
61
  expect: () => expect,
61
62
  extractCharClasses: () => extractCharClasses,
62
63
  field: () => field,
@@ -79,8 +80,10 @@ __export(index_exports, {
79
80
  lineBreaksIn: () => lineBreaksIn,
80
81
  lineStartWithin: () => lineStartWithin,
81
82
  literal: () => literal,
83
+ makeWhen: () => makeWhen,
82
84
  makeWord: () => makeWord,
83
85
  many: () => many,
86
+ matches: () => matches,
84
87
  noTrivia: () => noTrivia,
85
88
  node: () => node,
86
89
  not: () => not,
@@ -97,6 +100,7 @@ __export(index_exports, {
97
100
  regex: () => regex,
98
101
  relativize: () => relativize,
99
102
  relativizeCST: () => relativizeCST,
103
+ routed: () => routed,
100
104
  rules: () => rules,
101
105
  run: () => run,
102
106
  runWithGrammarCoverage: () => runWithGrammarCoverage,
@@ -106,6 +110,7 @@ __export(index_exports, {
106
110
  shiftAbsolute: () => shiftAbsolute,
107
111
  siteToString: () => siteToString,
108
112
  skip: () => skip,
113
+ startsWith: () => startsWith,
109
114
  token: () => token,
110
115
  transform: () => transform,
111
116
  trivia: () => trivia,
@@ -171,6 +176,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
171
176
  return d.value.length === 0;
172
177
  case "keywords":
173
178
  return false;
179
+ case "routed":
180
+ return false;
174
181
  case "regex":
175
182
  try {
176
183
  const m = new RegExp(d.source).exec("");
@@ -1232,6 +1239,8 @@ function derive(c, seen) {
1232
1239
  case "not":
1233
1240
  case "peek":
1234
1241
  return deriveExpected2(def.parser);
1242
+ case "routed":
1243
+ return ["routed()"];
1235
1244
  case "lazy": {
1236
1245
  const name = c._ruleName;
1237
1246
  if (seen.has(c)) return name ? [name] : [];
@@ -2056,42 +2065,198 @@ function sequence(...parsers) {
2056
2065
  }
2057
2066
 
2058
2067
  // src/combinators/dispatch.ts
2059
- function when(key, parser2) {
2068
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2069
+ if (seen.has(parser2)) return false;
2070
+ seen.add(parser2);
2071
+ const def = parser2._def;
2072
+ switch (def.tag) {
2073
+ case "routed":
2074
+ return true;
2075
+ case "sequence":
2076
+ case "choice":
2077
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2078
+ case "dispatch":
2079
+ return false;
2080
+ case "sepBy":
2081
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2082
+ case "skip":
2083
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2084
+ case "grammar":
2085
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2086
+ case "scanTo":
2087
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2088
+ case "recover":
2089
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2090
+ case "lazy":
2091
+ try {
2092
+ return parserUsesRouted(def.thunk(), seen);
2093
+ } catch {
2094
+ return false;
2095
+ }
2096
+ case "many":
2097
+ case "oneOrMore":
2098
+ case "optional":
2099
+ case "attempt":
2100
+ case "transform":
2101
+ case "trivia":
2102
+ case "token":
2103
+ case "leaf":
2104
+ case "label":
2105
+ case "field":
2106
+ case "not":
2107
+ case "peek":
2108
+ case "node":
2109
+ case "withCtx":
2110
+ case "expect":
2111
+ return parserUsesRouted(def.parser, seen);
2112
+ default:
2113
+ return false;
2114
+ }
2115
+ }
2116
+ function branchUsesRouted(branch) {
2117
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2118
+ }
2119
+ function asciiFoldKey(key) {
2120
+ let out = "";
2121
+ for (let i = 0; i < key.length; i++) {
2122
+ const c = key.charCodeAt(i);
2123
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2124
+ }
2125
+ return out;
2126
+ }
2127
+ function matchesDispatchMatcher(value, matcher) {
2128
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2129
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2130
+ switch (matcher.kind) {
2131
+ case "startsWith":
2132
+ return candidate.startsWith(expected);
2133
+ case "endsWith":
2134
+ return candidate.endsWith(expected);
2135
+ case "matches": {
2136
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2137
+ return new RegExp(matcher.value, flags).test(value);
2138
+ }
2139
+ }
2140
+ }
2141
+ function isStringMatcher(value) {
2142
+ if (typeof value !== "object" || value === null) return false;
2143
+ const kind = value.kind;
2144
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2145
+ }
2146
+ function fixedMatcher(kind, value) {
2147
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2148
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2149
+ return { kind, value };
2150
+ }
2151
+ function startsWith(prefix) {
2152
+ return fixedMatcher("startsWith", prefix);
2153
+ }
2154
+ function endsWith(suffix) {
2155
+ return fixedMatcher("endsWith", suffix);
2156
+ }
2157
+ function matches(pattern) {
2158
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2159
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2160
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2161
+ }
2162
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2163
+ }
2164
+ function when(key, parser2, opts = {}) {
2165
+ const usesRouted = parserUsesRouted(parser2);
2166
+ if (isStringMatcher(key)) {
2167
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2168
+ }
2060
2169
  const keys = Array.isArray(key) ? [...key] : [key];
2061
2170
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2062
2171
  for (const item of keys) {
2063
2172
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2064
2173
  }
2065
- return { kind: "when", keys, parser: parser2 };
2174
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2175
+ }
2176
+ function makeWhen(opts = {}) {
2177
+ return (key, parser2) => when(key, parser2, opts);
2066
2178
  }
2067
2179
  function otherwise(parser2) {
2068
- return { kind: "otherwise", parser: parser2 };
2180
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2181
+ }
2182
+ function routed() {
2183
+ return {
2184
+ _tag: "routed",
2185
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2186
+ _def: { tag: "routed" },
2187
+ parse(_input, pos, ctx) {
2188
+ const item = ctx._routed;
2189
+ if (item === void 0) {
2190
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2191
+ }
2192
+ if (pos !== item.span.start) {
2193
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2194
+ }
2195
+ if (cstCaptureActive(ctx)) {
2196
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2197
+ }
2198
+ return { ok: true, value: item.value, span: item.span };
2199
+ }
2200
+ };
2069
2201
  }
2070
2202
  function dispatch(selector, ...arms) {
2203
+ if (parserUsesRouted(selector)) {
2204
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2205
+ }
2071
2206
  let fallback;
2207
+ let fallbackUsesRouted = false;
2208
+ const matchers = [];
2072
2209
  const cases = [];
2073
- const seen = /* @__PURE__ */ new Set();
2210
+ const seen = [];
2074
2211
  for (let i = 0; i < arms.length; i++) {
2075
2212
  const arm = arms[i];
2076
2213
  if (arm.kind === "otherwise") {
2077
2214
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2078
2215
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2079
2216
  fallback = arm.parser;
2217
+ fallbackUsesRouted = arm.usesRouted === true;
2080
2218
  continue;
2081
2219
  }
2082
- for (const key of arm.keys) {
2083
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2084
- seen.add(key);
2220
+ if (arm.kind === "whenMatcher") {
2221
+ const matcher = arm.matcher;
2222
+ matchers.push({
2223
+ kind: matcher.kind,
2224
+ value: matcher.value,
2225
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2226
+ parser: arm.parser,
2227
+ caseInsensitive: arm.caseInsensitive,
2228
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2229
+ });
2230
+ continue;
2085
2231
  }
2086
- cases.push({ keys: arm.keys, parser: arm.parser });
2232
+ for (const key of arm.keys) {
2233
+ const folded = asciiFoldKey(key);
2234
+ for (const prior of seen) {
2235
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2236
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2237
+ }
2238
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2239
+ }
2240
+ cases.push({
2241
+ keys: arm.keys,
2242
+ parser: arm.parser,
2243
+ caseInsensitive: arm.caseInsensitive,
2244
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2245
+ });
2087
2246
  }
2088
2247
  const meta = {
2089
2248
  firstSet: selector._meta.firstSet,
2090
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2249
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2091
2250
  isTrivia: false
2092
2251
  };
2093
2252
  const byKey = /* @__PURE__ */ new Map();
2094
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2253
+ const byFoldedKey = /* @__PURE__ */ new Map();
2254
+ for (const entry of cases) {
2255
+ for (const key of entry.keys) {
2256
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2257
+ else byKey.set(key, entry);
2258
+ }
2259
+ }
2095
2260
  return {
2096
2261
  _tag: "dispatch",
2097
2262
  _meta: meta,
@@ -2099,18 +2264,35 @@ function dispatch(selector, ...arms) {
2099
2264
  tag: "dispatch",
2100
2265
  selector,
2101
2266
  cases,
2102
- ...fallback === void 0 ? {} : { otherwise: fallback }
2267
+ ...matchers.length === 0 ? {} : { matchers },
2268
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2103
2269
  },
2104
2270
  parse(input, pos, ctx) {
2271
+ if (parserUsesRouted(selector)) {
2272
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2273
+ }
2274
+ const selectorMark = saveTriviaMark(ctx);
2105
2275
  const selected = selector.parse(input, pos, ctx);
2106
2276
  if (!selected.ok) return selected;
2107
- const tail = byKey.get(selected.value) ?? fallback;
2108
- if (tail === void 0) {
2277
+ 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 });
2278
+ if (branch === void 0) {
2109
2279
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2110
2280
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2111
2281
  }
2112
- const mark = saveTriviaMark(ctx);
2113
- const result = tail.parse(input, selected.span.end, ctx);
2282
+ const savedRouted = ctx._routed;
2283
+ const usesRouted = branchUsesRouted(branch);
2284
+ let mark = saveTriviaMark(ctx);
2285
+ if (usesRouted) {
2286
+ rollbackTrivia(ctx, selectorMark);
2287
+ mark = saveTriviaMark(ctx);
2288
+ ctx._routed = { value: selected.value, span: selected.span };
2289
+ }
2290
+ let result;
2291
+ try {
2292
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2293
+ } finally {
2294
+ if (usesRouted) ctx._routed = savedRouted;
2295
+ }
2114
2296
  if (!result.ok) {
2115
2297
  rollbackTrivia(ctx, mark);
2116
2298
  return { ...result, committed: true };
@@ -2497,6 +2679,7 @@ function markUnusedValues(root) {
2497
2679
  case "dispatch":
2498
2680
  visit(def.selector, consumed);
2499
2681
  for (const entry of def.cases) visit(entry.parser, consumed);
2682
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
2500
2683
  if (def.otherwise) visit(def.otherwise, consumed);
2501
2684
  return;
2502
2685
  case "node":
@@ -2775,7 +2958,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2775
2958
  case "choice":
2776
2959
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2777
2960
  case "dispatch":
2778
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2961
+ 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);
2779
2962
  case "sepBy":
2780
2963
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2781
2964
  case "skip":
@@ -2844,7 +3027,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2844
3027
  case "choice":
2845
3028
  return d.parsers.some((x) => parserHasTriviaSite(x, seen));
2846
3029
  case "dispatch":
2847
- return parserHasTriviaSite(d.selector, seen) || d.cases.some((x) => parserHasTriviaSite(x.parser, seen)) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false);
3030
+ 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);
2848
3031
  case "grammar":
2849
3032
  return parserHasTriviaSite(d.parser, seen);
2850
3033
  case "lazy": {
@@ -2871,7 +3054,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2871
3054
  case "choice":
2872
3055
  return d.parsers.some((x) => parserEnablesTriviaCapture(x, seen));
2873
3056
  case "dispatch":
2874
- return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some((x) => parserEnablesTriviaCapture(x.parser, seen)) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false);
3057
+ 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);
2875
3058
  case "sepBy":
2876
3059
  return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen);
2877
3060
  case "skip":
@@ -2932,12 +3115,30 @@ function isCstChild(value) {
2932
3115
  function missingInferredType() {
2933
3116
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
2934
3117
  }
3118
+ function normalizeProject(project) {
3119
+ if (project === void 0) return void 0;
3120
+ if (!Number.isInteger(project) || project < 0) {
3121
+ throw new Error("node() project child must be a non-negative integer");
3122
+ }
3123
+ return project;
3124
+ }
3125
+ function projectChild(children2, project, type) {
3126
+ if (!(project in children2)) {
3127
+ throw new Error(`node(${JSON.stringify(type)}) project child ${project} was not captured`);
3128
+ }
3129
+ return unwrapChild(children2[project]);
3130
+ }
2935
3131
  function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2936
3132
  const hasExplicitType = typeof typeOrCombinator === "string";
2937
3133
  const type = hasExplicitType ? typeOrCombinator : void 0;
2938
3134
  const combinator = hasExplicitType ? combinatorOrBuild : typeOrCombinator;
2939
- const build = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2940
- const opts = hasExplicitType ? maybeOpts : buildOrOpts;
3135
+ const buildArg = hasExplicitType ? buildOrOpts : combinatorOrBuild;
3136
+ const build = typeof buildArg === "function" ? buildArg : void 0;
3137
+ const opts = typeof buildArg === "function" ? maybeOpts : buildArg ?? maybeOpts;
3138
+ const project = normalizeProject(opts?.project);
3139
+ if (project !== void 0 && build !== void 0) {
3140
+ throw new Error("node() options cannot combine project with a build callback");
3141
+ }
2941
3142
  const baseDef = { tag: "node", parser: combinator, ...type === void 0 ? {} : { type }, ...build === void 0 ? {} : { build } };
2942
3143
  const meta = {
2943
3144
  firstSet: combinator._meta.firstSet,
@@ -2951,11 +3152,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2951
3152
  if (unwrap && collapse) {
2952
3153
  throw new Error("node() options cannot set both unwrap and collapse");
2953
3154
  }
2954
- const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2955
- const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
2956
- const clonesState = build ? buildReadsState(def) : true;
3155
+ if (project !== void 0 && (unwrap || collapse)) {
3156
+ throw new Error("node() options cannot combine project with unwrap or collapse");
3157
+ }
3158
+ const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
3159
+ const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
3160
+ const clonesState = build ? buildReadsState(def) : project === void 0;
2957
3161
  const hasOwnFields = parserHasOwnFields(combinator);
2958
- const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
3162
+ const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === void 0);
2959
3163
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2960
3164
  let failExpected;
2961
3165
  return {
@@ -2970,14 +3174,15 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2970
3174
  }
2971
3175
  return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
2972
3176
  }
2973
- const hostCst = build !== void 0 && cstOutputHost(ctx.build);
3177
+ const hasDirectValue = build !== void 0 || project !== void 0;
3178
+ const hostCst = hasDirectValue && cstOutputHost(ctx.build);
2974
3179
  const effTrivia = capturesTrivia || hostCst;
2975
3180
  const effFields = capturesFields || hostCst && hasOwnFields;
2976
3181
  const saved = beginCstNodeCapture(ctx);
2977
3182
  const savedFields = ctx._fields;
2978
3183
  ctx._fields = effFields ? [] : void 0;
2979
3184
  const savedMask = ctx._triviaCaptureMask;
2980
- if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
3185
+ if (build === void 0 && project === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2981
3186
  ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
2982
3187
  }
2983
3188
  if (!effTrivia) ctx.captureTrivia = false;
@@ -2995,7 +3200,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2995
3200
  const nodeType = def.type ?? missingInferredType();
2996
3201
  const cstOutput = cstOutputHost(ctx.build);
2997
3202
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2998
- const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, hostState) : build(children2, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
3203
+ const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && project === void 0 && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, hostState) : projectChild(children2, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, hostState) : build(children2, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
2999
3204
  const rawEntry = isCstChild(built) ? built : { _tag: "leaf", value: typeof built === "string" ? built : typeof built === "object" && built !== null ? input.slice(r.span.start, r.span.end) : "", span: r.span };
3000
3205
  if (saved.buf !== void 0 || saved.ch !== void 0) {
3001
3206
  pushCstChild(ctx, built, rawEntry);
@@ -3273,7 +3478,7 @@ function leaf(root, fn) {
3273
3478
  }
3274
3479
 
3275
3480
  // src/version.ts
3276
- var PARSEMAN_VERSION = "0.39.0";
3481
+ var PARSEMAN_VERSION = "0.40.0";
3277
3482
 
3278
3483
  // src/cst/host-mode.ts
3279
3484
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3533,6 +3738,7 @@ function analyzeGatingRules(ruleMap, opts) {
3533
3738
  if (d.tag === "dispatch") {
3534
3739
  kids.push(d.selector);
3535
3740
  for (const c of d.cases) kids.push(c.parser);
3741
+ if (d.matchers) for (const c of d.matchers) kids.push(c.parser);
3536
3742
  if (d.otherwise !== void 0) kids.push(d.otherwise);
3537
3743
  }
3538
3744
  if (Array.isArray(rec.skip)) kids.push(...rec.skip);
@@ -5056,13 +5262,25 @@ function escapeClassChar(ch2) {
5056
5262
  }
5057
5263
 
5058
5264
  // src/compiler/grammar-coverage-ids.ts
5265
+ function idPart(value) {
5266
+ return encodeURIComponent(value);
5267
+ }
5268
+ function dispatchArmIds(def, path) {
5269
+ const ids = [];
5270
+ for (const entry of def.cases) ids.push(`dispatch:${path}/when:${entry.keys.map(idPart).join("|")}`);
5271
+ for (const entry of def.matchers ?? []) {
5272
+ ids.push(`dispatch:${path}/matcher:${entry.kind}:${idPart(entry.value)}${entry.kind === "matches" && entry.flags ? `:${idPart(entry.flags)}` : ""}`);
5273
+ }
5274
+ if (def.otherwise) ids.push(`dispatch:${path}/otherwise`);
5275
+ return ids;
5276
+ }
5059
5277
  function children(def, winners) {
5060
5278
  switch (def.tag) {
5061
5279
  case "sequence":
5062
5280
  case "choice":
5063
5281
  return def.parsers;
5064
5282
  case "dispatch":
5065
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
5283
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
5066
5284
  case "many":
5067
5285
  case "oneOrMore":
5068
5286
  case "optional":
@@ -5105,6 +5323,7 @@ function children(def, winners) {
5105
5323
  function buildGrammarPlan(entry, winners) {
5106
5324
  const definitions = /* @__PURE__ */ new Map();
5107
5325
  const choices = /* @__PURE__ */ new WeakMap();
5326
+ const dispatches = /* @__PURE__ */ new WeakMap();
5108
5327
  const attempts = /* @__PURE__ */ new WeakMap();
5109
5328
  const labels = /* @__PURE__ */ new WeakMap();
5110
5329
  const rules2 = /* @__PURE__ */ new WeakMap();
@@ -5135,6 +5354,11 @@ function buildGrammarPlan(entry, winners) {
5135
5354
  choices.set(parser2, ids);
5136
5355
  ids.forEach((id) => definitions.set(id, { id, kind: "choice-arm" }));
5137
5356
  }
5357
+ if (parser2._def.tag === "dispatch") {
5358
+ const ids = dispatchArmIds(parser2._def, path);
5359
+ dispatches.set(parser2, ids);
5360
+ ids.forEach((id) => definitions.set(id, { id, kind: "dispatch-arm" }));
5361
+ }
5138
5362
  if (parser2._def.tag === "attempt") attempts.set(parser2, `attempt:${path}`);
5139
5363
  if (parser2._def.tag === "label") {
5140
5364
  const id = `label:${path}`;
@@ -5147,7 +5371,7 @@ function buildGrammarPlan(entry, winners) {
5147
5371
  for (const root of roots) {
5148
5372
  visit(root, winnerNames.get(root) ?? root._ruleName ?? "entry");
5149
5373
  }
5150
- return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, attempts, labels, rules: rules2 };
5374
+ return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules: rules2 };
5151
5375
  }
5152
5376
 
5153
5377
  // src/compiler/scannable-run.ts
@@ -6521,6 +6745,7 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
6521
6745
  case "literal":
6522
6746
  case "regex":
6523
6747
  case "keywords":
6748
+ case "routed":
6524
6749
  return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
6525
6750
  case "choice": {
6526
6751
  const alts = [];
@@ -6871,7 +7096,7 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
6871
7096
  case "choice":
6872
7097
  return false;
6873
7098
  case "dispatch":
6874
- 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);
7099
+ 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);
6875
7100
  case "attempt":
6876
7101
  return false;
6877
7102
  // optional never fails; many/oneOrMore only "fail" with zero captured items.
@@ -6970,6 +7195,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6970
7195
  case "keywords":
6971
7196
  case "node":
6972
7197
  case "token":
7198
+ case "routed":
6973
7199
  case "leaf":
6974
7200
  return true;
6975
7201
  case "not":
@@ -6982,7 +7208,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6982
7208
  case "choice":
6983
7209
  return d.parsers.some((x) => capturesLeaf(x, seen));
6984
7210
  case "dispatch":
6985
- return capturesLeaf(d.selector, seen) || d.cases.some((x) => capturesLeaf(x.parser, seen)) || (d.otherwise ? capturesLeaf(d.otherwise, seen) : false);
7211
+ 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);
6986
7212
  case "sepBy":
6987
7213
  return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen);
6988
7214
  case "many":
@@ -7460,6 +7686,7 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7460
7686
  for (const item of d.cases) {
7461
7687
  if (mayRecordRecoveryError(item.parser, recovery, seen)) return true;
7462
7688
  }
7689
+ if (d.matchers?.some((item) => mayRecordRecoveryError(item.parser, recovery, seen))) return true;
7463
7690
  return d.otherwise ? mayRecordRecoveryError(d.otherwise, recovery, seen) : false;
7464
7691
  }
7465
7692
  case "attempt":
@@ -7494,53 +7721,120 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7494
7721
  return false;
7495
7722
  }
7496
7723
  }
7497
- function emitDispatchCombinator(def, ctx, pos) {
7724
+ function emitDispatchCombinator(parser2, def, ctx, pos) {
7725
+ if (parserUsesRouted2(def.selector)) {
7726
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
7727
+ }
7728
+ const coverageIds = ctx.coverage?.plan.dispatches.get(parser2) ?? [];
7729
+ const branchUsesRouted2 = (entry) => entry.usesRouted === true || parserUsesRouted2(entry.parser);
7730
+ const hasRoutedBranch = def.cases.some(branchUsesRouted2) || (def.matchers ? def.matchers.some(branchUsesRouted2) : false) || (def.otherwise ? def.otherwiseUsesRouted === true || parserUsesRouted2(def.otherwise) : false);
7731
+ const selectorLeafMark = hasRoutedBranch ? v(ctx, "_dsl") : null;
7732
+ const selectorRawMark = hasRoutedBranch ? v(ctx, "_dsr") : null;
7733
+ const selectorTriviaMark = hasRoutedBranch ? v(ctx, "_dst") : null;
7734
+ const selectorLogMark = hasRoutedBranch ? v(ctx, "_dsg") : null;
7735
+ const selectorFieldMark = hasRoutedBranch ? v(ctx, "_dsf") : null;
7736
+ const selectorErrorMark = hasRoutedBranch ? v(ctx, "_dse") : null;
7498
7737
  const selector = emit(def.selector, ctx, pos);
7499
7738
  const outV = v(ctx, "_dval");
7500
7739
  const outE = v(ctx, "_dend");
7501
7740
  const keyV = v(ctx, "_dkey");
7502
7741
  const stmts = [
7742
+ ...selectorLeafMark ? [
7743
+ `${ind(ctx)}const ${selectorLeafMark} = _ctx._cstLeaves?.length ?? 0`,
7744
+ `${ind(ctx)}const ${selectorRawMark} = _ctx._cstRawChildren?.length ?? 0`,
7745
+ `${ind(ctx)}const ${selectorTriviaMark} = _ctx._cstTriviaLog?.length ?? 0`,
7746
+ `${ind(ctx)}const ${selectorLogMark} = _ctx._triviaLog?.length ?? 0`,
7747
+ `${ind(ctx)}const ${selectorFieldMark} = _ctx._fields?.length ?? 0`,
7748
+ `${ind(ctx)}const ${selectorErrorMark} = _ctx._errors?.length ?? 0`
7749
+ ] : [],
7503
7750
  ...selector.stmts,
7504
7751
  `${ind(ctx)}const ${keyV} = ${selector.valueVar}`,
7505
7752
  `${ind(ctx)}let ${outV}, ${outE} = ${selector.endVar}`
7506
7753
  ];
7507
7754
  let wroteBranch = false;
7508
- const emitSelectedTail = (parser2, keyword, condition) => {
7755
+ const emitSelectedTail = (entry, keyword, condition, coverageId) => {
7756
+ const parser3 = entry.parser;
7509
7757
  const head = keyword === "else" ? `${ind(ctx)}else {` : `${ind(ctx)}${keyword} (${condition}) {`;
7510
7758
  stmts.push(head);
7511
7759
  ctx.indent++;
7512
- const mayError = mayRecordRecoveryError(parser2, !!ctx.recovery);
7760
+ const usesRouted = branchUsesRouted2(entry);
7761
+ const routedV = usesRouted ? v(ctx, "_drt") : null;
7762
+ const mayError = mayRecordRecoveryError(parser3, !!ctx.recovery);
7513
7763
  const errMark = mayError ? v(ctx, "_derr") : null;
7514
7764
  const logMark = ctx.activeTrivia ? v(ctx, "_dlog") : null;
7765
+ if (routedV) {
7766
+ stmts.push(
7767
+ `${ind(ctx)}const ${routedV} = _ctx._routed; _ctx._routed = { value: ${selector.valueVar}, span: { start: ${pos}, end: ${selector.endVar} } }`,
7768
+ ...selectorLeafMark ? [
7769
+ `${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}`
7770
+ ] : []
7771
+ );
7772
+ }
7515
7773
  if (errMark) stmts.push(`${ind(ctx)}const ${errMark} = _ctx._errors?.length ?? 0`);
7516
7774
  if (logMark) stmts.push(`${ind(ctx)}const ${logMark} = _ctx._triviaLog?.length ?? 0`);
7517
- const tail = emitTailFallible(parser2, ctx, selector.endVar);
7775
+ const tail = emitTailFallible(parser3, ctx, usesRouted ? pos : selector.endVar);
7776
+ stmts.push(...coverageAttempt(ctx, coverageId, pos));
7518
7777
  const errorRollback = errMark ? `if (_ctx._errors && _ctx._errors.length !== ${errMark}) _ctx._errors.length = ${errMark}; ` : "";
7519
7778
  const logRollback = logMark ? `if (_ctx._triviaLog && _ctx._triviaLog.length !== ${logMark}) _ctx._triviaLog.length = ${logMark}; ` : "";
7779
+ const routedRollback = routedV ? `_ctx._routed = ${routedV}; ` : "";
7780
+ const coverageFailure = coverageId === void 0 ? "" : `_ctx._grammarTrace?.write({ id: ${JSON.stringify(coverageId)}, phase: 'failure', offset: _ctx._fe }); `;
7520
7781
  stmts.push(
7521
7782
  ...tail.stmts,
7522
- `${ind(ctx)}if (!${tail.okVar}) { ${errorRollback}${logRollback}${committedFailBody(ctx)} }`,
7783
+ `${ind(ctx)}if (!${tail.okVar}) { ${routedRollback}${errorRollback}${logRollback}${coverageFailure}${committedFailBody(ctx)} }`,
7784
+ ...routedV ? [`${ind(ctx)}_ctx._routed = ${routedV}`] : [],
7523
7785
  `${ind(ctx)}${outV} = [${selector.valueVar}, ${tail.valVar}]`,
7524
- `${ind(ctx)}${outE} = ${tail.endVar}`
7786
+ `${ind(ctx)}${outE} = ${tail.endVar}`,
7787
+ ...coverageHit(ctx, coverageId, pos, tail.endVar)
7525
7788
  );
7526
7789
  ctx.indent--;
7527
7790
  stmts.push(`${ind(ctx)}}`);
7528
7791
  };
7792
+ let coverageIndex = 0;
7529
7793
  for (const entry of def.cases) {
7530
- const condition = entry.keys.map((key) => `${keyV} === ${JSON.stringify(key)}`).join(" || ");
7531
- emitSelectedTail(entry.parser, wroteBranch ? "else if" : "if", condition);
7794
+ const condition = entry.keys.map((key) => emitDispatchKeyCondition(keyV, key, entry.caseInsensitive)).join(" || ");
7795
+ emitSelectedTail(entry, wroteBranch ? "else if" : "if", condition, coverageIds[coverageIndex++]);
7532
7796
  wroteBranch = true;
7533
7797
  }
7798
+ const emitMatcherTails = () => {
7799
+ for (const matcher of def.matchers ?? []) {
7800
+ emitSelectedTail(matcher, wroteBranch ? "else if" : "if", emitDispatchMatcherCondition(keyV, matcher), coverageIds[coverageIndex++]);
7801
+ wroteBranch = true;
7802
+ }
7803
+ };
7534
7804
  if (def.otherwise) {
7535
- if (wroteBranch) emitSelectedTail(def.otherwise, "else");
7536
- else emitSelectedTail(def.otherwise, "if", "true");
7805
+ emitMatcherTails();
7806
+ const otherwiseEntry = { parser: def.otherwise, usesRouted: def.otherwiseUsesRouted };
7807
+ if (wroteBranch) emitSelectedTail(otherwiseEntry, "else", void 0, coverageIds[coverageIndex++]);
7808
+ else emitSelectedTail(otherwiseEntry, "if", "true", coverageIds[coverageIndex++]);
7537
7809
  } else {
7810
+ emitMatcherTails();
7538
7811
  const expected = JSON.stringify(def.cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key))));
7539
7812
  const fail = failArrBody(ctx, expected, selector.endVar);
7540
7813
  stmts.push(wroteBranch ? `${ind(ctx)}else { ${fail} }` : `${ind(ctx)}${fail}`);
7541
7814
  }
7542
7815
  return { stmts, valueVar: outV, endVar: outE };
7543
7816
  }
7817
+ function emitDispatchKeyCondition(valueVar, key, caseInsensitive) {
7818
+ if (!caseInsensitive) return `${valueVar} === ${JSON.stringify(key)}`;
7819
+ const parts = [`${valueVar}.length === ${key.length}`];
7820
+ for (let i = 0; i < key.length; i++) {
7821
+ parts.push(foldEq(`${valueVar}.charCodeAt(${i})`, key.charCodeAt(i)));
7822
+ }
7823
+ return parts.join(" && ");
7824
+ }
7825
+ function emitDispatchMatcherCondition(valueVar, matcher) {
7826
+ if (matcher.kind === "matches") {
7827
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
7828
+ return `${new RegExp(matcher.value, flags).toString()}.test(${valueVar})`;
7829
+ }
7830
+ const text = matcher.value;
7831
+ const parts = [`${valueVar}.length >= ${text.length}`];
7832
+ const offset = matcher.kind === "endsWith" ? `${valueVar}.length - ${text.length}` : "0";
7833
+ for (let i = 0; i < text.length; i++) {
7834
+ parts.push(matcher.caseInsensitive ? foldEq(`${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`})`, text.charCodeAt(i)) : `${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`}) === ${text.charCodeAt(i)}`);
7835
+ }
7836
+ return parts.join(" && ");
7837
+ }
7544
7838
  function emitGreedyClassify(def, superIndex, ctx, pos, coverageBase) {
7545
7839
  const superParser = def.parsers[superIndex];
7546
7840
  const regexDef = getCoreRegexDef(superParser);
@@ -8195,6 +8489,18 @@ function emitPeek(def, ctx, pos) {
8195
8489
  endVar: pos
8196
8490
  };
8197
8491
  }
8492
+ function emitRouted(ctx, pos) {
8493
+ const item = v(ctx, "_rt");
8494
+ return {
8495
+ stmts: [
8496
+ `${ind(ctx)}const ${item} = _ctx._routed`,
8497
+ ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
8498
+ ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
8499
+ ],
8500
+ valueVar: `${item}.value`,
8501
+ endVar: `${item}.span.end`
8502
+ };
8503
+ }
8198
8504
  function escapeTokenLiteral(value) {
8199
8505
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
8200
8506
  }
@@ -8335,14 +8641,20 @@ function emitLeaf(def, ctx, pos) {
8335
8641
  endVar: inner.endVar
8336
8642
  };
8337
8643
  }
8644
+ function emitNodeProjectExpr(type, project, chV) {
8645
+ const child = `${chV}[${project}]`;
8646
+ const missing = `(() => { throw new Error(${JSON.stringify(`node(${JSON.stringify(type)}) project child ${project} was not captured`)}) })()`;
8647
+ const value = `(${child} !== null && typeof ${child} === 'object' && ${child}._tag === 'leaf' ? ${child}.value : ${child})`;
8648
+ return `(${project} in ${chV} ? ${value} : ${missing})`;
8649
+ }
8338
8650
  function emitNode(def, ctx, pos) {
8339
8651
  if (def.type === void 0) {
8340
8652
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
8341
8653
  }
8342
- const structural = def.build === void 0;
8343
- const mkType = structural ? null : analyzeMkInlineBuild(def);
8654
+ const structural = def.build === void 0 && def.project === void 0;
8655
+ const mkType = structural || def.project !== void 0 ? null : analyzeMkInlineBuild(def);
8344
8656
  let buildIdx = null;
8345
- if (!mkType && !structural) {
8657
+ if (!mkType && !structural && def.project === void 0) {
8346
8658
  buildIdx = ctx.buildFns.length;
8347
8659
  ctx.buildFns.push(def.build);
8348
8660
  ctx.buildSrcs.push(def.buildSrc ?? null);
@@ -8350,12 +8662,13 @@ function emitNode(def, ctx, pos) {
8350
8662
  const i = ind(ctx);
8351
8663
  const cstOut = ctx.hostMode === "cst" && !structural;
8352
8664
  if (!structural && !cstOut) ctx.hostBranchElided = true;
8353
- const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
8354
- const clonesState = !structural && (cstOut || buildReadsState(def));
8355
- const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
8356
- const capturesRaw = !structural && (cstOut || mkType !== null || buildReadsRaw(def));
8665
+ const hasProject = def.project !== void 0;
8666
+ const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && !hasProject && buildReadsTrivia(def);
8667
+ const clonesState = !structural && (cstOut || !hasProject && buildReadsState(def));
8668
+ const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || def.project !== void 0 || buildReadsChildren(def));
8669
+ const capturesRaw = !structural && (cstOut || mkType !== null || def.project === void 0 && buildReadsRaw(def));
8357
8670
  const hasFields = parserHasOwnFields(def.parser);
8358
- const capturesFields = hasFields && !structural && (cstOut || buildReadsFields(def));
8671
+ const capturesFields = hasFields && !structural && (cstOut || !hasProject && buildReadsFields(def));
8359
8672
  const innerEnablesTriviaCapture = parserEnablesTriviaCapture(def.parser);
8360
8673
  const chV = v(ctx, "_ch");
8361
8674
  const rawV = v(ctx, "_raw");
@@ -8453,7 +8766,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
8453
8766
  );
8454
8767
  }
8455
8768
  const ndV = v(ctx, "_nd");
8456
- const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: { start: ${pos}, end: ${endVar} }, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, pos, endVar, tlV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
8769
+ const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: { start: ${pos}, end: ${endVar} }, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, pos, endVar, tlV) : def.project !== void 0 ? emitNodeProjectExpr(def.type, def.project, chV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
8457
8770
  const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${JSON.stringify(def.type)}, ${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
8458
8771
  const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
8459
8772
  const hostCollapseExpr = structural ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
@@ -8660,7 +8973,7 @@ function emitDispatch(p, ctx, pos) {
8660
8973
  case "choice":
8661
8974
  return emitChoice(p, def, ctx, pos);
8662
8975
  case "dispatch":
8663
- return emitDispatchCombinator(def, ctx, pos);
8976
+ return emitDispatchCombinator(p, def, ctx, pos);
8664
8977
  case "attempt":
8665
8978
  return emitAttempt(p, def, ctx, pos);
8666
8979
  case "many":
@@ -8785,6 +9098,8 @@ function emitDispatch(p, ctx, pos) {
8785
9098
  return emitNot(def, ctx, pos);
8786
9099
  case "peek":
8787
9100
  return emitPeek(def, ctx, pos);
9101
+ case "routed":
9102
+ return emitRouted(ctx, pos);
8788
9103
  case "node":
8789
9104
  return emitNode(def, ctx, pos);
8790
9105
  case "scanTo":
@@ -8882,7 +9197,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8882
9197
  case "choice":
8883
9198
  return d.parsers.some((x) => hasNodeDef(x, seen));
8884
9199
  case "dispatch":
8885
- return hasNodeDef(d.selector, seen) || d.cases.some((x) => hasNodeDef(x.parser, seen)) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false);
9200
+ 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);
8886
9201
  case "sepBy":
8887
9202
  return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen);
8888
9203
  case "scanTo":
@@ -8897,6 +9212,26 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8897
9212
  return false;
8898
9213
  }
8899
9214
  }
9215
+ function parserUsesRouted2(p, seen = /* @__PURE__ */ new Set()) {
9216
+ if (seen.has(p)) return false;
9217
+ seen.add(p);
9218
+ const d = p._def;
9219
+ switch (d.tag) {
9220
+ case "routed":
9221
+ return true;
9222
+ case "lazy": {
9223
+ try {
9224
+ return parserUsesRouted2(d.thunk(), seen);
9225
+ } catch {
9226
+ return false;
9227
+ }
9228
+ }
9229
+ case "dispatch":
9230
+ return false;
9231
+ default:
9232
+ return childrenOf2(d).some((child) => parserUsesRouted2(child, seen));
9233
+ }
9234
+ }
8900
9235
  function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
8901
9236
  if (seen.has(p)) return false;
8902
9237
  seen.add(p);
@@ -8917,7 +9252,7 @@ function childrenOf2(def) {
8917
9252
  case "choice":
8918
9253
  return def.parsers;
8919
9254
  case "dispatch":
8920
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9255
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
8921
9256
  case "many":
8922
9257
  case "attempt":
8923
9258
  case "optional":
@@ -8954,6 +9289,7 @@ function childrenOf2(def) {
8954
9289
  case "regex":
8955
9290
  case "keywords":
8956
9291
  case "guard":
9292
+ case "routed":
8957
9293
  case "unknown":
8958
9294
  return [];
8959
9295
  }
@@ -9661,7 +9997,7 @@ function childrenOf3(def) {
9661
9997
  case "choice":
9662
9998
  return def.parsers;
9663
9999
  case "dispatch":
9664
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
10000
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
9665
10001
  case "many":
9666
10002
  case "oneOrMore":
9667
10003
  case "optional":
@@ -9690,6 +10026,7 @@ function childrenOf3(def) {
9690
10026
  case "literal":
9691
10027
  case "regex":
9692
10028
  case "keywords":
10029
+ case "routed":
9693
10030
  case "guard":
9694
10031
  case "recover":
9695
10032
  case "unknown":
@@ -9714,6 +10051,20 @@ function repeatOpts(min, max, trailing) {
9714
10051
  if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
9715
10052
  return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
9716
10053
  }
10054
+ function projectOpt(project) {
10055
+ if (project === void 0) return void 0;
10056
+ return `project: ${project}`;
10057
+ }
10058
+ function matcherExpr(entry) {
10059
+ switch (entry.kind) {
10060
+ case "startsWith":
10061
+ return `startsWith(${JSON.stringify(entry.value)})`;
10062
+ case "endsWith":
10063
+ return `endsWith(${JSON.stringify(entry.value)})`;
10064
+ case "matches":
10065
+ return `matches(${new RegExp(entry.value, entry.flags ?? "").toString()})`;
10066
+ }
10067
+ }
9717
10068
  function serializeRuleMap(ruleMap, scanSkip) {
9718
10069
  try {
9719
10070
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -9813,7 +10164,11 @@ function evalRuleMapIR(ir) {
9813
10164
  "choice",
9814
10165
  "dispatch",
9815
10166
  "when",
10167
+ "startsWith",
10168
+ "endsWith",
10169
+ "matches",
9816
10170
  "otherwise",
10171
+ "routed",
9817
10172
  "attempt",
9818
10173
  "many",
9819
10174
  "oneOrMore",
@@ -9849,7 +10204,11 @@ function evalRuleMapIR(ir) {
9849
10204
  choice,
9850
10205
  dispatch,
9851
10206
  when,
10207
+ startsWith,
10208
+ endsWith,
10209
+ matches,
9852
10210
  otherwise,
10211
+ routed,
9853
10212
  attempt,
9854
10213
  many,
9855
10214
  oneOrMore,
@@ -10037,8 +10396,13 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10037
10396
  }
10038
10397
  case "dispatch": {
10039
10398
  const arms = def.cases.map(
10040
- (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)})`
10399
+ (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`
10041
10400
  );
10401
+ if (def.matchers) {
10402
+ for (const entry of def.matchers) {
10403
+ arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`);
10404
+ }
10405
+ }
10042
10406
  if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`);
10043
10407
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? "" : `, ${arms.join(", ")}`})`;
10044
10408
  }
@@ -10058,6 +10422,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10058
10422
  return `not(${kid(def.parser)})`;
10059
10423
  case "peek":
10060
10424
  return `peek(${kid(def.parser)})`;
10425
+ case "routed":
10426
+ return "routed()";
10061
10427
  case "trivia":
10062
10428
  return `trivia(${kid(def.parser)})`;
10063
10429
  case "token":
@@ -10082,9 +10448,11 @@ ${outerDecls.map((d) => " " + d).join("\n")}
10082
10448
  }
10083
10449
  case "node": {
10084
10450
  if (def.build !== void 0 && def.buildSrc === void 0) throw new Unserializable("node build without buildSrc");
10451
+ const projectEntry = projectOpt(def.project);
10085
10452
  const optEntries = [
10086
10453
  ...def.unwrap ? ["unwrap: true"] : [],
10087
10454
  ...def.collapse ? ["collapse: true"] : [],
10455
+ ...projectEntry === void 0 ? [] : [projectEntry],
10088
10456
  ...def.captureTrivia ? ["captureTrivia: true"] : [],
10089
10457
  ...def.trailingTrivia ? ["trailingTrivia: true"] : []
10090
10458
  ];
@@ -11079,10 +11447,20 @@ function run(entry, input, options = {}) {
11079
11447
  }
11080
11448
 
11081
11449
  // src/coverage.ts
11450
+ function dispatchMatcher(entry) {
11451
+ switch (entry.kind) {
11452
+ case "startsWith":
11453
+ return startsWith(entry.value);
11454
+ case "endsWith":
11455
+ return endsWith(entry.value);
11456
+ case "matches":
11457
+ return matches(new RegExp(entry.value, entry.flags));
11458
+ }
11459
+ }
11082
11460
  var GRAMMAR_COVERAGE_DEFINITIONS = /* @__PURE__ */ Symbol.for("parseman.grammarCoverageDefinitions");
11083
11461
  function compiledGrammarCoverageDefinitions(grammar) {
11084
11462
  const definitions = grammar[GRAMMAR_COVERAGE_DEFINITIONS];
11085
- if (!Array.isArray(definitions) || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "label"))) {
11463
+ if (!Array.isArray(definitions) || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "dispatch-arm" || definition.kind === "label"))) {
11086
11464
  throw new TypeError("grammar has no coverage definitions; enable grammarCoverage for this macro build");
11087
11465
  }
11088
11466
  return definitions;
@@ -11262,10 +11640,54 @@ function coverageEntry(entry, collector, maps, trace) {
11262
11640
  return unsafeSequence(...def.parsers.map(build));
11263
11641
  }
11264
11642
  case "dispatch": {
11265
- const arms = def.cases.map((entry2) => when(entry2.keys, build(entry2.parser)));
11266
- if (def.otherwise) arms.push(otherwise(build(def.otherwise)));
11643
+ const ids = maps.dispatches.get(parser2) ?? [];
11644
+ let armIndex = 0;
11645
+ const dispatchStarts = [];
11646
+ const wrapArm = (arm, id) => {
11647
+ const child = build(arm);
11648
+ return {
11649
+ _tag: child._tag,
11650
+ _meta: child._meta,
11651
+ _def: child._def,
11652
+ parse(input, pos, ctx) {
11653
+ const dispatchStart = dispatchStarts[dispatchStarts.length - 1] ?? pos;
11654
+ if (id) trace?.write({ id, phase: "attempt", offset: dispatchStart });
11655
+ const result = child.parse(input, pos, ctx);
11656
+ if (result.ok) {
11657
+ if (id) {
11658
+ collector.hit(id);
11659
+ trace?.write({ id, phase: "selected", offset: dispatchStart, end: result.span.end });
11660
+ trace?.write({ id, phase: "success", offset: dispatchStart, end: result.span.end });
11661
+ }
11662
+ } else if (id) {
11663
+ trace?.write({ id, phase: "failure", offset: result.span.end });
11664
+ }
11665
+ return result;
11666
+ }
11667
+ };
11668
+ };
11669
+ const arms = def.cases.map(
11670
+ (entry2) => when(entry2.keys, wrapArm(entry2.parser, ids[armIndex++]), entry2.caseInsensitive ? { caseInsensitive: true } : void 0)
11671
+ );
11672
+ if (def.matchers) {
11673
+ for (const entry2 of def.matchers) {
11674
+ arms.push(when(dispatchMatcher(entry2), wrapArm(entry2.parser, ids[armIndex++]), entry2.caseInsensitive ? { caseInsensitive: true } : void 0));
11675
+ }
11676
+ }
11677
+ if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])));
11267
11678
  const unsafeDispatch = dispatch;
11268
- return unsafeDispatch(build(def.selector), ...arms);
11679
+ const base2 = unsafeDispatch(build(def.selector), ...arms);
11680
+ return {
11681
+ ...base2,
11682
+ parse(input, pos, ctx) {
11683
+ dispatchStarts.push(pos);
11684
+ try {
11685
+ return base2.parse(input, pos, ctx);
11686
+ } finally {
11687
+ dispatchStarts.pop();
11688
+ }
11689
+ }
11690
+ };
11269
11691
  }
11270
11692
  case "many":
11271
11693
  return many(build(def.parser));
@@ -11310,7 +11732,7 @@ function coverageEntry(entry, collector, maps, trace) {
11310
11732
  case "peek":
11311
11733
  return peek(build(def.parser));
11312
11734
  case "node": {
11313
- const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
11735
+ const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.project !== void 0 ? { project: def.project } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
11314
11736
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
11315
11737
  }
11316
11738
  case "guard":