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, AssetDecl, BaseType, Block, CapRef, CapabilityDecl, CommonsItem,
12 EmbedDecl, EventDecl, ExportKind, Expr, ExprId, ExprKind, FnDecl, FnName, HandlerKind, Ident,
13 Param, ProviderDecl, ServiceDecl, ServiceProtocol, Trivia, TypeBody, TypeDecl, TypeRef,
14 Visibility,
15};
16use bynk_syntax::error::CompileError;
17use bynk_syntax::span::Span;
18
19pub fn assemble_index(
25 parsed: &[ParsedFile],
26 unit_uses: &HashMap<String, Vec<String>>,
27 unit_consumes: &HashMap<String, Vec<String>>,
28 refs: RefSink,
29) -> ProjectIndex {
30 let mut builder = IndexBuilder::default();
31 let mut uses = unit_uses.clone();
32 uses.extend(refs.extra_uses);
33 builder.set_uses(uses);
34 builder.set_consumes(unit_consumes.clone());
35 for pf in parsed {
36 if matches!(pf.kind(), UnitKind::Test | UnitKind::Integration) {
37 continue;
38 }
39 let unit = pf.unit().name().joined();
40 if pf.is_synthetic() {
45 for item in pf.items() {
46 let (kind, name, modifiers) = match item {
47 CommonsItem::Type(t) => (
48 SymbolKind::Type,
49 &t.name.name,
50 symbol_modifiers(&unit, Some(t)),
51 ),
52 CommonsItem::Event(e) => (
58 SymbolKind::Type,
59 &e.name.name,
60 symbol_modifiers(&unit, None),
61 ),
62 CommonsItem::Fn(f) => match &f.name {
63 FnName::Free(id) => {
64 (SymbolKind::Fn, &id.name, symbol_modifiers(&unit, None))
65 }
66 FnName::Method { .. } => continue,
67 },
68 CommonsItem::Capability(c) => (
69 SymbolKind::Capability,
70 &c.name.name,
71 symbol_modifiers(&unit, None),
72 ),
73 CommonsItem::Service(s) => (
74 SymbolKind::Service,
75 &s.name.name,
76 symbol_modifiers(&unit, None),
77 ),
78 CommonsItem::Agent(a) => (
79 SymbolKind::Agent,
80 &a.name.name,
81 symbol_modifiers(&unit, None),
82 ),
83 CommonsItem::Provider(p) => (
84 SymbolKind::Provider,
85 &p.provider_name.name,
86 symbol_modifiers(&unit, None),
87 ),
88 CommonsItem::Actor(a) => (
89 SymbolKind::Actor,
90 &a.name.name,
91 symbol_modifiers(&unit, None),
92 ),
93 CommonsItem::Messages(m) => {
94 (SymbolKind::Messages, &m.tag, symbol_modifiers(&unit, None))
95 }
96 CommonsItem::Asset(a) => (
99 SymbolKind::Asset,
100 &a.name.name,
101 symbol_modifiers(&unit, None),
102 ),
103 CommonsItem::Embed(e) => (
106 SymbolKind::Embed,
107 &e.name.name,
108 symbol_modifiers(&unit, None),
109 ),
110 };
111 builder.add_first_party_def(&unit, kind, name, modifiers);
112 }
113 continue;
114 }
115 let site = |id: &Ident| SiteRef {
116 path: pf.identity_path(),
117 span: id.span,
118 };
119 for item in pf.items() {
120 match item {
121 CommonsItem::Type(t) => {
122 builder.add_def(
123 &unit,
124 SymbolKind::Type,
125 &t.name.name,
126 site(&t.name),
127 symbol_modifiers(&unit, Some(t)),
128 );
129 if let TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } = &t.body
134 {
135 builder.add_refinement(&unit, &t.name.name, *base);
136 }
137 if let TypeBody::Record(r) = &t.body {
140 for field in &r.fields {
141 builder.add_def(
142 &unit,
143 SymbolKind::Field,
144 &format!("{}.{}", t.name.name, field.name.name),
145 site(&field.name),
146 symbol_modifiers(&unit, None),
147 );
148 }
149 }
150 }
151 CommonsItem::Event(e) => {
157 builder.add_def(
158 &unit,
159 SymbolKind::Type,
160 &e.name.name,
161 site(&e.name),
162 symbol_modifiers(&unit, None),
163 );
164 for field in &e.body.fields {
165 builder.add_def(
166 &unit,
167 SymbolKind::Field,
168 &format!("{}.{}", e.name.name, field.name.name),
169 site(&field.name),
170 symbol_modifiers(&unit, None),
171 );
172 }
173 }
174 CommonsItem::Fn(f) => match &f.name {
175 FnName::Free(id) => {
176 builder.add_def(
177 &unit,
178 SymbolKind::Fn,
179 &id.name,
180 site(id),
181 symbol_modifiers(&unit, None),
182 );
183 }
184 FnName::Method { .. } => {
185 builder.add_owner(&unit, &f.name.display(), &pf.identity_path());
189 builder.add_def(
190 &unit,
191 SymbolKind::Method,
192 &f.name.display(),
193 site(f.name.ident()),
194 symbol_modifiers(&unit, None),
195 );
196 }
197 },
198 CommonsItem::Capability(c) => {
199 builder.add_def(
200 &unit,
201 SymbolKind::Capability,
202 &c.name.name,
203 site(&c.name),
204 symbol_modifiers(&unit, None),
205 );
206 for op in &c.ops {
209 builder.add_def(
210 &unit,
211 SymbolKind::CapabilityOp,
212 &format!("{}.{}", c.name.name, op.name.name),
213 site(&op.name),
214 symbol_modifiers(&unit, None),
215 );
216 }
217 }
218 CommonsItem::Service(s) => {
219 builder.add_def(
220 &unit,
221 SymbolKind::Service,
222 &s.name.name,
223 site(&s.name),
224 symbol_modifiers(&unit, None),
225 );
226 }
227 CommonsItem::Agent(a) => {
228 builder.add_def(
229 &unit,
230 SymbolKind::Agent,
231 &a.name.name,
232 site(&a.name),
233 symbol_modifiers(&unit, None),
234 );
235 for h in &a.handlers {
241 if let Some(name) = &h.method_name {
242 builder.add_def(
243 &unit,
244 SymbolKind::Handler,
245 &format!("{}.{}", a.name.name, name.name),
246 site(name),
247 symbol_modifiers(&unit, None),
248 );
249 }
250 }
251 }
252 CommonsItem::Provider(p) => {
253 builder.add_def(
254 &unit,
255 SymbolKind::Provider,
256 &p.provider_name.name,
257 site(&p.provider_name),
258 symbol_modifiers(&unit, None),
259 );
260 }
261 CommonsItem::Actor(a) => {
262 builder.add_def(
263 &unit,
264 SymbolKind::Actor,
265 &a.name.name,
266 site(&a.name),
267 symbol_modifiers(&unit, None),
268 );
269 }
270 CommonsItem::Messages(m) => {
271 builder.add_def(
274 &unit,
275 SymbolKind::Messages,
276 &m.tag,
277 SiteRef {
278 path: pf.identity_path(),
279 span: m.tag_span,
280 },
281 symbol_modifiers(&unit, None),
282 );
283 }
284 CommonsItem::Asset(a) => {
291 builder.add_leaf_def(
292 &unit,
293 SymbolKind::Asset,
294 &a.name.name,
295 site(&a.name),
296 symbol_modifiers(&unit, None),
297 );
298 }
299 CommonsItem::Embed(e) => {
303 builder.add_leaf_def(
304 &unit,
305 SymbolKind::Embed,
306 &e.name.name,
307 site(&e.name),
308 symbol_modifiers(&unit, None),
309 );
310 }
311 }
312 }
313 }
314 builder.build(refs.edges)
315}
316
317fn symbol_modifiers(unit: &str, type_decl: Option<&TypeDecl>) -> crate::index::SymbolModifiers {
323 let (refined, opaque) = match type_decl.map(|t| &t.body) {
324 Some(TypeBody::Refined { refinement, .. }) => (refinement.is_some(), false),
325 Some(TypeBody::Opaque { refinement, .. }) => (refinement.is_some(), true),
326 _ => (false, false),
327 };
328 crate::index::SymbolModifiers {
329 refined,
330 opaque,
331 platform_native: crate::firstparty::platform_of(unit).is_some(),
332 }
333}
334
335#[derive(Clone, Default)]
337pub struct UnitTable {
338 pub kind: Option<UnitKind>,
339 pub types: HashMap<String, Arc<TypeDecl>>,
340 pub fns: HashMap<String, Arc<FnDecl>>,
341 pub methods: HashMap<String, ResolverMethodTable>,
342 pub capabilities: HashMap<String, CapabilityDecl>,
344 pub providers: HashMap<String, ProviderDecl>,
347 pub services: HashMap<String, ServiceDecl>,
349 pub agents: HashMap<String, AgentDecl>,
351 pub removed_agents: Vec<bynk_syntax::ast::RemovedAgentDecl>,
354 pub actors: HashMap<String, ActorDecl>,
356 pub exported_capabilities: std::collections::HashSet<String>,
359 pub events: HashMap<String, EventDecl>,
368 pub assets: HashMap<String, AssetDecl>,
373 pub embeds: HashMap<String, EmbedDecl>,
378 pub flattened_caps: HashMap<String, String>,
383 pub derived: HashMap<String, DerivedInstance>,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct DerivedInstance {
396 pub base_unit: String,
398 pub base_cap: String,
400 pub binding: String,
402 pub options: Vec<(String, OptionValue)>,
406 pub span: bynk_syntax::span::Span,
408}
409
410pub fn is_edge_cache_base(unit: &str, capability: &str) -> bool {
413 unit == crate::firstparty::CLOUDFLARE_UNIT && capability == "EdgeCache"
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum OptionValue {
419 Str(String),
421 Int(i64),
423 Seconds(i64),
425}
426
427impl DerivedInstance {
428 pub fn option(&self, name: &str) -> Option<&str> {
430 self.options
431 .iter()
432 .find(|(o, _)| o == name)
433 .and_then(|(_, v)| match v {
434 OptionValue::Str(s) => Some(s.as_str()),
435 _ => None,
436 })
437 }
438
439 pub fn number(&self, name: &str) -> Option<i64> {
442 self.options
443 .iter()
444 .find(|(o, _)| o == name)
445 .and_then(|(_, v)| match v {
446 OptionValue::Int(n) | OptionValue::Seconds(n) => Some(*n),
447 OptionValue::Str(_) => None,
448 })
449 }
450
451 pub fn is_ratelimit(&self) -> bool {
453 self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "RateLimit"
454 }
455
456 pub fn is_edge_cache(&self) -> bool {
458 is_edge_cache_base(&self.base_unit, &self.base_cap)
459 }
460
461 pub fn rate_limit(&self) -> Option<(i64, i64)> {
464 if !self.is_ratelimit() {
465 return None;
466 }
467 Some((
468 self.number(crate::firstparty::RATELIMIT_LIMIT_OPTION)?,
469 self.number(crate::firstparty::RATELIMIT_PERIOD_OPTION)?,
470 ))
471 }
472
473 pub fn jurisdiction(&self) -> Option<&str> {
476 self.is_r2()
477 .then(|| self.option(crate::firstparty::R2_JURISDICTION_OPTION))
478 .flatten()
479 }
480
481 pub fn is_r2(&self) -> bool {
483 self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "R2"
484 }
485
486 pub fn worker(&self) -> Option<&str> {
489 (self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "Service")
490 .then(|| self.option(crate::firstparty::SERVICE_WORKER_OPTION))
491 .flatten()
492 }
493}
494
495pub fn record_flattened_caps(
498 unit_tables: &mut HashMap<String, UnitTable>,
499 unit_flattened: &HashMap<String, HashMap<String, String>>,
500) {
501 for (name, flattened) in unit_flattened {
502 if let Some(t) = unit_tables.get_mut(name) {
503 t.flattened_caps = flattened.clone();
504 }
505 }
506}
507
508pub fn build_unit_table(
514 _name: &str,
515 kind: UnitKind,
516 indices: &[usize],
517 parsed: &[ParsedFile],
518 out: &mut Vec<(PathBuf, CompileError)>,
519) -> UnitTable {
520 let mut table = UnitTable {
521 kind: Some(kind),
522 ..UnitTable::default()
523 };
524 for &i in indices {
525 let mut errors: Vec<CompileError> = Vec::new();
526 table
527 .removed_agents
528 .extend(parsed[i].removed_agents().iter().cloned());
529 for item in parsed[i].items() {
530 if let CommonsItem::Event(e) = item
537 && kind != UnitKind::Context
538 {
539 errors.push(CompileError::new(
540 "bynk.event.outside_context",
541 e.span,
542 "`event` declarations are only allowed inside a context",
543 ));
544 continue;
545 }
546 let as_type: Option<(&Ident, TypeDecl, bool)> = match item {
547 CommonsItem::Type(t) => Some((&t.name, t.clone(), false)),
548 CommonsItem::Event(e) => Some((&e.name, e.as_type_decl(), true)),
549 _ => None,
550 };
551 if let Some((name, decl, is_event)) = as_type {
552 if let Some(prev) = table.types.get(&name.name) {
553 errors.push(
554 CompileError::new(
555 "bynk.resolve.duplicate_type",
556 name.span,
557 format!("type `{}` is already declared", name.name),
558 )
559 .with_label(prev.name.span, "previously declared here"),
560 );
561 } else {
562 table.methods.entry(name.name.clone()).or_default();
563 if is_event {
564 let CommonsItem::Event(e) = item else {
565 unreachable!("is_event only set for CommonsItem::Event")
566 };
567 table.events.insert(name.name.clone(), e.clone());
568 }
569 table.types.insert(name.name.clone(), Arc::new(decl));
570 }
571 }
572 }
573 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
574 }
575 for &i in indices {
578 {
579 for clause in parsed[i].exports() {
580 if matches!(clause.kind, ExportKind::Capability) {
581 for n in clause.names.iter().map(|e| &e.name) {
582 table.exported_capabilities.insert(n.name.clone());
583 }
584 }
585 }
586 }
587 }
588 for &i in indices {
590 let mut errors: Vec<CompileError> = Vec::new();
591 for item in parsed[i].items() {
592 match item {
593 CommonsItem::Capability(c) => {
594 if kind != UnitKind::Context && kind != UnitKind::Adapter {
595 errors.push(CompileError::new(
596 "bynk.capability.outside_context",
597 c.span,
598 "`capability` declarations are only allowed inside a context or adapter",
599 ));
600 continue;
601 }
602 if let Some(prev) = table.capabilities.get(&c.name.name) {
603 errors.push(
604 CompileError::new(
605 "bynk.resolve.duplicate_capability",
606 c.name.span,
607 format!("capability `{}` is already declared", c.name.name),
608 )
609 .with_label(prev.name.span, "previously declared here"),
610 );
611 } else {
612 table.capabilities.insert(c.name.name.clone(), c.clone());
613 }
614 }
615 CommonsItem::Provider(p) => {
616 match kind {
617 UnitKind::Context => {
618 if p.external {
621 errors.push(CompileError::new(
622 "bynk.context.external_provider",
623 p.span,
624 "an external (bodiless) provider is only allowed inside an `adapter` — a context provider must have a Bynk body",
625 ));
626 continue;
627 }
628 }
629 UnitKind::Adapter => {
630 if !p.external {
633 errors.push(CompileError::new(
634 "bynk.adapter.provider_has_body",
635 p.span,
636 "a provider inside an `adapter` must be external (no body) — its implementation is supplied by the binding",
637 ));
638 continue;
639 }
640 }
641 _ => continue,
645 }
646 if let Some(prev) = table.providers.get(&p.capability.name) {
647 errors.push(
648 CompileError::new(
649 "bynk.resolve.duplicate_provider",
650 p.span,
651 format!(
652 "capability `{}` already has a provider in this context",
653 p.capability.name
654 ),
655 )
656 .with_label(prev.span, "previously provided here"),
657 );
658 } else {
659 table.providers.insert(p.capability.name.clone(), p.clone());
660 }
661 }
662 CommonsItem::Service(s) => {
663 if kind == UnitKind::Adapter {
664 errors.push(CompileError::new(
665 "bynk.adapter.disallowed_item",
666 s.span,
667 "an `adapter` may not declare a `service` — adapters contain only capabilities, boundary types, external providers, and helpers",
668 ));
669 continue;
670 }
671 if kind != UnitKind::Context {
677 continue;
678 }
679 if let Some(prev) = table.services.get(&s.name.name) {
680 errors.push(
681 CompileError::new(
682 "bynk.resolve.duplicate_service",
683 s.name.span,
684 format!("service `{}` is already declared", s.name.name),
685 )
686 .with_label(prev.name.span, "previously declared here"),
687 );
688 } else {
689 table.services.insert(s.name.name.clone(), s.clone());
690 }
691 }
692 CommonsItem::Agent(a) => {
693 if kind == UnitKind::Adapter {
694 errors.push(CompileError::new(
695 "bynk.adapter.disallowed_item",
696 a.span,
697 "an `adapter` may not declare an `agent` — adapters contain only capabilities, boundary types, external providers, and helpers",
698 ));
699 continue;
700 }
701 if kind != UnitKind::Context {
707 continue;
708 }
709 if let Some(prev) = table.agents.get(&a.name.name) {
710 errors.push(
711 CompileError::new(
712 "bynk.resolve.duplicate_agent",
713 a.name.span,
714 format!("agent `{}` is already declared", a.name.name),
715 )
716 .with_label(prev.name.span, "previously declared here"),
717 );
718 } else {
719 table.agents.insert(a.name.name.clone(), a.clone());
720 }
721 }
722 CommonsItem::Actor(a) => {
723 if kind == UnitKind::Adapter {
724 errors.push(CompileError::new(
725 "bynk.adapter.disallowed_item",
726 a.span,
727 "an `adapter` may not declare an `actor` — adapters contain only capabilities, boundary types, external providers, and helpers",
728 ));
729 continue;
730 }
731 if let Some(prev) = table.actors.get(&a.name.name) {
732 errors.push(
733 CompileError::new(
734 "bynk.resolve.duplicate_actor",
735 a.name.span,
736 format!("actor `{}` is already declared", a.name.name),
737 )
738 .with_label(prev.name.span, "previously declared here"),
739 );
740 } else {
741 table.actors.insert(a.name.name.clone(), a.clone());
742 }
743 }
744 _ => {}
745 }
746 }
747 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
748 }
749 for &i in indices {
750 let mut errors: Vec<CompileError> = Vec::new();
751 for item in parsed[i].items() {
752 let CommonsItem::Fn(f) = item else { continue };
753 match &f.name {
754 FnName::Free(id) => {
755 if let Some(prev) = table.fns.get(&id.name) {
756 errors.push(
757 CompileError::new(
758 "bynk.resolve.duplicate_fn",
759 id.span,
760 format!("function `{}` is already declared", id.name),
761 )
762 .with_label(prev.name.ident().span, "previously declared here"),
763 );
764 } else if let Some(prev) = table.types.get(&id.name) {
765 errors.push(
766 CompileError::new(
767 "bynk.resolve.name_conflict",
768 id.span,
769 format!(
770 "function `{}` conflicts with a type of the same name",
771 id.name
772 ),
773 )
774 .with_label(prev.name.span, "type declared here"),
775 );
776 } else {
777 table.fns.insert(id.name.clone(), Arc::new(f.clone()));
778 }
779 }
780 FnName::Method {
781 type_name,
782 method_name,
783 } => {
784 if !table.types.contains_key(&type_name.name) {
785 errors.push(
786 CompileError::new(
787 "bynk.resolve.method_unknown_type",
788 type_name.span,
789 format!(
790 "method `{}.{}` attached to an unknown type `{}`",
791 type_name.name, method_name.name, type_name.name
792 ),
793 )
794 .with_note(
795 "methods can only be declared on types defined in the same commons or context (across all of its files)",
796 ),
797 );
798 continue;
799 }
800 let mt = table.methods.entry(type_name.name.clone()).or_default();
801 let bucket = if f.has_self {
802 &mut mt.instance
803 } else {
804 &mut mt.statics
805 };
806 if let Some(prev) = bucket.get(&method_name.name) {
807 errors.push(
808 CompileError::new(
809 "bynk.resolve.duplicate_method",
810 method_name.span,
811 format!(
812 "method `{}.{}` is already declared",
813 type_name.name, method_name.name
814 ),
815 )
816 .with_label(prev.name.ident().span, "previously declared here"),
817 );
818 } else {
819 bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
820 }
821 }
822 }
823 }
824 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
825 }
826 for &i in indices {
831 let mut errors: Vec<CompileError> = Vec::new();
832 for item in parsed[i].items() {
833 let CommonsItem::Asset(a) = item else {
834 continue;
835 };
836 if !matches!(kind, UnitKind::Context | UnitKind::Commons) {
837 errors.push(
838 CompileError::new(
839 "bynk.asset.outside_context_or_commons",
840 a.span,
841 "`asset` declarations are only allowed inside a context or a commons",
842 )
843 .with_note(
844 "an asset ships with the Worker of each context that declares it or `uses` a commons that does",
845 ),
846 );
847 continue;
848 }
849 errors.extend(crate::assets::at_path_error(a));
850 if let Some(err) = crate::assets::path_errors(a) {
851 errors.push(err);
852 }
856 let key = &a.name.name;
857 match crate::assets::name_error(
858 a,
859 table.assets.get(key).map(|p| &p.name),
860 table.fns.get(key).map(|f| f.name.ident()),
861 table.types.get(key).map(|t| &t.name),
862 ) {
863 Some(err) => errors.push(err),
864 None => {
865 table.assets.insert(key.clone(), a.clone());
866 }
867 }
868 }
869 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
870 }
871 for &i in indices {
876 let mut errors: Vec<CompileError> = Vec::new();
877 for item in parsed[i].items() {
878 let CommonsItem::Embed(e) = item else {
879 continue;
880 };
881 if !matches!(kind, UnitKind::Context | UnitKind::Commons) {
882 errors.push(crate::embeds::placement_error(e));
883 continue;
884 }
885 if let Some(err) = crate::embeds::path_errors(e) {
886 errors.push(err);
887 }
891 let key = &e.name.name;
892 match crate::embeds::name_error(
893 e,
894 table.embeds.get(key).map(|p| &p.name),
895 table.assets.get(key).map(|a| &a.name),
896 table.fns.get(key).map(|f| f.name.ident()),
897 table.types.get(key).map(|t| &t.name),
898 ) {
899 Some(err) => errors.push(err),
900 None => {
901 table.embeds.insert(key.clone(), e.clone());
902 }
903 }
904 }
905 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
906 }
907 let mut providers_seen: HashMap<&str, Span> = HashMap::new();
915 for &i in indices {
916 let mut errors: Vec<CompileError> = Vec::new();
917 for item in parsed[i].items() {
918 let (what, name) = match item {
922 CommonsItem::Service(s)
923 if table.services.get(&s.name.name).map(|p| p.name.span)
924 == Some(s.name.span) =>
925 {
926 (ValueKind::Service, &s.name)
927 }
928 CommonsItem::Agent(a)
929 if table.agents.get(&a.name.name).map(|p| p.name.span) == Some(a.name.span) =>
930 {
931 (ValueKind::Agent, &a.name)
932 }
933 CommonsItem::Provider(p)
934 if !p.external
935 && table.providers.get(&p.capability.name).map(|q| q.span)
936 == Some(p.span) =>
937 {
938 (ValueKind::Provider, &p.provider_name)
939 }
940 _ => continue,
941 };
942 let earlier_provider = (what == ValueKind::Provider)
946 .then(|| providers_seen.get(name.name.as_str()).copied())
947 .flatten();
948 if let Some(err) = value_namespace_error(&table, what, name, earlier_provider) {
949 errors.push(err);
950 }
951 if what == ValueKind::Provider {
952 providers_seen.entry(&name.name).or_insert(name.span);
953 }
954 }
955 out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
956 }
957 if kind == UnitKind::Commons
972 && let Some(m) = indices.iter().find_map(|&i| {
973 parsed[i].items().iter().find_map(|item| match item {
974 CommonsItem::Messages(m) => Some(m),
975 _ => None,
976 })
977 })
978 {
979 if let Some(prev) = table.fns.get("render") {
980 out.push((
981 parsed[indices[0]].identity_path(),
982 CompileError::new(
983 "bynk.resolve.duplicate_fn",
984 m.span,
985 "function `render` is already declared",
986 )
987 .with_label(prev.name.ident().span, "previously declared here")
988 .with_note(
989 "a `messages` block in this commons implicitly declares its own \
990 `render(tag, msg) -> String` — name it something else",
991 ),
992 ));
993 } else {
994 table
995 .fns
996 .insert("render".to_string(), Arc::new(synthetic_render_fn()));
997 }
998 }
999 table
1000}
1001
1002pub fn local_value_kind(table: &UnitTable, name: &str) -> Option<(&'static str, Span)> {
1009 if let Some(f) = table.fns.get(name) {
1010 Some(("function", f.name.ident().span))
1011 } else if let Some(a) = table.assets.get(name) {
1012 Some(("asset", a.name.span))
1013 } else if let Some(e) = table.embeds.get(name) {
1014 Some(("embed", e.name.span))
1015 } else if let Some(e) = table.events.get(name) {
1016 Some(("event", e.name.span))
1017 } else if let Some(t) = table.types.get(name) {
1018 Some(("type", t.name.span))
1019 } else if let Some(s) = table.services.get(name) {
1020 Some(("service", s.name.span))
1021 } else if let Some(a) = table.agents.get(name) {
1022 Some(("agent", a.name.span))
1023 } else {
1024 table
1025 .providers
1026 .values()
1027 .find(|p| !p.external && p.provider_name.name == name)
1028 .map(|p| ("provider", p.provider_name.span))
1029 }
1030}
1031
1032#[derive(Clone, Copy, PartialEq, Eq)]
1035enum ValueKind {
1036 Service,
1037 Agent,
1038 Provider,
1039}
1040
1041fn value_namespace_error(
1047 table: &UnitTable,
1048 what: ValueKind,
1049 name: &Ident,
1050 earlier_provider: Option<Span>,
1051) -> Option<CompileError> {
1052 let key = &name.name;
1053 let services = || table.services.get(key).map(|s| s.name.span);
1054 let agents = || table.agents.get(key).map(|a| a.name.span);
1055 let (other, prev) = if let Some(f) = table.fns.get(key) {
1056 ("function", f.name.ident().span)
1057 } else if let Some(a) = table.assets.get(key) {
1058 ("asset", a.name.span)
1059 } else if let Some(e) = table.embeds.get(key) {
1060 ("embed", e.name.span)
1061 } else if let Some(e) = table.events.get(key) {
1062 ("event", e.name.span)
1063 } else if let Some(t) = table.types.get(key) {
1064 ("type", t.name.span)
1065 } else if let Some(span) = (what != ValueKind::Service).then(services).flatten() {
1066 ("service", span)
1067 } else if let Some(span) = (what == ValueKind::Provider).then(agents).flatten() {
1068 ("agent", span)
1069 } else if let Some(span) = earlier_provider {
1070 ("provider", span)
1071 } else {
1072 return None;
1073 };
1074 let what = match what {
1075 ValueKind::Service => "service",
1076 ValueKind::Agent => "agent",
1077 ValueKind::Provider => "provider",
1078 };
1079 let other_article = if matches!(other, "asset" | "embed" | "event" | "agent") {
1080 "an"
1081 } else {
1082 "a"
1083 };
1084 Some(
1085 CompileError::new(
1086 "bynk.resolve.name_conflict",
1087 name.span,
1088 format!("{what} `{key}` conflicts with {other_article} {other} of the same name"),
1089 )
1090 .with_label(prev, format!("{other} declared here"))
1091 .with_note(
1092 "a unit's services, agents, providers, functions, assets, types and events share one namespace",
1093 ),
1094 )
1095}
1096
1097fn synthetic_render_fn() -> FnDecl {
1102 let span = Span::default();
1103 FnDecl {
1104 type_params: Vec::new(),
1105 name: FnName::Free(Ident {
1106 name: "render".to_string(),
1107 span,
1108 }),
1109 params: vec![
1110 Param {
1111 name: Ident {
1112 name: "tag".to_string(),
1113 span,
1114 },
1115 type_ref: TypeRef::Named(Ident {
1116 name: "LocaleTag".to_string(),
1117 span,
1118 }),
1119 span,
1120 },
1121 Param {
1122 name: Ident {
1123 name: "msg".to_string(),
1124 span,
1125 },
1126 type_ref: TypeRef::Named(Ident {
1127 name: "Message".to_string(),
1128 span,
1129 }),
1130 span,
1131 },
1132 ],
1133 return_type: TypeRef::Base(BaseType::String, span),
1134 requires: Vec::new(),
1135 ensures: Vec::new(),
1136 body: Block {
1137 statements: Vec::new(),
1138 tail: Box::new(Expr {
1139 id: ExprId::SYNTHETIC,
1140 kind: ExprKind::StrLit(String::new()),
1141 span,
1142 }),
1143 span,
1144 tail_leading_comments: Vec::new(),
1145 implicit_tail: false,
1146 },
1147 has_self: false,
1148 documentation: None,
1149 span,
1150 trivia: Trivia::default(),
1151 }
1152}
1153
1154pub fn is_synthetic_render(f: &FnDecl) -> bool {
1160 matches!(&f.name, FnName::Free(n) if n.name == "render")
1161 && f.span == Span::default()
1162 && f.body.tail.id == ExprId::SYNTHETIC
1163}
1164
1165#[derive(Clone)]
1168pub struct FileDeclIndex {
1169 pub types: HashMap<String, PathBuf>,
1170 pub fns: HashMap<String, PathBuf>,
1171 pub methods: HashMap<String, HashMap<String, PathBuf>>,
1172 pub agents: HashMap<String, PathBuf>,
1175 pub capabilities: HashMap<String, PathBuf>,
1179}
1180
1181pub fn build_file_decl_index(indices: &[usize], parsed: &[ParsedFile]) -> FileDeclIndex {
1189 let mut idx = FileDeclIndex {
1190 types: HashMap::new(),
1191 fns: HashMap::new(),
1192 methods: HashMap::new(),
1193 agents: HashMap::new(),
1194 capabilities: HashMap::new(),
1195 };
1196 for &i in indices {
1197 let path = parsed[i].source_path();
1198 for item in parsed[i].items() {
1199 match item {
1200 CommonsItem::Type(t) => {
1201 idx.types
1202 .entry(t.name.name.clone())
1203 .or_insert_with(|| path.clone());
1204 }
1205 CommonsItem::Event(e) => {
1210 idx.types
1211 .entry(e.name.name.clone())
1212 .or_insert_with(|| path.clone());
1213 }
1214 CommonsItem::Fn(f) => match &f.name {
1215 FnName::Free(id) => {
1216 idx.fns
1217 .entry(id.name.clone())
1218 .or_insert_with(|| path.clone());
1219 }
1220 FnName::Method {
1221 type_name,
1222 method_name,
1223 } => {
1224 idx.methods
1225 .entry(type_name.name.clone())
1226 .or_default()
1227 .entry(method_name.name.clone())
1228 .or_insert_with(|| path.clone());
1229 }
1230 },
1231 CommonsItem::Agent(a) => {
1232 idx.agents
1233 .entry(a.name.name.clone())
1234 .or_insert_with(|| path.clone());
1235 }
1236 CommonsItem::Capability(c) => {
1237 idx.capabilities
1238 .entry(c.name.name.clone())
1239 .or_insert_with(|| path.clone());
1240 }
1241 CommonsItem::Provider(_)
1242 | CommonsItem::Service(_)
1243 | CommonsItem::Actor(_)
1244 | CommonsItem::Messages(_) => {}
1247 CommonsItem::Asset(a) => {
1251 idx.fns
1252 .entry(a.name.name.clone())
1253 .or_insert_with(|| path.clone());
1254 }
1255 CommonsItem::Embed(e) => {
1258 idx.fns
1259 .entry(e.name.name.clone())
1260 .or_insert_with(|| path.clone());
1261 }
1262 }
1263 }
1264 }
1265 idx
1266}
1267
1268pub fn uses_span_of(
1271 parsed: &[ParsedFile],
1272 indices: &[usize],
1273 target: &str,
1274) -> Option<(usize, Span)> {
1275 for &i in indices {
1276 for u in parsed[i].uses() {
1277 if u.target.joined() == target {
1278 return Some((i, u.span));
1279 }
1280 }
1281 }
1282 None
1283}
1284
1285pub fn build_cross_context_info(
1289 name: &str,
1290 unit_consumes: &HashMap<String, Vec<String>>,
1291 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1292 unit_uses: &HashMap<String, Vec<String>>,
1293 unit_tables: &HashMap<String, UnitTable>,
1294) -> resolver::CrossContextInfo {
1295 let consumed_contexts: Vec<String> = unit_consumes.get(name).cloned().unwrap_or_default();
1296 let aliases: HashMap<String, String> =
1297 unit_consumes_aliases.get(name).cloned().unwrap_or_default();
1298 let mut consumed_services: HashMap<String, HashMap<String, resolver::CrossContextService>> =
1299 HashMap::new();
1300 let mut consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>> = HashMap::new();
1301 let mut consumed_capabilities: HashMap<
1302 String,
1303 HashMap<String, resolver::CrossContextCapability>,
1304 > = HashMap::new();
1305 let mut consumed_event_names: HashMap<String, std::collections::HashSet<String>> =
1310 HashMap::new();
1311 let mut consumed_type_owners: HashMap<String, HashMap<String, String>> = HashMap::new();
1312 for t in &consumed_contexts {
1313 let (other_types_combined, owners) =
1314 contract_types_for(t, unit_tables, unit_uses, unit_consumes);
1315 consumed_types.insert(t.clone(), other_types_combined);
1316 if !owners.is_empty() {
1317 consumed_type_owners.insert(t.clone(), owners);
1318 }
1319 let Some(other_table) = unit_tables.get(t) else {
1320 continue;
1321 };
1322 consumed_event_names.insert(t.clone(), other_table.events.keys().cloned().collect());
1323 let mut svcs: HashMap<String, resolver::CrossContextService> = HashMap::new();
1324 for (sname, sdecl) in &other_table.services {
1325 if let Some(mut svc) = resolver::cross_context_service_for(sname, sdecl) {
1326 svc.foreign_callers = sdecl
1327 .handlers
1328 .iter()
1329 .find_map(|h| crate::actors::call_admission_for(h, &other_table.actors))
1330 .map(|a| a.foreign.into_iter().map(|f| f.actor).collect())
1331 .unwrap_or_default();
1332 svcs.insert(sname.clone(), svc);
1333 }
1334 }
1335 consumed_services.insert(t.clone(), svcs);
1336
1337 let mut caps: HashMap<String, resolver::CrossContextCapability> = HashMap::new();
1340 for cap_name in &other_table.exported_capabilities {
1341 let Some(decl) = other_table.capabilities.get(cap_name) else {
1342 continue;
1343 };
1344 let Some(provider) = other_table.providers.get(cap_name) else {
1345 continue;
1346 };
1347 let ops = decl
1348 .ops
1349 .iter()
1350 .map(|op| resolver::CrossContextCapabilityOp {
1351 name: op.name.name.clone(),
1352 type_params: op.type_params.iter().map(|p| p.name.name.clone()).collect(),
1353 params: op
1354 .params
1355 .iter()
1356 .map(|p| (p.name.name.clone(), p.type_ref.clone()))
1357 .collect(),
1358 return_type: op.return_type.clone(),
1359 })
1360 .collect();
1361 caps.insert(
1362 cap_name.clone(),
1363 resolver::CrossContextCapability {
1364 name: cap_name.clone(),
1365 ops,
1366 provider_name: provider.provider_name.name.clone(),
1367 provider_given: provider
1368 .given
1369 .iter()
1370 .filter(|c| !c.is_cross_context())
1371 .map(|c| c.key().to_string())
1372 .collect(),
1373 span: decl.span,
1374 },
1375 );
1376 }
1377 consumed_capabilities.insert(t.clone(), caps);
1378 }
1379 resolver::CrossContextInfo {
1380 self_context: Some(name.to_string()),
1381 consumed_contexts,
1382 aliases,
1383 consumed_services,
1384 consumed_types,
1385 consumed_type_renames: HashMap::new(),
1387 consumed_capabilities,
1388 flattened_caps: unit_tables
1390 .get(name)
1391 .map(|t| t.flattened_caps.clone())
1392 .unwrap_or_default(),
1393 consumed_event_names,
1394 consumed_type_owners,
1395 }
1396}
1397
1398pub fn discover_event_subscribers(
1411 unit_tables: &HashMap<String, UnitTable>,
1412 unit_consumes: &HashMap<String, Vec<String>>,
1413) -> BTreeMap<(String, String), Vec<(String, String)>> {
1414 let mut out: BTreeMap<(String, String), Vec<(String, String)>> = BTreeMap::new();
1415 for (ctx_name, table) in unit_tables {
1416 for (svc_name, svc) in &table.services {
1417 let ServiceProtocol::Events { event_type, .. } = &svc.protocol else {
1418 continue;
1419 };
1420 let TypeRef::Named(id) = event_type else {
1421 continue;
1422 };
1423 let name = &id.name;
1424 let owner = if table.events.contains_key(name) {
1425 Some(ctx_name.clone())
1426 } else {
1427 unit_consumes.get(ctx_name).and_then(|consumed| {
1428 consumed
1429 .iter()
1430 .find(|c| {
1431 unit_tables
1432 .get(c.as_str())
1433 .is_some_and(|t| t.events.contains_key(name))
1434 })
1435 .cloned()
1436 })
1437 };
1438 if let Some(owner) = owner {
1439 out.entry((owner, name.clone()))
1440 .or_default()
1441 .push((ctx_name.clone(), svc_name.clone()));
1442 }
1443 }
1444 }
1445 for subs in out.values_mut() {
1450 subs.sort();
1451 }
1452 out
1453}
1454
1455pub fn cron_and_queue_triggers(table: &UnitTable) -> (Vec<String>, Vec<String>) {
1460 let mut crons: Vec<String> = Vec::new();
1461 let mut queues: Vec<String> = Vec::new();
1462 for service in table.services.values() {
1463 for handler in &service.handlers {
1464 if let HandlerKind::Cron { expr } = &handler.kind {
1465 crons.push(expr.clone());
1466 }
1467 }
1468 if let ServiceProtocol::Queue { name } = &service.protocol {
1469 queues.push(name.clone());
1470 }
1471 }
1472 crons.sort();
1473 crons.dedup();
1474 queues.sort();
1475 queues.dedup();
1476 (crons, queues)
1477}
1478
1479pub fn record_capability_clause_ref(
1489 name: &Ident,
1490 cross_context: &resolver::CrossContextInfo,
1491 refs: &mut RefSink,
1492) {
1493 record_capability_clause_ref_inner(name, cross_context, refs, false);
1494}
1495
1496pub fn record_provides_clause_ref(
1501 name: &Ident,
1502 cross_context: &resolver::CrossContextInfo,
1503 refs: &mut RefSink,
1504) {
1505 record_capability_clause_ref_inner(name, cross_context, refs, true);
1506}
1507
1508fn record_capability_clause_ref_inner(
1509 name: &Ident,
1510 cross_context: &resolver::CrossContextInfo,
1511 refs: &mut RefSink,
1512 provides: bool,
1513) {
1514 let unit = cross_context.flattened_caps.get(&name.name);
1515 if provides {
1516 refs.record_provides(name.span, &name.name, unit.map(String::as_str));
1517 } else if let Some(unit) = unit {
1518 refs.record_in_unit(name.span, SymbolKind::Capability, &name.name, unit);
1519 } else {
1520 refs.record(name.span, SymbolKind::Capability, &name.name);
1521 }
1522}
1523
1524pub fn resolve_given_cap_ref(
1525 cap_ref: &CapRef,
1526 capability_info_map: &HashMap<String, CapabilityInfo>,
1527 cross_context: &resolver::CrossContextInfo,
1528 errors: &mut Vec<CompileError>,
1529 refs: &mut RefSink,
1530) -> Option<CapabilityInfo> {
1531 let Some(prefix) = cap_ref.prefix() else {
1532 match capability_info_map.get(cap_ref.key()) {
1534 Some(info) => {
1535 record_capability_clause_ref(&cap_ref.name, cross_context, refs);
1536 if let Some(unit) = cross_context.flattened_caps.get(cap_ref.key())
1541 && let Some(kind) = crate::firstparty::instantiable(unit, cap_ref.key())
1542 && !kind.bare
1543 {
1544 let example = crate::project_model::instance_example(kind);
1545 errors.push(
1546 CompileError::new(
1547 "bynk.capability.instance_only",
1548 cap_ref.span,
1549 format!(
1550 "`{unit}.{}` is used only through a named instance",
1551 cap_ref.key()
1552 ),
1553 )
1554 .with_note(format!("declare one and use it instead: `{example}`")),
1555 );
1556 }
1557 return Some(info.clone());
1558 }
1559 None => {
1560 errors.push(CompileError::new(
1561 "bynk.given.unknown_capability",
1562 cap_ref.span,
1563 format!(
1564 "capability `{}` is not declared in this context",
1565 cap_ref.key()
1566 ),
1567 ));
1568 return None;
1569 }
1570 }
1571 };
1572 let Some(ctx_name) = cross_context.resolve_prefix(&prefix) else {
1574 errors.push(
1575 CompileError::new(
1576 "bynk.resolve.unconsumed_context",
1577 cap_ref.span,
1578 format!(
1579 "`given {}.{}` refers to a context that this context does not `consumes`",
1580 prefix,
1581 cap_ref.key()
1582 ),
1583 )
1584 .with_note(
1585 "add a `consumes` clause for the providing context (optionally with an alias) at the top of this context",
1586 ),
1587 );
1588 return None;
1589 };
1590 let exports_it = cross_context
1591 .consumed_capabilities
1592 .get(&ctx_name)
1593 .is_some_and(|m| m.contains_key(cap_ref.key()));
1594 if exports_it {
1595 refs.record_in_unit(
1598 cap_ref.name.span,
1599 SymbolKind::Capability,
1600 cap_ref.key(),
1601 &ctx_name,
1602 );
1603 }
1604 if !exports_it {
1605 errors.push(
1606 CompileError::new(
1607 "bynk.given.cross_context_unknown_capability",
1608 cap_ref.span,
1609 format!(
1610 "context `{}` does not export a capability named `{}`",
1611 ctx_name,
1612 cap_ref.key()
1613 ),
1614 )
1615 .with_note(
1616 "the providing context must list the capability in an `exports capability { … }` clause",
1617 ),
1618 );
1619 }
1620 None
1621}
1622
1623pub fn contract_types_for(
1656 unit: &str,
1657 unit_tables: &HashMap<String, UnitTable>,
1658 unit_uses: &HashMap<String, Vec<String>>,
1659 unit_consumes: &HashMap<String, Vec<String>>,
1660) -> (HashMap<String, Arc<TypeDecl>>, HashMap<String, String>) {
1661 let mut walk = ContractWalk::default();
1662 let (table, owners, _) =
1663 contract_types_inner(unit, unit_tables, unit_uses, unit_consumes, &mut walk);
1664 (table, owners)
1665}
1666
1667pub fn display_type_name(name: &str) -> &str {
1675 match name.rsplit_once("__") {
1676 Some((prefix, last))
1677 if prefix.starts_with(|c: char| c.is_ascii_lowercase())
1678 && prefix
1679 .chars()
1680 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
1681 && last.starts_with(|c: char| c.is_ascii_uppercase()) =>
1682 {
1683 last
1684 }
1685 _ => name,
1686 }
1687}
1688
1689pub fn display_type_names_in(text: &str) -> String {
1692 let mut out = String::with_capacity(text.len());
1693 let mut word = String::new();
1694 let flush = |word: &mut String, out: &mut String| {
1695 out.push_str(display_type_name(word));
1696 word.clear();
1697 };
1698 for c in text.chars() {
1699 if c.is_ascii_alphanumeric() || c == '_' {
1700 word.push(c);
1701 } else {
1702 flush(&mut word, &mut out);
1703 out.push(c);
1704 }
1705 }
1706 flush(&mut word, &mut out);
1707 out
1708}
1709
1710type ContractView = (HashMap<String, Arc<TypeDecl>>, HashMap<String, String>);
1712
1713#[derive(Default)]
1718struct ContractWalk {
1719 visiting: std::collections::HashSet<String>,
1720 memo: HashMap<String, ContractView>,
1721}
1722
1723fn contract_types_inner(
1725 unit: &str,
1726 unit_tables: &HashMap<String, UnitTable>,
1727 unit_uses: &HashMap<String, Vec<String>>,
1728 unit_consumes: &HashMap<String, Vec<String>>,
1729 walk: &mut ContractWalk,
1730) -> (
1731 HashMap<String, Arc<TypeDecl>>,
1732 HashMap<String, String>,
1733 bool,
1734) {
1735 if let Some((table, owners)) = walk.memo.get(unit) {
1736 return (table.clone(), owners.clone(), false);
1737 }
1738 let mut out = combined_types_for(unit, unit_tables, unit_uses);
1739 let mut owners: HashMap<String, String> = HashMap::new();
1740 if !walk.visiting.insert(unit.to_string()) {
1741 return (out, owners, true);
1743 }
1744 let mut consumed: Vec<&String> = unit_consumes
1745 .get(unit)
1746 .into_iter()
1747 .flatten()
1748 .filter(|u| {
1749 unit_tables
1750 .get(*u)
1751 .is_some_and(|t| t.kind == Some(UnitKind::Context))
1752 })
1753 .collect();
1754 consumed.sort();
1755 consumed.dedup();
1756 let mut cyclic = false;
1758 let views: HashMap<String, ContractView> = consumed
1759 .iter()
1760 .map(|u| {
1761 let (table, owners, met) =
1762 contract_types_inner(u, unit_tables, unit_uses, unit_consumes, walk);
1763 cyclic |= met;
1764 ((*u).clone(), (table, owners))
1765 })
1766 .collect();
1767 let declarer = |owner: &str, r: &str, target: &Arc<TypeDecl>| -> Option<String> {
1771 if unit_tables
1772 .get(owner)
1773 .and_then(|t| t.types.get(r))
1774 .is_some_and(|d| Arc::ptr_eq(d, target) || d.span == target.span)
1775 {
1776 return Some(owner.to_string());
1777 }
1778 views.get(owner).and_then(|(_, o)| o.get(r).cloned())
1779 };
1780 let mut by_name: std::collections::BTreeMap<&String, Vec<&String>> = Default::default();
1781 for u in &consumed {
1782 for n in unit_tables[*u].types.keys() {
1783 if !out.contains_key(n) {
1784 by_name.entry(n).or_default().push(u);
1785 }
1786 }
1787 }
1788 let mut queue: Vec<(String, String)> = Vec::new();
1789 for (n, us) in by_name {
1790 if let [u] = us.as_slice() {
1791 out.insert(n.clone(), unit_tables[*u].types[n].clone());
1792 owners.insert(n.clone(), (*u).clone());
1793 queue.push((n.clone(), (*u).clone()));
1794 }
1795 }
1796 queue.sort();
1797 let same = |a: &Arc<TypeDecl>, b: &Arc<TypeDecl>| Arc::ptr_eq(a, b) || a.span == b.span;
1798 while let Some((key, via)) = queue.pop() {
1802 let owner = via;
1803 let Some((view, _)) = views.get(&owner) else {
1804 continue;
1805 };
1806 let decl = out[&key].clone();
1807 let mut subst: HashMap<String, String> = HashMap::new();
1808 for r in crate::project_model::type_decl_ref_names(&decl) {
1809 let Some(target) = view.get(&r) else {
1810 continue;
1811 };
1812 match out.get(&r) {
1813 Some(here) if same(here, target) => {}
1814 None => {
1815 out.insert(r.clone(), target.clone());
1816 if let Some(d) = declarer(&owner, &r, target) {
1817 owners.insert(r.clone(), d);
1818 }
1819 queue.push((r, owner.clone()));
1820 }
1821 Some(_) => {
1822 let d = declarer(&owner, &r, target);
1823 let qualified = format!(
1824 "{}__{r}",
1825 d.as_deref().unwrap_or(owner.as_str()).replace('.', "_")
1826 );
1827 if !out.contains_key(&qualified) {
1828 let mut renamed = (**target).clone();
1833 renamed.name.name = qualified.clone();
1834 out.insert(qualified.clone(), Arc::new(renamed));
1835 if let Some(d) = d {
1836 owners.insert(qualified.clone(), d);
1837 }
1838 queue.push((qualified.clone(), owner.clone()));
1839 }
1840 subst.insert(r, qualified);
1841 }
1842 }
1843 }
1844 if !subst.is_empty() {
1845 out.insert(key, Arc::new(rename_decl_refs(&decl, &subst)));
1846 }
1847 }
1848 walk.visiting.remove(unit);
1849 if !cyclic {
1850 walk.memo
1851 .insert(unit.to_string(), (out.clone(), owners.clone()));
1852 }
1853 (out, owners, cyclic)
1854}
1855
1856fn rename_decl_refs(decl: &TypeDecl, subst: &HashMap<String, String>) -> TypeDecl {
1859 let mut out = decl.clone();
1860 match &mut out.body {
1861 TypeBody::Record(r) => {
1862 for f in &mut r.fields {
1863 rename_type_ref(&mut f.type_ref, subst);
1864 }
1865 }
1866 TypeBody::Sum(sum) => {
1867 for v in &mut sum.variants {
1868 for f in &mut v.payload {
1869 rename_type_ref(&mut f.type_ref, subst);
1870 }
1871 }
1872 }
1873 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
1874 }
1875 out
1876}
1877
1878fn rename_type_ref(r: &mut TypeRef, subst: &HashMap<String, String>) {
1881 match r {
1882 TypeRef::Named(id) => {
1883 if let Some(to) = subst.get(&id.name) {
1884 id.name = to.clone();
1885 }
1886 }
1887 TypeRef::App { name, args, .. } => {
1888 if let Some(to) = subst.get(&name.name) {
1889 name.name = to.clone();
1890 }
1891 for a in args {
1892 rename_type_ref(a, subst);
1893 }
1894 }
1895 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
1896 rename_type_ref(a, subst);
1897 rename_type_ref(b, subst);
1898 }
1899 TypeRef::Option(t, _)
1900 | TypeRef::Effect(t, _)
1901 | TypeRef::HttpResult(t, _)
1902 | TypeRef::List(t, _)
1903 | TypeRef::Query(t, _)
1904 | TypeRef::Stream(t, _)
1905 | TypeRef::Connection(t, _)
1906 | TypeRef::History(t, _) => rename_type_ref(t, subst),
1907 TypeRef::Fn(params, ret, _) => {
1908 for p in params {
1909 rename_type_ref(p, subst);
1910 }
1911 rename_type_ref(ret, subst);
1912 }
1913 TypeRef::Base(..)
1914 | TypeRef::QueueResult(_)
1915 | TypeRef::Asset(_)
1916 | TypeRef::RequestHeaders(_)
1917 | TypeRef::TokenClaims(_)
1918 | TypeRef::BoundaryRejection(_)
1919 | TypeRef::ValidationError(_)
1920 | TypeRef::JsonError(_)
1921 | TypeRef::Unit(_) => {}
1922 }
1923}
1924
1925pub fn combined_types_for(
1926 unit: &str,
1927 unit_tables: &HashMap<String, UnitTable>,
1928 unit_uses: &HashMap<String, Vec<String>>,
1929) -> HashMap<String, Arc<TypeDecl>> {
1930 let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1931 if let Some(table) = unit_tables.get(unit) {
1932 for (n, d) in &table.types {
1933 out.insert(n.clone(), d.clone());
1934 }
1935 }
1936 if let Some(targets) = unit_uses.get(unit) {
1937 for t in targets {
1938 if let Some(used) = unit_tables.get(t) {
1939 for (n, d) in &used.types {
1940 out.entry(n.clone()).or_insert_with(|| d.clone());
1941 }
1942 }
1943 }
1944 }
1945 out
1946}
1947
1948pub fn combined_types_for_unit_info(
1959 unit: &str,
1960 unit_info: &BTreeMap<String, UnitInfo>,
1961) -> HashMap<String, Arc<TypeDecl>> {
1962 let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1963 let Some(info) = unit_info.get(unit) else {
1964 return out;
1965 };
1966 for (n, d) in &info.table.types {
1967 out.insert(n.clone(), d.clone());
1968 }
1969 for t in &info.uses {
1970 if let Some(used) = unit_info.get(t) {
1971 for (n, d) in &used.table.types {
1972 out.entry(n.clone()).or_insert_with(|| d.clone());
1973 }
1974 }
1975 }
1976 out
1977}
1978
1979pub enum ContextMessageBundle {
1992 None,
1994 One(MessageBundleInfo),
1996 Many(Vec<String>),
1998}
1999
2000pub struct MessageBundleInfo {
2001 pub commons: String,
2003 pub source_path: PathBuf,
2011}
2012
2013pub fn detect_context_message_bundle(
2019 ctx: &str,
2020 unit_uses: &HashMap<String, Vec<String>>,
2021 groups: &BTreeMap<String, Vec<usize>>,
2022 kinds: &BTreeMap<String, UnitKind>,
2023 parsed: &[ParsedFile],
2024) -> ContextMessageBundle {
2025 let mut found: Vec<MessageBundleInfo> = Vec::new();
2026 for target in unit_uses.get(ctx).into_iter().flatten() {
2027 if kinds.get(target) != Some(&UnitKind::Commons) {
2028 continue;
2029 }
2030 let Some(indices) = groups.get(target) else {
2031 continue;
2032 };
2033 for &i in indices {
2034 let has_reference = parsed[i].items().iter().any(|item| {
2035 matches!(item, CommonsItem::Messages(m) if m.annotations.iter().any(|a| a.name.name == "reference"))
2036 });
2037 if has_reference {
2038 found.push(MessageBundleInfo {
2039 commons: target.clone(),
2040 source_path: parsed[i].source_path(),
2041 });
2042 break;
2043 }
2044 }
2045 }
2046 match found.len() {
2047 0 => ContextMessageBundle::None,
2048 1 => ContextMessageBundle::One(found.pop().expect("len == 1")),
2049 _ => ContextMessageBundle::Many(found.into_iter().map(|b| b.commons).collect()),
2050 }
2051}
2052
2053#[cfg(test)]
2054mod detect_context_message_bundle_tests {
2055 use super::*;
2056 use bynk_syntax::ast::{
2057 Annotation, Commons, CommonsForm, Context, MessagesDecl, QualifiedName, SourceUnit,
2058 UsesDecl,
2059 };
2060
2061 fn ident(name: &str) -> Ident {
2062 Ident {
2063 name: name.to_string(),
2064 span: Span::default(),
2065 }
2066 }
2067
2068 fn qualified(name: &str) -> QualifiedName {
2069 QualifiedName {
2070 parts: name.split('.').map(ident).collect(),
2071 span: Span::default(),
2072 }
2073 }
2074
2075 fn commons_with_messages(name: &str, tag: &str, is_reference: bool) -> ParsedFile {
2080 let annotations = if is_reference {
2081 vec![Annotation {
2082 name: ident("reference"),
2083 args: Vec::new(),
2084 span: Span::default(),
2085 }]
2086 } else {
2087 Vec::new()
2088 };
2089 let messages = MessagesDecl {
2090 tag: tag.to_string(),
2091 tag_span: Span::default(),
2092 annotations,
2093 entries: Vec::new(),
2094 documentation: None,
2095 span: Span::default(),
2096 trivia: Trivia::default(),
2097 };
2098 ParsedFile::new(
2099 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
2100 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
2101 None,
2102 String::new(),
2103 SourceUnit::Commons(Commons {
2104 name: qualified(name),
2105 items: vec![CommonsItem::Messages(messages)],
2106 uses: Vec::new(),
2107 documentation: None,
2108 form: CommonsForm::Brace,
2109 span: Span::default(),
2110 trivia: Trivia::default(),
2111 trailing_comments: Vec::new(),
2112 }),
2113 UnitKind::Commons,
2114 false,
2115 )
2116 }
2117
2118 fn context_using(name: &str, targets: &[&str]) -> ParsedFile {
2121 ParsedFile::new(
2122 PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
2123 PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
2124 None,
2125 String::new(),
2126 SourceUnit::Context(Context {
2127 removed_agents: Vec::new(),
2128 name: qualified(name),
2129 uses: targets
2130 .iter()
2131 .map(|t| UsesDecl {
2132 target: qualified(t),
2133 span: Span::default(),
2134 trivia: Trivia::default(),
2135 })
2136 .collect(),
2137 consumes: Vec::new(),
2138 exports: Vec::new(),
2139 items: Vec::new(),
2140 documentation: None,
2141 form: CommonsForm::Brace,
2142 span: Span::default(),
2143 trivia: Trivia::default(),
2144 trailing_comments: Vec::new(),
2145 }),
2146 UnitKind::Context,
2147 false,
2148 )
2149 }
2150
2151 struct Scenario {
2155 parsed: Vec<ParsedFile>,
2156 groups: BTreeMap<String, Vec<usize>>,
2157 kinds: BTreeMap<String, UnitKind>,
2158 unit_uses: HashMap<String, Vec<String>>,
2159 }
2160
2161 fn scenario(ctx_name: &str, ctx_uses: &[&str], bundles: Vec<(&str, ParsedFile)>) -> Scenario {
2163 let mut parsed = vec![context_using(ctx_name, ctx_uses)];
2164 let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
2165 let mut kinds: BTreeMap<String, UnitKind> = BTreeMap::new();
2166 groups.insert(ctx_name.to_string(), vec![0]);
2167 kinds.insert(ctx_name.to_string(), UnitKind::Context);
2168 for (name, pf) in bundles {
2169 let idx = parsed.len();
2170 parsed.push(pf);
2171 groups.entry(name.to_string()).or_default().push(idx);
2172 kinds.insert(name.to_string(), UnitKind::Commons);
2173 }
2174 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
2175 unit_uses.insert(
2176 ctx_name.to_string(),
2177 ctx_uses.iter().map(|s| s.to_string()).collect(),
2178 );
2179 Scenario {
2180 parsed,
2181 groups,
2182 kinds,
2183 unit_uses,
2184 }
2185 }
2186
2187 #[test]
2188 fn zero_bundles_when_uses_reaches_no_messages_commons() {
2189 let Scenario {
2190 parsed,
2191 groups,
2192 kinds,
2193 unit_uses,
2194 } = scenario("app.web", &["app.other"], vec![]);
2195 assert!(matches!(
2196 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2197 ContextMessageBundle::None
2198 ));
2199 }
2200
2201 #[test]
2202 fn zero_bundles_when_uses_is_empty() {
2203 let Scenario {
2204 parsed,
2205 groups,
2206 kinds,
2207 unit_uses,
2208 } = scenario("app.web", &[], vec![]);
2209 assert!(matches!(
2210 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2211 ContextMessageBundle::None
2212 ));
2213 }
2214
2215 #[test]
2216 fn one_bundle_is_found_by_its_reference_block() {
2217 let bundle = commons_with_messages("app.msgs", "en", true);
2218 let Scenario {
2219 parsed,
2220 groups,
2221 kinds,
2222 unit_uses,
2223 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
2224 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
2225 let ContextMessageBundle::One(info) = found else {
2226 panic!("expected exactly one bundle");
2227 };
2228 assert_eq!(info.commons, "app.msgs");
2229 assert_eq!(info.source_path, PathBuf::from("app/msgs.bynk"));
2230 }
2231
2232 #[test]
2233 fn a_bundle_missing_its_reference_block_is_not_counted() {
2234 let bundle = commons_with_messages("app.msgs", "en", false);
2238 let Scenario {
2239 parsed,
2240 groups,
2241 kinds,
2242 unit_uses,
2243 } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
2244 assert!(matches!(
2245 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2246 ContextMessageBundle::None
2247 ));
2248 }
2249
2250 #[test]
2251 fn two_bundles_report_both_commons_names() {
2252 let a = commons_with_messages("app.msgs_a", "en", true);
2253 let b = commons_with_messages("app.msgs_b", "en", true);
2254 let Scenario {
2255 parsed,
2256 groups,
2257 kinds,
2258 unit_uses,
2259 } = scenario(
2260 "app.web",
2261 &["app.msgs_a", "app.msgs_b"],
2262 vec![("app.msgs_a", a), ("app.msgs_b", b)],
2263 );
2264 let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
2265 let ContextMessageBundle::Many(names) = found else {
2266 panic!("expected two bundles");
2267 };
2268 let mut names = names;
2269 names.sort();
2270 assert_eq!(
2271 names,
2272 vec!["app.msgs_a".to_string(), "app.msgs_b".to_string()]
2273 );
2274 }
2275
2276 #[test]
2277 fn a_commons_reached_only_transitively_is_not_counted() {
2278 let bundle = commons_with_messages("app.msgs", "en", true);
2282 let mut mid = context_using("app.mid", &["app.msgs"]);
2283 mid.set_kind(UnitKind::Commons);
2289 let Scenario {
2290 mut parsed,
2291 mut groups,
2292 mut kinds,
2293 unit_uses,
2294 } = scenario("app.web", &["app.mid"], vec![("app.msgs", bundle)]);
2295 let mid_idx = parsed.len();
2296 parsed.push(mid);
2297 groups.insert("app.mid".to_string(), vec![mid_idx]);
2298 kinds.insert("app.mid".to_string(), UnitKind::Commons);
2299 assert!(matches!(
2300 detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2301 ContextMessageBundle::None
2302 ));
2303 }
2304}
2305
2306pub fn consumes_span_of(
2309 parsed: &[ParsedFile],
2310 indices: &[usize],
2311 target: &str,
2312) -> Option<(usize, Span)> {
2313 for &i in indices {
2314 for c in parsed[i].consumes() {
2315 if c.target.joined() == target {
2316 return Some((i, c.span));
2317 }
2318 }
2319 }
2320 None
2321}
2322
2323pub fn parsed_alias_span(
2326 parsed: &[ParsedFile],
2327 indices: &[usize],
2328 alias: &str,
2329) -> Option<(usize, Span)> {
2330 for &i in indices {
2331 for c in parsed[i].consumes() {
2332 if let Some(a) = &c.alias
2333 && a.name == alias
2334 {
2335 return Some((i, a.span));
2336 }
2337 }
2338 }
2339 None
2340}
2341
2342#[derive(Debug, Clone)]
2345pub struct ConsumedType {
2346 pub owning_context: String,
2347 pub visibility: Visibility,
2348}
2349
2350#[cfg(test)]
2355mod value_namespace_tests {
2356 use super::*;
2357 use crate::testkit::analyse;
2358
2359 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2360 enum Kind {
2361 Fn,
2362 Type,
2363 Event,
2364 Asset,
2365 Embed,
2366 Service,
2367 Agent,
2368 Provider,
2369 Capability,
2370 Actor,
2371 }
2372
2373 const ALL: [Kind; 10] = [
2374 Kind::Fn,
2375 Kind::Type,
2376 Kind::Event,
2377 Kind::Asset,
2378 Kind::Embed,
2379 Kind::Service,
2380 Kind::Agent,
2381 Kind::Provider,
2382 Kind::Capability,
2383 Kind::Actor,
2384 ];
2385
2386 fn kind_of(item: &CommonsItem) -> Option<Kind> {
2390 match item {
2391 CommonsItem::Fn(_) => Some(Kind::Fn),
2392 CommonsItem::Type(_) => Some(Kind::Type),
2393 CommonsItem::Event(_) => Some(Kind::Event),
2394 CommonsItem::Asset(_) => Some(Kind::Asset),
2395 CommonsItem::Embed(_) => Some(Kind::Embed),
2396 CommonsItem::Service(_) => Some(Kind::Service),
2397 CommonsItem::Agent(_) => Some(Kind::Agent),
2398 CommonsItem::Provider(_) => Some(Kind::Provider),
2399 CommonsItem::Capability(_) => Some(Kind::Capability),
2400 CommonsItem::Actor(_) => Some(Kind::Actor),
2401 CommonsItem::Messages(_) => None,
2403 }
2404 }
2405
2406 fn shares_the_value_namespace(k: Kind) -> bool {
2407 !matches!(k, Kind::Capability | Kind::Actor)
2408 }
2409
2410 fn decl(kind: Kind, n: &str) -> String {
2413 match kind {
2414 Kind::Fn => format!("fn {n}() -> Int {{\n 1\n}}\n"),
2415 Kind::Type => format!("type {n} = {{ n: Int }}\n"),
2416 Kind::Event => format!("event {n} = {{ n: Int }}\n"),
2417 Kind::Asset => format!("asset {n} = \"assets/x.json\"\n"),
2418 Kind::Embed => format!("embed {n} = \"embeds/x.txt\"\n"),
2419 Kind::Service => {
2420 format!(
2421 "service {n} {{\n on call() -> Effect[Int] {{\n Effect.pure(1)\n }}\n}}\n"
2422 )
2423 }
2424 Kind::Agent => format!(
2425 "agent {n} {{\n key id: String\n\n store hits: Cell[Int]\n\n on call ping() -> Effect[Int] {{\n 1\n }}\n}}\n"
2426 ),
2427 Kind::Provider => format!(
2428 "capability ProbeCap {{\n fn go() -> Effect[Int]\n}}\n\nprovides ProbeCap = {n} {{\n fn go() -> Effect[Int] {{\n Effect.pure(1)\n }}\n}}\n"
2429 ),
2430 Kind::Capability => format!("capability {n} {{\n fn go() -> Effect[Int]\n}}\n"),
2431 Kind::Actor => {
2432 format!("actor {n} {{ auth = Bearer(secret = \"S\"), identity = ProbeId }}\n")
2433 }
2434 }
2435 }
2436
2437 const HEADER: &str = "context app\n\ntype ProbeId = opaque String\n\n";
2438
2439 #[test]
2440 fn each_snippet_declares_its_kind() {
2441 for k in ALL {
2442 let src = format!("{HEADER}{}", decl(k, "Thing"));
2443 let tokens = bynk_syntax::lexer::tokenize(&src).expect("lexes");
2444 let unit = bynk_syntax::parser::parse_unit(&tokens, &src).expect("parses");
2445 let bynk_syntax::ast::SourceUnit::Context(c) = unit else {
2446 panic!("a context")
2447 };
2448 assert!(
2449 c.items.iter().any(|i| kind_of(i) == Some(k)),
2450 "the {k:?} snippet declares no {k:?}"
2451 );
2452 }
2453 }
2454
2455 #[test]
2456 fn every_pair_of_kinds_against_the_value_namespace() {
2457 for (x, &a) in ALL.iter().enumerate() {
2458 for &b in &ALL[x + 1..] {
2459 for (first, second) in [(a, b), (b, a)] {
2461 for n in ["thing", "Thing"] {
2462 let src = format!("{HEADER}{}\n{}", decl(first, n), decl(second, n));
2463 let r = analyse(&[
2464 ("app/main.bynk", &src),
2465 ("assets/x.json", "{}"),
2466 ("embeds/x.txt", "x"),
2467 ]);
2468 let cats = r.error_categories();
2469 if shares_the_value_namespace(a) && shares_the_value_namespace(b) {
2470 let want = if matches!((a, b), (Kind::Type, Kind::Event)) {
2471 "bynk.resolve.duplicate_type"
2472 } else {
2473 "bynk.resolve.name_conflict"
2474 };
2475 assert_eq!(cats, vec![want], "{first:?} then {second:?} `{n}`");
2476 } else {
2477 assert!(
2478 cats.is_empty(),
2479 "{first:?} then {second:?} `{n}` should check clean: {cats:?}"
2480 );
2481 }
2482 }
2483 }
2484 }
2485 }
2486 }
2487
2488 #[test]
2491 fn a_split_context_is_one_namespace() {
2492 for (svc_file, asset_file) in [("a.bynk", "z.bynk"), ("z.bynk", "a.bynk")] {
2493 let svc = format!("context app\n\n{}", decl(Kind::Service, "api"));
2494 let asset = format!("context app\n\n{}", decl(Kind::Asset, "api"));
2495 let svc_path = format!("app/{svc_file}");
2496 let asset_path = format!("app/{asset_file}");
2497 let r = analyse(&[
2498 (svc_path.as_str(), svc.as_str()),
2499 (asset_path.as_str(), asset.as_str()),
2500 ("assets/x.json", "{}"),
2501 ]);
2502 assert_eq!(r.error_categories(), vec!["bynk.resolve.name_conflict"]);
2503 let d = &r.analysis.errors[0];
2504 assert!(
2505 d.source_path
2506 .as_deref()
2507 .is_some_and(|p| p.ends_with(svc_file)),
2508 "reported in the service's file: {:?}",
2509 d.source_path
2510 );
2511 }
2512 }
2513
2514 #[test]
2517 fn two_providers_under_one_name_conflict() {
2518 let src = "context app\n\ncapability A {\n fn go() -> Effect[Int]\n}\n\ncapability B {\n fn go() -> Effect[Int]\n}\n\nprovides A = Impl {\n fn go() -> Effect[Int] {\n Effect.pure(1)\n }\n}\n\nprovides B = Impl {\n fn go() -> Effect[Int] {\n Effect.pure(2)\n }\n}\n";
2519 let r = analyse(&[("app/main.bynk", src)]);
2520 assert_eq!(r.error_categories(), vec!["bynk.resolve.name_conflict"]);
2521 }
2522}
2523
2524#[cfg(test)]
2527mod contract_types_tests {
2528 use crate::testkit::analyse;
2529
2530 const ORDER: &str = "context shop.order\n\nexports transparent { Order }\n\ntype Region = enum { Domestic, International }\n\ntype Order = { id: String, region: Region }\n";
2531 const POST: &str = "context shop.post\n\nexports transparent { Parcel }\n\ntype Region = enum { North, South }\n\ntype Parcel = { ref: String, region: Region }\n";
2532 const LEDGER: &str = "context shop.ledger\n\nconsumes shop.order\nconsumes shop.post\n\ntype Region = enum { Here, There }\n\nservice orderId {\n on call(o: Order) -> Effect[String] {\n o.id\n }\n}\n\nservice parcelRef {\n on call(p: Parcel) -> Effect[String] {\n p.ref\n }\n}\n";
2533
2534 fn normal_form(service: &str) -> String {
2535 let a = analyse(&[
2536 ("shop/order.bynk", ORDER),
2537 ("shop/post.bynk", POST),
2538 ("shop/ledger.bynk", LEDGER),
2539 ]);
2540 a.assert_clean();
2541 let info = &a.analysis.boundary_info["shop.ledger"];
2542 let svc = crate::resolver::cross_context_service_for(service, &info.services[service])
2543 .expect("an `on call` service");
2544 crate::contract::service_normal_form(&svc, &info.types)
2545 }
2546
2547 #[test]
2551 fn a_qualified_key_displays_as_its_declarations_name() {
2552 use super::{display_type_name, display_type_names_in};
2553 assert_eq!(display_type_name("shop_order__Code"), "Code");
2554 assert_eq!(display_type_name("shop_ledger__shop_order__Code"), "Code");
2555 assert_eq!(display_type_name("Code"), "Code");
2556 assert_eq!(display_type_name("My__Thing"), "My__Thing");
2557 assert_eq!(display_type_name("shop_order__lower"), "shop_order__lower");
2558 assert_eq!(
2559 display_type_names_in("orderId(o: Order{code: shop_order__Code(String)})"),
2560 "orderId(o: Order{code: Code(String)})"
2561 );
2562 let tys = crate::checker::Types::default();
2563 let key = tys.intern(crate::checker::Ty::Named {
2564 name: "shop_order__Code".to_string(),
2565 kind: crate::checker::NamedKind::Opaque(bynk_syntax::ast::BaseType::String),
2566 args: Vec::new(),
2567 });
2568 assert_eq!(tys.display(key), "Code");
2569 let listed = tys.intern(crate::checker::Ty::List(key));
2570 assert_eq!(tys.display(listed), "List[Code]");
2571 }
2572
2573 #[test]
2574 fn a_borrowed_record_names_its_own_contexts_field_type() {
2575 let order = normal_form("orderId");
2576 assert!(
2577 order.contains("Domestic") && !order.contains("Here") && !order.contains("North"),
2578 "{order}"
2579 );
2580 let parcel = normal_form("parcelRef");
2581 assert!(
2582 parcel.contains("North") && !parcel.contains("Here") && !parcel.contains("Domestic"),
2583 "{parcel}"
2584 );
2585 }
2586}