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}