Python 3.11 compiles 'while cond:' with a bottom test: an entry guard
(eval cond; POP_JUMP_FORWARD_IF_FALSE end) before the body, and a
back-edge (eval cond; POP_JUMP_BACKWARD_IF_TRUE loop_start) at the end.
pycdc rendered both halves as separate 'if' blocks, so EVERY while loop
came out as 'if cond: ... if not cond: pass' and never looped.
- New ScanWhileLoops pre-pass pairs each conditional backward jump with
the forward guard immediately preceding its target (guard skipping to
the instruction after the back-edge) to identify genuine while loops.
- At the guard, open a BLK_WHILE with the condition instead of an if.
- At the back-edge, discard the duplicated condition and close the loop,
but only when a BLK_WHILE is actually open (guard against a
misidentified back-edge collapsing the block stack and crashing).
Fixes while loops across all files (including nested loops and
continue). decompilation target: 237/239 files, corpus 41/95 (+1: signature; 0 regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two combined fixes for the 'return inside if inside try / except as e'
pattern:
1. The return-in-if skip logic could consume the PUSH_EXC_INFO that
immediately follows a return inside an if within a try. Dropping it
left the handler without its exception sentinel, so the 'as e'
binding captured a garbage stack value and the handler was mis-nested
as a statement in the try body. Never skip PUSH_EXC_INFO.
2. Suppress the compiler cleanup 'e = None' when the store value is an
explicit None constant (LOAD_CONST None; STORE), not only the NULL
placeholder form. With the binding now correct, this removes the
spurious 'e = None; del e' tail. decompilation target: 236/239 files, corpus 41/95 (+1: utilities; 0 regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In 3.11 there is no POP_BLOCK, so a for-loop only closed via the
JUMP_BACKWARD that ends its body. The is_jump_to_start test compared
the RELATIVE jump operand against the loop's ABSOLUTE start position,
so it almost never matched and the loop never closed: any statement
after the loop (and even except handlers / the function's return) was
absorbed into the loop body, producing wrong (often still-compiling)
output and breaking nested try/except indentation.
- Compute the real jump target (pos - offs) in 3.10+ and compare to
the loop start.
- Distinguish the implicit loop-iteration back-jump (pos == block end,
closes the loop) from an explicit (earlier, emits continue).
- Guard the BLK_ELSE branch's stack_hist.top() against an empty stack
(a for nested in a while/else could otherwise crash, e.g. csv).
Fixes the core 'code after for loop' defect across all files. decompilation target: 235/239 files, corpus 41/95 (+2: realization, gettext; 0 regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In 3.11 a return inside an if/else may fall straight into a sibling
branch. The old code unconditionally consumed the next instruction to
skip a redundant jump; when that instruction was the LOAD_CONST of a
code object feeding a MAKE_FUNCTION (e.g. a list comprehension after
'if not x: return []'), dropping it left MAKE_FUNCTION without its
operand and crashed with std::bad_cast.
Now peek the next instruction and keep it only when it is a LOAD_CONST
of a code object; otherwise preserve the original skip behavior.
Added PycBuffer::pos()/setPos() for safe peeking. decompilation target: 234/239 files, corpus 40/95 (+1, 0 regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>