github skuznetsov/depyo.js v1.2.4

4 hours ago

Fixed — Issue #3: if cond: return x was being folded into return cond and x

lib/handlers/misc_other.js was unconditionally rewriting one-statement
if cond: return x blocks into a short-circuit return cond and x. The
rewrite is only sound when the compiler actually emitted a sticky-truth
jump (JUMP_IF_FALSE_OR_POP, real short-circuit). For a normal
if-statement the compiler emits POP_JUMP_IF_FALSE, and the fold then
quietly loses information.

What was broken

  • Falsy returns silently disappeared. if cond: return 0 collapses to
    cond and 0 which evaluates to cond itself when cond is truthy — the
    decompiled source no longer matches the original semantics. Same for
    return None, return "", return [], etc.
  • elif chains imploded. A 4-way if leftover == 1: ... elif leftover == 2: ...
    chain in stdlib base64.encode was rendered as
    return leftover == 1 and ...; return leftover == 2 and ...; ...
    on a single line.
  • Dead return artifacts. Code below the folded if was emitted as
    unreachable but still rendered, producing output that no human would
    write.
  • Cascade folds. if a: if b: return x could re-enter the fold and
    produce nonsense like return a and b and None.

Visible examples from the snapshot suite that this release refreshes:

  • test/ok_lib2.6/.../base64.py — encode elif chain
  • test/ok_lib2.6/.../calendar.pyif isinstance(i, slice): return [...]
  • test/ok_lib2.7/.../doctest.pyreturn self._exclude_empty and not docstring and None
  • test/ok_lib2.7/.../formatter.py — same pattern in writer.send_paragraph
  • test/.../py310_union_types.py, 10_if_else_ternary.py — toy cases

Fix

Removed the fold entirely. The handler still pops the if-block, appends it
to the parent block, and consumes the trailing JUMP_ABSOLUTE/JUMP_FORWARD
— but no longer rewrites the AST.

Genuine cond and x short-circuit was never coming through this path
anyway; it round-trips through the JUMP_IF_FALSE_OR_POP handler in
control_flow_jumps.js and is unaffected.

Why it took two releases to catch

The snapshot test suite had blessed the buggy outputbase64.encode,
doctest._DocTestFinder._find, calendar.WeekHeader.__getitem__,
py310_union_types.alias_* all had snapshot files showing the wrong fold,
silently passing CI. Issue #3
exposed it because a user actually read the output instead of trusting
green CI. This is the third regression in the v1.2.x line surfaced by
external eyes rather than the test harness; the snapshot framework cannot
distinguish "matches snapshot" from "correct".

Regression coverage

New fixture test/modern_features/issue12_if_return_falsy.py covers:

  • falsy_x — the exact issue #3 case (x = 1; if cond: return x; return 0)
  • none_x, empty_string — falsy return value variants
  • truthy_x — control case where the fold would have looked superficially fine
  • negative_branchif not cond form
  • real_short_circuit — genuine return cond and x (must still fold via
    the JUMP_IF_FALSE_OR_POP path)

Compiled for 3.10, 3.11, 3.12, 3.13, 3.14. 3.10–3.12 now produce
correct output. 3.13/3.14 expose a separate pre-existing
if cond: pass / return x artifact unrelated to this fix — documented
in the snapshot, not in scope.

Compatibility

No API changes. Decompiled output for any code matching the buggy fold
pattern will change shape — refresh your local snapshots if you maintain
any.

Thanks to @CtrlCThenCtrlV for the bug report (filed against the
Python port repo, fix applied here in parallel).

Don't miss a new depyo.js release

NewReleases is sending notifications on new releases.