{"atom":"regression/loop-state-frame-scan","family":"regression","slug":"loop-state-frame-scan","header":{"atom":"regression/loop-state-frame-scan","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/loop-state-frame-scan.raku","counts":{"a":1,"f":0,"d":0,"c":0,"r":0,"n":0},"files":["atoms/regression/loop-state-frame-scan.grid"],"records":[["0001","","# The per-loop-execution `state` frame is now allocated only when the loop's\n# subtree actually declares `state` (a conservative AST scan; anything it\n# cannot rule out keeps the frame). These pin the scanner's recursion paths:\n# a `state` hiding under an if, a bare block, a nested loop, and a ternary\n# must still get the fresh-frame-per-loop-execution semantics, while plain\n# loops must still behave identically without the frame. Passes on both\n# engines.\n\n# state directly in a loop body: resets per loop STATEMENT execution\nsub direct() { my $r = ''; for 1..3 { state $n = 0; $n++; $r ~= $n }; $r }\ndie \"direct: {direct}/{direct}\" unless direct() eq '123' && direct() eq '123';\n\n# state under an if inside the loop. NOTE a known engine divergence in the\n# GRANULARITY: Rakudo clones the if's block per entry (counts '111'), rakupp\n# hosts the var in the per-loop-execution frame (counts '123'). What BOTH\n# agree on — and what the scanner must preserve — is the reset across calls:\n# the second call repeats the first exactly, starting from 1.\nsub under-if() {\n    my $r = '';\n    for 1..3 {\n        if $_ > 0 { state $n = 0; $n++; $r ~= $n }\n    }\n    $r\n}\nmy ($ui1, $ui2) = under-if(), under-if();\ndie \"under-if: $ui1/$ui2\" unless $ui1 eq $ui2 && $ui1.substr(0, 1) eq '1';\n\n# state in a bare block inside the loop: same divergence, same invariant\nsub under-block() {\n    my $r = '';\n    for 1..3 { { state $n = 0; $n++; $r ~= $n } }\n    $r\n}\nmy ($ub1, $ub2) = under-block(), under-block();\ndie \"under-block: $ub1/$ub2\" unless $ub1 eq $ub2 && $ub1.substr(0, 1) eq '1';\n\n# state in a nested loop: resets per INNER loop execution (= per outer iteration)\nsub nested() {\n    my $r = '';\n    for 1..2 {\n        for 1..2 { state $n = 0; $n++; $r ~= $n }\n        $r ~= '|';\n    }\n    $r\n}\ndie \"nested: {nested}\" unless nested() eq '12|12|';\n\n# a state-free loop still works and `my` in a while COND still spans iterations\nmy $i = 0;\nmy $seen = '';\nwhile my $x = ($i < 3 ?? ++$i !! Nil) { $seen ~= $x }\ndie \"while-cond my: $seen\" unless $seen eq '123';\n\n# routine-level state (no loop) is untouched: persists across calls\nsub counter() { state $c = 0; ++$c }\ncounter(); counter();\nmy $third = counter();\ndie \"routine state: $third\" unless $third == 3;\n\nsay \"PASS\";","output PASS","a",["PASS","PASS"],0,[["budget","20s"]]]]}