From ba25f4820cb88c5e3cf8324b70ea7d06d8f806d1 Mon Sep 17 00:00:00 2001 From: Daniel Chao Date: Mon, 5 Oct 2026 09:09:03 -0700 Subject: [PATCH] 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 --- .../java/org/pkl/core/runtime/VmUtils.java | 10 +-- .../org/pkl/core/stdlib/base/StringNodes.java | 50 ++++++++++++--- .../input/api/stringEndsWith.pkl | 61 +++++++++++++++++++ .../output/api/stringEndsWith.pcf | 19 ++++++ 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/stringEndsWith.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/stringEndsWith.pcf diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index e1fd6a3fc..c999c80e0 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -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) diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/StringNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/StringNodes.java index c125bd7b3..f55a8033b 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/StringNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/StringNodes.java @@ -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 { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/stringEndsWith.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/stringEndsWith.pkl new file mode 100644 index 000000000..55564b20b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/stringEndsWith.pkl @@ -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)")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/stringEndsWith.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/stringEndsWith.pcf new file mode 100644 index 000000000..b48b97d8b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/stringEndsWith.pcf @@ -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