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