Skip to main content

python_ast/ast/tree/
function_def.rs

1use tracing::debug;
2use proc_macro2::TokenStream;
3use pyo3::{Borrowed, FromPyObject, PyAny, PyResult, prelude::PyAnyMethods};
4use quote::quote;
5use serde::{Deserialize, Serialize};
6use crate::ast::tree::statement::PyStatementTrait;
7
8use crate::{
9    CodeGen, CodeGenContext, ExprType, Object, ParameterList, PythonOptions, Statement,
10    StatementType, SymbolTableNode, SymbolTableScopes,
11};
12
13#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
14pub struct FunctionDef {
15    pub name: String,
16    pub args: ParameterList,
17    pub body: Vec<Statement>,
18    pub decorator_list: Vec<ExprType>,
19    /// The function's return annotation (`-> int`), if present.
20    pub returns: Option<Box<ExprType>>,
21}
22
23impl<'a, 'py> FromPyObject<'a, 'py> for FunctionDef {
24    type Error = pyo3::PyErr;
25    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
26        let name: String = ob.getattr("name")?.extract()?;
27        let args: ParameterList = ob.getattr("args")?.extract()?;
28        let body: Vec<Statement> = ob.getattr("body")?.extract()?;
29
30        // Extract decorator_list as Vec<ExprType>
31        let decorator_list: Vec<ExprType> = ob.getattr("decorator_list")?.extract().unwrap_or_default();
32
33        // Extract the return annotation, if any.
34        let returns: Option<Box<ExprType>> = match ob.getattr("returns") {
35            Ok(r) if !r.is_none() => r.extract().ok().map(Box::new),
36            _ => None,
37        };
38
39        Ok(FunctionDef {
40            name,
41            args,
42            body,
43            decorator_list,
44            returns,
45        })
46    }
47}
48
49impl PyStatementTrait for FunctionDef {
50}
51
52/// One add_argument spec collected at conversion time.
53struct ArgparseSpec {
54    name: String,
55    kind: &'static str, // "Str" | "Int" | "Float" | "StoreTrue"
56    default: Option<ExprType>,
57    help: Option<String>,
58}
59
60/// The argparse rewrite plan for a function body: parser-building
61/// statements to drop, the parse_args assignment to replace, and the
62/// literal specs. ArgumentParser/add_argument/parse_args are evaluated
63/// HERE, at conversion time — only literal specs can shape the typed
64/// namespace struct, so anything dynamic is a loud error.
65struct ArgparseRewrite {
66    skip: std::collections::HashSet<usize>,
67    parse_index: usize,
68    args_var: String,
69    prog: Option<String>,
70    description: Option<String>,
71    specs: Vec<ArgparseSpec>,
72}
73
74fn literal_str(e: &ExprType) -> Option<String> {
75    match e {
76        ExprType::Constant(c) => match &c.0 {
77            Some(litrs::Literal::String(s)) => Some(s.value().to_string()),
78            _ => None,
79        },
80        _ => None,
81    }
82}
83
84fn scan_argparse(
85    body: &[Statement],
86) -> Result<Option<ArgparseRewrite>, Box<dyn std::error::Error>> {
87    // Find `<var> = argparse.ArgumentParser(...)`.
88    let mut parser: Option<(usize, String, Option<String>, Option<String>)> = None;
89    for (i, stmt) in body.iter().enumerate() {
90        let StatementType::Assign(assign) = &stmt.statement else {
91            continue;
92        };
93        let ExprType::Call(call) = &assign.value else {
94            continue;
95        };
96        let ExprType::Attribute(attr) = call.func.as_ref() else {
97            continue;
98        };
99        let is_ctor = attr.attr == "ArgumentParser"
100            && matches!(attr.value.as_ref(), ExprType::Name(m) if m.id == "argparse");
101        if !is_ctor {
102            continue;
103        }
104        let [ExprType::Name(target)] = assign.targets.as_slice() else {
105            return Err("argparse.ArgumentParser must be assigned to a plain name".into());
106        };
107        if !call.args.is_empty() {
108            return Err(
109                "argparse.ArgumentParser: pass prog=/description= by keyword".into(),
110            );
111        }
112        let mut prog = None;
113        let mut description = None;
114        for kw in &call.keywords {
115            let value = literal_str(&kw.value).ok_or_else(|| {
116                format!(
117                    "argparse.ArgumentParser: {} must be a string literal (the parser \
118                     is evaluated at conversion time)",
119                    kw.arg.as_deref().unwrap_or("argument")
120                )
121            })?;
122            match kw.arg.as_deref() {
123                Some("prog") => prog = Some(value),
124                Some("description") => description = Some(value),
125                other => {
126                    return Err(format!(
127                        "argparse.ArgumentParser keyword '{}' is not supported yet",
128                        other.unwrap_or("**kwargs")
129                    )
130                    .into())
131                }
132            }
133        }
134        parser = Some((i, target.id.clone(), prog, description));
135        break;
136    }
137    let Some((ctor_index, pvar, prog, description)) = parser else {
138        return Ok(None);
139    };
140
141    // Collect `<pvar>.add_argument(...)` statements and the final
142    // `<args> = <pvar>.parse_args()`.
143    let mut skip = std::collections::HashSet::from([ctor_index]);
144    let mut specs = Vec::new();
145    let mut parse: Option<(usize, String)> = None;
146    for (i, stmt) in body.iter().enumerate().skip(ctor_index + 1) {
147        let call_on_parser = |call: &crate::Call| -> Option<String> {
148            let ExprType::Attribute(attr) = call.func.as_ref() else {
149                return None;
150            };
151            match attr.value.as_ref() {
152                ExprType::Name(m) if m.id == pvar => Some(attr.attr.clone()),
153                _ => None,
154            }
155        };
156        // A bare call statement surfaces as Expr(Call) or Call
157        // depending on the extraction path; normalize.
158        let stmt_call: Option<&crate::Call> = match &stmt.statement {
159            StatementType::Call(c) => Some(c),
160            StatementType::Expr(e) => match &e.value {
161                ExprType::Call(c) => Some(c),
162                _ => None,
163            },
164            _ => None,
165        };
166        match &stmt.statement {
167            _ if stmt_call.is_some_and(|c| call_on_parser(c) == Some("add_argument".into())) => {
168                let call = stmt_call.expect("checked");
169                if parse.is_some() {
170                    return Err("add_argument after parse_args is not supported".into());
171                }
172                let [name_expr] = call.args.as_slice() else {
173                    return Err(
174                        "add_argument takes exactly one name (short aliases are not \
175                         supported yet)"
176                            .into(),
177                    );
178                };
179                let name = literal_str(name_expr)
180                    .ok_or("add_argument: the name must be a string literal")?;
181                if name.starts_with('-') && !name.starts_with("--") {
182                    return Err(format!(
183                        "add_argument: short option '{}' is not supported yet; use the \
184                         --long form",
185                        name
186                    )
187                    .into());
188                }
189                let mut kind: Option<&'static str> = None;
190                let mut default = None;
191                let mut help = None;
192                let mut store_true = false;
193                for kw in &call.keywords {
194                    match kw.arg.as_deref() {
195                        Some("type") => {
196                            kind = Some(match &kw.value {
197                                ExprType::Name(n) if n.id == "int" => "Int",
198                                ExprType::Name(n) if n.id == "float" => "Float",
199                                ExprType::Name(n) if n.id == "str" => "Str",
200                                _ => {
201                                    return Err(format!(
202                                        "add_argument('{}'): type must be int, float, \
203                                         or str",
204                                        name
205                                    )
206                                    .into())
207                                }
208                            });
209                        }
210                        Some("default") => default = Some(kw.value.clone()),
211                        Some("help") => {
212                            help = Some(literal_str(&kw.value).ok_or_else(|| {
213                                format!(
214                                    "add_argument('{}'): help must be a string literal",
215                                    name
216                                )
217                            })?)
218                        }
219                        Some("action") => match literal_str(&kw.value).as_deref() {
220                            Some("store_true") => store_true = true,
221                            _ => {
222                                return Err(format!(
223                                    "add_argument('{}'): only action=\"store_true\" is \
224                                     supported",
225                                    name
226                                )
227                                .into())
228                            }
229                        },
230                        other => {
231                            return Err(format!(
232                                "add_argument('{}'): keyword '{}' is not supported yet",
233                                name,
234                                other.unwrap_or("**kwargs")
235                            )
236                            .into())
237                        }
238                    }
239                }
240                let kind = if store_true {
241                    if kind.is_some() || default.is_some() {
242                        return Err(format!(
243                            "add_argument('{}'): store_true takes neither type nor \
244                             default",
245                            name
246                        )
247                        .into());
248                    }
249                    "StoreTrue"
250                } else {
251                    kind.unwrap_or("Str")
252                };
253                let is_positional = !name.starts_with('-');
254                if is_positional && default.is_some() {
255                    return Err(format!(
256                        "add_argument('{}'): defaults on positionals are not supported",
257                        name
258                    )
259                    .into());
260                }
261                if !is_positional && !store_true && default.is_none() {
262                    return Err(format!(
263                        "add_argument('{}'): a value-taking option needs default= (its \
264                         Python default None cannot inhabit a typed field)",
265                        name
266                    )
267                    .into());
268                }
269                specs.push(ArgparseSpec {
270                    name,
271                    kind,
272                    default,
273                    help,
274                });
275                skip.insert(i);
276            }
277            StatementType::Assign(assign) => {
278                if let ExprType::Call(call) = &assign.value {
279                    if call_on_parser(call) == Some("parse_args".into()) {
280                        if !call.args.is_empty() || !call.keywords.is_empty() {
281                            return Err("parse_args with arguments is not supported".into());
282                        }
283                        let [ExprType::Name(t)] = assign.targets.as_slice() else {
284                            return Err("parse_args must be assigned to a plain name".into());
285                        };
286                        parse = Some((i, t.id.clone()));
287                    } else if call_on_parser(call).is_some() {
288                        return Err(format!(
289                            "argparse parser `{}`: only add_argument and parse_args \
290                             are supported",
291                            pvar
292                        )
293                        .into());
294                    }
295                }
296            }
297            _ if stmt_call.is_some_and(|c| call_on_parser(c).is_some()) => {
298                return Err(format!(
299                    "argparse parser `{}`: only add_argument and parse_args are \
300                     supported",
301                    pvar
302                )
303                .into());
304            }
305            _ => {}
306        }
307    }
308    let Some((parse_index, args_var)) = parse else {
309        return Err("argparse.ArgumentParser built but parse_args() never assigned".into());
310    };
311    Ok(Some(ArgparseRewrite {
312        skip,
313        parse_index,
314        args_var,
315        prog,
316        description,
317        specs,
318    }))
319}
320
321/// Emit the parse_args replacement: a namespace struct typed from the
322/// specs, the run_parser call, and the destructuring assignment into
323/// the (hoisted) namespace variable.
324fn lower_parse_args(
325    rw: &ArgparseRewrite,
326    ctx: &CodeGenContext,
327    options: &PythonOptions,
328    symbols: &SymbolTableScopes,
329) -> Result<TokenStream, Box<dyn std::error::Error>> {
330    use quote::format_ident;
331    let mut fields = Vec::new();
332    let mut field_types = Vec::new();
333    let mut spec_tokens = Vec::new();
334    let mut accessors = Vec::new();
335    for spec in &rw.specs {
336        let dest = spec.name.trim_start_matches('-').replace('-', "_");
337        fields.push(crate::safe_ident(&dest));
338        let (fty, kind, accessor) = match spec.kind {
339            "Int" => (quote!(i64), quote!(Int), format_ident!("into_int")),
340            "Float" => (quote!(f64), quote!(Float), format_ident!("into_float")),
341            "StoreTrue" => (quote!(bool), quote!(StoreTrue), format_ident!("into_flag")),
342            _ => (quote!(String), quote!(Str), format_ident!("into_str")),
343        };
344        field_types.push(fty);
345        accessors.push(accessor);
346        let default = match &spec.default {
347            None => quote!(None),
348            Some(e) => {
349                let d = e
350                    .clone()
351                    .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
352                // Coerce literal defaults onto the declared type
353                // (default=1 with type=float is valid Python).
354                match spec.kind {
355                    "Int" => quote!(Some(argparse::ParsedValue::Int((#d) as i64))),
356                    "Float" => quote!(Some(argparse::ParsedValue::Float((#d) as f64))),
357                    _ => quote!(Some(argparse::ParsedValue::Str((#d).to_string()))),
358                }
359            }
360        };
361        let name = &spec.name;
362        let help = match &spec.help {
363            Some(h) => quote!(Some(#h)),
364            None => quote!(None),
365        };
366        spec_tokens.push(quote!(argparse::ArgSpec {
367            name: #name,
368            kind: argparse::ArgKind::#kind,
369            default: #default,
370            help: #help,
371        }));
372    }
373    let prog = match &rw.prog {
374        Some(p) => quote!(Some(#p)),
375        None => quote!(None),
376    };
377    let description = match &rw.description {
378        Some(d) => quote!(Some(#d)),
379        None => quote!(None),
380    };
381    let args_var = crate::safe_ident(&rw.args_var);
382    Ok(quote! {
383        #[allow(non_camel_case_types)]
384        struct __ArgparseArgs {
385            #(#fields: #field_types,)*
386        }
387        let mut __parsed = argparse::run_parser(
388            #prog,
389            #description,
390            &[#(#spec_tokens),*],
391        )?
392        .into_iter();
393        #args_var = __ArgparseArgs {
394            #(#fields: __parsed.next().expect("one value per spec").#accessors(),)*
395        }
396    })
397}
398
399/// The cache discipline a functools cache decorator asks for: None
400/// means uncached; Some(None) is unbounded (functools.cache or
401/// lru_cache(maxsize=None)); Some(Some(n)) is a bounded LRU (Python's
402/// bare @lru_cache default is 128). ANY other decorator is a loud
403/// error: silently ignoring a decorator converts a program into a
404/// different one.
405fn parse_cache_decorator(
406    decorators: &[ExprType],
407) -> Result<Option<Option<i64>>, Box<dyn std::error::Error>> {
408    let unsupported = |what: &str| -> Box<dyn std::error::Error> {
409        format!(
410            "decorator `{}` is not supported yet (only functools.lru_cache and \
411             functools.cache are); rython refuses to silently ignore it",
412            what
413        )
414        .into()
415    };
416    let name_of = |e: &ExprType| -> Option<String> {
417        match e {
418            ExprType::Name(n) => Some(n.id.clone()),
419            ExprType::Attribute(a) => match a.value.as_ref() {
420                ExprType::Name(m) if m.id == "functools" => Some(a.attr.clone()),
421                _ => None,
422            },
423            _ => None,
424        }
425    };
426    match decorators {
427        [] => Ok(None),
428        [single] => {
429            let (base, call) = match single {
430                ExprType::Call(c) => (name_of(c.func.as_ref()), Some(c)),
431                other => (name_of(other), None),
432            };
433            match (base.as_deref(), call) {
434                (Some("cache"), None) => Ok(Some(None)),
435                (Some("cache"), Some(c)) if c.args.is_empty() && c.keywords.is_empty() => {
436                    Ok(Some(None))
437                }
438                (Some("lru_cache"), None) => Ok(Some(Some(128))),
439                (Some("lru_cache"), Some(c)) => {
440                    let maxsize = match (c.args.as_slice(), c.keywords.as_slice()) {
441                        ([], []) => return Ok(Some(Some(128))),
442                        ([e], []) => e.clone(),
443                        ([], [kw]) if kw.arg.as_deref() == Some("maxsize") => {
444                            kw.value.clone()
445                        }
446                        _ => {
447                            return Err(
448                                "lru_cache() takes at most a single maxsize argument"
449                                    .to_string()
450                                    .into(),
451                            )
452                        }
453                    };
454                    if crate::is_none_expr(&maxsize) {
455                        return Ok(Some(None));
456                    }
457                    match &maxsize {
458                        ExprType::Constant(c) => match &c.0 {
459                            Some(litrs::Literal::Integer(i)) => {
460                                let n: i64 = i.value().ok_or("maxsize out of range")?;
461                                Ok(Some(Some(n)))
462                            }
463                            _ => Err("lru_cache maxsize must be an integer literal or None"
464                                .to_string()
465                                .into()),
466                        },
467                        _ => Err("lru_cache maxsize must be an integer literal or None"
468                            .to_string()
469                            .into()),
470                    }
471                }
472                _ => Err(unsupported(&format!("{:?}", single))),
473            }
474        }
475        many => Err(unsupported(&format!("{:?}", many[0]))),
476    }
477}
478
479impl CodeGen for FunctionDef {
480    type Context = CodeGenContext;
481    type Options = PythonOptions;
482    type SymbolTable = SymbolTableScopes;
483
484    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
485        let mut symbols = symbols;
486        symbols.insert(
487            self.name.clone(),
488            SymbolTableNode::FunctionDef(self.clone()),
489        );
490        symbols
491    }
492
493    fn to_rust(
494        self,
495        ctx: Self::Context,
496        options: Self::Options,
497        symbols: SymbolTableScopes,
498    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
499        let mut streams = TokenStream::new();
500        let fn_name = crate::safe_ident(&self.name);
501
502        // An argparse parser in the body is evaluated at conversion time:
503        // its statements vanish and parse_args becomes a typed struct.
504        let argparse_rewrite = scan_argparse(&self.body)?;
505        let effective_body: Vec<Statement> = match &argparse_rewrite {
506            None => self.body.clone(),
507            Some(rw) => self
508                .body
509                .iter()
510                .enumerate()
511                .filter(|(i, _)| !rw.skip.contains(i))
512                .map(|(_, s)| s.clone())
513                .collect(),
514        };
515        // The parse_args statement's position within the filtered body.
516        let argparse_parse_at: Option<usize> = argparse_rewrite.as_ref().map(|rw| {
517            (0..rw.parse_index)
518                .filter(|i| !rw.skip.contains(i))
519                .count()
520        });
521
522        // functools cache decorators rewrite the whole definition below;
523        // any OTHER decorator is a loud error (see parse_cache_decorator).
524        let cache_spec = parse_cache_decorator(&self.decorator_list)?;
525        if cache_spec.is_some() && options.no_std {
526            return Err(format!(
527                "@lru_cache on `{}` needs a global Mutex, which the no_std \
528                 profile does not provide",
529                self.name
530            )
531            .into());
532        }
533
534        // The Python convention is that functions that begin with a single underscore,
535        // it's private. Otherwise, it's public. We formalize that by default.
536        let visibility = if self.name.starts_with("_") && !self.name.starts_with("__") {
537            quote!()  // private, no visibility modifier
538        } else if self.name.starts_with("__") && self.name.ends_with("__") {
539            quote!(pub(crate))  // dunder methods are crate-visible
540        } else {
541            quote!(pub)  // regular methods are public
542        };
543
544        // A nested function body is a fresh exception scope: a `raise` in it
545        // cannot return out of an enclosing try block's closure.
546        let ctx = ctx.strip_exception_scopes();
547
548        let is_async = match ctx.clone() {
549            CodeGenContext::Async(_) => {
550                quote!(async)
551            }
552            _ => quote!(),
553        };
554
555        // Local assignments participate in name resolution for the body:
556        // `p = Point(...)` makes `p`'s class known to method-call lowering.
557        let mut symbols = symbols;
558        for s in &self.body {
559            symbols = s.clone().find_symbols(symbols);
560        }
561
562        // A `def` in a class body whose first parameter is `self` is a
563        // method: `self` becomes the Rust receiver instead of a parameter —
564        // `&mut self` when the method stores through `self`, directly or by
565        // calling another method of the class that does.
566        let is_method = matches!(&ctx, CodeGenContext::Class(_))
567            && self
568                .args
569                .posonlyargs
570                .first()
571                .or(self.args.args.first())
572                .is_some_and(|p| p.arg == "self");
573        let mut render_args = self.args.clone();
574        let method_mutates_self = is_method
575            && match &ctx {
576                CodeGenContext::Class(class_name) => match symbols.get(class_name) {
577                    Some(crate::SymbolTableNode::ClassDef(c)) => {
578                        c.method_needs_mut_self(&self.name, &symbols)
579                    }
580                    _ => false,
581                },
582                _ => false,
583            };
584        if is_method {
585            crate::strip_self(&mut render_args);
586        }
587
588        let parameters = render_args
589            .clone()
590            .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
591
592        // A cached function's arguments form the cache KEY, so every
593        // parameter needs a hashable, nameable type: int, bool, or str
594        // (floats are not hashable in Rust — Python would cache them,
595        // which rython cannot reproduce, so it refuses loudly).
596        let cache_key: Option<Vec<(proc_macro2::Ident, TokenStream)>> = match cache_spec {
597            None => None,
598            Some(_) => {
599                if is_method {
600                    return Err(format!(
601                        "@lru_cache on method `{}` is not supported yet",
602                        self.name
603                    )
604                    .into());
605                }
606                if !self.args.posonlyargs.is_empty()
607                    || !self.args.kwonlyargs.is_empty()
608                    || self.args.vararg.is_some()
609                    || self.args.kwarg.is_some()
610                {
611                    return Err(format!(
612                        "@lru_cache on `{}`: only plain positional parameters are \
613                         supported",
614                        self.name
615                    )
616                    .into());
617                }
618                let mut key = Vec::new();
619                for p in &self.args.args {
620                    let ty = match p.annotation.as_deref() {
621                        Some(ExprType::Name(n)) if n.id == "int" => quote!(i64),
622                        Some(ExprType::Name(n)) if n.id == "bool" => quote!(bool),
623                        Some(ExprType::Name(n)) if n.id == "str" => quote!(String),
624                        _ => {
625                            return Err(format!(
626                                "@lru_cache on `{}`: parameter `{}` must be annotated \
627                                 int, bool, or str (the arguments form the cache key)",
628                                self.name, p.arg
629                            )
630                            .into());
631                        }
632                    };
633                    key.push((crate::safe_ident(&p.arg), ty));
634                }
635                Some(key)
636            }
637        };
638
639        // Python variables are function-scoped: hoist every assigned name to
640        // a declaration here so assignments inside nested blocks (if/loop/
641        // try bodies) store into the same variable instead of creating a
642        // shadowing binding. Scope analysis decides which declarations need
643        // `mut` (mirroring rustc's rules, so the generated code carries
644        // neither unused_mut warnings nor missing-mut errors), and which
645        // parameters must be rebound as mutable locals (Rust parameters are
646        // immutable; Python's are ordinary variables).
647        let mut param_names: Vec<String> = render_args
648            .args
649            .iter()
650            .chain(render_args.posonlyargs.iter())
651            .chain(render_args.kwonlyargs.iter())
652            .map(|p| p.arg.clone())
653            .chain(render_args.vararg.iter().map(|p| p.arg.clone()))
654            .chain(render_args.kwarg.iter().map(|p| p.arg.clone()))
655            .collect();
656        if is_method {
657            // The receiver is initialized like a parameter, but it is never
658            // rebound (`let mut self = self` is not legal Rust); its
659            // mutations select `&mut self` above instead.
660            param_names.push("self".to_string());
661        }
662        // The resolver makes class knowledge authoritative for method
663        // calls: `c.bump()` marks `c` mutable when bump takes &mut self,
664        // and a read-only user method shadowing a builtin mutator name
665        // (`pop`, `update`, ...) does NOT force a spurious `mut`.
666        let scope = crate::analyze_scope_with(
667            &effective_body,
668            &param_names,
669            &crate::class_call_resolver(&ctx, &symbols),
670        );
671        if is_method {
672            param_names.pop();
673        }
674        let receiver = if is_method {
675            if method_mutates_self || scope.needs_mut.contains("self") {
676                quote!(&mut self,)
677            } else {
678                quote!(&self,)
679            }
680        } else {
681            quote!()
682        };
683        // Optional names (assigned None on some path, or parameters with an
684        // Optional annotation) are visible to every assignment in the body:
685        // their non-None stores wrap in Some.
686        let mut options = options;
687        {
688            let mut optional = scope.optional.clone();
689            for p in self
690                .args
691                .posonlyargs
692                .iter()
693                .chain(self.args.args.iter())
694                .chain(self.args.kwonlyargs.iter())
695            {
696                if let Some(ann) = p.annotation.as_deref() {
697                    if crate::is_optional_annotation(ann) {
698                        optional.insert(p.arg.clone());
699                    }
700                }
701            }
702            options.optional_names = std::rc::Rc::new(optional);
703            options.clone_str_attribute_returns =
704                matches!(self.returns.as_deref(), Some(ExprType::Name(n)) if n.id == "str");
705        }
706        // Statically-known local types for isinstance(): parameter
707        // annotations plus literal assignments, as Python type names.
708        {
709            let mut known: std::collections::HashMap<String, String> =
710                std::collections::HashMap::new();
711            for param in self
712                .args
713                .args
714                .iter()
715                .chain(self.args.posonlyargs.iter())
716                .chain(self.args.kwonlyargs.iter())
717            {
718                if let Some(ExprType::Name(ann)) = param.annotation.as_deref() {
719                    if matches!(ann.id.as_str(), "int" | "float" | "str" | "bool") {
720                        known.insert(param.arg.clone(), ann.id.clone());
721                    }
722                }
723            }
724            let mut literal_types = std::collections::HashMap::new();
725            collect_local_types(&self.body, &mut literal_types);
726            for (name, ty) in literal_types {
727                let py = match ty.to_string().as_str() {
728                    "i64" => "int",
729                    "f64" => "float",
730                    "bool" => "bool",
731                    s if s.contains("str") || s.contains("String") => "str",
732                    _ => continue,
733                };
734                // A literal assignment overrides nothing: annotations win.
735                known.entry(name).or_insert_with(|| py.to_string());
736            }
737            options.local_types = std::rc::Rc::new(known);
738        }
739        // str parameters arrive as impl Into<String>; convert them to owned
740        // Strings up front so the body works with a concrete type.
741        let str_params: std::collections::HashSet<&str> = self
742            .args
743            .args
744            .iter()
745            .chain(self.args.posonlyargs.iter())
746            .chain(self.args.kwonlyargs.iter())
747            .filter(|p| {
748                matches!(
749                    p.annotation.as_deref(),
750                    Some(ExprType::Name(n)) if n.id == "str"
751                )
752            })
753            .map(|p| p.arg.as_str())
754            .collect();
755        let mut streams_prologue = TokenStream::new();
756        for name in &param_names {
757            let ident = crate::safe_ident(name);
758            if str_params.contains(name.as_str()) {
759                if scope.needs_mut.contains(name) {
760                    streams_prologue.extend(quote!(let mut #ident: String = #ident.into();));
761                } else {
762                    streams_prologue.extend(quote!(let #ident: String = #ident.into();));
763                }
764            } else if scope.needs_mut.contains(name) {
765                streams_prologue.extend(quote!(let mut #ident = #ident;));
766            }
767        }
768        for name in &scope.assigned {
769            let ident = crate::safe_ident(name);
770            if scope.needs_mut.contains(name) {
771                streams_prologue.extend(quote!(let mut #ident;));
772            } else {
773                streams_prologue.extend(quote!(let #ident;));
774            }
775        }
776        streams.extend(streams_prologue);
777
778        // A leading docstring is emitted as doc comments below; skip it here
779        // so it isn't also emitted as a statement.
780        let body_start = if self.get_docstring().is_some() { 1 } else { 0 };
781        for (i, s) in effective_body.iter().enumerate().skip(body_start) {
782            if Some(i) == argparse_parse_at {
783                let rw = argparse_rewrite.as_ref().expect("index implies rewrite");
784                streams.extend(lower_parse_args(
785                    rw,
786                    &ctx,
787                    &options,
788                    &symbols,
789                )?);
790                streams.extend(quote!(;));
791                continue;
792            }
793            streams.extend(
794                s.clone()
795                    .to_rust(ctx.clone(), options.clone(), symbols.clone())?,
796            );
797            streams.extend(quote!(;));
798        }
799
800        // Every generated function returns Result<T, PyException> so raised
801        // exceptions propagate across function boundaries the way Python's
802        // do: call sites append `?`, and an uncaught exception surfaces at
803        // the entry point. T is the resolved Python return type (unit when
804        // there is none).
805        let return_type = match self.resolved_return_type() {
806            Some(ty) => quote!(-> Result<#ty, PyException>),
807            None => quote!(-> Result<(), PyException>),
808        };
809
810        // A body that can fall off the end implicitly returns None: give the
811        // generated block an Ok(()) tail. Bodies that return (or raise) on
812        // every path end with `return`/`return Err`, which need no tail.
813        if !guarantees_return(&self.body) {
814            streams.extend(quote!(Ok(())));
815        }
816
817        // A cached function wraps its ORIGINAL body in an inner fn: the
818        // outer fn consults a static LRU keyed on the argument tuple,
819        // computes through the inner fn on a miss, and stores the clone.
820        // Recursive calls in the body resolve to the OUTER item, so
821        // recursion hits the cache, exactly like Python's wrapper.
822        let streams = if let (Some(maxsize), Some(key)) = (cache_spec, cache_key.as_ref()) {
823            let maxsize_tokens = match maxsize {
824                None => quote!(None),
825                Some(n) => quote!(Some(#n as usize)),
826            };
827            let key_types: Vec<&TokenStream> = key.iter().map(|(_, t)| t).collect();
828            let key_names: Vec<&proc_macro2::Ident> = key.iter().map(|(n, _)| n).collect();
829            let ret = match self.resolved_return_type() {
830                Some(ty) => quote!(#ty),
831                None => quote!(()),
832            };
833            // str parameters arrive as impl Into<String>; normalize them
834            // before building the key (the inner fn takes concrete String).
835            let mut outer_rebinds = TokenStream::new();
836            for (p, (name, _)) in self.args.args.iter().zip(key.iter()) {
837                if matches!(p.annotation.as_deref(), Some(ExprType::Name(n)) if n.id == "str")
838                {
839                    outer_rebinds.extend(quote!(let #name: String = #name.into();));
840                }
841            }
842            quote! {
843                #outer_rebinds
844                static __LRU_CACHE: std::sync::LazyLock<
845                    std::sync::Mutex<PyLruCache<(#(#key_types,)*), #ret>>,
846                > = std::sync::LazyLock::new(|| {
847                    std::sync::Mutex::new(PyLruCache::new(#maxsize_tokens))
848                });
849                if let Some(__hit) = __LRU_CACHE
850                    .lock()
851                    .expect("lru_cache mutex poisoned")
852                    .get(&(#((#key_names).clone(),)*))
853                {
854                    return Ok(__hit);
855                }
856                #[allow(non_snake_case)]
857                fn __lru_uncached(#(#key_names: #key_types),*) -> Result<#ret, PyException> {
858                    #streams
859                }
860                let __lru_value = __lru_uncached(#((#key_names).clone()),*)?;
861                __LRU_CACHE
862                    .lock()
863                    .expect("lru_cache mutex poisoned")
864                    .put((#((#key_names).clone(),)*), __lru_value.clone());
865                Ok(__lru_value)
866            }
867        } else {
868            streams
869        };
870
871        // Lossy conversions are silent semantic changes callers may not want
872        // — surface them as a compiler warning at every call site outside the
873        // generated crate via a single #[deprecated] note (the standard
874        // mechanism for user-defined warnings). An item can carry only one
875        // #[deprecated] attribute, so all notes are folded into it.
876        let lossy_warning = if options.lossy_warnings {
877            let notes = self.lossy_conversion_notes();
878            if notes.is_empty() {
879                quote!()
880            } else {
881                let note = notes.join("; ");
882                quote!(#[deprecated(note = #note)])
883            }
884        } else {
885            quote!()
886        };
887
888        let function = if let Some(docstring) = self.get_docstring() {
889            // Convert docstring to Rust doc comments
890            let doc_lines: Vec<_> = docstring
891                .lines()
892                .map(|line| {
893                    if line.trim().is_empty() {
894                        quote! { #[doc = ""] }
895                    } else {
896                        let doc_line = format!("{}", line);
897                        quote! { #[doc = #doc_line] }
898                    }
899                })
900                .collect();
901
902            quote! {
903                #(#doc_lines)*
904                #lossy_warning
905                #visibility #is_async fn #fn_name(#receiver #parameters) #return_type {
906                    #streams
907                }
908            }
909        } else {
910            quote! {
911                #lossy_warning
912                #visibility #is_async fn #fn_name(#receiver #parameters) #return_type {
913                    #streams
914                }
915            }
916        };
917
918        debug!("function: {}", function);
919        Ok(function)
920    }
921}
922
923/// Collect every `return` statement's value (None for a bare `return`)
924/// from a statement list, recursing into nested control-flow bodies but not
925/// into nested function or class definitions.
926fn collect_returns<'a>(body: &'a [Statement], out: &mut Vec<Option<&'a ExprType>>) {
927    for stmt in body {
928        match &stmt.statement {
929            StatementType::Return(value) => {
930                out.push(value.as_ref().map(|e| &e.value));
931            }
932            StatementType::If(s) => {
933                collect_returns(&s.body, out);
934                collect_returns(&s.orelse, out);
935            }
936            StatementType::For(s) => {
937                collect_returns(&s.body, out);
938                collect_returns(&s.orelse, out);
939            }
940            StatementType::While(s) => {
941                collect_returns(&s.body, out);
942                collect_returns(&s.orelse, out);
943            }
944            StatementType::With(s) => collect_returns(&s.body, out),
945            StatementType::AsyncWith(s) => collect_returns(&s.body, out),
946            StatementType::AsyncFor(s) => collect_returns(&s.body, out),
947            StatementType::Try(s) => {
948                collect_returns(&s.body, out);
949                for handler in &s.handlers {
950                    collect_returns(&handler.body, out);
951                }
952                collect_returns(&s.orelse, out);
953                collect_returns(&s.finalbody, out);
954            }
955            // Nested defs/classes have their own return scopes; everything
956            // else contains no return statements we care about.
957            _ => {}
958        }
959    }
960}
961
962/// Map an expression to an obviously-inferable Rust type, if any.
963pub(crate) fn simple_expr_type(expr: &ExprType) -> Option<TokenStream> {
964    match expr {
965        ExprType::Constant(c) => match &c.0 {
966            Some(litrs::Literal::Integer(_)) => Some(quote!(i64)),
967            Some(litrs::Literal::Float(_)) => Some(quote!(f64)),
968            Some(litrs::Literal::Bool(_)) => Some(quote!(bool)),
969            // A string constant lowers to a &'static str literal.
970            Some(litrs::Literal::String(_)) => Some(quote!(&'static str)),
971            _ => None,
972        },
973        ExprType::JoinedStr(_) => Some(quote!(String)),
974        _ => None,
975    }
976}
977
978/// Collect `name = <simply-typed constant>` assignments (recursing into
979/// control-flow bodies) so returns of those names can be inferred too.
980pub(crate) fn collect_local_types(
981    body: &[Statement],
982    out: &mut std::collections::HashMap<String, TokenStream>,
983) {
984    for stmt in body {
985        match &stmt.statement {
986            StatementType::Assign(assign) => {
987                if let [ExprType::Name(name)] = assign.targets.as_slice() {
988                    if let Some(ty) = simple_expr_type(&assign.value) {
989                        out.insert(name.id.clone(), ty);
990                    }
991                }
992            }
993            StatementType::If(s) => {
994                collect_local_types(&s.body, out);
995                collect_local_types(&s.orelse, out);
996            }
997            StatementType::For(s) => {
998                collect_local_types(&s.body, out);
999                collect_local_types(&s.orelse, out);
1000            }
1001            StatementType::While(s) => {
1002                collect_local_types(&s.body, out);
1003                collect_local_types(&s.orelse, out);
1004            }
1005            StatementType::With(s) => collect_local_types(&s.body, out),
1006            _ => {}
1007        }
1008    }
1009}
1010
1011/// Whether an annotation expression means `None` (`-> None` marks a
1012/// procedure): the parser may surface it as the NoneType variant, a
1013/// valueless constant, or the bare name `None`.
1014pub(crate) fn is_none_expr(ann: &ExprType) -> bool {
1015    match ann {
1016        ExprType::NoneType(_) => true,
1017        ExprType::Constant(c) => c.0.is_none(),
1018        ExprType::Name(name) => name.id == "None",
1019        _ => false,
1020    }
1021}
1022
1023/// Whether an expression already lowers to an `Option` value, so a store
1024/// into an optional-tracked name (or an Optional parameter slot) must NOT
1025/// wrap it in `Some` — double-wrapping turns an absent value into
1026/// `Some(None)`, and a later `is None` check silently answers wrongly.
1027pub(crate) fn expr_yields_option(
1028    expr: &ExprType,
1029    options: &PythonOptions,
1030    symbols: &SymbolTableScopes,
1031) -> bool {
1032    match expr {
1033        // A name that itself holds an Option (assigned None on some path,
1034        // or an Optional-annotated parameter).
1035        ExprType::Name(name) => options.optional_names.contains(&name.id),
1036        ExprType::Call(call) => match call.func.as_ref() {
1037            // dict.get(k) lowers to py_get, which returns Option<V>.
1038            ExprType::Attribute(attr) => attr.attr == "get" && call.args.len() == 1,
1039            // A user function annotated `-> Optional[T]` generates
1040            // `Result<Option<T>, PyException>`; the call site's `?` strips
1041            // only the Result layer, leaving an Option.
1042            ExprType::Name(name) => match symbols.get(&name.id) {
1043                Some(SymbolTableNode::FunctionDef(f)) => f
1044                    .returns
1045                    .as_deref()
1046                    .is_some_and(crate::is_optional_annotation),
1047                _ => false,
1048            },
1049            _ => false,
1050        },
1051        // A conditional yields an Option when either arm does (None counts):
1052        // the arms unify to one type, so an Option arm makes the whole
1053        // expression an Option. A plain-vs-Option mix fails to compile —
1054        // loud, never silent.
1055        ExprType::IfExp(e) => {
1056            let arm = |x: &ExprType| {
1057                crate::is_none_expr(x) || expr_yields_option(x, options, symbols)
1058            };
1059            arm(&e.body) || arm(&e.orelse)
1060        }
1061        _ => false,
1062    }
1063}
1064
1065/// Lower an expression destined for an Option slot (a store into an
1066/// optional-tracked name, or an Optional-annotated parameter): values that
1067/// already yield an Option (and None itself) pass through, plain values
1068/// wrap in `Some`, and conditionals wrap each arm independently — so
1069/// `x if c else None` becomes `if c { Some(x) } else { None }` instead of
1070/// burying the None arm inside `Some(...)`.
1071pub(crate) fn lower_optional_value(
1072    expr: &ExprType,
1073    ctx: CodeGenContext,
1074    options: PythonOptions,
1075    symbols: SymbolTableScopes,
1076) -> Result<TokenStream, Box<dyn std::error::Error>> {
1077    // Conditionals recurse per arm FIRST: even when one arm makes the whole
1078    // expression Option-typed (e.g. an `else None`), the other arm may be a
1079    // plain value that still needs its Some wrap.
1080    if let ExprType::IfExp(e) = expr {
1081        let test =
1082            crate::condition_to_rust(&e.test, ctx.clone(), options.clone(), symbols.clone())?;
1083        let body = lower_optional_value(&e.body, ctx.clone(), options.clone(), symbols.clone())?;
1084        let orelse = lower_optional_value(&e.orelse, ctx, options, symbols)?;
1085        return Ok(quote!(if #test { #body } else { #orelse }));
1086    }
1087    if is_none_expr(expr) || expr_yields_option(expr, &options, &symbols) {
1088        return expr.clone().to_rust(ctx, options, symbols);
1089    }
1090    let tokens = expr.clone().to_rust(ctx, options, symbols)?;
1091    Ok(quote!(Some(#tokens)))
1092}
1093
1094/// Best-effort Python-source rendering of an annotation expression, for
1095/// warning messages.
1096fn annotation_display(ann: &ExprType) -> String {
1097    match ann {
1098        ExprType::Name(name) => name.id.clone(),
1099        ExprType::Constant(c) => c.to_string(),
1100        _ => "<annotation>".to_string(),
1101    }
1102}
1103
1104/// Whether a statement list is guaranteed to return a value on every
1105/// control-flow path: its final statement is a `return <value>`, an
1106/// `if`/`else` whose branches both guarantee a return, or a diverging
1107/// `raise`. Loops and other constructs may fall through, so they never
1108/// guarantee a return.
1109pub(crate) fn guarantees_return(body: &[Statement]) -> bool {
1110    match body.last().map(|stmt| &stmt.statement) {
1111        Some(StatementType::Return(Some(_))) => true,
1112        Some(StatementType::If(s)) => {
1113            !s.orelse.is_empty() && guarantees_return(&s.body) && guarantees_return(&s.orelse)
1114        }
1115        // `raise` lowers to `return Err(...)`, which terminates the path.
1116        Some(StatementType::Raise(_)) => true,
1117        // A try guarantees a return when its no-exception path does (the
1118        // body, or the else clause the body falls into) and every handler
1119        // does too — or when the finally clause returns unconditionally.
1120        // Unhandled exceptions exit via Err, which also terminates.
1121        Some(StatementType::Try(t)) => {
1122            let normal_path = if t.orelse.is_empty() {
1123                guarantees_return(&t.body)
1124            } else {
1125                guarantees_return(&t.body) || guarantees_return(&t.orelse)
1126            };
1127            let handlers = t.handlers.iter().all(|h| guarantees_return(&h.body));
1128            (normal_path && handlers) || guarantees_return(&t.finalbody)
1129        }
1130        _ => false,
1131    }
1132}
1133
1134impl FunctionDef {
1135    /// The return type the generated Rust function actually carries, if any.
1136    ///
1137    /// Inference from the body comes first (it reflects the type the body
1138    /// actually produces — e.g. a string literal is a &'static str even
1139    /// under a `-> str` annotation); an explicit annotation with a known
1140    /// Rust mapping is the fallback for bodies inference can't see through.
1141    /// Both require the body to return on every path: a fall-through path
1142    /// yields `()`, which no concrete annotation can type. `-> None` and
1143    /// unmappable annotations yield None.
1144    ///
1145    /// Tools generating call-through code (e.g. PyO3 wrappers) must use this
1146    /// same method so their signatures match the generated function.
1147    pub fn resolved_return_type(&self) -> Option<TokenStream> {
1148        let annotated = if guarantees_return(&self.body) {
1149            self.returns.as_deref().and_then(|ann| {
1150                if is_none_expr(ann) {
1151                    None
1152                } else {
1153                    crate::python_annotation_to_rust_type(ann)
1154                }
1155            })
1156        } else {
1157            None
1158        };
1159        self.inferred_return_type().or(annotated)
1160    }
1161
1162    /// The Python-source text of a return annotation the generated function
1163    /// does not honor: the body can fall through (implicitly returning
1164    /// None), so the generated function returns `()` no matter what the
1165    /// annotation claims. This frequently marks a bug in the Python source
1166    /// — the author declared a return type but not every path returns one —
1167    /// so it must be surfaced, not silently reproduced.
1168    pub fn ignored_return_annotation(&self) -> Option<String> {
1169        let ann = self.returns.as_deref()?;
1170        if is_none_expr(ann) || guarantees_return(&self.body) {
1171            return None;
1172        }
1173        Some(annotation_display(ann))
1174    }
1175
1176    /// Human-readable notes for every lossy conversion this function's
1177    /// signature underwent. These become the #[deprecated] note on the
1178    /// generated function, and conversion tools report them to the user.
1179    pub fn lossy_conversion_notes(&self) -> Vec<String> {
1180        let mut notes = Vec::new();
1181        let dropped = self.dropped_default_parameters();
1182        if !dropped.is_empty() {
1183            notes.push(format!(
1184                "rython: Python default value(s) for parameter(s) `{}` were dropped \
1185                 (Rust has no default arguments); every argument must be passed explicitly",
1186                dropped.join("`, `")
1187            ));
1188        }
1189        if let Some(ann) = self.ignored_return_annotation() {
1190            notes.push(format!(
1191                "rython: the `-> {}` return annotation was ignored because the function \
1192                 body does not return a value on every path; the generated function \
1193                 returns `()` where Python would implicitly return None",
1194                ann
1195            ));
1196        }
1197        notes
1198    }
1199
1200    /// Names of parameters whose Python default values cannot be carried
1201    /// into the generated Rust signature (Rust has no default arguments).
1202    /// Used to attach a call-site warning to the generated function and to
1203    /// let tools report the loss during conversion.
1204    pub fn dropped_default_parameters(&self) -> Vec<String> {
1205        let mut dropped = Vec::new();
1206        let defaults_offset = self
1207            .args
1208            .args
1209            .len()
1210            .saturating_sub(self.args.defaults.len());
1211        for arg in &self.args.args[defaults_offset..] {
1212            dropped.push(arg.arg.clone());
1213        }
1214        for (i, arg) in self.args.kwonlyargs.iter().enumerate() {
1215            if self.args.kw_defaults.get(i).is_some_and(Option::is_some) {
1216                dropped.push(arg.arg.clone());
1217            }
1218        }
1219        dropped
1220    }
1221
1222    /// Infer a return type when the function is guaranteed to return on
1223    /// every control-flow path AND every return value in the body maps to
1224    /// the same simple type — either directly (a constant or f-string) or
1225    /// via a local variable assigned a constant. Partial/conditional
1226    /// returns (which implicitly return None on the fall-through path),
1227    /// mixed types, and uninferable values all yield None so the function
1228    /// stays unannotated, as before.
1229    pub fn inferred_return_type(&self) -> Option<TokenStream> {
1230        // A function that can fall off the end must not get a concrete
1231        // return annotation: the implicit tail is `()`.
1232        if !guarantees_return(&self.body) {
1233            return None;
1234        }
1235
1236        let mut returns = Vec::new();
1237        collect_returns(&self.body, &mut returns);
1238
1239        let mut locals = std::collections::HashMap::new();
1240        collect_local_types(&self.body, &mut locals);
1241
1242        let mut inferred: Option<TokenStream> = None;
1243        for ret in &returns {
1244            let value = (*ret)?; // a bare `return` means the type is unit
1245            let ty = match value {
1246                ExprType::Name(name) => locals.get(&name.id)?.clone(),
1247                other => simple_expr_type(other)?,
1248            };
1249            match &inferred {
1250                None => inferred = Some(ty),
1251                Some(prev) if prev.to_string() == ty.to_string() => {}
1252                _ => return None,
1253            }
1254        }
1255        inferred
1256    }
1257}
1258
1259impl FunctionDef {
1260    fn get_docstring(&self) -> Option<String> {
1261        if self.body.is_empty() {
1262            return None;
1263        }
1264        
1265        let expr = self.body[0].clone();
1266        match expr.statement {
1267            StatementType::Expr(e) => match e.value {
1268                ExprType::Constant(c) => {
1269                    let raw_string = c.to_string();
1270                    // Clean up the docstring for Rust documentation
1271                    Some(self.format_docstring(&raw_string))
1272                },
1273                _ => None,
1274            },
1275            _ => None,
1276        }
1277    }
1278    
1279    fn format_docstring(&self, raw: &str) -> String {
1280        // Remove surrounding quotes
1281        let content = raw.trim_matches('"');
1282        
1283        // Split into lines and clean up Python-style indentation
1284        let lines: Vec<&str> = content.lines().collect();
1285        if lines.is_empty() {
1286            return String::new();
1287        }
1288        
1289        // First line is usually the summary
1290        let mut formatted = vec![lines[0].trim().to_string()];
1291        
1292        if lines.len() > 1 {
1293            // Add empty line after summary if there are more lines
1294            if !lines[0].trim().is_empty() && !lines[1].trim().is_empty() {
1295                formatted.push(String::new());
1296            }
1297            
1298            // Process remaining lines, cleaning up indentation
1299            for line in lines.iter().skip(1) {
1300                let cleaned = line.trim();
1301                if cleaned.starts_with("Args:") {
1302                    formatted.push(String::new());
1303                    formatted.push("# Arguments".to_string());
1304                } else if cleaned.starts_with("Returns:") {
1305                    formatted.push(String::new());
1306                    formatted.push("# Returns".to_string());
1307                } else if cleaned.starts_with("Example:") {
1308                    formatted.push(String::new());
1309                    formatted.push("# Examples".to_string());
1310                } else if cleaned.starts_with(">>>") {
1311                    // Convert Python examples to Rust doc test format
1312                    formatted.push(format!("```rust"));
1313                    formatted.push(format!("// {}", cleaned));
1314                } else if !cleaned.is_empty() {
1315                    formatted.push(cleaned.to_string());
1316                }
1317            }
1318            
1319            // Close any open code blocks
1320            if content.contains(">>>") {
1321                formatted.push("```".to_string());
1322            }
1323        }
1324        
1325        formatted.join("\n")
1326    }
1327}
1328
1329impl Object for FunctionDef {}