Skip to main content

python_ast/ast/tree/
assign.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, FromPyObject, PyAny, PyResult, prelude::PyAnyMethods};
3use quote::quote;
4use serde::{Deserialize, Serialize};
5
6use crate::{extraction_failure, 
7    CodeGen, CodeGenContext, ExprType, PythonOptions, SymbolTableNode,
8    SymbolTableScopes,
9};
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
12pub struct Assign {
13    pub targets: Vec<ExprType>,
14    pub value: ExprType,
15    pub type_comment: Option<String>,
16}
17
18impl<'a, 'py> FromPyObject<'a, 'py> for Assign {
19    type Error = pyo3::PyErr;
20    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
21        let targets: Vec<ExprType> = ob
22            .getattr("targets")
23            .map_err(|e| extraction_failure("assignment targets", &ob, e))?
24            .extract()
25            .map_err(|e| extraction_failure("assignment targets", &ob, e))?;
26
27        let python_value = ob.getattr("value").map_err(|e| extraction_failure("value", &ob, e))?;
28
29        let value = python_value.extract().map_err(|e| extraction_failure("python_value", &ob, e))?;
30
31        Ok(Assign {
32            targets: targets,
33            value: value,
34            type_comment: None,
35        })
36    }
37}
38
39impl<'a> CodeGen for Assign {
40    type Context = CodeGenContext;
41    type Options = PythonOptions;
42    type SymbolTable = SymbolTableScopes;
43
44    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
45        let mut symbols = symbols;
46        let mut position = 0;
47        for target in self.targets {
48            // Only add symbols for Name assignments, not for Attribute assignments
49            if let ExprType::Name(name) = target {
50                symbols.insert(
51                    name.id,
52                    SymbolTableNode::Assign {
53                        position: position,
54                        value: self.value.clone(),
55                    },
56                );
57            }
58            // Could also handle other target types here if needed
59            position += 1;
60        }
61        symbols
62    }
63
64    fn to_rust(
65        self,
66        ctx: Self::Context,
67        options: Self::Options,
68        symbols: Self::SymbolTable,
69    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
70        let value_is_none_early = crate::is_none_expr(&self.value);
71        let value_yields_option = crate::expr_yields_option(&self.value, &options, &symbols);
72        let value_expr = self.value.clone();
73        let value = self
74            .value
75            .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
76
77        // Render one assignment for a single target. Python variables are
78        // function-scoped, so name targets are declared once (hoisted to a
79        // `let mut` at the top of the enclosing function/module scope by the
80        // scope's code generator) and every assignment is a plain store —
81        // emitting `let mut` per assignment would create a fresh shadowing
82        // binding inside nested blocks, silently dropping the store.
83        // A name that holds an Option (assigned None on some path) wraps
84        // its non-None stores in Some, so both arms unify to Option<T> —
85        // unless the value is already an Option (dict.get, another optional
86        // name, an Optional-returning call), which stores through unchanged.
87        let value_is_none = value_is_none_early;
88        // A string literal stored into an attribute becomes an owned String:
89        // struct fields hold String (Python strings are owned values), while
90        // the literal itself is a &'static str.
91        let value_is_str_literal = matches!(
92            &value_expr,
93            ExprType::Constant(c) if matches!(&c.0, Some(litrs::Literal::String(_)))
94        );
95        let render_one = |target: &ExprType,
96                          value: &TokenStream|
97         -> Result<TokenStream, Box<dyn std::error::Error>> {
98            let target_code =
99                target
100                    .clone()
101                    .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
102            Ok(match target {
103                ExprType::Name(name) => {
104                    if !value_is_none
105                        && !value_yields_option
106                        && options.optional_names.contains(&name.id)
107                    {
108                        quote!(#target_code = Some(#value);)
109                    } else {
110                        quote!(#target_code = #value;)
111                    }
112                }
113                // Destructuring assignment to the hoisted names.
114                ExprType::Tuple(_) => quote!((#target_code) = #value;),
115                ExprType::Attribute(_) if value_is_str_literal => {
116                    quote!(#target_code = (#value).to_string();)
117                }
118                _ => quote!(#target_code = #value;),
119            })
120        };
121
122        // Subscript stores don't go through the Load-position lowering
123        // (which reads via py_index): `x[i] = v` follows Python index rules
124        // through py_set_index — negatives from the end, catchable
125        // IndexError for lists, insert-or-overwrite for dicts.
126        let render_subscript_store = |sub: &crate::Subscript,
127                                      value: &TokenStream|
128         -> Result<TokenStream, Box<dyn std::error::Error>> {
129            // The receiver must be a PLACE (nested subscripts thread
130            // through py_index_mut): the Load lowering would clone, and the
131            // store would silently land on the clone.
132            let receiver = crate::subscript_receiver_place(
133                &sub.value,
134                ctx.clone(),
135                options.clone(),
136                symbols.clone(),
137            )?;
138            match &sub.kind {
139                crate::SubscriptKind::Index(index) => {
140                    let index = index
141                        .clone()
142                        .to_rust(ctx.clone(), options.clone(), symbols.clone())?;
143                    Ok(quote!((#receiver).py_set_index(#index, #value)?;))
144                }
145                crate::SubscriptKind::Slice { .. } => Err(
146                    "slice assignment (`x[a:b] = ...`) is not yet supported"
147                        .to_string()
148                        .into(),
149                ),
150            }
151        };
152
153        let render = |target: &ExprType,
154                      value: &TokenStream|
155         -> Result<TokenStream, Box<dyn std::error::Error>> {
156            match target {
157                ExprType::Subscript(sub) => render_subscript_store(sub, value),
158                _ => render_one(target, value),
159            }
160        };
161
162        if self.targets.len() == 1 {
163            // A store into an optional-tracked name goes through the
164            // Option-slot lowering, which passes Option values through,
165            // wraps plain values in Some, and handles conditional arms
166            // independently (`x if c else None`).
167            if let ExprType::Name(name) = &self.targets[0] {
168                if options.optional_names.contains(&name.id) {
169                    let target_code = self.targets[0].clone().to_rust(
170                        ctx.clone(),
171                        options.clone(),
172                        symbols.clone(),
173                    )?;
174                    let value = crate::lower_optional_value(
175                        &value_expr,
176                        ctx.clone(),
177                        options.clone(),
178                        symbols.clone(),
179                    )?;
180                    return Ok(quote!(#target_code = #value;));
181                }
182            }
183            render(&self.targets[0], &value)
184        } else {
185            // Chained assignment (`a = b = expr`): Python evaluates the value
186            // once and assigns it to each target in turn.
187            let mut stream = quote!(let __rython_chain = #value;);
188            for target in &self.targets {
189                stream.extend(render(target, &quote!(__rython_chain.clone()))?);
190            }
191            Ok(stream)
192        }
193    }
194}