Commit d0d5e9a44f for perl

commit d0d5e9a44f8054f91b0c334a76caa0af34a3423b
Author: Yves Orton <demerphq@gmail.com>
Date:   Sun Oct 11 00:26:13 2026 +0200

    regex: use the repeat scanner for all BMH shifts

    Once we have checked a whole UV of octets which cannot start a match, an
    identical UV cannot contain a start either. The BMH shift does not
    affect that, so we need not restrict the repeat scanner to shifts of
    zero or one.

    For /walrus|cat/, A has a shift of three. We used to keep looking up
    that shift on a long run of A's instead of scanning in UV sized chunks.

    On Linux x86_64, nonthreaded GCC 13.3 -O2 builds, three scans of 200
    million A's followed by cat drop from 0.489s to 0.048s. There is a cost
    when input does not repeat: 32 scans of 1 MiB of random uppercase ASCII
    for /abcdefgh|ijklmnop/ rise from 0.0101s to 0.0302s. Establishing the
    rejected window checks octets which BMH could otherwise skip.

diff --git a/regexec.c b/regexec.c
index 81307fa6ac..9f3d285326 100644
--- a/regexec.c
+++ b/regexec.c
@@ -3582,19 +3582,17 @@ S_find_byclass(pTHX_ regexp * prog, const regnode *c, char *s,
                                     distance = bmh_shift[uc[bmh_minlen - 1]];
                                 }

-                                /* For shifts zero and one, a start-map miss
-                                 * proves independently that no literal starts
-                                 * here.  The helper checks each later start
+                                /* A start-map miss proves that no literal
+                                 * starts here, regardless of the BMH shift.
+                                 * The helper checks each later start
                                  * until it has a full rejected word.  Equal
                                  * following words repeat those rejected
                                  * octets, so they cannot contain a start.
                                  * UTF-8 continuation octets cannot be starts
-                                 * either.  This proof does not depend on the
-                                 * BMH shift.  We use it for small shifts to
-                                 * avoid walking repeated input one octet at
-                                 * a time. */
-                                if (distance <= 1
-                                    && !(start_map[*uc] & start_mask))
+                                 * either.  The checked window therefore lets
+                                 * us skip equal windows without consulting
+                                 * their BMH anchors. */
+                                if (!(start_map[*uc] & start_mask))
                                 {
                                     /* The helper consumes at least the
                                      * rejected octet at uc. */
diff --git a/t/re/pat.t b/t/re/pat.t
index a4f353e070..376061ca89 100644
--- a/t/re/pat.t
+++ b/t/re/pat.t
@@ -28,7 +28,7 @@ skip_all_without_unicode_tables();
 my $has_locales = locales_enabled('LC_CTYPE');
 my $utf8_locale = find_utf8_ctype_locale();

-plan tests => 1786;  # Update this when adding/deleting tests.
+plan tests => 2218;  # Update this when adding/deleting tests.

 run_tests() unless caller;

@@ -384,6 +384,48 @@ sub run_tests {
                 }
             }

+            # These rejected octets have the maximum BMH shift.  The start
+            # map independently permits repeated-window scanning, including
+            # when the shift reaches the 255-octet cap.
+            my $long_first = 'a' . ('Z' x 254);
+            my $long_second = 'b' . ('Y' x 254);
+            my $long_re = qr/\Q$long_first\E|\Q$long_second\E/;
+
+            for my $case ([3, qr/walrus|cat/, 'cat'],
+                          [8, qr/abcdefgh|ijklmnop/, 'ijklmnop'],
+                          [255, $long_re, $long_second])
+            {
+                my ($shift, $re, $word) = @$case;
+
+                for my $unit ('Q', 'QR', 'QQRR', 'QQQQRRRR') {
+                    for my $offset (0 .. 7) {
+                        my $run = ('X' x $offset) . ($unit x 320);
+                        my $target = $run . $word;
+                        utf8::upgrade($target) if $encoding eq 'UTF-8';
+
+                        ok($target =~ /$re/ && $-[0] == length($run)
+                           && $+[0] == length($target),
+                           "AC $encoding shift $shift repeated '$unit' finds the match at alignment $offset");
+
+                        utf8::upgrade($run) if $encoding eq 'UTF-8';
+
+                        ok($run !~ /$re/,
+                           "AC $encoding shift $shift repeated '$unit' rejects a miss at alignment $offset");
+                    }
+                }
+
+                # Differing windows must return to ordinary scanning without
+                # losing a viable start inside the next window.
+                for my $offset (0 .. 7) {
+                    my $run = ('Q' x 320) . ('R' x $offset);
+                    my $target = $run . $word;
+                    utf8::upgrade($target) if $encoding eq 'UTF-8';
+
+                    ok($target =~ /$re/ && $-[0] == length($run),
+                       "AC $encoding shift $shift finds a start in a differing window at offset $offset");
+                }
+            }
+
             # Include runs shorter than a UV and tails shorter than a UV.
             # The helper must not read beyond the buffer or miss its last start.
             for my $run_length (0 .. 15) {