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