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}