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