Skip to main content

python_ast/ast/tree/
try_stmt.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, FromPyObject, PyAny, PyResult, prelude::PyAnyMethods};
3use quote::quote;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    CodeGen, CodeGenContext, ExprType, Node, PythonOptions, Statement, SymbolTableScopes,
8    extract_list,
9};
10
11/// Try statement (try/except/else/finally)
12#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
13pub struct Try {
14    /// The main body of the try block
15    pub body: Vec<Statement>,
16    /// Exception handlers (except clauses)
17    pub handlers: Vec<ExceptHandler>,
18    /// Optional else clause body (executed when no exception occurs)
19    pub orelse: Vec<Statement>,
20    /// Optional finally clause body (always executed)
21    pub finalbody: Vec<Statement>,
22    /// Position information
23    pub lineno: Option<usize>,
24    pub col_offset: Option<usize>,
25    pub end_lineno: Option<usize>,
26    pub end_col_offset: Option<usize>,
27}
28
29/// Exception handler (except clause)
30#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
31pub struct ExceptHandler {
32    /// The exception type to catch (None means catch all)
33    pub exception_type: Option<ExprType>,
34    /// Variable name to bind the exception to (optional)
35    pub name: Option<String>,
36    /// Body of the except clause
37    pub body: Vec<Statement>,
38    /// Position information
39    pub lineno: Option<usize>,
40    pub col_offset: Option<usize>,
41    pub end_lineno: Option<usize>,
42    pub end_col_offset: Option<usize>,
43}
44
45impl<'a, 'py> FromPyObject<'a, 'py> for Try {
46    type Error = pyo3::PyErr;
47    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
48        // Extract body
49        let body: Vec<Statement> = extract_list(&ob, "body", "try body")?;
50        
51        // Extract handlers
52        let handlers: Vec<ExceptHandler> = extract_list(&ob, "handlers", "try handlers")?;
53        
54        // Extract orelse (optional)
55        let orelse: Vec<Statement> = extract_list(&ob, "orelse", "try orelse").unwrap_or_default();
56        
57        // Extract finalbody (optional)
58        let finalbody: Vec<Statement> = extract_list(&ob, "finalbody", "try finalbody").unwrap_or_default();
59        
60        Ok(Try {
61            body, 
62            handlers,
63            orelse,
64            finalbody,
65            lineno: ob.lineno(),
66            col_offset: ob.col_offset(),
67            end_lineno: ob.end_lineno(),
68            end_col_offset: ob.end_col_offset(),
69        })
70    }
71}
72
73impl<'a, 'py> FromPyObject<'a, 'py> for ExceptHandler {
74    type Error = pyo3::PyErr;
75    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
76        // Extract exception type (optional)
77        let exception_type: Option<ExprType> = if let Ok(type_attr) = ob.getattr("type") {
78            if type_attr.is_none() {
79                None
80            } else {
81                Some(type_attr.extract()?)
82            }
83        } else {
84            None
85        };
86        
87        // Extract name (optional)
88        let name: Option<String> = if let Ok(name_attr) = ob.getattr("name") {
89            if name_attr.is_none() {
90                None
91            } else {
92                Some(name_attr.extract()?)
93            }
94        } else {
95            None
96        };
97        
98        // Extract body
99        let body: Vec<Statement> = extract_list(&ob, "body", "except handler body")?;
100        
101        Ok(ExceptHandler {
102            exception_type,
103            name,
104            body,
105            lineno: ob.lineno(),
106            col_offset: ob.col_offset(),
107            end_lineno: ob.end_lineno(),
108            end_col_offset: ob.end_col_offset(),
109        })
110    }
111}
112
113impl Node for Try {
114    fn lineno(&self) -> Option<usize> { self.lineno }
115    fn col_offset(&self) -> Option<usize> { self.col_offset }
116    fn end_lineno(&self) -> Option<usize> { self.end_lineno }
117    fn end_col_offset(&self) -> Option<usize> { self.end_col_offset }
118}
119
120impl Node for ExceptHandler {
121    fn lineno(&self) -> Option<usize> { self.lineno }
122    fn col_offset(&self) -> Option<usize> { self.col_offset }
123    fn end_lineno(&self) -> Option<usize> { self.end_lineno }
124    fn end_col_offset(&self) -> Option<usize> { self.end_col_offset }
125}
126
127impl CodeGen for Try {
128    type Context = CodeGenContext;
129    type Options = PythonOptions;
130    type SymbolTable = SymbolTableScopes;
131
132    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
133        // Process body, handlers, orelse, and finalbody
134        let symbols = self.body.into_iter().fold(symbols, |acc, stmt| stmt.find_symbols(acc));
135        let symbols = self.handlers.into_iter().fold(symbols, |acc, handler| {
136            let symbols = handler.body.into_iter().fold(acc, |acc, stmt| stmt.find_symbols(acc));
137            if let Some(exception_type) = handler.exception_type {
138                exception_type.find_symbols(symbols)
139            } else {
140                symbols
141            }
142        });
143        let symbols = self.orelse.into_iter().fold(symbols, |acc, stmt| stmt.find_symbols(acc));
144        self.finalbody.into_iter().fold(symbols, |acc, stmt| stmt.find_symbols(acc))
145    }
146
147    fn to_rust(
148        self,
149        ctx: Self::Context,
150        options: Self::Options,
151        symbols: Self::SymbolTable,
152    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
153        // The try body runs inside an immediately-invoked closure; `raise`
154        // (and failed `assert`) inside it lower to `return Err(...)`. When
155        // the body contains function-level returns, the closure's Ok value
156        // is a PyFlow carrying the returned value out (Return), a
157        // break/continue signal, or normal completion (Normal).
158        let has_return = crate::body_contains_function_return(&self.body);
159        // A `break`/`continue` in the try body targets a loop OUTSIDE the
160        // body's closure, so it cannot be emitted as a Rust jump — it is
161        // threaded out as a PyFlow signal and replayed below.
162        let body_escapes = crate::body_breaks_outward(&self.body);
163        // Handler and else bodies run inline UNLESS there is a finally
164        // clause, which wraps them in their own closure; a break there
165        // would escape that closure with no signal path back. Refuse at
166        // conversion time rather than emit Rust that cannot compile.
167        if !self.finalbody.is_empty() {
168            let where_ = if self.handlers.iter().any(|h| crate::body_breaks_outward(&h.body)) {
169                Some("except handler")
170            } else if crate::body_breaks_outward(&self.orelse) {
171                Some("else clause")
172            } else {
173                None
174            };
175            if let Some(where_) = where_ {
176                return Err(format!(
177                    "`break`/`continue` in a try statement's {} is not supported when the \
178                     statement also has a `finally` clause; move the loop control out of the \
179                     handler, or drop the finally clause",
180                    where_
181                )
182                .into());
183            }
184        }
185        let body_for_guarantee = self.body.clone();
186        let body_ctx = CodeGenContext::TryBlock {
187            parent: Box::new(ctx.clone()),
188        };
189        let try_body_tokens: Result<Vec<TokenStream>, Box<dyn std::error::Error>> = self
190            .body
191            .into_iter()
192            .map(|stmt| stmt.to_rust(body_ctx.clone(), options.clone(), symbols.clone()))
193            .collect();
194        let try_body_tokens = try_body_tokens?;
195
196        // A return that broke out of any of the closures below runs the
197        // finally body, then returns from the function — re-wrapped as
198        // another Break when this try is itself inside an enclosing try's
199        // closure.
200        let break_return = if ctx.in_try_block() {
201            quote!(return Ok(PyFlow::Return(__rython_ret));)
202        } else {
203            quote!(return Ok(__rython_ret);)
204        };
205
206        let has_finally = !self.finalbody.is_empty();
207        let finally_tokens = if has_finally {
208            let finally_body_tokens: Result<Vec<TokenStream>, Box<dyn std::error::Error>> = self
209                .finalbody
210                .clone()
211                .into_iter()
212                .map(|stmt| stmt.to_rust(ctx.clone(), options.clone(), symbols.clone()))
213                .collect();
214            let finally_body_tokens = finally_body_tokens?;
215            quote! { #(#finally_body_tokens;)* }
216        } else {
217            quote!()
218        };
219
220        // Handler bodies run outside the try closure (their exceptions are
221        // not caught by this try), with the caught exception in scope. When
222        // a finally clause exists, each handler body runs in its own
223        // closure so a return or raise inside it still executes the finally
224        // body before leaving the function, as Python requires.
225        let handler_ctx = CodeGenContext::ExceptHandler {
226            parent: Box::new(ctx.clone()),
227        };
228        let mut arms: Vec<TokenStream> = Vec::new();
229        let mut has_catch_all = false;
230        for handler in self.handlers {
231            let guard = match &handler.exception_type {
232                None => None,
233                Some(t) => exception_match_guard(t)?,
234            };
235            let bind = match &handler.name {
236                Some(name) => {
237                    let ident = crate::safe_ident(name);
238                    quote! {
239                        #[allow(unused_variables, unused_mut)]
240                        let mut #ident = __rython_exc.clone();
241                    }
242                }
243                None => quote!(),
244            };
245            let arm_body = lower_finally_guarded_body(
246                handler.body,
247                handler_ctx.clone(),
248                &options,
249                &symbols,
250                has_finally,
251                &finally_tokens,
252                &break_return,
253                "handler body terminates on every path",
254            )?;
255            match guard {
256                Some(g) => arms.push(quote! {
257                    Err(__rython_exc) if #g => { #bind #arm_body }
258                }),
259                None => {
260                    has_catch_all = true;
261                    arms.push(quote! {
262                        Err(__rython_exc) => { #bind #arm_body }
263                    });
264                    break; // later handlers are unreachable, as in Python
265                }
266            }
267        }
268
269        // Else clause: runs only when the body completed without raising;
270        // its own exceptions are not caught by this try's handlers — but a
271        // return or raise in it must still run the finally body first.
272        let else_tokens = if !self.orelse.is_empty() {
273            lower_finally_guarded_body(
274                self.orelse,
275                ctx.clone(),
276                &options,
277                &symbols,
278                has_finally,
279                &finally_tokens,
280                &break_return,
281                "else clause terminates on every path",
282            )?
283        } else {
284            quote!()
285        };
286
287        // When the try body terminates on every path (return/raise), the
288        // completed-normally arm is provably dead — mark it unreachable so
289        // the surrounding function (which emits no fall-through tail when
290        // all paths terminate) still typechecks.
291        let ok_arm_body = if crate::guarantees_return(&body_for_guarantee) {
292            quote!(unreachable!("try body terminates on every path"))
293        } else {
294            else_tokens
295        };
296
297        // An exception no handler matched propagates as an Err — to the
298        // enclosing try's closure when there is one, otherwise out of the
299        // function, as in Python. The finally body still runs first.
300        if !has_catch_all {
301            arms.push(quote! {
302                Err(__rython_exc) => { #finally_tokens return Err(__rython_exc); }
303            });
304        }
305
306        if has_return || body_escapes {
307            // The Return arm carries a value, so the parameter needs a
308            // type; a body with no `return` never constructs one, so pin
309            // it to () rather than leave it uninferable.
310            let flow_type = if has_return {
311                quote!(PyFlow<_>)
312            } else {
313                quote!(PyFlow<()>)
314            };
315            let return_arm = if has_return {
316                quote! {
317                    Ok(PyFlow::Return(__rython_ret)) => {
318                        #finally_tokens
319                        #break_return
320                    }
321                }
322            } else {
323                quote! { Ok(PyFlow::Return(_)) => unreachable!("try body has no return"), }
324            };
325            // Replay a signalled break/continue at the try statement's own
326            // position, AFTER the finally clause — Python's ordering. If
327            // this try is itself inside another try's closure, the signal
328            // is re-raised outward instead of becoming a Rust loop jump.
329            let (break_arm, continue_arm) = if body_escapes {
330                let replay_break = if ctx.break_crosses_try_closure() {
331                    quote!(return Ok(PyFlow::Break);)
332                } else if ctx.break_target_has_else() {
333                    quote!({ __rython_broke = true; break; })
334                } else {
335                    quote!(break;)
336                };
337                let replay_continue = if ctx.break_crosses_try_closure() {
338                    quote!(return Ok(PyFlow::Continue);)
339                } else {
340                    quote!(continue;)
341                };
342                (
343                    quote! { Ok(PyFlow::Break) => { #finally_tokens #replay_break } },
344                    quote! { Ok(PyFlow::Continue) => { #finally_tokens #replay_continue } },
345                )
346            } else {
347                (
348                    quote! { Ok(PyFlow::Break) => unreachable!("try body has no break"), },
349                    quote! { Ok(PyFlow::Continue) => unreachable!("try body has no continue"), },
350                )
351            };
352            Ok(quote! {
353                {
354                    #[allow(unreachable_code)]
355                    let __rython_try_result: std::result::Result<
356                        #flow_type,
357                        PyException,
358                    > = (|| {
359                        #(#try_body_tokens;)*
360                        Ok(PyFlow::Normal)
361                    })();
362                    match __rython_try_result {
363                        #return_arm
364                        #break_arm
365                        #continue_arm
366                        Ok(PyFlow::Normal) => { #ok_arm_body }
367                        #(#arms)*
368                    }
369                    #finally_tokens
370                }
371            })
372        } else {
373            Ok(quote! {
374                {
375                    #[allow(unreachable_code)]
376                    let __rython_try_result: std::result::Result<(), PyException> = (|| {
377                        #(#try_body_tokens;)*
378                        Ok(())
379                    })();
380                    match __rython_try_result {
381                        Ok(()) => { #ok_arm_body }
382                        #(#arms)*
383                    }
384                    #finally_tokens
385                }
386            })
387        }
388    }
389}
390
391/// Lower an except-handler or else-clause body. Without a finally clause
392/// the statements run inline. With one, the body runs in its own closure —
393/// like the try body — so a `return` (threaded out as PyFlow::Return)
394/// or a raise (an Err) still executes the finally body before leaving the
395/// function, as Python guarantees.
396#[allow(clippy::too_many_arguments)]
397fn lower_finally_guarded_body(
398    body: Vec<Statement>,
399    base_ctx: CodeGenContext,
400    options: &PythonOptions,
401    symbols: &SymbolTableScopes,
402    has_finally: bool,
403    finally_tokens: &TokenStream,
404    break_return: &TokenStream,
405    unreachable_note: &str,
406) -> Result<TokenStream, Box<dyn std::error::Error>> {
407    if !has_finally {
408        let tokens: Result<Vec<TokenStream>, Box<dyn std::error::Error>> = body
409            .into_iter()
410            .map(|stmt| stmt.to_rust(base_ctx.clone(), options.clone(), symbols.clone()))
411            .collect();
412        let tokens = tokens?;
413        return Ok(quote! { #(#tokens;)* });
414    }
415
416    let guarantees = crate::guarantees_return(&body);
417    let has_ret = crate::body_contains_function_return(&body);
418    let inner_ctx = CodeGenContext::TryBlock {
419        parent: Box::new(base_ctx),
420    };
421    let tokens: Result<Vec<TokenStream>, Box<dyn std::error::Error>> = body
422        .into_iter()
423        .map(|stmt| stmt.to_rust(inner_ctx.clone(), options.clone(), symbols.clone()))
424        .collect();
425    let tokens = tokens?;
426
427    let completed_arm = if guarantees {
428        quote!(unreachable!(#unreachable_note))
429    } else {
430        quote!()
431    };
432
433    if has_ret {
434        Ok(quote! {
435            #[allow(unreachable_code)]
436            let __rython_inner: std::result::Result<
437                PyFlow<_>,
438                PyException,
439            > = (|| {
440                #(#tokens;)*
441                Ok(PyFlow::Normal)
442            })();
443            match __rython_inner {
444                Ok(PyFlow::Return(__rython_ret)) => {
445                    #finally_tokens
446                    #break_return
447                }
448                // A break/continue in a closure-wrapped handler or else
449                // clause is rejected at conversion time, so these are
450                // structurally unreachable.
451                Ok(PyFlow::Break) => unreachable!("handler body has no break"),
452                Ok(PyFlow::Continue) => unreachable!("handler body has no continue"),
453                Ok(PyFlow::Normal) => { #completed_arm }
454                Err(__rython_reraise) => {
455                    #finally_tokens
456                    return Err(__rython_reraise);
457                }
458            }
459        })
460    } else {
461        Ok(quote! {
462            #[allow(unreachable_code)]
463            let __rython_inner: std::result::Result<(), PyException> = (|| {
464                #(#tokens;)*
465                Ok(())
466            })();
467            match __rython_inner {
468                Ok(()) => { #completed_arm }
469                Err(__rython_reraise) => {
470                    #finally_tokens
471                    return Err(__rython_reraise);
472                }
473            }
474        })
475    }
476}
477
478/// The match guard testing whether the caught exception matches an except
479/// clause's type expression: a name (`except ValueError`), a dotted name
480/// (`except os.error` — matched by its final attribute), or a tuple of
481/// either (`except (ValueError, TypeError)`).
482fn exception_match_guard(
483    exception_type: &ExprType,
484) -> Result<Option<TokenStream>, Box<dyn std::error::Error>> {
485    match exception_type {
486        ExprType::Name(name) => {
487            let n = &name.id;
488            Ok(Some(quote!(__rython_exc.matches(#n))))
489        }
490        ExprType::Attribute(attr) => {
491            let n = &attr.attr;
492            Ok(Some(quote!(__rython_exc.matches(#n))))
493        }
494        ExprType::Tuple(tuple) => {
495            let mut guards = Vec::new();
496            for elt in &tuple.elts {
497                match exception_match_guard(elt)? {
498                    Some(g) => guards.push(g),
499                    None => return Ok(None),
500                }
501            }
502            if guards.is_empty() {
503                Ok(None)
504            } else {
505                Ok(Some(quote!(#(#guards)||*)))
506            }
507        }
508        other => Err(format!(
509            "unsupported exception type in except clause: {:?} (use a name, \
510             dotted name, or tuple of names)",
511            other
512        )
513        .into()),
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    // Tests would go here - currently commented out as they need full AST infrastructure
520    // create_parse_test!(test_simple_try, "try:\n    pass\nexcept:\n    pass", "test.py");
521}