Skip to content

fix: route DFA_UNROLLED_WITH_GROUPS group-span divergences (A1+A2) to PIKEVM_CAPTURE - #90

Merged
jbachorik merged 27 commits into
mainfrom
fix/dfa-group-span-routing
Jun 30, 2026
Merged

jbachorik merged 27 commits into
mainfrom
fix/dfa-group-span-routing

Conversation

@jbachorik

Copy link
Copy Markdown
Collaborator

What does this PR do?

Routes two classes of DFA_UNROLLED_WITH_GROUPS group-span bugs to PIKEVM_CAPTURE, reducing the fuzz divergence budget from 65 to 13.

Motivation

The differential fuzzer (AlgorithmicFuzzTest.divergenceGate) reported 65 pre-existing divergences, all DFA_UNROLLED_WITH_GROUPS group-span bugs. Two root-cause classes were identified:

  • A2 — Group absent from some alternation branch: when pattern (a)|b matches via the branch without group 1, TDFA incorrectly binds group 1 instead of leaving it at [-1,-1).
  • A1 — Nullable first element inside capturing group: when a group body starts with a nullable element (e.g. a?, {0,n}), TDFA fires the group-start tag at an epsilon-reachable state, recording match-start instead of group-start.

Routing these patterns to PIKEVM_CAPTURE (linear-time NFA simulator) gives correct spans with no backtracking penalty.

Related Issue(s)

N/A

Change Type

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring (no functional change)
  • Documentation
  • Test improvement
  • Build/CI change

Checklist

  • I have read the CONTRIBUTING.md guidelines
  • All existing tests pass (./gradlew build)
  • I have added tests for my changes
  • I have updated documentation (if applicable)
  • My commits are signed

Performance Impact

Patterns with groups absent from some alternation branch or with nullable first group elements now route to PIKEVM_CAPTURE instead of DFA_UNROLLED_WITH_GROUPS. PikeVM runs in O(n·states) vs O(n) for TDFA, but these patterns were already producing wrong results on TDFA — correctness takes priority. All other patterns are unaffected.

Additional Notes

Implementation:

  • FallbackPatternDetector.hasCapturingGroupAbsentFromSomeAlternative() — detects A2 by collecting group numbers per alternative and checking for non-uniform sets
  • FallbackPatternDetector.hasCapturingGroupWithNullableFirstElement() — detects A1 by inspecting the first element of each capturing group's body via the existing isNullable() helper
  • Both routing conditions sit inside the existing !hasNamedGroups && !hasAnchorInNfa block, before the DFA_UNROLLED_WITH_GROUPS state-count check, each guarded by !hasNullableGroupContentWithNullableQuantifier

Remaining 13 divergences (out of scope for this PR) fall into three distinct classes:

  • Anchor-in-group / anchor-in-alternation: ($), $|[^0]{1}, $|[^c]{1} — outside the !hasAnchorInNfa routing block
  • Greedy group + required suffix overlap: (.+)_ — TDFA greedy group-end tag not rolled back when suffix character is also matched by the group
  • Different-length branches inside a group: ([1]|1.)[b]_ — TDFA group-end ambiguity when sibling alternatives have different lengths
  • Backreference: (.)\1{2}. — separate engine concern

@jbachorik jbachorik added the AI Generated or assisted by AI label Jun 28, 2026
@datadog-datadog-prod-us1-2

This comment has been minimized.

@jbachorik
jbachorik force-pushed the fix/dfa-group-span-routing branch from a392ffb to e137c29 Compare June 29, 2026 13:01
@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 7e1db7bdcc

ℹ️ 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".

Base automatically changed from fix/rebased-perconfig to main June 29, 2026 20:26
jbachorik and others added 23 commits June 29, 2026 22:33
Squash-merge of 78 commits from fix/optimized-nfa-backref-perconfig:

- Per-config captures for backref NFAs (Tasks 6-7)
- 64-bit NFA contentHashCode + verify-on-hit for structural cache
- Cache key NUL separator in RuntimeCompiler.cacheKeyFor()
- PikeVM per-call compilation (no cached volatile field)
- B16 guard: needsFallback check before PIKEVM_CAPTURE early return
- epsilonGroup flag on DFA.GroupAction for zero-width group tracking
- CRLF/end-anchor fixes (\Z/$ NEL/LS/PS) across all bytecode generators
- BackrefBacktrackMatcher and per-config worklist for backref NFAs
- Fix requiresBacktrackingForGroups false positive on anchor-only suffix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend hasStringEndAnchorInAlternation to cover $ (end-of-line anchor)
in addition to \Z. Add two routing tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B5: add missing broad-charset guard to hasGroupWithVariableLengthAlternationBody
B3b: restrict $ trigger to leading alternation branches; keep \Z triggering unconditionally;
     return PIKEVM_CAPTURE (not OPTIMIZED_NFA) for cap group path — needsFallback rejects the latter
B6: restore suffix-decline logic at call sites; remove it from inside detectFixedRepetitionBackref
B4: restore PIKEVM_CAPTURE routing before requiresBacktrackingForGroups; restore .+ decline in
    detectGreedyBacktrackPattern for non-group literal suffix (GREEDY_BACKTRACK overshoots on
    trailing-newline inputs); update contradicting test to expect PIKEVM_CAPTURE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add FuzzRunner.Config.patternSkip so sweeps can skip already-verified
pattern ranges without re-running the oracle. Expose via
-Dreggie.fuzz.skip=N in largeSweepConfig(). Add divergenceGate_extended
(skip=25k, budget=43) covering patterns 25001–50000 at depth 3 which
surfaces 22 unique pre-existing bugs (E1–E6); see
doc/2026-06-29-fuzz-extended-findings.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-06-29.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…0k patterns

Remove KNOWN_FINDINGS_BUDGET_EXTENDED and divergenceGate_extended.
Raise default sweep to 50k patterns (covers full validated range),
budget=43 (E1-E6 findings from doc/fuzz/2026-06-29.md).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… raw, 29 repros)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik
jbachorik force-pushed the fix/dfa-group-span-routing branch from 7494b35 to efb797e Compare June 29, 2026 20:34
@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

@codecov-commenter

codecov-commenter commented Jun 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.23129% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.5%. Comparing base (0c43a3c) to head (0bb9205).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ggie/codegen/analysis/FallbackPatternDetector.java 75.1% 23 Missing and 23 partials ⚠️
...doghq/reggie/codegen/analysis/PatternAnalyzer.java 81.2% 4 Missing and 14 partials ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##              main     #90     +/-   ##
=========================================
+ Coverage     82.4%   82.5%   +0.1%     
  Complexity       1       1             
=========================================
  Files          128     128             
  Lines        38962   39226    +264     
  Branches      4962    5094    +132     
=========================================
+ Hits         32136   32400    +264     
+ Misses        5276    5208     -68     
- Partials      1550    1618     +68     
Files with missing lines Coverage Δ
.../datadoghq/reggie/integration/fuzz/FuzzRunner.java 97.9% <100.0%> (+0.2%) ⬆️
...ggie/processor/ReggieMatcherBytecodeGenerator.java 70.3% <100.0%> (+0.3%) ⬆️
.../com/datadoghq/reggie/runtime/RuntimeCompiler.java 87.9% <100.0%> (+<0.1%) ⬆️
...doghq/reggie/codegen/analysis/PatternAnalyzer.java 74.0% <81.2%> (+0.6%) ⬆️
...ggie/codegen/analysis/FallbackPatternDetector.java 80.9% <75.1%> (-0.5%) ⬇️

... and 19 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0c43a3c...0bb9205. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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: efb797ecaf

ℹ️ 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".

@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 4142fd5ebd

ℹ️ 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".

@jbachorik

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: e32904ce15

ℹ️ 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".

@jbachorik

Copy link
Copy Markdown
Collaborator Author

Auto-expanded scope: 1 sibling issue fixed in the same pass:

  • reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/RuntimeCompiler.java:299 — partial fallback guard in compilePikeVm() (B16 only, missing B2/B3): replaced ad-hoc hasNullableGroupContentWithNullableQuantifier check with full FallbackPatternDetector.needsFallback() call

@jbachorik jbachorik added the sphinx:critical Sphinx: critical — human review required label Jun 30, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jbachorik
jbachorik marked this pull request as ready for review June 30, 2026 14:32
@jbachorik
jbachorik merged commit 6dd5dc7 into main Jun 30, 2026
9 checks passed
@jbachorik
jbachorik deleted the fix/dfa-group-span-routing branch June 30, 2026 14:32

@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: 0bb9205ec8

ℹ️ 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 on lines +1555 to +1557
if (ast instanceof GroupNode g && g.capturing) {
if (isAnchorOnlyBody(g.child)) return true;
return hasAnchorOnlyCapturingGroup(g.child);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recurse through transparent groups for anchor-only captures

When the only anchor is hidden by a non-capturing wrapper, e.g. ((?:$)) or (?:($)), this branch does not inspect that wrapper: isAnchorOnlyBody(g.child) rejects a non-capturing GroupNode, and the recursion here only starts from a capturing group. Fresh evidence in the current code is that there is no later non-capturing GroupNode recursion path for this predicate, so these patterns skip the B3a PIKEVM route and, when OnePass-eligible, can hit the same zero-width capture span bug fixed for ($).

Useful? React with 👍 / 👎.

* before the check, so both forms are correctly detected.
*/
public static boolean hasGreedyDotPlusGroupWithSuffix(RegexNode ast) {
if (!(ast instanceof ConcatNode concat)) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unwrap whole-pattern groups before B4 detection

For a transparent wrapper around the entire B4 shape, such as (?:(.+)_), the AST is a non-capturing GroupNode, so this early return false runs before the child concat is inspected. Fresh evidence in the current code is that unwrapNonCapturing is only applied to concat children after this check, not to ast itself. The semantically equivalent unwrapped (.+)_ routes to PIKEVM_CAPTURE to avoid TDFA extending group 1 into the suffix; the wrapped form can remain on the unsafe DFA-with-groups path.

Useful? React with 👍 / 👎.

@jbachorik jbachorik added this to the 0.4.0 milestone Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated or assisted by AI sphinx:critical Sphinx: critical — human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants