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