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}