Skip to main content

brokk_bifrost_python/
bindings.rs

1use brokk_bifrost_core::analyzer::common::node_source_text_trimmed as node_text;
2use brokk_bifrost_core::analyzer::tree_walk::{named_children_iter, push_bounded_nodes};
3use tree_sitter::Node;
4
5use brokk_bifrost_core::hash::{HashMap, HashSet};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PythonLexicalNameResolution {
9    Unbound,
10    Local,
11    Nonlocal,
12    Global,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum PythonDirectScopeBindingKind {
17    ClassDeclaration,
18    FunctionDeclaration,
19    Import,
20    Other,
21}
22
23/// Whether `reference` reads a target introduced by an enclosing comprehension.
24///
25/// Comprehensions create an implicit scope even at module level. The element
26/// expression is textually before its `for` clauses but evaluates after their
27/// binders. Only the first iterable evaluates in the enclosing scope; later
28/// iterables use comprehension-local names, including not-yet-bound targets.
29/// This structured walk models those scopes without source-text parsing.
30pub fn python_comprehension_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
31    python_comprehension_binds_name_at_bounded(name, reference, source, || true)
32        .expect("unbounded comprehension binding lookup completes")
33}
34
35/// The same scope proof under the caller's work/cancellation ledger.
36/// An interrupted walk must not be interpreted as an unbound name.
37pub fn python_comprehension_binds_name_at_bounded(
38    name: &str,
39    reference: Node<'_>,
40    source: &str,
41    mut scope_step: impl FnMut() -> bool,
42) -> Option<bool> {
43    if !scope_step() {
44        return None;
45    }
46    let mut current = reference;
47    while let Some(parent) = current.parent() {
48        if !scope_step() {
49            return None;
50        }
51        if is_comprehension(parent.kind()) {
52            let mut cursor = parent.walk();
53            let mut clauses = Vec::new();
54            for child in parent.named_children(&mut cursor) {
55                if !scope_step() {
56                    return None;
57                }
58                if child.kind() == "for_in_clause" {
59                    clauses.push(child);
60                }
61            }
62            let mut in_outer_iterable = false;
63            if let Some(clause) = clauses.first() {
64                let mut cursor = clause.walk();
65                for right in clause.children_by_field_name("right", &mut cursor) {
66                    if !scope_step() {
67                        return None;
68                    }
69                    if right.start_byte() <= reference.start_byte()
70                        && reference.end_byte() <= right.end_byte()
71                    {
72                        in_outer_iterable = true;
73                        break;
74                    }
75                }
76            }
77            if in_outer_iterable {
78                current = parent;
79                continue;
80            }
81            for clause in clauses {
82                if !scope_step() {
83                    return None;
84                }
85                let Some(target) = clause.child_by_field_name("left") else {
86                    continue;
87                };
88                let mut bound = false;
89                collect_binding_targets(target, source, &mut scope_step, |candidate, _| {
90                    bound |= candidate == name;
91                })?;
92                if bound {
93                    return Some(true);
94                }
95            }
96        }
97        if matches!(
98            parent.kind(),
99            "function_definition" | "lambda" | "class_definition" | "module"
100        ) {
101            break;
102        }
103        current = parent;
104    }
105    Some(false)
106}
107
108/// Whether `reference` reads a PEP 695 type parameter declared by its enclosing
109/// class, function, or type-alias definition.
110///
111/// Type parameters are lexical bindings rather than indexed workspace
112/// declarations. The binder is the first identifier in each structured
113/// parameter node; identifiers in bounds and constraints remain references.
114pub fn python_type_parameter_binds_name_at(name: &str, reference: Node<'_>, source: &str) -> bool {
115    python_type_parameter_binds_name_at_bounded(name, reference, source, || true)
116        .expect("unbounded type parameter lookup completes")
117}
118
119/// The shared type-parameter proof under the caller's live work budget.
120/// An interrupted ancestor, binder, or directive walk supplies no answer.
121pub fn python_type_parameter_binds_name_at_bounded(
122    name: &str,
123    reference: Node<'_>,
124    source: &str,
125    mut scope_step: impl FnMut() -> bool,
126) -> Option<bool> {
127    let mut current = reference;
128    let mut enclosing_functions = Vec::new();
129    while let Some(parent) = current.parent() {
130        if !scope_step() {
131            return None;
132        }
133        if parent.kind() == "function_definition" {
134            enclosing_functions.push(parent);
135        }
136        if matches!(
137            parent.kind(),
138            "class_definition" | "function_definition" | "type_alias_statement"
139        ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
140            && !(parameters.start_byte() <= reference.start_byte()
141                && reference.end_byte() <= parameters.end_byte())
142        {
143            let mut cursor = parameters.walk();
144            for parameter in parameters.named_children(&mut cursor) {
145                if !scope_step() {
146                    return None;
147                }
148                let binder = first_identifier_bounded(parameter, &mut scope_step)?;
149                if binder.is_some_and(|binder| node_text(binder, source) == name) {
150                    // Preserve the existing function-wide directive precedence.
151                    return Some(
152                        python_name_resolution_at_in_functions_bounded(
153                            name,
154                            reference,
155                            source,
156                            enclosing_functions,
157                            &mut scope_step,
158                        )? != PythonLexicalNameResolution::Global,
159                    );
160                }
161            }
162        }
163        current = parent;
164    }
165    Some(false)
166}
167
168/// Resolve `name` through the nearest enclosing function scope. This is the
169/// structured fallback for references inside nested functions that do not
170/// have their own indexed `CodeUnit` scope snapshot.
171pub fn python_name_resolution_at(
172    name: &str,
173    reference: Node<'_>,
174    source: &str,
175) -> PythonLexicalNameResolution {
176    let mut functions = Vec::new();
177    let mut current = reference;
178    while let Some(parent) = current.parent() {
179        if parent.kind() == "function_definition" {
180            functions.push(parent);
181        }
182        current = parent;
183    }
184    python_name_resolution_at_in_functions(name, reference, source, functions)
185}
186
187fn python_name_resolution_at_in_functions(
188    name: &str,
189    reference: Node<'_>,
190    source: &str,
191    functions: Vec<Node<'_>>,
192) -> PythonLexicalNameResolution {
193    python_name_resolution_at_in_functions_bounded(name, reference, source, functions, || true)
194        .expect("unbounded function binding lookup completes")
195}
196
197fn python_name_resolution_at_in_functions_bounded(
198    name: &str,
199    reference: Node<'_>,
200    source: &str,
201    functions: Vec<Node<'_>>,
202    mut scope_step: impl FnMut() -> bool,
203) -> Option<PythonLexicalNameResolution> {
204    for function in functions {
205        if !scope_step() {
206            return None;
207        }
208        let mut parameter_names = Vec::new();
209        if let Some(parameters) = function.child_by_field_name("parameters") {
210            let mut cursor = parameters.walk();
211            for parameter in parameters.named_children(&mut cursor) {
212                if !scope_step() {
213                    return None;
214                }
215                if let Some(name) = crate::syntax::python_parameter_name_bounded(
216                    parameter,
217                    source,
218                    &mut scope_step,
219                )? {
220                    parameter_names.push(name);
221                }
222            }
223        }
224        let inventory = PythonLexicalScopeInventory::collect_bounded(
225            function,
226            source,
227            parameter_names,
228            &mut scope_step,
229        )?;
230        let resolution = inventory.name_resolution_at_bounded(name, reference, &mut scope_step)?;
231        match resolution {
232            PythonLexicalNameResolution::Global
233            | PythonLexicalNameResolution::Local
234            | PythonLexicalNameResolution::Nonlocal => return Some(resolution),
235            PythonLexicalNameResolution::Unbound => {}
236        }
237    }
238    Some(PythonLexicalNameResolution::Unbound)
239}
240
241/// Whether this identifier is the binder (rather than a bound/constraint use)
242/// of one structured PEP 695 type parameter.
243pub fn python_is_type_parameter_binder(node: Node<'_>) -> bool {
244    if node.kind() != "identifier" {
245        return false;
246    }
247    let mut current = node;
248    while let Some(parent) = current.parent() {
249        if matches!(
250            parent.kind(),
251            "class_definition" | "function_definition" | "type_alias_statement"
252        ) && let Some(parameters) = parent.child_by_field_name("type_parameters")
253            && parameters.start_byte() <= node.start_byte()
254            && node.end_byte() <= parameters.end_byte()
255        {
256            let mut cursor = parameters.walk();
257            return parameters.named_children(&mut cursor).any(|parameter| {
258                let binder = python_type_parameter_binder_node(parameter);
259                binder.is_some_and(|binder| binder.id() == node.id())
260            });
261        }
262        if matches!(
263            parent.kind(),
264            "module" | "class_definition" | "function_definition" | "type_alias_statement"
265        ) {
266            return false;
267        }
268        current = parent;
269    }
270    false
271}
272
273fn python_type_parameter_binder_node(parameter: Node<'_>) -> Option<Node<'_>> {
274    if parameter.kind() == "identifier" {
275        Some(parameter)
276    } else {
277        first_identifier_bounded(parameter, &mut || true).flatten()
278    }
279}
280
281#[derive(Clone, Copy, Debug)]
282pub struct PythonDirectScopeBinding<'tree> {
283    pub declaration: Node<'tree>,
284    pub kind: PythonDirectScopeBindingKind,
285}
286
287#[derive(Clone, Debug)]
288struct PythonLocalBinding<'tree> {
289    name: Box<str>,
290    declaration: Node<'tree>,
291}
292
293#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294enum PythonLocalBindingKind {
295    FunctionOnly,
296    ClassOnly,
297    ImportOnly,
298    Other,
299}
300
301#[derive(Clone, Debug)]
302struct PythonComprehensionBinding {
303    name: Box<str>,
304    start_byte: usize,
305    end_byte: usize,
306    enclosing_iterable_ranges: Vec<(usize, usize)>,
307}
308
309/// Function-scope Python bindings discovered from tree-sitter structure.
310///
311/// The inventory models Python's whole-function symbol-table behavior while
312/// retaining the implicit scope of comprehension targets. Construction is
313/// iterative and every inspected node is gated by `scope_step`; `None` means
314/// the caller stopped discovery and must conservatively avoid module fallback.
315pub struct PythonLexicalScopeInventory<'tree> {
316    binding_nodes: Vec<Node<'tree>>,
317    locals: Vec<PythonLocalBinding<'tree>>,
318    bindings: PythonLexicalScopeBindings,
319}
320
321/// Completed function binding facts without borrowed tree-sitter nodes.
322/// Retain these only under the identity of the exact source that produced them.
323pub struct PythonLexicalScopeBindings {
324    parameters: HashSet<Box<str>>,
325    local_names: HashMap<Box<str>, PythonLocalBindingKind>,
326    binding_writes: HashSet<Box<str>>,
327    // Real binding events retained before global declarations leave locals.
328    global_binding_events: HashSet<Box<str>>,
329    globals: HashSet<Box<str>>,
330    nonlocals: HashSet<Box<str>>,
331    comprehensions: Vec<PythonComprehensionBinding>,
332}
333
334impl PythonLexicalScopeBindings {
335    pub fn name_resolution_at(
336        &self,
337        name: &str,
338        reference: Node<'_>,
339    ) -> PythonLexicalNameResolution {
340        self.name_resolution_at_bounded(name, reference, || true)
341            .expect("unbounded lexical name lookup completes")
342    }
343
344    /// Resolve a read while charging every comprehension binding and iterable
345    /// range inspected. Exhaustion is distinct from an unbound name.
346    pub fn name_resolution_at_bounded(
347        &self,
348        name: &str,
349        reference: Node<'_>,
350        mut scope_step: impl FnMut() -> bool,
351    ) -> Option<PythonLexicalNameResolution> {
352        if !scope_step() {
353            return None;
354        }
355        if self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)? {
356            return Some(PythonLexicalNameResolution::Local);
357        }
358        Some(if self.nonlocals.contains(name) {
359            PythonLexicalNameResolution::Nonlocal
360        } else if self.globals.contains(name) {
361            PythonLexicalNameResolution::Global
362        } else if self.parameters.contains(name) || self.local_names.contains_key(name) {
363            PythonLexicalNameResolution::Local
364        } else {
365            PythonLexicalNameResolution::Unbound
366        })
367    }
368
369    fn comprehension_binds_name_at_bounded(
370        &self,
371        name: &str,
372        reference: Node<'_>,
373        mut scope_step: impl FnMut() -> bool,
374    ) -> Option<bool> {
375        let reference_byte = reference.start_byte();
376        for binding in &self.comprehensions {
377            if !scope_step() {
378                return None;
379            }
380            if binding.name.as_ref() == name
381                && binding.start_byte <= reference_byte
382                && reference_byte < binding.end_byte
383            {
384                let mut in_outer_iterable = false;
385                for (start, end) in &binding.enclosing_iterable_ranges {
386                    if !scope_step() {
387                        return None;
388                    }
389                    in_outer_iterable |= *start <= reference_byte && reference_byte < *end;
390                }
391                if !in_outer_iterable {
392                    return Some(true);
393                }
394            }
395        }
396        Some(false)
397    }
398
399    /// A directive alone is not an event. Declarations, imports, assignments,
400    /// and every other collected binder/delete count, without value inference.
401    pub fn has_global_binding_event(&self, name: &str) -> bool {
402        self.global_binding_events.contains(name)
403    }
404
405    /// All actual global binding events, before any read-time filtering.
406    /// Callers that copy this set must charge their own bounded iteration.
407    pub fn global_binding_events(&self) -> impl Iterator<Item = &str> {
408        self.global_binding_events.iter().map(|name| name.as_ref())
409    }
410
411    pub fn resolves_to_local_function_bounded(
412        &self,
413        name: &str,
414        reference: Node<'_>,
415        mut scope_step: impl FnMut() -> bool,
416    ) -> Option<bool> {
417        if !scope_step() {
418            return None;
419        }
420        Some(
421            !self.parameters.contains(name)
422                && !self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?
423                && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly),
424        )
425    }
426
427    /// Whether the active callable obtains `name` from a runtime binding
428    /// rather than an untouched function or import declaration.
429    pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
430        self.has_runtime_callable_binding_at_bounded(name, reference, || true)
431            .expect("unbounded runtime binding lookup completes")
432    }
433
434    pub fn has_runtime_callable_binding_at_bounded(
435        &self,
436        name: &str,
437        reference: Node<'_>,
438        mut scope_step: impl FnMut() -> bool,
439    ) -> Option<bool> {
440        if !scope_step() {
441            return None;
442        }
443        Some(
444            self.parameters.contains(name)
445                || self.binding_writes.contains(name)
446                || self.comprehension_binds_name_at_bounded(name, reference, &mut scope_step)?,
447        )
448    }
449}
450
451/// Binding events in an executing class namespace at one reference. Only a
452/// proved static superclass path precedes that class statement's name binding.
453/// Lazy type-parameter expressions and other header contexts keep that event.
454/// Other events retain the existing whole-scope conservative interpretation.
455pub struct PythonClassReferenceScope<'tree> {
456    scope: Node<'tree>,
457    pending_name: Option<Node<'tree>>,
458}
459
460impl<'tree> PythonClassReferenceScope<'tree> {
461    pub fn new_bounded(
462        scope: Node<'tree>,
463        reference: Node<'tree>,
464        mut scope_step: impl FnMut() -> bool,
465    ) -> Option<Self> {
466        assert_eq!(scope.kind(), "class_definition");
467        let mut current = reference;
468        let mut pending_name = None;
469        while current != scope {
470            if !scope_step() {
471                return None;
472            }
473            if current.kind() == "class_definition"
474                && let Some(superclasses) = current.child_by_field_name("superclasses")
475            {
476                for expression in named_children_iter(superclasses) {
477                    if !scope_step() {
478                        return None;
479                    }
480                    if expression.start_byte() <= reference.start_byte()
481                        && reference.end_byte() <= expression.end_byte()
482                        && let Some(path) = crate::syntax::python_static_value_path_bounded(
483                            expression,
484                            &mut scope_step,
485                        )?
486                        && (path.first().copied() == Some(reference) || expression == reference)
487                    {
488                        pending_name = Some(current.child_by_field_name("name")?);
489                    }
490                }
491            }
492            current = current.parent()?;
493        }
494        Some(Self {
495            scope,
496            pending_name,
497        })
498    }
499
500    fn includes_binding(&self, declaration: Node<'tree>) -> bool {
501        Some(declaration) != self.pending_name
502    }
503
504    pub fn collect_bounded(
505        &self,
506        source: &str,
507        scope_step: impl FnMut() -> bool,
508    ) -> Option<PythonLexicalScopeInventory<'tree>> {
509        PythonLexicalScopeInventory::collect_with_declaration_filter_bounded(
510            self.scope,
511            source,
512            std::iter::empty::<String>(),
513            |declaration| self.includes_binding(declaration),
514            scope_step,
515        )
516    }
517
518    pub fn binds_name_bounded(
519        &self,
520        name: &str,
521        source: &str,
522        scope_step: impl FnMut() -> bool,
523    ) -> Option<bool> {
524        python_scope_binds_name_with_filter_bounded(
525            self.scope,
526            name,
527            source,
528            |declaration| self.includes_binding(declaration),
529            scope_step,
530        )
531    }
532}
533
534impl<'tree> PythonLexicalScopeInventory<'tree> {
535    /// `parameter_names` is the callable's formal-parameter name stream. The
536    /// layout it comes from is resolved by `analyzer/lexical_definitions.rs`,
537    /// which dispatches through the analysis-side language registry and so
538    /// cannot be named here; `analyzer/python/lexical_scope.rs` in
539    /// `brokk-bifrost-analysis` is the one caller that computes it, under the
540    /// same `scope_step` meter this walk uses.
541    pub fn collect_bounded(
542        callable: Node<'tree>,
543        source: &str,
544        parameter_names: impl IntoIterator<Item = String>,
545        scope_step: impl FnMut() -> bool,
546    ) -> Option<Self> {
547        Self::collect_with_declaration_filter_bounded(
548            callable,
549            source,
550            parameter_names,
551            |_| true,
552            scope_step,
553        )
554    }
555
556    fn collect_with_declaration_filter_bounded(
557        callable: Node<'tree>,
558        source: &str,
559        parameter_names: impl IntoIterator<Item = String>,
560        include_declaration: impl Fn(Node<'tree>) -> bool,
561        mut scope_step: impl FnMut() -> bool,
562    ) -> Option<Self> {
563        let mut inventory = Self {
564            binding_nodes: Vec::new(),
565            locals: Vec::new(),
566            bindings: PythonLexicalScopeBindings {
567                parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
568                local_names: HashMap::default(),
569                binding_writes: HashSet::default(),
570                global_binding_events: HashSet::default(),
571                globals: HashSet::default(),
572                nonlocals: HashSet::default(),
573                comprehensions: Vec::new(),
574            },
575        };
576        let body = if callable.kind() == "module" {
577            callable
578        } else if let Some(body) = callable.child_by_field_name("body") {
579            body
580        } else {
581            return Some(inventory);
582        };
583        let mut stack = vec![body];
584
585        while let Some(node) = stack.pop() {
586            if !scope_step() {
587                return None;
588            }
589            let nested_scope = node != body
590                && matches!(
591                    node.kind(),
592                    "function_definition" | "lambda" | "class_definition"
593                );
594            if nested_scope {
595                if matches!(node.kind(), "function_definition" | "class_definition")
596                    && let Some(name) = node.child_by_field_name("name")
597                    && include_declaration(name)
598                {
599                    inventory.record_local_node(name, source);
600                }
601                // Defaults, annotations, bases, and type-parameter expressions
602                // execute in an enclosing scope. Keep scanning those headers
603                // for walrus/comprehension bindings while pruning the nested
604                // callable or class body itself.
605                push_nested_scope_header_children(&mut stack, node, &mut scope_step)?;
606                continue;
607            }
608
609            match node.kind() {
610                "global_statement" => {
611                    if callable.kind() == "module" {
612                        continue;
613                    }
614                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
615                        inventory.bindings.globals.insert(name.into());
616                    })?;
617                    continue;
618                }
619                "nonlocal_statement" => {
620                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
621                        inventory.bindings.nonlocals.insert(name.into());
622                    })?;
623                    continue;
624                }
625                "import_statement" | "import_from_statement" => {
626                    collect_import_bindings(node, &mut scope_step, |binding| {
627                        inventory.record_local_kind(
628                            node_text(binding, source),
629                            binding,
630                            PythonLocalBindingKind::ImportOnly,
631                        )
632                    })?;
633                    continue;
634                }
635                "assignment" | "augmented_assignment" => {
636                    if let Some(target) = node.child_by_field_name("left") {
637                        collect_binding_targets(
638                            target,
639                            source,
640                            &mut scope_step,
641                            |name, declaration| {
642                                inventory.record_binding_write(name, declaration);
643                            },
644                        )?;
645                    }
646                }
647                "type_alias_statement" => {
648                    if let Some(target) = node.child_by_field_name("left")
649                        && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
650                    {
651                        inventory.record_local_node(binding, source);
652                    }
653                }
654                "named_expression" => {
655                    // PEP 572 binds a comprehension walrus in the containing
656                    // non-comprehension scope, so this remains a function local.
657                    if let Some(target) = node.child_by_field_name("name") {
658                        collect_binding_targets(
659                            target,
660                            source,
661                            &mut scope_step,
662                            |name, declaration| {
663                                inventory.record_binding_write(name, declaration);
664                            },
665                        )?;
666                    }
667                }
668                "for_statement" => {
669                    if let Some(target) = node.child_by_field_name("left") {
670                        collect_binding_targets(
671                            target,
672                            source,
673                            &mut scope_step,
674                            |name, declaration| {
675                                inventory.record_binding_write(name, declaration);
676                            },
677                        )?;
678                    }
679                }
680                "for_in_clause" => {
681                    // These occur inside comprehensions and belong to their
682                    // implicit scope. The enclosing comprehension records them.
683                }
684                "delete_statement" => {
685                    for target in named_children_bounded(node, &mut scope_step)? {
686                        collect_binding_targets(
687                            target,
688                            source,
689                            &mut scope_step,
690                            |name, declaration| {
691                                inventory.record_binding_write(name, declaration);
692                            },
693                        )?;
694                    }
695                    continue;
696                }
697                "as_pattern" => {
698                    if let Some(alias) = node.child_by_field_name("alias") {
699                        collect_binding_targets(
700                            alias,
701                            source,
702                            &mut scope_step,
703                            |name, declaration| {
704                                inventory.record_binding_write(name, declaration);
705                            },
706                        )?;
707                        push_named_children_except(&mut stack, node, alias, &mut scope_step)?;
708                        continue;
709                    }
710                }
711                "except_clause" => {
712                    // Older grammar shapes expose the alias directly. Current
713                    // tree-sitter-python nests it in an `as_pattern`, handled
714                    // above when the clause's children are scanned.
715                    if let Some(alias) = node.child_by_field_name("alias") {
716                        collect_binding_targets(
717                            alias,
718                            source,
719                            &mut scope_step,
720                            |name, declaration| {
721                                inventory.record_binding_write(name, declaration);
722                            },
723                        )?;
724                    }
725                }
726                "case_clause" => {
727                    let children = named_children_bounded(node, &mut scope_step)?;
728                    for child in children.iter().copied() {
729                        if child.kind() == "case_pattern" {
730                            collect_match_pattern_bindings(
731                                child,
732                                source,
733                                &mut scope_step,
734                                |name, declaration| {
735                                    inventory.record_binding_write(name, declaration);
736                                },
737                            )?;
738                        }
739                    }
740                    for child in children.into_iter().rev() {
741                        if child.kind() != "case_pattern" {
742                            stack.push(child);
743                        }
744                    }
745                    continue;
746                }
747                kind if is_comprehension(kind) => {
748                    let range = (node.start_byte(), node.end_byte());
749                    let children = named_children_bounded(node, &mut scope_step)?;
750                    let enclosing_iterable_ranges = if let Some(first_clause) = children
751                        .iter()
752                        .copied()
753                        .find(|child| child.kind() == "for_in_clause")
754                    {
755                        children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
756                            .into_iter()
757                            .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
758                            .collect()
759                    } else {
760                        Vec::new()
761                    };
762                    for clause in children
763                        .iter()
764                        .copied()
765                        .filter(|child| child.kind() == "for_in_clause")
766                    {
767                        if let Some(target) = clause.child_by_field_name("left") {
768                            collect_binding_targets(target, source, &mut scope_step, |name, _| {
769                                inventory.bindings.comprehensions.push(
770                                    PythonComprehensionBinding {
771                                        name: name.into(),
772                                        start_byte: range.0,
773                                        end_byte: range.1,
774                                        enclosing_iterable_ranges: enclosing_iterable_ranges
775                                            .clone(),
776                                    },
777                                );
778                            })?;
779                        }
780                    }
781                    for child in children.into_iter().rev() {
782                        stack.push(child);
783                    }
784                    continue;
785                }
786                _ => {}
787            }
788
789            push_named_children(&mut stack, node, &mut scope_step)?;
790        }
791
792        // Preserve all real global binding events before pruning semantic
793        // locals, including def/class/import declarations that intentionally
794        // do not belong to the runtime-callable-value predicate.
795        for name in inventory.bindings.local_names.keys() {
796            if !scope_step() {
797                return None;
798            }
799            if inventory.bindings.globals.contains(name.as_ref()) {
800                inventory
801                    .bindings
802                    .global_binding_events
803                    .insert(name.clone());
804            }
805        }
806
807        // `global` and `nonlocal` are whole-function directives regardless of
808        // source order. Neither declaration may become a semantic local.
809        inventory.locals.retain(|binding| {
810            !inventory.bindings.globals.contains(binding.name.as_ref())
811                && !inventory.bindings.nonlocals.contains(binding.name.as_ref())
812        });
813        inventory.bindings.local_names.retain(|name, _| {
814            !inventory.bindings.globals.contains(name.as_ref())
815                && !inventory.bindings.nonlocals.contains(name.as_ref())
816        });
817        Some(inventory)
818    }
819
820    pub fn into_bindings(self) -> PythonLexicalScopeBindings {
821        self.bindings
822    }
823
824    pub fn name_resolution_at(
825        &self,
826        name: &str,
827        reference: Node<'_>,
828    ) -> PythonLexicalNameResolution {
829        self.bindings.name_resolution_at(name, reference)
830    }
831
832    pub fn name_resolution_at_bounded(
833        &self,
834        name: &str,
835        reference: Node<'_>,
836        scope_step: impl FnMut() -> bool,
837    ) -> Option<PythonLexicalNameResolution> {
838        self.bindings
839            .name_resolution_at_bounded(name, reference, scope_step)
840    }
841
842    pub fn has_runtime_callable_binding_at(&self, name: &str, reference: Node<'_>) -> bool {
843        self.bindings
844            .has_runtime_callable_binding_at(name, reference)
845    }
846
847    pub fn has_runtime_callable_binding_at_bounded(
848        &self,
849        name: &str,
850        reference: Node<'_>,
851        scope_step: impl FnMut() -> bool,
852    ) -> Option<bool> {
853        self.bindings
854            .has_runtime_callable_binding_at_bounded(name, reference, scope_step)
855    }
856
857    pub fn local_function_declaration(
858        &self,
859        name: &str,
860        reference: Node<'_>,
861    ) -> Option<Node<'tree>> {
862        self.local_function_declaration_bounded(name, reference, || true)
863    }
864
865    pub fn local_function_declaration_bounded(
866        &self,
867        name: &str,
868        reference: Node<'_>,
869        mut scope_step: impl FnMut() -> bool,
870    ) -> Option<Node<'tree>> {
871        if !self
872            .bindings
873            .resolves_to_local_function_bounded(name, reference, &mut scope_step)?
874        {
875            return None;
876        }
877        for binding in &self.locals {
878            if !scope_step() {
879                return None;
880            }
881            if binding.name.as_ref() == name {
882                return binding
883                    .declaration
884                    .parent()
885                    .filter(|declaration| declaration.kind() == "function_definition");
886            }
887        }
888        None
889    }
890
891    /// A single untouched local class declaration, not an inferred instance
892    /// type. Statement ordering/containment is checked at the selected use.
893    pub fn local_class_declaration_bounded(
894        &self,
895        name: &str,
896        reference: Node<'_>,
897        mut scope_step: impl FnMut() -> bool,
898    ) -> Option<Node<'tree>> {
899        if self.name_resolution_at_bounded(name, reference, &mut scope_step)?
900            != PythonLexicalNameResolution::Local
901            || self.bindings.has_runtime_callable_binding_at_bounded(
902                name,
903                reference,
904                &mut scope_step,
905            )?
906            || self.bindings.local_names.get(name) != Some(&PythonLocalBindingKind::ClassOnly)
907        {
908            return None;
909        }
910        for binding in &self.locals {
911            if !scope_step() {
912                return None;
913            }
914            if binding.name.as_ref() == name {
915                return binding
916                    .declaration
917                    .parent()
918                    .filter(|node| node.kind() == "class_definition");
919            }
920        }
921        None
922    }
923
924    /// The import must be the only binding event, not merely the first
925    /// retained token. Competing imports, def/class names and writes collapse
926    /// ImportOnly to Other during collection, even when import metadata is absent.
927    pub fn unique_import_declaration_bounded(
928        &self,
929        name: &str,
930        reference: Node<'_>,
931        mut scope_step: impl FnMut() -> bool,
932    ) -> Option<Option<Node<'tree>>> {
933        if self.name_resolution_at_bounded(name, reference, &mut scope_step)?
934            != PythonLexicalNameResolution::Local
935            || self.bindings.has_runtime_callable_binding_at_bounded(
936                name,
937                reference,
938                &mut scope_step,
939            )?
940            || self.bindings.local_names.get(name) != Some(&PythonLocalBindingKind::ImportOnly)
941        {
942            return Some(None);
943        }
944        for binding in &self.locals {
945            if !scope_step() {
946                return None;
947            }
948            if binding.name.as_ref() == name {
949                return Some(Some(binding.declaration));
950            }
951        }
952        unreachable!("ImportOnly is constructed with its declaration token")
953    }
954
955    /// All binding tokens, including repeated and globally redirected writes.
956    /// The native producer attributes these after the whole scope inventory exists.
957    pub fn binding_nodes(&self) -> impl Iterator<Item = Node<'tree>> + '_ {
958        self.binding_nodes.iter().copied()
959    }
960
961    pub fn global_names(&self) -> impl Iterator<Item = &str> {
962        self.bindings.globals.iter().map(AsRef::as_ref)
963    }
964
965    pub fn nonlocal_names(&self) -> impl Iterator<Item = &str> {
966        self.bindings.nonlocals.iter().map(AsRef::as_ref)
967    }
968
969    pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
970        self.locals
971            .iter()
972            .map(|binding| (binding.name.as_ref(), binding.declaration))
973    }
974
975    fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
976        let name = node_text(node, source);
977        self.record_local(name, node);
978    }
979
980    fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
981        if name.is_empty() {
982            return;
983        }
984        let binding_kind = if is_function_declaration_name(declaration) {
985            PythonLocalBindingKind::FunctionOnly
986        } else if declaration.parent().is_some_and(|parent| {
987            parent.kind() == "class_definition"
988                && brokk_bifrost_core::analyzer::tree_walk::is_field_child(
989                    parent,
990                    declaration,
991                    "name",
992                )
993        }) {
994            PythonLocalBindingKind::ClassOnly
995        } else {
996            PythonLocalBindingKind::Other
997        };
998        self.record_local_kind(name, declaration, binding_kind);
999    }
1000
1001    fn record_local_kind(
1002        &mut self,
1003        name: &str,
1004        declaration: Node<'tree>,
1005        binding_kind: PythonLocalBindingKind,
1006    ) {
1007        if name.is_empty() {
1008            return;
1009        }
1010        self.binding_nodes.push(declaration);
1011        match self.bindings.local_names.entry(name.into()) {
1012            std::collections::hash_map::Entry::Vacant(entry) => {
1013                entry.insert(binding_kind);
1014                self.locals.push(PythonLocalBinding {
1015                    name: name.into(),
1016                    declaration,
1017                });
1018            }
1019            std::collections::hash_map::Entry::Occupied(mut entry) => {
1020                entry.insert(PythonLocalBindingKind::Other);
1021            }
1022        }
1023    }
1024
1025    fn record_binding_write(&mut self, name: &str, declaration: Node<'tree>) {
1026        if !name.is_empty() {
1027            self.bindings.binding_writes.insert(name.into());
1028        }
1029        self.record_local(name, declaration);
1030    }
1031}
1032
1033/// Prove that a selected `global` route has no known binding event in its
1034/// executing enclosing scopes. Outer locals are unrelated to this namespace;
1035/// an outer scope declaring the same global can still replace its identity.
1036/// Class namespaces are not captured: only explicit global events are read.
1037/// The caller has already selected `global_scope` by lexical precedence.
1038pub fn python_global_binding_is_untouched_bounded(
1039    name: &str,
1040    reference: Node<'_>,
1041    global_scope: Node<'_>,
1042    source: &str,
1043    mut scope_step: impl FnMut() -> bool,
1044) -> Option<bool> {
1045    assert!(matches!(
1046        global_scope.kind(),
1047        "function_definition" | "lambda" | "class_definition"
1048    ));
1049    let mut current = Some(global_scope);
1050    while let Some(scope) = current {
1051        if !scope_step() {
1052            return None;
1053        }
1054        if matches!(
1055            scope.kind(),
1056            "function_definition" | "lambda" | "class_definition"
1057        ) && scope.child_by_field_name("body").is_some_and(|body| {
1058            body.start_byte() <= reference.start_byte() && reference.end_byte() <= body.end_byte()
1059        }) {
1060            let inventory = if scope.kind() == "class_definition" {
1061                PythonClassReferenceScope::new_bounded(scope, reference, &mut scope_step)?
1062                    .collect_bounded(source, &mut scope_step)?
1063            } else {
1064                PythonLexicalScopeInventory::collect_bounded(
1065                    scope,
1066                    source,
1067                    std::iter::empty::<String>(),
1068                    &mut scope_step,
1069                )?
1070            };
1071            if inventory.bindings.has_global_binding_event(name) {
1072                return Some(false);
1073            }
1074        }
1075        current = scope.parent();
1076    }
1077    Some(true)
1078}
1079
1080fn is_function_declaration_name(node: Node<'_>) -> bool {
1081    node.parent().is_some_and(|parent| {
1082        parent.kind() == "function_definition"
1083            && brokk_bifrost_core::analyzer::tree_walk::is_field_child(parent, node, "name")
1084    })
1085}
1086
1087/// Collect the binders in one already-selected assignment or loop target.
1088/// Attribute/subscript operands remain reads, and exhaustion stays explicit.
1089/// Callers supply the grammar field rather than reconstructing target syntax.
1090pub fn python_binding_targets_bounded<'tree>(
1091    target: Node<'tree>,
1092    source: &str,
1093    mut scope_step: impl FnMut() -> bool,
1094) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1095    let mut bindings = Vec::new();
1096    collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
1097        bindings.push(PythonDirectScopeBinding {
1098            declaration,
1099            kind: PythonDirectScopeBindingKind::Other,
1100        });
1101    })?;
1102    Some(bindings)
1103}
1104
1105/// Return the bindings introduced directly by `node`.
1106///
1107/// Descendant traversal remains the caller's responsibility. This lets the
1108/// semantic file walk build a module binding inventory without adding a
1109/// second whole-file scan, while reusing the same structured target handling
1110/// as function symbol-table discovery.
1111pub fn python_direct_scope_bindings_bounded<'tree>(
1112    node: Node<'tree>,
1113    source: &str,
1114    mut scope_step: impl FnMut() -> bool,
1115) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
1116    let mut bindings = Vec::new();
1117
1118    match node.kind() {
1119        "function_definition" => {
1120            if let Some(name) = node.child_by_field_name("name") {
1121                bindings.push(PythonDirectScopeBinding {
1122                    declaration: name,
1123                    kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1124                        PythonDirectScopeBindingKind::FunctionDeclaration
1125                    } else {
1126                        PythonDirectScopeBindingKind::Other
1127                    },
1128                });
1129            }
1130        }
1131        "class_definition" => {
1132            if let Some(name) = node.child_by_field_name("name") {
1133                bindings.push(PythonDirectScopeBinding {
1134                    declaration: name,
1135                    kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
1136                        PythonDirectScopeBindingKind::ClassDeclaration
1137                    } else {
1138                        PythonDirectScopeBindingKind::Other
1139                    },
1140                });
1141            }
1142        }
1143        "import_statement" | "import_from_statement" => {
1144            let module_level = is_direct_module_definition_bounded(node, &mut scope_step)?;
1145            collect_import_bindings(node, &mut scope_step, |declaration| {
1146                bindings.push(PythonDirectScopeBinding {
1147                    declaration,
1148                    kind: if module_level {
1149                        PythonDirectScopeBindingKind::Import
1150                    } else {
1151                        PythonDirectScopeBindingKind::Other
1152                    },
1153                });
1154            })?;
1155        }
1156        "assignment" | "annotated_assignment" | "augmented_assignment" => {
1157            if let Some(target) = node.child_by_field_name("left") {
1158                bindings.extend(python_binding_targets_bounded(
1159                    target,
1160                    source,
1161                    &mut scope_step,
1162                )?);
1163            }
1164        }
1165        "type_alias_statement" => {
1166            if let Some(target) = node.child_by_field_name("left")
1167                && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
1168            {
1169                bindings.push(PythonDirectScopeBinding {
1170                    declaration,
1171                    kind: PythonDirectScopeBindingKind::Other,
1172                });
1173            }
1174        }
1175        "named_expression" => {
1176            if let Some(target) = node.child_by_field_name("name") {
1177                bindings.extend(python_binding_targets_bounded(
1178                    target,
1179                    source,
1180                    &mut scope_step,
1181                )?);
1182            }
1183        }
1184        "for_statement" | "async_for_statement" => {
1185            if let Some(target) = node.child_by_field_name("left") {
1186                bindings.extend(python_binding_targets_bounded(
1187                    target,
1188                    source,
1189                    &mut scope_step,
1190                )?);
1191            }
1192        }
1193        "with_statement" => {
1194            let mut cursor = node.walk();
1195            for item in node
1196                .named_children(&mut cursor)
1197                .filter(|item| item.kind() == "with_item")
1198            {
1199                if let Some(alias) = item.child_by_field_name("alias") {
1200                    collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1201                        bindings.push(PythonDirectScopeBinding {
1202                            declaration,
1203                            kind: PythonDirectScopeBindingKind::Other,
1204                        });
1205                    })?;
1206                }
1207            }
1208        }
1209        "delete_statement" => {
1210            for target in named_children_bounded(node, &mut scope_step)? {
1211                bindings.extend(python_binding_targets_bounded(
1212                    target,
1213                    source,
1214                    &mut scope_step,
1215                )?);
1216            }
1217        }
1218        "as_pattern" => {
1219            if let Some(alias) = node.child_by_field_name("alias") {
1220                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1221                    bindings.push(PythonDirectScopeBinding {
1222                        declaration,
1223                        kind: PythonDirectScopeBindingKind::Other,
1224                    });
1225                })?;
1226            }
1227        }
1228        "except_clause" => {
1229            if let Some(alias) = node.child_by_field_name("alias") {
1230                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
1231                    bindings.push(PythonDirectScopeBinding {
1232                        declaration,
1233                        kind: PythonDirectScopeBindingKind::Other,
1234                    });
1235                })?;
1236            }
1237        }
1238        "case_clause" => {
1239            for child in named_children_bounded(node, &mut scope_step)? {
1240                if child.kind() == "case_pattern" {
1241                    collect_match_pattern_bindings(
1242                        child,
1243                        source,
1244                        &mut scope_step,
1245                        |_, declaration| {
1246                            bindings.push(PythonDirectScopeBinding {
1247                                declaration,
1248                                kind: PythonDirectScopeBindingKind::Other,
1249                            });
1250                        },
1251                    )?;
1252                }
1253            }
1254        }
1255        _ => {}
1256    }
1257    Some(bindings)
1258}
1259
1260/// Whether a module or class execution scope binds `target_name` anywhere in
1261/// its direct scope. Nested callable and class bodies are separate scopes, but
1262/// their headers still execute in this scope. A wildcard import keeps the
1263/// name's runtime identity open even when it does not expose a static binder.
1264pub fn python_module_or_class_scope_binds_name_bounded(
1265    scope: Node<'_>,
1266    target_name: &str,
1267    source: &str,
1268    scope_step: impl FnMut() -> bool,
1269) -> Option<bool> {
1270    python_scope_binds_name_with_filter_bounded(scope, target_name, source, |_| true, scope_step)
1271}
1272
1273fn python_scope_binds_name_with_filter_bounded<'tree>(
1274    scope: Node<'tree>,
1275    target_name: &str,
1276    source: &str,
1277    include_binding: impl Fn(Node<'tree>) -> bool,
1278    mut scope_step: impl FnMut() -> bool,
1279) -> Option<bool> {
1280    assert!(matches!(scope.kind(), "module" | "class_definition"));
1281    let body = scope.child_by_field_name("body").unwrap_or(scope);
1282    let mut stack = vec![body];
1283    while let Some(node) = stack.pop() {
1284        if !scope_step() {
1285            return None;
1286        }
1287        if node.kind() == "wildcard_import" {
1288            return Some(true);
1289        }
1290        if python_direct_scope_bindings_bounded(node, source, &mut scope_step)?
1291            .into_iter()
1292            .any(|binding| {
1293                include_binding(binding.declaration)
1294                    && node_text(binding.declaration, source) == target_name
1295            })
1296        {
1297            return Some(true);
1298        }
1299        stack.extend(python_executable_scope_children(node));
1300    }
1301    Some(false)
1302}
1303
1304/// Iterate executable-scope children, skipping nested callable and class bodies.
1305pub fn python_executable_scope_children<'tree>(
1306    node: Node<'tree>,
1307) -> impl Iterator<Item = Node<'tree>> {
1308    let excluded_body = matches!(
1309        node.kind(),
1310        "class_definition" | "function_definition" | "lambda"
1311    )
1312    .then(|| node.child_by_field_name("body").map(|body| body.id()))
1313    .flatten();
1314    named_children_iter(node).filter(move |child| Some(child.id()) != excluded_body)
1315}
1316
1317pub fn python_unambiguous_module_class_binding_bounded(
1318    root: Node<'_>,
1319    source: &str,
1320    target_name: &str,
1321    scope_step: impl FnMut() -> bool,
1322) -> Option<bool> {
1323    Some(
1324        python_unambiguous_module_binding_bounded(root, source, target_name, scope_step)?
1325            == Some(PythonDirectScopeBindingKind::ClassDeclaration),
1326    )
1327}
1328
1329/// The kind of the module scope's one binding of `target_name`.
1330///
1331/// `Some(None)` says the module does not bind the name, binds it more than
1332/// once, or leaves its identity open through a wildcard import; the name is
1333/// then not a proven reference to one module-level declaration. `None` says the
1334/// bounded walk ran out of work.
1335pub fn python_unambiguous_module_binding_bounded(
1336    root: Node<'_>,
1337    source: &str,
1338    target_name: &str,
1339    scope_step: impl FnMut() -> bool,
1340) -> Option<Option<PythonDirectScopeBindingKind>> {
1341    Some(
1342        python_unambiguous_module_binding_node_bounded(root, source, target_name, scope_step)?
1343            .map(|binding| binding.kind),
1344    )
1345}
1346
1347/// The module scope's unique binding event, after the bounded proof completes.
1348///
1349/// A returned node identifies the actual binder, not a proven runtime value.
1350/// `Other` still needs the caller's syntax and visibility checks. Competing
1351/// events or a later wildcard return `Some(None)`; an interrupted proof returns
1352/// `None` and must never expose a binder found before the interruption.
1353pub fn python_unambiguous_module_binding_node_bounded<'tree>(
1354    root: Node<'tree>,
1355    source: &str,
1356    target_name: &str,
1357    mut scope_step: impl FnMut() -> bool,
1358) -> Option<Option<PythonDirectScopeBinding<'tree>>> {
1359    let mut matched = None;
1360    let mut stack = vec![root];
1361    while let Some(node) = stack.pop() {
1362        if !scope_step() {
1363            return None;
1364        }
1365        // A later unconditional declaration replaces an earlier wildcard
1366        // binding; a wildcard after it leaves its identity open.
1367        if node.kind() == "wildcard_import" && matched.is_some() {
1368            return Some(None);
1369        }
1370        for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
1371            if node_text(binding.declaration, source) != target_name {
1372                continue;
1373            }
1374            if matched.is_some() {
1375                return Some(None);
1376            }
1377            matched = Some(binding);
1378        }
1379
1380        let body = matches!(
1381            node.kind(),
1382            "function_definition" | "class_definition" | "lambda"
1383        )
1384        .then(|| node.child_by_field_name("body").map(|child| child.id()))
1385        .flatten();
1386        let name = matches!(node.kind(), "function_definition" | "class_definition")
1387            .then(|| node.child_by_field_name("name").map(|child| child.id()))
1388            .flatten();
1389        for child in named_children_bounded(node, &mut scope_step)?
1390            .into_iter()
1391            .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
1392            .rev()
1393        {
1394            stack.push(child);
1395        }
1396    }
1397    Some(matched)
1398}
1399
1400fn is_direct_module_definition_bounded(
1401    node: Node<'_>,
1402    scope_step: &mut impl FnMut() -> bool,
1403) -> Option<bool> {
1404    if !scope_step() {
1405        return None;
1406    }
1407    let Some(mut parent) = node.parent() else {
1408        return Some(false);
1409    };
1410    if parent.kind() == "decorated_definition" {
1411        if !scope_step() {
1412            return None;
1413        }
1414        let Some(grandparent) = parent.parent() else {
1415            return Some(false);
1416        };
1417        parent = grandparent;
1418    }
1419    Some(parent.kind() == "module")
1420}
1421
1422fn collect_direct_identifier_names(
1423    node: Node<'_>,
1424    source: &str,
1425    scope_step: &mut impl FnMut() -> bool,
1426    mut record: impl FnMut(&str),
1427) -> Option<()> {
1428    for child in named_children_bounded(node, scope_step)? {
1429        if child.kind() == "identifier" {
1430            let name = node_text(child, source);
1431            if !name.is_empty() {
1432                record(name);
1433            }
1434        }
1435    }
1436    Some(())
1437}
1438
1439fn collect_import_bindings<'tree>(
1440    statement: Node<'tree>,
1441    scope_step: &mut impl FnMut() -> bool,
1442    mut record: impl FnMut(Node<'tree>),
1443) -> Option<()> {
1444    let mut cursor = statement.walk();
1445    let mut imports = Vec::new();
1446    for imported in statement.children_by_field_name("name", &mut cursor) {
1447        if !scope_step() {
1448            return None;
1449        }
1450        imports.push(imported);
1451    }
1452    for imported in imports {
1453        if let Some(alias) = imported.child_by_field_name("alias") {
1454            record(alias);
1455            continue;
1456        }
1457        let name = imported.child_by_field_name("name").unwrap_or(imported);
1458        let binding = if statement.kind() == "import_statement" {
1459            first_identifier_bounded(name, scope_step)?
1460        } else {
1461            last_identifier_bounded(name, scope_step)?
1462        };
1463        if let Some(binding) = binding {
1464            record(binding);
1465        }
1466    }
1467    Some(())
1468}
1469
1470fn first_identifier_bounded<'tree>(
1471    root: Node<'tree>,
1472    scope_step: &mut impl FnMut() -> bool,
1473) -> Option<Option<Node<'tree>>> {
1474    let mut stack = vec![root];
1475    while let Some(node) = stack.pop() {
1476        if !scope_step() {
1477            return None;
1478        }
1479        if node.kind() == "identifier" {
1480            return Some(Some(node));
1481        }
1482        let children = named_children_bounded(node, scope_step)?;
1483        stack.extend(children.into_iter().rev());
1484    }
1485    Some(None)
1486}
1487
1488fn last_identifier_bounded<'tree>(
1489    root: Node<'tree>,
1490    scope_step: &mut impl FnMut() -> bool,
1491) -> Option<Option<Node<'tree>>> {
1492    let mut result = None;
1493    let mut stack = vec![root];
1494    while let Some(node) = stack.pop() {
1495        if !scope_step() {
1496            return None;
1497        }
1498        if node.kind() == "identifier" {
1499            result = Some(node);
1500            continue;
1501        }
1502        let children = named_children_bounded(node, scope_step)?;
1503        stack.extend(children.into_iter().rev());
1504    }
1505    Some(result)
1506}
1507
1508pub(crate) fn collect_binding_targets<'tree>(
1509    target: Node<'tree>,
1510    source: &str,
1511    scope_step: &mut impl FnMut() -> bool,
1512    mut record: impl FnMut(&str, Node<'tree>),
1513) -> Option<()> {
1514    let mut stack = vec![target];
1515    while let Some(node) = stack.pop() {
1516        if !scope_step() {
1517            return None;
1518        }
1519        match node.kind() {
1520            // These mutate an existing object and do not bind either the
1521            // receiver or member name in the function.
1522            "attribute" | "subscript" => continue,
1523            "identifier" | "keyword_identifier" => {
1524                let name = node_text(node, source);
1525                if !name.is_empty() {
1526                    record(name, node);
1527                }
1528                continue;
1529            }
1530            _ => {}
1531        }
1532        let children = named_children_bounded(node, scope_step)?;
1533        if node.kind() == "as_pattern_target" && children.is_empty() {
1534            let name = node_text(node, source);
1535            if !name.is_empty() {
1536                record(name, node);
1537            }
1538            continue;
1539        }
1540        stack.extend(children.into_iter().rev());
1541    }
1542    Some(())
1543}
1544
1545fn collect_match_pattern_bindings<'tree>(
1546    pattern: Node<'tree>,
1547    source: &str,
1548    scope_step: &mut impl FnMut() -> bool,
1549    mut record: impl FnMut(&str, Node<'tree>),
1550) -> Option<()> {
1551    let mut stack = vec![pattern];
1552    while let Some(node) = stack.pop() {
1553        if !scope_step() {
1554            return None;
1555        }
1556        match node.kind() {
1557            "dotted_name" => {
1558                let identifiers = named_children_bounded(node, scope_step)?
1559                    .into_iter()
1560                    .filter(|child| child.kind() == "identifier")
1561                    .collect::<Vec<_>>();
1562                if let [binding] = identifiers.as_slice() {
1563                    let name = node_text(*binding, source);
1564                    if !name.is_empty() {
1565                        record(name, *binding);
1566                    }
1567                }
1568                continue;
1569            }
1570            "splat_pattern" => {
1571                for child in named_children_bounded(node, scope_step)? {
1572                    if child.kind() == "identifier" {
1573                        let name = node_text(child, source);
1574                        if !name.is_empty() {
1575                            record(name, child);
1576                        }
1577                    }
1578                }
1579                continue;
1580            }
1581            "class_pattern" => {
1582                let mut children = named_children_bounded(node, scope_step)?;
1583                if children
1584                    .first()
1585                    .is_some_and(|child| child.kind() == "dotted_name")
1586                {
1587                    children.remove(0);
1588                }
1589                stack.extend(children.into_iter().rev());
1590                continue;
1591            }
1592            "keyword_pattern" => {
1593                let mut children = named_children_bounded(node, scope_step)?;
1594                if children
1595                    .first()
1596                    .is_some_and(|child| child.kind() == "identifier")
1597                {
1598                    children.remove(0);
1599                }
1600                stack.extend(children.into_iter().rev());
1601                continue;
1602            }
1603            "dict_pattern" => {
1604                let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
1605                    .into_iter()
1606                    .map(|key| key.id())
1607                    .collect::<HashSet<_>>();
1608                let children = named_children_bounded(node, scope_step)?;
1609                stack.extend(
1610                    children
1611                        .into_iter()
1612                        .filter(|child| !key_ids.contains(&child.id()))
1613                        .rev(),
1614                );
1615                continue;
1616            }
1617            "as_pattern" => {
1618                if let Some(alias) = node.child_by_field_name("alias") {
1619                    collect_binding_targets(alias, source, scope_step, &mut record)?;
1620                    let children = named_children_bounded(node, scope_step)?;
1621                    stack.extend(
1622                        children
1623                            .into_iter()
1624                            .filter(|child| child.id() != alias.id())
1625                            .rev(),
1626                    );
1627                    continue;
1628                }
1629                let mut children = named_children_bounded(node, scope_step)?;
1630                if let Some(alias) = children
1631                    .last()
1632                    .copied()
1633                    .filter(|child| child.kind() == "identifier")
1634                {
1635                    let name = node_text(alias, source);
1636                    if !name.is_empty() {
1637                        record(name, alias);
1638                    }
1639                    children.pop();
1640                }
1641                stack.extend(children.into_iter().rev());
1642                continue;
1643            }
1644            "identifier" => {
1645                // Direct identifiers are match `as` aliases handled by their
1646                // parent. Keyword names and class heads are likewise skipped.
1647                continue;
1648            }
1649            kind if is_pattern_literal(kind) => continue,
1650            _ => {}
1651        }
1652        let children = named_children_bounded(node, scope_step)?;
1653        stack.extend(children.into_iter().rev());
1654    }
1655    Some(())
1656}
1657
1658fn push_named_children<'tree>(
1659    stack: &mut Vec<Node<'tree>>,
1660    node: Node<'tree>,
1661    scope_step: &mut impl FnMut() -> bool,
1662) -> Option<()> {
1663    let children = named_children_bounded(node, scope_step)?;
1664    push_bounded_children(stack, children.into_iter());
1665    Some(())
1666}
1667
1668fn push_named_children_except<'tree>(
1669    stack: &mut Vec<Node<'tree>>,
1670    node: Node<'tree>,
1671    excluded: Node<'tree>,
1672    scope_step: &mut impl FnMut() -> bool,
1673) -> Option<()> {
1674    let children = named_children_bounded(node, scope_step)?;
1675    push_bounded_children(
1676        stack,
1677        children
1678            .into_iter()
1679            .filter(|child| child.id() != excluded.id()),
1680    );
1681    Some(())
1682}
1683
1684fn push_nested_scope_header_children<'tree>(
1685    stack: &mut Vec<Node<'tree>>,
1686    node: Node<'tree>,
1687    scope_step: &mut impl FnMut() -> bool,
1688) -> Option<()> {
1689    let body = node.child_by_field_name("body").map(|child| child.id());
1690    let name = node.child_by_field_name("name").map(|child| child.id());
1691    // Prune execution boundaries before charging or visiting their bodies.
1692    // Only the header belongs to the enclosing scope's collector walk.
1693    let mut cursor = node.walk();
1694    let children = node
1695        .named_children(&mut cursor)
1696        .filter(|child| Some(child.id()) != body);
1697    let mut headers = Vec::new();
1698    push_bounded_nodes(children, &mut headers, scope_step).then_some(())?;
1699    push_bounded_children(
1700        stack,
1701        headers.into_iter().filter(|child| Some(child.id()) != name),
1702    );
1703    Some(())
1704}
1705
1706fn push_bounded_children<'tree>(
1707    stack: &mut Vec<Node<'tree>>,
1708    children: impl DoubleEndedIterator<Item = Node<'tree>>,
1709) {
1710    for child in children.rev() {
1711        stack.push(child);
1712    }
1713}
1714
1715fn named_children_bounded<'tree>(
1716    node: Node<'tree>,
1717    scope_step: &mut impl FnMut() -> bool,
1718) -> Option<Vec<Node<'tree>>> {
1719    let mut cursor = node.walk();
1720    let mut children = Vec::new();
1721    push_bounded_nodes(node.named_children(&mut cursor), &mut children, scope_step)
1722        .then_some(children)
1723}
1724
1725fn children_by_field_name_bounded<'tree>(
1726    node: Node<'tree>,
1727    field: &str,
1728    scope_step: &mut impl FnMut() -> bool,
1729) -> Option<Vec<Node<'tree>>> {
1730    let mut cursor = node.walk();
1731    let mut children = Vec::new();
1732    push_bounded_nodes(
1733        node.children_by_field_name(field, &mut cursor),
1734        &mut children,
1735        scope_step,
1736    )
1737    .then_some(children)
1738}
1739
1740pub(crate) fn is_comprehension(kind: &str) -> bool {
1741    matches!(
1742        kind,
1743        "list_comprehension"
1744            | "set_comprehension"
1745            | "dictionary_comprehension"
1746            | "generator_expression"
1747    )
1748}
1749
1750fn is_pattern_literal(kind: &str) -> bool {
1751    matches!(
1752        kind,
1753        "string"
1754            | "concatenated_string"
1755            | "integer"
1756            | "float"
1757            | "complex_pattern"
1758            | "true"
1759            | "false"
1760            | "none"
1761    )
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    use brokk_bifrost_test_macros::hardwon;
1767
1768    #[test]
1769    fn only_the_first_comprehension_iterable_uses_the_enclosing_scope() {
1770        let source = "def run(first, second):\n    return [second for first in first for second in second]\n";
1771        let mut parser = tree_sitter::Parser::new();
1772        parser
1773            .set_language(&tree_sitter_python::LANGUAGE.into())
1774            .unwrap();
1775        let tree = parser.parse(source, None).unwrap();
1776        let mut stack = vec![tree.root_node()];
1777        let comprehension = loop {
1778            let node = stack.pop().expect("fixture contains a comprehension");
1779            if node.kind() == "list_comprehension" {
1780                break node;
1781            }
1782            let mut cursor = node.walk();
1783            stack.extend(node.named_children(&mut cursor));
1784        };
1785        let mut cursor = comprehension.walk();
1786        let clauses = comprehension
1787            .named_children(&mut cursor)
1788            .filter(|node| node.kind() == "for_in_clause")
1789            .collect::<Vec<_>>();
1790        let outer = clauses[0].child_by_field_name("right").unwrap();
1791        let inner = clauses[1].child_by_field_name("right").unwrap();
1792        assert!(!super::python_comprehension_binds_name_at(
1793            "first", outer, source
1794        ));
1795        assert!(super::python_comprehension_binds_name_at(
1796            "second", inner, source
1797        ));
1798    }
1799
1800    #[test]
1801    #[hardwon(
1802        "#5044+#5511+#5751: bounded lexical proofs bypassed stops, lazy bindings or competing events"
1803    )]
1804    fn bounded_comprehension_lookup_preserves_exhaustion() {
1805        let source = "def run(xs):\n    class Base:\n        pass\n    def helper():\n        pass\n    class Derived(Base):\n        pass\n    helper()\n    return [target for target in xs]\n";
1806        let tree = crate::declarations::parse_python_tree(source).unwrap();
1807        let function = tree.root_node().named_child(0).unwrap();
1808        let inventory = super::PythonLexicalScopeInventory::collect_bounded(
1809            function,
1810            source,
1811            ["xs".to_owned()],
1812            || true,
1813        )
1814        .unwrap();
1815        let mut stack = vec![function];
1816        let comprehension = loop {
1817            let node = stack.pop().expect("fixture contains a comprehension");
1818            if node.kind() == "list_comprehension" {
1819                break node;
1820            }
1821            stack.extend(brokk_bifrost_core::analyzer::tree_walk::named_children_iter(node));
1822        };
1823        let reference = comprehension.named_child(0).unwrap();
1824        for mut remaining in [0_usize, 1] {
1825            assert_eq!(
1826                inventory.name_resolution_at_bounded("target", reference, || {
1827                    let available = remaining > 0;
1828                    remaining = remaining.saturating_sub(1);
1829                    available
1830                }),
1831                None
1832            );
1833        }
1834        assert_eq!(
1835            inventory.name_resolution_at_bounded("target", reference, || true),
1836            Some(super::PythonLexicalNameResolution::Local)
1837        );
1838        let body = function.child_by_field_name("body").unwrap();
1839        let derived = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1840            .find(|node| node.child_by_field_name("superclasses").is_some())
1841            .unwrap();
1842        let base_reference = derived
1843            .child_by_field_name("superclasses")
1844            .unwrap()
1845            .named_child(0)
1846            .unwrap();
1847        let call = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(body)
1848            .filter(|node| node.kind() == "expression_statement")
1849            .find_map(|node| node.named_child(0).filter(|node| node.kind() == "call"))
1850            .unwrap();
1851        let helper_reference = call.child_by_field_name("function").unwrap();
1852        // Collection has completed, but every later lookup must still charge
1853        // retained comprehension facts. One step cannot finish that scan.
1854        let one_step = || {
1855            let mut available = true;
1856            move || std::mem::take(&mut available)
1857        };
1858        assert!(
1859            inventory
1860                .has_runtime_callable_binding_at_bounded("helper", helper_reference, one_step())
1861                .is_none()
1862        );
1863        assert!(
1864            inventory
1865                .local_function_declaration_bounded("helper", helper_reference, one_step())
1866                .is_none()
1867        );
1868        assert!(
1869            inventory
1870                .local_class_declaration_bounded("Base", base_reference, one_step())
1871                .is_none()
1872        );
1873        assert_eq!(
1874            inventory
1875                .local_function_declaration_bounded("helper", helper_reference, || true)
1876                .unwrap()
1877                .kind(),
1878            "function_definition"
1879        );
1880        assert_eq!(
1881            inventory
1882                .local_class_declaration_bounded("Base", base_reference, || true)
1883                .unwrap()
1884                .kind(),
1885            "class_definition"
1886        );
1887        let bindings = inventory.into_bindings();
1888        assert_eq!(
1889            bindings.name_resolution_at_bounded("missing", reference, one_step()),
1890            None,
1891            "stopped lookup must not manufacture Unbound"
1892        );
1893        assert_eq!(
1894            bindings.name_resolution_at_bounded("missing", reference, || true),
1895            Some(super::PythonLexicalNameResolution::Unbound)
1896        );
1897        assert_eq!(
1898            bindings.resolves_to_local_function_bounded("helper", helper_reference, one_step()),
1899            None
1900        );
1901        assert_eq!(
1902            bindings.resolves_to_local_function_bounded("helper", helper_reference, || true),
1903            Some(true)
1904        );
1905        assert_eq!(
1906            super::python_comprehension_binds_name_at_bounded(
1907                "target",
1908                reference,
1909                source,
1910                one_step()
1911            ),
1912            None
1913        );
1914        assert_eq!(
1915            super::python_comprehension_binds_name_at_bounded("target", reference, source, || true),
1916            Some(true)
1917        );
1918        let clause = brokk_bifrost_core::analyzer::tree_walk::named_children_iter(comprehension)
1919            .find(|node| node.kind() == "for_in_clause")
1920            .unwrap();
1921        let targets = super::python_binding_targets_bounded(
1922            clause.child_by_field_name("left").unwrap(),
1923            source,
1924            || true,
1925        )
1926        .unwrap();
1927        assert_eq!(targets.len(), 1);
1928        assert_eq!(
1929            Some(targets[0].declaration),
1930            clause.child_by_field_name("left")
1931        );
1932
1933        // An assignment binder is positive evidence only after later events
1934        // and executable headers have been checked. Body-local writes do not
1935        // compete, but header walrus writes and later wildcards do.
1936        use super::PythonDirectScopeBindingKind::{ClassDeclaration, Other};
1937        for (source, expected_kind) in [
1938            ("Alias = Base\ntrailing = None\n", Some(Other)),
1939            ("Alias = Base\nAlias = Other\n", None),
1940            ("Alias = Base\ndel Alias\n", None),
1941            ("Alias = Base\nfrom pkg import Target as Alias\n", None),
1942            ("Alias = Base\nfrom pkg import *\n", None),
1943            ("from pkg import *\nAlias = Base\n", Some(Other)),
1944            (
1945                "Alias = Base\ndef inner():\n    Alias = Other\nclass Container:\n    Alias = Other\n",
1946                Some(Other),
1947            ),
1948            (
1949                "Alias = Base\ndef inner(value=(Alias := Other)):\n    pass\n",
1950                None,
1951            ),
1952            ("class Alias:\n    pass\n", Some(ClassDeclaration)),
1953            (
1954                "from pkg import *\nclass Alias:\n    pass\n",
1955                Some(ClassDeclaration),
1956            ),
1957            ("class Alias:\n    pass\nfrom pkg import *\n", None),
1958            ("Other = Base\n", None),
1959        ] {
1960            let tree = crate::declarations::parse_python_tree(source).unwrap();
1961            let root = tree.root_node();
1962            assert!(!root.has_error(), "{source}");
1963            let mut steps = 0_usize;
1964            let binding = super::python_unambiguous_module_binding_node_bounded(
1965                root,
1966                source,
1967                "Alias",
1968                || {
1969                    steps += 1;
1970                    true
1971                },
1972            )
1973            .expect("uninterrupted binding proof completes");
1974            assert_eq!(
1975                binding.map(|binding| binding.kind),
1976                expected_kind,
1977                "{source}"
1978            );
1979            if let Some(binding) = binding {
1980                assert_eq!(binding.declaration.kind(), "identifier");
1981                assert_eq!(super::node_text(binding.declaration, source), "Alias");
1982                let parent = binding.declaration.parent().unwrap();
1983                let field = if binding.kind == ClassDeclaration {
1984                    "name"
1985                } else {
1986                    "left"
1987                };
1988                assert_eq!(parent.child_by_field_name(field), Some(binding.declaration));
1989                let statement = if binding.kind == ClassDeclaration {
1990                    parent
1991                } else {
1992                    parent.parent().unwrap()
1993                };
1994                assert_eq!(statement.parent(), Some(root), "exact module-owned binder");
1995            }
1996            assert_eq!(
1997                super::python_unambiguous_module_binding_bounded(root, source, "Alias", || true),
1998                Some(expected_kind),
1999                "kind-only consumers retain the same positive requirement: {source}"
2000            );
2001            assert_eq!(
2002                super::python_unambiguous_module_class_binding_bounded(
2003                    root,
2004                    source,
2005                    "Alias",
2006                    || true
2007                ),
2008                Some(expected_kind == Some(ClassDeclaration))
2009            );
2010            for mut remaining in 0..steps {
2011                assert!(
2012                    super::python_unambiguous_module_binding_node_bounded(
2013                        root,
2014                        source,
2015                        "Alias",
2016                        || {
2017                            let available = remaining > 0;
2018                            remaining = remaining.saturating_sub(1);
2019                            available
2020                        },
2021                    )
2022                    .is_none(),
2023                    "a stopped proof cannot publish its early binder: {source}"
2024                );
2025            }
2026        }
2027
2028        // The same spelling is an immediate base root and a lazy bound root.
2029        // Generic hierarchy refusal cannot test this shared binding distinction.
2030        let source = "import ns_source as ns\nclass Outer:\n    class ns[T: ns.Target]((ns.Target)):\n        pass\n";
2031        let tree = crate::declarations::parse_python_tree(source).unwrap();
2032        assert!(!tree.root_node().has_error());
2033        let outer = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
2034            tree.root_node(),
2035            "class_definition",
2036        )
2037        .unwrap();
2038        let inner = brokk_bifrost_core::analyzer::tree_walk::first_named_child_of_kind(
2039            outer.child_by_field_name("body").unwrap(),
2040            "class_definition",
2041        )
2042        .unwrap();
2043        let base = inner
2044            .child_by_field_name("superclasses")
2045            .unwrap()
2046            .named_child(0)
2047            .unwrap();
2048        let immediate = crate::syntax::python_static_value_path_bounded(base, || true)
2049            .unwrap()
2050            .unwrap()[0];
2051        let bound =
2052            brokk_bifrost_core::analyzer::tree_walk::first_named_descendant_of_kind_inclusive(
2053                inner.child_by_field_name("type_parameters").unwrap(),
2054                "attribute",
2055            )
2056            .unwrap()
2057            .child_by_field_name("object")
2058            .unwrap();
2059        for (reference, resolution, binds) in [
2060            (
2061                immediate,
2062                super::PythonLexicalNameResolution::Unbound,
2063                false,
2064            ),
2065            (bound, super::PythonLexicalNameResolution::Local, true),
2066        ] {
2067            assert!(
2068                super::PythonClassReferenceScope::new_bounded(outer, reference, || false).is_none()
2069            );
2070            let scope =
2071                super::PythonClassReferenceScope::new_bounded(outer, reference, || true).unwrap();
2072            let inventory = scope.collect_bounded(source, || true).unwrap();
2073            assert_eq!(
2074                inventory.name_resolution_at_bounded("ns", reference, || true),
2075                Some(resolution)
2076            );
2077            assert_eq!(scope.binds_name_bounded("ns", source, || true), Some(binds));
2078        }
2079    }
2080}