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
21 changes: 11 additions & 10 deletions docs/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,20 @@ the installed source):
packaging/pylock (CALL-chain underflow in `select`: three stacked CALLs
after an arg-position ternary)

## Remaining non-parsing shapes (2 files of the full-stdlib corpora)
## Remaining reconstruction gaps (2 files of the full-stdlib corpora)

Full-stdlib benchmarks (1017 modules of 3.12.14, 1251 of 3.8.20) leave
exactly two modules whose decompiled output does not parse. Both are
marked incomplete (never a crash, never silent corruption) and both are
structural reconstruction gaps in the loop-guard machinery rather than
rendering bugs:
Full-stdlib benchmarks (1017 modules of 3.12.14, 1251 of 3.8.20) identified
two modules with non-parsing output. The remaining reconstruction gaps are
tracked below; correcting a split statement's printing does not establish
semantic correctness for the entire module.

- `multiprocessing/connection.pyc` 3.12 — `try: ... except OSError: ...
else: <nested try/except/finally>` : the else body is emitted as a
sibling statement and the outer `else:`/`finally:` labels then land
after it, so the file dies with `expected 'except' or 'finally'
block`. Reproduces in `PipeListener.accept` (win32 branch).
else: <nested try/except/finally>` in `PipeListener.accept` (win32
branch): reconstruction can still displace the else body or duplicate
cleanup. The printer now rejoins a handler-less outer try's else/finally
with its sole inner handler-bearing try, removing the unattached-else
syntax error for that split shape. The exception-region structure still
needs correction.
- `random.pyc` 3.8 — `while True: if not 1e-7 < u1 < 0.9999999:
continue` : a negated multi-link guard at a loop top loses the
enclosing `while True` entirely and degrades into
Expand Down
22 changes: 22 additions & 0 deletions src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,28 @@ impl Printer {
orelse,
finalbody,
} => {
// Exception-region reconstruction can split try/except from
// its success and finally suites. An else without handlers
// cannot belong to the outer try. Rejoin the single inner
// handler-bearing try, retaining every recovered statement.
if self.version.at_least(2, 5)
&& handlers.is_empty() && !orelse.is_empty() && !finalbody.is_empty()
&& body.len() == 1
{
if let Stmt::Try {
body: inner_body, handlers: inner_handlers,
orelse: inner_else, finalbody: inner_finally,
} = &body[0]
{
if !inner_handlers.is_empty() && inner_else.is_empty() && inner_finally.is_empty() {
self.stmt(&Stmt::Try {
body: inner_body.clone(), handlers: inner_handlers.clone(),
orelse: orelse.clone(), finalbody: finalbody.clone(),
});
return;
}
}
}
// a bare `except:` must be last (and unique); drop extras
let mut handlers = handlers.clone();
handlers.sort_by_key(|h| if h.type_.is_none() { 1 } else { 0 });
Expand Down
38 changes: 38 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,44 @@ fn assert_contains(haystack: &str, needles: &[&str], ctx: &str) {
}
}

#[test]
fn split_exception_suites_generate_one_complete_try_statement() {
use pycdc::ast::{ExceptHandler, Expr, Stmt};
use pycdc::version::PythonVersion;
use std::rc::Rc;
let name = |value: &str| Rc::new(Expr::Name(value.into()));
let inner = Stmt::Try {
body: vec![Stmt::Expr(name("work")), Stmt::Expr(name("more_work"))],
handlers: vec![ExceptHandler {
type_: Some(name("ValueError")), name: Some(name("error")),
body: vec![Stmt::Expr(name("handled"))], is_star: false,
}],
orelse: vec![], finalbody: vec![],
};
let outer = Stmt::Try {
body: vec![inner.clone()], handlers: vec![],
orelse: vec![Stmt::Expr(name("success"))],
finalbody: vec![Stmt::Expr(name("cleanup")), Stmt::Expr(name("more_cleanup"))],
};
let module = PycModule::from_bytes(include_bytes!("pyc/hello.3.14.pyc")).unwrap();
// Unified try/except/finally syntax starts in Python 2.5. The repair
// concerns the recovered AST shape, rather than one opcode version.
for (major, minor) in [(2, 5), (2, 7), (3, 8), (3, 11), (3, 12), (3, 13), (3, 14)] {
let version = PythonVersion { major, minor, ..module.version() };
let binding = if major == 2 { ", error" } else { " as error" };
let expected = format!("try:\n work\n more_work\nexcept ValueError{binding}:\n handled\nelse:\n success\nfinally:\n cleanup\n more_cleanup\n");
assert_eq!(generate(&[outer.clone()], version, true), expected);

// A legitimate outer try/finally has no success suite to reunite.
let nested = Stmt::Try {
body: vec![inner.clone()], handlers: vec![], orelse: vec![],
finalbody: vec![Stmt::Expr(name("cleanup"))],
};
let expected = format!("try:\n try:\n work\n more_work\n except ValueError{binding}:\n handled\nfinally:\n cleanup\n");
assert_eq!(generate(&[nested], version, true), expected);
}
}

#[test]
fn py27_if_elif_else() {
let (src, clean) = decompile_fixture("if_elif_else.2.7.pyc");
Expand Down