Perf improvements for String.endWith(Regex) (#1884)

* Compute a derived regex expression with `\z` anchor
* Ensure derived regex can't be messed up with comments (in extended mode) or quote start

Follow up to #1880
This commit is contained in:
Daniel Chao authored and GitHub committed 2026-10-05 09:09:03 -07:00
1 parent adaafc4045
commit ba25f4820c
4 files changed
+129 -11

No files matched your search

@@ -84,13 +84,14 @@ public final class VmUtils {
}
};
public static final int REGEX_FLAGS = Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE;
private static final Engine PKL_ENGINE =
Engine.newBuilder("pkl").option("engine.WarnInterpreterOnly", "false").build();
@SuppressWarnings("MagicConstant")
private static final Pattern DOC_COMMENT_LINE_START =
Pattern.compile(
"(?:^|\n|\r\n?)[ \t\f]*///[ \t\f]?",
Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE);
Pattern.compile("(?:^|\n|\r\n?)[ \t\f]*///[ \t\f]?", REGEX_FLAGS);
private static final SourceSection UNAVAILABLE_SOURCE_SECTION =
Source.newBuilder("pkl", "", "unavailable")
@@ -906,7 +907,8 @@ public final class VmUtils {
@TruffleBoundary
public static Pattern compilePattern(String pattern, Node location) {
try {
return Pattern.compile(pattern, Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE);
//noinspection MagicConstant
return Pattern.compile(pattern, REGEX_FLAGS);
} catch (PatternSyntaxException e) {
throw new VmExceptionBuilder()
.withLocation(location)
@@ -17,6 +17,7 @@ package org.pkl.core.stdlib.base;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.LoopNode;
import java.io.UnsupportedEncodingException;
@@ -332,15 +333,50 @@ public final class StringNodes {
}
@TruffleBoundary
@Specialization
protected boolean eval(String self, VmRegex regex) {
// try every suffix; `find()` would skip matches that overlap an earlier one
var matcher = regex.matcher(self).useTransparentBounds(true).useAnchoringBounds(false);
for (var start = self.length(); ; start = self.offsetByCodePoints(start, -1)) {
if (matcher.region(start, self.length()).matches()) return true;
if (start == 0) return false;
@Specialization(guards = "regex.equals(cachedRegex)")
protected boolean evalRegexCached(
String self,
VmRegex regex,
@Cached("regex") VmRegex cachedRegex,
@Cached("computePattern(cachedRegex)") Pattern pattern) {
return pattern.matcher(self).find();
}
@TruffleBoundary
@Specialization(replaces = "evalRegexCached")
protected boolean evalRegex(String self, VmRegex regex) {
return computePattern(regex).matcher(self).find();
}
// incorrect warning; non-capture group here is used to ensure correct precedence (e.g. prevent
// constructing a regex like `foo|bar$`)
@SuppressWarnings({
"RegExpUnnecessaryNonCapturingGroup",
"RegExpUnexpectedAnchor",
"MagicConstant"
})
protected final Pattern computePattern(VmRegex regex) {
var existingPattern = regex.getPattern();
var suffix = isCommentsEnabled(regex) ? "\n)\\z" : ")\\z";
try {
return Pattern.compile("(?:" + existingPattern.pattern() + suffix, VmUtils.REGEX_FLAGS);
} catch (PatternSyntaxException e) {
// if we have an unclosed group, the issue here must be that there was a quote start with
// an unmatched quote end (e.g. `foo\Qs`)
if (e.getMessage().contains("Unclosed group")) {
// incorrect regex error diagnostic here but not suppressable.
return Pattern.compile(
"(?:" + existingPattern.pattern() + "\\E" + suffix, VmUtils.REGEX_FLAGS);
} else {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().bug(e.getMessage()).withCause(e).build();
}
}
}
private boolean isCommentsEnabled(VmRegex regex) {
return (regex.getPattern().flags() & Pattern.COMMENTS) != 0;
}
}
public abstract static class indexOf extends ExternalMethod1Node {
@@ -0,0 +1,61 @@
res1 =
!"""
aa
zz
""".endsWith(Regex("(?m)aa"))
res1a =
"""
zz
aaa
""".endsWith(Regex("(?m)aa"))
res2 =
!"""
aa
zz
""".endsWith("aa")
res3 =
"""
aa
zz
""".endsWith("zz")
res4 =
"""
aa
zz
""".endsWith(
Regex(
"""
(?x)
zz # this is the comment for zz
""",
),
)
res5 =
!"""
aabbcc
""".endsWith(Regex("aa|zz"))
res6 = "zz".endsWith(Regex("(?x)zz"))
res7 = "zz".endsWith(Regex("(?x) zz # comment"))
res8 = "a\né".endsWith(Regex(#"(?x)\w"#))
res9 = "a\nÉ".endsWith(Regex("(?x)(?i)é"))
res10 = "é".endsWith(Regex(#"\w"#))
res11 = !"foo\n".endsWith(Regex("foo"))
res12 = !"foo\r\n".endsWith(Regex("foo"))
res13 = "a\nb".endsWith(Regex(#"(?m)a(?:\nb)??"#))
res14 = "x)".endsWith(Regex(#"x\Q)"#))
res14a = "x # some comment".endsWith(Regex(#"(?x)x\Q # some comment"#))
res15 = !"Ab".endsWith(Regex("a(?i)b"))
res16 = !"x\n".endsWith(Regex("x.(?s)"))
res17 = "a b".endsWith(Regex("a b(?x)"))
@@ -0,0 +1,19 @@
res1 = true
res1a = true
res2 = true
res3 = true
res4 = true
res5 = true
res6 = true
res7 = true
res8 = true
res9 = true
res10 = true
res11 = true
res12 = true
res13 = true
res14 = true
res14a = true
res15 = true
res16 = true
res17 = true