Reggie is a high-performance Java regex library that provides two complementary approaches to pattern matching:
Both approaches analyze each pattern and route it to one of three strategy families — DFA-backed (deterministic, longest-match), NFA/PikeVM-backed (Thompson construction, leftmost-first), or bounded-backtracking bytecode (shape-restricted, still O(n)) — then generate specialized bytecode for guaranteed linear-time matching without ReDoS vulnerabilities.
Traditional Java regex engines (like java.util.regex.Pattern) have several drawbacks:
Reggie solves these problems:
| Feature | JDK Pattern | Reggie (Compile-Time) | Reggie (Runtime) |
|---|---|---|---|
| Compilation overhead | Every startup | Zero (at build time) | First use only (~5-10ms) |
| ReDoS protection | ❌ No | ✅ Yes (linear time) | ✅ Yes (linear time) |
| Error detection | Runtime | Compile time | Runtime |
| Performance | Good | Excellent (10-20x) | Excellent (10-20x) |
| JIT optimization | Limited | Maximum | Maximum |
| Dynamic patterns | ✅ Yes | ❌ No | ✅ Yes |
No build configuration needed - just use it:
import com.datadoghq.reggie.Reggie;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public class Example {
public static void main(String[] args) {
// Compile pattern once (cached automatically)
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
// Use it multiple times - fast!
System.out.println(phone.matches("123-456-7890")); // true
System.out.println(phone.matches("invalid")); // false
// Find in text
System.out.println(phone.find("Call 123-456-7890")); // true
System.out.println(phone.findFrom("Call 123-456-7890", 0)); // 5
}
}
First-use latency: ~5-10ms for pattern compilation, then <1µs cache lookup.
Define patterns at compile time for maximum performance:
Step 1: Create a pattern provider class:
// MyPatterns.java
import com.datadoghq.reggie.ReggiePatterns;
import com.datadoghq.reggie.annotations.RegexPattern;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public abstract class MyPatterns implements ReggiePatterns {
@RegexPattern("\\d{3}-\\d{3}-\\d{4}")
public abstract ReggieMatcher phone();
@RegexPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
public abstract ReggieMatcher email();
@RegexPattern("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b")
public abstract ReggieMatcher ipv4();
}
Step 2: Use the patterns (implementation generated at compile time):
import com.datadoghq.reggie.Reggie;
public class Example {
public static void main(String[] args) {
// Get pattern provider instance (generated implementation)
MyPatterns patterns = Reggie.patterns(MyPatterns.class);
// Use the matchers - zero overhead!
System.out.println(patterns.phone().matches("123-456-7890")); // true
System.out.println(patterns.email().matches("user@example.com")); // true
System.out.println(patterns.ipv4().matches("192.168.1.1")); // true
}
}
First-use latency: 0ms (compiled at build time).
Reggie is faster than JDK's Pattern and RE2J on typical patterns because it compiles each pattern
to specialized bytecode instead of interpreting a generic instruction stream. We don't publish
specific speedup multipliers here: the only benchmark run on file predates the most recent
performance work (6841723, 5db1866) and is not committed to this repo, so it cannot be trusted
as a current number. Run the command below to generate a report for your own JVM/hardware/patterns.
Run ./gradlew :reggie-benchmark:benchmarkAndReport to generate a detailed HTML report comparing
Reggie, JDK Pattern, and RE2J across match/find/group-extraction/backreference/assertion/split
categories.
Pattern.compile() overhead| Feature | Reggie | JDK Pattern | RE2J |
|---|---|---|---|
| Time Complexity | O(n) guaranteed¹ | O(2^n) worst case | O(n) guaranteed |
| Implementation | JIT-compiled bytecode | Interpreted backtracking | NFA simulation |
| ReDoS Safe | ✅ Yes¹ | ❌ No | ✅ Yes |
| Backreferences | ✅ Yes | ✅ Yes | ❌ No |
| Lookahead/Lookbehind | ✅ Yes | ✅ Yes | ❌ No |
¹ Applies to Reggie.compile() (default, native engine only — throws UnsupportedPatternException
rather than silently degrading). Opting into compileAllowingFallback() /
ReggieOption.ALLOW_JDK_FALLBACK delegates unsupported patterns to java.util.regex, which
inherits JDK's backtracking worst case and is not ReDoS-safe. See
doc/agents-fallback-and-limitations.md.
TL;DR: Add this JVM argument for a modest additional performance boost (exact magnitude depends on your JVM/patterns — not independently benchmarked at current HEAD):
--add-opens java.base/java.lang=ALL-UNNAMED
Reggie uses a smart multi-tier strategy for accessing string content during pattern matching:
--add-opens): Direct access to String's internal byte array via MethodHandles - fastestString.charAt() - still fastThe library works perfectly without any JVM arguments - it automatically falls back to copy-based or charAt mode. However, adding --add-opens eliminates the O(n) copy overhead for an extra performance edge.
The performance gain from --add-opens depends on your patterns:
| Pattern Type | Impact | Example |
|---|---|---|
| Short strings (<100 chars) | Minimal (~1-2%) | Short validation patterns |
| Long strings + SIMD patterns | Moderate (~5%) | [0-9a-fA-F]+ on large text |
| Tight loops, hot paths | Noticeable (~10%) | Millions of matches/sec |
| Anchored patterns | None | ^abc (early bailout) |
Gradle:
tasks.withType(JavaExec) {
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
}
test {
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
}
Maven:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
Command Line:
java --add-opens java.base/java.lang=ALL-UNNAMED -jar your-app.jar
IDE (IntelliJ IDEA):
--add-opens java.base/java.lang=ALL-UNNAMEDCheck if zero-copy is active:
import com.datadoghq.reggie.runtime.StringView;
if (StringView.isZeroCopyAvailable()) {
System.out.println("Zero-copy optimization enabled!");
} else {
System.out.println("Using copy-based fallback (still fast!)");
}
Bottom line: The library works great out-of-box. Add --add-opens if you want to squeeze out every last microsecond in high-throughput scenarios.
Add to your build.gradle:
repositories {
mavenCentral() // or your repository
}
dependencies {
// Reggie (runtime API + bundled annotation processor)
implementation 'com.datadoghq:reggie:<version>'
// Add for compile-time API (annotation processing)
annotationProcessor 'com.datadoghq:reggie:<version>'
}
Add to your pom.xml:
<dependencies>
<!-- Reggie (runtime API + bundled annotation processor) -->
<dependency>
<groupId>com.datadoghq</groupId>
<artifactId>reggie</artifactId>
<version><!-- version --></version>
</dependency>
</dependencies>
The runtime API compiles patterns on-demand with automatic caching.
import com.datadoghq.reggie.Reggie;
import com.datadoghq.reggie.runtime.ReggieMatcher;
// Compile pattern (automatically cached)
ReggieMatcher matcher = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
// Test if entire string matches
boolean matches = matcher.matches("123-456-7890"); // true
// Find pattern anywhere in string
boolean found = matcher.find("Call 123-456-7890 now"); // true
// Find pattern starting at position
int position = matcher.findFrom("Multiple: 123-456-7890 and 999-888-7777", 0);
// Returns 10 (start of first match)
// Automatic caching (pattern string is the key)
ReggieMatcher m1 = Reggie.compile("\\d+");
ReggieMatcher m2 = Reggie.compile("\\d+");
assert m1 == m2; // Same instance returned
// Explicit cache key for user input
String userPattern = getUserInput();
ReggieMatcher matcher = Reggie.cached("user-search-pattern", userPattern);
// Check cache status
System.out.println("Cached patterns: " + Reggie.cacheSize());
System.out.println("Keys: " + Reggie.cachedPatterns());
// Clear cache (e.g., on configuration reload)
Reggie.clearCache();
try {
ReggieMatcher matcher = Reggie.compile("[invalid");
} catch (java.util.regex.PatternSyntaxException e) {
System.err.println("Invalid pattern: " + e.getMessage());
}
// ✅ GOOD: Compile once, reuse many times
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
for (String input : inputs) {
if (phone.matches(input)) {
// process
}
}
// ❌ BAD: Don't compile in loops
for (String input : inputs) {
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}"); // Cached but wasteful
if (phone.matches(input)) {
// process
}
}
The compile-time API generates specialized matchers during build for zero runtime overhead.
1. Create Pattern Provider Class
Create an abstract class implementing ReggiePatterns with abstract methods annotated with @RegexPattern:
package com.example.patterns;
import com.datadoghq.reggie.ReggiePatterns;
import com.datadoghq.reggie.annotations.RegexPattern;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public abstract class ValidationPatterns implements ReggiePatterns {
// Simple patterns
@RegexPattern("\\d+")
public abstract ReggieMatcher digits();
@RegexPattern("[a-zA-Z]+")
public abstract ReggieMatcher letters();
// Real-world patterns
@RegexPattern("\\d{3}-\\d{3}-\\d{4}")
public abstract ReggieMatcher usPhone();
@RegexPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
public abstract ReggieMatcher email();
@RegexPattern("(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%]).{8,}")
public abstract ReggieMatcher strongPassword();
}
2. Build Your Project
The annotation processor runs automatically during compilation:
./gradlew build
Generated files (in build/generated/sources/annotationProcessor):
ValidationPatterns$Impl.java - Implementation of your pattern provider3. Use the Patterns
import com.datadoghq.reggie.Reggie;
import com.example.patterns.ValidationPatterns;
public class Validator {
// Singleton pattern (optional but recommended)
private static final ValidationPatterns PATTERNS =
Reggie.patterns(ValidationPatterns.class);
public boolean isValidEmail(String email) {
return PATTERNS.email().matches(email);
}
public boolean isStrongPassword(String password) {
return PATTERNS.strongPassword().matches(password);
}
public boolean hasDigits(String text) {
return PATTERNS.digits().find(text);
}
}
You can organize patterns into multiple classes:
// NetworkPatterns.java
public abstract class NetworkPatterns implements ReggiePatterns {
@RegexPattern("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b")
public abstract ReggieMatcher ipv4();
@RegexPattern("([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}")
public abstract ReggieMatcher ipv6();
}
// FilePatterns.java
public abstract class FilePatterns implements ReggiePatterns {
@RegexPattern(".*\\.java$")
public abstract ReggieMatcher javaFile();
@RegexPattern(".*\\.(jpg|png|gif)$")
public abstract ReggieMatcher imageFile();
}
// Usage
NetworkPatterns net = Reggie.patterns(NetworkPatterns.class);
FilePatterns files = Reggie.patterns(FilePatterns.class);
if (net.ipv4().matches(address)) { /* ... */ }
if (files.javaFile().matches(filename)) { /* ... */ }
Invalid patterns are caught at build time:
@RegexPattern("[invalid") // Missing closing bracket
public abstract ReggieMatcher broken();
// Build output:
// error: Invalid regex pattern: Unclosed character class near index 7
// [invalid
// ^
Modern IDEs (IntelliJ IDEA, VS Code with Java extensions) automatically run annotation processors:
| Use Case | Recommended | Why |
|---|---|---|
| Known patterns in hot paths | Compile-Time | Zero overhead, compile-time validation |
| User-provided search | Runtime | Dynamic pattern support |
| Configuration-driven patterns | Runtime | Flexibility to change patterns |
| Form validation | Compile-Time | Patterns known at build time |
| Log parsing (fixed formats) | Compile-Time | Maximum performance |
| Log parsing (user filters) | Runtime | User can customize |
| GraalVM native-image | Compile-Time | No runtime bytecode generation |
General Rule: Use compile-time for static patterns (95% of use cases), runtime for dynamic patterns.
Reggie Class// Compile pattern with automatic caching
public static ReggieMatcher compile(String pattern)
// Compile with explicit cache key
public static ReggieMatcher cached(String key, String pattern)
// Cache management
public static void clearCache()
public static int cacheSize()
public static Set<String> cachedPatterns()
ReggieMatcher Class// Test if entire string matches
public abstract boolean matches(String input)
// Find pattern anywhere in string
public abstract boolean find(String input)
// Find pattern starting at position
public abstract int findFrom(String input, int start)
// Returns: start position of match, or -1 if not found
// Get the pattern string
public final String pattern()
@RegexPattern Annotation@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface RegexPattern {
String value(); // The regex pattern
ReggieOption[] options() default {}; // Compilation flags (e.g. ALLOW_JDK_FALLBACK)
}
Requirements:
ReggieMatcherReggiePatternsReggie.patterns() Methodpublic static <T extends ReggiePatterns> T patterns(Class<T> patternClass)
Returns an instance of the generated implementation class.
PCRE Compatibility: 115/123 test cases pass (3 fail, 5 error) on a curated common-patterns suite —
email/URL/IP/phone/JSON-style patterns; see CorrectnessTest.testCommonPatterns. On the full
364-entry PCRE conformance corpus
(CorrectnessTest.testPCRECapturingGroups), Reggie passes 98.1% of the 262 cases it can
evaluate; the remaining 102 entries use PCRE features not yet implemented (see
PCRE Conformance Roadmap).
[abc], [a-z], [^abc], [a-zA-Z0-9]\d, \w, \s (and negated: \D, \W, \S)*, +, ?, {n}, {n,}, {n,m}
{ 3, 5 }, { 3 } (PCRE compatible)\n, \t, \r, \\, \/\100 (octal 100 = '@'), \377\x40 (hex 40 = '@'), \xFF|(...)(?:...)(?<name>...), including extraction by name(?|(...)|(...))(?>...)^, $\A (absolute start), \Z (end before optional newline)\b (word boundary), \B (non-word boundary)(?=...), (?!...) (positive/negative)(?<=...), (?<!...) (positive/negative)(?i)(?s) - . matches newlines(?m)(?x) - ignore whitespace and comments(?i:...) - modifier applies only inside the group*?, +?, ??), possessive (*+, ++, ?+)\p{L}, \p{N}, and their negations \P{L}, \P{N} (script-based forms like
\p{Script=Greek} are not yet supported)\1, \2, etc., including self-referencing backrefs within a single group
((a\1?){4}), with limitations - see below(?1), (?R) for non-self-embedding references (calls that don't recurse
into themselves); self-embedding/context-free recursion is a permanent limitation - see below(abc)\1 matches "abcabc"(a{2})\1 matches "aaaa"(a+)\1 matches minimal cases only<(\w+)>.*</\1> (HTML tags)*?, +?, ??
(?(condition)yes|no) - basic cases work; combining a conditional with
a backref inside a repeated group ((a(?(1)\1)){4}) is a known bug, not yet fixedPermanent limitation (would require unbounded backtracking to support - see PCRE Conformance Roadmap):
(?1)/(?R) calls that recurse into their own group,
e.g. palindrome patterns like ^((.)(?1)\2|.?)$(*MARK), (*PRUNE), (*SKIP), (*THEN)Not yet implemented (ordinary backlog, no architecture change needed):
(?|(?'a'aaa)|(?'a'b)) - the numbered form works(?-2), (?+1)\p{Script=Greek}, \p{Name=...}\100, \377); note (abc)\100 immediately after a captured group is
a known parsing bug (ambiguity with backreference \1 followed by digits 00)\x40, \xFF)(?s) - dot matches newlines\A and \Z\p{L}/\p{N}, scoped inline modifiers, named
group extraction, self-referencing backrefsSee PCRE Conformance Roadmap for detailed compatibility status.
Reggie analyzes each pattern and selects the optimal matching strategy:
Pattern Analysis Decision Tree:
│
├─ Has backreferences? ───────────────────────► Thompson NFA (bytecode)
│
├─ Has lookahead/lookbehind? ─────────────────► Hybrid DFA+NFA (bytecode)
│
├─ Pure regular (no extended features)?
│ │
│ ├─ Simple pattern (<50 states)? ──────────► Pure DFA Unrolled (bytecode)
│ │
│ ├─ Medium complexity (50-500 states)? ────► Pure DFA Switch (bytecode)
│ │
│ └─ Complex pattern (>500 states)? ────────► Thompson NFA (bytecode)
│
└─ Unsupported features? ─────────────────────► Compile-time error
Beyond this top-level routing, some structural shapes get a dedicated fast path instead of a
general-purpose engine. For example, BITSTATE_BYTECODE recognizes patterns of the form
^(?:leadingWs(kw1|kw2|...)separatorWs)?mandatoryCharSet+trailingWs*(tail) (an optional
prefix keyword plus a mandatory scan and tail — e.g. shell-command-style patterns) and
compiles them to straight-line, non-backtracking bytecode rather than routing through the
general BITSTATE_CAPTURE interpreter. See
doc/2026-07-08-bitstate-bytecode-generator-design.md
for the design rationale.
hello)Generated matcher:
public boolean matches(String input) {
return input != null && input.equals("hello");
}
public boolean find(String input) {
return input != null && input.contains("hello");
}
\d{3}-\d{3}-\d{4})Generated matcher (simplified):
public boolean matches(String input) {
if (input == null || input.length() != 12) return false;
int pos = 0;
// Check 3 digits
for (int i = 0; i < 3; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
// Check dash
if (input.charAt(pos++) != '-') return false;
// Check 3 digits
for (int i = 0; i < 3; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
// Check dash
if (input.charAt(pos++) != '-') return false;
// Check 4 digits
for (int i = 0; i < 4; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
return pos == input.length();
}
For patterns with multiple states, generates switch-based DFA:
public boolean matches(String input) {
if (input == null) return false;
int state = 0; // Initial state
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
switch (state) {
case 0: state = transition0(c); break;
case 1: state = transition1(c); break;
// ... more states
case -1: return false; // Error state
}
}
return isAcceptState(state);
}
┌─────────────────────────────────────────────────────┐
│ Reggie API │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Compile-Time │ │ Runtime │ │
│ │ Patterns │ │ Patterns │ │
│ │ @RegexPattern│ │ Reggie.compile()│ │
│ └──────┬───────┘ └────────┬────────┘ │
└─────────┼──────────────────────────────┼───────────┘
│ │
│ │
┌─────▼─────────┐ ┌───────▼──────────┐
│ Annotation │ │ Runtime │
│ Processor │ │ Compiler │
│ (Build Time) │ │ (First Use) │
└───────┬───────┘ └────────┬─────────┘
│ │
└──────────┬──────────────────┘
│
┌──────────▼──────────┐
│ Shared Codegen │
│ ┌──────────────┐ │
│ │ AST Parser │ │
│ │ NFA Builder │ │
│ │ DFA Builder │ │
│ │ Bytecode Gen │ │
│ └──────────────┘ │
└─────────────────────┘
│
┌──────────▼──────────┐
│ Generated Matcher │
│ (Bytecode) │
└─────────────────────┘
reggie/
├── reggie-annotations/ # @RegexPattern annotation definition
├── reggie-codegen/ # Shared bytecode generation (AST, NFA, DFA, codegen)
├── reggie-processor/ # Annotation processor (compile-time path)
├── reggie-runtime/ # Runtime API + interfaces
├── reggie-benchmark/ # Performance benchmarks and examples
├── reggie-integration-tests/ # PCRE/RE2 conformance test suites
└── doc/ # Documentation and research notes
Design Principle: The reggie-codegen module contains all pattern analysis and bytecode generation logic, shared by both the annotation processor (compile-time) and runtime compiler. This eliminates code duplication and ensures consistent behavior.
# Clone repository
git clone https://github.com/DataDog/java-reggie.git
cd java-reggie
# Build all modules
./gradlew build
# Run tests
./gradlew test
# Run benchmarks
./gradlew :reggie-benchmark:run
# Run JMH benchmarks
./gradlew :reggie-benchmark:jmh
# Clean build
./gradlew clean build
# Simple matcher tests with performance comparison
./gradlew :reggie-benchmark:run
# Expected output:
# Testing generated matchers...
#
# === Phone Matcher ===
# Phone Matcher: PASSED
#
# === Hello Matcher ===
# Hello Matcher: PASSED
#
# === Performance Comparison ===
# Reggie matcher: <N> ms
# JDK Pattern: <N> ms
# Speedup: <N>x
#
# Actual timings and speedup vary by hardware and JVM; see the Performance
# section above and run `./gradlew :reggie-benchmark:benchmarkAndReport` for
# a current, trustworthy comparison.
Reggie is based on decades of regex engine research:
Regular Expression Matching Can Be Simple And Fast - Russ Cox (2007)
RE2: Google's linear-time regex engine
.NET Regex Source Generators (.NET 7+)
Needle: DFA-based regex with bytecode compilation
PCRE (Perl Compatible Regular Expressions)
Based on extensive research, Reggie's hybrid compile-time/runtime approach is novel in the Java ecosystem:
The differential fuzzer (AlgorithmicFuzzTest.divergenceGate) tracks 28 pre-existing divergences
between Reggie and JDK on adversarial degenerate inputs:
DFA_UNROLLED_WITH_GROUPS and SPECIALIZED_CONCAT_GREEDY_GROUP.OPTIMIZED_NFA_WITH_BACKREFS; \A anchor enforcement in DFA_SWITCH; backref/anchor-combo
patterns.All affected patterns are O(n) / ReDoS-safe. These gaps affect adversarial or synthetically
generated patterns; typical production patterns are unlikely to trigger them. The budget ratchets
down as each root-cause class is fixed (-Dreggie.fuzz.maxFindings=N overrides the gate; see
doc/agents-fallback-and-limitations.md for the
authoritative, currently-maintained breakdown).
Reggie is production-ready. Contributions are welcome!
git checkout -b feature/my-feature./gradlew testSee CONTRIBUTING.md for detailed guidelines.
Jaroslav Bachořík (@jbachorik) Email: jaroslav.bachorik@datadoghq.com
For security issues, please see SECURITY.md.
Apache License 2.0 - see LICENSE file for details
Author: Jaroslav Bachorik
Questions? Open an issue on GitHub
Java
98.7%
Reggie is a high-performance Java regex library that provides two complementary approaches to pattern matching:
Both approaches analyze each pattern and route it to one of three strategy families — DFA-backed (deterministic, longest-match), NFA/PikeVM-backed (Thompson construction, leftmost-first), or bounded-backtracking bytecode (shape-restricted, still O(n)) — then generate specialized bytecode for guaranteed linear-time matching without ReDoS vulnerabilities.
Traditional Java regex engines (like java.util.regex.Pattern) have several drawbacks:
Reggie solves these problems:
| Feature | JDK Pattern | Reggie (Compile-Time) | Reggie (Runtime) |
|---|---|---|---|
| Compilation overhead | Every startup | Zero (at build time) | First use only (~5-10ms) |
| ReDoS protection | ❌ No | ✅ Yes (linear time) | ✅ Yes (linear time) |
| Error detection | Runtime | Compile time | Runtime |
| Performance | Good | Excellent (10-20x) | Excellent (10-20x) |
| JIT optimization | Limited | Maximum | Maximum |
| Dynamic patterns | ✅ Yes | ❌ No | ✅ Yes |
No build configuration needed - just use it:
import com.datadoghq.reggie.Reggie;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public class Example {
public static void main(String[] args) {
// Compile pattern once (cached automatically)
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
// Use it multiple times - fast!
System.out.println(phone.matches("123-456-7890")); // true
System.out.println(phone.matches("invalid")); // false
// Find in text
System.out.println(phone.find("Call 123-456-7890")); // true
System.out.println(phone.findFrom("Call 123-456-7890", 0)); // 5
}
}
First-use latency: ~5-10ms for pattern compilation, then <1µs cache lookup.
Define patterns at compile time for maximum performance:
Step 1: Create a pattern provider class:
// MyPatterns.java
import com.datadoghq.reggie.ReggiePatterns;
import com.datadoghq.reggie.annotations.RegexPattern;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public abstract class MyPatterns implements ReggiePatterns {
@RegexPattern("\\d{3}-\\d{3}-\\d{4}")
public abstract ReggieMatcher phone();
@RegexPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
public abstract ReggieMatcher email();
@RegexPattern("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b")
public abstract ReggieMatcher ipv4();
}
Step 2: Use the patterns (implementation generated at compile time):
import com.datadoghq.reggie.Reggie;
public class Example {
public static void main(String[] args) {
// Get pattern provider instance (generated implementation)
MyPatterns patterns = Reggie.patterns(MyPatterns.class);
// Use the matchers - zero overhead!
System.out.println(patterns.phone().matches("123-456-7890")); // true
System.out.println(patterns.email().matches("user@example.com")); // true
System.out.println(patterns.ipv4().matches("192.168.1.1")); // true
}
}
First-use latency: 0ms (compiled at build time).
Reggie is faster than JDK's Pattern and RE2J on typical patterns because it compiles each pattern
to specialized bytecode instead of interpreting a generic instruction stream. We don't publish
specific speedup multipliers here: the only benchmark run on file predates the most recent
performance work (6841723, 5db1866) and is not committed to this repo, so it cannot be trusted
as a current number. Run the command below to generate a report for your own JVM/hardware/patterns.
Run ./gradlew :reggie-benchmark:benchmarkAndReport to generate a detailed HTML report comparing
Reggie, JDK Pattern, and RE2J across match/find/group-extraction/backreference/assertion/split
categories.
Pattern.compile() overhead| Feature | Reggie | JDK Pattern | RE2J |
|---|---|---|---|
| Time Complexity | O(n) guaranteed¹ | O(2^n) worst case | O(n) guaranteed |
| Implementation | JIT-compiled bytecode | Interpreted backtracking | NFA simulation |
| ReDoS Safe | ✅ Yes¹ | ❌ No | ✅ Yes |
| Backreferences | ✅ Yes | ✅ Yes | ❌ No |
| Lookahead/Lookbehind | ✅ Yes | ✅ Yes | ❌ No |
¹ Applies to Reggie.compile() (default, native engine only — throws UnsupportedPatternException
rather than silently degrading). Opting into compileAllowingFallback() /
ReggieOption.ALLOW_JDK_FALLBACK delegates unsupported patterns to java.util.regex, which
inherits JDK's backtracking worst case and is not ReDoS-safe. See
doc/agents-fallback-and-limitations.md.
TL;DR: Add this JVM argument for a modest additional performance boost (exact magnitude depends on your JVM/patterns — not independently benchmarked at current HEAD):
--add-opens java.base/java.lang=ALL-UNNAMED
Reggie uses a smart multi-tier strategy for accessing string content during pattern matching:
--add-opens): Direct access to String's internal byte array via MethodHandles - fastestString.charAt() - still fastThe library works perfectly without any JVM arguments - it automatically falls back to copy-based or charAt mode. However, adding --add-opens eliminates the O(n) copy overhead for an extra performance edge.
The performance gain from --add-opens depends on your patterns:
| Pattern Type | Impact | Example |
|---|---|---|
| Short strings (<100 chars) | Minimal (~1-2%) | Short validation patterns |
| Long strings + SIMD patterns | Moderate (~5%) | [0-9a-fA-F]+ on large text |
| Tight loops, hot paths | Noticeable (~10%) | Millions of matches/sec |
| Anchored patterns | None | ^abc (early bailout) |
Gradle:
tasks.withType(JavaExec) {
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
}
test {
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
}
Maven:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
Command Line:
java --add-opens java.base/java.lang=ALL-UNNAMED -jar your-app.jar
IDE (IntelliJ IDEA):
--add-opens java.base/java.lang=ALL-UNNAMEDCheck if zero-copy is active:
import com.datadoghq.reggie.runtime.StringView;
if (StringView.isZeroCopyAvailable()) {
System.out.println("Zero-copy optimization enabled!");
} else {
System.out.println("Using copy-based fallback (still fast!)");
}
Bottom line: The library works great out-of-box. Add --add-opens if you want to squeeze out every last microsecond in high-throughput scenarios.
Add to your build.gradle:
repositories {
mavenCentral() // or your repository
}
dependencies {
// Reggie (runtime API + bundled annotation processor)
implementation 'com.datadoghq:reggie:<version>'
// Add for compile-time API (annotation processing)
annotationProcessor 'com.datadoghq:reggie:<version>'
}
Add to your pom.xml:
<dependencies>
<!-- Reggie (runtime API + bundled annotation processor) -->
<dependency>
<groupId>com.datadoghq</groupId>
<artifactId>reggie</artifactId>
<version><!-- version --></version>
</dependency>
</dependencies>
The runtime API compiles patterns on-demand with automatic caching.
import com.datadoghq.reggie.Reggie;
import com.datadoghq.reggie.runtime.ReggieMatcher;
// Compile pattern (automatically cached)
ReggieMatcher matcher = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
// Test if entire string matches
boolean matches = matcher.matches("123-456-7890"); // true
// Find pattern anywhere in string
boolean found = matcher.find("Call 123-456-7890 now"); // true
// Find pattern starting at position
int position = matcher.findFrom("Multiple: 123-456-7890 and 999-888-7777", 0);
// Returns 10 (start of first match)
// Automatic caching (pattern string is the key)
ReggieMatcher m1 = Reggie.compile("\\d+");
ReggieMatcher m2 = Reggie.compile("\\d+");
assert m1 == m2; // Same instance returned
// Explicit cache key for user input
String userPattern = getUserInput();
ReggieMatcher matcher = Reggie.cached("user-search-pattern", userPattern);
// Check cache status
System.out.println("Cached patterns: " + Reggie.cacheSize());
System.out.println("Keys: " + Reggie.cachedPatterns());
// Clear cache (e.g., on configuration reload)
Reggie.clearCache();
try {
ReggieMatcher matcher = Reggie.compile("[invalid");
} catch (java.util.regex.PatternSyntaxException e) {
System.err.println("Invalid pattern: " + e.getMessage());
}
// ✅ GOOD: Compile once, reuse many times
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}");
for (String input : inputs) {
if (phone.matches(input)) {
// process
}
}
// ❌ BAD: Don't compile in loops
for (String input : inputs) {
ReggieMatcher phone = Reggie.compile("\\d{3}-\\d{3}-\\d{4}"); // Cached but wasteful
if (phone.matches(input)) {
// process
}
}
The compile-time API generates specialized matchers during build for zero runtime overhead.
1. Create Pattern Provider Class
Create an abstract class implementing ReggiePatterns with abstract methods annotated with @RegexPattern:
package com.example.patterns;
import com.datadoghq.reggie.ReggiePatterns;
import com.datadoghq.reggie.annotations.RegexPattern;
import com.datadoghq.reggie.runtime.ReggieMatcher;
public abstract class ValidationPatterns implements ReggiePatterns {
// Simple patterns
@RegexPattern("\\d+")
public abstract ReggieMatcher digits();
@RegexPattern("[a-zA-Z]+")
public abstract ReggieMatcher letters();
// Real-world patterns
@RegexPattern("\\d{3}-\\d{3}-\\d{4}")
public abstract ReggieMatcher usPhone();
@RegexPattern("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
public abstract ReggieMatcher email();
@RegexPattern("(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%]).{8,}")
public abstract ReggieMatcher strongPassword();
}
2. Build Your Project
The annotation processor runs automatically during compilation:
./gradlew build
Generated files (in build/generated/sources/annotationProcessor):
ValidationPatterns$Impl.java - Implementation of your pattern provider3. Use the Patterns
import com.datadoghq.reggie.Reggie;
import com.example.patterns.ValidationPatterns;
public class Validator {
// Singleton pattern (optional but recommended)
private static final ValidationPatterns PATTERNS =
Reggie.patterns(ValidationPatterns.class);
public boolean isValidEmail(String email) {
return PATTERNS.email().matches(email);
}
public boolean isStrongPassword(String password) {
return PATTERNS.strongPassword().matches(password);
}
public boolean hasDigits(String text) {
return PATTERNS.digits().find(text);
}
}
You can organize patterns into multiple classes:
// NetworkPatterns.java
public abstract class NetworkPatterns implements ReggiePatterns {
@RegexPattern("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b")
public abstract ReggieMatcher ipv4();
@RegexPattern("([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}")
public abstract ReggieMatcher ipv6();
}
// FilePatterns.java
public abstract class FilePatterns implements ReggiePatterns {
@RegexPattern(".*\\.java$")
public abstract ReggieMatcher javaFile();
@RegexPattern(".*\\.(jpg|png|gif)$")
public abstract ReggieMatcher imageFile();
}
// Usage
NetworkPatterns net = Reggie.patterns(NetworkPatterns.class);
FilePatterns files = Reggie.patterns(FilePatterns.class);
if (net.ipv4().matches(address)) { /* ... */ }
if (files.javaFile().matches(filename)) { /* ... */ }
Invalid patterns are caught at build time:
@RegexPattern("[invalid") // Missing closing bracket
public abstract ReggieMatcher broken();
// Build output:
// error: Invalid regex pattern: Unclosed character class near index 7
// [invalid
// ^
Modern IDEs (IntelliJ IDEA, VS Code with Java extensions) automatically run annotation processors:
| Use Case | Recommended | Why |
|---|---|---|
| Known patterns in hot paths | Compile-Time | Zero overhead, compile-time validation |
| User-provided search | Runtime | Dynamic pattern support |
| Configuration-driven patterns | Runtime | Flexibility to change patterns |
| Form validation | Compile-Time | Patterns known at build time |
| Log parsing (fixed formats) | Compile-Time | Maximum performance |
| Log parsing (user filters) | Runtime | User can customize |
| GraalVM native-image | Compile-Time | No runtime bytecode generation |
General Rule: Use compile-time for static patterns (95% of use cases), runtime for dynamic patterns.
Reggie Class// Compile pattern with automatic caching
public static ReggieMatcher compile(String pattern)
// Compile with explicit cache key
public static ReggieMatcher cached(String key, String pattern)
// Cache management
public static void clearCache()
public static int cacheSize()
public static Set<String> cachedPatterns()
ReggieMatcher Class// Test if entire string matches
public abstract boolean matches(String input)
// Find pattern anywhere in string
public abstract boolean find(String input)
// Find pattern starting at position
public abstract int findFrom(String input, int start)
// Returns: start position of match, or -1 if not found
// Get the pattern string
public final String pattern()
@RegexPattern Annotation@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface RegexPattern {
String value(); // The regex pattern
ReggieOption[] options() default {}; // Compilation flags (e.g. ALLOW_JDK_FALLBACK)
}
Requirements:
ReggieMatcherReggiePatternsReggie.patterns() Methodpublic static <T extends ReggiePatterns> T patterns(Class<T> patternClass)
Returns an instance of the generated implementation class.
PCRE Compatibility: 115/123 test cases pass (3 fail, 5 error) on a curated common-patterns suite —
email/URL/IP/phone/JSON-style patterns; see CorrectnessTest.testCommonPatterns. On the full
364-entry PCRE conformance corpus
(CorrectnessTest.testPCRECapturingGroups), Reggie passes 98.1% of the 262 cases it can
evaluate; the remaining 102 entries use PCRE features not yet implemented (see
PCRE Conformance Roadmap).
[abc], [a-z], [^abc], [a-zA-Z0-9]\d, \w, \s (and negated: \D, \W, \S)*, +, ?, {n}, {n,}, {n,m}
{ 3, 5 }, { 3 } (PCRE compatible)\n, \t, \r, \\, \/\100 (octal 100 = '@'), \377\x40 (hex 40 = '@'), \xFF|(...)(?:...)(?<name>...), including extraction by name(?|(...)|(...))(?>...)^, $\A (absolute start), \Z (end before optional newline)\b (word boundary), \B (non-word boundary)(?=...), (?!...) (positive/negative)(?<=...), (?<!...) (positive/negative)(?i)(?s) - . matches newlines(?m)(?x) - ignore whitespace and comments(?i:...) - modifier applies only inside the group*?, +?, ??), possessive (*+, ++, ?+)\p{L}, \p{N}, and their negations \P{L}, \P{N} (script-based forms like
\p{Script=Greek} are not yet supported)\1, \2, etc., including self-referencing backrefs within a single group
((a\1?){4}), with limitations - see below(?1), (?R) for non-self-embedding references (calls that don't recurse
into themselves); self-embedding/context-free recursion is a permanent limitation - see below(abc)\1 matches "abcabc"(a{2})\1 matches "aaaa"(a+)\1 matches minimal cases only<(\w+)>.*</\1> (HTML tags)*?, +?, ??
(?(condition)yes|no) - basic cases work; combining a conditional with
a backref inside a repeated group ((a(?(1)\1)){4}) is a known bug, not yet fixedPermanent limitation (would require unbounded backtracking to support - see PCRE Conformance Roadmap):
(?1)/(?R) calls that recurse into their own group,
e.g. palindrome patterns like ^((.)(?1)\2|.?)$(*MARK), (*PRUNE), (*SKIP), (*THEN)Not yet implemented (ordinary backlog, no architecture change needed):
(?|(?'a'aaa)|(?'a'b)) - the numbered form works(?-2), (?+1)\p{Script=Greek}, \p{Name=...}\100, \377); note (abc)\100 immediately after a captured group is
a known parsing bug (ambiguity with backreference \1 followed by digits 00)\x40, \xFF)(?s) - dot matches newlines\A and \Z\p{L}/\p{N}, scoped inline modifiers, named
group extraction, self-referencing backrefsSee PCRE Conformance Roadmap for detailed compatibility status.
Reggie analyzes each pattern and selects the optimal matching strategy:
Pattern Analysis Decision Tree:
│
├─ Has backreferences? ───────────────────────► Thompson NFA (bytecode)
│
├─ Has lookahead/lookbehind? ─────────────────► Hybrid DFA+NFA (bytecode)
│
├─ Pure regular (no extended features)?
│ │
│ ├─ Simple pattern (<50 states)? ──────────► Pure DFA Unrolled (bytecode)
│ │
│ ├─ Medium complexity (50-500 states)? ────► Pure DFA Switch (bytecode)
│ │
│ └─ Complex pattern (>500 states)? ────────► Thompson NFA (bytecode)
│
└─ Unsupported features? ─────────────────────► Compile-time error
Beyond this top-level routing, some structural shapes get a dedicated fast path instead of a
general-purpose engine. For example, BITSTATE_BYTECODE recognizes patterns of the form
^(?:leadingWs(kw1|kw2|...)separatorWs)?mandatoryCharSet+trailingWs*(tail) (an optional
prefix keyword plus a mandatory scan and tail — e.g. shell-command-style patterns) and
compiles them to straight-line, non-backtracking bytecode rather than routing through the
general BITSTATE_CAPTURE interpreter. See
doc/2026-07-08-bitstate-bytecode-generator-design.md
for the design rationale.
hello)Generated matcher:
public boolean matches(String input) {
return input != null && input.equals("hello");
}
public boolean find(String input) {
return input != null && input.contains("hello");
}
\d{3}-\d{3}-\d{4})Generated matcher (simplified):
public boolean matches(String input) {
if (input == null || input.length() != 12) return false;
int pos = 0;
// Check 3 digits
for (int i = 0; i < 3; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
// Check dash
if (input.charAt(pos++) != '-') return false;
// Check 3 digits
for (int i = 0; i < 3; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
// Check dash
if (input.charAt(pos++) != '-') return false;
// Check 4 digits
for (int i = 0; i < 4; i++) {
if (!Character.isDigit(input.charAt(pos++))) return false;
}
return pos == input.length();
}
For patterns with multiple states, generates switch-based DFA:
public boolean matches(String input) {
if (input == null) return false;
int state = 0; // Initial state
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
switch (state) {
case 0: state = transition0(c); break;
case 1: state = transition1(c); break;
// ... more states
case -1: return false; // Error state
}
}
return isAcceptState(state);
}
┌─────────────────────────────────────────────────────┐
│ Reggie API │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Compile-Time │ │ Runtime │ │
│ │ Patterns │ │ Patterns │ │
│ │ @RegexPattern│ │ Reggie.compile()│ │
│ └──────┬───────┘ └────────┬────────┘ │
└─────────┼──────────────────────────────┼───────────┘
│ │
│ │
┌─────▼─────────┐ ┌───────▼──────────┐
│ Annotation │ │ Runtime │
│ Processor │ │ Compiler │
│ (Build Time) │ │ (First Use) │
└───────┬───────┘ └────────┬─────────┘
│ │
└──────────┬──────────────────┘
│
┌──────────▼──────────┐
│ Shared Codegen │
│ ┌──────────────┐ │
│ │ AST Parser │ │
│ │ NFA Builder │ │
│ │ DFA Builder │ │
│ │ Bytecode Gen │ │
│ └──────────────┘ │
└─────────────────────┘
│
┌──────────▼──────────┐
│ Generated Matcher │
│ (Bytecode) │
└─────────────────────┘
reggie/
├── reggie-annotations/ # @RegexPattern annotation definition
├── reggie-codegen/ # Shared bytecode generation (AST, NFA, DFA, codegen)
├── reggie-processor/ # Annotation processor (compile-time path)
├── reggie-runtime/ # Runtime API + interfaces
├── reggie-benchmark/ # Performance benchmarks and examples
├── reggie-integration-tests/ # PCRE/RE2 conformance test suites
└── doc/ # Documentation and research notes
Design Principle: The reggie-codegen module contains all pattern analysis and bytecode generation logic, shared by both the annotation processor (compile-time) and runtime compiler. This eliminates code duplication and ensures consistent behavior.
# Clone repository
git clone https://github.com/DataDog/java-reggie.git
cd java-reggie
# Build all modules
./gradlew build
# Run tests
./gradlew test
# Run benchmarks
./gradlew :reggie-benchmark:run
# Run JMH benchmarks
./gradlew :reggie-benchmark:jmh
# Clean build
./gradlew clean build
# Simple matcher tests with performance comparison
./gradlew :reggie-benchmark:run
# Expected output:
# Testing generated matchers...
#
# === Phone Matcher ===
# Phone Matcher: PASSED
#
# === Hello Matcher ===
# Hello Matcher: PASSED
#
# === Performance Comparison ===
# Reggie matcher: <N> ms
# JDK Pattern: <N> ms
# Speedup: <N>x
#
# Actual timings and speedup vary by hardware and JVM; see the Performance
# section above and run `./gradlew :reggie-benchmark:benchmarkAndReport` for
# a current, trustworthy comparison.
Reggie is based on decades of regex engine research:
Regular Expression Matching Can Be Simple And Fast - Russ Cox (2007)
RE2: Google's linear-time regex engine
.NET Regex Source Generators (.NET 7+)
Needle: DFA-based regex with bytecode compilation
PCRE (Perl Compatible Regular Expressions)
Based on extensive research, Reggie's hybrid compile-time/runtime approach is novel in the Java ecosystem:
The differential fuzzer (AlgorithmicFuzzTest.divergenceGate) tracks 28 pre-existing divergences
between Reggie and JDK on adversarial degenerate inputs:
DFA_UNROLLED_WITH_GROUPS and SPECIALIZED_CONCAT_GREEDY_GROUP.OPTIMIZED_NFA_WITH_BACKREFS; \A anchor enforcement in DFA_SWITCH; backref/anchor-combo
patterns.All affected patterns are O(n) / ReDoS-safe. These gaps affect adversarial or synthetically
generated patterns; typical production patterns are unlikely to trigger them. The budget ratchets
down as each root-cause class is fixed (-Dreggie.fuzz.maxFindings=N overrides the gate; see
doc/agents-fallback-and-limitations.md for the
authoritative, currently-maintained breakdown).
Reggie is production-ready. Contributions are welcome!
git checkout -b feature/my-feature./gradlew testSee CONTRIBUTING.md for detailed guidelines.
Jaroslav Bachořík (@jbachorik) Email: jaroslav.bachorik@datadoghq.com
For security issues, please see SECURITY.md.
Apache License 2.0 - see LICENSE file for details
Author: Jaroslav Bachorik
Questions? Open an issue on GitHub
Java
98.7%