From 68b9602d3b763de7e61fabf83624f2880d0cdd37 Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 13 Oct 2026 15:35:45 -0400 Subject: [PATCH] Rejoin split try except else finally suites when printing --- docs/LIMITATIONS.md | 21 +++++++++++---------- src/codegen.rs | 22 ++++++++++++++++++++++ tests/integration.rs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 6c90274a..f5ad6906 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -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: ` : 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: ` 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 diff --git a/src/codegen.rs b/src/codegen.rs index 89be6894..619926ba 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -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 }); diff --git a/tests/integration.rs b/tests/integration.rs index f4af5082..ad45b7d8 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -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");