Problem
SkipGroupName (RegexParser.cs:295-300) scans to the group-name terminator without validating the captured name:
while (!AtEnd && Peek() != terminator) { _index++; }
Anything between (?< and > (or between the quotes of the (?'name') form) is treated as an ordinary named group. Two well-formed .NET constructs slip through:
- A balancing group
(?<-name>…) / (?<name1-name2>…). This is non-regular — it manipulates a capture stack, the same family as backreferences, which the library proudly rejects. Any.StringMatching(@"(?<a>y)?(?<-a>x)") generates "x", which the real engine does not match (the pattern's language is exactly {"yx"} — the -a pop forces the optional a group to have fired).
- An invalid group name — e.g.
(?<a b>x) (a space) or (?<1a>x) (a digit-led name that is not a valid number). .NET rejects the name at compile time; Dummies accepts it.
Impact
This is the one place in the audited surface where the library's signature promise is broken. UnsupportedRegexException (and AnyPattern's docs, ADR-0025, and the package README) all state that a non-regular or malformed construct is "refused with a clear error rather than a silently non-matching value." Here a balancing-group pattern silently produces strings the pattern's real language excludes — the exact "silently non-matching value" the design forbids.
Direction
Validate the captured group name (in SkipGroupName or at its call sites):
- If it contains
-, throw Unsupported("a balancing group '(?<name1-name2>…)'", position) — non-regular, same treatment as backreferences. This applies even when the target group is undefined (e.g. (?<-a>x) on its own). .NET happens to reject that particular case as a malformed pattern ("reference to undefined group name"), because the group is never captured — but distinguishing a defined target (which .NET accepts, and which is genuinely non-regular) from an undefined one (which .NET rejects) would require tracking every captured group by name, a capture table this generator deliberately does not keep. We therefore treat any - in a group name as an unsupported balancing group. This is a deliberate, assumed divergence: for the undefined-target case the error kind differs from .NET (UnsupportedRegexException vs ArgumentException), but both reject the pattern and neither mis-generates — so the signature promise (never a silently non-matching value) holds.
- Otherwise, validate the name exactly as the real engine does:
- a name that opens with a digit is an explicit capture number, valid only as a positive integer with no leading zero —
1, 10 pass; 0 (reserved for the whole match), 01 and 1a are refused;
- any other name must be word characters (letter, digit or underscore);
- a violation throws
Malformed(...), mirroring the real engine.
The earlier phrasing of this rule — "letter/digit/underscore, not starting with a digit" — was incorrect: it would reject valid explicitly-numbered groups such as (?<1>x) and (?'2'x), which .NET accepts. The corrected rule above keeps them working.
Acceptance criteria
Any.StringMatching(@"(?<-a>x)") (undefined target) and @"(?<a>y)?(?<-a>x)") (defined target — a pattern .NET accepts) both throw UnsupportedRegexException naming the balancing group.
Any.StringMatching(@"(?<1a>x)"), @"(?<0>x)" and @"(?<01>x)" throw ArgumentException (malformed), matching .NET's rejection.
- Explicitly-numbered groups (
(?<1>x), (?'2'x), (?<10>ab)) and ordinary named / non-capturing groups continue to parse and generate unchanged.
- Tests cover both forms (
(?<…>) and (?'…')) and both rejection channels.
Context
Surfaced by the 2026-07-20 architecture & design audit of the standalone Dummies library (doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md, §4.1(d); branch claude/dummies-architecture-audit-cru5aq).
Filed from the Claude Code session that produced the 2026-07-20 Dummies architecture & design audit: https://claude.ai/code/session_01GvrWFVoG74xiys3oyEft1G
Problem
SkipGroupName(RegexParser.cs:295-300) scans to the group-name terminator without validating the captured name:Anything between
(?<and>(or between the quotes of the(?'name')form) is treated as an ordinary named group. Two well-formed .NET constructs slip through:(?<-name>…)/(?<name1-name2>…). This is non-regular — it manipulates a capture stack, the same family as backreferences, which the library proudly rejects.Any.StringMatching(@"(?<a>y)?(?<-a>x)")generates"x", which the real engine does not match (the pattern's language is exactly{"yx"}— the-apop forces the optionalagroup to have fired).(?<a b>x)(a space) or(?<1a>x)(a digit-led name that is not a valid number). .NET rejects the name at compile time; Dummies accepts it.Impact
This is the one place in the audited surface where the library's signature promise is broken.
UnsupportedRegexException(andAnyPattern's docs, ADR-0025, and the package README) all state that a non-regular or malformed construct is "refused with a clear error rather than a silently non-matching value." Here a balancing-group pattern silently produces strings the pattern's real language excludes — the exact "silently non-matching value" the design forbids.Direction
Validate the captured group name (in
SkipGroupNameor at its call sites):-, throwUnsupported("a balancing group '(?<name1-name2>…)'", position)— non-regular, same treatment as backreferences. This applies even when the target group is undefined (e.g.(?<-a>x)on its own). .NET happens to reject that particular case as a malformed pattern ("reference to undefined group name"), because the group is never captured — but distinguishing a defined target (which .NET accepts, and which is genuinely non-regular) from an undefined one (which .NET rejects) would require tracking every captured group by name, a capture table this generator deliberately does not keep. We therefore treat any-in a group name as an unsupported balancing group. This is a deliberate, assumed divergence: for the undefined-target case the error kind differs from .NET (UnsupportedRegexExceptionvsArgumentException), but both reject the pattern and neither mis-generates — so the signature promise (never a silently non-matching value) holds.1,10pass;0(reserved for the whole match),01and1aare refused;Malformed(...), mirroring the real engine.Acceptance criteria
Any.StringMatching(@"(?<-a>x)")(undefined target) and@"(?<a>y)?(?<-a>x)")(defined target — a pattern .NET accepts) both throwUnsupportedRegexExceptionnaming the balancing group.Any.StringMatching(@"(?<1a>x)"),@"(?<0>x)"and@"(?<01>x)"throwArgumentException(malformed), matching .NET's rejection.(?<1>x),(?'2'x),(?<10>ab)) and ordinary named / non-capturing groups continue to parse and generate unchanged.(?<…>)and(?'…')) and both rejection channels.Context
Surfaced by the 2026-07-20 architecture & design audit of the standalone
Dummieslibrary (doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md, §4.1(d); branchclaude/dummies-architecture-audit-cru5aq).Filed from the Claude Code session that produced the 2026-07-20 Dummies architecture & design audit: https://claude.ai/code/session_01GvrWFVoG74xiys3oyEft1G