{"atom":"regression/routine-scoped-match-and-when-last","family":"regression","slug":"routine-scoped-match-and-when-last","header":{"atom":"regression/routine-scoped-match-and-when-last","kind":"whole-program — a minimal repro, not yet reduced to an atom","source":"curated"},"axes":null,"cols":null,"engines":["rakudo-2026.07","rakupp-3.14.0"],"kinds":["ref","pp"],"defFrom":"rakupp:t/regression/routine-scoped-match-and-when-last.raku","counts":{"a":1,"f":0,"d":0,"c":0,"r":0,"n":0},"files":["atoms/regression/routine-scoped-match-and-when-last.grid"],"records":[["0001","","# Regression: found by running HTTP::Tiny's t/responses.t.\n#   * `$/` scopes to a METHOD activation, not just a sub's — a match inside a\n#     method was destroying the caller's `$/`\n#   * `last` inside a bare `when` inside a `for` stayed set after the loop\n#     consumed the when-flag, escaped the routine, and broke the CALLER's loop\n#   * `.slurp` belongs to IO::Path, not to every Str\n#   * `.^lookup` answers Mu for a method the type does not have\n# Runs clean under Rakudo too.\n\nmy $fails = 0;\nsub ck($got, $want, $desc) {\n    if $got eqv $want { say \"ok - $desc\" }\n    else { $fails++; say \"FAIL: $desc — {$got.raku} vs {$want.raku}\" }\n}\n\n# --- $/ is per-routine, methods included ------------------------------------\nclass M {\n    method inner(@l) { for @l { $_ ~~ /(\\w+)/ }; 'done' }\n    sub innersub(@l) { for @l { $_ ~~ /(\\w+)/ }; 'done' }\n    method outer() {\n        'abc123' ~~ / $<num> = [\\d ** 3] /;\n        my $before = ~$<num>;\n        self.inner(['x']);\n        my $afterMethod = $<num>.defined ?? ~$<num> !! 'GONE';\n        'abc123' ~~ / $<num> = [\\d ** 3] /;\n        innersub(['x']);\n        my $afterSub = $<num>.defined ?? ~$<num> !! 'GONE';\n        ($before, $afterMethod, $afterSub)\n    }\n}\nck M.new.outer.List, ('123', '123', '123'), 'a method call leaves the caller\\'s $/ alone';\n\n# the real shape: read the match again after a helper method ran\nclass H {\n    method headers(@l) { my %h; for @l { %h{$_} = 1 }; %h }\n    method parse(Str $s) {\n        $s ~~ / $<status> = [\\d ** 3] /;\n        { status => +$<status>, extra => self.headers(['a']), ok => $<status>.starts-with('2') }\n    }\n}\nck H.new.parse('200 OK')<ok>, True, 'and a later $<capture> still reads';\n\n# --- `last` in a `when` inside a `for` stays in its own routine -------------\nsub scan(@l) { my $n = 0; for @l { when .not { last }; default { $n++ } }; $n }\nclass S { method scan(@l) { my $n = 0; for @l { when .not { last }; default { $n++ } }; $n } }\nmy @seen;\nmy $i = 0;\nwhile $i < 3 { $i++; @seen.push: scan(['a', '', 'b']) }\nck @seen.List, (1, 1, 1), 'a `when { last }` in a sub does not break the caller\\'s loop';\nmy @seen2;\nmy $j = 0;\nrepeat while $j < 3 { $j++; @seen2.push: S.new.scan(['a', '', 'b']) }\nck @seen2.List, (1, 1, 1), 'nor in a method, nor a repeat loop';\nck scan(['a', 'b']), 2, 'and the `last` still works where it belongs';\n\n# a `when { return }` must still count as MATCHED, or a CATCH rethrows\nclass C { method m() { CATCH { when X::AdHoc { return 'CAUGHT' } }; die 'boom' } }\nck C.new.m, 'CAUGHT', 'a `when { return }` inside CATCH still handles';\n\n# a `when { next }` inside a CATCH inside a loop: BOTH the when-match and the\n# loop control fire, and the handler must still see the match (File::Find's\n# `keep-going` is written exactly this way)\nsub walk(@dirs, Bool :$keep-going) {\n    my @seen;\n    for @dirs -> $d {\n        if $d eq 'bad' {\n            die X::AdHoc.new(payload => 'boom');\n            CATCH { when X::AdHoc { .rethrow unless $keep-going; next } }\n        }\n        @seen.push: $d;\n    }\n    @seen\n}\nck walk(<a bad b>, :keep-going).List, ('a', 'b'), 'a `when { next }` in a CATCH resumes the loop';\nck (try walk(<a bad b>)).defined, False, 'and rethrows without :keep-going';\n\n# --- a topic that is an ELEMENT aliases, but only of a plain container -------\nmy %h = k => 'a';\n$_ = 'b' with %h<k>;\nck %h<k>, 'b', 'a hash element topic writes back';\n\n# reading a key off a Match must NOT create it\n'ab12' ~~ / $<word> = [\\w+] /;\nmy $m = $/;\nmy $touched = False;\n$touched = True with $m<nosuch>;\nck $touched, False, 'an absent Match key stays absent';\nck ($m<word> // '').Str, 'ab12', 'and the real capture is intact';\n\n# --- .slurp is IO::Path's, and .^lookup says so -----------------------------\nck ('some string'.^lookup('slurp')).defined, False, '.^lookup is Mu for a method Str lacks';\nck ('some string'.^lookup('uc')).defined, True,  'and a real one is still found';\nck (try 'nonexistent-path'.slurp).defined, False, 'a Str is not a path';\nck $*TMPDIR.IO.child('x').Str.chars > 0, True, 'IO::Path is untouched';\n\nsay $fails ?? \"\\n$fails FAILED\" !! \"\\nPASS\";\nexit $fails ?? 1 !! 0;","output ok - a method call leaves the caller's $/ alone\nok - and a later $<capture> still reads\nok - a `when { last }` in a sub does not break the caller's loop\nok - nor in a method, nor a repeat loop\nok - and the `last` still works where it belongs\nok - a `when { return }` inside CATCH still handles\nok - a `when { next }` in a CATCH resumes the loop\nok - and rethrows without :keep-going\nok - a hash element topic writes back\nok - an absent Match key stays absent\nok - and the real capture is intact\nok - .^lookup is Mu for a method Str lacks\nok - and a real one is still found\nok - a Str is not a path\nok - IO::Path is untouched\n\nPASS","a",["ok - a method call leaves the caller's $/ alone …(17 lines)","ok - a method call leaves the caller's $/ alone …(17 lines)"],0,[["budget","20s"]]]]}