Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/decompiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31768,10 +31768,30 @@ return None;
// elif test would create a nested If at the same target, so keep
// the Else open in that case.
if let Some(top) = self.blocks.last() {
// A modern Else opened immediately after the then arm's
// unconditional skip has an authoritative finite span. A guard
// inside that span can share its merge without ending the Else:
// closing it here ejects the guarded query result and may read
// its local on the other arm before assignment. Keep older and
// inferred/try-else boundaries on their existing path.
let guarded_else_merge = self.version.at_least(3, 11)
&& top.kind == BlockType::Else
&& self.cur_offset > top.start
&& self.cur_offset < top.end
&& self.idx_of.get(&top.start).map_or(false, |&si| {
si > 0 && {
let skip = &self.instrs[si - 1];
!skip.is_backward
&& skip.end() == top.start
&& skip.target == Some(top.end)
&& matches!(skip.op, Op::JUMP_FORWARD | Op::JUMP | Op::JUMP_ABSOLUTE)
}
});
if matches!(top.kind, BlockType::Else | BlockType::TryElse)
&& top.end == target
&& !top.is_elif
&& self.cur_offset != top.start
&& !guarded_else_merge
{
self.close_blocks_at(target);
}
Expand Down
29 changes: 29 additions & 0 deletions tests/behavior/cases/b_guarded_else_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# MIN_VERSION: 3.11
import asyncio

async def awaited(owner):
value = None
active = owner.active
if active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = float(row[0])
owner.record(value)
return value


class Owner:
def __init__(self, active, row):
self.active = active
self.row = row
async def fetch(self):
print("fetch", self.row)
return self.row
def record(self, value):
print("record", value)
print(asyncio.run(awaited(Owner(True, (9,)))))
print(asyncio.run(awaited(Owner(False, (9,)))))
print(asyncio.run(awaited(Owner(False, (None,)))))
print(asyncio.run(awaited(Owner(False, None))))
67 changes: 67 additions & 0 deletions tests/fixtures/guarded_else_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Versioned .pyc files are consumed by tests/integration.rs.

# The finite else-span guard applies to Python 3.11+.

async def awaited(owner):
value = None
active = owner.active
if active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = float(row[0])
owner.record(value)
return value


async def synchronous(owner):
value = None
active = owner.active
if active:
value = float(1)
else:
row = owner.fetch()
if row and row[0] is not None:
value = 2
owner.record(value)
return value


async def attribute(owner):
value = None
active = owner.active
if active:
value = float(active['total'])
else:
row = owner.row
if row and row[0] is not None:
value = float(row[0])
owner.record(value)
return value


async def following_else_work(owner):
value = None
active = owner.active
if active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = 2
owner.record('queried')
owner.record(value)
return value


async def independent(owner):
value = None
row = None
if owner.active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = 2
return value
65 changes: 65 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,68 @@ fn py314_hello() {
"py3.14 hello",
);
}

#[test]
fn finite_else_span_keeps_its_inner_guard() {
for version in ["3.11", "3.12", "3.13", "3.14"] {
let (src, clean) = decompile_fixture(&format!("guarded_else_merge.{version}.pyc"));
assert!(clean, "Python {version}: output marked incomplete:\n{src}");
assert_contains(&src, &[
r#"async def awaited(owner):
value = None
active = owner.active
if active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = float(row[0])
owner.record(value)
return value"#,
r#"async def synchronous(owner):
value = None
active = owner.active
if active:
value = float(1)
else:
row = owner.fetch()
if row and row[0] is not None:
value = 2
owner.record(value)
return value"#,
r#"async def attribute(owner):
value = None
active = owner.active
if active:
value = float(active['total'])
else:
row = owner.row
if row and row[0] is not None:
value = float(row[0])
owner.record(value)
return value"#,
r#"async def following_else_work(owner):
value = None
active = owner.active
if active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = 2
owner.record('queried')
owner.record(value)
return value"#,
r#"async def independent(owner):
value = None
row = None
if owner.active:
value = 1
else:
row = await owner.fetch()
if row and row[0] is not None:
value = 2
return value"#,
], version);
}
}
Binary file added tests/pyc/guarded_else_merge.3.11.pyc
Binary file not shown.
Binary file added tests/pyc/guarded_else_merge.3.12.pyc
Binary file not shown.
Binary file added tests/pyc/guarded_else_merge.3.13.pyc
Binary file not shown.
Binary file added tests/pyc/guarded_else_merge.3.14.pyc
Binary file not shown.