Commit 2d36917781 for perl
commit 2d3691778163f15eb7aed772adbbd8fd2848874c
Author: Yves Orton <demerphq@gmail.com>
Date: Mon Oct 12 22:32:55 2026 +0200
regcomp: do not restrict the start class with a 0 length lookahead-assertion
For /\A(?=a*)[0-9]a\z/, study_chunk() derives a class containing 'a'
and the empty string from the STAR inside the IFMATCH. Intersecting
that class with [0-9] produces ANYOF[], so the optimizer rejects "0a"
before we even try the pattern in the regex engine proper.
With this change the resulting start class is ANYOF[0-9], and the pattern
matches.
Lookahead bodies with a positive minimum length continue to contribute
their start-class constraints. We apply the same rule to the optional
positive assertion study path.
Adds new tests which fail without the fix.
I wonder if we could be a bit more clear in the debug output that
ANYOF[] means "pattern can never match".
Fixes https://github.com/Perl/perl5/issues/24943
diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 7d3d7a4baf..08f2ccceca 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -426,6 +426,14 @@ manager will later use a regex to expand these into links.
=item *
+The regular expression optimizer no longer rejects valid matches when a
+positive lookahead can match an empty string. For example,
+C</\A(?=a*)[0-9]a\z/> now matches C<"0a">.
+
+[GH #24943]
+
+=item *
+
Line number information is stored rather than discarded when an anonymous
subroutine is encountered during compilation. This should improve the
accuracy of line numbers emitted by diagnostic messages and `caller()`
diff --git a/regcomp_study.c b/regcomp_study.c
index 29b229b857..5611f0615d 100644
--- a/regcomp_study.c
+++ b/regcomp_study.c
@@ -3258,6 +3258,12 @@ Perl_study_chunk(pTHX_
recursed_depth, NULL, f, depth+1,
mutate_ok);
+ /* A lookahead body which can match without consuming any
+ * characters cannot restrict the next input character to
+ * its consuming paths. Treat its start class as anything. */
+ if ((f & SCF_DO_STCLASS_AND) && minnext == 0)
+ ssc_init(pRExC_state, &intrnl);
+
if (FLAGS(scan)) {
if ( deltanext < 0
|| deltanext > (I32) U8_MAX
@@ -3376,6 +3382,8 @@ Perl_study_chunk(pTHX_
&deltanext, last, &data_fake,
stopparen, recursed_depth, NULL,
f, depth+1, mutate_ok);
+ if ((f & SCF_DO_STCLASS_AND) && *minnextp == 0)
+ ssc_init(pRExC_state, &intrnl);
if (FLAGS(scan)) {
assert(0); /* This code has never been tested since this
is normally not compiled */
diff --git a/t/re/opt.t b/t/re/opt.t
index ad7323e3fe..f184e15b91 100644
--- a/t/re/opt.t
+++ b/t/re/opt.t
@@ -292,3 +292,5 @@ abc(*ACCEPT)xyz 3 0+abc - -
\R 1 - - stclass=LNBREAK \R start class
\Rfoo 4 - 1:2+foo stclass=LNBREAK \R start class with floating substr
\R+foo 4 - 1+foo skip,stclass=LNBREAK \R+ start class with PLUS and floating substr
+
+\A(?=a*)[0-9]a 2 1+a - noscan,anchor SBOL,stclass=~^ANYOF\[0-9\] nullable lookahead does not restrict the start class
diff --git a/t/re/re_tests b/t/re/re_tests
index a23ae33cae..db50922101 100644
--- a/t/re/re_tests
+++ b/t/re/re_tests
@@ -2295,6 +2295,24 @@ a+X(*COMMIT)j aXaXj n - - - PLUS skip after (*COMMIT) [ASCII]
(?:x+?\x{100})*a+ aaaa y $& aaaa - minmod cleared by PLUS
(?:x*?\x{100})*a+ aaaa y $& aaaa - minmod cleared by STAR
+# GH #24943
+\A(?=a*)[0-9]a\z 0a y $& 0a - nullable lookahead must not restrict the start class
+\A(?=a*?)[0-9]a\z 0a y $& 0a - lazy nullable lookahead
+\A(?=a?)[0-9]a\z 0a y $& 0a - optional lookahead body
+\A(?=a{0,3})[0-9]a\z 0a y $& 0a - bounded nullable lookahead
+\A(?=(?:ab)*)[0-9]a\z 0a y $& 0a - nullable multi-character lookahead
+\A(?=(?:a|))[0-9]a\z 0a y $& 0a - lookahead with an empty alternative
+\A(?=(?=a*)b*)[0-9]a\z 0a y $& 0a - nested nullable lookahead
+(?=a*)[0-9]a zz0a y $& 0a - unanchored nullable lookahead
+\A(?=a*)[0-9]a\z ba n - - - consuming class must still be checked
+\A(?=a+)[0-9]a\z 0a n - - - nonnullable lookahead must still be checked
+\A(?=a*b)[0-9]a\z 0a n - - - nonnullable suffix in lookahead
+\A(?=a*)[a-z]a\z ba y $& ba - consuming class partly overlaps the nullable body
+\A(?=\x{100}*)[0-9]a\z 0a y $& 0a - nullable Unicode lookahead
+\A(?=a*)\x{100}a\z \x{100}a y $& \x{100}a - Unicode consuming node after nullable lookahead
+\A(?=(?=a)b*)[a-z]a\z aa y $& aa - zero-length body with a required nested assertion
+\A(?=(?=a)b*)[a-z]a\z ba n - - - required nested assertion must still be checked
+
# Keep these lines at the end of the file
# pat string y/n/etc expr expected-expr skip-reason comment
# vim: softtabstop=0 noexpandtab