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