Skip to main content

python_ast/ast/tree/
call.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, FromPyObject, PyAny, PyResult};
3use quote::{format_ident, quote};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    extract_required_attr, CodeGen, CodeGenContext, ExprType, Keyword, PythonOptions,
8    SymbolTableNode, SymbolTableScopes,
9};
10
11#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
12pub struct Call {
13    pub func: Box<ExprType>,
14    pub args: Vec<ExprType>,
15    pub keywords: Vec<Keyword>,
16}
17
18impl<'a, 'py> FromPyObject<'a, 'py> for Call {
19    type Error = pyo3::PyErr;
20    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
21        let func: ExprType = extract_required_attr(&ob, "func", "function call expression")?;
22        let args: Vec<ExprType> = extract_required_attr(&ob, "args", "function call arguments")?;
23        let keywords: Vec<Keyword> = extract_required_attr(&ob, "keywords", "function call keywords")?;
24        
25        Ok(Call {
26            func: Box::new(func),
27            args,
28            keywords,
29        })
30    }
31}
32
33/// Is this callee functools.partial — via `from functools import
34/// partial` (the symbol table maps the bare name to the functools
35/// import) or the `functools.partial` attribute spelling? A user
36/// function named partial shadows the import in the symbol table and
37/// does not match.
38fn is_partial_target(func: &ExprType, symbols: &SymbolTableScopes) -> bool {
39    match func {
40        ExprType::Name(n) => {
41            n.id == "partial"
42                && matches!(
43                    symbols.get(&n.id),
44                    Some(SymbolTableNode::ImportFrom(i)) if i.module == "functools"
45                )
46        }
47        ExprType::Attribute(attr) => {
48            attr.attr == "partial"
49                && matches!(attr.value.as_ref(), ExprType::Name(m) if m.id == "functools")
50        }
51        _ => false,
52    }
53}
54
55impl<'a> CodeGen for Call {
56    type Context = CodeGenContext;
57    type Options = PythonOptions;
58    type SymbolTable = SymbolTableScopes;
59
60    fn to_rust(
61        self,
62        ctx: Self::Context,
63        options: Self::Options,
64        symbols: Self::SymbolTable,
65    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
66        // Calls to functions that return Result<T, PyException> get `?` so
67        // exceptions propagate to the caller (or an enclosing try block),
68        // as in Python: user-defined functions (known from the symbol
69        // table), names imported from user modules, and the Result-returning
70        // stdpython builtins.
71        let propagates_exceptions = match self.func.as_ref() {
72            ExprType::Name(name) => {
73                matches!(name.id.as_str(), "int" | "float")
74                    || match symbols.get(&name.id) {
75                        Some(SymbolTableNode::FunctionDef(_)) => true,
76                        Some(SymbolTableNode::ImportFrom(import)) => {
77                            let root = import.module.split('.').next().unwrap_or("");
78                            !crate::is_stdpython_module(root)
79                        }
80                        // A name bound to functools.partial(f, ...) is a
81                        // closure returning f's Result: propagate.
82                        Some(SymbolTableNode::Assign { value: ExprType::Call(c), .. }) => {
83                            is_partial_target(c.func.as_ref(), &symbols)
84                        }
85                        _ => false,
86                    }
87            }
88            _ => false,
89        };
90
91        // The I/O builtins have no no_std lowering (stdpython gates them
92        // behind std): fail at conversion time with the reason, rather than
93        // let the generated crate fail with a bare unresolved-name error. A
94        // user definition of the same name shadows the builtin as usual.
95        if options.no_std {
96            if let ExprType::Name(n) = self.func.as_ref() {
97                if matches!(n.id.as_str(), "print" | "input" | "open")
98                    && symbols.get(&n.id).is_none()
99                {
100                    return Err(format!(
101                        "`{}()` requires OS I/O, which the no_std profile does not \
102                         provide; remove the call or convert without the no_std \
103                         profile",
104                        n.id
105                    )
106                    .into());
107                }
108            }
109        }
110
111        // Multi-argument range() maps to the arity-specific runtime
112        // functions (Rust has no overloading); the 3-argument form can
113        // raise ValueError on a zero step, hence `?`. A user-defined
114        // `range` shadows the builtin and skips this mapping.
115        if let ExprType::Name(n) = self.func.as_ref() {
116            if n.id == "range"
117                && symbols.get("range").is_none()
118                && self.keywords.is_empty()
119                && matches!(self.args.len(), 2 | 3)
120            {
121                let mut rendered = Vec::new();
122                for arg in &self.args {
123                    rendered.push(arg.clone().to_rust(
124                        ctx.clone(),
125                        options.clone(),
126                        symbols.clone(),
127                    )?);
128                }
129                return Ok(if rendered.len() == 2 {
130                    let (a, b) = (&rendered[0], &rendered[1]);
131                    quote!(range_start_stop(#a, #b))
132                } else {
133                    let (a, b, c) = (&rendered[0], &rendered[1], &rendered[2]);
134                    quote!(range_start_stop_step(#a, #b, #c)?)
135                });
136            }
137        }
138
139        // Builtins with keyword variants or by-reference runtime shapes:
140        // min/max (key=, default=, n-ary), sorted (key=, reverse=),
141        // enumerate (start=), pow (3-arg modular), and the by-reference
142        // len/repr/reversed. Each spelling maps to its runtime variant; a
143        // user definition of the same name shadows the builtin, and
144        // unknown or duplicate keywords are loud errors, as Python raises
145        // TypeError for them.
146        if let ExprType::Name(n) = self.func.as_ref() {
147            let bname = n.id.as_str();
148            if matches!(
149                bname,
150                "min" | "max" | "sorted" | "enumerate" | "pow" | "len" | "repr"
151                    | "reversed" | "frozenset" | "map" | "filter" | "list"
152                    | "isinstance" | "hash" | "print" | "open"
153            ) && symbols.get(bname).is_none()
154            {
155                let mut rendered = Vec::new();
156                for arg in &self.args {
157                    rendered.push(arg.clone().to_rust(
158                        ctx.clone(),
159                        options.clone(),
160                        symbols.clone(),
161                    )?);
162                }
163                let unexpected = |kw: Option<&str>| -> Box<dyn std::error::Error> {
164                    format!(
165                        "{}() got an unexpected or duplicate keyword argument '{}'",
166                        bname,
167                        kw.unwrap_or("**kwargs")
168                    )
169                    .into()
170                };
171                match bname {
172                    "min" | "max" => {
173                        let mut key = None;
174                        let mut default = None;
175                        for kw in &self.keywords {
176                            match kw.arg.as_deref() {
177                                Some("key") if key.is_none() => key = Some(kw.value.clone()),
178                                Some("default") if default.is_none() => {
179                                    default = Some(kw.value.clone())
180                                }
181                                other => return Err(unexpected(other)),
182                            }
183                        }
184                        if rendered.is_empty() {
185                            return Err(
186                                format!("{}() expected at least 1 argument", bname).into()
187                            );
188                        }
189                        if rendered.len() >= 2 {
190                            if key.is_some() || default.is_some() {
191                                return Err(format!(
192                                    "{}() with multiple positional values and keywords \
193                                     is not supported yet; pass a list instead",
194                                    bname
195                                )
196                                .into());
197                            }
198                            // Python min(a, b, c) folds pairwise; ties keep
199                            // the earlier argument.
200                            let two = format_ident!("{}2", bname);
201                            let mut acc = rendered[0].clone();
202                            for next in &rendered[1..] {
203                                acc = quote!(#two(#acc, #next));
204                            }
205                            return Ok(acc);
206                        }
207                        let a = &rendered[0];
208                        let render = |e: crate::ExprType| {
209                            e.to_rust(ctx.clone(), options.clone(), symbols.clone())
210                        };
211                        return Ok(match (key, default) {
212                            (None, None) => {
213                                let f = format_ident!("{}", bname);
214                                quote!(#f(&(#a))?)
215                            }
216                            (Some(k), None) => {
217                                let k = render(k)?;
218                                let f = format_ident!("{}_key", bname);
219                                quote!(#f(&(#a), #k)?)
220                            }
221                            (None, Some(d)) => {
222                                let d = render(d)?;
223                                let f = format_ident!("{}_default", bname);
224                                quote!(#f(&(#a), #d))
225                            }
226                            (Some(k), Some(d)) => {
227                                let k = render(k)?;
228                                let d = render(d)?;
229                                let f = format_ident!("{}_key_default", bname);
230                                quote!(#f(&(#a), #k, #d))
231                            }
232                        });
233                    }
234                    "sorted" => {
235                        let mut key = None;
236                        let mut reverse = None;
237                        for kw in &self.keywords {
238                            match kw.arg.as_deref() {
239                                Some("key") if key.is_none() => key = Some(kw.value.clone()),
240                                Some("reverse") if reverse.is_none() => {
241                                    reverse = Some(kw.value.clone())
242                                }
243                                other => return Err(unexpected(other)),
244                            }
245                        }
246                        if rendered.len() != 1 {
247                            return Err("sorted() takes exactly one positional argument"
248                                .to_string()
249                                .into());
250                        }
251                        let a = &rendered[0];
252                        let render = |e: crate::ExprType| {
253                            e.to_rust(ctx.clone(), options.clone(), symbols.clone())
254                        };
255                        return Ok(match (key, reverse) {
256                            (None, None) => quote!(sorted(&(#a))),
257                            (Some(k), None) => {
258                                let k = render(k)?;
259                                quote!(sorted_key(&(#a), #k))
260                            }
261                            (None, Some(r)) => {
262                                let r = render(r)?;
263                                quote!(sorted_reverse(&(#a), #r))
264                            }
265                            (Some(k), Some(r)) => {
266                                let k = render(k)?;
267                                let r = render(r)?;
268                                quote!(sorted_key_reverse(&(#a), #k, #r))
269                            }
270                        });
271                    }
272                    "enumerate" => {
273                        let mut start = self.args.get(1).cloned();
274                        if self.args.len() > 2 {
275                            return Err("enumerate() takes at most 2 arguments"
276                                .to_string()
277                                .into());
278                        }
279                        for kw in &self.keywords {
280                            match kw.arg.as_deref() {
281                                Some("start") if start.is_none() => {
282                                    start = Some(kw.value.clone())
283                                }
284                                other => return Err(unexpected(other)),
285                            }
286                        }
287                        if rendered.is_empty() {
288                            return Err("enumerate() expected an iterable".to_string().into());
289                        }
290                        let a = &rendered[0];
291                        return Ok(match start {
292                            None => quote!(enumerate(#a)),
293                            Some(s) => {
294                                let s =
295                                    s.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
296                                quote!(enumerate_start(#a, #s))
297                            }
298                        });
299                    }
300                    "pow" => {
301                        if !self.keywords.is_empty() {
302                            return Err(unexpected(
303                                self.keywords[0].arg.as_deref(),
304                            ));
305                        }
306                        return match rendered.as_slice() {
307                            [b, e] => Ok(quote!(pow(#b, #e))),
308                            [b, e, m] => Ok(quote!(pow_mod(#b, #e, #m)?)),
309                            _ => Err("pow() takes 2 or 3 arguments".to_string().into()),
310                        };
311                    }
312                    // isinstance is statically decidable in a typed
313                    // lowering: it becomes the constant true/false when the
314                    // argument's type is known (annotation or literal), and
315                    // a loud error when it is not.
316                    "isinstance" => {
317                        if !self.keywords.is_empty() {
318                            return Err(unexpected(self.keywords[0].arg.as_deref()));
319                        }
320                        if self.args.len() != 2 {
321                            return Err("isinstance() takes exactly 2 arguments"
322                                .to_string()
323                                .into());
324                        }
325                        let target = match &self.args[1] {
326                            ExprType::Name(t)
327                                if matches!(
328                                    t.id.as_str(),
329                                    "int" | "float" | "str" | "bool"
330                                ) =>
331                            {
332                                t.id.clone()
333                            }
334                            other => {
335                                return Err(format!(
336                                    "isinstance() second argument must be int, float, \
337                                     str, or bool (got `{:?}`); tuples of types are not \
338                                     supported yet",
339                                    other
340                                )
341                                .into());
342                            }
343                        };
344                        let actual: Option<String> = match &self.args[0] {
345                            ExprType::Name(n) => options.local_types.get(&n.id).cloned(),
346                            lit => crate::ast::tree::function_def::simple_expr_type(lit)
347                                .map(|ty| match ty.to_string().as_str() {
348                                    "i64" => "int".to_string(),
349                                    "f64" => "float".to_string(),
350                                    "bool" => "bool".to_string(),
351                                    _ => "str".to_string(),
352                                }),
353                        };
354                        let Some(actual) = actual else {
355                            return Err(format!(
356                                "isinstance(): the type of `{:?}` is not statically \
357                                 known; annotate it (or assign it a literal) so the \
358                                 check can be decided at conversion time",
359                                self.args[0]
360                            )
361                            .into());
362                        };
363                        // bool is a subclass of int in Python.
364                        let result = actual == target
365                            || (actual == "bool" && target == "int");
366                        return Ok(if result { quote!(true) } else { quote!(false) });
367                    }
368                    // The by-reference builtins: their runtime functions
369                    // borrow, and Python's calls never consume the value.
370                    "print" => {
371                        // print builds on py_display (Python's str
372                        // semantics: True, 1e+16, unquoted strings) — the
373                        // Display fallback would silently diverge.
374                        let mut sep = None;
375                        let mut end = None;
376                        let mut flush = None;
377                        for kw in &self.keywords {
378                            match kw.arg.as_deref() {
379                                Some("sep") if sep.is_none() => sep = Some(kw.value.clone()),
380                                Some("end") if end.is_none() => end = Some(kw.value.clone()),
381                                Some("flush") if flush.is_none() => {
382                                    flush = Some(kw.value.clone())
383                                }
384                                Some("file") => {
385                                    return Err("print(file=...) is not supported: \
386                                                generated code writes to stdout only"
387                                        .to_string()
388                                        .into());
389                                }
390                                other => return Err(unexpected(other)),
391                            }
392                        }
393                        // sep=None / end=None mean the defaults in Python.
394                        let sep = sep.filter(|s| !crate::is_none_expr(s));
395                        let end = end.filter(|e| !crate::is_none_expr(e));
396                        let render = |e: crate::ExprType| {
397                            e.to_rust(ctx.clone(), options.clone(), symbols.clone())
398                        };
399                        if sep.is_none() && end.is_none() && flush.is_none() {
400                            match rendered.as_slice() {
401                                [] => return Ok(quote!(println!())),
402                                [a] => return Ok(quote!(print(&(#a)))),
403                                _ => {}
404                            }
405                        }
406                        let sep = match sep {
407                            Some(s) => render(s)?,
408                            None => quote!(" "),
409                        };
410                        let end = match end {
411                            Some(e) => render(e)?,
412                            None => quote!("\n"),
413                        };
414                        // print(end="") with no arguments still needs an
415                        // element type for the empty parts slice.
416                        let parts = if rendered.is_empty() {
417                            quote!(&[] as &[&str])
418                        } else {
419                            quote!(&[#(py_display(&(#rendered))),*])
420                        };
421                        return Ok(match flush {
422                            None => quote!(print_parts(#parts, #sep, #end)),
423                            Some(f) => {
424                                let f = render(f)?;
425                                quote!(print_parts_flush(#parts, #sep, #end, #f))
426                            }
427                        });
428                    }
429                    "open" => {
430                        // The runtime signature takes mode as an Option;
431                        // the arity split wraps it here. Text modes only —
432                        // encoding/newline/binary spellings are loud.
433                        if !self.keywords.is_empty() {
434                            return Err(unexpected(self.keywords[0].arg.as_deref()));
435                        }
436                        return Ok(match rendered.as_slice() {
437                            [p] => quote!(open(&(#p), None::<&str>)?),
438                            [p, m] => quote!(open(&(#p), Some(#m))?),
439                            _ => {
440                                return Err(
441                                    "open() takes 1 or 2 arguments (path and mode)"
442                                        .to_string()
443                                        .into(),
444                                )
445                            }
446                        });
447                    }
448                    "len" | "repr" | "reversed" | "hash" => {
449                        if !self.keywords.is_empty() {
450                            return Err(unexpected(self.keywords[0].arg.as_deref()));
451                        }
452                        if rendered.len() != 1 {
453                            return Err(
454                                format!("{}() takes exactly one argument", bname).into()
455                            );
456                        }
457                        let f = format_ident!("{}", bname);
458                        let a = &rendered[0];
459                        return Ok(quote!(#f(&(#a))));
460                    }
461                    "frozenset" => {
462                        if !self.keywords.is_empty() {
463                            return Err(unexpected(self.keywords[0].arg.as_deref()));
464                        }
465                        if rendered.len() != 1 {
466                            return Err(
467                                "frozenset() requires an iterable argument in rython \
468                                 (an empty frozenset has no inferable element type)"
469                                    .to_string()
470                                    .into(),
471                            );
472                        }
473                        let a = &rendered[0];
474                        return Ok(quote!(frozenset(#a)));
475                    }
476                    // map/filter dispatch on the FUNCTION argument's shape:
477                    // lambdas are plain closures, while user-defined
478                    // functions return Result and route through the
479                    // fallible variants so their exceptions propagate.
480                    "map" | "filter" => {
481                        if !self.keywords.is_empty() {
482                            return Err(unexpected(self.keywords[0].arg.as_deref()));
483                        }
484                        let fallible = matches!(self.args.first(), Some(ExprType::Name(f))
485                            if matches!(symbols.get(&f.id), Some(SymbolTableNode::FunctionDef(_))));
486                        if bname == "filter" {
487                            if rendered.len() != 2 {
488                                return Err("filter() takes a function and an iterable"
489                                    .to_string()
490                                    .into());
491                            }
492                            let (f, xs) = (&rendered[0], &rendered[1]);
493                            // filter(None, xs) keeps the truthy elements.
494                            if self
495                                .args
496                                .first()
497                                .is_some_and(crate::is_none_expr)
498                            {
499                                return Ok(quote!(filter_truthy(#xs)));
500                            }
501                            return Ok(if fallible {
502                                quote!(filter_fallible(#f, #xs)?)
503                            } else {
504                                quote!(filter(#f, #xs))
505                            });
506                        }
507                        return match rendered.as_slice() {
508                            [f, xs] => Ok(if fallible {
509                                quote!(map_fallible(#f, #xs)?)
510                            } else {
511                                quote!(map(#f, #xs))
512                            }),
513                            [f, a, b] => {
514                                if fallible {
515                                    return Err(
516                                        "map() over two iterables with a user-defined \
517                                         function is not supported yet; use a lambda"
518                                            .to_string()
519                                            .into(),
520                                    );
521                                }
522                                Ok(quote!(map2(#f, #a, #b)))
523                            }
524                            _ => Err("map() takes a function and 1-2 iterables"
525                                .to_string()
526                                .into()),
527                        };
528                    }
529                    "list" => {
530                        if !self.keywords.is_empty() {
531                            return Err(unexpected(self.keywords[0].arg.as_deref()));
532                        }
533                        if rendered.len() != 1 {
534                            return Err(
535                                "list() requires an iterable argument in rython (an \
536                                 empty list has no inferable element type; use [])"
537                                    .to_string()
538                                    .into(),
539                            );
540                        }
541                        let a = &rendered[0];
542                        return Ok(quote!(list(#a)));
543                    }
544                    _ => unreachable!(),
545                }
546            }
547        }
548
549        // datetime constructors imported via `from datetime import ...`:
550        // date/datetime/timedelta calls resolve their positional and
551        // keyword arguments against the Python signatures and lower to the
552        // runtime ::new constructors (Option-typed for the defaulted
553        // parameters). date/datetime validate and propagate with `?`.
554        if let ExprType::Name(n) = self.func.as_ref() {
555            let from_datetime = matches!(
556                symbols.get(&n.id),
557                Some(SymbolTableNode::ImportFrom(import))
558                    if import.module == "datetime"
559            );
560            if from_datetime && matches!(n.id.as_str(), "date" | "datetime" | "timedelta") {
561                let (params, required): (&[&str], usize) = match n.id.as_str() {
562                    "date" => (&["year", "month", "day"], 3),
563                    "datetime" => (
564                        &["year", "month", "day", "hour", "minute", "second", "microsecond"],
565                        3,
566                    ),
567                    _ => (
568                        &[
569                            "days",
570                            "seconds",
571                            "microseconds",
572                            "milliseconds",
573                            "minutes",
574                            "hours",
575                            "weeks",
576                        ],
577                        0,
578                    ),
579                };
580                if self.args.len() > params.len() {
581                    return Err(format!(
582                        "{}() takes at most {} arguments ({} given)",
583                        n.id,
584                        params.len(),
585                        self.args.len()
586                    )
587                    .into());
588                }
589                let mut slots: Vec<Option<crate::ExprType>> = vec![None; params.len()];
590                for (i, arg) in self.args.iter().enumerate() {
591                    slots[i] = Some(arg.clone());
592                }
593                for kw in &self.keywords {
594                    let idx = kw
595                        .arg
596                        .as_deref()
597                        .and_then(|k| params.iter().position(|p| *p == k));
598                    match idx {
599                        Some(i) if slots[i].is_none() => slots[i] = Some(kw.value.clone()),
600                        Some(i) => {
601                            return Err(format!(
602                                "{}() got multiple values for argument '{}'",
603                                n.id, params[i]
604                            )
605                            .into());
606                        }
607                        None => {
608                            return Err(format!(
609                                "{}() got an unexpected keyword argument '{}'",
610                                n.id,
611                                kw.arg.as_deref().unwrap_or("**kwargs")
612                            )
613                            .into());
614                        }
615                    }
616                }
617                let mut rendered = Vec::new();
618                for (i, slot) in slots.iter().enumerate() {
619                    let tok = match slot {
620                        Some(e) => {
621                            let v = e.clone().to_rust(
622                                ctx.clone(),
623                                options.clone(),
624                                symbols.clone(),
625                            )?;
626                            if i < required { v } else { quote!(Some(#v)) }
627                        }
628                        None if i < required => {
629                            return Err(format!(
630                                "{}() missing required argument: '{}'",
631                                n.id, params[i]
632                            )
633                            .into());
634                        }
635                        None => quote!(None),
636                    };
637                    rendered.push(tok);
638                }
639                let ty = crate::safe_ident(&n.id);
640                let call = quote!(#ty::new(#(#rendered),*));
641                // timedelta::new is infallible; date/datetime validate.
642                return Ok(if n.id == "timedelta" { call } else { quote!(#call?) });
643            }
644        }
645
646        // itertools functions imported via `from itertools import ...`:
647        // keyword spellings (initial=, repeat=, fillvalue=, key=) map to
648        // arity-specific runtime variants, and iterable arguments are
649        // borrowed (the runtime takes slices; Python calls never consume).
650        if let ExprType::Name(n) = self.func.as_ref() {
651            let from_itertools = matches!(
652                symbols.get(&n.id),
653                Some(SymbolTableNode::ImportFrom(import))
654                    if import.module == "itertools"
655            );
656            let handled = matches!(
657                n.id.as_str(),
658                "accumulate"
659                    | "product"
660                    | "zip_longest"
661                    | "groupby"
662                    | "pairwise"
663                    | "combinations"
664                    | "combinations_with_replacement"
665                    | "permutations"
666                    | "starmap"
667            );
668            if from_itertools && handled {
669                let name = n.id.as_str();
670                let mut rendered = Vec::new();
671                for arg in &self.args {
672                    rendered.push(arg.clone().to_rust(
673                        ctx.clone(),
674                        options.clone(),
675                        symbols.clone(),
676                    )?);
677                }
678                let render = |e: crate::ExprType| {
679                    e.to_rust(ctx.clone(), options.clone(), symbols.clone())
680                };
681                let kw_of = |allowed: &[&str]| -> Result<
682                    Vec<Option<crate::ExprType>>,
683                    Box<dyn std::error::Error>,
684                > {
685                    let mut out: Vec<Option<crate::ExprType>> = vec![None; allowed.len()];
686                    for kw in &self.keywords {
687                        let idx = kw
688                            .arg
689                            .as_deref()
690                            .and_then(|k| allowed.iter().position(|a| *a == k));
691                        match idx {
692                            Some(i) if out[i].is_none() => out[i] = Some(kw.value.clone()),
693                            _ => {
694                                return Err(format!(
695                                    "{}() got an unexpected or duplicate keyword \
696                                     argument '{}'",
697                                    name,
698                                    kw.arg.as_deref().unwrap_or("**kwargs")
699                                )
700                                .into());
701                            }
702                        }
703                    }
704                    Ok(out)
705                };
706                match name {
707                    "accumulate" => {
708                        let kws = kw_of(&["initial"])?;
709                        let initial = kws.into_iter().next().unwrap();
710                        let (xs, func) = match rendered.as_slice() {
711                            [xs] => (xs.clone(), None),
712                            [xs, f] => (xs.clone(), Some(f.clone())),
713                            _ => {
714                                return Err("accumulate() takes 1 or 2 positional \
715                                            arguments"
716                                    .to_string()
717                                    .into());
718                            }
719                        };
720                        return Ok(match (func, initial) {
721                            (None, None) => quote!(accumulate_sum(&(#xs))),
722                            (Some(f), None) => quote!(accumulate_func(&(#xs), #f)),
723                            (None, Some(init)) => {
724                                let init = render(init)?;
725                                quote!(accumulate_sum_initial(&(#xs), #init))
726                            }
727                            (Some(f), Some(init)) => {
728                                let init = render(init)?;
729                                quote!(accumulate_func_initial(&(#xs), #f, #init))
730                            }
731                        });
732                    }
733                    "product" => {
734                        let kws = kw_of(&["repeat"])?;
735                        let repeat = kws.into_iter().next().unwrap();
736                        if let Some(r) = repeat {
737                            if rendered.len() != 1 {
738                                return Err("product(iterable, repeat=n) takes one \
739                                            iterable"
740                                    .to_string()
741                                    .into());
742                            }
743                            let r = render(r)?;
744                            let xs = &rendered[0];
745                            return match r.to_string().as_str() {
746                                "2" => Ok(quote!(product_repeat2(&(#xs)))),
747                                "3" => Ok(quote!(product_repeat3(&(#xs)))),
748                                other => Err(format!(
749                                    "product() repeat must be the literal 2 or 3 \
750                                     (tuple arity is a compile-time shape); got {}",
751                                    other
752                                )
753                                .into()),
754                            };
755                        }
756                        return match rendered.as_slice() {
757                            [a, b] => Ok(quote!(product2(&(#a), &(#b)))),
758                            [a, b, c] => Ok(quote!(product3(&(#a), &(#b), &(#c)))),
759                            _ => Err("product() supports 2 or 3 iterables, or one \
760                                      iterable with repeat=2/3"
761                                .to_string()
762                                .into()),
763                        };
764                    }
765                    "zip_longest" => {
766                        let kws = kw_of(&["fillvalue"])?;
767                        let fill = kws.into_iter().next().unwrap();
768                        if rendered.len() != 2 {
769                            return Err("zip_longest() supports exactly 2 iterables"
770                                .to_string()
771                                .into());
772                        }
773                        let (a, b) = (&rendered[0], &rendered[1]);
774                        return Ok(match fill {
775                            Some(v) => {
776                                let v = render(v)?;
777                                quote!(zip_longest_fill(&(#a), &(#b), #v))
778                            }
779                            None => quote!(zip_longest(&(#a), &(#b))),
780                        });
781                    }
782                    "groupby" => {
783                        let kws = kw_of(&["key"])?;
784                        let key = kws.into_iter().next().unwrap();
785                        if rendered.len() != 1 {
786                            return Err("groupby() takes one iterable".to_string().into());
787                        }
788                        let xs = &rendered[0];
789                        return Ok(match key {
790                            Some(f) => {
791                                let f = render(f)?;
792                                quote!(groupby_key(&(#xs), #f))
793                            }
794                            None => quote!(groupby(&(#xs))),
795                        });
796                    }
797                    "pairwise" => {
798                        kw_of(&[])?;
799                        if rendered.len() != 1 {
800                            return Err("pairwise() takes one iterable".to_string().into());
801                        }
802                        let xs = &rendered[0];
803                        return Ok(quote!(pairwise(&(#xs))));
804                    }
805                    "combinations" | "combinations_with_replacement" => {
806                        kw_of(&[])?;
807                        if rendered.len() != 2 {
808                            return Err(
809                                format!("{}() takes an iterable and r", name).into()
810                            );
811                        }
812                        let f = format_ident!("{}", name);
813                        let (xs, r) = (&rendered[0], &rendered[1]);
814                        // Negative r raises ValueError, hence the `?`.
815                        return Ok(quote!(#f(&(#xs), #r)?));
816                    }
817                    "permutations" => {
818                        kw_of(&[])?;
819                        return match rendered.as_slice() {
820                            [xs] => Ok(quote!(permutations(&(#xs), None)?)),
821                            [xs, r] => Ok(quote!(permutations(&(#xs), Some(#r))?)),
822                            _ => Err("permutations() takes an iterable and optional r"
823                                .to_string()
824                                .into()),
825                        };
826                    }
827                    "starmap" => {
828                        kw_of(&[])?;
829                        if rendered.len() != 2 {
830                            return Err("starmap() takes a function and an iterable"
831                                .to_string()
832                                .into());
833                        }
834                        let (f, xs) = (&rendered[0], &rendered[1]);
835                        return Ok(quote!(starmap(#f, &(#xs))));
836                    }
837                    _ => unreachable!(),
838                }
839            }
840        }
841
842        // functools.partial over a STATICALLY-KNOWN function: the
843        // signature comes from the symbol table, so the call lowers to a
844        // move closure binding the leading arguments, with the remaining
845        // parameters keeping their Python names. The closure returns the
846        // function's Result directly; calls through the bound name get
847        // `?` (see propagates_exceptions). Dynamic first arguments have
848        // no signature to consult and are a loud error.
849        if is_partial_target(self.func.as_ref(), &symbols) {
850            if !self.keywords.is_empty() {
851                return Err("functools.partial with keyword arguments is not \
852                            supported yet; bind the arguments positionally"
853                    .to_string()
854                    .into());
855            }
856            let Some(ExprType::Name(f)) = self.args.first() else {
857                return Err("functools.partial requires a function defined in this \
858                            module as its first argument"
859                    .to_string()
860                    .into());
861            };
862            let Some(SymbolTableNode::FunctionDef(fdef)) = symbols.get(&f.id) else {
863                return Err(format!(
864                    "functools.partial: `{}` is not a function defined in this \
865                     module (only statically-known functions can be bound)",
866                    f.id
867                )
868                .into());
869            };
870            let params: Vec<String> =
871                fdef.args.args.iter().map(|p| p.arg.clone()).collect();
872            let bound_n = self.args.len() - 1;
873            if bound_n > params.len() {
874                return Err(format!(
875                    "functools.partial: `{}` takes {} argument(s), but {} were bound",
876                    f.id,
877                    params.len(),
878                    bound_n
879                )
880                .into());
881            }
882            let mut bound = Vec::new();
883            for arg in &self.args[1..] {
884                bound.push(arg.clone().to_rust(
885                    ctx.clone(),
886                    options.clone(),
887                    symbols.clone(),
888                )?);
889            }
890            let rest: Vec<_> = params[bound_n..]
891                .iter()
892                .map(|p| crate::safe_ident(p))
893                .collect();
894            let fident = crate::safe_ident(&f.id);
895            return Ok(quote!(move |#(#rest),*| #fident(#(#bound,)* #(#rest),*)));
896        }
897
898        // functools/heapq/copy/textwrap/re functions: their runtime shapes
899        // borrow (or mutably borrow) arguments, reduce() splits by arity,
900        // and the re functions validate patterns at runtime (hence `?`).
901        // Handled for both `from X import f; f(...)` and `import X;
902        // X.f(...)` spellings.
903        {
904            let target: Option<(String, Option<&'static str>)> = match self.func.as_ref() {
905                ExprType::Name(n) => match symbols.get(&n.id) {
906                    Some(SymbolTableNode::ImportFrom(import))
907                        if matches!(
908                            import.module.as_str(),
909                            "functools" | "heapq" | "copy" | "textwrap" | "re" | "hashlib"
910                                | "csv" | "io"
911                        ) =>
912                    {
913                        Some((n.id.clone(), None))
914                    }
915                    _ => None,
916                },
917                ExprType::Attribute(attr) => match attr.value.as_ref() {
918                    ExprType::Name(m)
919                        if matches!(
920                            m.id.as_str(),
921                            "functools" | "heapq" | "textwrap" | "re" | "hashlib" | "csv"
922                                | "io"
923                        ) =>
924                    {
925                        let module: &'static str = match m.id.as_str() {
926                            "functools" => "functools",
927                            "heapq" => "heapq",
928                            "re" => "re",
929                            "hashlib" => "hashlib",
930                            "csv" => "csv",
931                            "io" => "io",
932                            _ => "textwrap",
933                        };
934                        Some((attr.attr.clone(), Some(module)))
935                    }
936                    _ => None,
937                },
938                _ => None,
939            };
940            let known = target.as_ref().is_some_and(|(f, _)| {
941                matches!(
942                    f.as_str(),
943                    "reduce"
944                        | "heappush"
945                        | "heappop"
946                        | "heapify"
947                        | "heappushpop"
948                        | "heapreplace"
949                        | "nlargest"
950                        | "nsmallest"
951                        | "copy"
952                        | "deepcopy"
953                        | "dedent"
954                        | "indent"
955                        | "search"
956                        | "match"
957                        | "fullmatch"
958                        | "findall"
959                        | "finditer"
960                        | "sub"
961                        | "split"
962                        | "md5"
963                        | "sha1"
964                        | "sha256"
965                        | "sha512"
966                        | "wrap"
967                        | "fill"
968                        | "reader"
969                        | "writer"
970                        | "StringIO"
971                )
972            });
973            if let (Some((fname, module_prefix)), true) = (target, known) {
974                // wrap/fill accept width=, the re functions accept
975                // flags= (and sub also count=); everything else takes no
976                // keywords.
977                let is_re_fn = matches!(
978                    fname.as_str(),
979                    "search" | "match" | "fullmatch" | "findall" | "finditer" | "sub"
980                        | "split"
981                );
982                let mut width_kw: Option<crate::ExprType> = None;
983                let mut flags_kw: Option<crate::ExprType> = None;
984                let mut count_kw: Option<crate::ExprType> = None;
985                let mut maxsplit_kw: Option<crate::ExprType> = None;
986                for kw in &self.keywords {
987                    let slot = match kw.arg.as_deref() {
988                        Some("width")
989                            if matches!(fname.as_str(), "wrap" | "fill") =>
990                        {
991                            &mut width_kw
992                        }
993                        Some("flags") if is_re_fn => &mut flags_kw,
994                        Some("count") if fname == "sub" => &mut count_kw,
995                        Some("maxsplit") if fname == "split" => &mut maxsplit_kw,
996                        _ => {
997                            return Err(format!(
998                                "{}() got an unexpected keyword argument '{}'",
999                                fname,
1000                                kw.arg.as_deref().unwrap_or("**kwargs")
1001                            )
1002                            .into());
1003                        }
1004                    };
1005                    if slot.is_some() {
1006                        return Err(format!(
1007                            "{}() got multiple values for a keyword argument",
1008                            fname
1009                        )
1010                        .into());
1011                    }
1012                    *slot = Some(kw.value.clone());
1013                }
1014                // Python re flags lower to inline flag letters: single
1015                // constants or |-combinations of re.IGNORECASE/I,
1016                // re.MULTILINE/M, re.DOTALL/S. Anything else is loud.
1017                fn flag_letters(
1018                    e: &crate::ExprType,
1019                ) -> Result<String, Box<dyn std::error::Error>> {
1020                    let name_of = |id: &str| -> Result<String, Box<dyn std::error::Error>> {
1021                        match id {
1022                            "IGNORECASE" | "I" => Ok("i".to_string()),
1023                            "MULTILINE" | "M" => Ok("m".to_string()),
1024                            "DOTALL" | "S" => Ok("s".to_string()),
1025                            other => Err(format!(
1026                                "unsupported re flag `{}`; supported: IGNORECASE,                                  MULTILINE, DOTALL (and | combinations)",
1027                                other
1028                            )
1029                            .into()),
1030                        }
1031                    };
1032                    match e {
1033                        ExprType::Attribute(a)
1034                            if matches!(a.value.as_ref(), ExprType::Name(m) if m.id == "re") =>
1035                        {
1036                            name_of(&a.attr)
1037                        }
1038                        ExprType::Name(n) => name_of(&n.id),
1039                        ExprType::BinOp(b)
1040                            if matches!(b.op, crate::ast::tree::bin_ops::BinOps::BitOr) =>
1041                        {
1042                            Ok(format!(
1043                                "{}{}",
1044                                flag_letters(&b.left)?,
1045                                flag_letters(&b.right)?
1046                            ))
1047                        }
1048                        other => Err(format!(
1049                            "unsupported re flags expression `{:?}`; use re.IGNORECASE,                              re.MULTILINE, re.DOTALL, or | combinations of them",
1050                            other
1051                        )
1052                        .into()),
1053                    }
1054                }
1055                let mut rendered = Vec::new();
1056                for arg in &self.args {
1057                    rendered.push(arg.clone().to_rust(
1058                        ctx.clone(),
1059                        options.clone(),
1060                        symbols.clone(),
1061                    )?);
1062                }
1063                // The heap mutators take their first argument by &mut, so
1064                // it must be lowered as a PLACE: `heappush(rows[i], v)`
1065                // through the Load path would clone the element and the
1066                // push would silently vanish (the same clone-mutation bug
1067                // fixed for mutating methods on subscripted receivers).
1068                // rendered[0] becomes the full mutable-borrow expression:
1069                // py_index_mut already yields &mut for subscripts, names
1070                // take a fresh &mut.
1071                let heap_mutator = matches!(
1072                    fname.as_str(),
1073                    "heappush" | "heappop" | "heapify" | "heappushpop" | "heapreplace"
1074                );
1075                if heap_mutator {
1076                    if let Some(first) = self.args.first() {
1077                        rendered[0] = if matches!(first, ExprType::Subscript(_)) {
1078                            crate::ast::tree::subscript::subscript_receiver_place(
1079                                first,
1080                                ctx.clone(),
1081                                options.clone(),
1082                                symbols.clone(),
1083                            )?
1084                        } else {
1085                            let v = &rendered[0];
1086                            quote!(&mut (#v))
1087                        };
1088                    }
1089                }
1090                let qual = |name: &str| {
1091                    let f = crate::safe_ident(name);
1092                    match module_prefix {
1093                        Some(m) => {
1094                            let m = format_ident!("{}", m);
1095                            quote!(#m::#f)
1096                        }
1097                        None => quote!(#f),
1098                    }
1099                };
1100                let arity = |expected: &str| -> Box<dyn std::error::Error> {
1101                    format!("{}() takes {} arguments", fname, expected).into()
1102                };
1103                return match (fname.as_str(), rendered.as_slice()) {
1104                    ("reduce", [f, xs]) => {
1105                        let p = qual("reduce");
1106                        Ok(quote!(#p(#f, &(#xs))?))
1107                    }
1108                    ("reduce", [f, xs, init]) => {
1109                        let p = qual("reduce_initial");
1110                        Ok(quote!(#p(#f, &(#xs), #init)))
1111                    }
1112                    ("reduce", _) => Err(arity("2 or 3")),
1113                    ("heappush", [h, x]) => {
1114                        let p = qual("heappush");
1115                        Ok(quote!(#p(#h, #x)))
1116                    }
1117                    ("heappop", [h]) => {
1118                        let p = qual("heappop");
1119                        Ok(quote!(#p(#h)?))
1120                    }
1121                    ("heapify", [h]) => {
1122                        let p = qual("heapify");
1123                        Ok(quote!(#p(#h)))
1124                    }
1125                    ("heappushpop", [h, x]) => {
1126                        let p = qual("heappushpop");
1127                        Ok(quote!(#p(#h, #x)))
1128                    }
1129                    ("heapreplace", [h, x]) => {
1130                        let p = qual("heapreplace");
1131                        Ok(quote!(#p(#h, #x)?))
1132                    }
1133                    ("nlargest" | "nsmallest", [n_arg, xs]) => {
1134                        let p = qual(&fname);
1135                        Ok(quote!(#p(#n_arg, &(#xs))))
1136                    }
1137                    ("copy" | "deepcopy", [x]) => {
1138                        let p = qual(&fname);
1139                        Ok(quote!(#p(&(#x))))
1140                    }
1141                    ("dedent", [s]) => {
1142                        let p = qual("dedent");
1143                        Ok(quote!(#p(&(#s))))
1144                    }
1145                    // wrap/fill: width by position, keyword, or Python's
1146                    // default of 70. They validate width, hence `?`.
1147                    ("wrap" | "fill", [t]) | ("wrap" | "fill", [t, _]) => {
1148                        let p = qual(&fname);
1149                        let width = match (rendered.get(1), width_kw) {
1150                            (Some(_), Some(_)) => {
1151                                return Err(format!(
1152                                    "{}() got multiple values for argument 'width'",
1153                                    fname
1154                                )
1155                                .into());
1156                            }
1157                            (Some(w), None) => quote!(#w),
1158                            (None, Some(w)) => {
1159                                let w = w.to_rust(
1160                                    ctx.clone(),
1161                                    options.clone(),
1162                                    symbols.clone(),
1163                                )?;
1164                                quote!(#w)
1165                            }
1166                            (None, None) => quote!(70),
1167                        };
1168                        Ok(quote!(#p(&(#t), #width)?))
1169                    }
1170                    (
1171                        "search" | "match" | "fullmatch" | "findall" | "finditer",
1172                        [pat, text, ..],
1173                    ) => {
1174                        if rendered.len() > 3 {
1175                            return Err(format!(
1176                                "{}() takes at most 3 positional arguments",
1177                                fname
1178                            )
1179                            .into());
1180                        }
1181                        if rendered.len() > 2 && flags_kw.is_some() {
1182                            return Err(format!(
1183                                "{}() got multiple values for argument 'flags'",
1184                                fname
1185                            )
1186                            .into());
1187                        }
1188                        let flags = match (self.args.get(2), flags_kw) {
1189                            (Some(e), None) => flag_letters(e)?,
1190                            (None, Some(e)) => flag_letters(&e)?,
1191                            (None, None) => String::new(),
1192                            _ => unreachable!(),
1193                        };
1194                        // findall's result SHAPE depends on the pattern's
1195                        // capture-group count (strings for 0-1 groups,
1196                        // tuples beyond), so a literal pattern is compiled
1197                        // here at conversion time to pick the variant —
1198                        // which also surfaces bad patterns before the
1199                        // program ever runs. Non-literal patterns keep the
1200                        // string shape; 2+ groups there stay a loud
1201                        // runtime error.
1202                        let mut target = fname.clone();
1203                        if fname == "findall" {
1204                            if let ExprType::Constant(c) = &self.args[0] {
1205                                if let Some(litrs::Literal::String(slit)) = &c.0 {
1206                                    let pattern = slit.value();
1207                                    let re = regex::Regex::new(&pattern).map_err(|e| {
1208                                        format!(
1209                                            "re.findall(): cannot compile pattern {:?}: {} \
1210                                             (the regex engine does not support Python's \
1211                                             backreferences or lookarounds)",
1212                                            pattern, e
1213                                        )
1214                                    })?;
1215                                    match re.captures_len() - 1 {
1216                                        0 | 1 => {}
1217                                        2 => target = "findall2".to_string(),
1218                                        3 => target = "findall3".to_string(),
1219                                        n => {
1220                                            return Err(format!(
1221                                                "re.findall() with {} capture groups is \
1222                                                 not supported yet (at most 3)",
1223                                                n
1224                                            )
1225                                            .into());
1226                                        }
1227                                    }
1228                                }
1229                            }
1230                        }
1231                        let p = qual(&target);
1232                        Ok(quote!(#p(&(#pat), &(#text), #flags)?))
1233                    }
1234                    // re.split(pattern, string, maxsplit=0, flags=0):
1235                    // the THIRD positional is maxsplit, unlike the other
1236                    // re functions where it is flags.
1237                    ("split", [pat, text, ..]) => {
1238                        if rendered.len() > 4 {
1239                            return Err("split() takes at most 4 positional arguments"
1240                                .to_string()
1241                                .into());
1242                        }
1243                        if rendered.len() > 2 && maxsplit_kw.is_some() {
1244                            return Err(
1245                                "split() got multiple values for argument 'maxsplit'"
1246                                    .to_string()
1247                                    .into(),
1248                            );
1249                        }
1250                        if rendered.len() > 3 && flags_kw.is_some() {
1251                            return Err(
1252                                "split() got multiple values for argument 'flags'"
1253                                    .to_string()
1254                                    .into(),
1255                            );
1256                        }
1257                        let maxsplit = match (rendered.get(2), maxsplit_kw) {
1258                            (Some(m), None) => quote!(#m),
1259                            (None, Some(m)) => {
1260                                let m = m.to_rust(
1261                                    ctx.clone(),
1262                                    options.clone(),
1263                                    symbols.clone(),
1264                                )?;
1265                                quote!(#m)
1266                            }
1267                            (None, None) => quote!(0),
1268                            _ => unreachable!(),
1269                        };
1270                        let flags = match (self.args.get(3), flags_kw) {
1271                            (Some(e), None) => flag_letters(e)?,
1272                            (None, Some(e)) => flag_letters(&e)?,
1273                            (None, None) => String::new(),
1274                            _ => unreachable!(),
1275                        };
1276                        let p = qual("split");
1277                        Ok(quote!(#p(&(#pat), &(#text), #maxsplit, #flags)?))
1278                    }
1279                    ("sub", [pat, repl, text, ..]) => {
1280                        if rendered.len() > 4 {
1281                            return Err("sub() takes at most 4 positional arguments"
1282                                .to_string()
1283                                .into());
1284                        }
1285                        if rendered.len() > 3 && count_kw.is_some() {
1286                            return Err(
1287                                "sub() got multiple values for argument 'count'"
1288                                    .to_string()
1289                                    .into(),
1290                            );
1291                        }
1292                        let count = match (rendered.get(3), count_kw) {
1293                            (Some(c), None) => quote!(#c),
1294                            (None, Some(c)) => {
1295                                let c = c.to_rust(
1296                                    ctx.clone(),
1297                                    options.clone(),
1298                                    symbols.clone(),
1299                                )?;
1300                                quote!(#c)
1301                            }
1302                            (None, None) => quote!(0),
1303                            _ => unreachable!(),
1304                        };
1305                        let flags = match flags_kw {
1306                            Some(e) => flag_letters(&e)?,
1307                            None => String::new(),
1308                        };
1309                        let p = qual("sub");
1310                        Ok(quote!(#p(&(#pat), &(#repl), &(#text), #count, #flags)?))
1311                    }
1312                    // hashlib constructors: with initial data, or the
1313                    // empty + update() idiom.
1314                    ("md5" | "sha1" | "sha256" | "sha512", [data]) => {
1315                        let p = qual(&fname);
1316                        Ok(quote!(#p(&(#data))))
1317                    }
1318                    // io.StringIO: arity split — the seeded form starts
1319                    // with the cursor at 0, as in Python.
1320                    ("StringIO", []) => {
1321                        let p = qual("StringIO");
1322                        Ok(quote!(#p()))
1323                    }
1324                    ("StringIO", [initial]) => {
1325                        let p = qual("StringIO_seeded");
1326                        Ok(quote!(#p(&(#initial))))
1327                    }
1328                    // csv.writer(f) borrows the file mutably for the
1329                    // writer's lifetime (scope analysis marks f mut).
1330                    ("writer", [f]) => {
1331                        let p = qual("writer");
1332                        Ok(quote!(#p(&mut (#f))))
1333                    }
1334                    ("reader", [lines]) => {
1335                        let p = qual("reader");
1336                        Ok(quote!(#p(&(#lines))?))
1337                    }
1338                    ("md5" | "sha1" | "sha256" | "sha512", []) => {
1339                        let p = qual(&format!("{}_new", fname));
1340                        Ok(quote!(#p()))
1341                    }
1342                    ("indent", [s, prefix]) => {
1343                        let p = qual("indent");
1344                        Ok(quote!(#p(&(#s), &(#prefix))))
1345                    }
1346                    ("heappush" | "heappushpop" | "heapreplace" | "indent"
1347                        | "nlargest" | "nsmallest", _) => Err(arity("2")),
1348                    _ => Err(arity("the documented number of")),
1349                };
1350            }
1351        }
1352
1353        // Constructing a class instance: `Point(args)` lowers to
1354        // `Point::new(args)?`, with arguments resolved against __init__'s
1355        // signature (minus self) so keywords and defaults follow Python
1356        // call semantics.
1357        if let ExprType::Name(n) = self.func.as_ref() {
1358            if let Some(SymbolTableNode::ClassDef(c)) = symbols.get(&n.id) {
1359                let cname = crate::safe_ident(&n.id);
1360                match c.init_method() {
1361                    Some(init) => {
1362                        if init.args.vararg.is_some() || init.args.kwarg.is_some() {
1363                            return Err(format!(
1364                                "`{}.__init__` takes *args/**kwargs, which is not \
1365                                 supported yet",
1366                                n.id
1367                            )
1368                            .into());
1369                        }
1370                        let mut sig = init.clone();
1371                        crate::strip_self(&mut sig.args);
1372                        let mapped = map_call_arguments(
1373                            &sig,
1374                            &self.args,
1375                            &self.keywords,
1376                            &ctx,
1377                            &options,
1378                            &symbols,
1379                        )?;
1380                        return Ok(quote!(#cname::new(#(#mapped),*)?));
1381                    }
1382                    None => {
1383                        if !self.args.is_empty() || !self.keywords.is_empty() {
1384                            return Err(format!(
1385                                "{}() takes no arguments: the class defines no __init__",
1386                                n.id
1387                            )
1388                            .into());
1389                        }
1390                        return Ok(quote!(#cname::new()?));
1391                    }
1392                }
1393            }
1394        }
1395
1396        // Python methods whose Rust inherent namesakes have DIFFERENT
1397        // semantics (or the wrong shape) are rewritten here; methods with no
1398        // Rust conflict resolve through the stdpython PyListOps/PyStrOps
1399        // traits without any rewriting.
1400        if let ExprType::Attribute(attr) = self.func.as_ref() {
1401            // A method call on a receiver whose class is known — `self`
1402            // inside a method, or a name assigned a construction — resolves
1403            // against the class's own methods FIRST, so a user-defined
1404            // method named like a builtin (`get`, `pop`, ...) is not
1405            // rewritten out from under the class. Calls propagate
1406            // exceptions (`?`) and map keywords/defaults like any user
1407            // function call.
1408            if let Some(class) = receiver_class(&attr.value, &ctx, &symbols) {
1409                if let Some(method) = class.methods().find(|m| m.name == attr.attr).cloned() {
1410                    if method.args.vararg.is_some() || method.args.kwarg.is_some() {
1411                        return Err(format!(
1412                            "`{}.{}` takes *args/**kwargs, which is not supported yet",
1413                            class.name, method.name
1414                        )
1415                        .into());
1416                    }
1417                    let mut sig = method;
1418                    crate::strip_self(&mut sig.args);
1419                    let mapped = map_call_arguments(
1420                        &sig,
1421                        &self.args,
1422                        &self.keywords,
1423                        &ctx,
1424                        &options,
1425                        &symbols,
1426                    )?;
1427                    let receiver = attr.value.clone().to_rust(
1428                        ctx.clone(),
1429                        options.clone(),
1430                        symbols.clone(),
1431                    )?;
1432                    let method_name = crate::safe_ident(&attr.attr);
1433                    return Ok(quote!((#receiver).#method_name(#(#mapped),*)?));
1434                }
1435            }
1436            // A mutating method on a subscripted receiver must go through
1437            // the PLACE lowering: `xs[0].append(v)` has to mutate the real
1438            // element, where the Load lowering (py_index) yields a clone
1439            // and the write silently vanishes.
1440            let receiver = if matches!(attr.value.as_ref(), ExprType::Subscript(_))
1441                && crate::ast::tree::scope::MUTATING_METHODS.contains(&attr.attr.as_str())
1442            {
1443                crate::ast::tree::subscript::subscript_receiver_place(
1444                    attr.value.as_ref(),
1445                    ctx.clone(),
1446                    options.clone(),
1447                    symbols.clone(),
1448                )?
1449            } else {
1450                attr.value
1451                    .clone()
1452                    .to_rust(ctx.clone(), options.clone(), symbols.clone())?
1453            };
1454
1455            // list.sort(): in-place, stable, with Python's keyword-only
1456            // key=/reverse=. Vec's inherent sort demands a total order
1457            // (rejecting floats), so every shape routes through the
1458            // PySort variants, which share sorted()'s NaN-loud comparator
1459            // and run key exactly once per element.
1460            if attr.attr == "sort" {
1461                if !self.args.is_empty() {
1462                    return Err("sort() takes no positional arguments".to_string().into());
1463                }
1464                let mut key = None;
1465                let mut reverse = None;
1466                for kw in &self.keywords {
1467                    match kw.arg.as_deref() {
1468                        Some("key") if key.is_none() => key = Some(kw.value.clone()),
1469                        Some("reverse") if reverse.is_none() => {
1470                            reverse = Some(kw.value.clone())
1471                        }
1472                        other => {
1473                            return Err(format!(
1474                                "sort() got an unexpected or duplicate keyword \
1475                                 argument '{}'",
1476                                other.unwrap_or("**kwargs")
1477                            )
1478                            .into());
1479                        }
1480                    }
1481                }
1482                let render = |e: crate::ExprType| {
1483                    e.to_rust(ctx.clone(), options.clone(), symbols.clone())
1484                };
1485                return Ok(match (key, reverse) {
1486                    (None, None) => quote!((#receiver).py_sort()),
1487                    (None, Some(r)) => {
1488                        let r = render(r)?;
1489                        quote!((#receiver).py_sort_reverse(#r))
1490                    }
1491                    (Some(k), None) => {
1492                        let k = render(k)?;
1493                        quote!((#receiver).py_sort_key(#k))
1494                    }
1495                    (Some(k), Some(r)) => {
1496                        let k = render(k)?;
1497                        let r = render(r)?;
1498                        quote!((#receiver).py_sort_key_reverse(#k, #r))
1499                    }
1500                });
1501            }
1502
1503            // replace(...) with datetime-family keywords: one lowering
1504            // through the PyReplace trait, whose receiver impls
1505            // (datetime/date/time) each validate their own field set and
1506            // raise Python's exact TypeError for foreign fields. Only
1507            // keyword spellings route here — bare/positional replace
1508            // stays the plain method call (str.replace among others).
1509            if attr.attr == "replace" && !self.keywords.is_empty() {
1510                const FIELDS: [&str; 7] = [
1511                    "year", "month", "day", "hour", "minute", "second", "microsecond",
1512                ];
1513                if self
1514                    .keywords
1515                    .iter()
1516                    .all(|kw| kw.arg.as_deref().is_some_and(|a| FIELDS.contains(&a)))
1517                {
1518                    let mut slots: [Option<crate::ExprType>; 7] = Default::default();
1519                    if self.args.len() > FIELDS.len() {
1520                        return Err("replace() takes at most 7 arguments".to_string().into());
1521                    }
1522                    for (i, arg) in self.args.iter().enumerate() {
1523                        slots[i] = Some(arg.clone());
1524                    }
1525                    for kw in &self.keywords {
1526                        let name = kw.arg.as_deref().expect("checked above");
1527                        let idx = FIELDS.iter().position(|f| *f == name).expect("checked");
1528                        if slots[idx].is_some() {
1529                            return Err(format!(
1530                                "replace() got multiple values for argument '{}'",
1531                                name
1532                            )
1533                            .into());
1534                        }
1535                        slots[idx] = Some(kw.value.clone());
1536                    }
1537                    let mut inits = Vec::new();
1538                    for (idx, slot) in slots.into_iter().enumerate() {
1539                        if let Some(e) = slot {
1540                            let field = crate::safe_ident(FIELDS[idx]);
1541                            let v =
1542                                e.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1543                            inits.push(quote!(#field: Some(#v)));
1544                        }
1545                    }
1546                    return Ok(quote!(
1547                        (#receiver).py_replace(ReplaceArgs {
1548                            #(#inits,)*
1549                            ..ReplaceArgs::default()
1550                        })?
1551                    ));
1552                }
1553                // A keyword outside the field set: Python's message, at
1554                // conversion time (str.replace takes no keywords here).
1555                let bad = self
1556                    .keywords
1557                    .iter()
1558                    .find(|kw| {
1559                        !kw.arg.as_deref().is_some_and(|a| FIELDS.contains(&a))
1560                    })
1561                    .and_then(|kw| kw.arg.clone())
1562                    .unwrap_or_else(|| "**kwargs".to_string());
1563                return Err(format!(
1564                    "'{}' is an invalid keyword argument for replace()",
1565                    bad
1566                )
1567                .into());
1568            }
1569
1570            // str.split / str.rsplit take sep and maxsplit by position or
1571            // keyword, with sep=None (or absent) meaning whitespace mode.
1572            // Normalized here so every spelling maps to the right runtime
1573            // variant; unknown or duplicate keywords are loud errors, as
1574            // Python raises TypeError for them.
1575            if matches!(attr.attr.as_str(), "split" | "rsplit") {
1576                if self.args.len() > 2 {
1577                    return Err(format!(
1578                        "{}() takes at most 2 arguments ({} given)",
1579                        attr.attr,
1580                        self.args.len()
1581                    )
1582                    .into());
1583                }
1584                let mut sep = self.args.first().cloned();
1585                let mut maxsplit = self.args.get(1).cloned();
1586                for kw in &self.keywords {
1587                    match kw.arg.as_deref() {
1588                        Some("sep") => {
1589                            if sep.is_some() {
1590                                return Err(format!(
1591                                    "{}() got multiple values for argument 'sep'",
1592                                    attr.attr
1593                                )
1594                                .into());
1595                            }
1596                            sep = Some(kw.value.clone());
1597                        }
1598                        Some("maxsplit") => {
1599                            if maxsplit.is_some() {
1600                                return Err(format!(
1601                                    "{}() got multiple values for argument 'maxsplit'",
1602                                    attr.attr
1603                                )
1604                                .into());
1605                            }
1606                            maxsplit = Some(kw.value.clone());
1607                        }
1608                        other => {
1609                            return Err(format!(
1610                                "{}() got an unexpected keyword argument '{}'",
1611                                attr.attr,
1612                                other.unwrap_or("**kwargs")
1613                            )
1614                            .into());
1615                        }
1616                    }
1617                }
1618                let is_rsplit = attr.attr == "rsplit";
1619                let sep = sep.filter(|s| !crate::is_none_expr(s));
1620                return Ok(match (sep, maxsplit) {
1621                    (None, None) => quote!((#receiver).py_split_whitespace()),
1622                    (None, Some(m)) => {
1623                        let m = m.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1624                        if is_rsplit {
1625                            quote!((#receiver).py_rsplit_whitespace_maxsplit(#m))
1626                        } else {
1627                            quote!((#receiver).py_split_whitespace_maxsplit(#m))
1628                        }
1629                    }
1630                    (Some(s), None) => {
1631                        let s = s.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1632                        if is_rsplit {
1633                            quote!((#receiver).py_rsplit(&(#s))?)
1634                        } else {
1635                            quote!((#receiver).py_split(&(#s))?)
1636                        }
1637                    }
1638                    (Some(s), Some(m)) => {
1639                        let s = s.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1640                        let m = m.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1641                        if is_rsplit {
1642                            quote!((#receiver).py_rsplit_maxsplit(&(#s), #m)?)
1643                        } else {
1644                            quote!((#receiver).py_split_maxsplit(&(#s), #m)?)
1645                        }
1646                    }
1647                });
1648            }
1649
1650            // str.format on a LITERAL template translates to format! at
1651            // conversion time: auto-numbering, {0} positions, {name}
1652            // keywords, {{ escaping, and format specs all map — and any
1653            // spec Rust cannot reproduce exactly is a loud conversion
1654            // error, never approximated output.
1655            if attr.attr == "format" {
1656                let template = match attr.value.as_ref() {
1657                    ExprType::Constant(c)
1658                        if matches!(&c.0, Some(litrs::Literal::String(_))) =>
1659                    {
1660                        match &c.0 {
1661                            Some(litrs::Literal::String(s)) => s.value().to_string(),
1662                            _ => unreachable!(),
1663                        }
1664                    }
1665                    _ => {
1666                        return Err(
1667                            "str.format on a non-literal template is not supported yet: \
1668                             the template must be a string literal so the fields can be \
1669                             checked at conversion time"
1670                                .to_string()
1671                                .into(),
1672                        );
1673                    }
1674                };
1675                return lower_str_format(
1676                    &template,
1677                    &self.args,
1678                    &self.keywords,
1679                    &ctx,
1680                    &options,
1681                    &symbols,
1682                );
1683            }
1684
1685            // The remaining builtin methods are positional-only in Python;
1686            // a keyword here would be silently dropped by the positional
1687            // pattern match below, so fall through to the generic path,
1688            // which rejects keywords without a resolvable signature.
1689            if !self.keywords.is_empty() {
1690                // fall through
1691            } else {
1692            let mut rendered_args = Vec::new();
1693            for arg in &self.args {
1694                rendered_args.push(arg.clone().to_rust(
1695                    ctx.clone(),
1696                    options.clone(),
1697                    symbols.clone(),
1698                )?);
1699            }
1700            match (attr.attr.as_str(), rendered_args.as_slice()) {
1701                // list.append(x) pushes one element; Vec::append (inherent)
1702                // concatenates another Vec — silently different.
1703                ("append", [value]) => {
1704                    return Ok(quote!((#receiver).push(#value)));
1705                }
1706                // list.count(x): the PyListOps method takes a reference.
1707                ("count", [value]) => {
1708                    return Ok(quote!((#receiver).count(&(#value))));
1709                }
1710                // File-object and csv.Writer methods return Result (I/O
1711                // can fail; Python raises): thread `?`.
1712                ("read", []) | ("readline", []) | ("readlines", []) | ("close", [])
1713                | ("getvalue", []) => {
1714                    let m = crate::safe_ident(&attr.attr);
1715                    return Ok(quote!((#receiver).#m()?));
1716                }
1717                ("write", [d]) => {
1718                    return Ok(quote!((#receiver).write(&(#d))?));
1719                }
1720                ("writelines", [l]) => {
1721                    return Ok(quote!((#receiver).writelines(&(#l))?));
1722                }
1723                ("writerow", [r]) => {
1724                    // writerow([]) (an empty record) still needs an
1725                    // element type for the slice.
1726                    if r.to_string() == "vec ! []" {
1727                        return Ok(quote!((#receiver).writerow(&[] as &[&str])?));
1728                    }
1729                    return Ok(quote!((#receiver).writerow(&(#r))?));
1730                }
1731                ("writerows", [r]) => {
1732                    return Ok(quote!((#receiver).writerows(&(#r))?));
1733                }
1734                // re Match: m.group() is m.group(0); Rust can't overload.
1735                ("group", []) => {
1736                    return Ok(quote!((#receiver).group(0)));
1737                }
1738                // m.group("name") for (?P<name>...) groups: Rust can't
1739                // overload on the argument type, so the string spelling
1740                // routes to group_name. Numeric group(i) falls through to
1741                // the plain method call.
1742                ("group", [g]) if g.to_string().starts_with('"') => {
1743                    return Ok(quote!((#receiver).group_name(#g)));
1744                }
1745                // str.encode() / encode("utf-8"): UTF-8 bytes, which is
1746                // exactly what Rust strings hold.
1747                ("encode", []) => {
1748                    return Ok(quote!((#receiver).as_bytes().to_vec()));
1749                }
1750                ("encode", [enc]) => {
1751                    if enc.to_string().trim_matches('"') != "utf-8" {
1752                        return Err(format!(
1753                            "str.encode({}): only \"utf-8\" is supported",
1754                            enc
1755                        )
1756                        .into());
1757                    }
1758                    return Ok(quote!((#receiver).as_bytes().to_vec()));
1759                }
1760                // list.pop() returns the last element or raises IndexError
1761                // (Vec::pop returns an Option).
1762                ("pop", []) => {
1763                    return Ok(quote! {
1764                        (#receiver).pop().ok_or_else(|| {
1765                            PyException::new("IndexError", "pop from empty list")
1766                        })?
1767                    });
1768                }
1769                // pop with an argument dispatches by receiver through the
1770                // PyPop trait: list.pop(i) by index (IndexError), dict.pop(k)
1771                // by key (KeyError).
1772                ("pop", [arg]) => {
1773                    return Ok(quote!((#receiver).py_pop(#arg)?));
1774                }
1775                ("pop", [key, default]) => {
1776                    return Ok(quote!((#receiver).py_pop_default(#key, #default)));
1777                }
1778                // dict.get never raises: value-or-None (an Option), or the
1779                // provided default. IndexMap's inherent get returns a
1780                // borrowed Option, so both forms map to py_ versions.
1781                ("get", [key]) => {
1782                    return Ok(quote!((#receiver).py_get(&(#key))));
1783                }
1784                ("get", [key, default]) => {
1785                    return Ok(quote!((#receiver).py_get_default(&(#key), #default)));
1786                }
1787                // Views materialize as Vecs in insertion order.
1788                ("keys", []) => {
1789                    return Ok(quote!((#receiver).py_keys()));
1790                }
1791                ("values", []) => {
1792                    return Ok(quote!((#receiver).py_values()));
1793                }
1794                ("items", []) => {
1795                    return Ok(quote!((#receiver).py_items()));
1796                }
1797                ("setdefault", [key, default]) => {
1798                    return Ok(quote!((#receiver).py_setdefault(#key, #default)));
1799                }
1800                // list.remove(x) removes by VALUE and raises ValueError;
1801                // Vec::remove removes by index — silently different.
1802                ("remove", [value]) => {
1803                    return Ok(quote! {
1804                        {
1805                            let __rython_pos = (#receiver)
1806                                .iter()
1807                                .position(|__rython_e| __rython_e == &(#value))
1808                                .ok_or_else(|| {
1809                                    PyException::new(
1810                                        "ValueError",
1811                                        "list.remove(x): x not in list",
1812                                    )
1813                                })?;
1814                            (#receiver).remove(__rython_pos);
1815                        }
1816                    });
1817                }
1818                // list.insert follows Python index rules (negative counts
1819                // from the end, out-of-range clamps); Vec::insert takes a
1820                // usize and panics past len.
1821                ("insert", [idx, value]) => {
1822                    return Ok(quote!((#receiver).py_insert(#idx, #value)));
1823                }
1824                // partition/rpartition raise ValueError on an empty
1825                // separator, so the calls take `?`.
1826                ("partition", [sep]) => {
1827                    return Ok(quote!((#receiver).partition(&(#sep))?));
1828                }
1829                ("rpartition", [sep]) => {
1830                    return Ok(quote!((#receiver).rpartition(&(#sep))?));
1831                }
1832                // strip family with a chars argument (the no-arg forms
1833                // resolve through PyStrOps directly).
1834                ("strip", [chars]) => {
1835                    return Ok(quote!((#receiver).py_strip_chars(&(#chars))));
1836                }
1837                ("lstrip", [chars]) => {
1838                    return Ok(quote!((#receiver).py_lstrip_chars(&(#chars))));
1839                }
1840                ("rstrip", [chars]) => {
1841                    return Ok(quote!((#receiver).py_rstrip_chars(&(#chars))));
1842                }
1843                // ljust/rjust: the optional fillchar selects the py_ form
1844                // (space by default).
1845                ("ljust", [width]) => {
1846                    return Ok(quote!((#receiver).py_ljust(#width, " ")?));
1847                }
1848                ("ljust", [width, fill]) => {
1849                    return Ok(quote!((#receiver).py_ljust(#width, &(#fill))?));
1850                }
1851                ("rjust", [width]) => {
1852                    return Ok(quote!((#receiver).py_rjust(#width, " ")?));
1853                }
1854                ("rjust", [width, fill]) => {
1855                    return Ok(quote!((#receiver).py_rjust(#width, &(#fill))?));
1856                }
1857                // str.find returns -1 when absent; str::find an Option.
1858                ("find", [needle]) => {
1859                    return Ok(quote!((#receiver).py_find(&(#needle))));
1860                }
1861                _ => {}
1862            }
1863            }
1864        }
1865
1866        // Keyword arguments and omitted defaulted parameters resolve
1867        // against the callee's signature: keywords map to their parameter
1868        // positions and missing parameters fill from their default values,
1869        // matching Python call semantics. Without a known signature,
1870        // keywords would silently become misordered positional arguments —
1871        // that is a loud conversion error instead.
1872        let callee = match self.func.as_ref() {
1873            ExprType::Name(n) => match symbols.get(&n.id) {
1874                Some(SymbolTableNode::FunctionDef(f)) => Some(f.clone()),
1875                _ => None,
1876            },
1877            _ => None,
1878        };
1879        if let Some(callee_def) = &callee {
1880            let simple_signature =
1881                callee_def.args.vararg.is_none() && callee_def.args.kwarg.is_none();
1882            let pos_param_count =
1883                callee_def.args.posonlyargs.len() + callee_def.args.args.len();
1884            let has_optional_params = callee_def
1885                .args
1886                .posonlyargs
1887                .iter()
1888                .chain(callee_def.args.args.iter())
1889                .chain(callee_def.args.kwonlyargs.iter())
1890                .any(|p| {
1891                    p.annotation
1892                        .as_deref()
1893                        .is_some_and(crate::is_optional_annotation)
1894                });
1895            let needs_mapping = !self.keywords.is_empty()
1896                || !callee_def.args.kwonlyargs.is_empty()
1897                || self.args.len() < pos_param_count
1898                || has_optional_params;
1899            if simple_signature && needs_mapping {
1900                let mapped = map_call_arguments(
1901                    callee_def,
1902                    &self.args,
1903                    &self.keywords,
1904                    &ctx,
1905                    &options,
1906                    &symbols,
1907                )?;
1908                let name = self.func.to_rust(ctx, options, symbols)?;
1909                let call = quote!(#name(#(#mapped),*));
1910                return Ok(if propagates_exceptions {
1911                    quote!(#call?)
1912                } else {
1913                    call
1914                });
1915            }
1916            if !simple_signature && !self.keywords.is_empty() {
1917                return Err(format!(
1918                    "keyword arguments in a call to `{}` are not supported yet: \
1919                     its signature takes *args/**kwargs",
1920                    callee_def.name
1921                )
1922                .into());
1923            }
1924        } else if !self.keywords.is_empty() {
1925            return Err(format!(
1926                "keyword arguments require the callee's signature, and `{}` is not \
1927                 a function defined in this module; pass the arguments positionally",
1928                self.func
1929                    .clone()
1930                    .to_rust(ctx.clone(), options.clone(), symbols.clone())
1931                    .map(|t| t.to_string())
1932                    .unwrap_or_else(|_| "<callee>".to_string())
1933            )
1934            .into());
1935        }
1936
1937        let name = self.func.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1938
1939        let mut all_args = Vec::new();
1940
1941        // Add positional arguments
1942        for arg in self.args {
1943            let rust_arg = arg.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1944            all_args.push(rust_arg);
1945        }
1946        
1947        // Add keyword arguments
1948        for keyword in self.keywords {
1949            let rust_kw = keyword.to_rust(ctx.clone(), options.clone(), symbols.clone())?;
1950            all_args.push(rust_kw);
1951        }
1952        
1953        // Check if we're in an async context and if the function being called is async
1954        let call_expr = quote!(#name(#(#all_args),*));
1955        
1956        // Check if this function returns a Result that should be unwrapped
1957        let name_str = format!("{}", name);
1958
1959        // datetime.strptime parses and validates, so it raises ValueError
1960        // like Python; propagate rather than hand back a bare Result.
1961        if name_str.ends_with(":: strptime") {
1962            return Ok(quote!(#call_expr?));
1963        }
1964        let needs_unwrap = matches!(name_str.as_str(), 
1965            "subprocess :: run" | "subprocess :: run_with_env" | "subprocess :: check_call" | 
1966            "subprocess :: check_output" | "os :: getcwd" | "os :: chdir" | "os :: execv" |
1967            "os :: path :: abspath"
1968        );
1969        
1970        // Special handling for subprocess.run and os.execv with fallback for compatibility
1971        let final_call = if propagates_exceptions {
1972            quote!(#call_expr?)
1973        } else if name_str == "subprocess :: run" {
1974            // Try mixed_args version first, fallback to regular version
1975            if all_args.len() >= 2 {
1976                let args_param = &all_args[0];
1977                let cwd_param = &all_args[1];
1978                // Convert args to Vec<String> to avoid lifetime issues, then pass owned strings
1979                quote!({
1980                    let args_owned: Vec<String> = #args_param;
1981                    let args_vec: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
1982                    let cwd_str = #cwd_param;
1983                    subprocess::run(args_vec, Some(&cwd_str)).unwrap()
1984                })
1985            } else {
1986                let args_param = &all_args[0];
1987                quote!({
1988                    let args_owned: Vec<String> = #args_param;
1989                    let args_vec: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
1990                    subprocess::run(args_vec, None).unwrap()
1991                })
1992            }
1993        } else if name_str == "os :: execv" {
1994            // Convert to Vec<&str> for compatibility with standard execv function
1995            let program_param = &all_args[0];
1996            let args_param = &all_args[1];
1997            quote!({
1998                let program_str: String = (#program_param).clone();
1999                let args_owned: Vec<String> = #args_param;
2000                let args_vec: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
2001                os::execv(&program_str, args_vec).unwrap()
2002            })
2003        } else if needs_unwrap {
2004            quote!(#call_expr.unwrap())
2005        } else {
2006            call_expr
2007        };
2008        
2009        // `.await` is added only by an explicit `await` expression (the Await
2010        // node), mirroring Python: calling an async function without await
2011        // does not implicitly run it. The old behavior appended `.await` to
2012        // any call whose name started with "a" in async contexts, which broke
2013        // calls like abs(x).
2014        Ok(final_call)
2015    }
2016}
2017
2018/// Lower a literal `template.format(args...)` call to a Rust `format!`.
2019///
2020/// Every argument (used or not) is evaluated exactly once, in Python's
2021/// order, into a local binding — Python evaluates unused arguments too.
2022/// Used bindings are referenced from the format string by name; unused
2023/// ones bind to `_` so no warning fires. Errors mirror Python's:
2024/// mixing auto and manual numbering, out-of-range indices, and missing
2025/// keywords are conversion-time failures.
2026fn lower_str_format(
2027    template: &str,
2028    args: &[ExprType],
2029    keywords: &[Keyword],
2030    ctx: &CodeGenContext,
2031    options: &PythonOptions,
2032    symbols: &SymbolTableScopes,
2033) -> Result<TokenStream, Box<dyn std::error::Error>> {
2034    use crate::pyformat::{parse_template, translate_format_spec, FieldRef, Piece};
2035
2036    let pieces = parse_template(template).map_err(|e| format!("str.format: {}", e))?;
2037
2038    for kw in keywords {
2039        if kw.arg.is_none() {
2040            return Err("str.format with **kwargs is not supported yet".to_string().into());
2041        }
2042    }
2043
2044    // Resolve each field to an argument slot and build the format string.
2045    let mut fmt = String::new();
2046    let mut used_positions: std::collections::HashSet<usize> = Default::default();
2047    let mut used_names: std::collections::HashSet<String> = Default::default();
2048    let mut auto_next = 0usize;
2049    let mut saw_auto = false;
2050    let mut saw_manual = false;
2051    let mut field_bindings: Vec<TokenStream> = Vec::new();
2052    for piece in &pieces {
2053        match piece {
2054            Piece::Literal(text) => {
2055                fmt.push_str(&text.replace('{', "{{").replace('}', "}}"));
2056            }
2057            Piece::Field { arg, conversion, spec } => {
2058                let index_name = match arg {
2059                    FieldRef::Auto => {
2060                        saw_auto = true;
2061                        let i = auto_next;
2062                        auto_next += 1;
2063                        if i >= args.len() {
2064                            return Err(format!(
2065                                "str.format: not enough positional arguments (field {} of \
2066                                 template {:?})",
2067                                i, template
2068                            )
2069                            .into());
2070                        }
2071                        used_positions.insert(i);
2072                        format!("__rython_fmt{}", i)
2073                    }
2074                    FieldRef::Index(i) => {
2075                        saw_manual = true;
2076                        if *i >= args.len() {
2077                            return Err(format!(
2078                                "str.format: replacement index {} out of range for \
2079                                 template {:?}",
2080                                i, template
2081                            )
2082                            .into());
2083                        }
2084                        used_positions.insert(*i);
2085                        format!("__rython_fmt{}", i)
2086                    }
2087                    FieldRef::Name(name) => {
2088                        if !keywords
2089                            .iter()
2090                            .any(|k| k.arg.as_deref() == Some(name.as_str()))
2091                        {
2092                            return Err(format!(
2093                                "str.format: template {:?} refers to {:?}, which is not \
2094                                 among the keyword arguments",
2095                                template, name
2096                            )
2097                            .into());
2098                        }
2099                        used_names.insert(name.clone());
2100                        format!("__rython_fmt_{}", name)
2101                    }
2102                };
2103                if saw_auto && saw_manual {
2104                    return Err(
2105                        "str.format: cannot switch between automatic field numbering \
2106                         and manual field specification"
2107                            .to_string()
2108                            .into(),
2109                    );
2110                }
2111                let is_repr = matches!(conversion, Some('r') | Some('a'));
2112                let lowering =
2113                    translate_format_spec(spec).map_err(|e| format!("str.format: {}", e))?;
2114                if is_repr {
2115                    // Python's !r renders the repr STRING and applies the
2116                    // spec to it; Rust's `{:?}` would print its own Debug
2117                    // form ("ab" with double quotes) and diverge.
2118                    let crate::pyformat::SpecLowering::Inline(suffix) = lowering else {
2119                        return Err("str.format: numeric presentation types cannot combine \
2120                                    with !r/!a (Python applies the spec to the repr string \
2121                                    and raises)"
2122                            .to_string()
2123                            .into());
2124                    };
2125                    let fld = format!("__rython_fld{}", field_bindings.len());
2126                    let src = crate::safe_ident(&index_name);
2127                    let ident = crate::safe_ident(&fld);
2128                    field_bindings.push(quote!(let #ident = repr(&(#src));));
2129                    if suffix.is_empty() {
2130                        fmt.push_str(&format!("{{{}}}", fld));
2131                    } else {
2132                        fmt.push_str(&format!("{{{}:{}}}", fld, suffix));
2133                    }
2134                    continue;
2135                }
2136                match lowering {
2137                    crate::pyformat::SpecLowering::Inline(suffix) => {
2138                        if suffix.is_empty() {
2139                            fmt.push_str(&format!("{{{}}}", index_name));
2140                        } else {
2141                            fmt.push_str(&format!("{{{}:{}}}", index_name, suffix));
2142                        }
2143                    }
2144                    // The operand coerces or converts per-field (one
2145                    // argument may be reused with different specs), via a
2146                    // field-local binding referencing the argument's.
2147                    crate::pyformat::SpecLowering::CastF64(suffix) => {
2148                        let fld = format!("__rython_fld{}", field_bindings.len());
2149                        let src = crate::safe_ident(&index_name);
2150                        let ident = crate::safe_ident(&fld);
2151                        field_bindings.push(quote!(let #ident = (#src) as f64;));
2152                        if suffix.is_empty() {
2153                            fmt.push_str(&format!("{{{}}}", fld));
2154                        } else {
2155                            fmt.push_str(&format!("{{{}:{}}}", fld, suffix));
2156                        }
2157                    }
2158                    crate::pyformat::SpecLowering::IntRadix {
2159                        fill,
2160                        align,
2161                        plus,
2162                        alternate,
2163                        zero,
2164                        width,
2165                        radix,
2166                    } => {
2167                        let fld = format!("__rython_fld{}", field_bindings.len());
2168                        let src = crate::safe_ident(&index_name);
2169                        let ident = crate::safe_ident(&fld);
2170                        field_bindings.push(quote!(
2171                            let #ident = py_int_radix_format(
2172                                #src, #fill, #align, #plus, #alternate, #zero, #width, #radix,
2173                            );
2174                        ));
2175                        fmt.push_str(&format!("{{{}}}", fld));
2176                    }
2177                }
2178            }
2179        }
2180    }
2181
2182    // Bindings: every argument evaluates exactly once, in order.
2183    let mut bindings = TokenStream::new();
2184    for (i, arg) in args.iter().enumerate() {
2185        let value = arg.clone().to_rust(ctx.clone(), options.clone(), symbols.clone())?;
2186        if used_positions.contains(&i) {
2187            let ident = crate::safe_ident(&format!("__rython_fmt{}", i));
2188            bindings.extend(quote!(let #ident = #value;));
2189        } else {
2190            bindings.extend(quote!(let _ = #value;));
2191        }
2192    }
2193    for kw in keywords {
2194        let name = kw.arg.as_deref().unwrap_or_default();
2195        let value = kw
2196            .value
2197            .clone()
2198            .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
2199        if used_names.contains(name) {
2200            let ident = crate::safe_ident(&format!("__rython_fmt_{}", name));
2201            bindings.extend(quote!(let #ident = #value;));
2202        } else {
2203            bindings.extend(quote!(let _ = #value;));
2204        }
2205    }
2206
2207    for fb in field_bindings {
2208        bindings.extend(fb);
2209    }
2210
2211    Ok(quote!({
2212        #bindings
2213        format!(#fmt)
2214    }))
2215}
2216
2217/// The class of a method-call receiver, when it is statically known:
2218/// `self` inside a class's method body, or a local/module name whose
2219/// (symbol-table-recorded) assignment constructs a known class. Unknown
2220/// receivers return None and fall through to the generic lowering — where
2221/// a genuine user-method call fails to compile (loud), never silently
2222/// drops exception propagation.
2223pub(crate) fn receiver_class(
2224    recv: &ExprType,
2225    ctx: &CodeGenContext,
2226    symbols: &SymbolTableScopes,
2227) -> Option<crate::ClassDef> {
2228    let class_name = match recv {
2229        ExprType::Name(n) if n.id == "self" => ctx.enclosing_class_name()?.to_string(),
2230        ExprType::Name(n) => match symbols.get(&n.id) {
2231            Some(SymbolTableNode::Assign { value: ExprType::Call(call), .. }) => {
2232                match call.func.as_ref() {
2233                    ExprType::Name(cn) => cn.id.clone(),
2234                    _ => return None,
2235                }
2236            }
2237            _ => return None,
2238        },
2239        // Composition: `self.field.method()` resolves through the owner
2240        // class's field types.
2241        ExprType::Attribute(attr) => {
2242            let owner = receiver_class(&attr.value, ctx, symbols)?;
2243            owner.field_class(&attr.attr, symbols)?
2244        }
2245        _ => return None,
2246    };
2247    match symbols.get(&class_name) {
2248        Some(SymbolTableNode::ClassDef(c)) => Some(c.clone()),
2249        _ => None,
2250    }
2251}
2252
2253/// Resolve a call's arguments against the callee's signature, in Python's
2254/// order: positionals fill left to right, keywords map by name, missing
2255/// parameters take their default values, and every mismatch Python would
2256/// raise a TypeError for is a conversion-time error.
2257fn map_call_arguments(
2258    func: &crate::FunctionDef,
2259    args: &[ExprType],
2260    keywords: &[Keyword],
2261    ctx: &CodeGenContext,
2262    options: &PythonOptions,
2263    symbols: &SymbolTableScopes,
2264) -> Result<Vec<TokenStream>, Box<dyn std::error::Error>> {
2265    let fname = &func.name;
2266    // Optional-annotated parameters take Option values: the Option-slot
2267    // lowering wraps plain arguments in Some, passes None and
2268    // already-Option values (dict.get, another optional name, an
2269    // Optional-returning call) through unwrapped, and handles conditional
2270    // arms independently.
2271    let fill = |param: &crate::Parameter,
2272                expr: &ExprType|
2273     -> Result<TokenStream, Box<dyn std::error::Error>> {
2274        let optional = param
2275            .annotation
2276            .as_deref()
2277            .is_some_and(crate::is_optional_annotation);
2278        if optional {
2279            crate::lower_optional_value(expr, ctx.clone(), options.clone(), symbols.clone())
2280        } else {
2281            expr.clone().to_rust(ctx.clone(), options.clone(), symbols.clone())
2282        }
2283    };
2284
2285    let pos_params: Vec<&crate::Parameter> = func
2286        .args
2287        .posonlyargs
2288        .iter()
2289        .chain(func.args.args.iter())
2290        .collect();
2291    let n = pos_params.len();
2292    if args.len() > n {
2293        return Err(format!(
2294            "{}() takes {} positional argument(s) but {} were given",
2295            fname,
2296            n,
2297            args.len()
2298        )
2299        .into());
2300    }
2301
2302    let mut slots: Vec<Option<TokenStream>> = vec![None; n];
2303    for (i, arg) in args.iter().enumerate() {
2304        slots[i] = Some(fill(pos_params[i], arg)?);
2305    }
2306
2307    let mut kwonly_slots: Vec<Option<TokenStream>> = vec![None; func.args.kwonlyargs.len()];
2308    for kw in keywords {
2309        let Some(kw_name) = &kw.arg else {
2310            return Err(format!(
2311                "**kwargs unpacking in a call to {}() is not supported",
2312                fname
2313            )
2314            .into());
2315        };
2316        if let Some(idx) = pos_params.iter().position(|p| &p.arg == kw_name) {
2317            let value = fill(pos_params[idx], &kw.value)?;
2318            if idx < func.args.posonlyargs.len() {
2319                return Err(format!(
2320                    "{}(): parameter `{}` is positional-only and cannot be passed by keyword",
2321                    fname, kw_name
2322                )
2323                .into());
2324            }
2325            if slots[idx].is_some() {
2326                return Err(format!(
2327                    "{}() got multiple values for argument `{}`",
2328                    fname, kw_name
2329                )
2330                .into());
2331            }
2332            slots[idx] = Some(value);
2333        } else if let Some(idx) = func
2334            .args
2335            .kwonlyargs
2336            .iter()
2337            .position(|p| &p.arg == kw_name)
2338        {
2339            let value = fill(&func.args.kwonlyargs[idx], &kw.value)?;
2340            if kwonly_slots[idx].is_some() {
2341                return Err(format!(
2342                    "{}() got multiple values for argument `{}`",
2343                    fname, kw_name
2344                )
2345                .into());
2346            }
2347            kwonly_slots[idx] = Some(value);
2348        } else {
2349            return Err(format!(
2350                "{}() got an unexpected keyword argument `{}`",
2351                fname, kw_name
2352            )
2353            .into());
2354        }
2355    }
2356
2357    // Defaults align with the tail of the positional parameter list.
2358    let default_offset = n - func.args.defaults.len();
2359    for i in 0..n {
2360        if slots[i].is_none() {
2361            if i >= default_offset {
2362                slots[i] = Some(fill(pos_params[i], &func.args.defaults[i - default_offset])?);
2363            } else {
2364                return Err(format!(
2365                    "{}() missing required argument `{}`",
2366                    fname, pos_params[i].arg
2367                )
2368                .into());
2369            }
2370        }
2371    }
2372    for (i, param) in func.args.kwonlyargs.iter().enumerate() {
2373        if kwonly_slots[i].is_none() {
2374            match func.args.kw_defaults.get(i).and_then(|d| d.as_ref()) {
2375                Some(default) => kwonly_slots[i] = Some(fill(param, default)?),
2376                None => {
2377                    return Err(format!(
2378                        "{}() missing required keyword-only argument `{}`",
2379                        fname, param.arg
2380                    )
2381                    .into())
2382                }
2383            }
2384        }
2385    }
2386
2387    Ok(slots
2388        .into_iter()
2389        .chain(kwonly_slots)
2390        .map(|s| s.expect("all argument slots filled"))
2391        .collect())
2392}
2393
2394#[cfg(test)]
2395mod tests {
2396    use super::*;
2397
2398    #[test]
2399    fn test_lookup_of_function() {
2400        let options = PythonOptions::default();
2401        let result = crate::parse(
2402            "def foo(a = 7):
2403    pass
2404
2405foo(a=9)",
2406            "test.py",
2407        )
2408        .unwrap();
2409        let symbols = result.clone().find_symbols(SymbolTableScopes::new());
2410        let code = result
2411            .to_rust(
2412                CodeGenContext::Module("test".to_string()),
2413                options,
2414                symbols,
2415            )
2416            .unwrap()
2417            .to_string();
2418        assert!(code.contains("foo (9)"), "generated: {}", code);
2419    }
2420
2421    #[test]
2422    fn unknown_keyword_argument_is_a_conversion_error() {
2423        // Python raises TypeError for foo(b=9) when foo has no parameter b;
2424        // silently passing it positionally would be wrong.
2425        let options = PythonOptions::default();
2426        let result = crate::parse(
2427            "def foo(a = 7):
2428    pass
2429
2430foo(b=9)",
2431            "test.py",
2432        )
2433        .unwrap();
2434        let symbols = result.clone().find_symbols(SymbolTableScopes::new());
2435        let err = result
2436            .to_rust(
2437                CodeGenContext::Module("test".to_string()),
2438                options,
2439                symbols,
2440            )
2441            .expect_err("unexpected keyword must not convert");
2442        assert!(
2443            format!("{}", err).contains("unexpected keyword"),
2444            "error: {}",
2445            err
2446        );
2447    }
2448}