<commit_before>my class Cursor {... }
my class Range  {... }
my class Match  {... }
my class X::Str::Numeric  { ... }
my class X::Str::Match::x { ... }
my class X::Str::Trans::IllegalKey { ... }
my class X::Str::Trans::InvalidArg { ... }
my class X::Numeric::Confused { ... }

my constant $?TABSTOP = 8;

my class Str does Stringy { # declared in BOOTSTRAP
    # class Str is Cool {
    #     has str $!value is box_target;

    multi method WHY('Life, the Universe, and Everything':) { 42 }

    multi method WHICH(Str:D:) {
        nqp::box_s(
            nqp::concat(
                nqp::concat(nqp::unbox_s(self.^name), '|'),
                $!value
            ),
            ObjAt
        );
    }
    submethod BUILD(Str() :$value = '') {
        nqp::bindattr_s(self, Str, '$!value', nqp::unbox_s($value))
    }

    multi method Bool(Str:D:) {
        nqp::p6bool(nqp::chars($!value));
    }

    multi method Str(Str:D:)     { self }
    multi method Stringy(Str:D:) { self }
    multi method DUMP(Str:D:) { self.perl }

    method Int(Str:D:) { self.Numeric.Int; }
    method Num(Str:D:) { self.Numeric.Num; }

    multi method ACCEPTS(Str:D: Str:D \other) {
        nqp::p6bool(nqp::iseq_s(nqp::unbox_s(other),$!value));
    }
    multi method ACCEPTS(Str:D: Any:U \other) {
        False;
    }
    multi method ACCEPTS(Str:D: Any:D \other) {
        nqp::p6bool(nqp::iseq_s(nqp::unbox_s(other.Str),$!value));
    }

    method chomp(Str:D:) {
        my str $str   = nqp::unbox_s(self);
        my int $chars = nqp::chars($str);

        $chars && nqp::iscclass(nqp::const::CCLASS_NEWLINE,$str,$chars-1)
            ?? nqp::p6box_s(nqp::substr($str,0,$chars-1))
            !! self
    }

    method chop(Str:D: Int() $chopping = 1) {
        my str $str   = nqp::unbox_s(self);
        my int $chars = nqp::chars($str) - $chopping;
        $chars > 0 ?? nqp::p6box_s(nqp::substr($str,0,$chars)) !! '';
    }

    # chars used to handle ranges for pred/succ
    my str $RANGECHAR =
        "01234567890"                                # arabic digits
        ~ "ABCDEFGHIJKLMNOPQRSTUVWXYZA"              # latin uppercase
        ~ "abcdefghijklmnopqrstuvwxyza"              # latin lowercase
        ~ "\x[391,392,393,394,395,396,397,398,399,39A,39B,39C,39D,39E,39F,3A0,3A1,3A3,3A4,3A5,3A6,3A7,3A8,3A9,391]" # greek uppercase
        ~ "\x[3B1,3B2,3B3,3B4,3B5,3B6,3B7,3B8,3B9,3BA,3BB,3BC,3BD,3BE,3BF,3C0,3C1,3C3,3C4,3C5,3C6,3C7,3C8,3C9,3B1]" # greek lowercase
        ~ "\x[5D0,5D1,5D2,5D3,5D4,5D5,5D6,5D7,5D8,5D9,5DA,5DB,5DC,5DD,5DE,5DF,5E0,5E1,5E2,5E3,5E4,5E5,5E6,5E7,5E8,5E9,5EA,5D0]" # hebrew
        ~ "\x[410,411,412,413,414,415,416,417,418,419,41A,41B,41C,41D,41E,41F,420,421,422,423,424,425,426,427,428,429,42A,42B,42C,42D,42E,42F,410]" # cyrillic uppercase
        ~ "\x[430,431,432,433,434,435,436,437,438,439,43A,43B,43C,43D,43E,43F,440,441,442,443,444,445,446,447,448,449,44A,44B,44C,44D,44E,44F,430]" # cyrillic lowercase
        ~ "\x[660,661,662,663,664,665,666,667,668,669,660]" # arabic-indic digits
        ~ "\x[966,967,968,969,96A,96B,96C,96D,96E,96F,966]" # devanagari digits
        ~ "\x[9E6,9E7,9E8,9E9,9EA,9EB,9EC,9ED,9EE,9EF,9E6]" # bengali digits
        ~ "\x[A66,A67,A68,A69,A6A,A6B,A6C,A6D,A6E,A6F,A66]" # gurmukhi digits
        ~ "\x[AE6,AE7,AE8,AE9,AEA,AEB,AEC,AED,AEE,AEF,AE6]" # gujarati digits
        ~ "\x[B66,B67,B68,B69,B6A,B6B,B6C,B6D,B6E,B6F,B66]" # oriya digits
        ~ "\x[FF10,FF11,FF12,FF13,FF14,FF15,FF16,FF17,FF18,FF19,FF10]" # fullwidth digits
        ~ "\x[2070,2071,00B2,00B3,2074,2075,2076,2077,2078,2079]" # superscripts
        ~ "\x[2080,2081,2082,2083,2084,2085,2086,2087,2088,2089]" # subscripts
        ~ "\x[2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,216a,216b,2160]" # clock roman uc
        ~ "\x[2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,217a,217b,2170]" # clock roman lc
        ~ "\x[2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,246A,246B,246C,246D,246E,246F,2470,2471,2472,2473,2460]" # circled digits 1..20
        ~ "\x[2474,2475,2476,2477,2478,2479,247A,247B,247C,247D,247E,247F,2480,2481,2482,2483,2484,2485,2486,2487,2474]" # parenthesized digits 1..20
        ~ "\x[249C,249D,249E,249F,24A0,24A1,24A2,24A3,24A4,24A5,24A6,24A7,24A8,24A9,24AA,24AB,24AC,24AD,24AE,24AF,24B0,24B1,24B2,24B3,24B4,24B5,249C]" # parenthesized latin lc
        ~ "\x[2581,2582,2583,2584,2585,2586,2587,2588]" # lower blocks
        ~ "\x[2680,2681,2682,2683,2684,2685,2680]" # die faces
        ~ "\x[2776,2777,2778,2779,277A,277B,277C,277D,277E,277F,2776]" # dingbat negative circled 1..10
        ~ "\x[1F37A,1F37B]"  # beer mugs
        ~ "\x[1F42A,1F42B]"; # camels

    # digit to extend the string with if carried past first rangechar position
    my $carrydigit := nqp::hash(
       '0',      '1',      # arabic
       "\x0660", "\x0661", # arabic-indic
       "\x0966", "\x0967", # devanagari
       "\x09E6", "\x09E7", # bengali
       "\x0A66", "\x0A67", # gurmukhi
       "\x0AE6", "\x0AE7", # gujarati
       "\x0B66", "\x0B67", # oriya
       "\xFF10", "\xFF11", # fullwidth XXX: should be treated as digit?
       "\x2070", "\x2071", # superscripts XXX: should be treated as digit?
       "\x2080", "\x2081", # subscripts XXX: should be treated as digit?
       "\x1F37A","\x1F37B",# beer mugs
       "\x1F42A","\x1F42B",# camels
    );
    # calculate the beginning and ending positions of <!after '.'><rangechar+>
    my sub RANGEPOS(str $str) {
        my int $pos = nqp::chars($str);
        while $pos > 0 {
            $pos = $pos - 1;
            my str $ch = nqp::substr($str, $pos, 1);
            if nqp::isge_i(nqp::index($RANGECHAR, $ch, 0), 0) {
                my int $end = $pos;
                while $pos > 0 {
                    $pos = $pos - 1;
                    $ch = nqp::substr($str, $pos, 1);
                    last if nqp::iseq_s($ch, '.');
                    return ($pos+1, $end)
                        unless nqp::isge_i(nqp::index($RANGECHAR, $ch, 0), 0);
                }
                return ($pos, $end) unless nqp::iseq_s($ch, '.');
            }
        }
        (0, -1);
    }

    method pred(Str:D:) {
        my str $str = self;
        my Int ($Ir0, $Ir1) = RANGEPOS($str);
        my int $r0 = $Ir0;
        my int $r1 = $Ir1;
        while $r1 >= $r0 {
            my str $ch0  = nqp::substr($str, $r1, 1);
            my int $ipos = nqp::index($RANGECHAR, $ch0);
            $ipos = $RANGECHAR.index($ch0, $ipos+1) // $ipos;
            my str $ch1 = nqp::substr($RANGECHAR, $ipos-1, 1);
            $str = nqp::replace($str, $r1, 1, $ch1);
            # return if no carry
            return $str if $ch0 gt $ch1;
            # carry to previous position
            $r1 = $r1 - 1;
        }
        # cannot carry beyond first rangechar position
        fail('Decrement out of range');
    }

    method succ(Str:D:) {
        my str $str = self;
        my Int ($Ir0, $Ir1) = RANGEPOS($str);
        my int $r0 = $Ir0;
        my int $r1 = $Ir1;
        while $r1 >= $r0 {
            my str $ch0  = nqp::substr($str, $r1, 1);
            my int $ipos = nqp::index($RANGECHAR, $ch0);
            my str $ch1  = nqp::substr($RANGECHAR, $ipos+1, 1);
            $str = nqp::replace($str, $r1, 1, $ch1);
            return $str if $ch1 gt $ch0;
            # carry to previous position
            $r1 = $r1 - 1;
            # extend string if carried past first rangechar position
            $str = nqp::replace($str, $r0, 0,
                       nqp::existskey($carrydigit, $ch1)
                           ?? nqp::atkey($carrydigit, $ch1)
                           !! $ch1)
                if $r1 < $r0;
        }
        $str;
    }

    multi method Numeric(Str:D: :$strict = True) {
        # Handle special empty string
        return 0 if self.trim eq "";

        val(self, :val-or-fail);
    }

    my %esc = (
        '$' => '\$',  '@' => '\@',  '%' => '\%',  '&' => '\&',  '{' => '\{',
        "\b" => '\b', "\x0A" => '\n', "\r" => '\r', "\t" => '\t', '"' => '\"',
        '\\' => '\\\\' );

    multi method gist(Str:D:) { self }
    multi method perl(Str:D:) {
        sub char-to-escapes($ch) {
#?if moar
            $ch.NFC.list.map(*.fmt('\x[%x]')).join
#?endif
#?if !moar
            $ch.ord.fmt('\x[%x]')
#?endif
        }

        # Under NFG-supporting implementations, must be sure that any leading
        # combiners are escaped, otherwise they will be combined onto the "
        # under concatenation closure, which ruins round-tripping. Also handle
        # the \r\n grapheme correctly.
        my $result = '"';
        my $to-encode = self;

        for ^$to-encode.chars -> $i {
            my $ch = nqp::substr($to-encode, $i, 1);
            my int $ord = nqp::ord($ch);
#?if moar
            if $ord >= 256 && +uniprop($ord, 'Canonical_Combining_Class') {
                $result ~= char-to-escapes($ch);
                next;
            }
            elsif $ch eq "\r\n" {
                $result ~= '\r\n';
                next;
            }
#?endif
            $result ~= %esc{$ch}
                       //  (nqp::iscclass( nqp::const::CCLASS_PRINTING,
                                                  nqp::unbox_s($ch), 0)
                           ?? $ch
                           !! char-to-escapes($ch)
                           );
        }
        $result ~ '"'
    }

    role ProcessStr does Iterator {
        has str $!str;
        has int $!chars;
        submethod BUILD(\string) {
            $!str   = nqp::unbox_s(string);
            $!chars = nqp::chars($!str);
            self
        }
        method new(\string) { nqp::create(self).BUILD(string) }
    }

    multi method comb(Str:D:) {
        Seq.new(class :: does ProcessStr {
            has int $!pos;
            method pull-one() {
                $!pos < $!chars
                  ?? nqp::p6box_s(nqp::substr($!str, $!pos++, 1))
                  !! IterationEnd
            }
            method count-only() { nqp::p6box_i($!pos = $!chars) }
        }.new(self));
    }
    multi method comb(Str:D: Int:D $size, $limit = *) {
        my int $inf = nqp::istype($limit,Whatever) || $limit == Inf;
        return self.comb if $size <= 1 && $inf;

        Seq.new(class :: does Iterator {
            has str $!str;
            has int $!chars;
            has int $!size;
            has int $!pos;
            has int $!max;
            has int $!todo;
            submethod BUILD(\string,\size,\limit,\inf) {
                $!str   = nqp::unbox_s(string);
                $!chars = nqp::chars($!str);
                $!size  = 1 max size;
                $!pos   = -size;
                $!max   = 1 + floor( ( $!chars - 1 ) / $!size );
                $!todo  = (inf ?? $!max !! (0 max limit)) + 1;
                self
            }
            method new(\s,\z,\l,\i) { nqp::create(self).BUILD(s,z,l,i) }
            method pull-one() {
                ($!todo = $!todo - 1) && ($!pos = $!pos + $!size) < $!chars
                  ?? nqp::p6box_s(nqp::substr($!str, $!pos, $!size))
                  !! IterationEnd
            }
            method push-all($target) {
                my int $todo  = $!todo;
                my int $pos   = $!pos;
                my int $size  = $!size;
                my int $chars = $!chars;
                $target.push(nqp::p6box_s(nqp::substr($!str, $pos, $size)))
                  while ($todo = $todo - 1 ) && ($pos = $pos + $size) < $chars;
                $!pos = $!chars;
                IterationEnd
            }
            method count-only() { $!pos = $!chars; $!max }
        }.new(self,$size,$limit,$inf))
    }
    multi method comb(Str:D: Str $pat) {
        Seq.new(class :: does Iterator {
            has str $!str;
            has str $!pat;
            has int $!pos;
            submethod BUILD(\string, \pat) {
                $!str = nqp::unbox_s(string);
                $!pat = nqp::unbox_s(pat);
                self
            }
            method new(\string, \pat) { nqp::create(self).BUILD(string, pat) }
            method pull-one() {
                my int $found = nqp::index($!str, $!pat, $!pos);
                if $found < 0 {
                    IterationEnd
                }
                else {
                    $!pos = $found + 1;
                    nqp::p6box_s($!pat)
                }
            }
            method count-only() {
                my int $seen;
                my int $found;
                until ($found = nqp::index($!str, $!pat, $!pos)) < 0 {
                    $seen = $seen + 1;
                    $!pos = $found + 1;
                }
                nqp::p6box_i($seen)
            }
        }.new(self, $pat));
    }
    multi method comb(Str:D: Str $pat, $limit) {
        return self.comb($pat)
          if nqp::istype($limit,Whatever) || $limit == Inf;

        Seq.new(class :: does Iterator {
            has str $!str;
            has str $!pat;
            has int $!pos;
            has int $!todo;
            submethod BUILD(\string, \pat, \limit) {
                $!str  = nqp::unbox_s(string);
                $!pat  = nqp::unbox_s(pat);
                $!todo = nqp::unbox_i(limit.Int);
                self
            }
            method new(\string, \pat, \limit) {
                nqp::create(self).BUILD(string, pat, limit)
            }
            method pull-one() {
                my int $found = nqp::index($!str, $!pat, $!pos);
                if $found < 0 || $!todo == 0 {
                    IterationEnd
                }
                else {
                    $!pos  = $found + 1;
                    $!todo = $!todo - 1;
                    nqp::p6box_s($!pat)
                }
            }
            method count-only() {
                my int $seen;
                my int $found;
                until ($found = nqp::index($!str, $!pat, $!pos)) < 0
                  || $!todo == 0 {
                    $seen  = $seen + 1;
                    $!pos  = $found + 1;
                    $!todo = $!todo - 1;
                }
                nqp::p6box_i($seen)
            }
        }.new(self, $pat, $limit));
    }
    multi method comb(Str:D: Regex $pat, $limit = Inf, :$match) {
        my $x;
        $x = (1..$limit) unless nqp::istype($limit, Whatever) || $limit == Inf;
        $match
            ?? self.match(:g, :$x, $pat)
            !! self.match(:g, :$x, $pat).map: { .Str }
    }

    # A temporary routine for differential testing of .match overhead.
    # This can only be used for a single non-multi match.
    method simplematch($pat) {
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my %opts;
        %opts<c> = 0;
        my $patrx := nqp::istype($pat,Code) ?? $pat !! / "$pat": /;
        my $cur := $patrx(Cursor.'!cursor_init'(self, |%opts));

        my \result = $cur.pos >= 0
            ?? $cur.MATCH_SAVE
            !! Nil;
        $caller_dollar_slash = result;
        result;
    }

    method match($pat,
                 :continue(:$c), :pos(:$p),
                 :global(:$g), :overlap(:$ov), :exhaustive(:$ex),
                 # :st(:nd(:rd(:th($nth)))) is cute, but slow
                 :st(:$nd), :rd(:$th), :$nth = $nd // $th, :$x) {
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my %opts;
        if $p.defined { %opts<p> = $p }
        else { %opts<c> = $c // 0; }
        my $patrx := nqp::istype($pat,Code) ?? $pat !! / "$pat": /;
        my $cur := $patrx(Cursor.'!cursor_init'(self, |%opts));

        %opts<ov> = $ov if $ov;
        %opts<ex> = $ex if $ex;

        my $matches := gather {
            while $cur.pos >= 0 {
                take $cur.MATCH_SAVE;
                $cur := $cur.'!cursor_more'(|%opts);
            }
        }

        my $multi = $g || $ov || $ex;

        my @matches;

        if $nth.defined or $x.defined {
            my $clip;
            my $idxs;

            # Translate :nth lists to monotonic 0-based indices
            my sub nthidx($n is copy) {

                if nqp::istype($n, Callable) or nqp::istype($n, Whatever) {
                    # WhateverCode forces us to remember early
                    once @matches := $matches.list;
                    once $matches := Nil;
                    # WhateverCode is 1-based
                    $n = nqp::istype($n, Whatever) ?? +@matches !! $n(+@matches);
                }

                state $max = -Inf;

                if ($n > $max or once $n == -Inf) {
                    $max = $n;
                    # After first positive, <= 0 are "ignored" per spec
                    once die "Attempt to retrieve before :1st match -- :nth($n)"
                       if $max < 1;
                    $n - 1;
                }
                else {
                    Slip.new();
                }
            }
            if $nth.defined and not $x.defined {
                $multi = Positional.ACCEPTS($nth);
                $idxs := $nth.map(&nthidx).Array;
                $clip := $idxs.elems..Inf;
            }
            if $x.defined {
                $multi = True;
                if nqp::istype($x, Int) {
                    $clip := $x..$x;
                }
                elsif nqp::istype($x, Range) {
                    my $mx = $x.max.floor;
                    $mx = $mx - 1 unless $mx ~~ $x;
                    my $mn = $x.min.ceiling;
                    $mn = $mn + 1 unless $mn ~~ $x;
                    $clip := $mn..$mx;
                }
                elsif nqp::istype($x, Whatever) {
                    $clip := 0..Inf;
                }
                else {
                    X::Str::Match::x.new(:got($x)).fail;
                }
                $clip := 0..($clip.max) if $clip.min < 0;
                return Slip.new() if $clip.max < 1 or $clip.max < $clip.min;

                if $nth.defined {
                    $idxs := $nth.map(&nthidx).Array;
                    return Slip.new()
                        if $clip.min and not $idxs.EXISTS-POS($clip.min - 1);
                }
                else {
                    $idxs := (0..Inf).Array;
                }
            }

            unless $matches.defined {
                # Whatever, we have an extra layer of memoization.
                $matches := @matches.values;
                @matches := ();
            }

            # Just "list $matches.grep", once we have True.last
            @matches := (gather do for $matches -> $m {
                state $i = 0;
                state $took = 0;
                state $n = $idxs.EXISTS-POS(0) ?? $idxs.shift !! Nil;
                last unless $n.defined;

                if $i == $n {
                    $n = $idxs.EXISTS-POS(0) ?? $idxs.shift !! Nil;
                    take $m;
                    $took++;
                    last if $took >= $clip.max;
                };
                $i++;

                last unless $n.defined;
            }).cache;
            @matches := () unless not $clip.min or @matches.EXISTS-POS($clip.min - 1);
        }
        else {
            @matches := $matches.list;
        }
        if $multi {
            try $caller_dollar_slash = @matches;
            @matches
        }
        else {
            try $caller_dollar_slash = (@matches[0] // $cur.MATCH_SAVE);
            (@matches[0] // $cur.MATCH_SAVE)
        }
    }

    multi method subst-mutate($self is rw: $matcher, $replacement,
                       :ii(:$samecase), :ss(:$samespace), :mm(:$samemark), *%options) {
        my $global = %options<g> || %options<global>;
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my $SET_DOLLAR_SLASH     = nqp::istype($matcher, Regex);
        my $word_by_word = so $samespace || %options<s> || %options<sigspace>;

        try $caller_dollar_slash = $/ if $SET_DOLLAR_SLASH;
        my @matches              = self.match($matcher, |%options);

        if !@matches || (@matches == 1 && !@matches[0]) {
            Nil;
        }
        else {
            $self = $self!APPLY-MATCHES(
              @matches,
              $replacement,
              $caller_dollar_slash,
              $SET_DOLLAR_SLASH,
              $word_by_word,
              $samespace,
              $samecase,
              $samemark,
            );
            if $global {
                my \result := nqp::create(List);
                nqp::bindattr(result, List, '$!reified', nqp::getattr(@matches, List, '$!reified'));
                result
            }
            else {
                @matches[0]
            }
        }
    }

    multi method subst(Str:D: Str \from, Str \to, :$global!, *%adverbs) {
        if $global && !%adverbs {
            Rakudo::Internals.TRANSPOSE(self,from,to);
        }
        else {
            $/ := nqp::getlexdyn('$/');
            self.subst(from, to, |%adverbs);
        }
    }
    multi method subst(Str:D: $matcher, $replacement,
                       :ii(:$samecase), :ss(:$samespace), :mm(:$samemark),
                       *%options) {

        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my $SET_DOLLAR_SLASH     = nqp::istype($matcher, Regex);
        my $word_by_word = so $samespace || %options<s> || %options<sigspace>;

        # nothing to do
        try $caller_dollar_slash = $/ if $SET_DOLLAR_SLASH;
        my @matches = self.match($matcher, |%options);

        !@matches || (@matches == 1 && !@matches[0])
          ?? self
          !! self!APPLY-MATCHES(
               @matches,
               $replacement,
               $caller_dollar_slash,<commit_msg>Fix some issues wrt :global/:g and extras<commit_after>my class Cursor {... }
my class Range  {... }
my class Match  {... }
my class X::Str::Numeric  { ... }
my class X::Str::Match::x { ... }
my class X::Str::Trans::IllegalKey { ... }
my class X::Str::Trans::InvalidArg { ... }
my class X::Numeric::Confused { ... }

my constant $?TABSTOP = 8;

my class Str does Stringy { # declared in BOOTSTRAP
    # class Str is Cool {
    #     has str $!value is box_target;

    multi method WHY('Life, the Universe, and Everything':) { 42 }

    multi method WHICH(Str:D:) {
        nqp::box_s(
            nqp::concat(
                nqp::concat(nqp::unbox_s(self.^name), '|'),
                $!value
            ),
            ObjAt
        );
    }
    submethod BUILD(Str() :$value = '') {
        nqp::bindattr_s(self, Str, '$!value', nqp::unbox_s($value))
    }

    multi method Bool(Str:D:) {
        nqp::p6bool(nqp::chars($!value));
    }

    multi method Str(Str:D:)     { self }
    multi method Stringy(Str:D:) { self }
    multi method DUMP(Str:D:) { self.perl }

    method Int(Str:D:) { self.Numeric.Int; }
    method Num(Str:D:) { self.Numeric.Num; }

    multi method ACCEPTS(Str:D: Str:D \other) {
        nqp::p6bool(nqp::iseq_s(nqp::unbox_s(other),$!value));
    }
    multi method ACCEPTS(Str:D: Any:U \other) {
        False;
    }
    multi method ACCEPTS(Str:D: Any:D \other) {
        nqp::p6bool(nqp::iseq_s(nqp::unbox_s(other.Str),$!value));
    }

    method chomp(Str:D:) {
        my str $str   = nqp::unbox_s(self);
        my int $chars = nqp::chars($str);

        $chars && nqp::iscclass(nqp::const::CCLASS_NEWLINE,$str,$chars-1)
            ?? nqp::p6box_s(nqp::substr($str,0,$chars-1))
            !! self
    }

    method chop(Str:D: Int() $chopping = 1) {
        my str $str   = nqp::unbox_s(self);
        my int $chars = nqp::chars($str) - $chopping;
        $chars > 0 ?? nqp::p6box_s(nqp::substr($str,0,$chars)) !! '';
    }

    # chars used to handle ranges for pred/succ
    my str $RANGECHAR =
        "01234567890"                                # arabic digits
        ~ "ABCDEFGHIJKLMNOPQRSTUVWXYZA"              # latin uppercase
        ~ "abcdefghijklmnopqrstuvwxyza"              # latin lowercase
        ~ "\x[391,392,393,394,395,396,397,398,399,39A,39B,39C,39D,39E,39F,3A0,3A1,3A3,3A4,3A5,3A6,3A7,3A8,3A9,391]" # greek uppercase
        ~ "\x[3B1,3B2,3B3,3B4,3B5,3B6,3B7,3B8,3B9,3BA,3BB,3BC,3BD,3BE,3BF,3C0,3C1,3C3,3C4,3C5,3C6,3C7,3C8,3C9,3B1]" # greek lowercase
        ~ "\x[5D0,5D1,5D2,5D3,5D4,5D5,5D6,5D7,5D8,5D9,5DA,5DB,5DC,5DD,5DE,5DF,5E0,5E1,5E2,5E3,5E4,5E5,5E6,5E7,5E8,5E9,5EA,5D0]" # hebrew
        ~ "\x[410,411,412,413,414,415,416,417,418,419,41A,41B,41C,41D,41E,41F,420,421,422,423,424,425,426,427,428,429,42A,42B,42C,42D,42E,42F,410]" # cyrillic uppercase
        ~ "\x[430,431,432,433,434,435,436,437,438,439,43A,43B,43C,43D,43E,43F,440,441,442,443,444,445,446,447,448,449,44A,44B,44C,44D,44E,44F,430]" # cyrillic lowercase
        ~ "\x[660,661,662,663,664,665,666,667,668,669,660]" # arabic-indic digits
        ~ "\x[966,967,968,969,96A,96B,96C,96D,96E,96F,966]" # devanagari digits
        ~ "\x[9E6,9E7,9E8,9E9,9EA,9EB,9EC,9ED,9EE,9EF,9E6]" # bengali digits
        ~ "\x[A66,A67,A68,A69,A6A,A6B,A6C,A6D,A6E,A6F,A66]" # gurmukhi digits
        ~ "\x[AE6,AE7,AE8,AE9,AEA,AEB,AEC,AED,AEE,AEF,AE6]" # gujarati digits
        ~ "\x[B66,B67,B68,B69,B6A,B6B,B6C,B6D,B6E,B6F,B66]" # oriya digits
        ~ "\x[FF10,FF11,FF12,FF13,FF14,FF15,FF16,FF17,FF18,FF19,FF10]" # fullwidth digits
        ~ "\x[2070,2071,00B2,00B3,2074,2075,2076,2077,2078,2079]" # superscripts
        ~ "\x[2080,2081,2082,2083,2084,2085,2086,2087,2088,2089]" # subscripts
        ~ "\x[2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,216a,216b,2160]" # clock roman uc
        ~ "\x[2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,217a,217b,2170]" # clock roman lc
        ~ "\x[2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,246A,246B,246C,246D,246E,246F,2470,2471,2472,2473,2460]" # circled digits 1..20
        ~ "\x[2474,2475,2476,2477,2478,2479,247A,247B,247C,247D,247E,247F,2480,2481,2482,2483,2484,2485,2486,2487,2474]" # parenthesized digits 1..20
        ~ "\x[249C,249D,249E,249F,24A0,24A1,24A2,24A3,24A4,24A5,24A6,24A7,24A8,24A9,24AA,24AB,24AC,24AD,24AE,24AF,24B0,24B1,24B2,24B3,24B4,24B5,249C]" # parenthesized latin lc
        ~ "\x[2581,2582,2583,2584,2585,2586,2587,2588]" # lower blocks
        ~ "\x[2680,2681,2682,2683,2684,2685,2680]" # die faces
        ~ "\x[2776,2777,2778,2779,277A,277B,277C,277D,277E,277F,2776]" # dingbat negative circled 1..10
        ~ "\x[1F37A,1F37B]"  # beer mugs
        ~ "\x[1F42A,1F42B]"; # camels

    # digit to extend the string with if carried past first rangechar position
    my $carrydigit := nqp::hash(
       '0',      '1',      # arabic
       "\x0660", "\x0661", # arabic-indic
       "\x0966", "\x0967", # devanagari
       "\x09E6", "\x09E7", # bengali
       "\x0A66", "\x0A67", # gurmukhi
       "\x0AE6", "\x0AE7", # gujarati
       "\x0B66", "\x0B67", # oriya
       "\xFF10", "\xFF11", # fullwidth XXX: should be treated as digit?
       "\x2070", "\x2071", # superscripts XXX: should be treated as digit?
       "\x2080", "\x2081", # subscripts XXX: should be treated as digit?
       "\x1F37A","\x1F37B",# beer mugs
       "\x1F42A","\x1F42B",# camels
    );
    # calculate the beginning and ending positions of <!after '.'><rangechar+>
    my sub RANGEPOS(str $str) {
        my int $pos = nqp::chars($str);
        while $pos > 0 {
            $pos = $pos - 1;
            my str $ch = nqp::substr($str, $pos, 1);
            if nqp::isge_i(nqp::index($RANGECHAR, $ch, 0), 0) {
                my int $end = $pos;
                while $pos > 0 {
                    $pos = $pos - 1;
                    $ch = nqp::substr($str, $pos, 1);
                    last if nqp::iseq_s($ch, '.');
                    return ($pos+1, $end)
                        unless nqp::isge_i(nqp::index($RANGECHAR, $ch, 0), 0);
                }
                return ($pos, $end) unless nqp::iseq_s($ch, '.');
            }
        }
        (0, -1);
    }

    method pred(Str:D:) {
        my str $str = self;
        my Int ($Ir0, $Ir1) = RANGEPOS($str);
        my int $r0 = $Ir0;
        my int $r1 = $Ir1;
        while $r1 >= $r0 {
            my str $ch0  = nqp::substr($str, $r1, 1);
            my int $ipos = nqp::index($RANGECHAR, $ch0);
            $ipos = $RANGECHAR.index($ch0, $ipos+1) // $ipos;
            my str $ch1 = nqp::substr($RANGECHAR, $ipos-1, 1);
            $str = nqp::replace($str, $r1, 1, $ch1);
            # return if no carry
            return $str if $ch0 gt $ch1;
            # carry to previous position
            $r1 = $r1 - 1;
        }
        # cannot carry beyond first rangechar position
        fail('Decrement out of range');
    }

    method succ(Str:D:) {
        my str $str = self;
        my Int ($Ir0, $Ir1) = RANGEPOS($str);
        my int $r0 = $Ir0;
        my int $r1 = $Ir1;
        while $r1 >= $r0 {
            my str $ch0  = nqp::substr($str, $r1, 1);
            my int $ipos = nqp::index($RANGECHAR, $ch0);
            my str $ch1  = nqp::substr($RANGECHAR, $ipos+1, 1);
            $str = nqp::replace($str, $r1, 1, $ch1);
            return $str if $ch1 gt $ch0;
            # carry to previous position
            $r1 = $r1 - 1;
            # extend string if carried past first rangechar position
            $str = nqp::replace($str, $r0, 0,
                       nqp::existskey($carrydigit, $ch1)
                           ?? nqp::atkey($carrydigit, $ch1)
                           !! $ch1)
                if $r1 < $r0;
        }
        $str;
    }

    multi method Numeric(Str:D: :$strict = True) {
        # Handle special empty string
        return 0 if self.trim eq "";

        val(self, :val-or-fail);
    }

    my %esc = (
        '$' => '\$',  '@' => '\@',  '%' => '\%',  '&' => '\&',  '{' => '\{',
        "\b" => '\b', "\x0A" => '\n', "\r" => '\r', "\t" => '\t', '"' => '\"',
        '\\' => '\\\\' );

    multi method gist(Str:D:) { self }
    multi method perl(Str:D:) {
        sub char-to-escapes($ch) {
#?if moar
            $ch.NFC.list.map(*.fmt('\x[%x]')).join
#?endif
#?if !moar
            $ch.ord.fmt('\x[%x]')
#?endif
        }

        # Under NFG-supporting implementations, must be sure that any leading
        # combiners are escaped, otherwise they will be combined onto the "
        # under concatenation closure, which ruins round-tripping. Also handle
        # the \r\n grapheme correctly.
        my $result = '"';
        my $to-encode = self;

        for ^$to-encode.chars -> $i {
            my $ch = nqp::substr($to-encode, $i, 1);
            my int $ord = nqp::ord($ch);
#?if moar
            if $ord >= 256 && +uniprop($ord, 'Canonical_Combining_Class') {
                $result ~= char-to-escapes($ch);
                next;
            }
            elsif $ch eq "\r\n" {
                $result ~= '\r\n';
                next;
            }
#?endif
            $result ~= %esc{$ch}
                       //  (nqp::iscclass( nqp::const::CCLASS_PRINTING,
                                                  nqp::unbox_s($ch), 0)
                           ?? $ch
                           !! char-to-escapes($ch)
                           );
        }
        $result ~ '"'
    }

    role ProcessStr does Iterator {
        has str $!str;
        has int $!chars;
        submethod BUILD(\string) {
            $!str   = nqp::unbox_s(string);
            $!chars = nqp::chars($!str);
            self
        }
        method new(\string) { nqp::create(self).BUILD(string) }
    }

    multi method comb(Str:D:) {
        Seq.new(class :: does ProcessStr {
            has int $!pos;
            method pull-one() {
                $!pos < $!chars
                  ?? nqp::p6box_s(nqp::substr($!str, $!pos++, 1))
                  !! IterationEnd
            }
            method count-only() { nqp::p6box_i($!pos = $!chars) }
        }.new(self));
    }
    multi method comb(Str:D: Int:D $size, $limit = *) {
        my int $inf = nqp::istype($limit,Whatever) || $limit == Inf;
        return self.comb if $size <= 1 && $inf;

        Seq.new(class :: does Iterator {
            has str $!str;
            has int $!chars;
            has int $!size;
            has int $!pos;
            has int $!max;
            has int $!todo;
            submethod BUILD(\string,\size,\limit,\inf) {
                $!str   = nqp::unbox_s(string);
                $!chars = nqp::chars($!str);
                $!size  = 1 max size;
                $!pos   = -size;
                $!max   = 1 + floor( ( $!chars - 1 ) / $!size );
                $!todo  = (inf ?? $!max !! (0 max limit)) + 1;
                self
            }
            method new(\s,\z,\l,\i) { nqp::create(self).BUILD(s,z,l,i) }
            method pull-one() {
                ($!todo = $!todo - 1) && ($!pos = $!pos + $!size) < $!chars
                  ?? nqp::p6box_s(nqp::substr($!str, $!pos, $!size))
                  !! IterationEnd
            }
            method push-all($target) {
                my int $todo  = $!todo;
                my int $pos   = $!pos;
                my int $size  = $!size;
                my int $chars = $!chars;
                $target.push(nqp::p6box_s(nqp::substr($!str, $pos, $size)))
                  while ($todo = $todo - 1 ) && ($pos = $pos + $size) < $chars;
                $!pos = $!chars;
                IterationEnd
            }
            method count-only() { $!pos = $!chars; $!max }
        }.new(self,$size,$limit,$inf))
    }
    multi method comb(Str:D: Str $pat) {
        Seq.new(class :: does Iterator {
            has str $!str;
            has str $!pat;
            has int $!pos;
            submethod BUILD(\string, \pat) {
                $!str = nqp::unbox_s(string);
                $!pat = nqp::unbox_s(pat);
                self
            }
            method new(\string, \pat) { nqp::create(self).BUILD(string, pat) }
            method pull-one() {
                my int $found = nqp::index($!str, $!pat, $!pos);
                if $found < 0 {
                    IterationEnd
                }
                else {
                    $!pos = $found + 1;
                    nqp::p6box_s($!pat)
                }
            }
            method count-only() {
                my int $seen;
                my int $found;
                until ($found = nqp::index($!str, $!pat, $!pos)) < 0 {
                    $seen = $seen + 1;
                    $!pos = $found + 1;
                }
                nqp::p6box_i($seen)
            }
        }.new(self, $pat));
    }
    multi method comb(Str:D: Str $pat, $limit) {
        return self.comb($pat)
          if nqp::istype($limit,Whatever) || $limit == Inf;

        Seq.new(class :: does Iterator {
            has str $!str;
            has str $!pat;
            has int $!pos;
            has int $!todo;
            submethod BUILD(\string, \pat, \limit) {
                $!str  = nqp::unbox_s(string);
                $!pat  = nqp::unbox_s(pat);
                $!todo = nqp::unbox_i(limit.Int);
                self
            }
            method new(\string, \pat, \limit) {
                nqp::create(self).BUILD(string, pat, limit)
            }
            method pull-one() {
                my int $found = nqp::index($!str, $!pat, $!pos);
                if $found < 0 || $!todo == 0 {
                    IterationEnd
                }
                else {
                    $!pos  = $found + 1;
                    $!todo = $!todo - 1;
                    nqp::p6box_s($!pat)
                }
            }
            method count-only() {
                my int $seen;
                my int $found;
                until ($found = nqp::index($!str, $!pat, $!pos)) < 0
                  || $!todo == 0 {
                    $seen  = $seen + 1;
                    $!pos  = $found + 1;
                    $!todo = $!todo - 1;
                }
                nqp::p6box_i($seen)
            }
        }.new(self, $pat, $limit));
    }
    multi method comb(Str:D: Regex $pat, $limit = Inf, :$match) {
        my $x;
        $x = (1..$limit) unless nqp::istype($limit, Whatever) || $limit == Inf;
        $match
            ?? self.match(:g, :$x, $pat)
            !! self.match(:g, :$x, $pat).map: { .Str }
    }

    # A temporary routine for differential testing of .match overhead.
    # This can only be used for a single non-multi match.
    method simplematch($pat) {
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my %opts;
        %opts<c> = 0;
        my $patrx := nqp::istype($pat,Code) ?? $pat !! / "$pat": /;
        my $cur := $patrx(Cursor.'!cursor_init'(self, |%opts));

        my \result = $cur.pos >= 0
            ?? $cur.MATCH_SAVE
            !! Nil;
        $caller_dollar_slash = result;
        result;
    }

    method match($pat,
                 :continue(:$c), :pos(:$p),
                 :global(:$g), :overlap(:$ov), :exhaustive(:$ex),
                 # :st(:nd(:rd(:th($nth)))) is cute, but slow
                 :st(:$nd), :rd(:$th), :$nth = $nd // $th, :$x) {
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my %opts;
        if $p.defined { %opts<p> = $p }
        else { %opts<c> = $c // 0; }
        my $patrx := nqp::istype($pat,Code) ?? $pat !! / "$pat": /;
        my $cur := $patrx(Cursor.'!cursor_init'(self, |%opts));

        %opts<ov> = $ov if $ov;
        %opts<ex> = $ex if $ex;

        my $matches := gather {
            while $cur.pos >= 0 {
                take $cur.MATCH_SAVE;
                $cur := $cur.'!cursor_more'(|%opts);
            }
        }

        my $multi = $g || $ov || $ex;

        my @matches;

        if $nth.defined or $x.defined {
            my $clip;
            my $idxs;

            # Translate :nth lists to monotonic 0-based indices
            my sub nthidx($n is copy) {

                if nqp::istype($n, Callable) or nqp::istype($n, Whatever) {
                    # WhateverCode forces us to remember early
                    once @matches := $matches.list;
                    once $matches := Nil;
                    # WhateverCode is 1-based
                    $n = nqp::istype($n, Whatever) ?? +@matches !! $n(+@matches);
                }

                state $max = -Inf;

                if ($n > $max or once $n == -Inf) {
                    $max = $n;
                    # After first positive, <= 0 are "ignored" per spec
                    once die "Attempt to retrieve before :1st match -- :nth($n)"
                       if $max < 1;
                    $n - 1;
                }
                else {
                    Slip.new();
                }
            }
            if $nth.defined and not $x.defined {
                $multi = Positional.ACCEPTS($nth);
                $idxs := $nth.map(&nthidx).Array;
                $clip := $idxs.elems..Inf;
            }
            if $x.defined {
                $multi = True;
                if nqp::istype($x, Int) {
                    $clip := $x..$x;
                }
                elsif nqp::istype($x, Range) {
                    my $mx = $x.max.floor;
                    $mx = $mx - 1 unless $mx ~~ $x;
                    my $mn = $x.min.ceiling;
                    $mn = $mn + 1 unless $mn ~~ $x;
                    $clip := $mn..$mx;
                }
                elsif nqp::istype($x, Whatever) {
                    $clip := 0..Inf;
                }
                else {
                    X::Str::Match::x.new(:got($x)).fail;
                }
                $clip := 0..($clip.max) if $clip.min < 0;
                return Slip.new() if $clip.max < 1 or $clip.max < $clip.min;

                if $nth.defined {
                    $idxs := $nth.map(&nthidx).Array;
                    return Slip.new()
                        if $clip.min and not $idxs.EXISTS-POS($clip.min - 1);
                }
                else {
                    $idxs := (0..Inf).Array;
                }
            }

            unless $matches.defined {
                # Whatever, we have an extra layer of memoization.
                $matches := @matches.values;
                @matches := ();
            }

            # Just "list $matches.grep", once we have True.last
            @matches := (gather do for $matches -> $m {
                state $i = 0;
                state $took = 0;
                state $n = $idxs.EXISTS-POS(0) ?? $idxs.shift !! Nil;
                last unless $n.defined;

                if $i == $n {
                    $n = $idxs.EXISTS-POS(0) ?? $idxs.shift !! Nil;
                    take $m;
                    $took++;
                    last if $took >= $clip.max;
                };
                $i++;

                last unless $n.defined;
            }).cache;
            @matches := () unless not $clip.min or @matches.EXISTS-POS($clip.min - 1);
        }
        else {
            @matches := $matches.list;
        }
        if $multi {
            try $caller_dollar_slash = @matches;
            @matches
        }
        else {
            try $caller_dollar_slash = (@matches[0] // $cur.MATCH_SAVE);
            (@matches[0] // $cur.MATCH_SAVE)
        }
    }

    multi method subst-mutate($self is rw: $matcher, $replacement,
                       :ii(:$samecase), :ss(:$samespace), :mm(:$samemark), *%options) {
        my $global = %options<g> || %options<global>;
        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my $SET_DOLLAR_SLASH     = nqp::istype($matcher, Regex);
        my $word_by_word = so $samespace || %options<s> || %options<sigspace>;

        try $caller_dollar_slash = $/ if $SET_DOLLAR_SLASH;
        my @matches              = self.match($matcher, |%options);

        if !@matches || (@matches == 1 && !@matches[0]) {
            Nil;
        }
        else {
            $self = $self!APPLY-MATCHES(
              @matches,
              $replacement,
              $caller_dollar_slash,
              $SET_DOLLAR_SLASH,
              $word_by_word,
              $samespace,
              $samecase,
              $samemark,
            );
            if $global {
                my \result := nqp::create(List);
                nqp::bindattr(result, List, '$!reified', nqp::getattr(@matches, List, '$!reified'));
                result
            }
            else {
                @matches[0]
            }
        }
    }

    multi method subst(Str:D: $matcher, $replacement, :$global, :$g,
                       :ii(:$samecase), :ss(:$samespace), :mm(:$samemark),
                       *%options) {

        # take the fast lane if we can
        return Rakudo::Internals.TRANSPOSE(self,$matcher,$replacement)
          if nqp::istype($matcher,Str) && nqp::istype($replacement,Str)
          && ($g || $global)
          && !$samecase && !$samespace && !$samemark && !%options;

        my $caller_dollar_slash := nqp::getlexcaller('$/');
        my $SET_DOLLAR_SLASH     = nqp::istype($matcher, Regex);
        my $word_by_word = so $samespace || %options<s> || %options<sigspace>;

        # nothing to do
        try $caller_dollar_slash = $/ if $SET_DOLLAR_SLASH;
        my @matches = self.match($matcher, :g($g || $global), |%options);

        !@matches || (@matches == 1 && !@matches[0])
          ?? self
          !! self!APPLY-MATCHES(
               @matches,
               $replacement,
               $caller_dollar_slash,
{'fraction_non_alphanumeric': 0.1673524108917757, 'fraction_numerical': 0.05658545605843465, 'mean_word_length': 1.5199790307548928, 'pattern_counts': {'":': 4, '<': 57, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5675270', 'n_tokens_mistral': 16829, 'n_tokens_neox': 15093, 'n_words': 2951}