diff --git a/doc/2026-07-02-reggie-1.0.0-readiness-assessment.md b/doc/2026-07-02-reggie-1.0.0-readiness-assessment.md new file mode 100644 index 00000000..c37dd01e --- /dev/null +++ b/doc/2026-07-02-reggie-1.0.0-readiness-assessment.md @@ -0,0 +1,93 @@ +# Reggie 1.0.0 public-release readiness — re-assessment (2026-07-02) + +Re-evaluation against the June 18 punch list, after the 2026-06-18→07-02 correctness and feature +work. Evidence-backed (file:line / gate output / build). Current version: **`0.4.0-SNAPSHOT`** (build.gradle:8). + +## Verdict: **Materially closer to 1.0 — P0 and key P1 items resolved. A focused burst can close it.** + +The P0 boolean-correctness blocker (#31 sandwich lookaround) is fixed. The meta-test shows zero +mismatches across all strategies. The fuzz gate runs at 34 (down from 69 on June 18). New strategies +(COUNTING_GLUSHKOV, atomic groups, possessive quantifiers) are fully covered. The remaining blockers +are documentation hygiene (P0-1, P0-3) and one thread-safety validation (P1-4). + +## Gate metrics (current HEAD `3edd608`, 2026-07-02) + +| Metric | June 18 | July 2 | Change | +|---|---|---|---| +| `./gradlew test` | GREEN | **GREEN** | ✅ | +| Fuzz budget (`KNOWN_FINDINGS_BUDGET`) | 69 | **34** | ✅ −35 | +| Fuzz gate findings | 34/34 (at budget) | **34/34** | → holds | +| Meta-test mismatches | unknown | **0** | ✅ | +| PCRE conformance | inconsistent | **100.0% (53/53)** | ✅ | +| Build-breaking regressions (post-merge) | — | **3 files, now fixed** | fixed in this session | + +## Per-area status + +| Area | June 18 | July 2 | Notes | +|---|---|---|---| +| Build / full test suite | Ready | **Ready** | Post-merge duplicate-declaration bugs fixed this session | +| ReDoS / linear-time guarantee | Ready | **Ready** | COUNTING_GLUSHKOV adds O(n) bound-independent counting | +| Boolean match correctness | **Gap (#31)** | **Ready** | `fix(#28,#31)` (PR #87) fixed sandwich lookaround; meta-test = 0 mismatches | +| Group-span / capture correctness | Gap (69) | **Gap (34)** | Budget halved; 34 pre-existing native span divergences remain (all degenerate inputs) | +| Fallback policy (code) | Ready | **Ready** | throw-by-default; opt-in JDK via `allowJdkFallback()` | +| Correctness-guarantee docs | **Gap (P0)** | **Gap (P0)** | AGENTS.md prose still contradicts throw-by-default; not reconciled | +| Conformance metrics | **Gap (P0)** | **Ready** | AGENTS.md now states 100.0% (53/53); `CorrectnessTest` passes against Reggie | +| Thread-safety of cached matchers | **Gap (unverified)** | **Gap (unverified)** | RuntimeCompiler uses per-call factory for stateful matchers (line 218 comment); contract documented at line 816; no concurrency stress test yet | +| Public API surface | Ready | **Ready** | Unchanged | +| Release tooling | Ready | **Ready** | scripts/release.sh confirmed | +| Versioning / CHANGELOG | Gap (minor) | **Gap (minor)** | No 1.0 checklist in CHANGELOG | +| Parser robustness | Gap (minor) | **Gap (minor)** | Malformed patterns can still leak generic RuntimeException | +| Lazy quantifiers | Partial | **Partial** | Unchanged; native for some RECURSIVE_DESCENT cases; known limitation | +| Atomic groups / possessive | — | **Ready** | New in `ae96462` + `3edd608`; `AtomicGroupPikeVMTest` covers correctness | +| COUNTING_GLUSHKOV strategy | — | **Ready** | New in `3edd608`; routing test + runtime test cover correctness | + +## Updated pre-1.0.0 punch list + +**P0 — still open (must do before any public correctness claim):** +1. **Reconcile AGENTS.md / README prose** with actual throw-by-default behavior. State: native-or-throw + by default; opt-in JDK fallback via `compileAllowingFallback()`. Currently AGENTS.md:717-737 says + "logs a WARNING and falls back" which is wrong. +2. ~~Establish single source of truth for PCRE conformance~~ **DONE** (AGENTS.md:69 = 100.0%, 53/53). +3. **State the fuzz guarantee honestly**: "zero *boolean* divergences; 34 known native group-span + divergences on degenerate inputs, all O(n)/ReDoS-safe." Current wording does not say this. + +**P1 — one remaining:** +4. **Thread-safety**: RuntimeCompiler's contract is documented (line 816: "must not be shared across + threads or sequential compile() calls") but not stress-tested. A `java.util.regex.Pattern` + drop-in claim implies concurrent safety. Add one concurrency stress test and update docs. +5. ~~Fix / decline issue #31~~ **DONE** (`fix(#28,#31)` PR #87). +6. ~~Audit #28, #33, #34, #37~~ **DONE** via fixes in PR #87, PR #88, PR #89. + +**P2 — capture-correctness gaps (can ship 1.0 with documented known limitations):** +7. **34-finding fuzz gate**: drive down or document as "known, native, O(n), span-only, + degenerate-input" and give the budget headroom (budget > count). Currently at-budget (34/34) which + is brittle — any regression will hit zero headroom immediately. Recommend: budget = 40 with + explicit classification doc. +8. Quantify real-world frequency (fuzzer-shape evidence only so far). + +**P3 — release hygiene:** +9. Add 1.0 checklist to CHANGELOG. +10. Resolve perf regressions from benchmark-report.md (`(fo|foo)` group `match()` 0.57×; + `^(a)(b)(c)$` ONEPASS_NFA `find()` 0.28×) or accept/document. + +## What the June 18 → July 2 sprint materially closed + +- **Boolean correctness gap closed**: issue #31 (sandwich lookaround silent wrong-answer) fixed. + Meta-test at 0 mismatches across all 28+ strategies. +- **Fuzz budget halved** 69 → 34: per-config backref NFA (PR #89), DFA group-span routing (PR #90), + atomic group / possessive quantifiers (PRs #92 + `3edd608`), CRLF / fallback guards. +- **COUNTING_GLUSHKOV** strategy: O(n) bound-independent counting for `X{n,m}` (max>10, group-free + body). New engine path with routing + runtime tests. +- **Atomic groups** `(?>...)` and **possessive quantifiers** (`X*+`, `X?+`, etc.) added and tested. +- **Post-merge build fix**: duplicate-declaration bugs (NFA.atomicEntry, GlushkovAutomaton.findLastRequiredChar, + PatternAnalyzer.AtomicGroupDetector, PikeVMMatcher field block) from a bad rebase of `3edd608` + onto `ae96462` were repaired in this session. All three gates now clean. + +## Honest 1.0 timeline estimate + +With focused work: +- P0-1 (docs reconcile): 1 day +- P0-3 (fuzz guarantee prose + headroom bump): half day +- P1-4 (thread-safety stress test): 1 day + +**Net: 2–3 days of focused work remains before a defensible 1.0 tag.** diff --git a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PatternAnalyzer.java b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PatternAnalyzer.java index 4c6ad4fc..a4c0a37a 100644 --- a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PatternAnalyzer.java +++ b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/analysis/PatternAnalyzer.java @@ -5238,72 +5238,6 @@ public Boolean visitBranchReset(BranchResetNode node) { } } - /** Visitor to detect atomic groups ({@code (?>...)}) anywhere in the AST. */ - private static class AtomicGroupDetector implements RegexVisitor { - @Override - public Boolean visitLiteral(LiteralNode node) { - return false; - } - - @Override - public Boolean visitCharClass(CharClassNode node) { - return false; - } - - @Override - public Boolean visitConcat(ConcatNode node) { - return node.children.stream().anyMatch(child -> child.accept(this)); - } - - @Override - public Boolean visitAlternation(AlternationNode node) { - return node.alternatives.stream().anyMatch(alt -> alt.accept(this)); - } - - @Override - public Boolean visitQuantifier(QuantifierNode node) { - return node.child.accept(this); - } - - @Override - public Boolean visitGroup(GroupNode node) { - if (node.atomic) return true; - return node.child.accept(this); - } - - @Override - public Boolean visitAnchor(AnchorNode node) { - return false; - } - - @Override - public Boolean visitBackreference(BackreferenceNode node) { - return false; - } - - @Override - public Boolean visitAssertion(AssertionNode node) { - return node.subPattern != null && node.subPattern.accept(this); - } - - @Override - public Boolean visitSubroutine(SubroutineNode node) { - return false; - } - - @Override - public Boolean visitConditional(ConditionalNode node) { - boolean hasThen = node.thenBranch.accept(this); - boolean hasElse = node.elseBranch != null && node.elseBranch.accept(this); - return hasThen || hasElse; - } - - @Override - public Boolean visitBranchReset(BranchResetNode node) { - return node.alternatives.stream().anyMatch(alt -> alt.accept(this)); - } - } - /** * Visitor to detect start/end anchors (^, $) in AST. Word boundaries (\b) are not considered * anchors for this check. @@ -6107,11 +6041,6 @@ public Boolean visitBranchReset(BranchResetNode node) { } } - /** Returns {@code true} when the AST contains an atomic group or possessive quantifier. */ - private static boolean hasAtomicGroups(RegexNode node) { - return node.accept(new AtomicGroupDetector()); - } - /** Information about a greedy char class pattern like (\d+) or ([a-z]*). */ public static class GreedyCharClassInfo implements PatternInfo { public final CharSet charset; diff --git a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/GlushkovAutomaton.java b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/GlushkovAutomaton.java index 0694d93c..3ba83041 100644 --- a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/GlushkovAutomaton.java +++ b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/GlushkovAutomaton.java @@ -89,33 +89,6 @@ public final class GlushkovAutomaton { */ public final boolean startsAnywhere; - /** - * Returns the single ASCII character (0–127) that must appear at the accepting position of every - * match, or {@code -1} if none can be identified. - * - *

When the Last (accept) set has exactly one position {@code p} and exactly one ASCII - * character activates {@code p} through {@code entry[asciiClasses[c]] >> p & 1 != 0}, that - * character is required at every match end. The caller can use {@link String#indexOf(int, int)} - * to skip non-candidate regions in {@code find()}. - * - *

Returns {@code -1} when: the Last set has more than one position; more than one ASCII - * character activates the sole accepting position (class too wide); or the position is activated - * only by non-ASCII characters. - */ - public int findLastRequiredChar() { - if (Long.bitCount(accept) != 1) return -1; - int p = Long.numberOfTrailingZeros(accept); - int found = -1; - for (int c = 0; c < 128; c++) { - int cls = asciiClasses[c]; - if (((entry[cls] >> p) & 1L) != 0L) { - if (found != -1) return -1; - found = c; - } - } - return found; - } - private GlushkovAutomaton( int positionCount, boolean nullable, diff --git a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/NFA.java b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/NFA.java index e5c3ddce..22ee3e0b 100644 --- a/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/NFA.java +++ b/reggie-codegen/src/main/java/com/datadoghq/reggie/codegen/automaton/NFA.java @@ -52,17 +52,6 @@ public int getGroupCount() { return groupCount; } - /** - * Returns the number of atomic groups in this NFA (i.e. the count of distinct atomicEntry ids). - */ - public int getAtomicGroupCount() { - int max = -1; - for (NFAState s : states) { - if (s.atomicEntry > max) max = s.atomicEntry; - } - return max + 1; - } - /** * Check if this NFA contains a multiline start anchor (^ in multiline mode). * @@ -410,9 +399,6 @@ public static final class NFAState { public NFAState thenBranch = null; // Entry if group matched public NFAState elseBranch = null; // Entry if group didn't match (may be null) - public int atomicEntry = -1; // >= 0: entering atomic group with this id - public int atomicExit = -1; // >= 0: exiting atomic group with this id - public NFAState(int id) { this.id = id; } diff --git a/reggie-codegen/src/test/java/com/datadoghq/reggie/codegen/codegen/BitParallelGlushkovBytecodeGeneratorTest.java b/reggie-codegen/src/test/java/com/datadoghq/reggie/codegen/codegen/BitParallelGlushkovBytecodeGeneratorTest.java index 000f4dd4..12f17972 100644 --- a/reggie-codegen/src/test/java/com/datadoghq/reggie/codegen/codegen/BitParallelGlushkovBytecodeGeneratorTest.java +++ b/reggie-codegen/src/test/java/com/datadoghq/reggie/codegen/codegen/BitParallelGlushkovBytecodeGeneratorTest.java @@ -146,8 +146,7 @@ void lastRequiredChar_singleAsciiChar() throws Exception { } /** - * For {@code .*[abc]}, the accept position matches 'a', 'b', 'c', so lastRequiredChar must be - * -1. + * For {@code .*[abc]}, the accept position matches 'a', 'b', 'c', so lastRequiredChar must be -1. */ @Test void lastRequiredChar_multipleChars() throws Exception { diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/PikeVMMatcher.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/PikeVMMatcher.java index 937e7d95..1add0410 100644 --- a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/PikeVMMatcher.java +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/PikeVMMatcher.java @@ -103,23 +103,6 @@ public final class PikeVMMatcher extends ReggieMatcher { // Accept-state mask for O(1) accept check. private final boolean[] isAccept; - // Atomic group tracking: for each atomic group id N, - // atomicEnteredInClosure[N] = true when atomicEntry(N) was processed in the current epsilon - // closure — used to distinguish "zero-char skip" exits from "post-char" exits. - // atomicHasGreedyChar[N] = true when a char-consuming state (inside group N) has a transition - // that matches the current input character — used to block zero-char skip atomicExit paths. - // atomicGroupStates[N] = array of state ids inside atomic group N (reachable from atomicEntry(N) - // without crossing any atomicExit(N)) — used to detect whether the group is still running - // (i.e., any inside state is in nlist) when an atomicExit_consume is encountered in nlist. - // Parallel arrays for nlist (addThreadToNlist): - // atomicEnteredInNlistClosure[N] and atomicHasGreedyCharNlist[N]. - private final int atomicGroupCount; - private final boolean[] atomicEnteredInClosure; - private final boolean[] atomicHasGreedyChar; - private final boolean[] atomicEnteredInNlistClosure; - private final boolean[] atomicHasGreedyCharNlist; - private final int[][] atomicGroupStates; // atomicGroupStates[N] = inside-state ids for group N - // For each GroupExit state (indexed by state id): true when the group body can produce an // empty match (i.e. there is an epsilon-only path from the corresponding GroupEntry to this // GroupExit). Used by the trailing-empty-iteration rebind to avoid propagating captures when @@ -194,14 +177,6 @@ public PikeVMMatcher(NFA nfa, String pattern) { } mergeScratch = new int[stateCount]; - // Initialize atomic group tracking arrays. - atomicGroupCount = nfa.getAtomicGroupCount(); - atomicEnteredInClosure = new boolean[Math.max(1, atomicGroupCount)]; - atomicHasGreedyChar = new boolean[Math.max(1, atomicGroupCount)]; - atomicEnteredInNlistClosure = new boolean[Math.max(1, atomicGroupCount)]; - atomicHasGreedyCharNlist = new boolean[Math.max(1, atomicGroupCount)]; - atomicGroupStates = computeAtomicGroupStates(nfa, atomicGroupCount); - // Precompute groupBodyNullable: for each GroupExit state, determine whether there is // an epsilon-only path from its matching GroupEntry to that GroupExit. groupBodyNullable = computeGroupBodyNullable(nfa); @@ -302,9 +277,6 @@ public PikeVMMatcher(NFA nfa, String pattern) { * independent lazy DFA cannot model. */ private static boolean findDfaEligible(NFA nfa) { - // Atomic groups require the full PikeVM simulation (the DFA cannot enforce the - // no-backtracking commitment). Decline the DFA fast path for any NFA with atomic groups. - if (nfa.getAtomicGroupCount() > 0) return false; boolean hasStartAnchor = false; for (NFA.NFAState s : nfa.getStates()) { if (s.assertionType != null || s.backrefCheck != null) return false; @@ -822,11 +794,6 @@ private void stepChar(char ch, int nextPos, String input, int regionStart, int r * unrolled-quantifier consuming threads (e.g. {@code a copy3} in {@code (^a?){3}}) that arrived * via anchor firings interleaved with quantifier skips, distinguishing them from direct-sequence * anchored paths (e.g. {@code \A{3}a} where anchorFollowedBySkip remains false). - * - *

{@code currentAtomicId} is {@code -1} when outside any atomic group; it is set to {@code N} - * when the DFS path passed through an {@code atomicEntry(N)} state and has not yet exited the - * group. Used to set {@link #atomicHasGreedyChar}{@code [N]} when a char-consuming leaf is added - * inside the group, enabling the zero-char-skip-path blocking at {@code atomicExit(N)}. */ private void addThread( NFA.NFAState state, @@ -848,14 +815,12 @@ private void addThread( anchorFollowedBySkip, input, regionStart, - regionEnd, - -1); + regionEnd); } /** * Core addThread with atomic group tracking. {@code atomicPos[G]} holds the input position at - * which atomic group G was entered (-1 if not inside G). {@code currentAtomicId} is -1 when - * outside any atomic group, or the id N when the DFS path is inside atomic group N. + * which atomic group G was entered (-1 if not inside G). */ private void addThread( NFA.NFAState state, @@ -867,59 +832,9 @@ private void addThread( boolean anchorFollowedBySkip, String input, int regionStart, - int regionEnd, - int currentAtomicId) { + int regionEnd) { if (inClist[state.id]) return; - // Atomic group entry: mark this group as entered in the current closure and descend with - // currentAtomicId set to N so that char-consuming leaves inside the group set - // atomicHasGreedyChar. - if (atomicGroupCount > 0 && state.atomicEntry >= 0) { - int N = state.atomicEntry; - atomicEnteredInClosure[N] = true; - inClist[state.id] = true; - for (NFA.NFAState next : state.getEpsilonTransitions()) { - addThread( - next, - captures, - atomicPos, - pos, - depth, - anchorCount, - anchorFollowedBySkip, - input, - regionStart, - regionEnd, - N); - } - return; - } - - // Atomic group exit: block zero-char skip path if the group entered in this closure has a - // greedy char path; otherwise allow and continue with currentAtomicId cleared. - if (atomicGroupCount > 0 && state.atomicExit >= 0) { - int N = state.atomicExit; - if (atomicEnteredInClosure[N] && atomicHasGreedyChar[N]) { - return; // block zero-char skip path through atomic group - } - inClist[state.id] = true; - for (NFA.NFAState next : state.getEpsilonTransitions()) { - addThread( - next, - captures, - atomicPos, - pos, - depth, - anchorCount, - anchorFollowedBySkip, - input, - regionStart, - regionEnd, - -1); - } - return; - } - if (state.anchor != null) { if (!checkAnchor(state.anchor, input, pos, regionStart, regionEnd)) return; inClist[state.id] = true; @@ -935,8 +850,7 @@ private void addThread( anchorFollowedBySkip, input, regionStart, - regionEnd, - currentAtomicId); + regionEnd); } return; } @@ -986,8 +900,7 @@ private void addThread( childAnchorFollowedBySkip, input, regionStart, - regionEnd, - currentAtomicId); + regionEnd); if (willUpdateGroupEntry) { int scratchIdx = Math.min(depth + 2, scratchCaptures.length - 1); passedCaptures = scratchCaptures[scratchIdx]; @@ -998,20 +911,6 @@ private void addThread( // Leaf: has character transitions or is an accept state. inClist[state.id] = true; - // If inside an atomic group, check whether this leaf can actually consume a character at - // the current position. Setting atomicHasGreedyChar[N] means "a greedy char path CAN fire - // here" — only true when at least one of the leaf's char-transitions matches input[pos]. - // This distinction is necessary for bodies like `a*` at a position where no 'a' follows: - // in that case the greedy path cannot fire, so the zero-char skip exit must NOT be blocked. - if (currentAtomicId >= 0 && pos < input.length()) { - char curChar = input.charAt(pos); - for (NFA.Transition tr : state.getTransitions()) { - if (tr.chars.contains(curChar)) { - atomicHasGreedyChar[currentAtomicId] = true; - break; - } - } - } clistIds[clistSize] = state.id; System.arraycopy(ownCaptures, 0, clistCaptures[clistSize], 0, ownCaptures.length); if (atomicGroupCount > 0) { @@ -1039,69 +938,13 @@ private void addThreadToNlist( String input, int regionStart, int regionEnd) { - addThreadToNlist(state, captures, pos, depth, input, regionStart, regionEnd, -1); - } - - private void addThreadToNlist( - NFA.NFAState state, - int[] captures, - int pos, - int depth, - String input, - int regionStart, - int regionEnd, - int currentAtomicId) { if (inNlist[state.id]) return; - // Atomic group entry: mark and descend with currentAtomicId = N. - if (atomicGroupCount > 0 && state.atomicEntry >= 0) { - int N = state.atomicEntry; - atomicEnteredInNlistClosure[N] = true; - inNlist[state.id] = true; - for (NFA.NFAState next : state.getEpsilonTransitions()) { - addThreadToNlist(next, captures, pos, depth, input, regionStart, regionEnd, N); - } - return; - } - - // Atomic group exit: two blocking conditions: - // (a) zero-char skip: atomicEntry(N) was processed in this nlist closure AND the body can - // match the current char — block the skip path so the body commits to chars. - // (b) premature consume exit: a state inside group N is in nlist AND can actually match - // input[pos] (i.e., the loop path is alive and will consume more chars). Block this - // shorter-match exit; when the loop dies (no matching char), the exit is allowed. - if (atomicGroupCount > 0 && state.atomicExit >= 0) { - int N = state.atomicExit; - if (atomicEnteredInNlistClosure[N] && atomicHasGreedyCharNlist[N]) { - return; // (a) zero-char skip blocked - } - // (b) block if any inside-group state is in nlist AND can still match at pos. - if (atomicGroupStates.length > N && pos < input.length()) { - char curChar = input.charAt(pos); - for (int insideId : atomicGroupStates[N]) { - if (inNlist[insideId]) { - // Check if this inside-state can consume curChar (i.e., loop path is viable). - for (NFA.Transition tr : statesById[insideId].getTransitions()) { - if (tr.chars.contains(curChar)) { - return; // premature exit: loop path can still consume - } - } - } - } - } - inNlist[state.id] = true; - for (NFA.NFAState next : state.getEpsilonTransitions()) { - addThreadToNlist(next, captures, pos, depth, input, regionStart, regionEnd, -1); - } - return; - } - if (state.anchor != null) { if (!checkAnchor(state.anchor, input, pos, regionStart, regionEnd)) return; inNlist[state.id] = true; for (NFA.NFAState next : state.getEpsilonTransitions()) { - addThreadToNlist( - next, captures, pos, depth, input, regionStart, regionEnd, currentAtomicId); + addThreadToNlist(next, captures, atomicPos, pos, depth, input, regionStart, regionEnd); } return; } @@ -1126,7 +969,7 @@ private void addThreadToNlist( && next.enterGroup != null && !inNlist[next.id]; addThreadToNlist( - next, passedCaptures, pos, depth + 1, input, regionStart, regionEnd, currentAtomicId); + next, passedCaptures, passedAtomicPos, pos, depth + 1, input, regionStart, regionEnd); if (willUpdateGroupEntry) { int scratchIdx = Math.min(depth + 2, scratchCaptures.length - 1); passedCaptures = scratchCaptures[scratchIdx]; @@ -1136,16 +979,6 @@ private void addThreadToNlist( } inNlist[state.id] = true; - // If inside an atomic group, check whether this leaf can actually consume at pos. - if (currentAtomicId >= 0 && pos < input.length()) { - char curChar = input.charAt(pos); - for (NFA.Transition tr : state.getTransitions()) { - if (tr.chars.contains(curChar)) { - atomicHasGreedyCharNlist[currentAtomicId] = true; - break; - } - } - } int k = nlistSize; nlistIds[k] = state.id; System.arraycopy(ownCaptures, 0, nlistCaptures[k], 0, ownCaptures.length); @@ -1360,10 +1193,6 @@ private void resetClist() { Arrays.fill(clistViaMultipleAnchors, false); clistSize = 0; clistFirstAccept = -1; - if (atomicGroupCount > 0) { - Arrays.fill(atomicEnteredInClosure, false); - Arrays.fill(atomicHasGreedyChar, false); - } } /** @@ -1425,10 +1254,6 @@ private void resetNlist() { nlistSize = 0; // nlistAlive slots are reset to true as each new slot is committed in addThreadToNlist; // no explicit reset of all slots is needed since we only read slots 0..nlistSize-1. - if (atomicGroupCount > 0) { - Arrays.fill(atomicEnteredInNlistClosure, false); - Arrays.fill(atomicHasGreedyCharNlist, false); - } } private void swapLists() { @@ -1436,10 +1261,6 @@ private void swapLists() { // Full reset of clist guards then re-set from nlist (skipping dead atomic-pruned slots). Arrays.fill(inClist, false); clistFirstAccept = -1; - if (atomicGroupCount > 0) { - Arrays.fill(atomicEnteredInClosure, false); - Arrays.fill(atomicHasGreedyChar, false); - } int write = 0; for (int i = 0; i < nlistSize; i++) { if (atomicGroupCount > 0 && !nlistAlive[i]) { @@ -1488,76 +1309,6 @@ private MatchResult buildResult(String input, int[] caps) { * requires character consumption (e.g. {@code (.)+} has a non-nullable body; propagation must not * fire there even though the GroupExit → GroupEntry loop-back epsilon exists). */ - /** - * For each atomic group N, compute the set of NFA state ids that lie strictly INSIDE the group - * (reachable from the atomicEntry(N) state via epsilon/character transitions without crossing any - * atomicExit(N) state). These are the states whose presence in nlist signals that the group is - * still "running" — used to block premature exits via atomicExit_consume when the group body has - * not yet committed to its maximal match. - */ - private static int[][] computeAtomicGroupStates(NFA nfa, int atomicGroupCount) { - if (atomicGroupCount == 0) return new int[0][]; - List states = nfa.getStates(); - int n = states.size(); - NFA.NFAState[] byId = new NFA.NFAState[n]; - for (NFA.NFAState s : states) byId[s.id] = s; - - int[][] result = new int[atomicGroupCount][]; - boolean[] visited = new boolean[n]; - int[] stack = new int[n]; - int[] buf = new int[n]; - - for (int gid = 0; gid < atomicGroupCount; gid++) { - // Find the atomicEntry state for group gid. - NFA.NFAState entryState = null; - for (NFA.NFAState s : states) { - if (s.atomicEntry == gid) { - entryState = s; - break; - } - } - if (entryState == null) { - result[gid] = new int[0]; - continue; - } - - // BFS/DFS: collect states reachable from entryState via any (epsilon or char) transition, - // stopping at and excluding any atomicExit(gid) state. The entryState itself is excluded - // (it is the boundary, not "inside"). - Arrays.fill(visited, false); - int top = 0; - int bufSize = 0; - // Seed: follow entryState's epsilon transitions (the entry state itself is the boundary). - visited[entryState.id] = true; - for (NFA.NFAState next : entryState.getEpsilonTransitions()) { - if (!visited[next.id]) { - visited[next.id] = true; - stack[top++] = next.id; - } - } - while (top > 0) { - NFA.NFAState cur = byId[stack[--top]]; - if (cur.atomicExit == gid) continue; // boundary: do not include, do not traverse further - buf[bufSize++] = cur.id; - for (NFA.NFAState next : cur.getEpsilonTransitions()) { - if (!visited[next.id]) { - visited[next.id] = true; - stack[top++] = next.id; - } - } - for (NFA.Transition tr : cur.getTransitions()) { - NFA.NFAState next = tr.target; - if (!visited[next.id]) { - visited[next.id] = true; - stack[top++] = next.id; - } - } - } - result[gid] = Arrays.copyOf(buf, bufSize); - } - return result; - } - private static boolean[] computeGroupBodyNullable(NFA nfa) { List states = nfa.getStates(); int n = states.size();