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/// What kind of local binding this is — the distinction hover renders as its
23/// `let`/`param` prefix (v0.122, editor-currency slice 1). Every
24/// [`LocalsSink::record`] site knows its kind statically; this is a threaded
25/// argument, not inferred.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum LocalKind {
28    /// A `let =` / `let <-` binding.
29    Let,
30    /// A parameter — fn / handler / op / lambda, or an actor `by`-binder.
31    Param,
32}
33
34/// One local binding: its name, the binding-name span (the def site), its
35/// [`kind`](LocalKind), its rendered type (Bynk surface syntax, as hints
36/// render — no `Ty` on the surface), and the source range over which it is in
37/// scope.
38#[derive(Debug, Clone)]
39pub struct LocalBinding {
40    pub name: String,
41    pub def_span: Span,
42    pub kind: LocalKind,
43    pub ty: String,
44    pub scope: Span,
45}
46
47/// Project-relative source path → that file's local bindings, in source order.
48pub type FileLocals = HashMap<PathBuf, Vec<LocalBinding>>;
49
50/// Records local bindings per file. A fresh sink records nothing until
51/// [`enter_file`](Self::enter_file) attributes it.
52#[derive(Debug, Default)]
53pub struct LocalsSink {
54    files: FileLocals,
55    file: Option<PathBuf>,
56    /// Set for synthetic (toolchain-injected) files only.
57    muted: bool,
58}
59
60impl LocalsSink {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Enter a per-file recording context.
66    pub fn enter_file(&mut self, file: &Path, muted: bool) {
67        self.file = Some(file.to_path_buf());
68        self.muted = muted;
69    }
70
71    /// Record a binding `name` defined at `def_span`, of the given `kind` and
72    /// rendered type `ty`, in scope over `scope`. Dropped when muted or before
73    /// any `enter_file`.
74    pub fn record(
75        &mut self,
76        name: String,
77        def_span: Span,
78        kind: LocalKind,
79        ty: String,
80        scope: Span,
81    ) {
82        if self.muted {
83            return;
84        }
85        let Some(file) = &self.file else {
86            return;
87        };
88        self.files
89            .entry(file.clone())
90            .or_default()
91            .push(LocalBinding {
92                name,
93                def_span,
94                kind,
95                ty,
96                scope,
97            });
98    }
99
100    /// Drain the recorded bindings, each file's entries ordered by def span.
101    pub fn take_files(&mut self) -> FileLocals {
102        let mut files = std::mem::take(&mut self.files);
103        for locals in files.values_mut() {
104            locals.sort_by_key(|b| (b.def_span.start, b.def_span.end));
105        }
106        files
107    }
108}
109
110/// The local bindings in scope at `offset`, deduplicated by name with the
111/// **innermost/latest** definition winning (shadowing) — the completion and
112/// navigation query.
113pub fn locals_at(entries: &[LocalBinding], offset: usize) -> Vec<&LocalBinding> {
114    let mut by_name: HashMap<&str, &LocalBinding> = HashMap::new();
115    for b in entries {
116        if b.scope.start <= offset && offset <= b.scope.end {
117            // Later def (larger start) shadows an earlier same-name binding.
118            by_name
119                .entry(&b.name)
120                .and_modify(|cur| {
121                    if b.def_span.start >= cur.def_span.start {
122                        *cur = b;
123                    }
124                })
125                .or_insert(b);
126        }
127    }
128    let mut out: Vec<&LocalBinding> = by_name.into_values().collect();
129    out.sort_by_key(|b| (b.def_span.start, b.def_span.end));
130    out
131}
132
133/// The single binding whose **def-name** span covers `offset` — the
134/// definition-site query (for references/rename on a local's declaration).
135pub fn binding_at_def(entries: &[LocalBinding], offset: usize) -> Option<&LocalBinding> {
136    entries
137        .iter()
138        .find(|b| b.def_span.start <= offset && offset <= b.def_span.end)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    fn b(name: &str, def: usize, scope: (usize, usize)) -> LocalBinding {
146        LocalBinding {
147            name: name.to_string(),
148            def_span: Span::new(def, def + name.len()),
149            kind: LocalKind::Let,
150            ty: "Int".to_string(),
151            scope: Span::new(scope.0, scope.1),
152        }
153    }
154
155    #[test]
156    fn locals_at_filters_by_scope_and_resolves_shadowing() {
157        let entries = vec![
158            b("x", 0, (3, 50)),   // outer x
159            b("y", 10, (13, 30)), // y, narrower scope
160            b("x", 20, (23, 50)), // inner x shadows the outer
161        ];
162        // Before y's scope: just the outer x.
163        assert_eq!(names(&locals_at(&entries, 5)), vec!["x"]);
164        // Inside y's scope, before the inner x: x (outer) + y.
165        assert_eq!(names(&locals_at(&entries, 15)), vec!["x", "y"]);
166        // After the inner x and past y's scope: one x (the inner shadows).
167        let at = locals_at(&entries, 40);
168        assert_eq!(names(&at), vec!["x"]);
169        assert_eq!(at[0].def_span.start, 20, "latest x wins");
170    }
171
172    #[test]
173    fn binding_at_def_finds_the_declaration_under_the_cursor() {
174        let entries = vec![b("total", 4, (12, 40))];
175        assert!(binding_at_def(&entries, 6).is_some()); // on the name
176        assert!(binding_at_def(&entries, 20).is_none()); // a use site, not the def
177    }
178
179    fn names(bs: &[&LocalBinding]) -> Vec<String> {
180        bs.iter().map(|b| b.name.clone()).collect()
181    }
182}