feat(dummies): generate strings matching a regex via StringMatching - #185
Conversation
Verification notes — how the engine's correctness was establishedSince a hand-written regex engine is the riskiest piece of code in this library so far, its behavior was verified in three layers before this PR was opened. Recording the method and findings here for review context. 1. Empirical ground truth against the real engineA throwaway console program ran ~30 edge-case patterns through
Plus, surfaced by the same pass: The pass also refuted two suspected bugs — 2. Property oracle in the test suite
3. Rejection taxonomyDedicated facts pin the error contract both ways: non-regular constructs (lookaround, backreferences, Generated by Claude Code |
|
@codex please review this PR in depth. It introduces the riskiest piece of code in the Where scrutiny pays off most:
By-design decisions, recorded in ADR-0018 — please don't relitigate them: the supported dialect is the regular subset only (lookaround/backreferences/ Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3b3e43e47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
20ced45 to
cad0d27
Compare
Any.StringMatching(pattern) generates arbitrary strings a regular expression matches - the dummy for a format-validated value object. The engine is home-grown (zero dependencies, per the library's identity): a recursive-descent parser turns the regular subset of the pattern language into a node tree, and each Generate() walks it, drawing every choice from the seeded context, so runs stay reproducible. The generator is a terminal: the pattern is the whole specification, so it carries no further shape or length constraints - but it composes through As, OrNull, Combine and the collections. Supported: literals, escapes (\t-\v, \a, \e, \xHH, \uHHHH, \cX, octal), shorthands, classes with ranges/negation, quantifiers (unbounded ones draw min + 0..8), alternation, groups (incl. atomic and named), dot, anchors as no-ops. Terminals draw from printable ASCII. IgnoreCase is honoured on the Regex overload. Behavior is deliberately aligned with System.Text.RegularExpressions, verified against it as an oracle: non-regular constructs (lookaround, backreferences, \b, Unicode categories) raise a first-class UnsupportedRegexException; patterns the real engine rejects (unknown escapes, possessive quantifiers, empty classes) raise ArgumentException; a brace that is not a well-formed quantifier is a literal; \b inside a class is backspace; a class digit escape is octal. A depth cap and a generation length limit keep hostile patterns from overflowing the stack or expanding without bound. Tests: a property oracle asserts every generated value fully matches the real engine (anchored) across 43 patterns x 200 draws, plus facts for exact escapes, brace literals, quantifier bounds, alternation reach, IgnoreCase, composition, seed reproducibility, and the rejection taxonomy. Full suite green on both target legs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPrBqngpiSwdsF4DWvNPw
Record, as Proposed, the decision to generate matching strings from a home-grown parser over the regular subset of the pattern language - refusing non-regular constructs with a first-class exception - rather than taking a dependency on a regex-automaton library, preserving the zero-dependency identity guarded since ADR-0011. Includes the French translation and the index entry. Numbered 0025: the sequence advanced to 0024 on main in the meantime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPrBqngpiSwdsF4DWvNPw
Four Codex review findings, each a pattern for which the generator
could emit a value the real engine rejects; all now refused eagerly
instead of mis-generated:
- RegexOptions.IgnorePatternWhitespace changes how the pattern text
itself is read ("^A B$" matches "AB"), so the Regex overloads reject
it with ArgumentException; the remaining options still do not change
the matched language and stay ignored.
- Anchors are no-ops only at the start/end of the pattern or of a
top-level alternation branch, where a whole generated string is
guaranteed to satisfy them; a misplaced anchor (a^, $a, inside a
group) makes the pattern unmatchable and now raises
UnsupportedRegexException.
- Atomic groups commit to their first matching branch, so their
language is not that of the plain alternation ((?>ab|a)b matches
only "abb"); they are refused again rather than lowered to (?:...).
- .NET class subtraction ([a-z-[aeiou]]) is detected and refused;
parsing '-[' as members closed the class early and generated values
outside it.
Regression tests cover all four (plus ^a$|^b$ staying supported); the
oracle corpus swaps the atomic pattern for that anchored alternation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhPrBqngpiSwdsF4DWvNPw
ADR-0020, accepted on main while this branch was in flight, removes every implicit conversion from the generators: a value is materialized only by Generate(). Align the new pattern generator with it - drop AnyPattern's implicit string conversion and materialize through Generate() in its tests and doc examples, matching the rest of the suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhPrBqngpiSwdsF4DWvNPw
cad0d27 to
444ee2d
Compare
Summary
Adds
Any.StringMatching(pattern)to theDummiesDSL: a terminal generator of arbitrary strings that match a regular expression — the dummy for a format-validated value object — powered by a home-grown engine (zero dependencies) over the regular subset of the pattern language, with behavior deliberately aligned withSystem.Text.RegularExpressionsand verified against it as an oracle.Type of change
Changes
RegexParser(recursive descent over the regular subset) →RegexNodetree (characters / sequence / alternation / repeat) walked atGenerate()time, every choice drawn from the seeded context — reproducible like every other generator;RegexAlphabetscopes terminals to printable ASCII so dummies stay legible and every emitted character genuinely matches.\t \n \r \f \v \a \e,\xHH,\uHHHH,\cX, octal\0nn(and[\1]-style octal inside classes, where\bis backspace); shorthands\d \D \w \W \s \S; classes with ranges and negation; quantifiers? * + {n} {n,} {n,m}— unbounded ones draw min + 0..8, lazy?accepted, possessive rejected as .NET does; alternation; capturing/non-capturing/named groups; dot; anchors^ $at the start/end of the pattern or of a top-level alternation branch (no-ops there — a whole matching string is generated). A brace that is not a well-formed quantifier is a literal, exactly as in .NET.\bboundaries, Unicode categories) raiseUnsupportedRegexExceptionnaming the construct, and so do constructs a plain walk cannot honour — atomic groups (their first-branch commit is not language-equivalent to plain alternation), class subtraction[a-z-[aeiou]], and misplaced anchors (a^,$a: unmatchable by any whole string). Patterns the real engine rejects (unknown escapes,a*+, empty classes,{2}with nothing to repeat, empty group names) raiseArgumentException. A 256-level nesting cap and a generation length limit keep hostile patterns from a StackOverflow or unbounded expansion.AnyPattern— materialized only throughGenerate(), per ADR-0020; no chainable shape/length constraints (the pattern is the whole spec); composes throughAs,OrNull,Combine, collections. Entry pointsAny.StringMatching(string)/Any.StringMatching(Regex)(reuses the veryRegexthe production code validates with;IgnoreCasehonoured,IgnorePatternWhitespacerejected — it changes how the pattern text itself is read; the remaining options don't change the matched language and are ignored) + theAnyContextseeded twins.AnyPatternTests): a property oracle asserts every generated value is fully matched by the real .NET engine (anchored^(?:…)$, catching under- and over-generation) across 43 patterns × 200 draws — including UUID/IBAN-like/time/semver shapes and the tricky cases above; plus facts for exact escape characters, brace literals, quantifier bounds, alternation reach, IgnoreCase,.Ascomposition, seed reproducibility, the full rejection taxonomy (incl. the four Codex findings), and the depth guard. The seed reproducibility batch gains a pattern draw.df187b9): the four Codex findings —IgnorePatternWhitespace, misplaced anchors, atomic groups, class subtraction — are fixed as eager refusals, each with regression tests; see the resolved threads.cad0d27): ADR-0020 (materialize only throughGenerate()) was accepted onmainwhile this branch was in flight;AnyPattern's implicit string conversion is dropped and every usage materializes throughGenerate(), matching the refactored suite.Testing
dotnet build FirstClassErrors.sln— succeeded, 0 warnings (bothnetstandard2.0andnet8.0legs), re-verified after the rebase ontomain(post ADR-0020 refactor) and the compliance commit.dotnet test FirstClassErrors.sln— all green (Dummies 222; FCE 423; Analyzers 85; GenDoc 169; PropertyTests 21+3; Binder 152 + Usage 14; Cli 64).FirstClassErrors.Analyzers.UnitTests) — ran as part of the full suite above.Documentation
AnyPattern,UnsupportedRegexException, the facade entries).doc/updated —Dummies/README.nuget.mdand ADR-0025.doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changedThe handwritten EN/FR user guides stay deferred until the V1 surface stabilizes (ADR-0011 follow-up); only the packaged NuGet readme is kept current.
Architecture decisions
Proposed: ADR-0025ADR-0025 records the choice of a home-grown parser over a regex-automaton dependency (Fare), preserving the zero-dependency identity guarded since ADR-0011, with the first-class refusal of non-regular constructs as the safety net. The branch was also brought into line with the newly accepted ADR-0020 (no implicit conversions).
Rebased onto
maintwice as it advanced (ADR renumbered 0017 → 0018 → 0025 after intervening ADRs landed).🤖 Generated with Claude Code