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