Commit 77af3ea6c1 for perl

commit 77af3ea6c10adddf36abd1cde6074a37e104f244
Author: Yves Orton <demerphq@gmail.com>
Date:   Mon Oct 12 23:45:06 2026 +0200

    regexec.c: clear captures when CURLYX chooses zero repetitions

    We clear the captures in a CURLYX body when choosing zero repetitions,
    as CURLYN and CURLYM do. We save the incoming values first so that
    backtracking into the body can use the original capture state.

    Adding a no-op (?{}) no longer changes whether

        "abba" =~ /\A(?:(a)?b)+\1\z/

    matches. Both forms reject the string because the final iteration
    skips (a), leaving no capture for \1. A capture which participates and
    matches the empty string remains defined.

    Fixes:
        https://github.com/Perl/perl5/issues/18865
        https://github.com/Perl/perl5/issues/8267

diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 4b59cb71d6..afd3038dc2 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -434,6 +434,15 @@ C</\A(?=a*)[0-9]a\z/> now matches C<"0a">.

 =item *

+General regular expression repeats clear captures in their bodies when
+matching zero repetitions, as optimized repeats do. Adding a no-op
+C<(?{})> code block no longer changes whether a backreference to a skipped
+optional group matches.
+
+[GH #8267] [GH #18865]
+
+=item *
+
 Negative lookahead and lookbehind assertions restore capture buffers after
 evaluating their bodies. Captures from failed assertion attempts no longer
 appear in a successful match or affect later backreferences and conditions.
diff --git a/regexec.c b/regexec.c
index 85f1aaa729..d3877b8fd2 100644
--- a/regexec.c
+++ b/regexec.c
@@ -9398,6 +9398,15 @@ NULL
             /* Prefer B over A for minimal matching. */

             if (cur_curlyx->u.curlyx.minmod) {
+                if (!cur_curlyx->u.curlyx.count
+                    && ARG3a(cur_curlyx->u.curlyx.me))
+                {
+                    ST.cp = regcppush(rex, 0, maxopenparen);
+                    REGCP_SET(ST.lastcp);
+                    CAPTURE_CLEAR(ARG3a(cur_curlyx->u.curlyx.me),
+                                  ARG3b(cur_curlyx->u.curlyx.me),
+                                  "WHILEM zero repetitions");
+                }
                 ST.save_curlyx = cur_curlyx;
                 cur_curlyx = cur_curlyx->u.curlyx.prev_curlyx;
                 ST.saved_seen_nonregular = seen_nonregular;
@@ -9432,6 +9441,12 @@ NULL
         case WHILEM_B_max_fail: /* just failed to match B in a maximal match */
             seen_nonregular |= ST.saved_seen_nonregular;
             cur_curlyx = ST.save_curlyx;
+            if (!cur_curlyx->u.curlyx.count
+                && ARG3a(cur_curlyx->u.curlyx.me))
+            {
+                REGCP_UNWIND(ST.lastcp);
+                regcppop(rex, &maxopenparen);
+            }
             cur_curlyx->u.curlyx.lastloc = ST.save_lastloc;
             cur_curlyx->u.curlyx.count--;
             CACHEsayNO;
@@ -9456,6 +9471,18 @@ NULL

           do_whilem_B_max:
             /* now try B */
+            /* Zero repetitions leave the captures in this body unset, just
+             * as CURLYN and CURLYM do. Save the incoming values so that
+             * backtracking can try the body with its original capture state. */
+            if (!cur_curlyx->u.curlyx.count
+                && ARG3a(cur_curlyx->u.curlyx.me))
+            {
+                ST.cp = regcppush(rex, 0, maxopenparen);
+                REGCP_SET(ST.lastcp);
+                CAPTURE_CLEAR(ARG3a(cur_curlyx->u.curlyx.me),
+                              ARG3b(cur_curlyx->u.curlyx.me),
+                              "WHILEM zero repetitions");
+            }
             ST.save_curlyx = cur_curlyx;
             cur_curlyx = cur_curlyx->u.curlyx.prev_curlyx;
             ST.saved_seen_nonregular = seen_nonregular;
@@ -9467,6 +9494,12 @@ NULL
         case WHILEM_B_min_fail: /* just failed to match B in a minimal match */
             seen_nonregular |= ST.saved_seen_nonregular;
             cur_curlyx = ST.save_curlyx;
+            if (!cur_curlyx->u.curlyx.count
+                && ARG3a(cur_curlyx->u.curlyx.me))
+            {
+                REGCP_UNWIND(ST.lastcp);
+                regcppop(rex, &maxopenparen);
+            }

             if (cur_curlyx->u.curlyx.count >= /*max*/ARG2i(cur_curlyx->u.curlyx.me)) {
                 /* Maximum greed exceeded */
diff --git a/t/re/pat.t b/t/re/pat.t
index 6b964669ef..f9efb3334a 100644
--- a/t/re/pat.t
+++ b/t/re/pat.t
@@ -1309,15 +1309,14 @@ sub run_tests {
     }

     {
-        local $::TODO = "[perl #38133]";
-
         "A" =~ /(((?:A))?)+/;
         my $first = $2;

         "A" =~ /(((A))?)+/;
         my $second = $2;

-        is($first, $second);
+        is($first, $second,
+           "GH #8267: zero repetitions clear nested captures consistently");
     }

     {
@@ -2456,33 +2455,24 @@ SKIP:
         ok( "aaa" =~ /(?:((?{})a)?\1)+/,
             "GH Issue #18865 'aaa' - deoptimized pattern matches");
         $y = "($-[0],$+[0])";
-        {
-            local $::TODO = "Not Yet Implemented";
-            is( $y, $x,
-                "GH Issue #18865 'aaa' - test optimization");
-        }
+        is( $y, $x,
+            "GH Issue #18865 'aaa' - test optimization");
         ok( "ababab" =~ /(?:(?:(ab))?\1)+/,
             "GH Issue #18865 'ababab' - pattern matches");
         $x = "($-[0],$+[0])";
         ok( "ababab" =~ /(?:(?:((?{})ab))?\1)+/,
             "GH Issue #18865 'ababab' - deoptimized pattern matches");
         $y = "($-[0],$+[0])";
-        {
-            local $::TODO = "Not Yet Implemented";
-            is( $y, $x,
-                "GH Issue #18865 'ababab' - test optimization");
-        }
+        is( $y, $x,
+            "GH Issue #18865 'ababab' - test optimization");
         ok( "XaaXbbXb" =~ /(?:X([ab])?\1)+/,
             "GH Issue #18865 'XaaXbbXb' - pattern matches");
         $x = "($-[0],$+[0])";
         ok( "XaaXbbXb" =~ /(?:X((?{})[ab])?\1)+/,
             "GH Issue #18865 'XaaXbbXb' - deoptimized pattern matches");
         $y = "($-[0],$+[0])";
-        {
-            local $::TODO = "Not Yet Implemented";
-            is( $y, $x,
-                "GH Issue #18865 'XaaXbbXb' - test optimization");
-        }
+        is( $y, $x,
+            "GH Issue #18865 'XaaXbbXb' - test optimization");
     }
     {
         # Test that ${^LAST_SUCCESSFUL_PATTERN} works as expected.
diff --git a/t/re/pat_rt_report.t b/t/re/pat_rt_report.t
index 701cf9dbfb..21bec9df97 100644
--- a/t/re/pat_rt_report.t
+++ b/t/re/pat_rt_report.t
@@ -997,15 +997,14 @@ sub run_tests {
     }

     {
-        local $::TODO = "[perl #38133]";
-
         "A" =~ /(((?:A))?)+/;
         my $first = $2;

         "A" =~ /(((A))?)+/;
         my $second = $2;

-        is($first, $second);
+        is($first, $second,
+           "GH #8267: zero repetitions clear nested captures consistently");
     }

     {
diff --git a/t/re/re_tests b/t/re/re_tests
index 44c6bc4765..d3407ef0c9 100644
--- a/t/re/re_tests
+++ b/t/re/re_tests
@@ -2350,6 +2350,23 @@ a+X(*COMMIT)j	aXaXj	n	-	-	-	PLUS skip after (*COMMIT) [ASCII]
 \A(?(?!(a)b)a|b)\z	a	y	${\(defined($1) ? $1 : q(undef))}	undef	-	negative assertion used as a condition
 \A...(?<!(a{1,2})b)c\z	aaac	y	${\(defined($1) ? $1 : q(undef))}	undef	-	variable-length negative lookbehind restores each attempt

+# GH #18865: zero repetitions must clear captures in optimized and general paths
+\A(?:(a)?b)+\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	optimized optional capture is unset in the final iteration
+(?{})\A(?:(a)?b)+\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	general optional capture is unset in the final iteration
+\A(?:(a)?b)+\1\z	abba	n	-	-	-	optimized backreference to a skipped capture fails
+(?{})\A(?:(a)?b)+\1\z	abba	n	-	-	-	general backreference to a skipped capture fails
+(?{})\A(?:(aa)?b)+\z	aabb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	multi-character optional capture
+(?{})\A(?:(a)*b)+\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	inner star with zero repetitions
+(?{})\A(?:(a){0,2}b)+\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	bounded inner quantifier with zero repetitions
+(?{})\A(?:(a)??b)+\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	lazy optional capture
+(?{})\A(?:(a)?b)+?\z	abb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	lazy outer repeat
+(?{})\A(?:(a)??b)+\z	abab	y	$1	a	-	backtracking from zero into a matching optional group
+(?{})\A(?:(a|b\1)??c)+\z	acbac	y	$1	ba	-	backtracking into the body restores the incoming capture
+(?{})\A(?:(a*)b)+\z	abb	y	<${\(defined($1) ? q(defined) : q(undef))}=$1>	<defined=>	-	matched empty capture stays defined
+(?{})\A(?:(a)?b)+(?(1)a|c)\z	abbc	y	$&	abbc	-	conditional sees the skipped capture as unset
+(?{})\A(?:(\x{100})?b)+\z	\x{100}bb	y	${\(defined($1) ? $1 : q(undef))}	undef	-	Unicode optional capture
+(?{})\A(?:(a)?b)+\z	abab	y	$1	a	-	final participating capture is preserved
+
 # Keep these lines at the end of the file
 # pat	string	y/n/etc	expr	expected-expr	skip-reason	comment
 # vim: softtabstop=0 noexpandtab