Commit 9e049654e2 for perl
commit 9e049654e20db808db6a1c4eeec53e7b21ae0d39
Author: Yves Orton <demerphq@gmail.com>
Date: Mon Oct 12 23:44:56 2026 +0200
regcomp.c: preserve captures when nested repeat iterations fail
We leave repeats containing captures inside nested quantifiers as
CURLYX. CURLYM can reconstruct a capture around its whole body, but
cannot restore captures inside a nested quantifier when an iteration
fails or is backtracked. CURLYX already has the iteration checkpoints
needed to restore these captures.
For example, "aa" =~ /(?:(aa){2})*/ matches the empty string and now
leaves $1 undefined. Likewise, "aabbaa" =~ /(?:([ab]{2}){2})*/ matches
"aabb" and preserves "bb" from the completed iteration, rather than
"aa" from the failed next iteration.
Fixes:
https://github.com/Perl/perl5/issues/24945
diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 08f2ccceca..c3c3ed2186 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -434,6 +434,16 @@ C</\A(?=a*)[0-9]a\z/> now matches C<"0a">.
=item *
+The regular expression optimizer no longer uses the fixed-width repeat
+optimization for a body containing captures inside nested quantifiers.
+This prevents failed or backtracked iterations from leaving incorrect
+capture values. For example, C<"aa" =~ /(?:(aa){2})*/> now leaves C<$1>
+undefined.
+
+[GH #24945]
+
+=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 5611f0615d..12958d5ca0 100644
--- a/regcomp_study.c
+++ b/regcomp_study.c
@@ -2704,6 +2704,14 @@ Perl_study_chunk(pTHX_
non optimistic eval is seen
anywhere.*/
&& !(data->flags & SF_HAS_PAR) /* no parens! */
+ /* CURLYM can emulate a capture around its whole body,
+ * but cannot restore captures inside a nested quantifier
+ * when an iteration fails or is backtracked. */
+ && (!ARG3a(oscan)
+ || ((data->flags & SF_IN_PAR)
+ && ARG3a(oscan) == ARG3b(oscan)
+ && OP(REGNODE_AFTER_type(oscan,
+ tregnode_CURLYX)) == OPEN))
&& !deltanext /* atom is fixed width */
&& minnext != 0 /* CURLYM can't handle zero width */
/* Nor characters whose fold at run-time may be
diff --git a/t/re/re_tests b/t/re/re_tests
index db50922101..b13f3112ef 100644
--- a/t/re/re_tests
+++ b/t/re/re_tests
@@ -2313,6 +2313,20 @@ a+X(*COMMIT)j aXaXj n - - - PLUS skip after (*COMMIT) [ASCII]
\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
+# GH #24945: captures from failed iterations of nested fixed-width quantifiers
+(?:(aa){2})* aa y ${\(defined($1) ? $1 : q(undef))} undef - failed first outer iteration must not leave a capture
+(?:([ab]{2}){2})* aabbaa y $&-$1 aabb-bb - failed next outer iteration must restore the last completed capture
+\A(?:([a-d]{2}){2})*ccdd\z aabbccdd y $1 bb - backtracking an outer iteration must restore the capture
+\A(?:([a-d]{2}){2})*?ccdd\z aabbccdd y $1 bb - lazy outer quantifier with a nested capture
+\A(?:([a-d]{2}){2}){0,2}ccdd\z aabbccdd y $1 bb - bounded outer quantifier must restore the capture
+\A(?:(aa){2})*aa\z aa y ${\(defined($1) ? $1 : q(undef))} undef - tail succeeds after a failed first iteration
+\A(?:([ab]){4})*aa\z aabbaa y $1 b - nested CURLYN capture must be restored
+\A(?:([ab]{2}){2})*\1\z aabbbb y $1 bb - backreference uses the last completed iteration
+\A((?:([a-d]{2}){2})*)ccdd\z aabbccdd y $1-$2 aabb-bb - outer capture around a nested quantifier
+\A(?:((aa){2}))*aa\z aa y ${\(defined($1) || defined($2) ? q(set) : q(undef))} undef - multiple captures in a failed first iteration
+(?:(\x{100}\x{101}){2})* \x{100}\x{101} y ${\(defined($1) ? $1 : q(undef))} undef - failed UTF-8 iteration must not leave a capture
+\A(?:([\x{100}-\x{103}]{2}){2})*\x{102}\x{102}\x{103}\x{103}\z \x{100}\x{100}\x{101}\x{101}\x{102}\x{102}\x{103}\x{103} y $1 \x{101}\x{101} - UTF-8 backtracking restores the capture
+
# Keep these lines at the end of the file
# pat string y/n/etc expr expected-expr skip-reason comment
# vim: softtabstop=0 noexpandtab