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