Skip to main content

bynk_check/
expr_types.rs

1//! v0.30.2 (ADR 0063): the expression-type sink.
2//!
3//! The checker computes `expr_types: HashMap<Span, Ty>` per file as it types
4//! each expression, but that map rides inside the `Ok(TypedCommons)` payload
5//! `check_record` drops on error, and the LSP `Analyse` path discards it
6//! entirely. This sink carries it out to the analysis so completion can ask
7//! *"what is the type of the expression at this offset?"* (the receiver before
8//! a `.`), mirroring [`HintSink`](crate::hints::HintSink).
9//!
10//! Capture is on the **Ok path** — a file's types are recorded only when it
11//! checks clean (`check_record` returns `Ok`), so a mid-edit file with errors
12//! yields nothing for that file (the slice-3 "clean-file ceiling", ADR 0063).
13//! Unlike hints, **test/integration files are not muted** (completion runs in
14//! them); only synthetic toolchain-injected files are.
15
16use crate::checker::Ty;
17use bynk_syntax::span::Span;
18use std::collections::HashMap;
19use std::path::{Path, PathBuf};
20
21/// Project-relative source path → that file's `(expr span, type)` entries,
22/// ordered by span (innermost-last within a start, so a containment search can
23/// prefer the tightest match).
24pub type FileExprTypes = HashMap<PathBuf, Vec<(Span, Ty)>>;
25
26/// Records per-file expression types. A fresh sink records nothing until
27/// [`enter_file`](Self::enter_file) attributes it.
28#[derive(Debug, Default)]
29pub struct ExprTypeSink {
30    files: FileExprTypes,
31    file: Option<PathBuf>,
32    /// Set for synthetic (toolchain-injected) files — their types never serve
33    /// a user-visible completion.
34    muted: bool,
35}
36
37impl ExprTypeSink {
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Enter a per-file recording context.
43    pub fn enter_file(&mut self, file: &Path, muted: bool) {
44        self.file = Some(file.to_path_buf());
45        self.muted = muted;
46    }
47
48    /// Record a whole file's `expr_types` map (the Ok-path capture). Dropped
49    /// when muted or before any `enter_file`.
50    pub fn record_file(&mut self, expr_types: &HashMap<Span, Ty>) {
51        if self.muted {
52            return;
53        }
54        let Some(file) = &self.file else {
55            return;
56        };
57        let entry = self.files.entry(file.clone()).or_default();
58        entry.extend(expr_types.iter().map(|(span, ty)| (*span, ty.clone())));
59    }
60
61    /// Drain the recorded types, each file's entries ordered by span (start
62    /// ascending, then **widest first** so a forward scan ends on the tightest
63    /// containing span).
64    pub fn take_files(&mut self) -> FileExprTypes {
65        let mut files = std::mem::take(&mut self.files);
66        for entries in files.values_mut() {
67            entries.sort_by_key(|(span, _)| (span.start, std::cmp::Reverse(span.end)));
68        }
69        files
70    }
71}
72
73/// The type of the **innermost** expression whose span contains `offset`, if
74/// any — the receiver-typing query for `.`-member completion.
75pub fn type_at_offset(entries: &[(Span, Ty)], offset: usize) -> Option<&Ty> {
76    entries
77        .iter()
78        .filter(|(span, _)| span.start <= offset && offset <= span.end)
79        .min_by_key(|(span, _)| span.end - span.start)
80        .map(|(_, ty)| ty)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use bynk_syntax::ast::BaseType;
87
88    fn span(start: usize, end: usize) -> Span {
89        Span { start, end }
90    }
91
92    #[test]
93    fn type_at_offset_prefers_the_innermost_span() {
94        let int = Ty::Base(BaseType::Int);
95        let string = Ty::Base(BaseType::String);
96        // An outer `String` expression 0..10 with an inner `Int` 2..4.
97        let entries = vec![(span(0, 10), string.clone()), (span(2, 4), int.clone())];
98        assert_eq!(type_at_offset(&entries, 3), Some(&int)); // inside the inner span
99        assert_eq!(type_at_offset(&entries, 7), Some(&string)); // outer span only
100        assert_eq!(type_at_offset(&entries, 20), None); // outside everything
101    }
102}