1use proc_macro2::TokenStream;
21use pyo3::FromPyObject;
22use quote::quote;
23
24use crate::{
25 CodeGen, CodeGenContext, ExprType, FunctionDef, Name, PythonOptions, Statement,
26 StatementType, SymbolTableNode, SymbolTableScopes,
27};
28
29use serde::{Deserialize, Serialize};
30
31#[derive(Clone, Debug, Default, FromPyObject, Serialize, Deserialize, PartialEq)]
32pub struct ClassDef {
33 pub name: String,
34 pub bases: Vec<Name>,
35 pub keywords: Vec<String>,
36 pub body: Vec<Statement>,
37}
38
39impl ClassDef {
40 pub fn init_method(&self) -> Option<&FunctionDef> {
42 self.methods().find(|m| m.name == "__init__")
43 }
44
45 pub fn methods(&self) -> impl Iterator<Item = &FunctionDef> {
47 self.body.iter().filter_map(|s| match &s.statement {
48 StatementType::FunctionDef(f) => Some(f),
49 _ => None,
50 })
51 }
52
53 pub(crate) fn field_class(
58 &self,
59 attr: &str,
60 symbols: &SymbolTableScopes,
61 ) -> Option<String> {
62 let init = self.init_method()?;
63 let mut stores = Vec::new();
64 collect_field_stores(&init.body, &mut stores);
65 let store = stores.iter().find(|s| s.attr == attr)?;
66 let class_name = match store.value {
67 ExprType::Call(call) => match call.func.as_ref() {
68 ExprType::Name(n) => n.id.clone(),
69 _ => return None,
70 },
71 ExprType::Name(n) => {
72 let param = init
73 .args
74 .posonlyargs
75 .iter()
76 .chain(init.args.args.iter())
77 .chain(init.args.kwonlyargs.iter())
78 .find(|p| p.arg == n.id)?;
79 match param.annotation.as_deref() {
80 Some(ExprType::Name(ann)) => ann.id.clone(),
81 _ => return None,
82 }
83 }
84 _ => return None,
85 };
86 match symbols.get(&class_name) {
87 Some(SymbolTableNode::ClassDef(_)) => Some(class_name),
88 _ => None,
89 }
90 }
91
92 pub(crate) fn method_needs_mut_self(
98 &self,
99 method: &str,
100 symbols: &SymbolTableScopes,
101 ) -> bool {
102 let mut visited = std::collections::HashSet::new();
103 self.method_mut_inner(method, symbols, &mut visited)
104 }
105
106 fn method_mut_inner(
107 &self,
108 method: &str,
109 symbols: &SymbolTableScopes,
110 visited: &mut std::collections::HashSet<(String, String)>,
111 ) -> bool {
112 if !visited.insert((self.name.clone(), method.to_string())) {
115 return false;
116 }
117 let Some(m) = self.methods().find(|m| m.name == method) else {
118 return false;
119 };
120 let params = method_param_names(m);
121 let ctx = CodeGenContext::Class(self.name.clone());
122 let visited = std::cell::RefCell::new(visited);
126 let resolve = |call: &crate::Call| -> Option<bool> {
127 let ExprType::Attribute(attr) = call.func.as_ref() else {
128 return None;
129 };
130 let class = crate::receiver_class(&attr.value, &ctx, symbols)?;
131 if !class.methods().any(|mm| mm.name == attr.attr) {
132 return None;
133 }
134 Some(class.method_mut_inner(&attr.attr, symbols, &mut **visited.borrow_mut()))
135 };
136 crate::analyze_scope_with(&m.body, ¶ms, &resolve)
137 .needs_mut
138 .contains("self")
139 }
140}
141
142fn method_param_names(m: &FunctionDef) -> Vec<String> {
144 m.args
145 .posonlyargs
146 .iter()
147 .chain(m.args.args.iter())
148 .chain(m.args.kwonlyargs.iter())
149 .map(|p| p.arg.clone())
150 .chain(m.args.vararg.iter().map(|p| p.arg.clone()))
151 .chain(m.args.kwarg.iter().map(|p| p.arg.clone()))
152 .collect()
153}
154
155struct FieldStore<'a> {
158 attr: String,
159 value: &'a ExprType,
160}
161
162fn collect_field_stores<'a>(body: &'a [Statement], out: &mut Vec<FieldStore<'a>>) {
165 for stmt in body {
166 match &stmt.statement {
167 StatementType::Assign(assign) => {
168 for target in &assign.targets {
169 if let ExprType::Attribute(attr) = target {
170 if matches!(attr.value.as_ref(), ExprType::Name(n) if n.id == "self") {
171 out.push(FieldStore {
172 attr: attr.attr.clone(),
173 value: &assign.value,
174 });
175 }
176 }
177 }
178 }
179 StatementType::If(s) => {
180 collect_field_stores(&s.body, out);
181 collect_field_stores(&s.orelse, out);
182 }
183 StatementType::For(s) => {
184 collect_field_stores(&s.body, out);
185 collect_field_stores(&s.orelse, out);
186 }
187 StatementType::While(s) => {
188 collect_field_stores(&s.body, out);
189 collect_field_stores(&s.orelse, out);
190 }
191 StatementType::With(s) => collect_field_stores(&s.body, out),
192 StatementType::Try(s) => {
193 collect_field_stores(&s.body, out);
194 for h in &s.handlers {
195 collect_field_stores(&h.body, out);
196 }
197 collect_field_stores(&s.orelse, out);
198 collect_field_stores(&s.finalbody, out);
199 }
200 _ => {}
201 }
202 }
203}
204
205impl CodeGen for ClassDef {
206 type Context = CodeGenContext;
207 type Options = PythonOptions;
208 type SymbolTable = SymbolTableScopes;
209
210 fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
211 let mut symbols = symbols;
212 symbols.insert(self.name.clone(), SymbolTableNode::ClassDef(self.clone()));
213 symbols
214 }
215
216 fn to_rust(
217 self,
218 _ctx: Self::Context,
219 options: Self::Options,
220 symbols: Self::SymbolTable,
221 ) -> Result<TokenStream, Box<dyn std::error::Error>> {
222 let class_name = crate::safe_ident(&self.name);
223
224 let real_bases: Vec<&str> = self
229 .bases
230 .iter()
231 .map(|b| b.id.as_str())
232 .filter(|b| *b != "object")
233 .collect();
234 if !real_bases.is_empty() {
235 return Err(format!(
236 "class `{}` uses inheritance (base{} {}), which is not supported yet: \
237 classes lower to plain Rust structs",
238 self.name,
239 if real_bases.len() == 1 { "" } else { "s" },
240 real_bases.join(", "),
241 )
242 .into());
243 }
244
245 let body_start = if self.get_docstring().is_some() { 1 } else { 0 };
249 for stmt in self.body.iter().skip(body_start) {
250 match &stmt.statement {
251 StatementType::FunctionDef(_) | StatementType::Pass => {}
252 StatementType::AsyncFunctionDef(f) => {
253 return Err(format!(
254 "async method `{}.{}` is not supported yet",
255 self.name, f.name
256 )
257 .into());
258 }
259 other => {
260 let kind = match other {
261 StatementType::Assign(_) | StatementType::AugAssign(_) => {
262 "a class attribute assignment"
263 }
264 StatementType::ClassDef(_) => "a nested class",
265 StatementType::Import(_) | StatementType::ImportFrom(_) => "an import",
266 _ => "a statement",
267 };
268 return Err(format!(
269 "class `{}` contains {} at class level, which is not supported \
270 yet: only methods, a docstring, and `pass` lower",
271 self.name, kind,
272 )
273 .into());
274 }
275 }
276 }
277
278 if self.methods().any(|m| m.name == "new") {
282 return Err(format!(
283 "class `{}` defines a method named `new`, which collides with the \
284 constructor synthesized for `{}(...)` call sites; rename the method",
285 self.name, self.name
286 )
287 .into());
288 }
289
290 let mut fields: Vec<(String, TokenStream)> = Vec::new();
292 if let Some(init) = self.init_method() {
293 let mut name_types: std::collections::HashMap<String, TokenStream> =
296 std::collections::HashMap::new();
297 crate::collect_local_types(&init.body, &mut name_types);
298 for p in init
299 .args
300 .posonlyargs
301 .iter()
302 .chain(init.args.args.iter())
303 .chain(init.args.kwonlyargs.iter())
304 {
305 if let Some(ann) = p.annotation.as_deref() {
306 let ty = if matches!(ann, ExprType::Name(n) if n.id == "str") {
311 Some(quote!(String))
312 } else if let ExprType::Name(n) = ann {
313 match symbols.get(&n.id) {
314 Some(SymbolTableNode::ClassDef(_)) => {
315 let ident = crate::safe_ident(&n.id);
316 Some(quote!(#ident))
317 }
318 _ => crate::python_annotation_to_rust_type(ann),
319 }
320 } else {
321 crate::python_annotation_to_rust_type(ann)
322 };
323 if let Some(ty) = ty {
324 name_types.insert(p.arg.clone(), ty);
325 }
326 }
327 }
328
329 let mut stores = Vec::new();
330 collect_field_stores(&init.body, &mut stores);
331 for store in &stores {
332 let ty = infer_field_type(store.value, &name_types, &symbols);
333 match ty {
334 Some(ty) => {
335 match fields.iter().find(|(name, _)| *name == store.attr) {
336 None => fields.push((store.attr.clone(), ty)),
337 Some((_, prev)) if prev.to_string() == ty.to_string() => {}
338 Some((_, prev)) => {
339 return Err(format!(
340 "attribute `self.{}` of class `{}` is assigned \
341 conflicting types ({} and {}); a struct field needs \
342 one type",
343 store.attr, self.name, prev, ty
344 )
345 .into());
346 }
347 }
348 }
349 None => {
350 return Err(format!(
351 "cannot infer a type for attribute `self.{}` of class `{}`: \
352 assign it from an annotated __init__ parameter, a literal, \
353 or a constructed class instance (None-valued attributes are \
354 not supported yet)",
355 store.attr, self.name
356 )
357 .into());
358 }
359 }
360 }
361 }
362
363 let field_defs: Vec<TokenStream> = fields
364 .iter()
365 .map(|(name, ty)| {
366 let ident = crate::safe_ident(name);
367 quote!(pub #ident: #ty)
368 })
369 .collect();
370
371 let method_ctx = CodeGenContext::Class(self.name.clone());
373 let mut methods_stream = TokenStream::new();
374 for stmt in self.body.iter().skip(body_start) {
375 if let StatementType::FunctionDef(_) = &stmt.statement {
376 methods_stream.extend(stmt.clone().to_rust(
377 method_ctx.clone(),
378 options.clone(),
379 symbols.clone(),
380 )?);
381 }
382 }
383
384 let constructor = match self.init_method() {
389 Some(init) => {
390 if init.args.vararg.is_some() || init.args.kwarg.is_some() {
391 return Err(format!(
392 "`{}.__init__` takes *args/**kwargs, which is not supported yet",
393 self.name
394 )
395 .into());
396 }
397 let mut params = init.args.clone();
398 strip_self(&mut params);
399 let param_names: Vec<_> = params
400 .posonlyargs
401 .iter()
402 .chain(params.args.iter())
403 .chain(params.kwonlyargs.iter())
404 .map(|p| crate::safe_ident(&p.arg))
405 .collect();
406 let rendered = params.to_rust(
407 method_ctx.clone(),
408 options.clone(),
409 symbols.clone(),
410 )?;
411 quote! {
412 pub fn new(#rendered) -> Result<Self, PyException> {
413 let mut __rython_self = Self::default();
414 __rython_self.__init__(#(#param_names),*)?;
415 Ok(__rython_self)
416 }
417 }
418 }
419 None => quote! {
420 pub fn new() -> Result<Self, PyException> {
421 Ok(Self::default())
422 }
423 },
424 };
425
426 let docs = match self.get_docstring() {
427 Some(docstring) => {
428 let doc_lines: Vec<_> = docstring
429 .lines()
430 .map(|line| {
431 let doc_line = line.to_string();
432 quote! { #[doc = #doc_line] }
433 })
434 .collect();
435 quote!(#(#doc_lines)*)
436 }
437 None => quote!(),
438 };
439
440 Ok(quote! {
441 #docs
442 #[derive(Clone, Default)]
443 pub struct #class_name {
444 #(#field_defs),*
445 }
446 impl #class_name {
447 #constructor
448 #methods_stream
449 }
450 })
451 }
452}
453
454pub(crate) fn strip_self(args: &mut crate::ParameterList) {
456 if args
457 .posonlyargs
458 .first()
459 .is_some_and(|p| p.arg == "self")
460 {
461 args.posonlyargs.remove(0);
462 } else if args.args.first().is_some_and(|p| p.arg == "self") {
463 args.args.remove(0);
464 }
465}
466
467fn infer_field_type(
469 value: &ExprType,
470 name_types: &std::collections::HashMap<String, TokenStream>,
471 symbols: &SymbolTableScopes,
472) -> Option<TokenStream> {
473 match value {
474 ExprType::Name(n) => name_types.get(&n.id).cloned(),
475 ExprType::Call(call) => match call.func.as_ref() {
478 ExprType::Name(n) => match symbols.get(&n.id) {
479 Some(SymbolTableNode::ClassDef(_)) => {
480 let ident = crate::safe_ident(&n.id);
481 Some(quote!(#ident))
482 }
483 _ => None,
484 },
485 _ => None,
486 },
487 other => match crate::simple_expr_type(other) {
488 Some(ty) if ty.to_string() == "& 'static str" => Some(quote!(String)),
491 other => other,
492 },
493 }
494}
495
496impl ClassDef {
497 fn get_docstring(&self) -> Option<String> {
498 if self.body.is_empty() {
499 return None;
500 }
501
502 let expr = self.body[0].clone();
503 match expr.statement {
504 StatementType::Expr(e) => match e.value {
505 ExprType::Constant(c) => {
506 let raw_string = c.to_string();
507 Some(self.format_docstring(&raw_string))
508 }
509 _ => None,
510 },
511 _ => None,
512 }
513 }
514
515 fn format_docstring(&self, raw: &str) -> String {
516 let content = raw.trim_matches('"');
517 let lines: Vec<&str> = content.lines().collect();
518 if lines.is_empty() {
519 return String::new();
520 }
521
522 let mut formatted = vec![lines[0].trim().to_string()];
523
524 if lines.len() > 1 {
525 if !lines[0].trim().is_empty() && !lines[1].trim().is_empty() {
526 formatted.push(String::new());
527 }
528 for line in lines.iter().skip(1) {
529 let cleaned = line.trim();
530 if !cleaned.is_empty() {
531 formatted.push(cleaned.to_string());
532 }
533 }
534 }
535
536 formatted.join("\n")
537 }
538}