Skip to main content

python_ast/ast/tree/
class_def.rs

1//! Struct-based class lowering.
2//!
3//! A Python class lowers to a Rust struct plus an inherent impl block:
4//!
5//! - Instance attributes become struct fields, inferred from the `self.attr`
6//!   assignments in `__init__` (from annotated parameters, literals, or
7//!   construction of another known class).
8//! - `__init__` lowers as an ordinary method taking `&mut self`, and a
9//!   synthesized `new(...)` constructor default-initializes the struct and
10//!   runs it; `ClassName(...)` call sites lower to `ClassName::new(...)?`.
11//! - Methods lower as inherent methods; the receiver is `&self`, or
12//!   `&mut self` when the method stores through `self` (directly or by
13//!   calling another method of the class that does).
14//!
15//! Unsupported class constructs — inheritance, class-level statements,
16//! attributes whose types can't be inferred — are conversion-time errors,
17//! never silently dropped: lowering that diverges from Python must fail
18//! loudly.
19
20use 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    /// The class's `__init__` method, if it defines one.
41    pub fn init_method(&self) -> Option<&FunctionDef> {
42        self.methods().find(|m| m.name == "__init__")
43    }
44
45    /// The methods defined directly on the class, in source order.
46    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    /// The class of the value stored in field `attr`, when the field holds
54    /// an instance of another known class (composition): inferred from the
55    /// `__init__` stores, either a direct construction or an
56    /// annotated parameter whose annotation names a class.
57    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    /// Whether `method` mutates `self` — directly (attribute stores,
93    /// mutating container methods on `self.attr`) or transitively through
94    /// a call that bases at `self`: another method of this class
95    /// (`self.helper()`) or a mutating method of a composed field's class
96    /// (`self.inner.bump()`).
97    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        // A cycle in the call graph resolves optimistically: the mutation,
113        // if real, is found on the acyclic part of some path.
114        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        // The same resolver-backed analysis codegen uses, threading the
123        // visited set through recursive method resolution (RefCell because
124        // the resolver is a shared Fn).
125        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, &params, &resolve)
137            .needs_mut
138            .contains("self")
139    }
140}
141
142/// All parameter names of a method, `self` included.
143fn 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
155/// A `self.attr = value` assignment found in `__init__`, used for field
156/// inference.
157struct FieldStore<'a> {
158    attr: String,
159    value: &'a ExprType,
160}
161
162/// Collect `self.attr = ...` stores anywhere in a body (recursing into
163/// control flow), in first-store order.
164fn 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        // Inheritance changes method resolution and construction in ways a
225        // plain struct can't reproduce; failing loudly beats generating
226        // something that behaves differently. `object` is every class's
227        // implicit base, so naming it changes nothing.
228        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        // Only methods (plus a docstring and `pass`) are supported in class
246        // bodies. Class-level assignments (class attributes) would need a
247        // shared-state story; erroring is the loud option.
248        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        // The synthesized constructor occupies `new` in the inherent impl;
279        // a user method with that name would be a confusing duplicate-item
280        // compile error instead of a conversion-time one.
281        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        // ---- Field inference from __init__ ----
291        let mut fields: Vec<(String, TokenStream)> = Vec::new();
292        if let Some(init) = self.init_method() {
293            // Types known for names in the __init__ body: annotated
294            // parameters first, then simply-typed locals.
295            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                    // Mirror Parameter::to_rust: a `str` parameter becomes an
307                    // owned String local via the prologue. A parameter
308                    // annotated with a known class types the field as that
309                    // class's struct (composition).
310                    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        // ---- Methods ----
372        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        // ---- Synthesized constructor ----
385        // Python constructs with `ClassName(args)`: default-initialize the
386        // struct and run __init__ on it. Call sites lower to
387        // `ClassName::new(args)?` (see Call::to_rust).
388        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
454/// Remove the leading `self` parameter from a method's parameter list.
455pub(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
467/// Infer the struct field type for a value stored into `self.attr`.
468fn 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        // A constructed instance of a known class types the field as that
476        // class's struct.
477        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            // String literals are owned in fields; the store side converts
489            // (see Assign).
490            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}