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.js CHANGED
@@ -51,6 +51,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
51
51
  return d.value.length === 0;
52
52
  case "keywords":
53
53
  return false;
54
+ case "routed":
55
+ return false;
54
56
  case "regex":
55
57
  try {
56
58
  const m = new RegExp(d.source).exec("");
@@ -1112,6 +1114,8 @@ function derive(c, seen) {
1112
1114
  case "not":
1113
1115
  case "peek":
1114
1116
  return deriveExpected2(def.parser);
1117
+ case "routed":
1118
+ return ["routed()"];
1115
1119
  case "lazy": {
1116
1120
  const name = c._ruleName;
1117
1121
  if (seen.has(c)) return name ? [name] : [];
@@ -1936,42 +1940,198 @@ function sequence(...parsers) {
1936
1940
  }
1937
1941
 
1938
1942
  // src/combinators/dispatch.ts
1939
- function when(key, parser2) {
1943
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
1944
+ if (seen.has(parser2)) return false;
1945
+ seen.add(parser2);
1946
+ const def = parser2._def;
1947
+ switch (def.tag) {
1948
+ case "routed":
1949
+ return true;
1950
+ case "sequence":
1951
+ case "choice":
1952
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
1953
+ case "dispatch":
1954
+ return false;
1955
+ case "sepBy":
1956
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
1957
+ case "skip":
1958
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
1959
+ case "grammar":
1960
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
1961
+ case "scanTo":
1962
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
1963
+ case "recover":
1964
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
1965
+ case "lazy":
1966
+ try {
1967
+ return parserUsesRouted(def.thunk(), seen);
1968
+ } catch {
1969
+ return false;
1970
+ }
1971
+ case "many":
1972
+ case "oneOrMore":
1973
+ case "optional":
1974
+ case "attempt":
1975
+ case "transform":
1976
+ case "trivia":
1977
+ case "token":
1978
+ case "leaf":
1979
+ case "label":
1980
+ case "field":
1981
+ case "not":
1982
+ case "peek":
1983
+ case "node":
1984
+ case "withCtx":
1985
+ case "expect":
1986
+ return parserUsesRouted(def.parser, seen);
1987
+ default:
1988
+ return false;
1989
+ }
1990
+ }
1991
+ function branchUsesRouted(branch) {
1992
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
1993
+ }
1994
+ function asciiFoldKey(key) {
1995
+ let out = "";
1996
+ for (let i = 0; i < key.length; i++) {
1997
+ const c = key.charCodeAt(i);
1998
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
1999
+ }
2000
+ return out;
2001
+ }
2002
+ function matchesDispatchMatcher(value, matcher) {
2003
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2004
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2005
+ switch (matcher.kind) {
2006
+ case "startsWith":
2007
+ return candidate.startsWith(expected);
2008
+ case "endsWith":
2009
+ return candidate.endsWith(expected);
2010
+ case "matches": {
2011
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2012
+ return new RegExp(matcher.value, flags).test(value);
2013
+ }
2014
+ }
2015
+ }
2016
+ function isStringMatcher(value) {
2017
+ if (typeof value !== "object" || value === null) return false;
2018
+ const kind = value.kind;
2019
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2020
+ }
2021
+ function fixedMatcher(kind, value) {
2022
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2023
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2024
+ return { kind, value };
2025
+ }
2026
+ function startsWith(prefix) {
2027
+ return fixedMatcher("startsWith", prefix);
2028
+ }
2029
+ function endsWith(suffix) {
2030
+ return fixedMatcher("endsWith", suffix);
2031
+ }
2032
+ function matches(pattern) {
2033
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2034
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2035
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2036
+ }
2037
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2038
+ }
2039
+ function when(key, parser2, opts = {}) {
2040
+ const usesRouted = parserUsesRouted(parser2);
2041
+ if (isStringMatcher(key)) {
2042
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2043
+ }
1940
2044
  const keys = Array.isArray(key) ? [...key] : [key];
1941
2045
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
1942
2046
  for (const item of keys) {
1943
2047
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
1944
2048
  }
1945
- return { kind: "when", keys, parser: parser2 };
2049
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2050
+ }
2051
+ function makeWhen(opts = {}) {
2052
+ return (key, parser2) => when(key, parser2, opts);
1946
2053
  }
1947
2054
  function otherwise(parser2) {
1948
- return { kind: "otherwise", parser: parser2 };
2055
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2056
+ }
2057
+ function routed() {
2058
+ return {
2059
+ _tag: "routed",
2060
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2061
+ _def: { tag: "routed" },
2062
+ parse(_input, pos, ctx) {
2063
+ const item = ctx._routed;
2064
+ if (item === void 0) {
2065
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2066
+ }
2067
+ if (pos !== item.span.start) {
2068
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2069
+ }
2070
+ if (cstCaptureActive(ctx)) {
2071
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2072
+ }
2073
+ return { ok: true, value: item.value, span: item.span };
2074
+ }
2075
+ };
1949
2076
  }
1950
2077
  function dispatch(selector, ...arms) {
2078
+ if (parserUsesRouted(selector)) {
2079
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2080
+ }
1951
2081
  let fallback;
2082
+ let fallbackUsesRouted = false;
2083
+ const matchers = [];
1952
2084
  const cases = [];
1953
- const seen = /* @__PURE__ */ new Set();
2085
+ const seen = [];
1954
2086
  for (let i = 0; i < arms.length; i++) {
1955
2087
  const arm = arms[i];
1956
2088
  if (arm.kind === "otherwise") {
1957
2089
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
1958
2090
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
1959
2091
  fallback = arm.parser;
2092
+ fallbackUsesRouted = arm.usesRouted === true;
1960
2093
  continue;
1961
2094
  }
1962
- for (const key of arm.keys) {
1963
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
1964
- seen.add(key);
2095
+ if (arm.kind === "whenMatcher") {
2096
+ const matcher = arm.matcher;
2097
+ matchers.push({
2098
+ kind: matcher.kind,
2099
+ value: matcher.value,
2100
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2101
+ parser: arm.parser,
2102
+ caseInsensitive: arm.caseInsensitive,
2103
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2104
+ });
2105
+ continue;
1965
2106
  }
1966
- cases.push({ keys: arm.keys, parser: arm.parser });
2107
+ for (const key of arm.keys) {
2108
+ const folded = asciiFoldKey(key);
2109
+ for (const prior of seen) {
2110
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2111
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2112
+ }
2113
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2114
+ }
2115
+ cases.push({
2116
+ keys: arm.keys,
2117
+ parser: arm.parser,
2118
+ caseInsensitive: arm.caseInsensitive,
2119
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2120
+ });
1967
2121
  }
1968
2122
  const meta = {
1969
2123
  firstSet: selector._meta.firstSet,
1970
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2124
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
1971
2125
  isTrivia: false
1972
2126
  };
1973
2127
  const byKey = /* @__PURE__ */ new Map();
1974
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2128
+ const byFoldedKey = /* @__PURE__ */ new Map();
2129
+ for (const entry of cases) {
2130
+ for (const key of entry.keys) {
2131
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2132
+ else byKey.set(key, entry);
2133
+ }
2134
+ }
1975
2135
  return {
1976
2136
  _tag: "dispatch",
1977
2137
  _meta: meta,
@@ -1979,18 +2139,35 @@ function dispatch(selector, ...arms) {
1979
2139
  tag: "dispatch",
1980
2140
  selector,
1981
2141
  cases,
1982
- ...fallback === void 0 ? {} : { otherwise: fallback }
2142
+ ...matchers.length === 0 ? {} : { matchers },
2143
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
1983
2144
  },
1984
2145
  parse(input, pos, ctx) {
2146
+ if (parserUsesRouted(selector)) {
2147
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2148
+ }
2149
+ const selectorMark = saveTriviaMark(ctx);
1985
2150
  const selected = selector.parse(input, pos, ctx);
1986
2151
  if (!selected.ok) return selected;
1987
- const tail = byKey.get(selected.value) ?? fallback;
1988
- if (tail === void 0) {
2152
+ 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 });
2153
+ if (branch === void 0) {
1989
2154
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
1990
2155
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
1991
2156
  }
1992
- const mark = saveTriviaMark(ctx);
1993
- const result = tail.parse(input, selected.span.end, ctx);
2157
+ const savedRouted = ctx._routed;
2158
+ const usesRouted = branchUsesRouted(branch);
2159
+ let mark = saveTriviaMark(ctx);
2160
+ if (usesRouted) {
2161
+ rollbackTrivia(ctx, selectorMark);
2162
+ mark = saveTriviaMark(ctx);
2163
+ ctx._routed = { value: selected.value, span: selected.span };
2164
+ }
2165
+ let result;
2166
+ try {
2167
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2168
+ } finally {
2169
+ if (usesRouted) ctx._routed = savedRouted;
2170
+ }
1994
2171
  if (!result.ok) {
1995
2172
  rollbackTrivia(ctx, mark);
1996
2173
  return { ...result, committed: true };
@@ -2377,6 +2554,7 @@ function markUnusedValues(root) {
2377
2554
  case "dispatch":
2378
2555
  visit(def.selector, consumed);
2379
2556
  for (const entry of def.cases) visit(entry.parser, consumed);
2557
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
2380
2558
  if (def.otherwise) visit(def.otherwise, consumed);
2381
2559
  return;
2382
2560
  case "node":
@@ -2655,7 +2833,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2655
2833
  case "choice":
2656
2834
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2657
2835
  case "dispatch":
2658
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2836
+ 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);
2659
2837
  case "sepBy":
2660
2838
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2661
2839
  case "skip":
@@ -2724,7 +2902,7 @@ function parserHasTriviaSite(p, seen = /* @__PURE__ */ new Set()) {
2724
2902
  case "choice":
2725
2903
  return d.parsers.some((x) => parserHasTriviaSite(x, seen));
2726
2904
  case "dispatch":
2727
- return parserHasTriviaSite(d.selector, seen) || d.cases.some((x) => parserHasTriviaSite(x.parser, seen)) || (d.otherwise ? parserHasTriviaSite(d.otherwise, seen) : false);
2905
+ 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);
2728
2906
  case "grammar":
2729
2907
  return parserHasTriviaSite(d.parser, seen);
2730
2908
  case "lazy": {
@@ -2751,7 +2929,7 @@ function parserEnablesTriviaCapture(p, seen = /* @__PURE__ */ new Set()) {
2751
2929
  case "choice":
2752
2930
  return d.parsers.some((x) => parserEnablesTriviaCapture(x, seen));
2753
2931
  case "dispatch":
2754
- return parserEnablesTriviaCapture(d.selector, seen) || d.cases.some((x) => parserEnablesTriviaCapture(x.parser, seen)) || (d.otherwise ? parserEnablesTriviaCapture(d.otherwise, seen) : false);
2932
+ 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);
2755
2933
  case "sepBy":
2756
2934
  return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.separator, seen);
2757
2935
  case "skip":
@@ -2812,12 +2990,30 @@ function isCstChild(value) {
2812
2990
  function missingInferredType() {
2813
2991
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
2814
2992
  }
2993
+ function normalizeProject(project) {
2994
+ if (project === void 0) return void 0;
2995
+ if (!Number.isInteger(project) || project < 0) {
2996
+ throw new Error("node() project child must be a non-negative integer");
2997
+ }
2998
+ return project;
2999
+ }
3000
+ function projectChild(children2, project, type) {
3001
+ if (!(project in children2)) {
3002
+ throw new Error(`node(${JSON.stringify(type)}) project child ${project} was not captured`);
3003
+ }
3004
+ return unwrapChild(children2[project]);
3005
+ }
2815
3006
  function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2816
3007
  const hasExplicitType = typeof typeOrCombinator === "string";
2817
3008
  const type = hasExplicitType ? typeOrCombinator : void 0;
2818
3009
  const combinator = hasExplicitType ? combinatorOrBuild : typeOrCombinator;
2819
- const build = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2820
- const opts = hasExplicitType ? maybeOpts : buildOrOpts;
3010
+ const buildArg = hasExplicitType ? buildOrOpts : combinatorOrBuild;
3011
+ const build = typeof buildArg === "function" ? buildArg : void 0;
3012
+ const opts = typeof buildArg === "function" ? maybeOpts : buildArg ?? maybeOpts;
3013
+ const project = normalizeProject(opts?.project);
3014
+ if (project !== void 0 && build !== void 0) {
3015
+ throw new Error("node() options cannot combine project with a build callback");
3016
+ }
2821
3017
  const baseDef = { tag: "node", parser: combinator, ...type === void 0 ? {} : { type }, ...build === void 0 ? {} : { build } };
2822
3018
  const meta = {
2823
3019
  firstSet: combinator._meta.firstSet,
@@ -2831,11 +3027,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2831
3027
  if (unwrap && collapse) {
2832
3028
  throw new Error("node() options cannot set both unwrap and collapse");
2833
3029
  }
2834
- const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2835
- const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
2836
- const clonesState = build ? buildReadsState(def) : true;
3030
+ if (project !== void 0 && (unwrap || collapse)) {
3031
+ throw new Error("node() options cannot combine project with unwrap or collapse");
3032
+ }
3033
+ 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;
3034
+ const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
3035
+ const clonesState = build ? buildReadsState(def) : project === void 0;
2837
3036
  const hasOwnFields = parserHasOwnFields(combinator);
2838
- const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
3037
+ const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === void 0);
2839
3038
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2840
3039
  let failExpected;
2841
3040
  return {
@@ -2850,14 +3049,15 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2850
3049
  }
2851
3050
  return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
2852
3051
  }
2853
- const hostCst = build !== void 0 && cstOutputHost(ctx.build);
3052
+ const hasDirectValue = build !== void 0 || project !== void 0;
3053
+ const hostCst = hasDirectValue && cstOutputHost(ctx.build);
2854
3054
  const effTrivia = capturesTrivia || hostCst;
2855
3055
  const effFields = capturesFields || hostCst && hasOwnFields;
2856
3056
  const saved = beginCstNodeCapture(ctx);
2857
3057
  const savedFields = ctx._fields;
2858
3058
  ctx._fields = effFields ? [] : void 0;
2859
3059
  const savedMask = ctx._triviaCaptureMask;
2860
- if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
3060
+ if (build === void 0 && project === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2861
3061
  ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
2862
3062
  }
2863
3063
  if (!effTrivia) ctx.captureTrivia = false;
@@ -2875,7 +3075,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2875
3075
  const nodeType = def.type ?? missingInferredType();
2876
3076
  const cstOutput = cstOutputHost(ctx.build);
2877
3077
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2878
- 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 };
3078
+ 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 };
2879
3079
  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 };
2880
3080
  if (saved.buf !== void 0 || saved.ch !== void 0) {
2881
3081
  pushCstChild(ctx, built, rawEntry);
@@ -3153,7 +3353,7 @@ function leaf(root, fn) {
3153
3353
  }
3154
3354
 
3155
3355
  // src/version.ts
3156
- var PARSEMAN_VERSION = "0.39.0";
3356
+ var PARSEMAN_VERSION = "0.40.0";
3157
3357
 
3158
3358
  // src/cst/host-mode.ts
3159
3359
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3413,6 +3613,7 @@ function analyzeGatingRules(ruleMap, opts) {
3413
3613
  if (d.tag === "dispatch") {
3414
3614
  kids.push(d.selector);
3415
3615
  for (const c of d.cases) kids.push(c.parser);
3616
+ if (d.matchers) for (const c of d.matchers) kids.push(c.parser);
3416
3617
  if (d.otherwise !== void 0) kids.push(d.otherwise);
3417
3618
  }
3418
3619
  if (Array.isArray(rec.skip)) kids.push(...rec.skip);
@@ -4936,13 +5137,25 @@ function escapeClassChar(ch2) {
4936
5137
  }
4937
5138
 
4938
5139
  // src/compiler/grammar-coverage-ids.ts
5140
+ function idPart(value) {
5141
+ return encodeURIComponent(value);
5142
+ }
5143
+ function dispatchArmIds(def, path) {
5144
+ const ids = [];
5145
+ for (const entry of def.cases) ids.push(`dispatch:${path}/when:${entry.keys.map(idPart).join("|")}`);
5146
+ for (const entry of def.matchers ?? []) {
5147
+ ids.push(`dispatch:${path}/matcher:${entry.kind}:${idPart(entry.value)}${entry.kind === "matches" && entry.flags ? `:${idPart(entry.flags)}` : ""}`);
5148
+ }
5149
+ if (def.otherwise) ids.push(`dispatch:${path}/otherwise`);
5150
+ return ids;
5151
+ }
4939
5152
  function children(def, winners) {
4940
5153
  switch (def.tag) {
4941
5154
  case "sequence":
4942
5155
  case "choice":
4943
5156
  return def.parsers;
4944
5157
  case "dispatch":
4945
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
5158
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
4946
5159
  case "many":
4947
5160
  case "oneOrMore":
4948
5161
  case "optional":
@@ -4985,6 +5198,7 @@ function children(def, winners) {
4985
5198
  function buildGrammarPlan(entry, winners) {
4986
5199
  const definitions = /* @__PURE__ */ new Map();
4987
5200
  const choices = /* @__PURE__ */ new WeakMap();
5201
+ const dispatches = /* @__PURE__ */ new WeakMap();
4988
5202
  const attempts = /* @__PURE__ */ new WeakMap();
4989
5203
  const labels = /* @__PURE__ */ new WeakMap();
4990
5204
  const rules2 = /* @__PURE__ */ new WeakMap();
@@ -5015,6 +5229,11 @@ function buildGrammarPlan(entry, winners) {
5015
5229
  choices.set(parser2, ids);
5016
5230
  ids.forEach((id) => definitions.set(id, { id, kind: "choice-arm" }));
5017
5231
  }
5232
+ if (parser2._def.tag === "dispatch") {
5233
+ const ids = dispatchArmIds(parser2._def, path);
5234
+ dispatches.set(parser2, ids);
5235
+ ids.forEach((id) => definitions.set(id, { id, kind: "dispatch-arm" }));
5236
+ }
5018
5237
  if (parser2._def.tag === "attempt") attempts.set(parser2, `attempt:${path}`);
5019
5238
  if (parser2._def.tag === "label") {
5020
5239
  const id = `label:${path}`;
@@ -5027,7 +5246,7 @@ function buildGrammarPlan(entry, winners) {
5027
5246
  for (const root of roots) {
5028
5247
  visit(root, winnerNames.get(root) ?? root._ruleName ?? "entry");
5029
5248
  }
5030
- return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, attempts, labels, rules: rules2 };
5249
+ return { definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)), choices, dispatches, attempts, labels, rules: rules2 };
5031
5250
  }
5032
5251
 
5033
5252
  // src/compiler/scannable-run.ts
@@ -6401,6 +6620,7 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
6401
6620
  case "literal":
6402
6621
  case "regex":
6403
6622
  case "keywords":
6623
+ case "routed":
6404
6624
  return { alts: [[seg(p._meta.firstSet, matchesEmpty(p))]] };
6405
6625
  case "choice": {
6406
6626
  const alts = [];
@@ -6751,7 +6971,7 @@ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
6751
6971
  case "choice":
6752
6972
  return false;
6753
6973
  case "dispatch":
6754
- 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);
6974
+ 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);
6755
6975
  case "attempt":
6756
6976
  return false;
6757
6977
  // optional never fails; many/oneOrMore only "fail" with zero captured items.
@@ -6850,6 +7070,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6850
7070
  case "keywords":
6851
7071
  case "node":
6852
7072
  case "token":
7073
+ case "routed":
6853
7074
  case "leaf":
6854
7075
  return true;
6855
7076
  case "not":
@@ -6862,7 +7083,7 @@ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
6862
7083
  case "choice":
6863
7084
  return d.parsers.some((x) => capturesLeaf(x, seen));
6864
7085
  case "dispatch":
6865
- return capturesLeaf(d.selector, seen) || d.cases.some((x) => capturesLeaf(x.parser, seen)) || (d.otherwise ? capturesLeaf(d.otherwise, seen) : false);
7086
+ 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);
6866
7087
  case "sepBy":
6867
7088
  return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen);
6868
7089
  case "many":
@@ -7340,6 +7561,7 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7340
7561
  for (const item of d.cases) {
7341
7562
  if (mayRecordRecoveryError(item.parser, recovery, seen)) return true;
7342
7563
  }
7564
+ if (d.matchers?.some((item) => mayRecordRecoveryError(item.parser, recovery, seen))) return true;
7343
7565
  return d.otherwise ? mayRecordRecoveryError(d.otherwise, recovery, seen) : false;
7344
7566
  }
7345
7567
  case "attempt":
@@ -7374,53 +7596,120 @@ function mayRecordRecoveryError(p, recovery, seen = /* @__PURE__ */ new Set()) {
7374
7596
  return false;
7375
7597
  }
7376
7598
  }
7377
- function emitDispatchCombinator(def, ctx, pos) {
7599
+ function emitDispatchCombinator(parser2, def, ctx, pos) {
7600
+ if (parserUsesRouted2(def.selector)) {
7601
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
7602
+ }
7603
+ const coverageIds = ctx.coverage?.plan.dispatches.get(parser2) ?? [];
7604
+ const branchUsesRouted2 = (entry) => entry.usesRouted === true || parserUsesRouted2(entry.parser);
7605
+ const hasRoutedBranch = def.cases.some(branchUsesRouted2) || (def.matchers ? def.matchers.some(branchUsesRouted2) : false) || (def.otherwise ? def.otherwiseUsesRouted === true || parserUsesRouted2(def.otherwise) : false);
7606
+ const selectorLeafMark = hasRoutedBranch ? v(ctx, "_dsl") : null;
7607
+ const selectorRawMark = hasRoutedBranch ? v(ctx, "_dsr") : null;
7608
+ const selectorTriviaMark = hasRoutedBranch ? v(ctx, "_dst") : null;
7609
+ const selectorLogMark = hasRoutedBranch ? v(ctx, "_dsg") : null;
7610
+ const selectorFieldMark = hasRoutedBranch ? v(ctx, "_dsf") : null;
7611
+ const selectorErrorMark = hasRoutedBranch ? v(ctx, "_dse") : null;
7378
7612
  const selector = emit(def.selector, ctx, pos);
7379
7613
  const outV = v(ctx, "_dval");
7380
7614
  const outE = v(ctx, "_dend");
7381
7615
  const keyV = v(ctx, "_dkey");
7382
7616
  const stmts = [
7617
+ ...selectorLeafMark ? [
7618
+ `${ind(ctx)}const ${selectorLeafMark} = _ctx._cstLeaves?.length ?? 0`,
7619
+ `${ind(ctx)}const ${selectorRawMark} = _ctx._cstRawChildren?.length ?? 0`,
7620
+ `${ind(ctx)}const ${selectorTriviaMark} = _ctx._cstTriviaLog?.length ?? 0`,
7621
+ `${ind(ctx)}const ${selectorLogMark} = _ctx._triviaLog?.length ?? 0`,
7622
+ `${ind(ctx)}const ${selectorFieldMark} = _ctx._fields?.length ?? 0`,
7623
+ `${ind(ctx)}const ${selectorErrorMark} = _ctx._errors?.length ?? 0`
7624
+ ] : [],
7383
7625
  ...selector.stmts,
7384
7626
  `${ind(ctx)}const ${keyV} = ${selector.valueVar}`,
7385
7627
  `${ind(ctx)}let ${outV}, ${outE} = ${selector.endVar}`
7386
7628
  ];
7387
7629
  let wroteBranch = false;
7388
- const emitSelectedTail = (parser2, keyword, condition) => {
7630
+ const emitSelectedTail = (entry, keyword, condition, coverageId) => {
7631
+ const parser3 = entry.parser;
7389
7632
  const head = keyword === "else" ? `${ind(ctx)}else {` : `${ind(ctx)}${keyword} (${condition}) {`;
7390
7633
  stmts.push(head);
7391
7634
  ctx.indent++;
7392
- const mayError = mayRecordRecoveryError(parser2, !!ctx.recovery);
7635
+ const usesRouted = branchUsesRouted2(entry);
7636
+ const routedV = usesRouted ? v(ctx, "_drt") : null;
7637
+ const mayError = mayRecordRecoveryError(parser3, !!ctx.recovery);
7393
7638
  const errMark = mayError ? v(ctx, "_derr") : null;
7394
7639
  const logMark = ctx.activeTrivia ? v(ctx, "_dlog") : null;
7640
+ if (routedV) {
7641
+ stmts.push(
7642
+ `${ind(ctx)}const ${routedV} = _ctx._routed; _ctx._routed = { value: ${selector.valueVar}, span: { start: ${pos}, end: ${selector.endVar} } }`,
7643
+ ...selectorLeafMark ? [
7644
+ `${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}`
7645
+ ] : []
7646
+ );
7647
+ }
7395
7648
  if (errMark) stmts.push(`${ind(ctx)}const ${errMark} = _ctx._errors?.length ?? 0`);
7396
7649
  if (logMark) stmts.push(`${ind(ctx)}const ${logMark} = _ctx._triviaLog?.length ?? 0`);
7397
- const tail = emitTailFallible(parser2, ctx, selector.endVar);
7650
+ const tail = emitTailFallible(parser3, ctx, usesRouted ? pos : selector.endVar);
7651
+ stmts.push(...coverageAttempt(ctx, coverageId, pos));
7398
7652
  const errorRollback = errMark ? `if (_ctx._errors && _ctx._errors.length !== ${errMark}) _ctx._errors.length = ${errMark}; ` : "";
7399
7653
  const logRollback = logMark ? `if (_ctx._triviaLog && _ctx._triviaLog.length !== ${logMark}) _ctx._triviaLog.length = ${logMark}; ` : "";
7654
+ const routedRollback = routedV ? `_ctx._routed = ${routedV}; ` : "";
7655
+ const coverageFailure = coverageId === void 0 ? "" : `_ctx._grammarTrace?.write({ id: ${JSON.stringify(coverageId)}, phase: 'failure', offset: _ctx._fe }); `;
7400
7656
  stmts.push(
7401
7657
  ...tail.stmts,
7402
- `${ind(ctx)}if (!${tail.okVar}) { ${errorRollback}${logRollback}${committedFailBody(ctx)} }`,
7658
+ `${ind(ctx)}if (!${tail.okVar}) { ${routedRollback}${errorRollback}${logRollback}${coverageFailure}${committedFailBody(ctx)} }`,
7659
+ ...routedV ? [`${ind(ctx)}_ctx._routed = ${routedV}`] : [],
7403
7660
  `${ind(ctx)}${outV} = [${selector.valueVar}, ${tail.valVar}]`,
7404
- `${ind(ctx)}${outE} = ${tail.endVar}`
7661
+ `${ind(ctx)}${outE} = ${tail.endVar}`,
7662
+ ...coverageHit(ctx, coverageId, pos, tail.endVar)
7405
7663
  );
7406
7664
  ctx.indent--;
7407
7665
  stmts.push(`${ind(ctx)}}`);
7408
7666
  };
7667
+ let coverageIndex = 0;
7409
7668
  for (const entry of def.cases) {
7410
- const condition = entry.keys.map((key) => `${keyV} === ${JSON.stringify(key)}`).join(" || ");
7411
- emitSelectedTail(entry.parser, wroteBranch ? "else if" : "if", condition);
7669
+ const condition = entry.keys.map((key) => emitDispatchKeyCondition(keyV, key, entry.caseInsensitive)).join(" || ");
7670
+ emitSelectedTail(entry, wroteBranch ? "else if" : "if", condition, coverageIds[coverageIndex++]);
7412
7671
  wroteBranch = true;
7413
7672
  }
7673
+ const emitMatcherTails = () => {
7674
+ for (const matcher of def.matchers ?? []) {
7675
+ emitSelectedTail(matcher, wroteBranch ? "else if" : "if", emitDispatchMatcherCondition(keyV, matcher), coverageIds[coverageIndex++]);
7676
+ wroteBranch = true;
7677
+ }
7678
+ };
7414
7679
  if (def.otherwise) {
7415
- if (wroteBranch) emitSelectedTail(def.otherwise, "else");
7416
- else emitSelectedTail(def.otherwise, "if", "true");
7680
+ emitMatcherTails();
7681
+ const otherwiseEntry = { parser: def.otherwise, usesRouted: def.otherwiseUsesRouted };
7682
+ if (wroteBranch) emitSelectedTail(otherwiseEntry, "else", void 0, coverageIds[coverageIndex++]);
7683
+ else emitSelectedTail(otherwiseEntry, "if", "true", coverageIds[coverageIndex++]);
7417
7684
  } else {
7685
+ emitMatcherTails();
7418
7686
  const expected = JSON.stringify(def.cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key))));
7419
7687
  const fail = failArrBody(ctx, expected, selector.endVar);
7420
7688
  stmts.push(wroteBranch ? `${ind(ctx)}else { ${fail} }` : `${ind(ctx)}${fail}`);
7421
7689
  }
7422
7690
  return { stmts, valueVar: outV, endVar: outE };
7423
7691
  }
7692
+ function emitDispatchKeyCondition(valueVar, key, caseInsensitive) {
7693
+ if (!caseInsensitive) return `${valueVar} === ${JSON.stringify(key)}`;
7694
+ const parts = [`${valueVar}.length === ${key.length}`];
7695
+ for (let i = 0; i < key.length; i++) {
7696
+ parts.push(foldEq(`${valueVar}.charCodeAt(${i})`, key.charCodeAt(i)));
7697
+ }
7698
+ return parts.join(" && ");
7699
+ }
7700
+ function emitDispatchMatcherCondition(valueVar, matcher) {
7701
+ if (matcher.kind === "matches") {
7702
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
7703
+ return `${new RegExp(matcher.value, flags).toString()}.test(${valueVar})`;
7704
+ }
7705
+ const text = matcher.value;
7706
+ const parts = [`${valueVar}.length >= ${text.length}`];
7707
+ const offset = matcher.kind === "endsWith" ? `${valueVar}.length - ${text.length}` : "0";
7708
+ for (let i = 0; i < text.length; i++) {
7709
+ parts.push(matcher.caseInsensitive ? foldEq(`${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`})`, text.charCodeAt(i)) : `${valueVar}.charCodeAt(${offset}${i === 0 ? "" : ` + ${i}`}) === ${text.charCodeAt(i)}`);
7710
+ }
7711
+ return parts.join(" && ");
7712
+ }
7424
7713
  function emitGreedyClassify(def, superIndex, ctx, pos, coverageBase) {
7425
7714
  const superParser = def.parsers[superIndex];
7426
7715
  const regexDef = getCoreRegexDef(superParser);
@@ -8075,6 +8364,18 @@ function emitPeek(def, ctx, pos) {
8075
8364
  endVar: pos
8076
8365
  };
8077
8366
  }
8367
+ function emitRouted(ctx, pos) {
8368
+ const item = v(ctx, "_rt");
8369
+ return {
8370
+ stmts: [
8371
+ `${ind(ctx)}const ${item} = _ctx._routed`,
8372
+ ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
8373
+ ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
8374
+ ],
8375
+ valueVar: `${item}.value`,
8376
+ endVar: `${item}.span.end`
8377
+ };
8378
+ }
8078
8379
  function escapeTokenLiteral(value) {
8079
8380
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
8080
8381
  }
@@ -8215,14 +8516,20 @@ function emitLeaf(def, ctx, pos) {
8215
8516
  endVar: inner.endVar
8216
8517
  };
8217
8518
  }
8519
+ function emitNodeProjectExpr(type, project, chV) {
8520
+ const child = `${chV}[${project}]`;
8521
+ const missing = `(() => { throw new Error(${JSON.stringify(`node(${JSON.stringify(type)}) project child ${project} was not captured`)}) })()`;
8522
+ const value = `(${child} !== null && typeof ${child} === 'object' && ${child}._tag === 'leaf' ? ${child}.value : ${child})`;
8523
+ return `(${project} in ${chV} ? ${value} : ${missing})`;
8524
+ }
8218
8525
  function emitNode(def, ctx, pos) {
8219
8526
  if (def.type === void 0) {
8220
8527
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
8221
8528
  }
8222
- const structural = def.build === void 0;
8223
- const mkType = structural ? null : analyzeMkInlineBuild(def);
8529
+ const structural = def.build === void 0 && def.project === void 0;
8530
+ const mkType = structural || def.project !== void 0 ? null : analyzeMkInlineBuild(def);
8224
8531
  let buildIdx = null;
8225
- if (!mkType && !structural) {
8532
+ if (!mkType && !structural && def.project === void 0) {
8226
8533
  buildIdx = ctx.buildFns.length;
8227
8534
  ctx.buildFns.push(def.build);
8228
8535
  ctx.buildSrcs.push(def.buildSrc ?? null);
@@ -8230,12 +8537,13 @@ function emitNode(def, ctx, pos) {
8230
8537
  const i = ind(ctx);
8231
8538
  const cstOut = ctx.hostMode === "cst" && !structural;
8232
8539
  if (!structural && !cstOut) ctx.hostBranchElided = true;
8233
- const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
8234
- const clonesState = !structural && (cstOut || buildReadsState(def));
8235
- const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
8236
- const capturesRaw = !structural && (cstOut || mkType !== null || buildReadsRaw(def));
8540
+ const hasProject = def.project !== void 0;
8541
+ const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && !hasProject && buildReadsTrivia(def);
8542
+ const clonesState = !structural && (cstOut || !hasProject && buildReadsState(def));
8543
+ const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || def.project !== void 0 || buildReadsChildren(def));
8544
+ const capturesRaw = !structural && (cstOut || mkType !== null || def.project === void 0 && buildReadsRaw(def));
8237
8545
  const hasFields = parserHasOwnFields(def.parser);
8238
- const capturesFields = hasFields && !structural && (cstOut || buildReadsFields(def));
8546
+ const capturesFields = hasFields && !structural && (cstOut || !hasProject && buildReadsFields(def));
8239
8547
  const innerEnablesTriviaCapture = parserEnablesTriviaCapture(def.parser);
8240
8548
  const chV = v(ctx, "_ch");
8241
8549
  const rawV = v(ctx, "_raw");
@@ -8333,7 +8641,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
8333
8641
  );
8334
8642
  }
8335
8643
  const ndV = v(ctx, "_nd");
8336
- 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})`;
8644
+ 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})`;
8337
8645
  const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${JSON.stringify(def.type)}, ${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
8338
8646
  const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
8339
8647
  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;
@@ -8540,7 +8848,7 @@ function emitDispatch(p, ctx, pos) {
8540
8848
  case "choice":
8541
8849
  return emitChoice(p, def, ctx, pos);
8542
8850
  case "dispatch":
8543
- return emitDispatchCombinator(def, ctx, pos);
8851
+ return emitDispatchCombinator(p, def, ctx, pos);
8544
8852
  case "attempt":
8545
8853
  return emitAttempt(p, def, ctx, pos);
8546
8854
  case "many":
@@ -8665,6 +8973,8 @@ function emitDispatch(p, ctx, pos) {
8665
8973
  return emitNot(def, ctx, pos);
8666
8974
  case "peek":
8667
8975
  return emitPeek(def, ctx, pos);
8976
+ case "routed":
8977
+ return emitRouted(ctx, pos);
8668
8978
  case "node":
8669
8979
  return emitNode(def, ctx, pos);
8670
8980
  case "scanTo":
@@ -8762,7 +9072,7 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8762
9072
  case "choice":
8763
9073
  return d.parsers.some((x) => hasNodeDef(x, seen));
8764
9074
  case "dispatch":
8765
- return hasNodeDef(d.selector, seen) || d.cases.some((x) => hasNodeDef(x.parser, seen)) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false);
9075
+ 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);
8766
9076
  case "sepBy":
8767
9077
  return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen);
8768
9078
  case "scanTo":
@@ -8777,6 +9087,26 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
8777
9087
  return false;
8778
9088
  }
8779
9089
  }
9090
+ function parserUsesRouted2(p, seen = /* @__PURE__ */ new Set()) {
9091
+ if (seen.has(p)) return false;
9092
+ seen.add(p);
9093
+ const d = p._def;
9094
+ switch (d.tag) {
9095
+ case "routed":
9096
+ return true;
9097
+ case "lazy": {
9098
+ try {
9099
+ return parserUsesRouted2(d.thunk(), seen);
9100
+ } catch {
9101
+ return false;
9102
+ }
9103
+ }
9104
+ case "dispatch":
9105
+ return false;
9106
+ default:
9107
+ return childrenOf2(d).some((child) => parserUsesRouted2(child, seen));
9108
+ }
9109
+ }
8780
9110
  function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
8781
9111
  if (seen.has(p)) return false;
8782
9112
  seen.add(p);
@@ -8797,7 +9127,7 @@ function childrenOf2(def) {
8797
9127
  case "choice":
8798
9128
  return def.parsers;
8799
9129
  case "dispatch":
8800
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9130
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
8801
9131
  case "many":
8802
9132
  case "attempt":
8803
9133
  case "optional":
@@ -8834,6 +9164,7 @@ function childrenOf2(def) {
8834
9164
  case "regex":
8835
9165
  case "keywords":
8836
9166
  case "guard":
9167
+ case "routed":
8837
9168
  case "unknown":
8838
9169
  return [];
8839
9170
  }
@@ -9541,7 +9872,7 @@ function childrenOf3(def) {
9541
9872
  case "choice":
9542
9873
  return def.parsers;
9543
9874
  case "dispatch":
9544
- return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.otherwise ? [def.otherwise] : []];
9875
+ return [def.selector, ...def.cases.map((entry) => entry.parser), ...def.matchers ? def.matchers.map((entry) => entry.parser) : [], ...def.otherwise ? [def.otherwise] : []];
9545
9876
  case "many":
9546
9877
  case "oneOrMore":
9547
9878
  case "optional":
@@ -9570,6 +9901,7 @@ function childrenOf3(def) {
9570
9901
  case "literal":
9571
9902
  case "regex":
9572
9903
  case "keywords":
9904
+ case "routed":
9573
9905
  case "guard":
9574
9906
  case "recover":
9575
9907
  case "unknown":
@@ -9594,6 +9926,20 @@ function repeatOpts(min, max, trailing) {
9594
9926
  if (trailing !== void 0) parts.push(`trailing: ${JSON.stringify(trailing)}`);
9595
9927
  return parts.length === 0 ? "" : `, { ${parts.join(", ")} }`;
9596
9928
  }
9929
+ function projectOpt(project) {
9930
+ if (project === void 0) return void 0;
9931
+ return `project: ${project}`;
9932
+ }
9933
+ function matcherExpr(entry) {
9934
+ switch (entry.kind) {
9935
+ case "startsWith":
9936
+ return `startsWith(${JSON.stringify(entry.value)})`;
9937
+ case "endsWith":
9938
+ return `endsWith(${JSON.stringify(entry.value)})`;
9939
+ case "matches":
9940
+ return `matches(${new RegExp(entry.value, entry.flags ?? "").toString()})`;
9941
+ }
9942
+ }
9597
9943
  function serializeRuleMap(ruleMap, scanSkip) {
9598
9944
  try {
9599
9945
  return new Serializer(ruleMap, scanSkip ?? []).run();
@@ -9693,7 +10039,11 @@ function evalRuleMapIR(ir) {
9693
10039
  "choice",
9694
10040
  "dispatch",
9695
10041
  "when",
10042
+ "startsWith",
10043
+ "endsWith",
10044
+ "matches",
9696
10045
  "otherwise",
10046
+ "routed",
9697
10047
  "attempt",
9698
10048
  "many",
9699
10049
  "oneOrMore",
@@ -9729,7 +10079,11 @@ function evalRuleMapIR(ir) {
9729
10079
  choice,
9730
10080
  dispatch,
9731
10081
  when,
10082
+ startsWith,
10083
+ endsWith,
10084
+ matches,
9732
10085
  otherwise,
10086
+ routed,
9733
10087
  attempt,
9734
10088
  many,
9735
10089
  oneOrMore,
@@ -9917,8 +10271,13 @@ ${outerDecls.map((d) => " " + d).join("\n")}
9917
10271
  }
9918
10272
  case "dispatch": {
9919
10273
  const arms = def.cases.map(
9920
- (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)})`
10274
+ (entry) => `when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`
9921
10275
  );
10276
+ if (def.matchers) {
10277
+ for (const entry of def.matchers) {
10278
+ arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ", { caseInsensitive: true }" : ""})`);
10279
+ }
10280
+ }
9922
10281
  if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`);
9923
10282
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? "" : `, ${arms.join(", ")}`})`;
9924
10283
  }
@@ -9938,6 +10297,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
9938
10297
  return `not(${kid(def.parser)})`;
9939
10298
  case "peek":
9940
10299
  return `peek(${kid(def.parser)})`;
10300
+ case "routed":
10301
+ return "routed()";
9941
10302
  case "trivia":
9942
10303
  return `trivia(${kid(def.parser)})`;
9943
10304
  case "token":
@@ -9962,9 +10323,11 @@ ${outerDecls.map((d) => " " + d).join("\n")}
9962
10323
  }
9963
10324
  case "node": {
9964
10325
  if (def.build !== void 0 && def.buildSrc === void 0) throw new Unserializable("node build without buildSrc");
10326
+ const projectEntry = projectOpt(def.project);
9965
10327
  const optEntries = [
9966
10328
  ...def.unwrap ? ["unwrap: true"] : [],
9967
10329
  ...def.collapse ? ["collapse: true"] : [],
10330
+ ...projectEntry === void 0 ? [] : [projectEntry],
9968
10331
  ...def.captureTrivia ? ["captureTrivia: true"] : [],
9969
10332
  ...def.trailingTrivia ? ["trailingTrivia: true"] : []
9970
10333
  ];
@@ -10959,10 +11322,20 @@ function run(entry, input, options = {}) {
10959
11322
  }
10960
11323
 
10961
11324
  // src/coverage.ts
11325
+ function dispatchMatcher(entry) {
11326
+ switch (entry.kind) {
11327
+ case "startsWith":
11328
+ return startsWith(entry.value);
11329
+ case "endsWith":
11330
+ return endsWith(entry.value);
11331
+ case "matches":
11332
+ return matches(new RegExp(entry.value, entry.flags));
11333
+ }
11334
+ }
10962
11335
  var GRAMMAR_COVERAGE_DEFINITIONS = /* @__PURE__ */ Symbol.for("parseman.grammarCoverageDefinitions");
10963
11336
  function compiledGrammarCoverageDefinitions(grammar) {
10964
11337
  const definitions = grammar[GRAMMAR_COVERAGE_DEFINITIONS];
10965
- 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"))) {
11338
+ 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"))) {
10966
11339
  throw new TypeError("grammar has no coverage definitions; enable grammarCoverage for this macro build");
10967
11340
  }
10968
11341
  return definitions;
@@ -11142,10 +11515,54 @@ function coverageEntry(entry, collector, maps, trace) {
11142
11515
  return unsafeSequence(...def.parsers.map(build));
11143
11516
  }
11144
11517
  case "dispatch": {
11145
- const arms = def.cases.map((entry2) => when(entry2.keys, build(entry2.parser)));
11146
- if (def.otherwise) arms.push(otherwise(build(def.otherwise)));
11518
+ const ids = maps.dispatches.get(parser2) ?? [];
11519
+ let armIndex = 0;
11520
+ const dispatchStarts = [];
11521
+ const wrapArm = (arm, id) => {
11522
+ const child = build(arm);
11523
+ return {
11524
+ _tag: child._tag,
11525
+ _meta: child._meta,
11526
+ _def: child._def,
11527
+ parse(input, pos, ctx) {
11528
+ const dispatchStart = dispatchStarts[dispatchStarts.length - 1] ?? pos;
11529
+ if (id) trace?.write({ id, phase: "attempt", offset: dispatchStart });
11530
+ const result = child.parse(input, pos, ctx);
11531
+ if (result.ok) {
11532
+ if (id) {
11533
+ collector.hit(id);
11534
+ trace?.write({ id, phase: "selected", offset: dispatchStart, end: result.span.end });
11535
+ trace?.write({ id, phase: "success", offset: dispatchStart, end: result.span.end });
11536
+ }
11537
+ } else if (id) {
11538
+ trace?.write({ id, phase: "failure", offset: result.span.end });
11539
+ }
11540
+ return result;
11541
+ }
11542
+ };
11543
+ };
11544
+ const arms = def.cases.map(
11545
+ (entry2) => when(entry2.keys, wrapArm(entry2.parser, ids[armIndex++]), entry2.caseInsensitive ? { caseInsensitive: true } : void 0)
11546
+ );
11547
+ if (def.matchers) {
11548
+ for (const entry2 of def.matchers) {
11549
+ arms.push(when(dispatchMatcher(entry2), wrapArm(entry2.parser, ids[armIndex++]), entry2.caseInsensitive ? { caseInsensitive: true } : void 0));
11550
+ }
11551
+ }
11552
+ if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])));
11147
11553
  const unsafeDispatch = dispatch;
11148
- return unsafeDispatch(build(def.selector), ...arms);
11554
+ const base2 = unsafeDispatch(build(def.selector), ...arms);
11555
+ return {
11556
+ ...base2,
11557
+ parse(input, pos, ctx) {
11558
+ dispatchStarts.push(pos);
11559
+ try {
11560
+ return base2.parse(input, pos, ctx);
11561
+ } finally {
11562
+ dispatchStarts.pop();
11563
+ }
11564
+ }
11565
+ };
11149
11566
  }
11150
11567
  case "many":
11151
11568
  return many(build(def.parser));
@@ -11190,7 +11607,7 @@ function coverageEntry(entry, collector, maps, trace) {
11190
11607
  case "peek":
11191
11608
  return peek(build(def.parser));
11192
11609
  case "node": {
11193
- const opts = { ...def.unwrap ? { unwrap: true } : {}, ...def.collapse ? { collapse: true } : {}, ...def.captureTrivia ? { captureTrivia: true } : {}, ...def.trailingTrivia ? { trailingTrivia: true } : {} };
11610
+ 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 } : {} };
11194
11611
  return def.type === void 0 ? node(build(def.parser), def.build, opts) : node(def.type, build(def.parser), def.build, opts);
11195
11612
  }
11196
11613
  case "guard":
@@ -11593,6 +12010,7 @@ export {
11593
12010
  cstBuildHost,
11594
12011
  dispatch,
11595
12012
  duplicationFindingCount,
12013
+ endsWith,
11596
12014
  expect,
11597
12015
  extractCharClasses,
11598
12016
  field,
@@ -11615,8 +12033,10 @@ export {
11615
12033
  lineBreaksIn,
11616
12034
  lineStartWithin,
11617
12035
  literal,
12036
+ makeWhen,
11618
12037
  makeWord,
11619
12038
  many,
12039
+ matches,
11620
12040
  noTrivia,
11621
12041
  node,
11622
12042
  not,
@@ -11633,6 +12053,7 @@ export {
11633
12053
  regex,
11634
12054
  relativize,
11635
12055
  relativizeCST,
12056
+ routed,
11636
12057
  rules,
11637
12058
  run,
11638
12059
  runWithGrammarCoverage,
@@ -11642,6 +12063,7 @@ export {
11642
12063
  shiftAbsolute,
11643
12064
  siteToString,
11644
12065
  skip,
12066
+ startsWith,
11645
12067
  token,
11646
12068
  transform,
11647
12069
  trivia,