Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
package com.datadoghq.reggie.codegen.analysis;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/** Executable, deterministic plan for a categorized linear-token-sequence regex. */
public record LinearTokenSequencePlan(List<Op> ops, int groupCount) {
Expand Down Expand Up @@ -74,6 +78,37 @@ static Op optional(List<Op> children) {
ops = List.copyOf(ops);
}

/**
* Returns whether this plan has a capture-producing operation for every requested group index.
*
* <p>The check recurses into optional sequences because an optional capture remains observable
* when its present branch matches. It deliberately does not use {@link #groupCount()}: that value
* is a top-level construction detail and may omit captures nested in an optional sequence.
*/
public boolean coversCaptureIndexes(Collection<Integer> requiredIndexes) {
Objects.requireNonNull(requiredIndexes, "requiredIndexes");
Set<Integer> remaining = new HashSet<>();
for (Integer index : requiredIndexes) {
if (index == null || index <= 0) {
throw new IllegalArgumentException("capture group indexes must be positive: " + index);
}
remaining.add(index);
}
removeCoveredIndexes(ops, remaining);
return remaining.isEmpty();
}

private static void removeCoveredIndexes(List<Op> ops, Set<Integer> remaining) {
for (Op op : ops) {
if (op.groupNumber() > 0) {
remaining.remove(op.groupNumber());
}
if (!op.children().isEmpty()) {
removeCoveredIndexes(op.children(), remaining);
}
}
}

/** Converts categorizer atoms into a closed, executable linear-token-sequence plan. */
public static Optional<LinearTokenSequencePlan> from(PatternCategorization categorization) {
if (!categorization.isLinearTokenSequence()) return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package com.datadoghq.reggie.codegen.analysis;

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 com.datadoghq.reggie.codegen.ast.RegexNode;
Expand Down Expand Up @@ -63,6 +65,41 @@ void failsClosedForGeneralRegexCategories() throws Exception {
assertTrue(LinearTokenSequencePlan.from(categorization).isEmpty());
}

@Test
void coverageIncludesCapturesNestedInOptionalSequences() throws Exception {
LinearTokenSequencePlan plan = planFor("(?:|(?<value>\\S+))");

assertEquals(0, plan.groupCount());
assertTrue(plan.coversCaptureIndexes(List.of(1)));
assertFalse(plan.coversCaptureIndexes(List.of(2)));
}

@Test
void coverageRecursesThroughMultipleNestedOptionalLevels() throws Exception {
LinearTokenSequencePlan plan = planFor("(?:(?:(?:(?<deep>\\d+))|)|)");

assertTrue(plan.coversCaptureIndexes(List.of(1)));
assertFalse(plan.coversCaptureIndexes(List.of(2)));
}

@Test
void coverageRequiresEveryNamedCaptureNotOnlyTheLargestGroupNumber() throws Exception {
RegexParser parser = new RegexParser();
RegexNode ast = parser.parse("(?<outer>(?:-|(?<inner>[+-]?\\d+)))");
LinearTokenSequencePlan plan =
LinearTokenSequencePlan.from(PatternCategorizer.categorize(ast)).orElseThrow();

assertTrue(plan.coversCaptureIndexes(List.of(2)));
assertFalse(plan.coversCaptureIndexes(parser.getGroupNameMap().values()));
}

@Test
void coverageRejectsNonPositiveGroupIndexes() throws Exception {
LinearTokenSequencePlan plan = planFor("(?<value>\\S+)");

assertThrows(IllegalArgumentException.class, () -> plan.coversCaptureIndexes(List.of(0)));
}

private static LinearTokenSequencePlan planFor(String pattern) throws Exception {
return LinearTokenSequencePlan.from(categorize(pattern)).orElseThrow();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
final class LinearTokenSequenceMatcher extends ReggieMatcher {
private final LinearTokenSequencePlan plan;
private final int groupCount;
private final int[] scratchStarts;
private final int[] scratchEnds;
private final int[][] optionalScratchStarts;
private final int[][] optionalScratchEnds;
private final int optionalDepth;
private final ThreadLocal<MatchWorkspace> workspace;

private MatchWorkspace workspace() {
return workspace.get();
}

LinearTokenSequenceMatcher(
String pattern,
Expand All @@ -38,11 +40,8 @@ final class LinearTokenSequenceMatcher extends ReggieMatcher {
this.plan = plan;
this.groupCount = groupCount;
this.nameToIndex = Map.copyOf(nameToIndex);
this.scratchStarts = new int[groupCount + 1];
this.scratchEnds = new int[groupCount + 1];
int optionalDepth = maxOptionalDepth(plan.ops());
this.optionalScratchStarts = new int[optionalDepth][groupCount + 1];
this.optionalScratchEnds = new int[optionalDepth][groupCount + 1];
this.optionalDepth = maxOptionalDepth(plan.ops());
this.workspace = ThreadLocal.withInitial(() -> new MatchWorkspace(groupCount, optionalDepth));
}

@Override
Expand All @@ -52,7 +51,8 @@ boolean embedsNameMap() {

@Override
public boolean matches(String input) {
return matchInto(input, scratchStarts, scratchEnds);
Objects.requireNonNull(input, "input");
return matchesAt(input, 0, workspace(), true);
}

@Override
Expand All @@ -64,17 +64,20 @@ public boolean find(String input) {
public int findFrom(String input, int start) {
Objects.requireNonNull(input, "input");
if (start < 0 || start > input.length()) return -1;
MatchWorkspace ws = workspace();
for (int pos = start; pos <= input.length(); pos++) {
if (matchesAt(input, pos, scratchStarts, scratchEnds, false)) return pos;
if (matchesAt(input, pos, ws, false)) return pos;
}
return -1;
}

@Override
public MatchResult match(String input) {
int[] starts = new int[groupCount + 1];
int[] ends = new int[groupCount + 1];
if (!matchInto(input, starts, ends)) return null;
Objects.requireNonNull(input, "input");
MatchWorkspace ws = workspace();
if (!matchesAt(input, 0, ws, true)) return null;
int[] starts = Arrays.copyOf(ws.starts, groupCount + 1);
int[] ends = Arrays.copyOf(ws.ends, groupCount + 1);
if (!nameToIndex.isEmpty()) {
return new NamedMatchResultImpl(input, starts, ends, groupCount, nameToIndex);
}
Expand Down Expand Up @@ -104,15 +107,21 @@ public MatchResult findMatch(String input) {

@Override
public MatchResult findMatchFrom(String input, int start) {
int pos = findFrom(input, start);
if (pos < 0) return null;
int[] starts = new int[groupCount + 1];
int[] ends = new int[groupCount + 1];
if (!matchesAt(input, pos, starts, ends, false)) return null;
if (!nameToIndex.isEmpty()) {
return new NamedMatchResultImpl(input, starts, ends, groupCount, nameToIndex);
Objects.requireNonNull(input, "input");
if (start < 0 || start > input.length()) return null;
MatchWorkspace ws = workspace();
for (int pos = start; pos <= input.length(); pos++) {
if (!matchesAt(input, pos, ws, false)) {
continue;
}
int[] starts = Arrays.copyOf(ws.starts, groupCount + 1);
int[] ends = Arrays.copyOf(ws.ends, groupCount + 1);
if (!nameToIndex.isEmpty()) {
return new NamedMatchResultImpl(input, starts, ends, groupCount, nameToIndex);
}
return new MatchResultImpl(input, starts, ends, groupCount, nameToIndex);
}
return new MatchResultImpl(input, starts, ends, groupCount, nameToIndex);
return null;
}

@Override
Expand All @@ -123,13 +132,16 @@ public boolean matchInto(String input, int[] groupStarts, int[] groupEnds) {
if (groupStarts.length <= groupCount || groupEnds.length <= groupCount) {
throw new IndexOutOfBoundsException("group arrays too small for " + groupCount + " groups");
}
if (!matchesAt(input, 0, scratchStarts, scratchEnds, true)) return false;
System.arraycopy(scratchStarts, 0, groupStarts, 0, groupCount + 1);
System.arraycopy(scratchEnds, 0, groupEnds, 0, groupCount + 1);
MatchWorkspace ws = workspace();
if (!matchesAt(input, 0, ws, true)) return false;
System.arraycopy(ws.starts, 0, groupStarts, 0, groupCount + 1);
System.arraycopy(ws.ends, 0, groupEnds, 0, groupCount + 1);
return true;
}

private boolean matchesAt(String input, int offset, int[] starts, int[] ends, boolean fullMatch) {
private boolean matchesAt(String input, int offset, MatchWorkspace workspace, boolean fullMatch) {
int[] starts = workspace.starts;
int[] ends = workspace.ends;
Arrays.fill(starts, -1);
Arrays.fill(ends, -1);
starts[0] = offset;
Expand All @@ -144,7 +156,7 @@ private boolean matchesAt(String input, int offset, int[] starts, int[] ends, bo
i++;
continue;
}
pos = apply(op, input, pos, starts, ends, i == plan.ops().size() - 1, 0);
pos = apply(op, input, pos, starts, ends, i == plan.ops().size() - 1, workspace, 0);
if (pos < 0) return false;
}
if (fullMatch && pos != input.length()) return false;
Expand All @@ -159,6 +171,7 @@ private int apply(
int[] starts,
int[] ends,
boolean lastOp,
MatchWorkspace workspace,
int optionalDepth) {
return switch (op.kind()) {
case LITERAL -> startsWith(input, pos, op.literal()) ? pos + op.literal().length() : -1;
Expand Down Expand Up @@ -187,7 +200,8 @@ private int apply(
captureBracketedWordAfterSkip(input, pos, op.groupNumber(), starts, ends);
case SKIP_ANY -> lastOp ? input.length() : -1;
case ANCHOR -> pos;
case OPTIONAL_SEQUENCE -> applyOptional(op, input, pos, starts, ends, optionalDepth);
case OPTIONAL_SEQUENCE ->
applyOptional(op, input, pos, starts, ends, workspace, optionalDepth);
};
}

Expand Down Expand Up @@ -292,24 +306,35 @@ private static int captureIpOrHost(String input, int pos, int group, int[] start

private static int captureBracketedWordAfterSkip(
String input, int pos, int group, int[] starts, int[] ends) {
int search = pos;
int lastStart = -1;
int lastEnd = -1;
while (search < input.length()) {
int open = input.indexOf('[', search);
if (open < 0) break;
int close = input.indexOf(']', open + 1);
if (close < 0) break;
int wordEnd = open + 1;
while (wordEnd < close && isWord(input.charAt(wordEnd))) wordEnd++;
if (wordEnd == close
&& wordEnd > open + 1
&& close + 1 < input.length()
&& Character.isWhitespace(input.charAt(close + 1))) {
lastStart = open + 1;
lastEnd = close;
int open = -1;
int wordEnd = -1;
for (int index = pos; index < input.length(); index++) {
char ch = input.charAt(index);
if (ch == '[') {
open = index;
wordEnd = index + 1;
continue;
}
if (open < 0) {
continue;
}
if (ch == ']') {
if (wordEnd == index
&& wordEnd > open + 1
&& index + 1 < input.length()
&& Character.isWhitespace(input.charAt(index + 1))) {
lastStart = open + 1;
lastEnd = index;
}
open = -1;
wordEnd = -1;
} else if (wordEnd == index && isWord(ch)) {
wordEnd++;
} else {
wordEnd = -1;
}
search = open + 1;
}
if (lastStart < 0) return -1;
set(starts, ends, group, lastStart, lastEnd);
Expand Down Expand Up @@ -365,9 +390,10 @@ private int applyOptional(
int pos,
int[] starts,
int[] ends,
MatchWorkspace workspace,
int optionalDepth) {
int[] savedStarts = optionalScratchStarts[optionalDepth];
int[] savedEnds = optionalScratchEnds[optionalDepth];
int[] savedStarts = workspace.optionalStarts[optionalDepth];
int[] savedEnds = workspace.optionalEnds[optionalDepth];
System.arraycopy(starts, 0, savedStarts, 0, starts.length);
System.arraycopy(ends, 0, savedEnds, 0, ends.length);
int next = pos;
Expand All @@ -380,6 +406,7 @@ private int applyOptional(
starts,
ends,
i == op.children().size() - 1,
workspace,
optionalDepth + 1);
if (next < 0) {
System.arraycopy(savedStarts, 0, starts, 0, starts.length);
Expand All @@ -404,6 +431,20 @@ private static int maxOptionalDepth(Iterable<LinearTokenSequencePlan.Op> ops) {
return max;
}

private static final class MatchWorkspace {
final int[] starts;
final int[] ends;
final int[][] optionalStarts;
final int[][] optionalEnds;

MatchWorkspace(int groupCount, int optionalDepth) {
starts = new int[groupCount + 1];
ends = new int[groupCount + 1];
optionalStarts = new int[optionalDepth][groupCount + 1];
optionalEnds = new int[optionalDepth][groupCount + 1];
}
}

private static int skipWhitespace(String input, int pos) {
int start = pos;
while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) pos++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ private static ReggieMatcher compileInternal(
private static ReggieMatcher tryCompileLinearTokenSequence(
String pattern, RegexNode ast, Map<String, Integer> nameMap) {
return LinearTokenSequencePlan.from(PatternCategorizer.categorize(ast))
.filter(plan -> plan.coversCaptureIndexes(nameMap.values()))
.filter(RuntimeCompiler::isRuntimeExecutableLinearTokenSequence)
.map(plan -> new LinearTokenSequenceMatcher(pattern, plan, countGroups(pattern), nameMap))
.map(m -> m.embedsNameMap() ? m : new NameEnrichingMatcher(m))
Expand Down
Loading
Loading