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