1use std::collections::{BTreeMap, HashMap};
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use crate::checker::CapabilityInfo;
6use crate::index::{IndexBuilder, ProjectIndex, RefSink, SiteRef, SymbolKind};
7use crate::resolver::{self, MethodTable as ResolverMethodTable};
8use bynk_project::{ParsedFile, UnitKind};
9use bynk_syntax::ast::{
10 ActorDecl, AgentDecl, BaseType, Block, CapRef, CapabilityDecl, CommonsItem, EventDecl,
11 ExportKind, Expr, ExprId, ExprKind, FnDecl, FnName, HandlerKind, Ident, Param, ProviderDecl,
12 ServiceDecl, Trivia, TypeBody, TypeDecl, TypeRef, Visibility,
13};
14use bynk_syntax::error::CompileError;
15use bynk_syntax::span::Span;
16
17pub fn assemble_index(
23 parsed: &[ParsedFile],
24 unit_uses: &HashMap<String, Vec<String>>,
25 unit_consumes: &HashMap<String, Vec<String>>,
26 refs: RefSink,
27) -> ProjectIndex {
28 let mut builder = IndexBuilder::default();
29 let mut uses = unit_uses.clone();
30 uses.extend(refs.extra_uses);
31 builder.set_uses(uses);
32 builder.set_consumes(unit_consumes.clone());
33 for pf in parsed {
34 if matches!(pf.kind(), UnitKind::Test | UnitKind::Integration) {
35 continue;
36 }
37 let unit = pf.unit().name().joined();
38 if pf.is_synthetic() {
43 for item in pf.items() {
44 let (kind, name, modifiers) = match item {
45 CommonsItem::Type(t) => (
46 SymbolKind::Type,
47 &t.name.name,
48 symbol_modifiers(&unit, Some(t)),
49 ),
50 CommonsItem::Event(e) => (
56 SymbolKind::Type,
57 &e.name.name,
58 symbol_modifiers(&unit, None),
59 ),
60 CommonsItem::Fn(f) => match &f.name {
61 FnName::Free(id) => {
62 (SymbolKind::Fn, &id.name, symbol_modifiers(&unit, None))
63 }
64 FnName::Method { .. } => continue,
65 },
66 CommonsItem::Capability(c) => (
67 SymbolKind::Capability,
68 &c.name.name,
69 symbol_modifiers(&unit, None),
70 ),
71 CommonsItem::Service(s) => (
72 SymbolKind::Service,
73 &s.name.name,
74 symbol_modifiers(&unit, None),
75 ),
76 CommonsItem::Agent(a) => (
77 SymbolKind::Agent,
78 &a.name.name,
79 symbol_modifiers(&unit, None),
80 ),
81 CommonsItem::Provider(p) => (
82 SymbolKind::Provider,
83 &p.provider_name.name,
84 symbol_modifiers(&unit, None),
85 ),
86 CommonsItem::Actor(a) => (
87 SymbolKind::Actor,
88 &a.name.name,
89 symbol_modifiers(&unit, None),
90 ),
91 CommonsItem::Messages(m) => {
92 (SymbolKind::Messages, &m.tag, symbol_modifiers(&unit, None))
93 }
94 };
95 builder.add_first_party_def(&unit, kind, name, modifiers);
96 }
97 continue;
98 }
99 let site = |id: &Ident| SiteRef {
100 path: pf.identity_path(),
101 span: id.span,
102 };
103 for item in pf.items() {
104 match item {
105 CommonsItem::Type(t) => {
106 builder.add_def(
107 &unit,
108 SymbolKind::Type,
109 &t.name.name,
110 site(&t.name),
111 symbol_modifiers(&unit, Some(t)),
112 );
113 if let TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } = &t.body
118 {
119 builder.add_refinement(&unit, &t.name.name, *base);
120 }
121 if let TypeBody::Record(r) = &t.body {
124 for field in &r.fields {
125 builder.add_def(
126 &unit,
127 SymbolKind::Field,
128 &format!("{}.{}", t.name.name, field.name.name),
129 site(&field.name),
130 symbol_modifiers(&unit, None),
131 );
132 }
133 }
134 }
135 CommonsItem::Event(e) => {
141 builder.add_def(
142 &unit,
143 SymbolKind::Type,
144 &e.name.name,
145 site(&e.name),
146 symbol_modifiers(&unit, None),
147 );
148 for field in &e.body.fields {
149 builder.add_def(
150 &unit,
151 SymbolKind::Field,
152 &format!("{}.{}", e.name.name, field.name.name),
153 site(&field.name),
154 symbol_modifiers(&unit, None),
155 );
156 }
157 }
158 CommonsItem::Fn(f) => match &f.name {
159 FnName::Free(id) => {
160 builder.add_def(
161 &unit,
162 SymbolKind::Fn,
163 &id.name,
164 site(id),
165 symbol_modifiers(&unit, None),
166 );
167 }
168 FnName::Method { .. } => {
169 builder.add_owner(&unit, &f.name.display(), &pf.identity_path());
173 builder.add_def(
174 &unit,
175 SymbolKind::Method,
176 &f.name.display(),
177 site(f.name.ident()),
178 symbol_modifiers(&unit, None),
179 );
180 }
181 },
182 CommonsItem::Capability(c) => {
183 builder.add_def(
184 &unit,
185 SymbolKind::Capability,
186 &c.name.name,
187 site(&c.name),
188 symbol_modifiers(&unit, None),
189 );
190 for op in &c.ops {
193 builder.add_def(
194 &unit,
195 SymbolKind::CapabilityOp,
196 &format!("{}.{}", c.name.name, op.name.name),
197 site(&op.name),
198 symbol_modifiers(&unit, None),
199 );
200 }
201 }
202 CommonsItem::Service(s) => {
203 builder.add_def(
204 &unit,
205 SymbolKind::Service,
206 &s.name.name,
207 site(&s.name),
208 symbol_modifiers(&unit, None),
209 );
210 }
211 CommonsItem::Agent(a) => {
212 builder.add_def(
213 &unit,
214 SymbolKind::Agent,
215 &a.name.name,
216 site(&a.name),
217 symbol_modifiers(&unit, None),
218 );
219 for h in &a.handlers {
225 if let Some(name) = &h.method_name {
226 builder.add_def(
227 &unit,
228 SymbolKind::Handler,
229 &format!("{}.{}", a.name.name, name.name),
230 site(name),
231 symbol_modifiers(&unit, None),
232 );
233 }
234 }
235 }
236 CommonsItem::Provider(p) => {
237 builder.add_def(
238 &unit,
239 SymbolKind::Provider,
240 &p.provider_name.name,
241 site(&p.provider_name),
242 symbol_modifiers(&unit, None),
243 );
244 }
245 CommonsItem::Actor(a) => {
246 builder.add_def(
247 &unit,
248 SymbolKind::Actor,
249 &a.name.name,
250 site(&a.name),
251 symbol_modifiers(&unit, None),
252 );
253 }
254 CommonsItem::Messages(m) => {
255 builder.add_def(
258 &unit,
259 SymbolKind::Messages,
260 &m.tag,
261 SiteRef {
262 path: pf.identity_path(),
263 span: m.tag_span,
264 },
265 symbol_modifiers(&unit, None),
266 );
267 }
268 }
269 }
270 }
271 builder.build(refs.edges)
272}
273
274fn symbol_modifiers(unit: &str, type_decl: Option<&TypeDecl>) -> crate::index::SymbolModifiers {
280 let (refined, opaque) = match type_decl.map(|t| &t.body) {
281 Some(TypeBody::Refined { refinement, .. }) => (refinement.is_some(), false),
282 Some(TypeBody::Opaque { refinement, .. }) => (refinement.is_some(), true),
283 _ => (false, false),
284 };
285 crate::index::SymbolModifiers {
286 refined,
287 opaque,
288 platform_native: crate::firstparty::platform_of(unit).is_some(),
289 }
290}
291
292#[derive(Clone, Default)]
294pub struct UnitTable {
295 #[allow(dead_code)]
296 pub kind: Option<UnitKind>,
297 pub types: HashMap<String, Arc<TypeDecl>>,
298 pub fns: HashMap<String, Arc<FnDecl>>,
299 pub methods: HashMap<String, ResolverMethodTable>,
300 pub capabilities: HashMap<String, CapabilityDecl>,
302 pub providers: HashMap<String, ProviderDecl>,
305 pub services: HashMap<String, ServiceDecl>,
307 pub agents: HashMap<String, AgentDecl>,
309 pub actors: HashMap<String, ActorDecl>,
311 pub exported_capabilities: std::collections::HashSet<String>,
314 pub events: HashMap<String, EventDecl>,
323}
324
325pub fn build_unit_table(
331 _name: &str,
332 kind: UnitKind,
333 indices: &[usize],
334 parsed: &[ParsedFile],
335 out: &mut Vec<(PathBuf, CompileError)>,
336) -> UnitTable {
337 let mut table = UnitTable {
338 kind: Some(kind),
339 ..UnitTable::default()
340 };
341 for &i in indices {
342 let mut errors: Vec<CompileError> = Vec::new();
343 for item in parsed[i].items() {
344 if let CommonsItem::Event(e) = item
351 && kind != UnitKind::Context
352 {
353 errors.push(CompileError::new(
354 "bynk.event.outside_context",
355 e.span,
356 "`event` declarations are only allowed inside a context",
357 ));
358 continue;
359 }
360 let as_type: Option<(&Ident, TypeDecl, bool)> = match item {
361 CommonsItem::Type(t) => Some((&t.name, t.clone(), false)),
362 CommonsItem::Event(e) => Some((&e.name, e.as_type_decl(), true)),
363 _ => None,
364 };
365 if let Some((name, decl, is_event)) = as_type {
366 if let Some(prev) = table.types.get(&name.name) {
367 errors.push(
368 CompileError::new(
369 "bynk.resolve.duplicate_type",
370 name.span,
371 format!("type `{}` is already declared", name.name),
372 )
373 .with_label(prev.name.span, "previously declared here"),
374 );
375 } else {
376 table.methods.entry(name.name.clone()).or_default();
377 if is_event {
378 let CommonsItem::Event(e) = item else {
379 unreachable!("is_event only set for CommonsItem::Event")
380 };
381 table.events.insert(name.name.clone(), e.clone());
382 }
383 table.types.insert(name.name.clone(), Arc::new(decl));
384 }
385 }
386 }
387 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
388 }
389 for &i in indices {
392 {
393 for clause in parsed[i].exports() {
394 if matches!(clause.kind, ExportKind::Capability) {
395 for n in &clause.names {
396 table.exported_capabilities.insert(n.name.clone());
397 }
398 }
399 }
400 }
401 }
402 for &i in indices {
404 let mut errors: Vec<CompileError> = Vec::new();
405 for item in parsed[i].items() {
406 match item {
407 CommonsItem::Capability(c) => {
408 if kind != UnitKind::Context && kind != UnitKind::Adapter {
409 errors.push(CompileError::new(
410 "bynk.capability.outside_context",
411 c.span,
412 "`capability` declarations are only allowed inside a context or adapter",
413 ));
414 continue;
415 }
416 if let Some(prev) = table.capabilities.get(&c.name.name) {
417 errors.push(
418 CompileError::new(
419 "bynk.resolve.duplicate_capability",
420 c.name.span,
421 format!("capability `{}` is already declared", c.name.name),
422 )
423 .with_label(prev.name.span, "previously declared here"),
424 );
425 } else {
426 table.capabilities.insert(c.name.name.clone(), c.clone());
427 }
428 }
429 CommonsItem::Provider(p) => {
430 match kind {
431 UnitKind::Context => {
432 if p.external {
435 errors.push(CompileError::new(
436 "bynk.context.external_provider",
437 p.span,
438 "an external (bodiless) provider is only allowed inside an `adapter` — a context provider must have a Bynk body",
439 ));
440 continue;
441 }
442 }
443 UnitKind::Adapter => {
444 if !p.external {
447 errors.push(CompileError::new(
448 "bynk.adapter.provider_has_body",
449 p.span,
450 "a provider inside an `adapter` must be external (no body) — its implementation is supplied by the binding",
451 ));
452 continue;
453 }
454 }
455 _ => {
456 errors.push(CompileError::new(
457 "bynk.provider.outside_context",
458 p.span,
459 "`provides` declarations are only allowed inside a context or adapter",
460 ));
461 continue;
462 }
463 }
464 if let Some(prev) = table.providers.get(&p.capability.name) {
465 errors.push(
466 CompileError::new(
467 "bynk.resolve.duplicate_provider",
468 p.span,
469 format!(
470 "capability `{}` already has a provider in this context",
471 p.capability.name
472 ),
473 )
474 .with_label(prev.span, "previously provided here"),
475 );
476 } else {
477 table.providers.insert(p.capability.name.clone(), p.clone());
478 }
479 }
480 CommonsItem::Service(s) => {
481 if kind == UnitKind::Adapter {
482 errors.push(CompileError::new(
483 "bynk.adapter.disallowed_item",
484 s.span,
485 "an `adapter` may not declare a `service` — adapters contain only capabilities, boundary types, external providers, and helpers",
486 ));
487 continue;
488 }
489 if kind != UnitKind::Context {
490 errors.push(CompileError::new(
491 "bynk.service.outside_context",
492 s.span,
493 "`service` declarations are only allowed inside a context, not a commons",
494 ));
495 continue;
496 }
497 if let Some(prev) = table.services.get(&s.name.name) {
498 errors.push(
499 CompileError::new(
500 "bynk.resolve.duplicate_service",
501 s.name.span,
502 format!("service `{}` is already declared", s.name.name),
503 )
504 .with_label(prev.name.span, "previously declared here"),
505 );
506 } else {
507 table.services.insert(s.name.name.clone(), s.clone());
508 }
509 }
510 CommonsItem::Agent(a) => {
511 if kind == UnitKind::Adapter {
512 errors.push(CompileError::new(
513 "bynk.adapter.disallowed_item",
514 a.span,
515 "an `adapter` may not declare an `agent` — adapters contain only capabilities, boundary types, external providers, and helpers",
516 ));
517 continue;
518 }
519 if kind != UnitKind::Context {
520 errors.push(CompileError::new(
521 "bynk.agent.outside_context",
522 a.span,
523 "`agent` declarations are only allowed inside a context, not a commons",
524 ));
525 continue;
526 }
527 if let Some(prev) = table.agents.get(&a.name.name) {
528 errors.push(
529 CompileError::new(
530 "bynk.resolve.duplicate_agent",
531 a.name.span,
532 format!("agent `{}` is already declared", a.name.name),
533 )
534 .with_label(prev.name.span, "previously declared here"),
535 );
536 } else {
537 table.agents.insert(a.name.name.clone(), a.clone());
538 }
539 }
540 CommonsItem::Actor(a) => {
541 if kind == UnitKind::Adapter {
542 errors.push(CompileError::new(
543 "bynk.adapter.disallowed_item",
544 a.span,
545 "an `adapter` may not declare an `actor` — adapters contain only capabilities, boundary types, external providers, and helpers",
546 ));
547 continue;
548 }
549 if let Some(prev) = table.actors.get(&a.name.name) {
550 errors.push(
551 CompileError::new(
552 "bynk.resolve.duplicate_actor",
553 a.name.span,
554 format!("actor `{}` is already declared", a.name.name),
555 )
556 .with_label(prev.name.span, "previously declared here"),
557 );
558 } else {
559 table.actors.insert(a.name.name.clone(), a.clone());
560 }
561 }
562 _ => {}
563 }
564 }
565 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
566 }
567 for &i in indices {
568 let mut errors: Vec<CompileError> = Vec::new();
569 for item in parsed[i].items() {
570 let CommonsItem::Fn(f) = item else { continue };
571 match &f.name {
572 FnName::Free(id) => {
573 if let Some(prev) = table.fns.get(&id.name) {
574 errors.push(
575 CompileError::new(
576 "bynk.resolve.duplicate_fn",
577 id.span,
578 format!("function `{}` is already declared", id.name),
579 )
580 .with_label(prev.name.ident().span, "previously declared here"),
581 );
582 } else if let Some(prev) = table.types.get(&id.name) {
583 errors.push(
584 CompileError::new(
585 "bynk.resolve.name_conflict",
586 id.span,
587 format!(
588 "function `{}` conflicts with a type of the same name",
589 id.name
590 ),
591 )
592 .with_label(prev.name.span, "type declared here"),
593 );
594 } else {
595 table.fns.insert(id.name.clone(), Arc::new(f.clone()));
596 }
597 }
598 FnName::Method {
599 type_name,
600 method_name,
601 } => {
602 if !table.types.contains_key(&type_name.name) {
603 errors.push(
604 CompileError::new(
605 "bynk.resolve.method_unknown_type",
606 type_name.span,
607 format!(
608 "method `{}.{}` attached to an unknown type `{}`",
609 type_name.name, method_name.name, type_name.name
610 ),
611 )
612 .with_note(
613 "methods can only be declared on types defined in the same commons or context (across all of its files)",
614 ),
615 );
616 continue;
617 }
618 let mt = table.methods.entry(type_name.name.clone()).or_default();
619 let bucket = if f.has_self {
620 &mut mt.instance
621 } else {
622 &mut mt.statics
623 };
624 if let Some(prev) = bucket.get(&method_name.name) {
625 errors.push(
626 CompileError::new(
627 "bynk.resolve.duplicate_method",
628 method_name.span,
629 format!(
630 "method `{}.{}` is already declared",
631 type_name.name, method_name.name
632 ),
633 )
634 .with_label(prev.name.ident().span, "previously declared here"),
635 );
636 } else {
637 bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
638 }
639 }
640 }
641 }
642 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
643 }
644 if kind == UnitKind::Commons
659 && let Some(m) = indices.iter().find_map(|&i| {
660 parsed[i].items().iter().find_map(|item| match item {
661 CommonsItem::Messages(m) => Some(m),
662 _ => None,
663 })
664 })
665 {
666 if let Some(prev) = table.fns.get("render") {
667 out.push((
668 parsed[indices[0]].identity_path(),
669 CompileError::new(
670 "bynk.resolve.duplicate_fn",
671 m.span,
672 "function `render` is already declared",
673 )
674 .with_label(prev.name.ident().span, "previously declared here")
675 .with_note(
676 "a `messages` block in this commons implicitly declares its own \
677 `render(tag, msg) -> String` — name it something else",
678 ),
679 ));
680 } else {
681 table
682 .fns
683 .insert("render".to_string(), Arc::new(synthetic_render_fn()));
684 }
685 }
686 table
687}
688
689fn synthetic_render_fn() -> FnDecl {
694 let span = Span::default();
695 FnDecl {
696 type_params: Vec::new(),
697 name: FnName::Free(Ident {
698 name: "render".to_string(),
699 span,
700 }),
701 params: vec![
702 Param {
703 name: Ident {
704 name: "tag".to_string(),
705 span,
706 },
707 type_ref: TypeRef::Named(Ident {
708 name: "LocaleTag".to_string(),
709 span,
710 }),
711 span,
712 },
713 Param {
714 name: Ident {
715 name: "msg".to_string(),
716 span,
717 },
718 type_ref: TypeRef::Named(Ident {
719 name: "Message".to_string(),
720 span,
721 }),
722 span,
723 },
724 ],
725 return_type: TypeRef::Base(BaseType::String, span),
726 requires: Vec::new(),
727 ensures: Vec::new(),
728 body: Block {
729 statements: Vec::new(),
730 tail: Box::new(Expr {
731 id: ExprId::SYNTHETIC,
732 kind: ExprKind::StrLit(String::new()),
733 span,
734 }),
735 span,
736 tail_leading_comments: Vec::new(),
737 implicit_tail: false,
738 },
739 has_self: false,
740 documentation: None,
741 span,
742 trivia: Trivia::default(),
743 }
744}
745
746#[derive(Clone)]
749pub struct FileDeclIndex {
750 pub types: HashMap<String, PathBuf>,
751 pub fns: HashMap<String, PathBuf>,
752 pub methods: HashMap<String, HashMap<String, PathBuf>>,
753}
754
755pub fn build_file_decl_index(indices: &[usize], parsed: &[ParsedFile]) -> FileDeclIndex {
763 let mut idx = FileDeclIndex {
764 types: HashMap::new(),
765 fns: HashMap::new(),
766 methods: HashMap::new(),
767 };
768 for &i in indices {
769 let path = parsed[i].source_path();
770 for item in parsed[i].items() {
771 match item {
772 CommonsItem::Type(t) => {
773 idx.types
774 .entry(t.name.name.clone())
775 .or_insert_with(|| path.clone());
776 }
777 CommonsItem::Event(e) => {
782 idx.types
783 .entry(e.name.name.clone())
784 .or_insert_with(|| path.clone());
785 }
786 CommonsItem::Fn(f) => match &f.name {
787 FnName::Free(id) => {
788 idx.fns
789 .entry(id.name.clone())
790 .or_insert_with(|| path.clone());
791 }
792 FnName::Method {
793 type_name,
794 method_name,
795 } => {
796 idx.methods
797 .entry(type_name.name.clone())
798 .or_default()
799 .entry(method_name.name.clone())
800 .or_insert_with(|| path.clone());
801 }
802 },
803 CommonsItem::Capability(_)
804 | CommonsItem::Provider(_)
805 | CommonsItem::Service(_)
806 | CommonsItem::Agent(_)
807 | CommonsItem::Actor(_)
808 | CommonsItem::Messages(_) => {}
811 }
812 }
813 }
814 idx
815}
816
817pub fn uses_span_of(
820 parsed: &[ParsedFile],
821 indices: &[usize],
822 target: &str,
823) -> Option<(usize, Span)> {
824 for &i in indices {
825 for u in parsed[i].uses() {
826 if u.target.joined() == target {
827 return Some((i, u.span));
828 }
829 }
830 }
831 None
832}
833
834pub fn build_cross_context_info(
838 name: &str,
839 unit_consumes: &HashMap<String, Vec<String>>,
840 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
841 unit_uses: &HashMap<String, Vec<String>>,
842 unit_tables: &HashMap<String, UnitTable>,
843) -> resolver::CrossContextInfo {
844 let consumed_contexts: Vec<String> = unit_consumes.get(name).cloned().unwrap_or_default();
845 let aliases: HashMap<String, String> =
846 unit_consumes_aliases.get(name).cloned().unwrap_or_default();
847 let mut consumed_services: HashMap<String, HashMap<String, resolver::CrossContextService>> =
848 HashMap::new();
849 let mut consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>> = HashMap::new();
850 let mut consumed_capabilities: HashMap<
851 String,
852 HashMap<String, resolver::CrossContextCapability>,
853 > = HashMap::new();
854 let mut consumed_event_names: HashMap<String, std::collections::HashSet<String>> =
859 HashMap::new();
860 for t in &consumed_contexts {
861 let other_types_combined = combined_types_for(t, unit_tables, unit_uses);
862 consumed_types.insert(t.clone(), other_types_combined.clone());
863 let Some(other_table) = unit_tables.get(t) else {
864 continue;
865 };
866 consumed_event_names.insert(t.clone(), other_table.events.keys().cloned().collect());
867 let mut svcs: HashMap<String, resolver::CrossContextService> = HashMap::new();
868 for (sname, sdecl) in &other_table.services {
869 let Some(handler) = sdecl
870 .handlers
871 .iter()
872 .find(|h| matches!(h.kind, HandlerKind::Call))
873 else {
874 continue;
875 };
876 let params: Vec<(String, TypeRef)> = handler
877 .params
878 .iter()
879 .map(|p| (p.name.name.clone(), p.type_ref.clone()))
880 .collect();
881 svcs.insert(
882 sname.clone(),
883 resolver::CrossContextService {
884 name: sname.clone(),
885 params,
886 return_type: handler.return_type.clone(),
887 span: sdecl.span,
888 },
889 );
890 }
891 consumed_services.insert(t.clone(), svcs);
892
893 let mut caps: HashMap<String, resolver::CrossContextCapability> = HashMap::new();
896 for cap_name in &other_table.exported_capabilities {
897 let Some(decl) = other_table.capabilities.get(cap_name) else {
898 continue;
899 };
900 let Some(provider) = other_table.providers.get(cap_name) else {
901 continue;
902 };
903 let ops = decl
904 .ops
905 .iter()
906 .map(|op| resolver::CrossContextCapabilityOp {
907 name: op.name.name.clone(),
908 type_params: op.type_params.iter().map(|p| p.name.name.clone()).collect(),
909 params: op
910 .params
911 .iter()
912 .map(|p| (p.name.name.clone(), p.type_ref.clone()))
913 .collect(),
914 return_type: op.return_type.clone(),
915 })
916 .collect();
917 caps.insert(
918 cap_name.clone(),
919 resolver::CrossContextCapability {
920 name: cap_name.clone(),
921 ops,
922 provider_name: provider.provider_name.name.clone(),
923 provider_given: provider
924 .given
925 .iter()
926 .filter(|c| !c.is_cross_context())
927 .map(|c| c.key().to_string())
928 .collect(),
929 span: decl.span,
930 },
931 );
932 }
933 consumed_capabilities.insert(t.clone(), caps);
934 }
935 resolver::CrossContextInfo {
936 self_context: Some(name.to_string()),
937 consumed_contexts,
938 aliases,
939 consumed_services,
940 consumed_types,
941 consumed_capabilities,
942 flattened_caps: HashMap::new(),
944 consumed_event_names,
945 }
946}
947
948pub fn record_capability_clause_ref(
958 name: &Ident,
959 cross_context: &resolver::CrossContextInfo,
960 refs: &mut RefSink,
961) {
962 record_capability_clause_ref_inner(name, cross_context, refs, false);
963}
964
965pub fn record_provides_clause_ref(
970 name: &Ident,
971 cross_context: &resolver::CrossContextInfo,
972 refs: &mut RefSink,
973) {
974 record_capability_clause_ref_inner(name, cross_context, refs, true);
975}
976
977fn record_capability_clause_ref_inner(
978 name: &Ident,
979 cross_context: &resolver::CrossContextInfo,
980 refs: &mut RefSink,
981 provides: bool,
982) {
983 let unit = cross_context.flattened_caps.get(&name.name);
984 if provides {
985 refs.record_provides(name.span, &name.name, unit.map(String::as_str));
986 } else if let Some(unit) = unit {
987 refs.record_in_unit(name.span, SymbolKind::Capability, &name.name, unit);
988 } else {
989 refs.record(name.span, SymbolKind::Capability, &name.name);
990 }
991}
992
993pub fn resolve_given_cap_ref(
994 cap_ref: &CapRef,
995 capability_info_map: &HashMap<String, CapabilityInfo>,
996 cross_context: &resolver::CrossContextInfo,
997 errors: &mut Vec<CompileError>,
998 refs: &mut RefSink,
999) -> Option<CapabilityInfo> {
1000 let Some(prefix) = cap_ref.prefix() else {
1001 match capability_info_map.get(cap_ref.key()) {
1003 Some(info) => {
1004 record_capability_clause_ref(&cap_ref.name, cross_context, refs);
1005 return Some(info.clone());
1006 }
1007 None => {
1008 errors.push(CompileError::new(
1009 "bynk.given.unknown_capability",
1010 cap_ref.span,
1011 format!(
1012 "capability `{}` is not declared in this context",
1013 cap_ref.key()
1014 ),
1015 ));
1016 return None;
1017 }
1018 }
1019 };
1020 let Some(ctx_name) = cross_context.resolve_prefix(&prefix) else {
1022 errors.push(
1023 CompileError::new(
1024 "bynk.resolve.unconsumed_context",
1025 cap_ref.span,
1026 format!(
1027 "`given {}.{}` refers to a context that this context does not `consumes`",
1028 prefix,
1029 cap_ref.key()
1030 ),
1031 )
1032 .with_note(
1033 "add a `consumes` clause for the providing context (optionally with an alias) at the top of this context",
1034 ),
1035 );
1036 return None;
1037 };
1038 let exports_it = cross_context
1039 .consumed_capabilities
1040 .get(&ctx_name)
1041 .is_some_and(|m| m.contains_key(cap_ref.key()));
1042 if exports_it {
1043 refs.record_in_unit(
1046 cap_ref.name.span,
1047 SymbolKind::Capability,
1048 cap_ref.key(),
1049 &ctx_name,
1050 );
1051 }
1052 if !exports_it {
1053 errors.push(
1054 CompileError::new(
1055 "bynk.given.cross_context_unknown_capability",
1056 cap_ref.span,
1057 format!(
1058 "context `{}` does not export a capability named `{}`",
1059 ctx_name,
1060 cap_ref.key()
1061 ),
1062 )
1063 .with_note(
1064 "the providing context must list the capability in an `exports capability { … }` clause",
1065 ),
1066 );
1067 }
1068 None
1069}
1070
1071pub fn combined_types_for(
1084 unit: &str,
1085 unit_tables: &HashMap<String, UnitTable>,
1086 unit_uses: &HashMap<String, Vec<String>>,
1087) -> HashMap<String, Arc<TypeDecl>> {
1088 let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1089 if let Some(table) = unit_tables.get(unit) {
1090 for (n, d) in &table.types {
1091 out.insert(n.clone(), d.clone());
1092 }
1093 }
1094 if let Some(targets) = unit_uses.get(unit) {
1095 for t in targets {
1096 if let Some(used) = unit_tables.get(t) {
1097 for (n, d) in &used.types {
1098 out.entry(n.clone()).or_insert_with(|| d.clone());
1099 }
1100 }
1101 }
1102 }
1103 out
1104}
1105
1106pub enum ContextMessageBundle {
1119 None,
1121 One(MessageBundleInfo),
1123 Many(Vec<String>),
1125}
1126
1127pub struct MessageBundleInfo {
1128 pub commons: String,
1130 pub source_path: PathBuf,
1138}
1139
1140pub fn detect_context_message_bundle(
1146 ctx: &str,
1147 unit_uses: &HashMap<String, Vec<String>>,
1148 groups: &BTreeMap<String, Vec<usize>>,
1149 kinds: &BTreeMap<String, UnitKind>,
1150 parsed: &[ParsedFile],
1151) -> ContextMessageBundle {
1152 let mut found: Vec<MessageBundleInfo> = Vec::new();
1153 for target in unit_uses.get(ctx).into_iter().flatten() {
1154 if kinds.get(target) != Some(&UnitKind::Commons) {
1155 continue;
1156 }
1157 let Some(indices) = groups.get(target) else {
1158 continue;
1159 };
1160 for &i in indices {
1161 let has_reference = parsed[i].items().iter().any(|item| {
1162 matches!(item, CommonsItem::Messages(m) if m.annotations.iter().any(|a| a.name.name == "reference"))
1163 });
1164 if has_reference {
1165 found.push(MessageBundleInfo {
1166 commons: target.clone(),
1167 source_path: parsed[i].source_path(),
1168 });
1169 break;
1170 }
1171 }
1172 }
1173 match found.len() {
1174 0 => ContextMessageBundle::None,
1175 1 => ContextMessageBundle::One(found.pop().expect("len == 1")),
1176 _ => ContextMessageBundle::Many(found.into_iter().map(|b| b.commons).collect()),
1177 }
1178}
1179
1180#[cfg(test)]
1181mod detect_context_message_bundle_tests {
1182 use super::*;
1183 use bynk_syntax::ast::{
1184 Annotation, Commons, CommonsForm, Context, MessagesDecl, QualifiedName, SourceUnit,
1185 UsesDecl,
1186 };
1187
1188 fn ident(name: &str) -> Ident {
1189 Ident {
1190 name: name.to_string(),
1191 span: Span::default(),
1192 }
1193 }
1194
1195 fn qualified(name: &str) -> QualifiedName {
1196 QualifiedName {
1197 parts: name.split('.').map(ident).collect(),
1198 span: Span::default(),
1199 }
1200 }
1201
1202 fn commons_with_messages(name: &str, tag: &str, is_reference: bool) -> ParsedFile {
1207 let annotations = if is_reference {
1208 vec![Annotation {
1209 name: ident("reference"),
1210 args: Vec::new(),
1211 span: Span::default(),
1212 }]
1213 } else {
1214 Vec::new()
1215 };
1216 let messages = MessagesDecl {
1217 tag: tag.to_string(),
1218 tag_span: Span::default(),
1219 annotations,
1220 entries: Vec::new(),
1221 documentation: None,
1222 span: Span::default(),
1223 trivia: Trivia::default(),
1224 };
1225 ParsedFile::new(
1226 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
1227 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
1228 None,
1229 String::new(),
1230 SourceUnit::Commons(Commons {
1231 name: qualified(name),
1232 items: vec![CommonsItem::Messages(messages)],
1233 uses: Vec::new(),
1234 documentation: None,
1235 form: CommonsForm::Brace,
1236 span: Span::default(),
1237 trivia: Trivia::default(),
1238 trailing_comments: Vec::new(),
1239 }),
1240 UnitKind::Commons,
1241 false,
1242 )
1243 }
1244
1245 fn context_using(name: &str, targets: &[&str]) -> ParsedFile {
1248 ParsedFile::new(
1249 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
1250 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
1251 None,
1252 String::new(),
1253 SourceUnit::Context(Context {
1254 name: qualified(name),
1255 uses: targets
1256 .iter()
1257 .map(|t| UsesDecl {
1258 target: qualified(t),
1259 span: Span::default(),
1260 trivia: Trivia::default(),
1261 })
1262 .collect(),
1263 consumes: Vec::new(),
1264 exports: Vec::new(),
1265 items: Vec::new(),
1266 documentation: None,
1267 form: CommonsForm::Brace,
1268 span: Span::default(),
1269 trivia: Trivia::default(),
1270 trailing_comments: Vec::new(),
1271 }),
1272 UnitKind::Context,
1273 false,
1274 )
1275 }
1276
1277 struct Scenario {
1281 parsed: Vec<ParsedFile>,
1282 groups: BTreeMap<String, Vec<usize>>,
1283 kinds: BTreeMap<String, UnitKind>,
1284 unit_uses: HashMap<String, Vec<String>>,
1285 }
1286
1287 fn scenario(ctx_name: &str, ctx_uses: &[&str], bundles: Vec<(&str, ParsedFile)>) -> Scenario {
1289 let mut parsed = vec![context_using(ctx_name, ctx_uses)];
1290 let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1291 let mut kinds: BTreeMap<String, UnitKind> = BTreeMap::new();
1292 groups.insert(ctx_name.to_string(), vec![0]);
1293 kinds.insert(ctx_name.to_string(), UnitKind::Context);
1294 for (name, pf) in bundles {
1295 let idx = parsed.len();
1296 parsed.push(pf);
1297 groups.entry(name.to_string()).or_default().push(idx);
1298 kinds.insert(name.to_string(), UnitKind::Commons);
1299 }
1300 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
1301 unit_uses.insert(
1302 ctx_name.to_string(),
1303 ctx_uses.iter().map(|s| s.to_string()).collect(),
1304 );
1305 Scenario {
1306 parsed,
1307 groups,
1308 kinds,
1309 unit_uses,
1310 }
1311 }
1312
1313 #[test]
1314 fn zero_bundles_when_uses_reaches_no_messages_commons() {
1315 let Scenario {
1316 parsed,
1317 groups,
1318 kinds,
1319 unit_uses,
1320 } = scenario("app.web", &["app.other"], vec![]);
1321 assert!(matches!(
1322 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1323 ContextMessageBundle::None
1324 ));
1325 }
1326
1327 #[test]
1328 fn zero_bundles_when_uses_is_empty() {
1329 let Scenario {
1330 parsed,
1331 groups,
1332 kinds,
1333 unit_uses,
1334 } = scenario("app.web", &[], vec![]);
1335 assert!(matches!(
1336 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1337 ContextMessageBundle::None
1338 ));
1339 }
1340
1341 #[test]
1342 fn one_bundle_is_found_by_its_reference_block() {
1343 let bundle = commons_with_messages("app.msgs", "en", true);
1344 let Scenario {
1345 parsed,
1346 groups,
1347 kinds,
1348 unit_uses,
1349 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
1350 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
1351 let ContextMessageBundle::One(info) = found else {
1352 panic!("expected exactly one bundle");
1353 };
1354 assert_eq!(info.commons, "app.msgs");
1355 assert_eq!(info.source_path, PathBuf::from("app/msgs.bynk"));
1356 }
1357
1358 #[test]
1359 fn a_bundle_missing_its_reference_block_is_not_counted() {
1360 let bundle = commons_with_messages("app.msgs", "en", false);
1364 let Scenario {
1365 parsed,
1366 groups,
1367 kinds,
1368 unit_uses,
1369 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
1370 assert!(matches!(
1371 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1372 ContextMessageBundle::None
1373 ));
1374 }
1375
1376 #[test]
1377 fn two_bundles_report_both_commons_names() {
1378 let a = commons_with_messages("app.msgs_a", "en", true);
1379 let b = commons_with_messages("app.msgs_b", "en", true);
1380 let Scenario {
1381 parsed,
1382 groups,
1383 kinds,
1384 unit_uses,
1385 } = scenario(
1386 "app.web",
1387 &["app.msgs_a", "app.msgs_b"],
1388 vec![("app.msgs_a", a), ("app.msgs_b", b)],
1389 );
1390 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
1391 let ContextMessageBundle::Many(names) = found else {
1392 panic!("expected two bundles");
1393 };
1394 let mut names = names;
1395 names.sort();
1396 assert_eq!(
1397 names,
1398 vec!["app.msgs_a".to_string(), "app.msgs_b".to_string()]
1399 );
1400 }
1401
1402 #[test]
1403 fn a_commons_reached_only_transitively_is_not_counted() {
1404 let bundle = commons_with_messages("app.msgs", "en", true);
1408 let mut mid = context_using("app.mid", &["app.msgs"]);
1409 mid.set_kind(UnitKind::Commons);
1415 let Scenario {
1416 mut parsed,
1417 mut groups,
1418 mut kinds,
1419 unit_uses,
1420 } = scenario("app.web", &["app.mid"], vec![("app.msgs", bundle)]);
1421 let mid_idx = parsed.len();
1422 parsed.push(mid);
1423 groups.insert("app.mid".to_string(), vec![mid_idx]);
1424 kinds.insert("app.mid".to_string(), UnitKind::Commons);
1425 assert!(matches!(
1426 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1427 ContextMessageBundle::None
1428 ));
1429 }
1430}
1431
1432pub fn consumes_span_of(
1435 parsed: &[ParsedFile],
1436 indices: &[usize],
1437 target: &str,
1438) -> Option<(usize, Span)> {
1439 for &i in indices {
1440 for c in parsed[i].consumes() {
1441 if c.target.joined() == target {
1442 return Some((i, c.span));
1443 }
1444 }
1445 }
1446 None
1447}
1448
1449pub fn parsed_alias_span(
1452 parsed: &[ParsedFile],
1453 indices: &[usize],
1454 alias: &str,
1455) -> Option<(usize, Span)> {
1456 for &i in indices {
1457 for c in parsed[i].consumes() {
1458 if let Some(a) = &c.alias
1459 && a.name == alias
1460 {
1461 return Some((i, a.span));
1462 }
1463 }
1464 }
1465 None
1466}
1467
1468#[derive(Debug, Clone)]
1471pub struct ConsumedType {
1472 pub owning_context: String,
1473 pub visibility: Visibility,
1474}