Problem
Three small edges in the regex subsystem, all cosmetic and all failing in the safe direction (a refusal or a wrong message, never a mis-generated value):
- Misleading generation-limit message.
RegexNode.cs:31-37 throws with a fixed message blaming "a nested unbounded quantifier … expanding without bound." But Any.StringMatching(@"(a{1000}){1000}").Generate() hits the 65 536-character ceiling (AnyPattern.cs:36) with no unbounded quantifier at all — the pattern deterministically asks for 1,000,000 characters. In a library whose signature is precise, honest diagnostics, a confidently wrong cause is off-brand.
- Conservative refusals of valid patterns, handled asymmetrically.
^* is accepted by .NET but Dummies throws ArgumentException (^ consumed as a no-op, leaving * with nothing to repeat). abc$$ is accepted by .NET but Dummies throws UnsupportedRegexException, while the symmetric ^^abc is accepted — an avoidable inconsistency.
- Negated-class taxonomy. A well-formed negated class whose members lie outside the printable universe is reported as
ArgumentException ("malformed") instead of UnsupportedRegexException ("unsupported"). The ArgumentException-vs-UnsupportedRegexException boundary is advertised (RegexParser.cs:22-24) as mirroring the real engine; these edges overstate "malformed".
Impact
None mis-generates — every discrepancy is a refusal or a wrong message, so this is quality polish, not a contract break (contrast the balancing-group issue). But the misleading limit message can send a user hunting for a quantifier that does not exist, and the ^^/$$ asymmetry is a small inconsistency in a surface that otherwise reads as carefully symmetric.
Direction
- Reword the limit exception to state the fact and offer both causes, e.g. "The pattern produced a string longer than the 65536-character generation limit — a nested unbounded quantifier, or bounded quantifiers whose product exceeds the limit." Optionally compute the tree's minimum length at parse time and reject deterministically-over-limit patterns eagerly.
- Treat a
$ whose successors are only more $ or end-of-branch as a no-op, mirroring the existing ^ handling; optionally accept a quantified leading anchor as no-op-with-quantifier.
- Only treat
-[ as class subtraction when a member precedes it; classify well-formed-but-non-generatable negated classes as UnsupportedRegexException.
Any subset is acceptable — these are independent, low-priority polish items. Documenting them as known conservative refusals is a valid alternative for (2)/(3).
Acceptance criteria
- The over-limit message no longer asserts an unbounded quantifier when the pattern has none; a test covers
(a{1000}){1000}.
^^abc and abc$$ are handled consistently (both accepted as no-op anchors, or both refused with the same channel), covered by tests.
- Any change keeps every currently-accepted pattern generating a string the real .NET engine matches (the oracle test stays green).
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(e); 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
Three small edges in the regex subsystem, all cosmetic and all failing in the safe direction (a refusal or a wrong message, never a mis-generated value):
RegexNode.cs:31-37throws with a fixed message blaming "a nested unbounded quantifier … expanding without bound." ButAny.StringMatching(@"(a{1000}){1000}").Generate()hits the 65 536-character ceiling (AnyPattern.cs:36) with no unbounded quantifier at all — the pattern deterministically asks for 1,000,000 characters. In a library whose signature is precise, honest diagnostics, a confidently wrong cause is off-brand.^*is accepted by .NET but Dummies throwsArgumentException(^consumed as a no-op, leaving*with nothing to repeat).abc$$is accepted by .NET but Dummies throwsUnsupportedRegexException, while the symmetric^^abcis accepted — an avoidable inconsistency.ArgumentException("malformed") instead ofUnsupportedRegexException("unsupported"). TheArgumentException-vs-UnsupportedRegexExceptionboundary is advertised (RegexParser.cs:22-24) as mirroring the real engine; these edges overstate "malformed".Impact
None mis-generates — every discrepancy is a refusal or a wrong message, so this is quality polish, not a contract break (contrast the balancing-group issue). But the misleading limit message can send a user hunting for a quantifier that does not exist, and the
^^/$$asymmetry is a small inconsistency in a surface that otherwise reads as carefully symmetric.Direction
$whose successors are only more$or end-of-branch as a no-op, mirroring the existing^handling; optionally accept a quantified leading anchor as no-op-with-quantifier.-[as class subtraction when a member precedes it; classify well-formed-but-non-generatable negated classes asUnsupportedRegexException.Any subset is acceptable — these are independent, low-priority polish items. Documenting them as known conservative refusals is a valid alternative for (2)/(3).
Acceptance criteria
(a{1000}){1000}.^^abcandabc$$are handled consistently (both accepted as no-op anchors, or both refused with the same channel), covered by tests.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(e); 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