Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions doc/2026-07-02-reggie-1.0.0-readiness-assessment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Reggie 1.0.0 public-release readiness — re-assessment (2026-07-02)
Comment thread
jbachorik marked this conversation as resolved.

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.**
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> {
@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.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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()}.
*
* <p>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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading