Skip to main content

bynk_check/
locals.rs

1//! v0.31 (ADR 0064): the local-binding sink.
2//!
3//! Records each local binding — `let`/`let <-`, lambda/fn/handler parameters,
4//! and match-arm pattern bindings — with its **lexical scope range**, so the
5//! LSP can offer/navigate locals (the recurring deferral: the v0.25 index, the
6//! v0.27 hints, the v0.28 tokens, and v0.30.2 completion all stop at top-level
7//! symbols for want of a scope-at-offset query).
8//!
9//! Mirrors [`HintSink`](crate::hints::HintSink): a `&mut` sink threaded through
10//! the checker, recording at the binding sites as types are computed — so it
11//! survives a transient error at the sites the checker still reaches, and (like
12//! hints) it is not part of the `Ok(TypedCommons)` payload. Scope ranges are
13//! taken from the enclosing block/body/arm span the checker already has at each
14//! binding site, not re-derived — so nesting and shadowing are the checker's
15//! own (tested) scoping, resolved in [`locals_at`]. Only synthetic files are
16//! muted (locals serve completion/navigation in test files too).
17
18use bynk_syntax::span::Span;
19use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21
22/// One local binding: its name, the binding-name span (the def site), its
23/// rendered type (Bynk surface syntax, as hints render — no `Ty` on the
24/// surface), and the source range over which it is in scope.
25#[derive(Debug, Clone)]
26pub struct LocalBinding {
27    pub name: String,
28    pub def_span: Span,
29    pub ty: String,
30    pub scope: Span,
31}
32
33/// Project-relative source path → that file's local bindings, in source order.
34pub type FileLocals = HashMap<PathBuf, Vec<LocalBinding>>;
35
36/// Records local bindings per file. A fresh sink records nothing until
37/// [`enter_file`](Self::enter_file) attributes it.
38#[derive(Debug, Default)]
39pub struct LocalsSink {
40    files: FileLocals,
41    file: Option<PathBuf>,
42    /// Set for synthetic (toolchain-injected) files only.
43    muted: bool,
44}
45
46impl LocalsSink {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Enter a per-file recording context.
52    pub fn enter_file(&mut self, file: &Path, muted: bool) {
53        self.file = Some(file.to_path_buf());
54        self.muted = muted;
55    }
56
57    /// Record a binding `name` defined at `def_span`, of rendered type `ty`,
58    /// in scope over `scope`. Dropped when muted or before any `enter_file`.
59    pub fn record(&mut self, name: String, def_span: Span, ty: String, scope: Span) {
60        if self.muted {
61            return;
62        }
63        let Some(file) = &self.file else {
64            return;
65        };
66        self.files
67            .entry(file.clone())
68            .or_default()
69            .push(LocalBinding {
70                name,
71                def_span,
72                ty,
73                scope,
74            });
75    }
76
77    /// Drain the recorded bindings, each file's entries ordered by def span.
78    pub fn take_files(&mut self) -> FileLocals {
79        let mut files = std::mem::take(&mut self.files);
80        for locals in files.values_mut() {
81            locals.sort_by_key(|b| (b.def_span.start, b.def_span.end));
82        }
83        files
84    }
85}
86
87/// The local bindings in scope at `offset`, deduplicated by name with the
88/// **innermost/latest** definition winning (shadowing) — the completion and
89/// navigation query.
90pub fn locals_at(entries: &[LocalBinding], offset: usize) -> Vec<&LocalBinding> {
91    let mut by_name: HashMap<&str, &LocalBinding> = HashMap::new();
92    for b in entries {
93        if b.scope.start <= offset && offset <= b.scope.end {
94            // Later def (larger start) shadows an earlier same-name binding.
95            by_name
96                .entry(&b.name)
97                .and_modify(|cur| {
98                    if b.def_span.start >= cur.def_span.start {
99                        *cur = b;
100                    }
101                })
102                .or_insert(b);
103        }
104    }
105    let mut out: Vec<&LocalBinding> = by_name.into_values().collect();
106    out.sort_by_key(|b| (b.def_span.start, b.def_span.end));
107    out
108}
109
110/// The single binding whose **def-name** span covers `offset` — the
111/// definition-site query (for references/rename on a local's declaration).
112pub fn binding_at_def(entries: &[LocalBinding], offset: usize) -> Option<&LocalBinding> {
113    entries
114        .iter()
115        .find(|b| b.def_span.start <= offset && offset <= b.def_span.end)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn b(name: &str, def: usize, scope: (usize, usize)) -> LocalBinding {
123        LocalBinding {
124            name: name.to_string(),
125            def_span: Span {
126                start: def,
127                end: def + name.len(),
128            },
129            ty: "Int".to_string(),
130            scope: Span {
131                start: scope.0,
132                end: scope.1,
133            },
134        }
135    }
136
137    #[test]
138    fn locals_at_filters_by_scope_and_resolves_shadowing() {
139        let entries = vec![
140            b("x", 0, (3, 50)),   // outer x
141            b("y", 10, (13, 30)), // y, narrower scope
142            b("x", 20, (23, 50)), // inner x shadows the outer
143        ];
144        // Before y's scope: just the outer x.
145        assert_eq!(names(&locals_at(&entries, 5)), vec!["x"]);
146        // Inside y's scope, before the inner x: x (outer) + y.
147        assert_eq!(names(&locals_at(&entries, 15)), vec!["x", "y"]);
148        // After the inner x and past y's scope: one x (the inner shadows).
149        let at = locals_at(&entries, 40);
150        assert_eq!(names(&at), vec!["x"]);
151        assert_eq!(at[0].def_span.start, 20, "latest x wins");
152    }
153
154    #[test]
155    fn binding_at_def_finds_the_declaration_under_the_cursor() {
156        let entries = vec![b("total", 4, (12, 40))];
157        assert!(binding_at_def(&entries, 6).is_some()); // on the name
158        assert!(binding_at_def(&entries, 20).is_none()); // a use site, not the def
159    }
160
161    fn names(bs: &[&LocalBinding]) -> Vec<String> {
162        bs.iter().map(|b| b.name.clone()).collect()
163    }
164}