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