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