{"atom":"regression/failed-match-stale-captures","family":"regression","slug":"failed-match-stale-captures","header":{"atom":"regression/failed-match-stale-captures","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/failed-match-stale-captures.raku","counts":{"a":1,"f":0,"d":0,"c":0,"r":0,"n":0},"files":["atoms/regression/failed-match-stale-captures.grid"],"records":[["0001","","# Regression: after a FAILED match, bare $0/$1 still answered the captures of\n# an EARLIER successful match.\n#\n# A successful match (and a substitution's replacement eval) defines $0..$N as\n# real variables in the scope current at match time, because $N is sugar for\n# $/[N] and $/ alone can't carry them into every lookup context. A failed match\n# reset $/ to Nil but left those bindings standing, so in\n#\n#     $baz ~~ s{.(a)(.)} = \"$1$0p\";   # file scope: $1 is now ｢z｣\n#     { \"abc\" ~~ m/^(nope)/; say ?$1 }  # was True — the stale ｢z｣, not Nil\n#\n# the block saw the leaked binding instead of falling through to the reset $/.\n# Rakudo's contract is that $N always tracks $/: after a failed match every $N\n# is Nil. Found by S05-modifier/Perl_0.t test 21 while building m:P5 support;\n# the bug itself is general and this repro uses plain Raku regexes only.\n#\n# Fixed in Interpreter::regexMatch — on a failed match, every $k that still\n# resolves is shadowed with Nil so the alias contract holds.\n#\n# Contract: exit 0 + last line PASS.\nmy @fail;\n\nsub check($got, $want, $desc) {\n    @fail.push(\"$desc: got {$got.raku}, want {$want.raku}\") unless $got eqv $want;\n}\n\n# a substitution leaves $/ (and so $0/$1) on the last match, Rakudo-style\nmy $baz = \"baz\";\n$baz ~~ s{.(a)(.)} = \"$1$0p\";\ncheck $baz, \"zap\", 'the substitution itself';\ncheck (~$0), \"a\", '$0 after s/// is the first capture';\ncheck (~$1), \"z\", '$1 after s/// is the second capture';\n\n# a failed match resets the whole family, not just $/\n{\n    \"abc\" ~~ m/^(nope)/;\n    check $/, Nil, '$/ is Nil after a failed match';\n    check (?$0), False, '$0 is falsy after a failed match';\n    check (?$1), False, '$1 is falsy after a failed match (was the stale ｢z｣)';\n}\n\n# a successful match still repopulates after a failure\n{\n    \"abc\" ~~ m/^(nope)/;\n    \"xy\" ~~ m/(x)(y)/;\n    check (~$0), \"x\", '$0 tracks the newest successful match';\n    check (~$1), \"y\", '$1 tracks the newest successful match';\n}\n\nif @fail {\n    note \"FAILED:\\n\" ~ @fail.map({ \"  - $_\" }).join(\"\\n\");\n    exit 1;\n}\nsay \"PASS\";","output PASS","a",["PASS","PASS"],0,[["budget","20s"]]]]}