Skip to content

feat(dummies): generate strings matching a regex via StringMatching - #185

Merged
Reefact merged 4 commits into
mainfrom
claude/dummies-matching
Jul 19, 2026
Merged

feat(dummies): generate strings matching a regex via StringMatching#185
Reefact merged 4 commits into
mainfrom
claude/dummies-matching

Conversation

@Reefact

@Reefact Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Any.StringMatching(pattern) to the Dummies DSL: 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 with System.Text.RegularExpressions and verified against it as an oracle.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation

Changes

  • Engine (all internal): RegexParser (recursive descent over the regular subset) → RegexNode tree (characters / sequence / alternation / repeat) walked at Generate() time, every choice drawn from the seeded context — reproducible like every other generator; RegexAlphabet scopes terminals to printable ASCII so dummies stay legible and every emitted character genuinely matches.
  • Dialect (mirroring the real engine, verified empirically against it): literals; escapes \t \n \r \f \v \a \e, \xHH, \uHHHH, \cX, octal \0nn (and [\1]-style octal inside classes, where \b is 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.
  • First-class refusal — never a silently non-matching value: non-regular constructs (lookaround, backreferences, \b boundaries, Unicode categories) raise UnsupportedRegexException naming 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) raise ArgumentException. A 256-level nesting cap and a generation length limit keep hostile patterns from a StackOverflow or unbounded expansion.
  • API: terminal AnyPattern — materialized only through Generate(), per ADR-0020; no chainable shape/length constraints (the pattern is the whole spec); composes through As, OrNull, Combine, collections. Entry points Any.StringMatching(string) / Any.StringMatching(Regex) (reuses the very Regex the production code validates with; IgnoreCase honoured, IgnorePatternWhitespace rejected — it changes how the pattern text itself is read; the remaining options don't change the matched language and are ignored) + the AnyContext seeded twins.
  • Tests (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, .As composition, seed reproducibility, the full rejection taxonomy (incl. the four Codex findings), and the depth guard. The seed reproducibility batch gains a pattern draw.
  • Docs: NuGet readme gains a "strings from a regex" bullet; ADR-0025 drafted (EN + FR) and indexed.
  • Review follow-up (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.
  • ADR-0020 alignment (cad0d27): ADR-0020 (materialize only through Generate()) was accepted on main while this branch was in flight; AnyPattern's implicit string conversion is dropped and every usage materializes through Generate(), matching the refactored suite.

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings (both netstandard2.0 and net8.0 legs), re-verified after the rebase onto main (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).
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — ran as part of the full suite above.

Documentation

  • Public API / error documentation updated — full XML docs on the new public surface (AnyPattern, UnsupportedRegexException, the facade entries).
  • README / doc/ updated — Dummies/README.nuget.md and ADR-0025.
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

The 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

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-0025
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

ADR-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 main twice as it advanced (ADR renumbered 0017 → 0018 → 0025 after intervening ADRs landed).

🤖 Generated with Claude Code

Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Verification notes — how the engine's correctness was established

Since 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 engine

A throwaway console program ran ~30 edge-case patterns through System.Text.RegularExpressions itself (validity + match checks) to establish what .NET actually does, instead of trusting assumptions. That pass confirmed five real divergences in the initial implementation, all fixed in this PR:

Case .NET behavior Initial implementation Fix
\a \e \x41 A \cA \0 \07 BEL / ESC / hex / control / octal characters generated the literal letter ("a", "x41", …) — a non-matching value escapes implemented exactly
\q and other unknown word-character escapes rejected (ArgumentException) accepted as literals rejected, mirroring .NET
a*+ (possessive) rejected — possessive quantifiers don't exist in .NET accepted and ignored rejected
a{x}, {abc}, a{2, literals (a brace that isn't a well-formed quantifier) rejected literal fallback; {2} with nothing to repeat stays an error, as in .NET
(?>…) atomic group valid, and regular (backtracking-only) wrongly rejected supported, generates like (?:…)

Plus, surfaced by the same pass: \b inside a class is backspace (never a word boundary), a class digit escape is octal ([\1]), an empty group name is rejected, and a 256-level nesting cap prevents a hostile pattern from overflowing the stack (an uncatchable process kill).

The pass also refuted two suspected bugs[]a] / [^]] (first-position ] is a literal in .NET, matching the implementation) and [a-b-z] (the dash after a range is a literal; the parser already produced exactly {a, b, -, z}) — which is precisely why the check was run against the real engine rather than from memory.

2. Property oracle in the test suite

AnyPatternTests.GeneratedValuesMatchTheRealEngine asserts that every generated value is fully matched by the real .NET engine, anchored as ^(?:pattern)$ so both under-generation and over-generation fail loudly. It covers 43 patterns × 200 draws each — including UUID/IBAN-like/time/semver shapes and every tricky case from the table above. This oracle stays in the suite, so any future parser change is re-verified against the real engine on every CI run.

3. Rejection taxonomy

Dedicated facts pin the error contract both ways: non-regular constructs (lookaround, backreferences, \b boundaries, \p{…}) raise UnsupportedRegexException naming the construct, and patterns .NET itself rejects raise ArgumentException — so "accepted by Dummies" stays aligned with "accepted by .NET", and no pattern ever yields a silently non-matching value.


Generated by Claude Code

Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@codex please review this PR in depth. It introduces the riskiest piece of code in the Dummies library so far: a hand-written regex parser and generator (Any.StringMatching). The verification notes in the previous comment describe how correctness was established (empirical ground truth against System.Text.RegularExpressions, plus an anchored property oracle in the suite); please try to break what that net might have missed.

Where scrutiny pays off most:

  1. Dummies/RegexParser.cs — the highest-risk file. Hunt for index/bounds mistakes near end-of-pattern, incorrect restore points in the brace-quantifier literal fallback (ReadBraceQuantifier returning null after partially consuming), character-class parsing (ranges, negation, first-position ], escapes and octal inside classes), and any pattern where our accept/reject verdict or the produced character set silently diverges from System.Text.RegularExpressions. A concrete counter-example pattern is the ideal finding format — it can be checked directly against the in-suite oracle.
  2. Dummies/RegexNode.cs — generation semantics: quantifier count math (inclusive bounds, unbounded = min + 0..8), the generation length limit, and whether IgnoreCase expansion (Literal/ExpandCase, including negated classes) can ever emit a character the case-insensitive .NET pattern would not match.
  3. Dummies/RegexAlphabet.cs — set soundness: every character each shorthand (\d \D \w \W \s \S, dot) can emit must genuinely match that shorthand under .NET semantics.
  4. Tests (Dummies.UnitTests/AnyPatternTests.cs) — blind spots in the oracle corpus: if you see a supported-dialect pattern family that is not covered and could plausibly fail, propose it.
  5. API & docsAnyPattern (terminal, implicit conversion), the Any/AnyContext.StringMatching entries, UnsupportedRegexException, and whether the XML docs and ADR-0018 accurately describe the implemented dialect.

By-design decisions, recorded in ADR-0018 — please don't relitigate them: the supported dialect is the regular subset only (lookaround/backreferences/\b boundaries/\p{…} are rejected eagerly, not generated); terminals draw from printable ASCII; unbounded quantifiers draw min + 0..8; the generator is deliberately terminal (no chainable length/shape constraints — the pattern is the whole specification); zero runtime dependencies.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread Dummies/RegexParser.cs Outdated
Comment thread Dummies/RegexParser.cs Outdated
Comment thread Dummies/Any.cs
Comment thread Dummies/RegexParser.cs
@Reefact
Reefact force-pushed the claude/dummies-matching branch from 20ced45 to cad0d27 Compare July 19, 2026 23:00
claude added 4 commits July 19, 2026 23:11
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
@Reefact
Reefact force-pushed the claude/dummies-matching branch from cad0d27 to 444ee2d Compare July 19, 2026 23:11
@Reefact
Reefact enabled auto-merge July 19, 2026 23:13
@Reefact
Reefact merged commit 967a766 into main Jul 19, 2026
15 checks passed
@Reefact
Reefact deleted the claude/dummies-matching branch July 19, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants