Skip to main content

bynk_emit/project/
symbols.rs

1use super::*;
2
3/// v0.25 (ADR 0053): walk every parsed file's top-level declarations into
4/// the def table (synthetic first-party units and test files excluded —
5/// neither declares user-editable symbols), then qualify and attach the
6/// recorded edges. Methods register as owners only (attribution), not as
7/// symbols — they are deferred along with fields and op names.
8pub(crate) fn assemble_index(
9    parsed: &[ParsedFile],
10    unit_uses: &HashMap<String, Vec<String>>,
11    unit_consumes: &HashMap<String, Vec<String>>,
12    refs: RefSink,
13) -> ProjectIndex {
14    let mut builder = IndexBuilder::default();
15    let mut uses = unit_uses.clone();
16    uses.extend(refs.extra_uses);
17    builder.set_uses(uses);
18    builder.set_consumes(unit_consumes.clone());
19    for pf in parsed {
20        if matches!(pf.kind, UnitKind::Test | UnitKind::Integration) {
21            continue;
22        }
23        let unit = pf.unit.name().joined();
24        // v0.28 (ADR 0057): synthetic first-party units stay out of
25        // `symbols` (their defs point at files not on disk — the v0.25
26        // rule), but their declarations register for the second
27        // qualification pass so references to them colour as tokens.
28        if pf.synthetic {
29            for item in pf.items() {
30                let (kind, name, modifiers) = match item {
31                    CommonsItem::Type(t) => (
32                        SymbolKind::Type,
33                        &t.name.name,
34                        symbol_modifiers(&unit, Some(t)),
35                    ),
36                    CommonsItem::Fn(f) => match &f.name {
37                        FnName::Free(id) => {
38                            (SymbolKind::Fn, &id.name, symbol_modifiers(&unit, None))
39                        }
40                        FnName::Method { .. } => continue,
41                    },
42                    CommonsItem::Capability(c) => (
43                        SymbolKind::Capability,
44                        &c.name.name,
45                        symbol_modifiers(&unit, None),
46                    ),
47                    CommonsItem::Service(s) => (
48                        SymbolKind::Service,
49                        &s.name.name,
50                        symbol_modifiers(&unit, None),
51                    ),
52                    CommonsItem::Agent(a) => (
53                        SymbolKind::Agent,
54                        &a.name.name,
55                        symbol_modifiers(&unit, None),
56                    ),
57                    CommonsItem::Provider(p) => (
58                        SymbolKind::Provider,
59                        &p.provider_name.name,
60                        symbol_modifiers(&unit, None),
61                    ),
62                    CommonsItem::Actor(a) => (
63                        SymbolKind::Actor,
64                        &a.name.name,
65                        symbol_modifiers(&unit, None),
66                    ),
67                };
68                builder.add_first_party_def(&unit, kind, name, modifiers);
69            }
70            continue;
71        }
72        let site = |id: &Ident| SiteRef {
73            path: pf.source_path.clone(),
74            span: id.span,
75        };
76        for item in pf.items() {
77            match item {
78                CommonsItem::Type(t) => {
79                    builder.add_def(
80                        &unit,
81                        SymbolKind::Type,
82                        &t.name.name,
83                        site(&t.name),
84                        symbol_modifiers(&unit, Some(t)),
85                    );
86                    // v0.36 (ADR 0069, slice 2): record fields are first-class
87                    // symbols keyed by the compound `"Type.field"` name.
88                    if let TypeBody::Record(r) = &t.body {
89                        for field in &r.fields {
90                            builder.add_def(
91                                &unit,
92                                SymbolKind::Field,
93                                &format!("{}.{}", t.name.name, field.name.name),
94                                site(&field.name),
95                                symbol_modifiers(&unit, None),
96                            );
97                        }
98                    }
99                }
100                CommonsItem::Fn(f) => match &f.name {
101                    FnName::Free(id) => {
102                        builder.add_def(
103                            &unit,
104                            SymbolKind::Fn,
105                            &id.name,
106                            site(id),
107                            symbol_modifiers(&unit, None),
108                        );
109                    }
110                    FnName::Method { .. } => {
111                        // v0.36 (ADR 0069): a method is a first-class symbol
112                        // keyed by the compound `"Type.method"` name, and (as
113                        // before) an attribution owner for call-hierarchy.
114                        builder.add_owner(&unit, &f.name.display(), &pf.source_path);
115                        builder.add_def(
116                            &unit,
117                            SymbolKind::Method,
118                            &f.name.display(),
119                            site(f.name.ident()),
120                            symbol_modifiers(&unit, None),
121                        );
122                    }
123                },
124                CommonsItem::Capability(c) => {
125                    builder.add_def(
126                        &unit,
127                        SymbolKind::Capability,
128                        &c.name.name,
129                        site(&c.name),
130                        symbol_modifiers(&unit, None),
131                    );
132                    // v0.36 (ADR 0069, slice 2): capability operations are
133                    // first-class symbols keyed by the compound `"Cap.op"` name.
134                    for op in &c.ops {
135                        builder.add_def(
136                            &unit,
137                            SymbolKind::CapabilityOp,
138                            &format!("{}.{}", c.name.name, op.name.name),
139                            site(&op.name),
140                            symbol_modifiers(&unit, None),
141                        );
142                    }
143                }
144                CommonsItem::Service(s) => {
145                    builder.add_def(
146                        &unit,
147                        SymbolKind::Service,
148                        &s.name.name,
149                        site(&s.name),
150                        symbol_modifiers(&unit, None),
151                    );
152                }
153                CommonsItem::Agent(a) => {
154                    builder.add_def(
155                        &unit,
156                        SymbolKind::Agent,
157                        &a.name.name,
158                        site(&a.name),
159                        symbol_modifiers(&unit, None),
160                    );
161                }
162                CommonsItem::Provider(p) => {
163                    builder.add_def(
164                        &unit,
165                        SymbolKind::Provider,
166                        &p.provider_name.name,
167                        site(&p.provider_name),
168                        symbol_modifiers(&unit, None),
169                    );
170                }
171                CommonsItem::Actor(a) => {
172                    builder.add_def(
173                        &unit,
174                        SymbolKind::Actor,
175                        &a.name.name,
176                        site(&a.name),
177                        symbol_modifiers(&unit, None),
178                    );
179                }
180            }
181        }
182    }
183    builder.build(refs.edges)
184}
185
186/// v0.28 (ADR 0057): a symbol's semantic-token modifiers from its
187/// declaration. `refined` only when a refinement is present — `type X = Int`
188/// is `Refined { refinement: None }`, a plain alias, and carries neither;
189/// `opaque` is orthogonal (an `opaque … where` type carries both).
190/// `platform_native` when the declaring unit is a platform adapter.
191fn symbol_modifiers(
192    unit: &str,
193    type_decl: Option<&TypeDecl>,
194) -> bynk_check::index::SymbolModifiers {
195    let (refined, opaque) = match type_decl.map(|t| &t.body) {
196        Some(TypeBody::Refined { refinement, .. }) => (refinement.is_some(), false),
197        Some(TypeBody::Opaque { refinement, .. }) => (refinement.is_some(), true),
198        _ => (false, false),
199    };
200    bynk_check::index::SymbolModifiers {
201        refined,
202        opaque,
203        platform_native: bynk_check::firstparty::platform_of(unit).is_some(),
204    }
205}
206
207/// Combined symbol tables for a single logical commons or context.
208#[derive(Clone, Default)]
209pub struct UnitTable {
210    #[allow(dead_code)]
211    pub kind: Option<UnitKind>,
212    pub types: HashMap<String, TypeDecl>,
213    pub fns: HashMap<String, FnDecl>,
214    pub methods: HashMap<String, ResolverMethodTable>,
215    /// Per-context capabilities (v0.5). Empty for commons.
216    pub capabilities: HashMap<String, CapabilityDecl>,
217    /// Per-context providers (v0.5). One provider per capability in v0.5.
218    /// Key: capability name. Value: provider declaration.
219    pub providers: HashMap<String, ProviderDecl>,
220    /// Per-context services (v0.5). Empty for commons.
221    pub services: HashMap<String, ServiceDecl>,
222    /// Per-context agents (v0.5). Empty for commons.
223    pub agents: HashMap<String, AgentDecl>,
224    /// v0.45: actors — boundary contracts consumed by handler `by` clauses.
225    pub actors: HashMap<String, ActorDecl>,
226    /// v0.15: capability names this context offers to consumers via
227    /// `exports capability { … }`. Empty for commons.
228    pub exported_capabilities: std::collections::HashSet<String>,
229}
230
231pub(crate) fn build_unit_table(
232    _name: &str,
233    kind: UnitKind,
234    indices: &[usize],
235    parsed: &[ParsedFile],
236    errors: &mut Vec<CompileError>,
237) -> UnitTable {
238    let mut table = UnitTable {
239        kind: Some(kind),
240        ..UnitTable::default()
241    };
242    for &i in indices {
243        for item in parsed[i].items() {
244            if let CommonsItem::Type(t) = item {
245                if let Some(prev) = table.types.get(&t.name.name) {
246                    errors.push(
247                        CompileError::new(
248                            "bynk.resolve.duplicate_type",
249                            t.name.span,
250                            format!("type `{}` is already declared", t.name.name),
251                        )
252                        .with_label(prev.name.span, "previously declared here"),
253                    );
254                } else {
255                    table.types.insert(t.name.name.clone(), t.clone());
256                    table.methods.entry(t.name.name.clone()).or_default();
257                }
258            }
259        }
260    }
261    // v0.15: collect the names a context exports as capabilities.
262    // v0.17: adapters export capabilities too.
263    for &i in indices {
264        {
265            for clause in parsed[i].exports() {
266                if matches!(clause.kind, ExportKind::Capability) {
267                    for n in &clause.names {
268                        table.exported_capabilities.insert(n.name.clone());
269                    }
270                }
271            }
272        }
273    }
274    // v0.5: collect capabilities, providers, services, agents.
275    for &i in indices {
276        for item in parsed[i].items() {
277            match item {
278                CommonsItem::Capability(c) => {
279                    if kind != UnitKind::Context && kind != UnitKind::Adapter {
280                        errors.push(CompileError::new(
281                            "bynk.capability.outside_context",
282                            c.span,
283                            "`capability` declarations are only allowed inside a context or adapter",
284                        ));
285                        continue;
286                    }
287                    if let Some(prev) = table.capabilities.get(&c.name.name) {
288                        errors.push(
289                            CompileError::new(
290                                "bynk.resolve.duplicate_capability",
291                                c.name.span,
292                                format!("capability `{}` is already declared", c.name.name),
293                            )
294                            .with_label(prev.name.span, "previously declared here"),
295                        );
296                    } else {
297                        table.capabilities.insert(c.name.name.clone(), c.clone());
298                    }
299                }
300                CommonsItem::Provider(p) => {
301                    match kind {
302                        UnitKind::Context => {
303                            // v0.17: a bodiless (external) provider is only legal
304                            // inside an adapter.
305                            if p.external {
306                                errors.push(CompileError::new(
307                                    "bynk.context.external_provider",
308                                    p.span,
309                                    "an external (bodiless) provider is only allowed inside an `adapter` — a context provider must have a Bynk body",
310                                ));
311                                continue;
312                            }
313                        }
314                        UnitKind::Adapter => {
315                            // v0.17: an adapter provider must be external — its
316                            // implementation comes from the binding.
317                            if !p.external {
318                                errors.push(CompileError::new(
319                                    "bynk.adapter.provider_has_body",
320                                    p.span,
321                                    "a provider inside an `adapter` must be external (no body) — its implementation is supplied by the binding",
322                                ));
323                                continue;
324                            }
325                        }
326                        _ => {
327                            errors.push(CompileError::new(
328                                "bynk.provider.outside_context",
329                                p.span,
330                                "`provides` declarations are only allowed inside a context or adapter",
331                            ));
332                            continue;
333                        }
334                    }
335                    if let Some(prev) = table.providers.get(&p.capability.name) {
336                        errors.push(
337                            CompileError::new(
338                                "bynk.resolve.duplicate_provider",
339                                p.span,
340                                format!(
341                                    "capability `{}` already has a provider in this context",
342                                    p.capability.name
343                                ),
344                            )
345                            .with_label(prev.span, "previously provided here"),
346                        );
347                    } else {
348                        table.providers.insert(p.capability.name.clone(), p.clone());
349                    }
350                }
351                CommonsItem::Service(s) => {
352                    if kind == UnitKind::Adapter {
353                        errors.push(CompileError::new(
354                            "bynk.adapter.disallowed_item",
355                            s.span,
356                            "an `adapter` may not declare a `service` — adapters contain only capabilities, boundary types, external providers, and helpers",
357                        ));
358                        continue;
359                    }
360                    if kind != UnitKind::Context {
361                        errors.push(CompileError::new(
362                            "bynk.service.outside_context",
363                            s.span,
364                            "`service` declarations are only allowed inside a context, not a commons",
365                        ));
366                        continue;
367                    }
368                    if let Some(prev) = table.services.get(&s.name.name) {
369                        errors.push(
370                            CompileError::new(
371                                "bynk.resolve.duplicate_service",
372                                s.name.span,
373                                format!("service `{}` is already declared", s.name.name),
374                            )
375                            .with_label(prev.name.span, "previously declared here"),
376                        );
377                    } else {
378                        table.services.insert(s.name.name.clone(), s.clone());
379                    }
380                }
381                CommonsItem::Agent(a) => {
382                    if kind == UnitKind::Adapter {
383                        errors.push(CompileError::new(
384                            "bynk.adapter.disallowed_item",
385                            a.span,
386                            "an `adapter` may not declare an `agent` — adapters contain only capabilities, boundary types, external providers, and helpers",
387                        ));
388                        continue;
389                    }
390                    if kind != UnitKind::Context {
391                        errors.push(CompileError::new(
392                            "bynk.agent.outside_context",
393                            a.span,
394                            "`agent` declarations are only allowed inside a context, not a commons",
395                        ));
396                        continue;
397                    }
398                    if let Some(prev) = table.agents.get(&a.name.name) {
399                        errors.push(
400                            CompileError::new(
401                                "bynk.resolve.duplicate_agent",
402                                a.name.span,
403                                format!("agent `{}` is already declared", a.name.name),
404                            )
405                            .with_label(prev.name.span, "previously declared here"),
406                        );
407                    } else {
408                        table.agents.insert(a.name.name.clone(), a.clone());
409                    }
410                }
411                CommonsItem::Actor(a) => {
412                    if kind == UnitKind::Adapter {
413                        errors.push(CompileError::new(
414                            "bynk.adapter.disallowed_item",
415                            a.span,
416                            "an `adapter` may not declare an `actor` — adapters contain only capabilities, boundary types, external providers, and helpers",
417                        ));
418                        continue;
419                    }
420                    if let Some(prev) = table.actors.get(&a.name.name) {
421                        errors.push(
422                            CompileError::new(
423                                "bynk.resolve.duplicate_actor",
424                                a.name.span,
425                                format!("actor `{}` is already declared", a.name.name),
426                            )
427                            .with_label(prev.name.span, "previously declared here"),
428                        );
429                    } else {
430                        table.actors.insert(a.name.name.clone(), a.clone());
431                    }
432                }
433                _ => {}
434            }
435        }
436    }
437    for &i in indices {
438        for item in parsed[i].items() {
439            let CommonsItem::Fn(f) = item else { continue };
440            match &f.name {
441                FnName::Free(id) => {
442                    if let Some(prev) = table.fns.get(&id.name) {
443                        errors.push(
444                            CompileError::new(
445                                "bynk.resolve.duplicate_fn",
446                                id.span,
447                                format!("function `{}` is already declared", id.name),
448                            )
449                            .with_label(prev.name.ident().span, "previously declared here"),
450                        );
451                    } else if let Some(prev) = table.types.get(&id.name) {
452                        errors.push(
453                            CompileError::new(
454                                "bynk.resolve.name_conflict",
455                                id.span,
456                                format!(
457                                    "function `{}` conflicts with a type of the same name",
458                                    id.name
459                                ),
460                            )
461                            .with_label(prev.name.span, "type declared here"),
462                        );
463                    } else {
464                        table.fns.insert(id.name.clone(), f.clone());
465                    }
466                }
467                FnName::Method {
468                    type_name,
469                    method_name,
470                } => {
471                    if !table.types.contains_key(&type_name.name) {
472                        errors.push(
473                            CompileError::new(
474                                "bynk.resolve.method_unknown_type",
475                                type_name.span,
476                                format!(
477                                    "method `{}.{}` attached to an unknown type `{}`",
478                                    type_name.name, method_name.name, type_name.name
479                                ),
480                            )
481                            .with_note(
482                                "methods can only be declared on types defined in the same commons or context (across all of its files)",
483                            ),
484                        );
485                        continue;
486                    }
487                    let mt = table.methods.entry(type_name.name.clone()).or_default();
488                    let bucket = if f.has_self {
489                        &mut mt.instance
490                    } else {
491                        &mut mt.statics
492                    };
493                    if let Some(prev) = bucket.get(&method_name.name) {
494                        errors.push(
495                            CompileError::new(
496                                "bynk.resolve.duplicate_method",
497                                method_name.span,
498                                format!(
499                                    "method `{}.{}` is already declared",
500                                    type_name.name, method_name.name
501                                ),
502                            )
503                            .with_label(prev.name.ident().span, "previously declared here"),
504                        );
505                    } else {
506                        bucket.insert(method_name.name.clone(), f.clone());
507                    }
508                }
509            }
510        }
511    }
512    table
513}
514
515/// For each name declared in the unit (type, fn, method), record which
516/// source file declared it. Used by the emitter to render relative imports.
517#[derive(Clone)]
518pub struct FileDeclIndex {
519    pub types: HashMap<String, PathBuf>,
520    pub fns: HashMap<String, PathBuf>,
521    pub methods: HashMap<String, HashMap<String, PathBuf>>,
522}
523
524pub(crate) fn build_file_decl_index(indices: &[usize], parsed: &[ParsedFile]) -> FileDeclIndex {
525    let mut idx = FileDeclIndex {
526        types: HashMap::new(),
527        fns: HashMap::new(),
528        methods: HashMap::new(),
529    };
530    for &i in indices {
531        let path = parsed[i].source_path.clone();
532        for item in parsed[i].items() {
533            match item {
534                CommonsItem::Type(t) => {
535                    idx.types
536                        .entry(t.name.name.clone())
537                        .or_insert_with(|| path.clone());
538                }
539                CommonsItem::Fn(f) => match &f.name {
540                    FnName::Free(id) => {
541                        idx.fns
542                            .entry(id.name.clone())
543                            .or_insert_with(|| path.clone());
544                    }
545                    FnName::Method {
546                        type_name,
547                        method_name,
548                    } => {
549                        idx.methods
550                            .entry(type_name.name.clone())
551                            .or_default()
552                            .entry(method_name.name.clone())
553                            .or_insert_with(|| path.clone());
554                    }
555                },
556                CommonsItem::Capability(_)
557                | CommonsItem::Provider(_)
558                | CommonsItem::Service(_)
559                | CommonsItem::Agent(_)
560                | CommonsItem::Actor(_) => {}
561            }
562        }
563    }
564    idx
565}
566
567pub(crate) fn uses_span_of(parsed: &[ParsedFile], indices: &[usize], target: &str) -> Option<Span> {
568    for &i in indices {
569        for u in parsed[i].uses() {
570            if u.target.joined() == target {
571                return Some(u.span);
572            }
573        }
574    }
575    None
576}
577
578/// Build the [`resolver::CrossContextInfo`] for a given consuming context.
579/// Used by both the resolver/checker (per-file processing) and the emitter
580/// (composition root + boundary casts).
581pub(crate) fn build_cross_context_info(
582    name: &str,
583    unit_consumes: &HashMap<String, Vec<String>>,
584    unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
585    unit_uses: &HashMap<String, Vec<String>>,
586    unit_tables: &HashMap<String, UnitTable>,
587) -> resolver::CrossContextInfo {
588    let consumed_contexts: Vec<String> = unit_consumes.get(name).cloned().unwrap_or_default();
589    let aliases: HashMap<String, String> =
590        unit_consumes_aliases.get(name).cloned().unwrap_or_default();
591    let mut consumed_services: HashMap<String, HashMap<String, resolver::CrossContextService>> =
592        HashMap::new();
593    let mut consumed_types: HashMap<String, HashMap<String, TypeDecl>> = HashMap::new();
594    let mut consumed_capabilities: HashMap<
595        String,
596        HashMap<String, resolver::CrossContextCapability>,
597    > = HashMap::new();
598    for t in &consumed_contexts {
599        let other_types_combined = combined_types_for(t, unit_tables, unit_uses);
600        consumed_types.insert(t.clone(), other_types_combined.clone());
601        let Some(other_table) = unit_tables.get(t) else {
602            continue;
603        };
604        let mut svcs: HashMap<String, resolver::CrossContextService> = HashMap::new();
605        for (sname, sdecl) in &other_table.services {
606            let Some(handler) = sdecl
607                .handlers
608                .iter()
609                .find(|h| matches!(h.kind, HandlerKind::Call))
610            else {
611                continue;
612            };
613            let params: Vec<(String, TypeRef)> = handler
614                .params
615                .iter()
616                .map(|p| (p.name.name.clone(), p.type_ref.clone()))
617                .collect();
618            svcs.insert(
619                sname.clone(),
620                resolver::CrossContextService {
621                    name: sname.clone(),
622                    params,
623                    return_type: handler.return_type.clone(),
624                    span: sdecl.span,
625                },
626            );
627        }
628        consumed_services.insert(t.clone(), svcs);
629
630        // v0.15: gather the consumed context's exported capabilities, each
631        // paired with the provider that implements it.
632        let mut caps: HashMap<String, resolver::CrossContextCapability> = HashMap::new();
633        for cap_name in &other_table.exported_capabilities {
634            let Some(decl) = other_table.capabilities.get(cap_name) else {
635                continue;
636            };
637            let Some(provider) = other_table.providers.get(cap_name) else {
638                continue;
639            };
640            let ops = decl
641                .ops
642                .iter()
643                .map(|op| resolver::CrossContextCapabilityOp {
644                    name: op.name.name.clone(),
645                    params: op
646                        .params
647                        .iter()
648                        .map(|p| (p.name.name.clone(), p.type_ref.clone()))
649                        .collect(),
650                    return_type: op.return_type.clone(),
651                })
652                .collect();
653            caps.insert(
654                cap_name.clone(),
655                resolver::CrossContextCapability {
656                    name: cap_name.clone(),
657                    ops,
658                    provider_name: provider.provider_name.name.clone(),
659                    provider_given: provider
660                        .given
661                        .iter()
662                        .filter(|c| !c.is_cross_context())
663                        .map(|c| c.key().to_string())
664                        .collect(),
665                    span: decl.span,
666                },
667            );
668        }
669        consumed_capabilities.insert(t.clone(), caps);
670    }
671    resolver::CrossContextInfo {
672        self_context: Some(name.to_string()),
673        consumed_contexts,
674        aliases,
675        consumed_services,
676        consumed_types,
677        consumed_capabilities,
678        // Set by the caller from the unit's `consumes U { … }` clauses.
679        flattened_caps: HashMap::new(),
680    }
681}
682
683/// v0.15: validate one `given` capability reference. A bare reference must name
684/// a capability declared in this context; a cross-context reference (`given
685/// B.Cap`) must name a capability the consumed context exports. Returns the
686/// local [`CapabilityInfo`] to add to the in-scope map for bare references;
687/// cross-context references return `None` (their calls are type-checked via
688/// `consumed_capabilities` at the call site) but are still validated here.
689/// v0.25: record a clause-position capability reference (`provides Cap`,
690/// bare `given Cap`), qualifying a flattened bare name to its providing
691/// unit. The span is the name segment only.
692pub(crate) fn record_capability_clause_ref(
693    name: &Ident,
694    cross_context: &resolver::CrossContextInfo,
695    refs: &mut RefSink,
696) {
697    record_capability_clause_ref_inner(name, cross_context, refs, false);
698}
699
700/// v0.35 (ADR 0068): the `Cap` of a `provides Cap = Provider` clause — a
701/// capability reference *and* an implementation edge (the ambient owner is the
702/// provider). Flagged so assembly can tell it apart from the provider's own
703/// `given` deps, which are capability refs owned by the same provider.
704pub(crate) fn record_provides_clause_ref(
705    name: &Ident,
706    cross_context: &resolver::CrossContextInfo,
707    refs: &mut RefSink,
708) {
709    record_capability_clause_ref_inner(name, cross_context, refs, true);
710}
711
712fn record_capability_clause_ref_inner(
713    name: &Ident,
714    cross_context: &resolver::CrossContextInfo,
715    refs: &mut RefSink,
716    provides: bool,
717) {
718    let unit = cross_context.flattened_caps.get(&name.name);
719    if provides {
720        refs.record_provides(name.span, &name.name, unit.map(String::as_str));
721    } else if let Some(unit) = unit {
722        refs.record_in_unit(name.span, SymbolKind::Capability, &name.name, unit);
723    } else {
724        refs.record(name.span, SymbolKind::Capability, &name.name);
725    }
726}
727
728pub(crate) fn resolve_given_cap_ref(
729    cap_ref: &CapRef,
730    capability_info_map: &HashMap<String, CapabilityInfo>,
731    cross_context: &resolver::CrossContextInfo,
732    errors: &mut Vec<CompileError>,
733    refs: &mut RefSink,
734) -> Option<CapabilityInfo> {
735    let Some(prefix) = cap_ref.prefix() else {
736        // Local capability.
737        match capability_info_map.get(cap_ref.key()) {
738            Some(info) => {
739                record_capability_clause_ref(&cap_ref.name, cross_context, refs);
740                return Some(info.clone());
741            }
742            None => {
743                errors.push(CompileError::new(
744                    "bynk.given.unknown_capability",
745                    cap_ref.span,
746                    format!(
747                        "capability `{}` is not declared in this context",
748                        cap_ref.key()
749                    ),
750                ));
751                return None;
752            }
753        }
754    };
755    // Cross-context capability (`given B.Cap` / `given Alias.Cap`).
756    let Some(ctx_name) = cross_context.resolve_prefix(&prefix) else {
757        errors.push(
758            CompileError::new(
759                "bynk.resolve.unconsumed_context",
760                cap_ref.span,
761                format!(
762                    "`given {}.{}` refers to a context that this context does not `consumes`",
763                    prefix,
764                    cap_ref.key()
765                ),
766            )
767            .with_note(
768                "add a `consumes` clause for the providing context (optionally with an alias) at the top of this context",
769            ),
770        );
771        return None;
772    };
773    let exports_it = cross_context
774        .consumed_capabilities
775        .get(&ctx_name)
776        .is_some_and(|m| m.contains_key(cap_ref.key()));
777    if exports_it {
778        // v0.25: dotted `given B.Cap` — the name segment, in the consumed
779        // unit's namespace.
780        refs.record_in_unit(
781            cap_ref.name.span,
782            SymbolKind::Capability,
783            cap_ref.key(),
784            &ctx_name,
785        );
786    }
787    if !exports_it {
788        errors.push(
789            CompileError::new(
790                "bynk.given.cross_context_unknown_capability",
791                cap_ref.span,
792                format!(
793                    "context `{}` does not export a capability named `{}`",
794                    ctx_name,
795                    cap_ref.key()
796                ),
797            )
798            .with_note(
799                "the providing context must list the capability in an `exports capability { … }` clause",
800            ),
801        );
802    }
803    None
804}
805
806/// Build the combined type table for `unit`: its own types merged with the
807/// types of every commons it `uses`. Used by cross-context resolution so we
808/// can resolve a consumed context's service signatures against that context's
809/// own view of types (v0.6 §4.5).
810fn combined_types_for(
811    unit: &str,
812    unit_tables: &HashMap<String, UnitTable>,
813    unit_uses: &HashMap<String, Vec<String>>,
814) -> HashMap<String, TypeDecl> {
815    let mut out: HashMap<String, TypeDecl> = HashMap::new();
816    if let Some(table) = unit_tables.get(unit) {
817        for (n, d) in &table.types {
818            out.insert(n.clone(), d.clone());
819        }
820    }
821    if let Some(targets) = unit_uses.get(unit) {
822        for t in targets {
823            if let Some(used) = unit_tables.get(t) {
824                for (n, d) in &used.types {
825                    out.entry(n.clone()).or_insert_with(|| d.clone());
826                }
827            }
828        }
829    }
830    out
831}
832
833pub(crate) fn consumes_span_of(
834    parsed: &[ParsedFile],
835    indices: &[usize],
836    target: &str,
837) -> Option<Span> {
838    for &i in indices {
839        for c in parsed[i].consumes() {
840            if c.target.joined() == target {
841                return Some(c.span);
842            }
843        }
844    }
845    None
846}
847
848pub(crate) fn parsed_alias_span(
849    parsed: &[ParsedFile],
850    indices: &[usize],
851    alias: &str,
852) -> Option<Span> {
853    for &i in indices {
854        for c in parsed[i].consumes() {
855            if let Some(a) = &c.alias
856                && a.name == alias
857            {
858                return Some(a.span);
859            }
860        }
861    }
862    None
863}
864
865/// A type imported into a context via `consumes`. Carries enough metadata for
866/// the checker and emitter to enforce / express visibility.
867#[derive(Debug, Clone)]
868pub struct ConsumedType {
869    pub owning_context: String,
870    pub visibility: Visibility,
871}