Skip to main content

brokk_bifrost_python/
bindings.rs

1use tree_sitter::Node;
2
3use brokk_bifrost_core::hash::{HashMap, HashSet};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum PythonLexicalNameResolution {
7    Unbound,
8    Local,
9    Nonlocal,
10    Global,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum PythonDirectScopeBindingKind {
15    ClassDeclaration,
16    Other,
17}
18
19#[derive(Clone, Copy, Debug)]
20pub struct PythonDirectScopeBinding<'tree> {
21    pub declaration: Node<'tree>,
22    pub kind: PythonDirectScopeBindingKind,
23}
24
25#[derive(Clone, Debug)]
26struct PythonLocalBinding<'tree> {
27    name: Box<str>,
28    declaration: Node<'tree>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32enum PythonLocalBindingKind {
33    FunctionOnly,
34    Other,
35}
36
37#[derive(Clone, Debug)]
38struct PythonComprehensionBinding {
39    name: Box<str>,
40    start_byte: usize,
41    end_byte: usize,
42    enclosing_iterable_ranges: Vec<(usize, usize)>,
43}
44
45/// Function-scope Python bindings discovered from tree-sitter structure.
46///
47/// The inventory models Python's whole-function symbol-table behavior while
48/// retaining the implicit scope of comprehension targets. Construction is
49/// iterative and every inspected node is gated by `scope_step`; `None` means
50/// the caller stopped discovery and must conservatively avoid module fallback.
51pub struct PythonLexicalScopeInventory<'tree> {
52    parameters: HashSet<Box<str>>,
53    locals: Vec<PythonLocalBinding<'tree>>,
54    local_names: HashMap<Box<str>, PythonLocalBindingKind>,
55    globals: HashSet<Box<str>>,
56    nonlocals: HashSet<Box<str>>,
57    comprehensions: Vec<PythonComprehensionBinding>,
58}
59
60#[derive(Clone, Copy)]
61struct ScanFrame<'tree> {
62    node: Node<'tree>,
63    in_comprehension: bool,
64}
65
66impl<'tree> PythonLexicalScopeInventory<'tree> {
67    /// `parameter_names` is the callable's formal-parameter name stream. The
68    /// layout it comes from is resolved by `analyzer/lexical_definitions.rs`,
69    /// which dispatches through the analysis-side language registry and so
70    /// cannot be named here; `analyzer/python/lexical_scope.rs` in
71    /// `brokk-bifrost-analysis` is the one caller that computes it, under the
72    /// same `scope_step` meter this walk uses.
73    pub fn collect_bounded(
74        callable: Node<'tree>,
75        source: &str,
76        parameter_names: impl IntoIterator<Item = String>,
77        mut scope_step: impl FnMut() -> bool,
78    ) -> Option<Self> {
79        let mut inventory = Self {
80            parameters: parameter_names.into_iter().map(Box::<str>::from).collect(),
81            locals: Vec::new(),
82            local_names: HashMap::default(),
83            globals: HashSet::default(),
84            nonlocals: HashSet::default(),
85            comprehensions: Vec::new(),
86        };
87        let Some(body) = callable.child_by_field_name("body") else {
88            return Some(inventory);
89        };
90        let mut stack = vec![ScanFrame {
91            node: body,
92            in_comprehension: false,
93        }];
94
95        while let Some(frame) = stack.pop() {
96            if !scope_step() {
97                return None;
98            }
99            let node = frame.node;
100            let nested_scope = node != body
101                && matches!(
102                    node.kind(),
103                    "function_definition" | "lambda" | "class_definition"
104                );
105            if nested_scope {
106                if matches!(node.kind(), "function_definition" | "class_definition")
107                    && let Some(name) = node.child_by_field_name("name")
108                {
109                    inventory.record_local_node(name, source);
110                }
111                // Defaults, annotations, bases, and type-parameter expressions
112                // execute in an enclosing scope. Keep scanning those headers
113                // for walrus/comprehension bindings while pruning the nested
114                // callable or class body itself.
115                push_nested_scope_header_children(
116                    &mut stack,
117                    node,
118                    frame.in_comprehension,
119                    &mut scope_step,
120                )?;
121                continue;
122            }
123
124            match node.kind() {
125                "global_statement" => {
126                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
127                        inventory.globals.insert(name.into());
128                    })?;
129                    continue;
130                }
131                "nonlocal_statement" => {
132                    collect_direct_identifier_names(node, source, &mut scope_step, |name| {
133                        inventory.nonlocals.insert(name.into());
134                    })?;
135                    continue;
136                }
137                "import_statement" | "import_from_statement" => {
138                    collect_import_bindings(node, &mut scope_step, |binding| {
139                        inventory.record_local_node(binding, source)
140                    })?;
141                    continue;
142                }
143                "assignment" | "augmented_assignment" => {
144                    if let Some(target) = node.child_by_field_name("left") {
145                        collect_binding_targets(
146                            target,
147                            source,
148                            &mut scope_step,
149                            |name, declaration| {
150                                inventory.record_local(name, declaration);
151                            },
152                        )?;
153                    }
154                }
155                "type_alias_statement" => {
156                    if let Some(target) = node.child_by_field_name("left")
157                        && let Some(binding) = first_identifier_bounded(target, &mut scope_step)?
158                    {
159                        inventory.record_local_node(binding, source);
160                    }
161                }
162                "named_expression" => {
163                    // PEP 572 binds a comprehension walrus in the containing
164                    // non-comprehension scope, so this remains a function local.
165                    if let Some(target) = node.child_by_field_name("name") {
166                        collect_binding_targets(
167                            target,
168                            source,
169                            &mut scope_step,
170                            |name, declaration| {
171                                inventory.record_local(name, declaration);
172                            },
173                        )?;
174                    }
175                }
176                "for_statement" => {
177                    if let Some(target) = node.child_by_field_name("left") {
178                        collect_binding_targets(
179                            target,
180                            source,
181                            &mut scope_step,
182                            |name, declaration| {
183                                inventory.record_local(name, declaration);
184                            },
185                        )?;
186                    }
187                }
188                "for_in_clause" => {
189                    // These occur inside comprehensions and belong to their
190                    // implicit scope. The enclosing comprehension records them.
191                }
192                "delete_statement" => {
193                    for target in named_children_bounded(node, &mut scope_step)? {
194                        collect_binding_targets(
195                            target,
196                            source,
197                            &mut scope_step,
198                            |name, declaration| {
199                                inventory.record_local(name, declaration);
200                            },
201                        )?;
202                    }
203                    continue;
204                }
205                "as_pattern" => {
206                    if let Some(alias) = node.child_by_field_name("alias") {
207                        collect_binding_targets(
208                            alias,
209                            source,
210                            &mut scope_step,
211                            |name, declaration| {
212                                inventory.record_local(name, declaration);
213                            },
214                        )?;
215                        push_named_children_except(
216                            &mut stack,
217                            node,
218                            alias,
219                            frame.in_comprehension,
220                            &mut scope_step,
221                        )?;
222                        continue;
223                    }
224                }
225                "except_clause" => {
226                    // Older grammar shapes expose the alias directly. Current
227                    // tree-sitter-python nests it in an `as_pattern`, handled
228                    // above when the clause's children are scanned.
229                    if let Some(alias) = node.child_by_field_name("alias") {
230                        collect_binding_targets(
231                            alias,
232                            source,
233                            &mut scope_step,
234                            |name, declaration| {
235                                inventory.record_local(name, declaration);
236                            },
237                        )?;
238                    }
239                }
240                "case_clause" => {
241                    let children = named_children_bounded(node, &mut scope_step)?;
242                    for child in children.iter().copied() {
243                        if child.kind() == "case_pattern" {
244                            collect_match_pattern_bindings(
245                                child,
246                                source,
247                                &mut scope_step,
248                                |name, declaration| {
249                                    inventory.record_local(name, declaration);
250                                },
251                            )?;
252                        }
253                    }
254                    for child in children.into_iter().rev() {
255                        if child.kind() != "case_pattern" {
256                            stack.push(ScanFrame {
257                                node: child,
258                                in_comprehension: frame.in_comprehension,
259                            });
260                        }
261                    }
262                    continue;
263                }
264                kind if is_comprehension(kind) => {
265                    let range = (node.start_byte(), node.end_byte());
266                    let children = named_children_bounded(node, &mut scope_step)?;
267                    let enclosing_iterable_ranges = if let Some(first_clause) = children
268                        .iter()
269                        .copied()
270                        .find(|child| child.kind() == "for_in_clause")
271                    {
272                        children_by_field_name_bounded(first_clause, "right", &mut scope_step)?
273                            .into_iter()
274                            .map(|iterable| (iterable.start_byte(), iterable.end_byte()))
275                            .collect()
276                    } else {
277                        Vec::new()
278                    };
279                    for clause in children
280                        .iter()
281                        .copied()
282                        .filter(|child| child.kind() == "for_in_clause")
283                    {
284                        if let Some(target) = clause.child_by_field_name("left") {
285                            collect_binding_targets(target, source, &mut scope_step, |name, _| {
286                                inventory.comprehensions.push(PythonComprehensionBinding {
287                                    name: name.into(),
288                                    start_byte: range.0,
289                                    end_byte: range.1,
290                                    enclosing_iterable_ranges: enclosing_iterable_ranges.clone(),
291                                });
292                            })?;
293                        }
294                    }
295                    for child in children.into_iter().rev() {
296                        stack.push(ScanFrame {
297                            node: child,
298                            in_comprehension: true,
299                        });
300                    }
301                    continue;
302                }
303                _ => {}
304            }
305
306            push_named_children(&mut stack, node, frame.in_comprehension, &mut scope_step)?;
307        }
308
309        // `global` and `nonlocal` are whole-function directives regardless of
310        // source order. Neither declaration may become a semantic local.
311        inventory.locals.retain(|binding| {
312            !inventory.globals.contains(binding.name.as_ref())
313                && !inventory.nonlocals.contains(binding.name.as_ref())
314        });
315        inventory.local_names.retain(|name, _| {
316            !inventory.globals.contains(name.as_ref())
317                && !inventory.nonlocals.contains(name.as_ref())
318        });
319        Some(inventory)
320    }
321
322    pub fn name_resolution_at(
323        &self,
324        name: &str,
325        reference: Node<'_>,
326    ) -> PythonLexicalNameResolution {
327        let reference_byte = reference.start_byte();
328        if self.comprehensions.iter().any(|binding| {
329            binding.name.as_ref() == name
330                && binding.start_byte <= reference_byte
331                && reference_byte < binding.end_byte
332                && !binding
333                    .enclosing_iterable_ranges
334                    .iter()
335                    .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
336        }) {
337            return PythonLexicalNameResolution::Local;
338        }
339        if self.nonlocals.contains(name) {
340            return PythonLexicalNameResolution::Nonlocal;
341        }
342        if self.globals.contains(name) {
343            return PythonLexicalNameResolution::Global;
344        }
345        if self.parameters.contains(name) || self.local_names.contains_key(name) {
346            PythonLexicalNameResolution::Local
347        } else {
348            PythonLexicalNameResolution::Unbound
349        }
350    }
351
352    pub fn resolves_to_local_function(&self, name: &str, reference: Node<'_>) -> bool {
353        let reference_byte = reference.start_byte();
354        !self.parameters.contains(name)
355            && !self.comprehensions.iter().any(|binding| {
356                binding.name.as_ref() == name
357                    && binding.start_byte <= reference_byte
358                    && reference_byte < binding.end_byte
359                    && !binding
360                        .enclosing_iterable_ranges
361                        .iter()
362                        .any(|(start, end)| *start <= reference_byte && reference_byte < *end)
363            })
364            && self.local_names.get(name) == Some(&PythonLocalBindingKind::FunctionOnly)
365    }
366
367    pub fn local_function_declaration(
368        &self,
369        name: &str,
370        reference: Node<'_>,
371    ) -> Option<Node<'tree>> {
372        self.resolves_to_local_function(name, reference)
373            .then(|| {
374                self.locals
375                    .iter()
376                    .find(|binding| binding.name.as_ref() == name)
377                    .and_then(|binding| binding.declaration.parent())
378                    .filter(|declaration| declaration.kind() == "function_definition")
379            })
380            .flatten()
381    }
382
383    pub fn local_bindings(&self) -> impl Iterator<Item = (&str, Node<'tree>)> + '_ {
384        self.locals
385            .iter()
386            .map(|binding| (binding.name.as_ref(), binding.declaration))
387    }
388
389    fn record_local_node(&mut self, node: Node<'tree>, source: &str) {
390        let name = node_text(node, source);
391        self.record_local(name, node);
392    }
393
394    fn record_local(&mut self, name: &str, declaration: Node<'tree>) {
395        if name.is_empty() {
396            return;
397        }
398        let binding_kind = if is_function_declaration_name(declaration) {
399            PythonLocalBindingKind::FunctionOnly
400        } else {
401            PythonLocalBindingKind::Other
402        };
403        match self.local_names.entry(name.into()) {
404            std::collections::hash_map::Entry::Vacant(entry) => {
405                entry.insert(binding_kind);
406                self.locals.push(PythonLocalBinding {
407                    name: name.into(),
408                    declaration,
409                });
410            }
411            std::collections::hash_map::Entry::Occupied(mut entry) => {
412                entry.insert(PythonLocalBindingKind::Other);
413            }
414        }
415    }
416}
417
418fn is_function_declaration_name(node: Node<'_>) -> bool {
419    node.parent().is_some_and(|parent| {
420        parent.kind() == "function_definition"
421            && parent
422                .child_by_field_name("name")
423                .is_some_and(|name| name.id() == node.id())
424    })
425}
426
427/// Return the bindings introduced directly by `node`.
428///
429/// Descendant traversal remains the caller's responsibility. This lets the
430/// semantic file walk build a module binding inventory without adding a
431/// second whole-file scan, while reusing the same structured target handling
432/// as function symbol-table discovery.
433pub fn python_direct_scope_bindings_bounded<'tree>(
434    node: Node<'tree>,
435    source: &str,
436    mut scope_step: impl FnMut() -> bool,
437) -> Option<Vec<PythonDirectScopeBinding<'tree>>> {
438    let mut bindings = Vec::new();
439
440    match node.kind() {
441        "function_definition" => {
442            if let Some(name) = node.child_by_field_name("name") {
443                bindings.push(PythonDirectScopeBinding {
444                    declaration: name,
445                    kind: PythonDirectScopeBindingKind::Other,
446                });
447            }
448        }
449        "class_definition" => {
450            if let Some(name) = node.child_by_field_name("name") {
451                bindings.push(PythonDirectScopeBinding {
452                    declaration: name,
453                    kind: if is_direct_module_definition_bounded(node, &mut scope_step)? {
454                        PythonDirectScopeBindingKind::ClassDeclaration
455                    } else {
456                        PythonDirectScopeBindingKind::Other
457                    },
458                });
459            }
460        }
461        "import_statement" | "import_from_statement" => {
462            collect_import_bindings(node, &mut scope_step, |declaration| {
463                bindings.push(PythonDirectScopeBinding {
464                    declaration,
465                    kind: PythonDirectScopeBindingKind::Other,
466                });
467            })?;
468        }
469        "assignment" | "augmented_assignment" => {
470            if let Some(target) = node.child_by_field_name("left") {
471                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
472                    bindings.push(PythonDirectScopeBinding {
473                        declaration,
474                        kind: PythonDirectScopeBindingKind::Other,
475                    });
476                })?;
477            }
478        }
479        "type_alias_statement" => {
480            if let Some(target) = node.child_by_field_name("left")
481                && let Some(declaration) = first_identifier_bounded(target, &mut scope_step)?
482            {
483                bindings.push(PythonDirectScopeBinding {
484                    declaration,
485                    kind: PythonDirectScopeBindingKind::Other,
486                });
487            }
488        }
489        "named_expression" => {
490            if let Some(target) = node.child_by_field_name("name") {
491                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
492                    bindings.push(PythonDirectScopeBinding {
493                        declaration,
494                        kind: PythonDirectScopeBindingKind::Other,
495                    });
496                })?;
497            }
498        }
499        "for_statement" => {
500            if let Some(target) = node.child_by_field_name("left") {
501                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
502                    bindings.push(PythonDirectScopeBinding {
503                        declaration,
504                        kind: PythonDirectScopeBindingKind::Other,
505                    });
506                })?;
507            }
508        }
509        "delete_statement" => {
510            for target in named_children_bounded(node, &mut scope_step)? {
511                collect_binding_targets(target, source, &mut scope_step, |_, declaration| {
512                    bindings.push(PythonDirectScopeBinding {
513                        declaration,
514                        kind: PythonDirectScopeBindingKind::Other,
515                    });
516                })?;
517            }
518        }
519        "as_pattern" => {
520            if let Some(alias) = node.child_by_field_name("alias") {
521                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
522                    bindings.push(PythonDirectScopeBinding {
523                        declaration,
524                        kind: PythonDirectScopeBindingKind::Other,
525                    });
526                })?;
527            }
528        }
529        "except_clause" => {
530            if let Some(alias) = node.child_by_field_name("alias") {
531                collect_binding_targets(alias, source, &mut scope_step, |_, declaration| {
532                    bindings.push(PythonDirectScopeBinding {
533                        declaration,
534                        kind: PythonDirectScopeBindingKind::Other,
535                    });
536                })?;
537            }
538        }
539        "case_clause" => {
540            for child in named_children_bounded(node, &mut scope_step)? {
541                if child.kind() == "case_pattern" {
542                    collect_match_pattern_bindings(
543                        child,
544                        source,
545                        &mut scope_step,
546                        |_, declaration| {
547                            bindings.push(PythonDirectScopeBinding {
548                                declaration,
549                                kind: PythonDirectScopeBindingKind::Other,
550                            });
551                        },
552                    )?;
553                }
554            }
555        }
556        _ => {}
557    }
558    Some(bindings)
559}
560
561pub fn python_unambiguous_module_class_binding_bounded(
562    root: Node<'_>,
563    source: &str,
564    target_name: &str,
565    mut scope_step: impl FnMut() -> bool,
566) -> Option<bool> {
567    let mut matched = None;
568    let mut stack = vec![root];
569    while let Some(node) = stack.pop() {
570        if !scope_step() {
571            return None;
572        }
573        for binding in python_direct_scope_bindings_bounded(node, source, &mut scope_step)? {
574            if node_text(binding.declaration, source) != target_name {
575                continue;
576            }
577            if matched.is_some() {
578                return Some(false);
579            }
580            matched = Some(binding.kind);
581            if binding.kind == PythonDirectScopeBindingKind::Other {
582                return Some(false);
583            }
584        }
585
586        let body = matches!(
587            node.kind(),
588            "function_definition" | "class_definition" | "lambda"
589        )
590        .then(|| node.child_by_field_name("body").map(|child| child.id()))
591        .flatten();
592        let name = matches!(node.kind(), "function_definition" | "class_definition")
593            .then(|| node.child_by_field_name("name").map(|child| child.id()))
594            .flatten();
595        for child in named_children_bounded(node, &mut scope_step)?
596            .into_iter()
597            .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
598            .rev()
599        {
600            stack.push(child);
601        }
602    }
603    Some(matched == Some(PythonDirectScopeBindingKind::ClassDeclaration))
604}
605
606fn is_direct_module_definition_bounded(
607    node: Node<'_>,
608    scope_step: &mut impl FnMut() -> bool,
609) -> Option<bool> {
610    if !scope_step() {
611        return None;
612    }
613    let Some(mut parent) = node.parent() else {
614        return Some(false);
615    };
616    if parent.kind() == "decorated_definition" {
617        if !scope_step() {
618            return None;
619        }
620        let Some(grandparent) = parent.parent() else {
621            return Some(false);
622        };
623        parent = grandparent;
624    }
625    Some(parent.kind() == "module")
626}
627
628fn collect_direct_identifier_names(
629    node: Node<'_>,
630    source: &str,
631    scope_step: &mut impl FnMut() -> bool,
632    mut record: impl FnMut(&str),
633) -> Option<()> {
634    for child in named_children_bounded(node, scope_step)? {
635        if child.kind() == "identifier" {
636            let name = node_text(child, source);
637            if !name.is_empty() {
638                record(name);
639            }
640        }
641    }
642    Some(())
643}
644
645fn collect_import_bindings<'tree>(
646    statement: Node<'tree>,
647    scope_step: &mut impl FnMut() -> bool,
648    mut record: impl FnMut(Node<'tree>),
649) -> Option<()> {
650    let mut cursor = statement.walk();
651    let mut imports = Vec::new();
652    for imported in statement.children_by_field_name("name", &mut cursor) {
653        if !scope_step() {
654            return None;
655        }
656        imports.push(imported);
657    }
658    for imported in imports {
659        if let Some(alias) = imported.child_by_field_name("alias") {
660            record(alias);
661            continue;
662        }
663        let name = imported.child_by_field_name("name").unwrap_or(imported);
664        let binding = if statement.kind() == "import_statement" {
665            first_identifier_bounded(name, scope_step)?
666        } else {
667            last_identifier_bounded(name, scope_step)?
668        };
669        if let Some(binding) = binding {
670            record(binding);
671        }
672    }
673    Some(())
674}
675
676fn first_identifier_bounded<'tree>(
677    root: Node<'tree>,
678    scope_step: &mut impl FnMut() -> bool,
679) -> Option<Option<Node<'tree>>> {
680    let mut stack = vec![root];
681    while let Some(node) = stack.pop() {
682        if !scope_step() {
683            return None;
684        }
685        if node.kind() == "identifier" {
686            return Some(Some(node));
687        }
688        let children = named_children_bounded(node, scope_step)?;
689        stack.extend(children.into_iter().rev());
690    }
691    Some(None)
692}
693
694fn last_identifier_bounded<'tree>(
695    root: Node<'tree>,
696    scope_step: &mut impl FnMut() -> bool,
697) -> Option<Option<Node<'tree>>> {
698    let mut result = None;
699    let mut stack = vec![root];
700    while let Some(node) = stack.pop() {
701        if !scope_step() {
702            return None;
703        }
704        if node.kind() == "identifier" {
705            result = Some(node);
706            continue;
707        }
708        let children = named_children_bounded(node, scope_step)?;
709        stack.extend(children.into_iter().rev());
710    }
711    Some(result)
712}
713
714fn collect_binding_targets<'tree>(
715    target: Node<'tree>,
716    source: &str,
717    scope_step: &mut impl FnMut() -> bool,
718    mut record: impl FnMut(&str, Node<'tree>),
719) -> Option<()> {
720    let mut stack = vec![target];
721    while let Some(node) = stack.pop() {
722        if !scope_step() {
723            return None;
724        }
725        match node.kind() {
726            // These mutate an existing object and do not bind either the
727            // receiver or member name in the function.
728            "attribute" | "subscript" => continue,
729            "identifier" | "keyword_identifier" => {
730                let name = node_text(node, source);
731                if !name.is_empty() {
732                    record(name, node);
733                }
734                continue;
735            }
736            _ => {}
737        }
738        let children = named_children_bounded(node, scope_step)?;
739        if node.kind() == "as_pattern_target" && children.is_empty() {
740            let name = node_text(node, source);
741            if !name.is_empty() {
742                record(name, node);
743            }
744            continue;
745        }
746        stack.extend(children.into_iter().rev());
747    }
748    Some(())
749}
750
751fn collect_match_pattern_bindings<'tree>(
752    pattern: Node<'tree>,
753    source: &str,
754    scope_step: &mut impl FnMut() -> bool,
755    mut record: impl FnMut(&str, Node<'tree>),
756) -> Option<()> {
757    let mut stack = vec![pattern];
758    while let Some(node) = stack.pop() {
759        if !scope_step() {
760            return None;
761        }
762        match node.kind() {
763            "dotted_name" => {
764                let identifiers = named_children_bounded(node, scope_step)?
765                    .into_iter()
766                    .filter(|child| child.kind() == "identifier")
767                    .collect::<Vec<_>>();
768                if let [binding] = identifiers.as_slice() {
769                    let name = node_text(*binding, source);
770                    if !name.is_empty() {
771                        record(name, *binding);
772                    }
773                }
774                continue;
775            }
776            "splat_pattern" => {
777                for child in named_children_bounded(node, scope_step)? {
778                    if child.kind() == "identifier" {
779                        let name = node_text(child, source);
780                        if !name.is_empty() {
781                            record(name, child);
782                        }
783                    }
784                }
785                continue;
786            }
787            "class_pattern" => {
788                let mut children = named_children_bounded(node, scope_step)?;
789                if children
790                    .first()
791                    .is_some_and(|child| child.kind() == "dotted_name")
792                {
793                    children.remove(0);
794                }
795                stack.extend(children.into_iter().rev());
796                continue;
797            }
798            "keyword_pattern" => {
799                let mut children = named_children_bounded(node, scope_step)?;
800                if children
801                    .first()
802                    .is_some_and(|child| child.kind() == "identifier")
803                {
804                    children.remove(0);
805                }
806                stack.extend(children.into_iter().rev());
807                continue;
808            }
809            "dict_pattern" => {
810                let key_ids = children_by_field_name_bounded(node, "key", scope_step)?
811                    .into_iter()
812                    .map(|key| key.id())
813                    .collect::<HashSet<_>>();
814                let children = named_children_bounded(node, scope_step)?;
815                stack.extend(
816                    children
817                        .into_iter()
818                        .filter(|child| !key_ids.contains(&child.id()))
819                        .rev(),
820                );
821                continue;
822            }
823            "as_pattern" => {
824                if let Some(alias) = node.child_by_field_name("alias") {
825                    collect_binding_targets(alias, source, scope_step, &mut record)?;
826                    let children = named_children_bounded(node, scope_step)?;
827                    stack.extend(
828                        children
829                            .into_iter()
830                            .filter(|child| child.id() != alias.id())
831                            .rev(),
832                    );
833                    continue;
834                }
835                let mut children = named_children_bounded(node, scope_step)?;
836                if let Some(alias) = children
837                    .last()
838                    .copied()
839                    .filter(|child| child.kind() == "identifier")
840                {
841                    let name = node_text(alias, source);
842                    if !name.is_empty() {
843                        record(name, alias);
844                    }
845                    children.pop();
846                }
847                stack.extend(children.into_iter().rev());
848                continue;
849            }
850            "identifier" => {
851                // Direct identifiers are match `as` aliases handled by their
852                // parent. Keyword names and class heads are likewise skipped.
853                continue;
854            }
855            kind if is_pattern_literal(kind) => continue,
856            _ => {}
857        }
858        let children = named_children_bounded(node, scope_step)?;
859        stack.extend(children.into_iter().rev());
860    }
861    Some(())
862}
863
864fn push_named_children<'tree>(
865    stack: &mut Vec<ScanFrame<'tree>>,
866    node: Node<'tree>,
867    in_comprehension: bool,
868    scope_step: &mut impl FnMut() -> bool,
869) -> Option<()> {
870    for child in named_children_bounded(node, scope_step)?.into_iter().rev() {
871        stack.push(ScanFrame {
872            node: child,
873            in_comprehension,
874        });
875    }
876    Some(())
877}
878
879fn push_named_children_except<'tree>(
880    stack: &mut Vec<ScanFrame<'tree>>,
881    node: Node<'tree>,
882    excluded: Node<'tree>,
883    in_comprehension: bool,
884    scope_step: &mut impl FnMut() -> bool,
885) -> Option<()> {
886    for child in named_children_bounded(node, scope_step)?
887        .into_iter()
888        .filter(|child| child.id() != excluded.id())
889        .rev()
890    {
891        stack.push(ScanFrame {
892            node: child,
893            in_comprehension,
894        });
895    }
896    Some(())
897}
898
899fn push_nested_scope_header_children<'tree>(
900    stack: &mut Vec<ScanFrame<'tree>>,
901    node: Node<'tree>,
902    in_comprehension: bool,
903    scope_step: &mut impl FnMut() -> bool,
904) -> Option<()> {
905    let body = node.child_by_field_name("body").map(|child| child.id());
906    let name = node.child_by_field_name("name").map(|child| child.id());
907    for child in named_children_bounded(node, scope_step)?
908        .into_iter()
909        .filter(|child| Some(child.id()) != body && Some(child.id()) != name)
910        .rev()
911    {
912        stack.push(ScanFrame {
913            node: child,
914            in_comprehension,
915        });
916    }
917    Some(())
918}
919
920fn named_children_bounded<'tree>(
921    node: Node<'tree>,
922    scope_step: &mut impl FnMut() -> bool,
923) -> Option<Vec<Node<'tree>>> {
924    let mut cursor = node.walk();
925    let mut children = Vec::new();
926    for child in node.named_children(&mut cursor) {
927        if !scope_step() {
928            return None;
929        }
930        children.push(child);
931    }
932    Some(children)
933}
934
935fn children_by_field_name_bounded<'tree>(
936    node: Node<'tree>,
937    field: &str,
938    scope_step: &mut impl FnMut() -> bool,
939) -> Option<Vec<Node<'tree>>> {
940    let mut cursor = node.walk();
941    let mut children = Vec::new();
942    for child in node.children_by_field_name(field, &mut cursor) {
943        if !scope_step() {
944            return None;
945        }
946        children.push(child);
947    }
948    Some(children)
949}
950
951fn is_comprehension(kind: &str) -> bool {
952    matches!(
953        kind,
954        "list_comprehension"
955            | "set_comprehension"
956            | "dictionary_comprehension"
957            | "generator_expression"
958    )
959}
960
961fn is_pattern_literal(kind: &str) -> bool {
962    matches!(
963        kind,
964        "string"
965            | "concatenated_string"
966            | "integer"
967            | "float"
968            | "complex_pattern"
969            | "true"
970            | "false"
971            | "none"
972    )
973}
974
975fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
976    brokk_bifrost_core::analyzer::common::node_source_text_trimmed(node, source)
977}