Skip to main content

bynk_check/
index.rs

1//! v0.25: the project-wide binding index (ADR 0053).
2//!
3//! [`RefSink`] collects use→def edges at the resolution sites themselves —
4//! the resolver's reference walk, the checker's capability/service call
5//! dispatch, and the project driver's clause wiring — mirroring v0.24's
6//! `ErrorSink` collection-point pattern. The project pass then qualifies
7//! bare names per unit and assembles a [`ProjectIndex`]: every in-scope
8//! symbol's definition site plus all of its reference sites, binding-correct
9//! (never name-matched).
10//!
11//! In-scope symbol kinds this increment: top-level types, free `fn`s,
12//! capabilities, services, agents, and providers. Instance methods, record
13//! fields, capability op names, and local bindings are deferred (no edges
14//! are recorded for them).
15
16use std::collections::{HashMap, HashSet};
17use std::path::{Path, PathBuf};
18
19use bynk_syntax::span::Span;
20
21/// The kind half of a symbol's structural key.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub enum SymbolKind {
24    Type,
25    Fn,
26    Capability,
27    Service,
28    Agent,
29    Provider,
30    /// v0.36 (ADR 0069): an instance method, keyed by the compound name
31    /// `"Type.method"` in the type's defining unit. The first parent-scoped
32    /// index kind (see the v0.36 members slice).
33    Method,
34    /// v0.36 (ADR 0069, slice 2): a record field, keyed by `"Type.field"`.
35    Field,
36    /// v0.36 (ADR 0069, slice 2): a capability operation, keyed by `"Cap.op"`.
37    CapabilityOp,
38    /// v0.45: an actor declaration — a boundary contract consumed by a
39    /// handler's `by` clause.
40    Actor,
41}
42
43impl SymbolKind {
44    pub fn display(self) -> &'static str {
45        match self {
46            SymbolKind::Type => "type",
47            SymbolKind::Fn => "fn",
48            SymbolKind::Capability => "capability",
49            SymbolKind::Service => "service",
50            SymbolKind::Agent => "agent",
51            SymbolKind::Provider => "provider",
52            SymbolKind::Method => "method",
53            SymbolKind::Field => "field",
54            SymbolKind::CapabilityOp => "operation",
55            SymbolKind::Actor => "actor",
56        }
57    }
58}
59
60/// Structural symbol identity (no `DefId` plumbing): the defining unit's
61/// qualified name, the declaration kind, and the declared name. Top-level
62/// names are unique within a unit, so the key is unambiguous.
63#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
64pub struct SymbolKey {
65    pub unit: String,
66    pub kind: SymbolKind,
67    pub name: String,
68}
69
70/// One recorded use→def edge, in collection-point context.
71///
72/// `unit: None` means the name resolved through the recording namespace's
73/// merged tables (local declarations + `uses` imports) and is qualified at
74/// assembly; `Some` means the resolution site already knew the defining
75/// unit (cross-context capability/service references, flattened caps).
76#[derive(Debug, Clone)]
77pub struct RefEdge {
78    /// The name-segment span (for dotted `B.Cap`, just `Cap`).
79    pub span: Span,
80    pub kind: SymbolKind,
81    pub name: String,
82    pub unit: Option<String>,
83    /// Project-relative file the span is an offset into (collection point).
84    pub file: PathBuf,
85    /// The unit whose merged namespace resolves a bare (`unit: None`) name.
86    /// For test/integration files this is the *target* unit.
87    pub namespace: Option<String>,
88    /// Display name of the enclosing top-level declaration, when known
89    /// (`"f"`, `"T.m"`, a service/provider name). Used at assembly to
90    /// re-attribute spans to the file that declares the owner — sibling-file
91    /// methods and unit-level handler tables are processed under a different
92    /// file than the one their spans index into.
93    pub owner: Option<String>,
94    /// v0.35 (ADR 0068): set only on the `Cap` of a `provides Cap = Provider`
95    /// clause (never on a `given Cap` dependency). With `owner` the provider,
96    /// this marks a capability→provider implementation edge — distinguishing
97    /// the provided capability from the provider's own `given` deps, which are
98    /// also capability refs owned by the same provider.
99    pub provides: bool,
100}
101
102/// Collection-point sink for use→def edges (the `ErrorSink` analogue).
103/// The pipeline sets the ambient file/namespace before each per-file phase;
104/// resolution sites only supply the span and target. A sink left in its
105/// default state (no file) discards edges — the single-file entry points
106/// resolve without recording.
107#[derive(Debug, Default)]
108pub struct RefSink {
109    pub edges: Vec<RefEdge>,
110    /// Synthetic namespaces (integration-test harness roots) → their `uses`
111    /// resolution order, merged with the project's `uses` table at assembly.
112    pub extra_uses: HashMap<String, Vec<String>>,
113    file: Option<PathBuf>,
114    namespace: Option<String>,
115    owner: Option<String>,
116    /// Set while processing synthetic (toolchain-injected) files: edges are
117    /// discarded — first-party units are not user-editable and out of index.
118    muted: bool,
119}
120
121impl RefSink {
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Declare a synthetic namespace's `uses` resolution order (integration
127    /// harness roots are not project units, so the project's `uses` table
128    /// has no entry for them).
129    pub fn declare_namespace(&mut self, namespace: &str, uses: Vec<String>) {
130        self.extra_uses.insert(namespace.to_string(), uses);
131    }
132
133    /// Enter a per-file recording context. `namespace` is the unit whose
134    /// merged tables resolve bare names in this file (the file's own unit,
135    /// or a test file's target unit).
136    pub fn enter_file(&mut self, file: &Path, namespace: &str, muted: bool) {
137        self.file = Some(file.to_path_buf());
138        self.namespace = Some(namespace.to_string());
139        self.owner = None;
140        self.muted = muted;
141    }
142
143    /// Set the enclosing top-level declaration for subsequent edges.
144    pub fn set_owner(&mut self, owner: impl Into<String>) {
145        self.owner = Some(owner.into());
146    }
147
148    pub fn clear_owner(&mut self) {
149        self.owner = None;
150    }
151
152    /// Record an edge whose defining unit is found at assembly.
153    pub fn record(&mut self, span: Span, kind: SymbolKind, name: &str) {
154        self.push(span, kind, name, None, false);
155    }
156
157    /// Record an edge whose defining unit the resolution site already knows.
158    pub fn record_in_unit(&mut self, span: Span, kind: SymbolKind, name: &str, unit: &str) {
159        self.push(span, kind, name, Some(unit.to_string()), false);
160    }
161
162    /// v0.35 (ADR 0068): record the `Cap` of a `provides Cap = Provider` clause
163    /// — a capability reference also flagged as an implementation edge (the
164    /// owner is the provider). `unit` is `Some` for a cross-context provided
165    /// capability, `None` when it resolves at assembly.
166    pub fn record_provides(&mut self, span: Span, name: &str, unit: Option<&str>) {
167        self.push(
168            span,
169            SymbolKind::Capability,
170            name,
171            unit.map(str::to_string),
172            true,
173        );
174    }
175
176    fn push(
177        &mut self,
178        span: Span,
179        kind: SymbolKind,
180        name: &str,
181        unit: Option<String>,
182        provides: bool,
183    ) {
184        if self.muted {
185            return;
186        }
187        let Some(file) = &self.file else {
188            return; // single-file mode: no recording context.
189        };
190        self.edges.push(RefEdge {
191            span,
192            kind,
193            name: name.to_string(),
194            unit,
195            file: file.clone(),
196            namespace: self.namespace.clone(),
197            owner: self.owner.clone(),
198            provides,
199        });
200    }
201}
202
203/// One occurrence of a symbol: the file (project-relative) and the
204/// name-segment span within that file's analysed snapshot.
205#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
206pub struct SiteRef {
207    pub path: PathBuf,
208    pub span: Span,
209}
210
211/// v0.28 (ADR 0057): the Bynk-specific semantic-token modifiers recorded on
212/// a symbol at assemble time. `refined` only when a refinement is present —
213/// `type Age = Int` parses as `Refined { refinement: None }` and is a plain
214/// alias, carrying neither; `opaque` is orthogonal, so `opaque B where …`
215/// carries both. `platform_native` when the declaring unit is a platform
216/// adapter (`firstparty::platform_of` is `Some`).
217#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
218pub struct SymbolModifiers {
219    pub refined: bool,
220    pub opaque: bool,
221    pub platform_native: bool,
222}
223
224/// A symbol's definition site plus every reference site.
225#[derive(Debug, Clone, Default)]
226pub struct SymbolEntry {
227    /// The declaration's name span. `None` only transiently during assembly;
228    /// symbols without a located definition are dropped from the index.
229    pub def: Option<SiteRef>,
230    /// Sorted, deduplicated. Does not include the definition site.
231    pub refs: Vec<SiteRef>,
232    /// v0.28 (ADR 0057): semantic-token modifiers, set from the declaration.
233    pub modifiers: SymbolModifiers,
234}
235
236/// v0.34 (ADR 0067): one resolved caller→callee call edge — a `Fn` reference
237/// (`callee`) occurring inside a known top-level declaration (`caller`), at
238/// `site` (the callee-name span, in the caller's file). The backing data for
239/// call hierarchy: incoming calls group edges by `callee`, outgoing by
240/// `caller`. v0.36 (ADR 0069): `Fn` and `Method` callees/callers; op-call and
241/// agent-dispatch edges are still absent (those callees aren't index symbols —
242/// the remaining deferred index kinds).
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct CallEdge {
245    pub caller: SymbolKey,
246    pub callee: SymbolKey,
247    pub site: SiteRef,
248}
249
250/// v0.35 (ADR 0068): one capability→provider implementation edge — a `provides
251/// Cap = P` clause records a `Capability` reference (`capability`) whose
252/// enclosing owner is the provider (`provider`), at `site` (the capability-name
253/// span in the `provides` clause). The backing data for implementation
254/// navigation: `implementation` on a capability returns its providers' defs.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct ImplEdge {
257    pub capability: SymbolKey,
258    pub provider: SymbolKey,
259    pub site: SiteRef,
260}
261
262/// v0.28 (ADR 0057): one reference to a first-party (`bynk.*`) symbol.
263/// Tokens-only: first-party defs point at synthetic files not on disk, so
264/// these sites are **never** read by definition/rename/workspace-symbol —
265/// the v0.25 exclusion of synthetic units from `symbols` stands untouched.
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct ForeignRef {
268    pub site: SiteRef,
269    pub kind: SymbolKind,
270    pub modifiers: SymbolModifiers,
271}
272
273/// The project-wide binding index: every in-scope symbol's definition and
274/// references, keyed structurally. Built by the v0.24 project pass in
275/// analyse mode; empty in build mode.
276#[derive(Debug, Clone, Default)]
277pub struct ProjectIndex {
278    pub symbols: HashMap<SymbolKey, SymbolEntry>,
279    /// v0.28 (ADR 0057): references to first-party symbols, sorted by
280    /// (path, span), deduplicated — read only by the semantic-tokens
281    /// producer (see [`ForeignRef`]).
282    pub foreign_refs: Vec<ForeignRef>,
283    /// v0.34 (ADR 0067): caller→callee call edges (`Fn` callees only), sorted
284    /// by (caller, callee, site). The call-hierarchy graph (see [`CallEdge`]).
285    pub calls: Vec<CallEdge>,
286    /// v0.35 (ADR 0068): capability→provider implementation edges, sorted by
287    /// (capability, provider, site). The implementation-nav graph (see
288    /// [`ImplEdge`]).
289    pub impls: Vec<ImplEdge>,
290}
291
292impl ProjectIndex {
293    /// The symbol whose definition or reference name-segment contains
294    /// `offset` within `path`. Spans are half-open; name segments never
295    /// overlap, so the first hit is the only hit.
296    pub fn symbol_at(&self, path: &Path, offset: usize) -> Option<(&SymbolKey, &SiteRef)> {
297        for (key, entry) in &self.symbols {
298            if let Some(def) = &entry.def
299                && def.path == path
300                && def.span.range().contains(&offset)
301            {
302                return Some((key, def));
303            }
304            for site in &entry.refs {
305                if site.path == path && site.span.range().contains(&offset) {
306                    return Some((key, site));
307                }
308            }
309        }
310        None
311    }
312
313    /// Definition + references for `key`, definition first.
314    pub fn sites(&self, key: &SymbolKey) -> Vec<&SiteRef> {
315        let Some(entry) = self.symbols.get(key) else {
316            return Vec::new();
317        };
318        entry.def.iter().chain(entry.refs.iter()).collect()
319    }
320
321    /// v0.34 (ADR 0067): call edges whose callee is `key` — its callers.
322    pub fn calls_into<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a CallEdge> {
323        let key = key.clone();
324        self.calls.iter().filter(move |e| e.callee == key)
325    }
326
327    /// v0.34 (ADR 0067): call edges whose caller is `key` — what it calls.
328    pub fn calls_from<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a CallEdge> {
329        let key = key.clone();
330        self.calls.iter().filter(move |e| e.caller == key)
331    }
332
333    /// v0.35 (ADR 0068): impl edges whose capability is `key` — its providers.
334    pub fn impls_of<'a>(&'a self, key: &SymbolKey) -> impl Iterator<Item = &'a ImplEdge> {
335        let key = key.clone();
336        self.impls.iter().filter(move |e| e.capability == key)
337    }
338
339    /// Structural equality after mapping `self`'s sites through `remap`
340    /// and renaming `from` to `to_name` — the rename capture/escape
341    /// validator. `remap` converts a pre-edit site to its post-edit
342    /// position (rename edits shift spans within edited files).
343    pub fn equals_modulo_rename(
344        &self,
345        post: &ProjectIndex,
346        from: &SymbolKey,
347        to_name: &str,
348        mut remap: impl FnMut(&SiteRef) -> SiteRef,
349    ) -> bool {
350        if self.symbols.len() != post.symbols.len() {
351            return false;
352        }
353        for (key, entry) in &self.symbols {
354            let expect_key = if key == from {
355                SymbolKey {
356                    unit: key.unit.clone(),
357                    kind: key.kind,
358                    name: to_name.to_string(),
359                }
360            } else {
361                key.clone()
362            };
363            let Some(post_entry) = post.symbols.get(&expect_key) else {
364                return false;
365            };
366            let expect_def = entry.def.as_ref().map(&mut remap);
367            if expect_def != post_entry.def {
368                return false;
369            }
370            let mut expect_refs: Vec<SiteRef> = entry.refs.iter().map(&mut remap).collect();
371            expect_refs.sort();
372            let mut post_refs = post_entry.refs.clone();
373            post_refs.sort();
374            if expect_refs != post_refs {
375                return false;
376            }
377        }
378        true
379    }
380}
381
382/// Assembles the index from per-file declaration walks plus the sink's
383/// edges. Built by the project pass, which alone knows unit membership,
384/// `uses` targets, and which file declares each top-level item.
385#[derive(Debug, Default)]
386pub struct IndexBuilder {
387    /// (unit, kind, name) → definition site + modifiers.
388    defs: HashMap<SymbolKey, (SiteRef, SymbolModifiers)>,
389    /// v0.28 (ADR 0057): first-party (`bynk.*`) symbols — kind + modifiers
390    /// only, no usable def site (synthetic files are not on disk). Edges
391    /// qualifying here route into [`ProjectIndex::foreign_refs`].
392    first_party_defs: HashMap<SymbolKey, SymbolModifiers>,
393    /// (unit, owner display name) → declaring file, for span re-attribution.
394    /// Includes methods (`"T.m"`), which are not index symbols.
395    owner_files: HashMap<(String, String), PathBuf>,
396    /// v0.34 (ADR 0067): (unit, owner display name) → the owner's symbol key,
397    /// for resolving a call edge's caller. Only index symbols (every
398    /// `add_def`); method owners (`add_owner`) are absent, so their call edges
399    /// are not recorded — same boundary as the deferred index kinds.
400    owner_keys: HashMap<(String, String), SymbolKey>,
401    /// unit → `uses` targets, resolution order.
402    uses: HashMap<String, Vec<String>>,
403    /// unit → `consumes` targets — bare names can also resolve to a consumed
404    /// unit's exported types (the consumer's merged table layers them after
405    /// `uses` imports).
406    consumes: HashMap<String, Vec<String>>,
407}
408
409impl IndexBuilder {
410    pub fn add_def(
411        &mut self,
412        unit: &str,
413        kind: SymbolKind,
414        name: &str,
415        site: SiteRef,
416        modifiers: SymbolModifiers,
417    ) {
418        self.owner_files
419            .insert((unit.to_string(), name.to_string()), site.path.clone());
420        let key = SymbolKey {
421            unit: unit.to_string(),
422            kind,
423            name: name.to_string(),
424        };
425        self.owner_keys
426            .insert((unit.to_string(), name.to_string()), key.clone());
427        self.defs.insert(key, (site, modifiers));
428    }
429
430    /// v0.28 (ADR 0057): register a first-party symbol for the second
431    /// qualification pass — kind + modifiers only, no def site.
432    pub fn add_first_party_def(
433        &mut self,
434        unit: &str,
435        kind: SymbolKind,
436        name: &str,
437        modifiers: SymbolModifiers,
438    ) {
439        self.first_party_defs.insert(
440            SymbolKey {
441                unit: unit.to_string(),
442                kind,
443                name: name.to_string(),
444            },
445            modifiers,
446        );
447    }
448
449    /// Register a non-symbol owner (a method) for attribution only.
450    pub fn add_owner(&mut self, unit: &str, owner: &str, path: &Path) {
451        self.owner_files
452            .insert((unit.to_string(), owner.to_string()), path.to_path_buf());
453    }
454
455    pub fn set_uses(&mut self, uses: HashMap<String, Vec<String>>) {
456        self.uses = uses;
457    }
458
459    pub fn set_consumes(&mut self, consumes: HashMap<String, Vec<String>>) {
460        self.consumes = consumes;
461    }
462
463    /// Qualify, attribute, dedupe, and assemble.
464    pub fn build(self, edges: Vec<RefEdge>) -> ProjectIndex {
465        let mut index = ProjectIndex::default();
466        for (key, (def, modifiers)) in &self.defs {
467            index.symbols.insert(
468                key.clone(),
469                SymbolEntry {
470                    def: Some(def.clone()),
471                    refs: Vec::new(),
472                    modifiers: *modifiers,
473                },
474            );
475        }
476        let mut seen: HashSet<(PathBuf, Span, SymbolKey)> = HashSet::new();
477        let mut foreign_seen: HashSet<(PathBuf, Span, SymbolKind)> = HashSet::new();
478        let mut calls: Vec<CallEdge> = Vec::new();
479        let mut impls: Vec<ImplEdge> = Vec::new();
480        for edge in edges {
481            // Re-attribute to the owner's declaring file when the owner
482            // lives in a different file than the collection point: sibling-
483            // file methods and unit-level handler tables are processed under
484            // a file other than the one their spans index into. The owner is
485            // declared in the *namespace* unit (the unit being processed).
486            let path = edge
487                .owner
488                .as_ref()
489                .zip(edge.namespace.as_ref())
490                .and_then(|(o, ns)| self.owner_files.get(&(ns.clone(), o.clone())))
491                .cloned()
492                .unwrap_or_else(|| edge.file.clone());
493            let Some(key) = self.qualify(&edge) else {
494                // v0.28 (ADR 0057): second pass — a positive match against
495                // the first-party defs routes into the tokens-only side
496                // table; genuinely unresolved targets stay dropped.
497                if let Some(key) =
498                    self.qualify_with(&edge, |k| self.first_party_defs.contains_key(k))
499                    && foreign_seen.insert((path.clone(), edge.span, key.kind))
500                {
501                    index.foreign_refs.push(ForeignRef {
502                        site: SiteRef {
503                            path,
504                            span: edge.span,
505                        },
506                        kind: key.kind,
507                        modifiers: self.first_party_defs[&key],
508                    });
509                }
510                continue;
511            };
512            let entry = index.symbols.entry(key.clone()).or_default();
513            let Some(def) = &entry.def else {
514                continue;
515            };
516            let site = SiteRef {
517                path,
518                span: edge.span,
519            };
520            // The definition's own name span is not also a reference.
521            if site == *def {
522                continue;
523            }
524            if seen.insert((site.path.clone(), site.span, key.clone())) {
525                // v0.34 (ADR 0067): a `Fn` call inside a known top-level owner
526                // is a call edge. The caller resolves via `owner_keys` exactly
527                // as the file re-attribution above resolves `owner_files`.
528                // v0.36 (ADR 0069): methods are call targets too, now that they
529                // are `add_def`'d index symbols (and callers, since `add_def`
530                // populates `owner_keys` for `"T.m"` owners).
531                if matches!(key.kind, SymbolKind::Fn | SymbolKind::Method)
532                    && let Some(caller) = edge
533                        .owner
534                        .as_ref()
535                        .zip(edge.namespace.as_ref())
536                        .and_then(|(o, ns)| self.owner_keys.get(&(ns.clone(), o.clone())))
537                {
538                    calls.push(CallEdge {
539                        caller: caller.clone(),
540                        callee: key.clone(),
541                        site: site.clone(),
542                    });
543                }
544                // v0.35 (ADR 0068): a `provides Cap = Provider` clause — a
545                // provides-flagged `Capability` ref whose owner is the provider.
546                // The flag distinguishes it from the provider's `given` deps,
547                // which are also capability refs owned by the same provider.
548                if edge.provides
549                    && let Some(provider) = edge
550                        .owner
551                        .as_ref()
552                        .zip(edge.namespace.as_ref())
553                        .and_then(|(o, ns)| self.owner_keys.get(&(ns.clone(), o.clone())))
554                    && provider.kind == SymbolKind::Provider
555                {
556                    impls.push(ImplEdge {
557                        capability: key.clone(),
558                        provider: provider.clone(),
559                        site: site.clone(),
560                    });
561                }
562                entry.refs.push(site);
563            }
564        }
565        for entry in index.symbols.values_mut() {
566            entry.refs.sort();
567        }
568        index.symbols.retain(|_, e| e.def.is_some());
569        index.foreign_refs.sort_by(|a, b| a.site.cmp(&b.site));
570        calls.sort_by(|a, b| (&a.caller, &a.callee, &a.site).cmp(&(&b.caller, &b.callee, &b.site)));
571        index.calls = calls;
572        impls.sort_by(|a, b| {
573            (&a.capability, &a.provider, &a.site).cmp(&(&b.capability, &b.provider, &b.site))
574        });
575        index.impls = impls;
576        index
577    }
578
579    fn qualify(&self, edge: &RefEdge) -> Option<SymbolKey> {
580        self.qualify_with(edge, |k| self.defs.contains_key(k))
581    }
582
583    /// The merged-table qualification against an arbitrary def set: a
584    /// site-known unit is looked up directly; a bare name layers local
585    /// first, then `uses` imports, then consumed units' exported types —
586    /// first hit wins, matching the pipeline's `or_insert` merge priority.
587    fn qualify_with(&self, edge: &RefEdge, has: impl Fn(&SymbolKey) -> bool) -> Option<SymbolKey> {
588        if let Some(unit) = &edge.unit {
589            let key = SymbolKey {
590                unit: unit.clone(),
591                kind: edge.kind,
592                name: edge.name.clone(),
593            };
594            return has(&key).then_some(key);
595        }
596        let ns = edge.namespace.as_ref()?;
597        let local = SymbolKey {
598            unit: ns.clone(),
599            kind: edge.kind,
600            name: edge.name.clone(),
601        };
602        if has(&local) {
603            return Some(local);
604        }
605        for target in self
606            .uses
607            .get(ns)
608            .into_iter()
609            .flatten()
610            .chain(self.consumes.get(ns).into_iter().flatten())
611        {
612            let imported = SymbolKey {
613                unit: target.clone(),
614                kind: edge.kind,
615                name: edge.name.clone(),
616            };
617            if has(&imported) {
618                return Some(imported);
619            }
620        }
621        None
622    }
623}