From 561a013c7e580af81a986d188326c601d2e82482 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Fri, 14 Aug 2026 15:42:19 +0200 Subject: [PATCH] feat: add native compiled pattern state API --- .../com/datadoghq/reggie/ReggieFlags.java | 1 + .../runtime/LinearTokenSequenceMatcher.java | 40 +++- .../runtime/ReggieCompilationRejection.java | 26 +++ .../runtime/ReggieCompilationResult.java | 59 +++++ .../reggie/runtime/ReggieCompileFlag.java | 34 +++ .../reggie/runtime/ReggieCompileRequest.java | 26 +++ .../reggie/runtime/ReggieCompiledPattern.java | 62 ++++++ .../reggie/runtime/ReggieMatchState.java | 82 +++++++ .../runtime/NamedOnlyLtsAdmissionTest.java | 44 ++-- .../runtime/ReggieCompiledPatternTest.java | 206 ++++++++++++++++++ 10 files changed, 552 insertions(+), 28 deletions(-) create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationRejection.java create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationResult.java create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileFlag.java create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileRequest.java create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompiledPattern.java create mode 100644 reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieMatchState.java create mode 100644 reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/ReggieCompiledPatternTest.java diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/ReggieFlags.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/ReggieFlags.java index f37c787c..eefd02c1 100644 --- a/reggie-runtime/src/main/java/com/datadoghq/reggie/ReggieFlags.java +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/ReggieFlags.java @@ -19,6 +19,7 @@ public final class ReggieFlags { // Keep these separate from java.util.regex.Pattern's values so an accidental JDK flag is never // silently interpreted as a different Reggie flag. + public static final int NONE = 0; public static final int CASE_INSENSITIVE = 1 << 24; public static final int MULTILINE = 1 << 25; public static final int DOTALL = 1 << 26; diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/LinearTokenSequenceMatcher.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/LinearTokenSequenceMatcher.java index 2ac0173e..53a9f454 100644 --- a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/LinearTokenSequenceMatcher.java +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/LinearTokenSequenceMatcher.java @@ -129,6 +129,44 @@ boolean matchIntoBounded( return true; } + MatchWorkspace newMatchWorkspace() { + return newWorkspace(); + } + + int groupCount() { + return groupCount; + } + + int groupIndex(String name) { + Objects.requireNonNull(name, "name"); + Integer index = nameToIndex.get(name); + if (index == null) { + throw new IllegalArgumentException("unknown group name: " + name); + } + return index; + } + + boolean matchIntoBounded( + CharSequence input, + int start, + int end, + int[] groupStarts, + int[] groupEnds, + MatchWorkspace workspace) { + Objects.requireNonNull(input, "input"); + Objects.requireNonNull(groupStarts, "groupStarts"); + Objects.requireNonNull(groupEnds, "groupEnds"); + Objects.requireNonNull(workspace, "workspace"); + validateRegion(input, start, end); + if (groupStarts.length <= groupCount || groupEnds.length <= groupCount) { + throw new IndexOutOfBoundsException("group arrays too small for " + groupCount + " groups"); + } + if (!matchesAt(input, start, end, workspace, true)) return false; + System.arraycopy(workspace.starts, 0, groupStarts, 0, groupCount + 1); + System.arraycopy(workspace.ends, 0, groupEnds, 0, groupCount + 1); + return true; + } + private MatchResult toMatchResult(String input, MatchWorkspace workspace) { if (!nameToIndex.isEmpty()) { return new NamedMatchResultImpl( @@ -479,7 +517,7 @@ private static int maxOptionalDepth(Iterable ops) { return max; } - private static final class MatchWorkspace { + static final class MatchWorkspace { final int[] starts; final int[] ends; final int[][] optionalStarts; diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationRejection.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationRejection.java new file mode 100644 index 00000000..8c6f6b4e --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationRejection.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +/** Reason the native named linear-token-sequence profile did not admit a request. */ +public enum ReggieCompilationRejection { + UNSUPPORTED_FLAGS, + SOURCE_INLINE_MODIFIER, + PARSE_FAILURE, + PLAN_UNAVAILABLE, + MISSING_NAMED_CAPTURE, + PROFILE_INELIGIBLE +} diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationResult.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationResult.java new file mode 100644 index 00000000..b70a44f6 --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompilationResult.java @@ -0,0 +1,59 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import java.util.Objects; + +/** Immutable outcome of a native named linear-token-sequence compilation request. */ +public final class ReggieCompilationResult { + private final ReggieCompiledPattern pattern; + private final ReggieCompilationRejection rejection; + + private ReggieCompilationResult( + ReggieCompiledPattern pattern, ReggieCompilationRejection rejection) { + if ((pattern == null) == (rejection == null)) { + throw new IllegalArgumentException("exactly one of pattern or rejection is required"); + } + this.pattern = pattern; + this.rejection = rejection; + } + + static ReggieCompilationResult admitted(ReggieCompiledPattern pattern) { + return new ReggieCompilationResult(Objects.requireNonNull(pattern, "pattern"), null); + } + + static ReggieCompilationResult rejected(ReggieCompilationRejection rejection) { + return new ReggieCompilationResult(null, Objects.requireNonNull(rejection, "rejection")); + } + + public boolean isAdmitted() { + return pattern != null; + } + + public ReggieCompiledPattern pattern() { + if (pattern == null) { + throw new IllegalStateException("compilation was rejected: " + rejection); + } + return pattern; + } + + public ReggieCompilationRejection rejection() { + if (rejection == null) { + throw new IllegalStateException("compilation was admitted"); + } + return rejection; + } +} diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileFlag.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileFlag.java new file mode 100644 index 00000000..29736152 --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileFlag.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import com.datadoghq.reggie.ReggieFlags; + +/** Flags supported by the native named linear-token-sequence compilation profile. */ +public enum ReggieCompileFlag { + NONE(ReggieFlags.NONE), + DOTALL(ReggieFlags.DOTALL); + + private final int reggieFlags; + + ReggieCompileFlag(int reggieFlags) { + this.reggieFlags = reggieFlags; + } + + int reggieFlags() { + return reggieFlags; + } +} diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileRequest.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileRequest.java new file mode 100644 index 00000000..c2a1fbbc --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompileRequest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import java.util.Objects; + +/** Immutable request for the native named linear-token-sequence compilation profile. */ +public record ReggieCompileRequest(String source, ReggieCompileFlag flag) { + public ReggieCompileRequest { + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(flag, "flag"); + } +} diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompiledPattern.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompiledPattern.java new file mode 100644 index 00000000..a9617f05 --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieCompiledPattern.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import java.util.Objects; + +/** + * Immutable native compiled pattern for the named linear-token-sequence profile. + * + *

This API never selects another Reggie strategy and never delegates to the JDK. + */ +public final class ReggieCompiledPattern { + private final LinearTokenSequenceMatcher matcher; + + private ReggieCompiledPattern(LinearTokenSequenceMatcher matcher) { + this.matcher = Objects.requireNonNull(matcher, "matcher"); + } + + /** + * Attempts native compilation without consulting the general compiler or either compiler cache. + */ + public static ReggieCompilationResult tryCompile(ReggieCompileRequest request) { + Objects.requireNonNull(request, "request"); + RuntimeCompiler.NamedOnlyLtsCompilation compilation = + RuntimeCompiler.tryCompileNamedOnlyLinearTokenSequence( + request.source(), request.flag().reggieFlags()); + if (compilation.matcher() != null) { + return ReggieCompilationResult.admitted(new ReggieCompiledPattern(compilation.matcher())); + } + return ReggieCompilationResult.rejected(mapRejection(compilation.rejection())); + } + + /** Creates a new single-thread-confined state object for matching this immutable pattern. */ + public ReggieMatchState newState() { + return new ReggieMatchState(matcher); + } + + private static ReggieCompilationRejection mapRejection( + RuntimeCompiler.NamedOnlyLtsRejection rejection) { + return switch (rejection) { + case UNSUPPORTED_FLAGS -> ReggieCompilationRejection.UNSUPPORTED_FLAGS; + case SOURCE_INLINE_MODIFIER -> ReggieCompilationRejection.SOURCE_INLINE_MODIFIER; + case PARSE_FAILURE -> ReggieCompilationRejection.PARSE_FAILURE; + case PLAN_UNAVAILABLE -> ReggieCompilationRejection.PLAN_UNAVAILABLE; + case MISSING_NAMED_CAPTURE -> ReggieCompilationRejection.MISSING_NAMED_CAPTURE; + case PROFILE_INELIGIBLE -> ReggieCompilationRejection.PROFILE_INELIGIBLE; + }; + } +} diff --git a/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieMatchState.java b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieMatchState.java new file mode 100644 index 00000000..01a7ce16 --- /dev/null +++ b/reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieMatchState.java @@ -0,0 +1,82 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Single-thread-confined mutable matching state for a {@link ReggieCompiledPattern}. + * + *

The state retains spans only; it never retains the input sequence. + */ +public final class ReggieMatchState { + private final LinearTokenSequenceMatcher matcher; + private final LinearTokenSequenceMatcher.MatchWorkspace workspace; + private final int[] starts; + private final int[] ends; + private boolean matched; + + ReggieMatchState(LinearTokenSequenceMatcher matcher) { + this.matcher = Objects.requireNonNull(matcher, "matcher"); + this.workspace = matcher.newMatchWorkspace(); + this.starts = new int[matcher.groupCount() + 1]; + this.ends = new int[matcher.groupCount() + 1]; + clear(); + } + + /** + * Attempts a full match inside {@code [start, end)}. + * + *

Prior state is cleared before validating the input or attempting the match. + */ + public boolean matches(CharSequence input, int start, int end) { + clear(); + return matched = matcher.matchIntoBounded(input, start, end, starts, ends, workspace); + } + + public int start() { + requireMatched(); + return starts[0]; + } + + public int end() { + requireMatched(); + return ends[0]; + } + + public int start(String name) { + requireMatched(); + return starts[matcher.groupIndex(name)]; + } + + public int end(String name) { + requireMatched(); + return ends[matcher.groupIndex(name)]; + } + + private void clear() { + Arrays.fill(starts, -1); + Arrays.fill(ends, -1); + matched = false; + } + + private void requireMatched() { + if (!matched) { + throw new IllegalStateException("there is no current match"); + } + } +} diff --git a/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/NamedOnlyLtsAdmissionTest.java b/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/NamedOnlyLtsAdmissionTest.java index 5e03580c..4826b6c0 100644 --- a/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/NamedOnlyLtsAdmissionTest.java +++ b/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/NamedOnlyLtsAdmissionTest.java @@ -19,12 +19,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import com.datadoghq.reggie.Reggie; import com.datadoghq.reggie.ReggieFlags; import com.datadoghq.reggie.ReggieOptions; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -121,32 +119,24 @@ void preservesOriginalNamedIndexWhileProjectingAnUnnamedCapture() { void admittedMatchersRemainIndependentUnderConcurrentUse() throws Exception { LinearTokenSequenceMatcher shared = RuntimeCompiler.tryCompileNamedOnlyLinearTokenSequence("(?\\S+)", 0).matcher(); - int threads = 4; - CountDownLatch done = new CountDownLatch(threads); - ConcurrentLinkedQueue failures = new ConcurrentLinkedQueue<>(); - try (ExecutorService executor = Executors.newFixedThreadPool(threads)) { - for (int thread = 0; thread < threads; thread++) { - int id = thread; - executor.execute( - () -> { - try { - LinearTokenSequenceMatcher independent = - RuntimeCompiler.tryCompileNamedOnlyLinearTokenSequence("(?\\S+)", 0) - .matcher(); - for (int iteration = 0; iteration < 100; iteration++) { - assertEquals("shared", shared.match("shared").group("value")); - assertEquals("value" + id, independent.match("value" + id).group("value")); - } - } catch (Throwable failure) { - failures.add(failure); - } finally { - done.countDown(); - } - }); - } - assertTrue(done.await(10, TimeUnit.SECONDS), "workers did not finish"); + ExecutorService executor = Executors.newFixedThreadPool(4); + CountDownLatch done = new CountDownLatch(4); + for (int thread = 0; thread < 4; thread++) { + int id = thread; + executor.execute( + () -> { + LinearTokenSequenceMatcher independent = + RuntimeCompiler.tryCompileNamedOnlyLinearTokenSequence("(?\\S+)", 0) + .matcher(); + for (int iteration = 0; iteration < 100; iteration++) { + assertEquals("shared", shared.match("shared").group("value")); + assertEquals("value" + id, independent.match("value" + id).group("value")); + } + done.countDown(); + }); } - assertTrue(failures.isEmpty(), () -> "concurrent admission failure: " + failures.peek()); + assertEquals(true, done.await(10, TimeUnit.SECONDS)); + executor.shutdownNow(); } private static void assertRejected( diff --git a/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/ReggieCompiledPatternTest.java b/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/ReggieCompiledPatternTest.java new file mode 100644 index 00000000..fd09f7df --- /dev/null +++ b/reggie-runtime/src/test/java/com/datadoghq/reggie/runtime/ReggieCompiledPatternTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2026-Present Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.reggie.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class ReggieCompiledPatternTest { + + @AfterEach + void clearCache() { + RuntimeCompiler.clearCache(); + } + + @Test + void compilesNativelyWithoutTouchingGeneralCompilerCaches() { + int patternCacheSize = RuntimeCompiler.cacheSize(); + int structuralCacheSize = RuntimeCompiler.structuralCacheSize(); + + ReggieCompilationResult result = + ReggieCompiledPattern.tryCompile( + new ReggieCompileRequest("(?\\S+)", ReggieCompileFlag.NONE)); + + assertTrue(result.isAdmitted()); + assertEquals(patternCacheSize, RuntimeCompiler.cacheSize()); + assertEquals(structuralCacheSize, RuntimeCompiler.structuralCacheSize()); + ReggieMatchState state = result.pattern().newState(); + assertTrue(state.matches(new GuardedCharSequence("value"), 0, 5)); + assertEquals(0, state.start("value")); + assertEquals(5, state.end("value")); + } + + @Test + void returnsNativeRejectionsWithoutFallbackOrCacheMutation() { + int patternCacheSize = RuntimeCompiler.cacheSize(); + int structuralCacheSize = RuntimeCompiler.structuralCacheSize(); + + ReggieCompilationResult result = + ReggieCompiledPattern.tryCompile( + new ReggieCompileRequest("(?[a-z]+)", ReggieCompileFlag.NONE)); + + assertFalse(result.isAdmitted()); + assertEquals(ReggieCompilationRejection.PLAN_UNAVAILABLE, result.rejection()); + assertThrows(IllegalStateException.class, result::pattern); + assertEquals(patternCacheSize, RuntimeCompiler.cacheSize()); + assertEquals(structuralCacheSize, RuntimeCompiler.structuralCacheSize()); + } + + @Test + void acceptsOnlyTheNativeProfileFlagValues() { + assertThrows( + NullPointerException.class, () -> new ReggieCompileRequest(null, ReggieCompileFlag.NONE)); + assertThrows(NullPointerException.class, () -> new ReggieCompileRequest("pattern", null)); + + ReggieMatchState state = + ReggieCompiledPattern.tryCompile( + new ReggieCompileRequest( + ".* \\[(?\\b\\w+\\b)\\] .*", ReggieCompileFlag.DOTALL)) + .pattern() + .newState(); + assertTrue(state.matches(new GuardedCharSequence("before\n [nginx] after"), 0, 21)); + assertEquals(9, state.start("logger")); + assertEquals(14, state.end("logger")); + } + + @Test + void exposesAbsoluteSpansWithoutMaterializingTheInput() { + ReggieCompiledPattern pattern = admitted("host=(?\\S+) status=(?\\d+)").pattern(); + ReggieMatchState state = pattern.newState(); + CharSequence input = new GuardedCharSequence("xx host=api status=200 yy"); + + assertTrue(state.matches(input, 3, input.length() - 3)); + assertEquals(3, state.start()); + assertEquals(input.length() - 3, state.end()); + assertEquals(8, state.start("host")); + assertEquals(11, state.end("host")); + assertEquals(19, state.start("status")); + assertEquals(22, state.end("status")); + } + + @Test + void usesTheNamedCaptureAfterAnUnnamedProjectedGroup() { + ReggieMatchState state = admitted("(x)(?\\S+)").pattern().newState(); + + assertTrue(state.matches(new GuardedCharSequence("xvalue"), 0, 6)); + assertEquals(1, state.start("value")); + assertEquals(6, state.end("value")); + } + + @Test + void clearsSpansBeforeFailedAndInvalidMatches() { + ReggieMatchState state = admitted("(?\\S+)").pattern().newState(); + assertTrue(state.matches(new GuardedCharSequence("value"), 0, 5)); + + assertFalse(state.matches(new GuardedCharSequence(""), 0, 0)); + assertThrows(IllegalStateException.class, state::start); + assertThrows(IllegalStateException.class, () -> state.start("value")); + + assertThrows( + IndexOutOfBoundsException.class, + () -> state.matches(new GuardedCharSequence("value"), -1, 5)); + assertThrows(IllegalStateException.class, state::end); + assertThrows(NullPointerException.class, () -> state.matches(null, 0, 0)); + assertThrows(IllegalStateException.class, () -> state.end("value")); + } + + @Test + void rejectsUnknownNamesAndReportsUnmatchedNamedGroups() { + ReggieMatchState state = + admitted("\"(?\\b\\w+\\b) (?\\S+)(?: HTTP/(?\\d+\\.\\d+)|)\"") + .pattern() + .newState(); + + assertTrue(state.matches(new GuardedCharSequence("\"GET /health\""), 0, 13)); + assertEquals(-1, state.start("version")); + assertEquals(-1, state.end("version")); + assertThrows(IllegalArgumentException.class, () -> state.start("missing")); + } + + @Test + void statesCanMatchIndependentlyInParallel() throws Exception { + ReggieCompiledPattern pattern = admitted("(?\\S+)").pattern(); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> calls = new ArrayList<>(); + for (int thread = 0; thread < 4; thread++) { + int id = thread; + calls.add( + () -> { + ReggieMatchState state = pattern.newState(); + for (int iteration = 0; iteration < 100; iteration++) { + String value = "value-" + id + "-" + iteration; + assertTrue(state.matches(new GuardedCharSequence(value), 0, value.length())); + assertEquals(0, state.start("value")); + assertEquals(value.length(), state.end("value")); + } + return null; + }); + } + for (Future result : executor.invokeAll(calls)) { + result.get(); + } + } finally { + executor.shutdownNow(); + } + } + + private static ReggieCompilationResult admitted(String source) { + ReggieCompilationResult result = + ReggieCompiledPattern.tryCompile(new ReggieCompileRequest(source, ReggieCompileFlag.NONE)); + assertTrue(result.isAdmitted(), () -> "rejection: " + result.rejection()); + return result; + } + + private static final class GuardedCharSequence implements CharSequence { + private final String value; + + private GuardedCharSequence(String value) { + this.value = value; + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + throw new AssertionError("matching must not materialize a subsequence"); + } + + @Override + public String toString() { + throw new AssertionError("matching must not materialize the input"); + } + } +}