Commit 3e98e31d6d for perl

commit 3e98e31d6d13a987f4b5031fb0811c6d08b9e68e
Author: Yves Orton <demerphq@gmail.com>
Date:   Sun Oct 18 13:39:20 2026 +0200

    regex: do not split folds when extracting trie prefixes

    A UTF-8-flagged ssx|sy pattern must match sharp-s followed by x under
    /iu. Extracting the first s as a prefix splits the fold, so neither node
    can match the input character. This regressed in 9c721a4477.

    Record the boundaries inside multi-character folds while building the
    words. Only extract a prefix at a boundary which is safe for every
    alternative, including overlapping folds in sss.

diff --git a/pod/perldelta.pod b/pod/perldelta.pod
index 0d79c45d3e..60ac918de8 100644
--- a/pod/perldelta.pod
+++ b/pod/perldelta.pod
@@ -431,6 +431,12 @@ manager will later use a regex to expand these into links.

 =item *

+Case-insensitive trie prefix extraction no longer splits a multi-character
+fold between regex nodes. For example, a UTF-8 flagged C<ssx|sy> pattern
+under C</iu> now matches C<"\x{df}x">.
+
+=item *
+
 A case-insensitive regular expression with a folded literal start class
 could loop indefinitely when searching at the end of a UTF-8 string.
 This could occur on a subsequent C</g> match after a successful match.
diff --git a/regcomp_trie.c b/regcomp_trie.c
index aeacb3d8b1..323da0aea4 100644
--- a/regcomp_trie.c
+++ b/regcomp_trie.c
@@ -588,6 +588,7 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
      * the same representation in both modes; awkward and high codepoints
      * require processed mode. */
     bool trie_needs_codepoint_processing;
+    bool unsafe_prefix[256] = { FALSE };

     /* Store the trie and, in debugging builds, its word list. */
 #ifdef DEBUGGING
@@ -805,6 +806,22 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
                         uvc = (U32)*uc;
                         len = 1;
                     }
+                    /* An input character can fold to several source
+                     * characters. Do not extract a prefix ending inside
+                     * that fold, including overlapping folds in "sss". */
+                    if (folder && UTF && wordlen < 256) {
+                        const STRLEN folded_octets
+                            = is_MULTI_CHAR_FOLD_utf8_safe(uc, e);
+                        if (folded_octets) {
+                            const U32 folded_chars
+                                = is_THREE_CHAR_FOLD_HEAD_utf8_safe(
+                                    uc, uc + folded_octets)
+                                ? 3 : 2;
+                            for (U32 i = 1; i < folded_chars
+                                    && wordlen - 1 + i < 256; i++)
+                                unsafe_prefix[wordlen - 1 + i] = TRUE;
+                        }
+                    }
                     TRIE_CHARCOUNT(trie)++;

                     /* YJO: KHW says that we dont need a NATIVE_TO_UNI() on uvc here */
@@ -1473,9 +1490,12 @@ Perl_make_trie(pTHX_ RExC_state_t *pRExC_state, regnode *startbranch,
                     octets++;
                     state = next_state;
                     if (!need) {
-                        complete_state = state;
-                        complete_octets = octets;
-                        complete_chars = ++chars;
+                        chars++;
+                        if (!unsafe_prefix[chars]) {
+                            complete_state = state;
+                            complete_octets = octets;
+                            complete_chars = chars;
+                        }
                     }
                 }
                 state = complete_state;
diff --git a/t/re/pat.t b/t/re/pat.t
index 46989c9a2e..8e33d4104a 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 => 2438 + 6 * $Config{uvsize};  # Update when adding/deleting tests.
+plan tests => 2630 + 6 * $Config{uvsize};  # Update when adding/deleting tests.

 run_tests() unless caller;

@@ -167,6 +167,47 @@ sub run_tests {
         }
     }

+    {
+        my @cases = (
+            ['ssx|sy', "\x{df}x!"],
+            ['sssx|ssy', "s\x{df}x!"],
+            ['ffix|fy', "\x{fb03}x!"],
+            ['ffix|ffy', "\x{fb03}x!"],
+            ["\x{3b9}\x{308}\x{301}x|\x{3b9}y", "\x{390}x!"],
+            ["\x{565}\x{582}x|\x{565}y", "\x{587}x!"],
+        );
+        for my $case (@cases) {
+            for my $charset ('u', 'a', 'aa', 'd') {
+                for my $jump (0, 1) {
+                    my $pattern = join '|', map {
+                        $_ . ($jump ? '([!]?)' : '')
+                    } split /\|/, $case->[0];
+                    for my $pattern_utf8 (0, 1) {
+                        my $source = "(?i$charset:$pattern)";
+                        utf8::upgrade($source) if $pattern_utf8;
+                        my ($plain, $trie);
+                        { local ${^RE_TRIE_MAXBUF} = -1;
+                          $plain = qr/$source(?#whole fold branches)/; }
+                        { local ${^RE_TRIE_MAXBUF} = 65536;
+                          $trie = qr/$source(?#whole fold trie)/; }
+                        for my $target_utf8 (0, 1) {
+                            my $target = '??' . $case->[1];
+                            utf8::upgrade($target) if $target_utf8;
+                            my @result;
+                            for my $re ($plain, $trie) {
+                                push @result, $target =~ $re
+                                    ? join(':', map { defined $_ ? $_ : '-' } @-, @+)
+                                    : 'no match';
+                            }
+                            is($result[1], $result[0],
+                               "trie prefix preserves folds: /$charset, jump=$jump, pattern_utf8=$pattern_utf8, target_utf8=$target_utf8");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     {
         my @folded_backtracks = (
             ['ss', "\x{df}"],