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 pub flattened_caps: HashMap<String, String>,
329}
330
331pub fn record_flattened_caps(
334 unit_tables: &mut HashMap<String, UnitTable>,
335 unit_flattened: &HashMap<String, HashMap<String, String>>,
336) {
337 for (name, flattened) in unit_flattened {
338 if let Some(t) = unit_tables.get_mut(name) {
339 t.flattened_caps = flattened.clone();
340 }
341 }
342}
343
344pub fn build_unit_table(
350 _name: &str,
351 kind: UnitKind,
352 indices: &[usize],
353 parsed: &[ParsedFile],
354 out: &mut Vec<(PathBuf, CompileError)>,
355) -> UnitTable {
356 let mut table = UnitTable {
357 kind: Some(kind),
358 ..UnitTable::default()
359 };
360 for &i in indices {
361 let mut errors: Vec<CompileError> = Vec::new();
362 for item in parsed[i].items() {
363 if let CommonsItem::Event(e) = item
370 && kind != UnitKind::Context
371 {
372 errors.push(CompileError::new(
373 "bynk.event.outside_context",
374 e.span,
375 "`event` declarations are only allowed inside a context",
376 ));
377 continue;
378 }
379 let as_type: Option<(&Ident, TypeDecl, bool)> = match item {
380 CommonsItem::Type(t) => Some((&t.name, t.clone(), false)),
381 CommonsItem::Event(e) => Some((&e.name, e.as_type_decl(), true)),
382 _ => None,
383 };
384 if let Some((name, decl, is_event)) = as_type {
385 if let Some(prev) = table.types.get(&name.name) {
386 errors.push(
387 CompileError::new(
388 "bynk.resolve.duplicate_type",
389 name.span,
390 format!("type `{}` is already declared", name.name),
391 )
392 .with_label(prev.name.span, "previously declared here"),
393 );
394 } else {
395 table.methods.entry(name.name.clone()).or_default();
396 if is_event {
397 let CommonsItem::Event(e) = item else {
398 unreachable!("is_event only set for CommonsItem::Event")
399 };
400 table.events.insert(name.name.clone(), e.clone());
401 }
402 table.types.insert(name.name.clone(), Arc::new(decl));
403 }
404 }
405 }
406 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
407 }
408 for &i in indices {
411 {
412 for clause in parsed[i].exports() {
413 if matches!(clause.kind, ExportKind::Capability) {
414 for n in clause.names.iter().map(|e| &e.name) {
415 table.exported_capabilities.insert(n.name.clone());
416 }
417 }
418 }
419 }
420 }
421 for &i in indices {
423 let mut errors: Vec<CompileError> = Vec::new();
424 for item in parsed[i].items() {
425 match item {
426 CommonsItem::Capability(c) => {
427 if kind != UnitKind::Context && kind != UnitKind::Adapter {
428 errors.push(CompileError::new(
429 "bynk.capability.outside_context",
430 c.span,
431 "`capability` declarations are only allowed inside a context or adapter",
432 ));
433 continue;
434 }
435 if let Some(prev) = table.capabilities.get(&c.name.name) {
436 errors.push(
437 CompileError::new(
438 "bynk.resolve.duplicate_capability",
439 c.name.span,
440 format!("capability `{}` is already declared", c.name.name),
441 )
442 .with_label(prev.name.span, "previously declared here"),
443 );
444 } else {
445 table.capabilities.insert(c.name.name.clone(), c.clone());
446 }
447 }
448 CommonsItem::Provider(p) => {
449 match kind {
450 UnitKind::Context => {
451 if p.external {
454 errors.push(CompileError::new(
455 "bynk.context.external_provider",
456 p.span,
457 "an external (bodiless) provider is only allowed inside an `adapter` — a context provider must have a Bynk body",
458 ));
459 continue;
460 }
461 }
462 UnitKind::Adapter => {
463 if !p.external {
466 errors.push(CompileError::new(
467 "bynk.adapter.provider_has_body",
468 p.span,
469 "a provider inside an `adapter` must be external (no body) — its implementation is supplied by the binding",
470 ));
471 continue;
472 }
473 }
474 _ => continue,
478 }
479 if let Some(prev) = table.providers.get(&p.capability.name) {
480 errors.push(
481 CompileError::new(
482 "bynk.resolve.duplicate_provider",
483 p.span,
484 format!(
485 "capability `{}` already has a provider in this context",
486 p.capability.name
487 ),
488 )
489 .with_label(prev.span, "previously provided here"),
490 );
491 } else {
492 table.providers.insert(p.capability.name.clone(), p.clone());
493 }
494 }
495 CommonsItem::Service(s) => {
496 if kind == UnitKind::Adapter {
497 errors.push(CompileError::new(
498 "bynk.adapter.disallowed_item",
499 s.span,
500 "an `adapter` may not declare a `service` — adapters contain only capabilities, boundary types, external providers, and helpers",
501 ));
502 continue;
503 }
504 if kind != UnitKind::Context {
510 continue;
511 }
512 if let Some(prev) = table.services.get(&s.name.name) {
513 errors.push(
514 CompileError::new(
515 "bynk.resolve.duplicate_service",
516 s.name.span,
517 format!("service `{}` is already declared", s.name.name),
518 )
519 .with_label(prev.name.span, "previously declared here"),
520 );
521 } else {
522 table.services.insert(s.name.name.clone(), s.clone());
523 }
524 }
525 CommonsItem::Agent(a) => {
526 if kind == UnitKind::Adapter {
527 errors.push(CompileError::new(
528 "bynk.adapter.disallowed_item",
529 a.span,
530 "an `adapter` may not declare an `agent` — adapters contain only capabilities, boundary types, external providers, and helpers",
531 ));
532 continue;
533 }
534 if kind != UnitKind::Context {
540 continue;
541 }
542 if let Some(prev) = table.agents.get(&a.name.name) {
543 errors.push(
544 CompileError::new(
545 "bynk.resolve.duplicate_agent",
546 a.name.span,
547 format!("agent `{}` is already declared", a.name.name),
548 )
549 .with_label(prev.name.span, "previously declared here"),
550 );
551 } else {
552 table.agents.insert(a.name.name.clone(), a.clone());
553 }
554 }
555 CommonsItem::Actor(a) => {
556 if kind == UnitKind::Adapter {
557 errors.push(CompileError::new(
558 "bynk.adapter.disallowed_item",
559 a.span,
560 "an `adapter` may not declare an `actor` — adapters contain only capabilities, boundary types, external providers, and helpers",
561 ));
562 continue;
563 }
564 if let Some(prev) = table.actors.get(&a.name.name) {
565 errors.push(
566 CompileError::new(
567 "bynk.resolve.duplicate_actor",
568 a.name.span,
569 format!("actor `{}` is already declared", a.name.name),
570 )
571 .with_label(prev.name.span, "previously declared here"),
572 );
573 } else {
574 table.actors.insert(a.name.name.clone(), a.clone());
575 }
576 }
577 _ => {}
578 }
579 }
580 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
581 }
582 for &i in indices {
583 let mut errors: Vec<CompileError> = Vec::new();
584 for item in parsed[i].items() {
585 let CommonsItem::Fn(f) = item else { continue };
586 match &f.name {
587 FnName::Free(id) => {
588 if let Some(prev) = table.fns.get(&id.name) {
589 errors.push(
590 CompileError::new(
591 "bynk.resolve.duplicate_fn",
592 id.span,
593 format!("function `{}` is already declared", id.name),
594 )
595 .with_label(prev.name.ident().span, "previously declared here"),
596 );
597 } else if let Some(prev) = table.types.get(&id.name) {
598 errors.push(
599 CompileError::new(
600 "bynk.resolve.name_conflict",
601 id.span,
602 format!(
603 "function `{}` conflicts with a type of the same name",
604 id.name
605 ),
606 )
607 .with_label(prev.name.span, "type declared here"),
608 );
609 } else {
610 table.fns.insert(id.name.clone(), Arc::new(f.clone()));
611 }
612 }
613 FnName::Method {
614 type_name,
615 method_name,
616 } => {
617 if !table.types.contains_key(&type_name.name) {
618 errors.push(
619 CompileError::new(
620 "bynk.resolve.method_unknown_type",
621 type_name.span,
622 format!(
623 "method `{}.{}` attached to an unknown type `{}`",
624 type_name.name, method_name.name, type_name.name
625 ),
626 )
627 .with_note(
628 "methods can only be declared on types defined in the same commons or context (across all of its files)",
629 ),
630 );
631 continue;
632 }
633 let mt = table.methods.entry(type_name.name.clone()).or_default();
634 let bucket = if f.has_self {
635 &mut mt.instance
636 } else {
637 &mut mt.statics
638 };
639 if let Some(prev) = bucket.get(&method_name.name) {
640 errors.push(
641 CompileError::new(
642 "bynk.resolve.duplicate_method",
643 method_name.span,
644 format!(
645 "method `{}.{}` is already declared",
646 type_name.name, method_name.name
647 ),
648 )
649 .with_label(prev.name.ident().span, "previously declared here"),
650 );
651 } else {
652 bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
653 }
654 }
655 }
656 }
657 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
658 }
659 if kind == UnitKind::Commons
674 && let Some(m) = indices.iter().find_map(|&i| {
675 parsed[i].items().iter().find_map(|item| match item {
676 CommonsItem::Messages(m) => Some(m),
677 _ => None,
678 })
679 })
680 {
681 if let Some(prev) = table.fns.get("render") {
682 out.push((
683 parsed[indices[0]].identity_path(),
684 CompileError::new(
685 "bynk.resolve.duplicate_fn",
686 m.span,
687 "function `render` is already declared",
688 )
689 .with_label(prev.name.ident().span, "previously declared here")
690 .with_note(
691 "a `messages` block in this commons implicitly declares its own \
692 `render(tag, msg) -> String` — name it something else",
693 ),
694 ));
695 } else {
696 table
697 .fns
698 .insert("render".to_string(), Arc::new(synthetic_render_fn()));
699 }
700 }
701 table
702}
703
704fn synthetic_render_fn() -> FnDecl {
709 let span = Span::default();
710 FnDecl {
711 type_params: Vec::new(),
712 name: FnName::Free(Ident {
713 name: "render".to_string(),
714 span,
715 }),
716 params: vec![
717 Param {
718 name: Ident {
719 name: "tag".to_string(),
720 span,
721 },
722 type_ref: TypeRef::Named(Ident {
723 name: "LocaleTag".to_string(),
724 span,
725 }),
726 span,
727 },
728 Param {
729 name: Ident {
730 name: "msg".to_string(),
731 span,
732 },
733 type_ref: TypeRef::Named(Ident {
734 name: "Message".to_string(),
735 span,
736 }),
737 span,
738 },
739 ],
740 return_type: TypeRef::Base(BaseType::String, span),
741 requires: Vec::new(),
742 ensures: Vec::new(),
743 body: Block {
744 statements: Vec::new(),
745 tail: Box::new(Expr {
746 id: ExprId::SYNTHETIC,
747 kind: ExprKind::StrLit(String::new()),
748 span,
749 }),
750 span,
751 tail_leading_comments: Vec::new(),
752 implicit_tail: false,
753 },
754 has_self: false,
755 documentation: None,
756 span,
757 trivia: Trivia::default(),
758 }
759}
760
761#[derive(Clone)]
764pub struct FileDeclIndex {
765 pub types: HashMap<String, PathBuf>,
766 pub fns: HashMap<String, PathBuf>,
767 pub methods: HashMap<String, HashMap<String, PathBuf>>,
768}
769
770pub fn build_file_decl_index(indices: &[usize], parsed: &[ParsedFile]) -> FileDeclIndex {
778 let mut idx = FileDeclIndex {
779 types: HashMap::new(),
780 fns: HashMap::new(),
781 methods: HashMap::new(),
782 };
783 for &i in indices {
784 let path = parsed[i].source_path();
785 for item in parsed[i].items() {
786 match item {
787 CommonsItem::Type(t) => {
788 idx.types
789 .entry(t.name.name.clone())
790 .or_insert_with(|| path.clone());
791 }
792 CommonsItem::Event(e) => {
797 idx.types
798 .entry(e.name.name.clone())
799 .or_insert_with(|| path.clone());
800 }
801 CommonsItem::Fn(f) => match &f.name {
802 FnName::Free(id) => {
803 idx.fns
804 .entry(id.name.clone())
805 .or_insert_with(|| path.clone());
806 }
807 FnName::Method {
808 type_name,
809 method_name,
810 } => {
811 idx.methods
812 .entry(type_name.name.clone())
813 .or_default()
814 .entry(method_name.name.clone())
815 .or_insert_with(|| path.clone());
816 }
817 },
818 CommonsItem::Capability(_)
819 | CommonsItem::Provider(_)
820 | CommonsItem::Service(_)
821 | CommonsItem::Agent(_)
822 | CommonsItem::Actor(_)
823 | CommonsItem::Messages(_) => {}
826 }
827 }
828 }
829 idx
830}
831
832pub fn uses_span_of(
835 parsed: &[ParsedFile],
836 indices: &[usize],
837 target: &str,
838) -> Option<(usize, Span)> {
839 for &i in indices {
840 for u in parsed[i].uses() {
841 if u.target.joined() == target {
842 return Some((i, u.span));
843 }
844 }
845 }
846 None
847}
848
849pub fn build_cross_context_info(
853 name: &str,
854 unit_consumes: &HashMap<String, Vec<String>>,
855 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
856 unit_uses: &HashMap<String, Vec<String>>,
857 unit_tables: &HashMap<String, UnitTable>,
858) -> resolver::CrossContextInfo {
859 let consumed_contexts: Vec<String> = unit_consumes.get(name).cloned().unwrap_or_default();
860 let aliases: HashMap<String, String> =
861 unit_consumes_aliases.get(name).cloned().unwrap_or_default();
862 let mut consumed_services: HashMap<String, HashMap<String, resolver::CrossContextService>> =
863 HashMap::new();
864 let mut consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>> = HashMap::new();
865 let mut consumed_capabilities: HashMap<
866 String,
867 HashMap<String, resolver::CrossContextCapability>,
868 > = HashMap::new();
869 let mut consumed_event_names: HashMap<String, std::collections::HashSet<String>> =
874 HashMap::new();
875 for t in &consumed_contexts {
876 let other_types_combined = combined_types_for(t, unit_tables, unit_uses);
877 consumed_types.insert(t.clone(), other_types_combined.clone());
878 let Some(other_table) = unit_tables.get(t) else {
879 continue;
880 };
881 consumed_event_names.insert(t.clone(), other_table.events.keys().cloned().collect());
882 let mut svcs: HashMap<String, resolver::CrossContextService> = HashMap::new();
883 for (sname, sdecl) in &other_table.services {
884 if let Some(svc) = resolver::cross_context_service_for(sname, sdecl) {
885 svcs.insert(sname.clone(), svc);
886 }
887 }
888 consumed_services.insert(t.clone(), svcs);
889
890 let mut caps: HashMap<String, resolver::CrossContextCapability> = HashMap::new();
893 for cap_name in &other_table.exported_capabilities {
894 let Some(decl) = other_table.capabilities.get(cap_name) else {
895 continue;
896 };
897 let Some(provider) = other_table.providers.get(cap_name) else {
898 continue;
899 };
900 let ops = decl
901 .ops
902 .iter()
903 .map(|op| resolver::CrossContextCapabilityOp {
904 name: op.name.name.clone(),
905 type_params: op.type_params.iter().map(|p| p.name.name.clone()).collect(),
906 params: op
907 .params
908 .iter()
909 .map(|p| (p.name.name.clone(), p.type_ref.clone()))
910 .collect(),
911 return_type: op.return_type.clone(),
912 })
913 .collect();
914 caps.insert(
915 cap_name.clone(),
916 resolver::CrossContextCapability {
917 name: cap_name.clone(),
918 ops,
919 provider_name: provider.provider_name.name.clone(),
920 provider_given: provider
921 .given
922 .iter()
923 .filter(|c| !c.is_cross_context())
924 .map(|c| c.key().to_string())
925 .collect(),
926 span: decl.span,
927 },
928 );
929 }
930 consumed_capabilities.insert(t.clone(), caps);
931 }
932 resolver::CrossContextInfo {
933 self_context: Some(name.to_string()),
934 consumed_contexts,
935 aliases,
936 consumed_services,
937 consumed_types,
938 consumed_capabilities,
939 flattened_caps: unit_tables
941 .get(name)
942 .map(|t| t.flattened_caps.clone())
943 .unwrap_or_default(),
944 consumed_event_names,
945 }
946}
947
948pub fn discover_event_subscribers(
961 unit_tables: &HashMap<String, UnitTable>,
962 unit_consumes: &HashMap<String, Vec<String>>,
963) -> BTreeMap<(String, String), Vec<(String, String)>> {
964 let mut out: BTreeMap<(String, String), Vec<(String, String)>> = BTreeMap::new();
965 for (ctx_name, table) in unit_tables {
966 for (svc_name, svc) in &table.services {
967 let ServiceProtocol::Events { event_type, .. } = &svc.protocol else {
968 continue;
969 };
970 let TypeRef::Named(id) = event_type else {
971 continue;
972 };
973 let name = &id.name;
974 let owner = if table.events.contains_key(name) {
975 Some(ctx_name.clone())
976 } else {
977 unit_consumes.get(ctx_name).and_then(|consumed| {
978 consumed
979 .iter()
980 .find(|c| {
981 unit_tables
982 .get(c.as_str())
983 .is_some_and(|t| t.events.contains_key(name))
984 })
985 .cloned()
986 })
987 };
988 if let Some(owner) = owner {
989 out.entry((owner, name.clone()))
990 .or_default()
991 .push((ctx_name.clone(), svc_name.clone()));
992 }
993 }
994 }
995 for subs in out.values_mut() {
1000 subs.sort();
1001 }
1002 out
1003}
1004
1005pub fn cron_and_queue_triggers(table: &UnitTable) -> (Vec<String>, Vec<String>) {
1010 let mut crons: Vec<String> = Vec::new();
1011 let mut queues: Vec<String> = Vec::new();
1012 for service in table.services.values() {
1013 for handler in &service.handlers {
1014 if let HandlerKind::Cron { expr } = &handler.kind {
1015 crons.push(expr.clone());
1016 }
1017 }
1018 if let ServiceProtocol::Queue { name } = &service.protocol {
1019 queues.push(name.clone());
1020 }
1021 }
1022 crons.sort();
1023 crons.dedup();
1024 queues.sort();
1025 queues.dedup();
1026 (crons, queues)
1027}
1028
1029pub fn record_capability_clause_ref(
1039 name: &Ident,
1040 cross_context: &resolver::CrossContextInfo,
1041 refs: &mut RefSink,
1042) {
1043 record_capability_clause_ref_inner(name, cross_context, refs, false);
1044}
1045
1046pub fn record_provides_clause_ref(
1051 name: &Ident,
1052 cross_context: &resolver::CrossContextInfo,
1053 refs: &mut RefSink,
1054) {
1055 record_capability_clause_ref_inner(name, cross_context, refs, true);
1056}
1057
1058fn record_capability_clause_ref_inner(
1059 name: &Ident,
1060 cross_context: &resolver::CrossContextInfo,
1061 refs: &mut RefSink,
1062 provides: bool,
1063) {
1064 let unit = cross_context.flattened_caps.get(&name.name);
1065 if provides {
1066 refs.record_provides(name.span, &name.name, unit.map(String::as_str));
1067 } else if let Some(unit) = unit {
1068 refs.record_in_unit(name.span, SymbolKind::Capability, &name.name, unit);
1069 } else {
1070 refs.record(name.span, SymbolKind::Capability, &name.name);
1071 }
1072}
1073
1074pub fn resolve_given_cap_ref(
1075 cap_ref: &CapRef,
1076 capability_info_map: &HashMap<String, CapabilityInfo>,
1077 cross_context: &resolver::CrossContextInfo,
1078 errors: &mut Vec<CompileError>,
1079 refs: &mut RefSink,
1080) -> Option<CapabilityInfo> {
1081 let Some(prefix) = cap_ref.prefix() else {
1082 match capability_info_map.get(cap_ref.key()) {
1084 Some(info) => {
1085 record_capability_clause_ref(&cap_ref.name, cross_context, refs);
1086 return Some(info.clone());
1087 }
1088 None => {
1089 errors.push(CompileError::new(
1090 "bynk.given.unknown_capability",
1091 cap_ref.span,
1092 format!(
1093 "capability `{}` is not declared in this context",
1094 cap_ref.key()
1095 ),
1096 ));
1097 return None;
1098 }
1099 }
1100 };
1101 let Some(ctx_name) = cross_context.resolve_prefix(&prefix) else {
1103 errors.push(
1104 CompileError::new(
1105 "bynk.resolve.unconsumed_context",
1106 cap_ref.span,
1107 format!(
1108 "`given {}.{}` refers to a context that this context does not `consumes`",
1109 prefix,
1110 cap_ref.key()
1111 ),
1112 )
1113 .with_note(
1114 "add a `consumes` clause for the providing context (optionally with an alias) at the top of this context",
1115 ),
1116 );
1117 return None;
1118 };
1119 let exports_it = cross_context
1120 .consumed_capabilities
1121 .get(&ctx_name)
1122 .is_some_and(|m| m.contains_key(cap_ref.key()));
1123 if exports_it {
1124 refs.record_in_unit(
1127 cap_ref.name.span,
1128 SymbolKind::Capability,
1129 cap_ref.key(),
1130 &ctx_name,
1131 );
1132 }
1133 if !exports_it {
1134 errors.push(
1135 CompileError::new(
1136 "bynk.given.cross_context_unknown_capability",
1137 cap_ref.span,
1138 format!(
1139 "context `{}` does not export a capability named `{}`",
1140 ctx_name,
1141 cap_ref.key()
1142 ),
1143 )
1144 .with_note(
1145 "the providing context must list the capability in an `exports capability { … }` clause",
1146 ),
1147 );
1148 }
1149 None
1150}
1151
1152pub fn combined_types_for(
1165 unit: &str,
1166 unit_tables: &HashMap<String, UnitTable>,
1167 unit_uses: &HashMap<String, Vec<String>>,
1168) -> HashMap<String, Arc<TypeDecl>> {
1169 let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1170 if let Some(table) = unit_tables.get(unit) {
1171 for (n, d) in &table.types {
1172 out.insert(n.clone(), d.clone());
1173 }
1174 }
1175 if let Some(targets) = unit_uses.get(unit) {
1176 for t in targets {
1177 if let Some(used) = unit_tables.get(t) {
1178 for (n, d) in &used.types {
1179 out.entry(n.clone()).or_insert_with(|| d.clone());
1180 }
1181 }
1182 }
1183 }
1184 out
1185}
1186
1187pub fn combined_types_for_unit_info(
1198 unit: &str,
1199 unit_info: &BTreeMap<String, UnitInfo>,
1200) -> HashMap<String, Arc<TypeDecl>> {
1201 let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1202 let Some(info) = unit_info.get(unit) else {
1203 return out;
1204 };
1205 for (n, d) in &info.table.types {
1206 out.insert(n.clone(), d.clone());
1207 }
1208 for t in &info.uses {
1209 if let Some(used) = unit_info.get(t) {
1210 for (n, d) in &used.table.types {
1211 out.entry(n.clone()).or_insert_with(|| d.clone());
1212 }
1213 }
1214 }
1215 out
1216}
1217
1218pub enum ContextMessageBundle {
1231 None,
1233 One(MessageBundleInfo),
1235 Many(Vec<String>),
1237}
1238
1239pub struct MessageBundleInfo {
1240 pub commons: String,
1242 pub source_path: PathBuf,
1250}
1251
1252pub fn detect_context_message_bundle(
1258 ctx: &str,
1259 unit_uses: &HashMap<String, Vec<String>>,
1260 groups: &BTreeMap<String, Vec<usize>>,
1261 kinds: &BTreeMap<String, UnitKind>,
1262 parsed: &[ParsedFile],
1263) -> ContextMessageBundle {
1264 let mut found: Vec<MessageBundleInfo> = Vec::new();
1265 for target in unit_uses.get(ctx).into_iter().flatten() {
1266 if kinds.get(target) != Some(&UnitKind::Commons) {
1267 continue;
1268 }
1269 let Some(indices) = groups.get(target) else {
1270 continue;
1271 };
1272 for &i in indices {
1273 let has_reference = parsed[i].items().iter().any(|item| {
1274 matches!(item, CommonsItem::Messages(m) if m.annotations.iter().any(|a| a.name.name == "reference"))
1275 });
1276 if has_reference {
1277 found.push(MessageBundleInfo {
1278 commons: target.clone(),
1279 source_path: parsed[i].source_path(),
1280 });
1281 break;
1282 }
1283 }
1284 }
1285 match found.len() {
1286 0 => ContextMessageBundle::None,
1287 1 => ContextMessageBundle::One(found.pop().expect("len == 1")),
1288 _ => ContextMessageBundle::Many(found.into_iter().map(|b| b.commons).collect()),
1289 }
1290}
1291
1292#[cfg(test)]
1293mod detect_context_message_bundle_tests {
1294 use super::*;
1295 use bynk_syntax::ast::{
1296 Annotation, Commons, CommonsForm, Context, MessagesDecl, QualifiedName, SourceUnit,
1297 UsesDecl,
1298 };
1299
1300 fn ident(name: &str) -> Ident {
1301 Ident {
1302 name: name.to_string(),
1303 span: Span::default(),
1304 }
1305 }
1306
1307 fn qualified(name: &str) -> QualifiedName {
1308 QualifiedName {
1309 parts: name.split('.').map(ident).collect(),
1310 span: Span::default(),
1311 }
1312 }
1313
1314 fn commons_with_messages(name: &str, tag: &str, is_reference: bool) -> ParsedFile {
1319 let annotations = if is_reference {
1320 vec![Annotation {
1321 name: ident("reference"),
1322 args: Vec::new(),
1323 span: Span::default(),
1324 }]
1325 } else {
1326 Vec::new()
1327 };
1328 let messages = MessagesDecl {
1329 tag: tag.to_string(),
1330 tag_span: Span::default(),
1331 annotations,
1332 entries: Vec::new(),
1333 documentation: None,
1334 span: Span::default(),
1335 trivia: Trivia::default(),
1336 };
1337 ParsedFile::new(
1338 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
1339 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
1340 None,
1341 String::new(),
1342 SourceUnit::Commons(Commons {
1343 name: qualified(name),
1344 items: vec![CommonsItem::Messages(messages)],
1345 uses: Vec::new(),
1346 documentation: None,
1347 form: CommonsForm::Brace,
1348 span: Span::default(),
1349 trivia: Trivia::default(),
1350 trailing_comments: Vec::new(),
1351 }),
1352 UnitKind::Commons,
1353 false,
1354 )
1355 }
1356
1357 fn context_using(name: &str, targets: &[&str]) -> ParsedFile {
1360 ParsedFile::new(
1361 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
1362 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
1363 None,
1364 String::new(),
1365 SourceUnit::Context(Context {
1366 name: qualified(name),
1367 uses: targets
1368 .iter()
1369 .map(|t| UsesDecl {
1370 target: qualified(t),
1371 span: Span::default(),
1372 trivia: Trivia::default(),
1373 })
1374 .collect(),
1375 consumes: Vec::new(),
1376 exports: Vec::new(),
1377 items: Vec::new(),
1378 documentation: None,
1379 form: CommonsForm::Brace,
1380 span: Span::default(),
1381 trivia: Trivia::default(),
1382 trailing_comments: Vec::new(),
1383 }),
1384 UnitKind::Context,
1385 false,
1386 )
1387 }
1388
1389 struct Scenario {
1393 parsed: Vec<ParsedFile>,
1394 groups: BTreeMap<String, Vec<usize>>,
1395 kinds: BTreeMap<String, UnitKind>,
1396 unit_uses: HashMap<String, Vec<String>>,
1397 }
1398
1399 fn scenario(ctx_name: &str, ctx_uses: &[&str], bundles: Vec<(&str, ParsedFile)>) -> Scenario {
1401 let mut parsed = vec![context_using(ctx_name, ctx_uses)];
1402 let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1403 let mut kinds: BTreeMap<String, UnitKind> = BTreeMap::new();
1404 groups.insert(ctx_name.to_string(), vec![0]);
1405 kinds.insert(ctx_name.to_string(), UnitKind::Context);
1406 for (name, pf) in bundles {
1407 let idx = parsed.len();
1408 parsed.push(pf);
1409 groups.entry(name.to_string()).or_default().push(idx);
1410 kinds.insert(name.to_string(), UnitKind::Commons);
1411 }
1412 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
1413 unit_uses.insert(
1414 ctx_name.to_string(),
1415 ctx_uses.iter().map(|s| s.to_string()).collect(),
1416 );
1417 Scenario {
1418 parsed,
1419 groups,
1420 kinds,
1421 unit_uses,
1422 }
1423 }
1424
1425 #[test]
1426 fn zero_bundles_when_uses_reaches_no_messages_commons() {
1427 let Scenario {
1428 parsed,
1429 groups,
1430 kinds,
1431 unit_uses,
1432 } = scenario("app.web", &["app.other"], vec![]);
1433 assert!(matches!(
1434 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1435 ContextMessageBundle::None
1436 ));
1437 }
1438
1439 #[test]
1440 fn zero_bundles_when_uses_is_empty() {
1441 let Scenario {
1442 parsed,
1443 groups,
1444 kinds,
1445 unit_uses,
1446 } = scenario("app.web", &[], vec![]);
1447 assert!(matches!(
1448 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1449 ContextMessageBundle::None
1450 ));
1451 }
1452
1453 #[test]
1454 fn one_bundle_is_found_by_its_reference_block() {
1455 let bundle = commons_with_messages("app.msgs", "en", true);
1456 let Scenario {
1457 parsed,
1458 groups,
1459 kinds,
1460 unit_uses,
1461 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
1462 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
1463 let ContextMessageBundle::One(info) = found else {
1464 panic!("expected exactly one bundle");
1465 };
1466 assert_eq!(info.commons, "app.msgs");
1467 assert_eq!(info.source_path, PathBuf::from("app/msgs.bynk"));
1468 }
1469
1470 #[test]
1471 fn a_bundle_missing_its_reference_block_is_not_counted() {
1472 let bundle = commons_with_messages("app.msgs", "en", false);
1476 let Scenario {
1477 parsed,
1478 groups,
1479 kinds,
1480 unit_uses,
1481 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
1482 assert!(matches!(
1483 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1484 ContextMessageBundle::None
1485 ));
1486 }
1487
1488 #[test]
1489 fn two_bundles_report_both_commons_names() {
1490 let a = commons_with_messages("app.msgs_a", "en", true);
1491 let b = commons_with_messages("app.msgs_b", "en", true);
1492 let Scenario {
1493 parsed,
1494 groups,
1495 kinds,
1496 unit_uses,
1497 } = scenario(
1498 "app.web",
1499 &["app.msgs_a", "app.msgs_b"],
1500 vec![("app.msgs_a", a), ("app.msgs_b", b)],
1501 );
1502 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
1503 let ContextMessageBundle::Many(names) = found else {
1504 panic!("expected two bundles");
1505 };
1506 let mut names = names;
1507 names.sort();
1508 assert_eq!(
1509 names,
1510 vec!["app.msgs_a".to_string(), "app.msgs_b".to_string()]
1511 );
1512 }
1513
1514 #[test]
1515 fn a_commons_reached_only_transitively_is_not_counted() {
1516 let bundle = commons_with_messages("app.msgs", "en", true);
1520 let mut mid = context_using("app.mid", &["app.msgs"]);
1521 mid.set_kind(UnitKind::Commons);
1527 let Scenario {
1528 mut parsed,
1529 mut groups,
1530 mut kinds,
1531 unit_uses,
1532 } = scenario("app.web", &["app.mid"], vec![("app.msgs", bundle)]);
1533 let mid_idx = parsed.len();
1534 parsed.push(mid);
1535 groups.insert("app.mid".to_string(), vec![mid_idx]);
1536 kinds.insert("app.mid".to_string(), UnitKind::Commons);
1537 assert!(matches!(
1538 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
1539 ContextMessageBundle::None
1540 ));
1541 }
1542}
1543
1544pub fn consumes_span_of(
1547 parsed: &[ParsedFile],
1548 indices: &[usize],
1549 target: &str,
1550) -> Option<(usize, Span)> {
1551 for &i in indices {
1552 for c in parsed[i].consumes() {
1553 if c.target.joined() == target {
1554 return Some((i, c.span));
1555 }
1556 }
1557 }
1558 None
1559}
1560
1561pub fn parsed_alias_span(
1564 parsed: &[ParsedFile],
1565 indices: &[usize],
1566 alias: &str,
1567) -> Option<(usize, Span)> {
1568 for &i in indices {
1569 for c in parsed[i].consumes() {
1570 if let Some(a) = &c.alias
1571 && a.name == alias
1572 {
1573 return Some((i, a.span));
1574 }
1575 }
1576 }
1577 None
1578}
1579
1580#[derive(Debug, Clone)]
1583pub struct ConsumedType {
1584 pub owning_context: String,
1585 pub visibility: Visibility,
1586}