Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c2c5152
Preserve disjunctive guards inside existing for loops
EiNSTeiN- Oct 29, 2026
51a94be
Keep padded None returns separate from calculated return tails
EiNSTeiN- Oct 29, 2026
7190720
Preserve calculated values across padded protected return boundaries
EiNSTeiN- Oct 29, 2026
8bffea1
Keep padded calculated returns inside their bounded try region
EiNSTeiN- Oct 29, 2026
9fe8e94
Keep chained comparisons as values inside inline comprehensions
EiNSTeiN- Oct 29, 2026
28fbed6
Reuse statement reconstruction for eligible modern generator expressions
EiNSTeiN- Oct 29, 2026
21d8f67
Preserve Boolean generator values behind continue-shaped filters
EiNSTeiN- Oct 29, 2026
02c412e
Reconstruct generator expressions in bounded expression simulation
EiNSTeiN- Oct 29, 2026
f08ae3a
Keep generator assignment expressions bound to their enclosing cells
EiNSTeiN- Oct 29, 2026
ff3dab3
Rebuild async exception regions across suspension cleanup
EiNSTeiN- Oct 29, 2026
1208ec6
Preserve context exits across early returns and finally cleanup
EiNSTeiN- Oct 29, 2026
cd53c9b
Keep computed returns inside contexts containing handler loops
EiNSTeiN- Oct 29, 2026
cb691e1
Preserve early async exits beyond the final protected fragment
EiNSTeiN- Oct 29, 2026
c733a61
Carry computed returns through nested context exits
EiNSTeiN- Oct 29, 2026
ef583fa
Keep nested per-row handlers owned by their loop
EiNSTeiN- Oct 29, 2026
d1c7d88
Close normal async exits without consuming return values or outer scopes
EiNSTeiN- Oct 29, 2026
0bba2c1
Follow typed mismatch edges past private handler-body bounds
EiNSTeiN- Oct 29, 2026
8f53b27
Preserve handler breaks after iterator disposal
EiNSTeiN- Oct 29, 2026
05db401
Recover shared None-led rejection guards
EiNSTeiN- Oct 29, 2026
7c7a3fd
Recover shared guards returning constructed outcomes
EiNSTeiN- Oct 29, 2026
3c77138
Recover shared validation raises and formatted error paths
EiNSTeiN- Oct 29, 2026
b2d655d
Normalize regression fixture endings and refresh source hashes
EiNSTeiN- Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 1 addition & 6 deletions docs/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,11 @@ constant handling.
On real code compiled with CPython 3.15.0 (pandas + scikit-learn, 2091
modules), **all 2091 recompile (100%)**.

One 3.15 edge remains outside that corpus: a `with` body whose only
statement is `return <expr>` (`with c as x: return x`) renders the body as
`pass` and loses the returned value (the exit protocol is not sunk into
the body). Attribute and subscript as-targets (`with c as h.attr:`,
Attribute and subscript as-targets (`with c as h.attr:`,
`with c as d[k]:`) are recovered on 3.14 and 3.15.

3.15 的 opcode 表由本机解释器直接生成,2.6–3.15 的行为用例全部通过。
真实代码(pandas + scikit-learn,2091 模块)**全部 2091 个可重编译(100%)**。
语料之外仍有一个 3.15 边角:`with` 体只有 `return <expr>` 时
(`with c as x: return x`)体会渲染成 `pass` 并丢失返回值。

## Class bodies with conditional method definitions (3.14/3.15)

Expand Down
987 changes: 933 additions & 54 deletions src/decompiler.rs

Large diffs are not rendered by default.

382 changes: 382 additions & 0 deletions src/decompiler_stack_tests.rs

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions tests/behavior/cases/b_await_context_controls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# MIN_VERSION: 3.12
import asyncio

async def sequential_contexts(manager, work, record, flag):
async with manager:
a = await work("a")
async with manager:
b = await work("b")
return a, b


async def awaited_then_context(manager, work, record, flag):
try:
a = await work("a")
async with manager:
b = await work("b")
except ValueError:
return "handled"
return a, b


async def outer_handler(manager, work, record, flag):
try:
async with manager:
a = await work("a")
b = await work("b")
except ValueError:
return "handled"
return a, b


async def exercise(function, failure, suppress):
events = []

async def work(label):
events.append(label)
await asyncio.sleep(0)
if failure == label:
raise ValueError(label)
return 1 if label == "a" else 2

def record(label):
events.append(label)

class Manager:
async def __aenter__(self):
events.append("enter")
await asyncio.sleep(0)
if failure == "enter":
raise ValueError("enter")
return self

async def __aexit__(self, typ, value, traceback):
events.append(("exit", typ.__name__ if typ else None))
await asyncio.sleep(0)
if failure == "exit":
raise ValueError("exit")
return suppress

try:
result = await function(Manager(), work, record, False)
print("return", result)
except BaseException as error:
print(type(error).__name__, str(error))
print(events)


for function in (sequential_contexts, awaited_then_context, outer_handler):
for failure in ("", "a", "b", "cleanup", "enter", "exit"):
for suppress in (False, True):
print(function.__name__, failure, suppress)
asyncio.run(exercise(function, failure, suppress))
65 changes: 65 additions & 0 deletions tests/behavior/cases/b_await_exception_regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# MIN_VERSION: 3.14
import asyncio

async def awaited_finally(manager, work, record, flag):
async with manager:
try:
await work("a")
finally:
record("cleanup")
await work("b")
return "tail"


async def awaited_else(manager, work, record, flag):
async with manager:
try:
a = await work("a")
except ValueError:
record("handled")
else:
await work("b")
return "tail"


async def exercise(function, failure, suppress):
events = []

async def work(label):
events.append(label)
await asyncio.sleep(0)
if failure == label:
raise ValueError(label)
return 1 if label == "a" else 2

def record(label):
events.append(label)

class Manager:
async def __aenter__(self):
events.append("enter")
await asyncio.sleep(0)
if failure == "enter":
raise ValueError("enter")
return self

async def __aexit__(self, typ, value, traceback):
events.append(("exit", typ.__name__ if typ else None))
await asyncio.sleep(0)
if failure == "exit":
raise ValueError("exit")
return suppress

try:
result = await function(Manager(), work, record, False)
print("return", result)
except BaseException as error:
print(type(error).__name__, str(error))
print(events)


for function in (awaited_finally, awaited_else):
for failure in ("", "a", "b", "cleanup", "enter", "exit"):
for suppress in (False, True):
print(function.__name__, failure, suppress)
asyncio.run(exercise(function, failure, suppress))
38 changes: 38 additions & 0 deletions tests/behavior/cases/b_bounded_generator_expressions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# MIN_VERSION: 3.13

collect = tuple

def plain(values):
return collect(value for value in values)


def captured(values, offset):
return collect(value + offset for value in values)


def multiple(values, first, second):
return collect((value + first) * second for value in values)


def ordinary_function(values):
return (lambda value: value)(values)


for values in ([], [1, 2, 3], ["x"]):
for function, args in ((plain, (values,)), (captured, (values, 5)),
(multiple, (values, 5, 3))):
try:
print(function.__name__, function(*args))
except TypeError as error:
print(function.__name__, type(error).__name__)


def values():
print("outer")
for value in (1, 2):
print("yield", value)
yield value


print(captured(values(), 5))
print(ordinary_function("unchanged"))
59 changes: 59 additions & 0 deletions tests/behavior/cases/b_bounded_try_returns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# MIN_VERSION: 3.11

# Regenerate with: python tools/compile_bounded_try_returns.py
# The compiled inputs add NOPs before returns and relocate exception ranges.
from datetime import datetime, timezone


def parse_timestamp(value):
if isinstance(value, datetime):
return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc)
if isinstance(value, (int, float)) and not isinstance(value, bool):
try:
return datetime.fromtimestamp(value, timezone.utc)
except (OverflowError, OSError, ValueError):
return
if isinstance(value, str):
try:
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
except ValueError:
return
return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc)


def unprotected_return(read):
try:
read("inside")
except ValueError:
return
return read("outside")


def else_return(read):
try:
read("inside")
except ValueError:
return
else:
return read("outside")


for value in (
None, True, False, 0, 123456789, float("inf"), float("nan"), "invalid",
"2026-10-12T12:00:00", "2026-10-12T12:00:00Z",
datetime(2026, 10, 12), datetime(2026, 10, 12, tzinfo=timezone.utc),
):
print(parse_timestamp(value))


def read(where):
print("read", where)
if where == "outside":
raise ValueError("outside the protected body")


for function in (unprotected_return, else_return):
try:
function(read)
except ValueError as error:
print(function.__name__, str(error))
87 changes: 87 additions & 0 deletions tests/behavior/cases/b_carried_value_returns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# MIN_VERSION: 3.11
from types import SimpleNamespace

# A computed return can be outside the protected range but still use its value.


def call_return(value, make, fallback, default):
try:
return make(value)
except Exception:
pass
return fallback.read().result or default.result


def attribute_return(value, make, fallback, default):
try:
return make(value).result
except Exception:
pass
return fallback.read().result or default.result


def tuple_return(value, make, fallback, default):
try:
return (make(value), 7)
except Exception:
pass
return fallback.read().result or default.result


def mapping_return(value, make, fallback, default):
try:
return {"value": make(value)}
except Exception:
pass
return fallback.read().result or default.result


def bare_return(value, make, fallback, default):
try:
make(value)
return None
except Exception:
return "handled"
return fallback.read().result or default.result

for function in (call_return, attribute_return, tuple_return, mapping_return, bare_return):
for truth in (False, True):
for result in (None, 0, "success"):
for failure in ("", "truth", "attribute", "make", "base", "fallback"):
events = []

class Value:
def __bool__(self):
events.append("truth")
if failure == "truth":
raise ValueError("truth failed")
return truth

class Result:
@property
def result(self):
events.append("attribute")
if failure == "attribute":
raise AttributeError("attribute failed")
return result

class Fallback:
def read(self):
events.append("fallback")
if failure == "fallback":
raise RuntimeError("fallback failed")
return SimpleNamespace(result="fallback-result")

def make(value):
events.append("make")
if failure == "make":
raise KeyError("make failed")
if failure == "base":
raise KeyboardInterrupt("owned interrupt")
return Result() if function is attribute_return else result

try:
outcome = function(Value(), make, Fallback(), SimpleNamespace(result="default"))
print(function.__name__, truth, result, failure, outcome, events)
except BaseException as error:
print(function.__name__, truth, result, failure, type(error).__name__, str(error), events)
Loading
Loading