Skip to main content

python_ast/ast/tree/
scope.rs

1//! Scope analysis for variable hoisting.
2//!
3//! Python variables are function-scoped, so assigned names are hoisted to
4//! declarations at the top of the generated scope and assignments lower to
5//! plain stores (see `Assign`). This module decides which of those
6//! declarations actually need `mut`, mirroring rustc's own
7//! definite-initialization rules so the generated code carries neither
8//! `unused_mut` warnings nor missing-`mut` errors:
9//!
10//! - A store into a variable that may already hold a value (reassignment,
11//!   assignment on a path where it was maybe-assigned, any assignment inside
12//!   a loop or try-closure) needs `mut`.
13//! - The first store into a definitely-uninitialized variable is
14//!   initialization and does not.
15//! - Augmented assignment, storing through the variable (`x[i] = v`,
16//!   `x.f = v`), and calling a known-mutating Python method on it
17//!   (`x.append(...)`) need `mut`.
18
19use std::collections::{HashMap, HashSet};
20
21use crate::{ExprType, Statement, StatementType};
22
23/// The binding facts for one generated scope.
24pub struct ScopeBindings {
25    /// Names bound by `Assign` statements, in first-assignment order.
26    pub assigned: Vec<String>,
27    /// The subset of names (assigned names and parameters) that need a
28    /// mutable binding.
29    pub needs_mut: HashSet<String>,
30    /// Names assigned `None` on some path: they hold an Option, and every
31    /// non-None store into them wraps in `Some`.
32    pub optional: HashSet<String>,
33}
34
35/// How definitely a variable holds a value at a program point.
36#[derive(Clone, Copy, PartialEq)]
37enum Init {
38    No,
39    Maybe,
40    Yes,
41}
42
43/// Python methods that mutate their receiver; calling one on a name means
44/// the binding must be mutable. An unlisted mutating method surfaces as a
45/// missing-`mut` compile error in the generated code (loud, not silent).
46pub(crate) const MUTATING_METHODS: &[&str] = &[
47    "append",
48    "extend",
49    "insert",
50    "remove",
51    "pop",
52    "popitem",
53    "clear",
54    "sort",
55    "reverse",
56    "update",
57    "add",
58    "discard",
59    "setdefault",
60    "intersection_update",
61    "difference_update",
62    "symmetric_difference_update",
63    "push",
64    // File-object methods take &mut self (reads advance the cursor);
65    // csv.Writer's row methods write through it.
66    "read",
67    "readline",
68    "readlines",
69    "write",
70    "writelines",
71    "close",
72    "writerow",
73    "writerows",
74];
75
76struct Analysis<'r> {
77    assigned: Vec<String>,
78    needs_mut: HashSet<String>,
79    optional: HashSet<String>,
80    state: HashMap<String, Init>,
81    /// Classifies a method call's effect on its receiver when the
82    /// receiver's class is statically known: Some(needs_mut) resolves the
83    /// question authoritatively (a user method shadowing a builtin mutator
84    /// name may be read-only); None means unknown, falling back to the
85    /// syntactic MUTATING_METHODS list.
86    resolve_call: &'r dyn Fn(&crate::Call) -> Option<bool>,
87}
88
89impl Analysis<'_> {
90    fn init_of(&self, name: &str) -> Init {
91        self.state.get(name).copied().unwrap_or(Init::No)
92    }
93
94    /// Record a store. `multi` is true when the store may execute more than
95    /// once (loop body) or runs inside the try-block closure.
96    fn record_store(&mut self, name: &str, multi: bool) {
97        if !self.assigned.contains(&name.to_string()) {
98            self.assigned.push(name.to_string());
99        }
100        if multi || self.init_of(name) != Init::No {
101            self.needs_mut.insert(name.to_string());
102        }
103        self.state.insert(name.to_string(), Init::Yes);
104    }
105
106    /// The variable is mutated in place (aug-assign, store-through, or a
107    /// mutating method call).
108    fn record_mutation(&mut self, name: &str) {
109        self.needs_mut.insert(name.to_string());
110    }
111}
112
113/// Merge the post-states of alternative branches.
114fn merge_states(
115    branches: Vec<HashMap<String, Init>>,
116) -> HashMap<String, Init> {
117    let mut merged: HashMap<String, Init> = HashMap::new();
118    let keys: HashSet<String> = branches
119        .iter()
120        .flat_map(|b| b.keys().cloned())
121        .collect();
122    for key in keys {
123        let states: Vec<Init> = branches
124            .iter()
125            .map(|b| b.get(&key).copied().unwrap_or(Init::No))
126            .collect();
127        let all_yes = states.iter().all(|s| *s == Init::Yes);
128        let all_no = states.iter().all(|s| *s == Init::No);
129        let v = if all_yes {
130            Init::Yes
131        } else if all_no {
132            Init::No
133        } else {
134            Init::Maybe
135        };
136        merged.insert(key, v);
137    }
138    merged
139}
140
141/// Analyze a statement list (one generated scope). `initialized` names —
142/// the parameters — hold values at entry, so any store into them needs a
143/// mutable rebinding.
144pub fn analyze_scope(body: &[Statement], initialized: &[String]) -> ScopeBindings {
145    analyze_scope_with(body, initialized, &|_| None)
146}
147
148/// analyze_scope with a call resolver: when the resolver classifies a
149/// method call (receiver class statically known), its answer is
150/// authoritative for whether the call mutates the receiver chain's base
151/// binding; unresolved calls fall back to the syntactic method-name list.
152pub(crate) fn analyze_scope_with(
153    body: &[Statement],
154    initialized: &[String],
155    resolve_call: &dyn Fn(&crate::Call) -> Option<bool>,
156) -> ScopeBindings {
157    let mut a = Analysis {
158        assigned: Vec::new(),
159        needs_mut: HashSet::new(),
160        optional: HashSet::new(),
161        state: initialized
162            .iter()
163            .map(|n| (n.clone(), Init::Yes))
164            .collect(),
165        resolve_call,
166    };
167    walk_stmts(body, &mut a, false);
168    // Parameters are tracked for needs_mut but are not hoisted declarations.
169    let assigned = a
170        .assigned
171        .into_iter()
172        .filter(|n| !initialized.contains(n))
173        .collect();
174    ScopeBindings {
175        assigned,
176        needs_mut: a.needs_mut,
177        optional: a.optional,
178    }
179}
180
181fn record_target(target: &ExprType, a: &mut Analysis<'_>, multi: bool) {
182    match target {
183        ExprType::Name(name) => a.record_store(&name.id, multi),
184        ExprType::Tuple(tuple) => {
185            for elt in &tuple.elts {
186                // Destructuring assignment; conservatively mutable.
187                if let ExprType::Name(name) = elt {
188                    a.record_store(&name.id, multi);
189                    a.record_mutation(&name.id);
190                } else {
191                    record_target(elt, a, multi);
192                }
193            }
194        }
195        // Stores through the base name: `x[i] = v`, `x.f = v`, and nested
196        // chains like `grid[i][j] = v` all mutate the chain's base binding.
197        ExprType::Subscript(sub) => {
198            if let Some(name) = chain_base_name(&sub.value) {
199                a.record_mutation(name);
200            }
201            walk_subscript_kind(&sub.kind, a);
202        }
203        ExprType::Attribute(attr) => {
204            if let Some(name) = chain_base_name(&attr.value) {
205                a.record_mutation(name);
206            }
207        }
208        _ => {}
209    }
210}
211
212/// The name at the base of a subscript/attribute chain (`grid` in
213/// `grid[i][j]` or `obj.rows[i]`), if the chain bottoms out in one.
214pub(crate) fn chain_base_name(expr: &ExprType) -> Option<&str> {
215    match expr {
216        ExprType::Name(name) => Some(&name.id),
217        ExprType::Subscript(sub) => chain_base_name(&sub.value),
218        ExprType::Attribute(attr) => chain_base_name(&attr.value),
219        _ => None,
220    }
221}
222
223/// The standard call resolver for analyze_scope_with, backed by the symbol
224/// table: a call whose receiver class is statically known resolves to that
225/// class's own method, and the method's receiver kind (&self / &mut self)
226/// decides whether the call mutates.
227pub(crate) fn class_call_resolver<'a>(
228    ctx: &'a crate::CodeGenContext,
229    symbols: &'a crate::SymbolTableScopes,
230) -> impl Fn(&crate::Call) -> Option<bool> + 'a {
231    move |call| {
232        let ExprType::Attribute(attr) = call.func.as_ref() else {
233            return None;
234        };
235        let class = crate::receiver_class(&attr.value, ctx, symbols)?;
236        if !class.methods().any(|m| m.name == attr.attr) {
237            return None;
238        }
239        Some(class.method_needs_mut_self(&attr.attr, symbols))
240    }
241}
242
243fn walk_stmts(body: &[Statement], a: &mut Analysis<'_>, multi: bool) {
244    for stmt in body {
245        match &stmt.statement {
246            StatementType::Assign(assign) => {
247                walk_expr(&assign.value, a);
248                let value_is_none = crate::is_none_expr(&assign.value);
249                for target in &assign.targets {
250                    record_target(target, a, multi);
251                    if value_is_none {
252                        if let ExprType::Name(name) = target {
253                            a.optional.insert(name.id.clone());
254                        }
255                    }
256                }
257            }
258            StatementType::AugAssign(aug) => {
259                walk_expr(&aug.value, a);
260                if let ExprType::Name(name) = &aug.target {
261                    // Reads and writes an existing value: always mutable.
262                    a.record_store(&name.id, multi);
263                    a.record_mutation(&name.id);
264                } else {
265                    record_target(&aug.target, a, multi);
266                }
267            }
268            StatementType::Expr(e) => walk_expr(&e.value, a),
269            StatementType::Call(call) => walk_call(call, a),
270            StatementType::Return(Some(e)) => walk_expr(&e.value, a),
271            StatementType::Assert { test, msg } => {
272                walk_expr(test, a);
273                if let Some(m) = msg {
274                    walk_expr(m, a);
275                }
276            }
277            StatementType::Raise(raise) => {
278                if let Some(exc) = &raise.exc {
279                    walk_expr(exc, a);
280                }
281                if let Some(cause) = &raise.cause {
282                    walk_expr(cause, a);
283                }
284            }
285            StatementType::If(s) => {
286                walk_expr(&s.test, a);
287                let before = a.state.clone();
288                walk_stmts(&s.body, a, multi);
289                let after_body = std::mem::replace(&mut a.state, before);
290                walk_stmts(&s.orelse, a, multi);
291                let after_else = std::mem::replace(&mut a.state, HashMap::new());
292                a.state = merge_states(vec![after_body, after_else]);
293            }
294            StatementType::While(s) => {
295                walk_expr(&s.test, a);
296                walk_loop(&s.body, &s.orelse, a, multi);
297            }
298            StatementType::For(s) => {
299                walk_expr(&s.iter, a);
300                walk_loop(&s.body, &s.orelse, a, multi);
301            }
302            StatementType::AsyncFor(s) => {
303                walk_expr(&s.iter, a);
304                walk_loop(&s.body, &s.orelse, a, multi);
305            }
306            StatementType::Try(s) => {
307                // The try body runs inside a closure; stores there behave
308                // like multi-execution stores (they mutate captured state).
309                let before = a.state.clone();
310                walk_stmts(&s.body, a, true);
311                let after_body = a.state.clone();
312                // Handlers may run with the body only partially executed.
313                let handler_entry =
314                    merge_states(vec![before, after_body.clone()]);
315                let mut exits = Vec::new();
316                for handler in &s.handlers {
317                    a.state = handler_entry.clone();
318                    walk_stmts(&handler.body, a, multi);
319                    exits.push(std::mem::replace(&mut a.state, HashMap::new()));
320                }
321                // The else path continues from the completed body.
322                a.state = after_body;
323                walk_stmts(&s.orelse, a, multi);
324                exits.push(std::mem::replace(&mut a.state, HashMap::new()));
325                a.state = merge_states(exits);
326                walk_stmts(&s.finalbody, a, multi);
327            }
328            StatementType::With(s) => {
329                for item in &s.items {
330                    walk_expr(&item.context_expr, a);
331                }
332                walk_stmts(&s.body, a, multi);
333            }
334            StatementType::AsyncWith(s) => {
335                for item in &s.items {
336                    walk_expr(&item.context_expr, a);
337                }
338                walk_stmts(&s.body, a, multi);
339            }
340            // Nested definitions are their own scopes.
341            _ => {}
342        }
343    }
344}
345
346/// A loop body may execute any number of times: analyze it as a branch that
347/// may or may not have run, with every store marked multi-execution.
348fn walk_loop(
349    body: &[Statement],
350    orelse: &[Statement],
351    a: &mut Analysis<'_>,
352    outer_multi: bool,
353) {
354    let before = a.state.clone();
355    walk_stmts(body, a, true);
356    let after_body = std::mem::replace(&mut a.state, HashMap::new());
357    a.state = merge_states(vec![before, after_body]);
358    walk_stmts(orelse, a, outer_multi);
359}
360
361/// Free functions that mutate their first argument in place: the heapq
362/// surface treats a plain list as a heap, so `heappush(h, x)` mutates `h`
363/// like a method call would.
364const FIRST_ARG_MUTATORS: &[&str] = &[
365    "heappush",
366    "heappop",
367    "heapify",
368    "heappushpop",
369    "heapreplace",
370    // csv.writer(f) holds &mut f for the writer's lifetime.
371    "writer",
372];
373
374fn walk_call(call: &crate::Call, a: &mut Analysis<'_>) {
375    if let ExprType::Attribute(attr) = call.func.as_ref() {
376        // The module-prefixed spelling mutates its first ARGUMENT, not the
377        // receiver: `heapq.heappush(h, x)` needs `h` mutable, mirroring
378        // the bare-function branch below.
379        if let ExprType::Name(m) = attr.value.as_ref() {
380            if matches!(m.id.as_str(), "heapq" | "csv")
381                && FIRST_ARG_MUTATORS.contains(&attr.attr.as_str())
382            {
383                if let Some(first) = call.args.first() {
384                    if let Some(name) = chain_base_name(first) {
385                        a.record_mutation(name);
386                    }
387                }
388            }
389        }
390        // A mutating method mutates the base binding of the whole receiver
391        // chain: `self.items.append(x)` mutates `self`, `rows[i].push(x)`
392        // mutates `rows`. When the receiver's class is statically known,
393        // the resolver's verdict is authoritative — a user method may
394        // shadow a builtin mutator name yet be read-only, or mutate under
395        // a name the syntactic list doesn't know.
396        let mutates = match (a.resolve_call)(call) {
397            Some(verdict) => verdict,
398            None => MUTATING_METHODS.contains(&attr.attr.as_str()),
399        };
400        if mutates {
401            if let Some(name) = chain_base_name(&attr.value) {
402                a.record_mutation(name);
403            }
404        }
405        walk_expr(&attr.value, a);
406    } else {
407        // Free functions that mutate their first argument in place; see
408        // FIRST_ARG_MUTATORS.
409        if let ExprType::Name(n) = call.func.as_ref() {
410            if FIRST_ARG_MUTATORS.contains(&n.id.as_str()) {
411                if let Some(first) = call.args.first() {
412                    if let Some(name) = chain_base_name(first) {
413                        a.record_mutation(name);
414                    }
415                }
416            }
417        }
418        walk_expr(&call.func, a);
419    }
420    for arg in &call.args {
421        walk_expr(arg, a);
422    }
423    // Keyword-argument values carry mutations too: `foo(x=c.bump())`
424    // mutates `c` just as surely as a positional argument would.
425    for kw in &call.keywords {
426        walk_expr(&kw.value, a);
427    }
428}
429
430fn walk_subscript_kind(kind: &crate::SubscriptKind, a: &mut Analysis<'_>) {
431    match kind {
432        crate::SubscriptKind::Index(i) => walk_expr(i, a),
433        crate::SubscriptKind::Slice { lower, upper, step } => {
434            for bound in [lower, upper, step].into_iter().flatten() {
435                walk_expr(bound, a);
436            }
437        }
438    }
439}
440
441fn walk_expr(expr: &ExprType, a: &mut Analysis<'_>) {
442    match expr {
443        ExprType::Call(call) => walk_call(call, a),
444        ExprType::BinOp(op) => {
445            walk_expr(&op.left, a);
446            walk_expr(&op.right, a);
447        }
448        ExprType::BoolOp(op) => {
449            for v in &op.values {
450                walk_expr(v, a);
451            }
452        }
453        ExprType::UnaryOp(op) => walk_expr(&op.operand, a),
454        ExprType::Compare(cmp) => {
455            walk_expr(&cmp.left, a);
456            for c in &cmp.comparators {
457                walk_expr(c, a);
458            }
459        }
460        ExprType::IfExp(e) => {
461            walk_expr(&e.test, a);
462            walk_expr(&e.body, a);
463            walk_expr(&e.orelse, a);
464        }
465        ExprType::NamedExpr(e) => {
466            walk_expr(&e.left, a);
467            walk_expr(&e.right, a);
468        }
469        ExprType::Dict(d) => {
470            for k in d.keys.iter().flatten() {
471                walk_expr(k, a);
472            }
473            for v in &d.values {
474                walk_expr(v, a);
475            }
476        }
477        ExprType::Set(s) => {
478            for e in &s.elts {
479                walk_expr(e, a);
480            }
481        }
482        ExprType::List(elts) => {
483            for e in elts {
484                walk_expr(e, a);
485            }
486        }
487        ExprType::Tuple(t) => {
488            for e in &t.elts {
489                walk_expr(e, a);
490            }
491        }
492        ExprType::Attribute(attr) => walk_expr(&attr.value, a),
493        ExprType::Subscript(sub) => {
494            walk_expr(&sub.value, a);
495            walk_subscript_kind(&sub.kind, a);
496        }
497        ExprType::Starred(s) => walk_expr(&s.value, a),
498        ExprType::Await(e) => walk_expr(&e.value, a),
499        ExprType::Yield(y) => {
500            if let Some(v) = &y.value {
501                walk_expr(v, a);
502            }
503        }
504        ExprType::YieldFrom(y) => walk_expr(&y.value, a),
505        ExprType::FormattedValue(f) => walk_expr(&f.value, a),
506        ExprType::JoinedStr(j) => {
507            for v in &j.values {
508                walk_expr(v, a);
509            }
510        }
511        // Comprehensions and lambdas are their own scopes; leaves carry no
512        // mutation.
513        _ => {}
514    }
515}