1use super::*;
2
3pub(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 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 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 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 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
186fn 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#[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 pub capabilities: HashMap<String, CapabilityDecl>,
217 pub providers: HashMap<String, ProviderDecl>,
220 pub services: HashMap<String, ServiceDecl>,
222 pub agents: HashMap<String, AgentDecl>,
224 pub actors: HashMap<String, ActorDecl>,
226 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 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 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 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 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#[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
578pub(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 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 flattened_caps: HashMap::new(),
680 }
681}
682
683pub(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
700pub(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 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 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 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
806fn 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#[derive(Debug, Clone)]
868pub struct ConsumedType {
869 pub owning_context: String,
870 pub visibility: Visibility,
871}