Commit graph

4 commits

Author SHA1 Message Date
polfg
9c5ea2f2bc Reconstruct 3.11 while-loops (bottom-test optimization)
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>
2026-06-08 12:27:24 +02:00
polfg
7a0109a6f6 Fix except-as-e handler after return-in-if inside try
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>
2026-06-08 12:15:57 +02:00
polfg
96a672e587 Fix for-loop not closing in 3.11 (return/code after loop absorbed into body)
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>
2026-06-08 12:06:54 +02:00
polfg
ba35489dd8 Fix std::bad_cast crash on return-in-if followed by comprehension
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>
2026-06-08 11:49:55 +02:00