Paul King created GROOVY-12123:
----------------------------------
Summary: Hoist constant regexes to static final fields
Key: GROOVY-12123
URL: https://issues.apache.org/jira/browse/GROOVY-12123
Project: Groovy
Issue Type: Improvement
Reporter: Paul King
h2. Summary
Hoist effectively-constant regular expressions to synthetic {{private static
final Pattern}}
fields so they are compiled once, rather than recompiled on every evaluation --
most
importantly inside loops. Transparent optimization; no new user-facing syntax.
h2. Motivation
Groovy's {{~/.../}} operator (and the {{=~}} / {{==~}} operators and JDK
conveniences such
as {{String#matches}}) construct/compile a {{Pattern}} each time they are
evaluated. In a
loop, a constant pattern is recompiled every iteration:
{code:groovy}
for (line in lines) {
if (line ==~ /\d{4}-\d{2}-\d{2}/) { ... } // recompiled every iteration
today
}
{code}
{{Pattern.compile}} is not cheap, and the pattern here never changes. Lifting
it to a
compile-once {{static final}} field removes the repeated compilation with no
change in
observable behaviour.
h2. Proposal
Where the compiler can prove a pattern is *effectively constant*, replace the
inline
regex expression with a reference to a synthetic {{private static final
Pattern}} field on
the enclosing class, initialized once. Identical (source + flags) patterns in
the same
class share a single field.
{code:groovy}
// conceptually, the loop above becomes:
private static final Pattern $pat$0 = Pattern.compile('\\d{4}-\\d{2}-\\d{2}')
...
for (line in lines) {
if ($pat$0.matcher(line).matches()) { ... }
}
{code}
This is loop-invariant code motion specialized for regex construction. It is
applied
automatically by the compiler when safe (see _Applicability_ and
_Correctness_), not via
an annotation.
h2. Applicability ("was applicable")
Only patterns with no runtime-dependent parts qualify:
* {{~/literal/}} with no interpolation -- hoistable.
* {{Pattern.compile("literal"[, constantFlags])}} -- hoistable (method-call
form).
* {{~"text ${CONST}"}} where the interpolated hole constant-folds -- hoistable
after folding.
* {{~"text ${var}"}} with a runtime {{var}} -- *not* hoistable; must build each
time.
* Any pattern closing over a captured, non-constant local -- *not* hoistable.
The primary targets are the {{~/.../}} operator and {{Pattern.compile}}.
Rewriting the JDK
convenience methods ({{String#matches}}/{{replaceAll}}/{{split}} with a
constant arg) into a
hoisted-{{Pattern}} form is a larger semantic change (routing through
{{Pattern}}/{{Matcher}})
and is a possible follow-up extension, not the initial cut.
h2. Correctness / caveats
* *Thread-safety is fine.* {{Pattern}} is immutable and thread-safe, so a shared
{{static final}} field is safe. {{Matcher}} is stateful and is *never*
hoisted -- only the
{{Pattern}}, with a fresh {{Matcher}} per match as today.
* *Exception timing.* An *invalid* constant pattern currently throws
{{PatternSyntaxException}}
at the point of evaluation (possibly never, if guarded by a branch). Eager
static-field
init would move that to class initialization
({{ExceptionInInitializerError}}). Mitigation:
only hoist patterns already known valid (the {{RegexChecker}} type checker
validates constant
literals), and/or use a lazy holder so init timing is preserved.
* *Identity.* User code must not rely on each {{~/.../}} yielding a distinct
{{Pattern}}
instance ({{===}}); {{Pattern}} carries no meaningful identity or mutability
contract, so
sharing is observationally equivalent.
h2. Scope
*In scope*
* Hoisting constant {{~/.../}} and {{Pattern.compile(constant[, constFlags])}}
to synthetic
{{static final}} fields, with per-class deduplication of identical patterns.
* Applying inside loops, closures, and script bodies (field on the enclosing
class).
*Out of scope / deferred*
* Rewriting {{String#matches}}/{{replaceAll}}/{{split}} convenience methods
(follow-up).
* Hoisting non-constant patterns or any form of runtime {{Pattern}} cache.
* Cross-class / cross-method caching.
h2. Relates to / must compose with GROOVY-12122
GROOVY-12122 adds a ReDoS runtime guard ({{RegexGuard}} helper, {{@SafeRegex}}
scoped
annotation). The two touch the same regex AST surface and sit on opposite ends
of the same
operation -- this ticket optimizes *Pattern construction*, GROOVY-12122 guards
*matching* --
so they compose, but the transforms must be ordering-aware:
* When {{@SafeRegex}} rewrites {{input ==~ /(a+)+$/}} to
{{RegexGuard.matches(~/(a+)+$/, input, ...)}}, this optimization must still
recognize the
{{~/(a+)+$/}} argument as a hoistable constant and lift it.
* When this optimization runs first and produces a {{static final Pattern P}},
{{@SafeRegex}}
must still recognize a match against {{P}} and wrap the input.
Desired combined result: the constant {{Pattern}} is compiled once into a
{{static final}}
field *and* each match wraps the input in the deadline guard. Whoever
implements the second
of the two should account for the first.
h2. Implementation notes
* A compiler optimization pass / AST transform over resolved expressions; runs
in both
dynamic and {{@CompileStatic}} modes.
* Constant detection reuses the same effectively-constant analysis as constant
folding.
* Synthetic field naming should be collision-free and deduplicated per class.
* Consider gating behind a flag initially (or restricting to
{{RegexChecker}}-validated
patterns) until the exception-timing behaviour is settled.
h2. References
* Related: GROOVY-12122 (ReDoS runtime guard -- RegexGuard / @SafeRegex):
https://issues.apache.org/jira/browse/GROOVY-12122
* {{java.util.regex.Pattern}} -- immutable/thread-safe; {{Matcher}} -- not
thread-safe
* {{RegexChecker}} in groovy-typecheckers (compile-time constant-pattern
validation)
--
This message was sent by Atlassian Jira
(v8.20.10#820010)