1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
32use std::path::{Path, PathBuf};
33use std::sync::{Arc, OnceLock};
34
35use crate::checker::{self, Ty, TyId, Types};
36use crate::context_checks::{
37 build_capability_op_info, reject_fn_types, ts_type_ref_display, type_ref_is_held,
38 type_ref_to_display, validate_store_field_value_types,
39};
40use crate::firstparty::{self, Platform};
41use crate::icu;
42use crate::index::{RefSink, SymbolKind};
43use crate::resolver::MethodTable as ResolverMethodTable;
44use crate::symbols::{
45 ConsumedType, ContextMessageBundle, FileDeclIndex, UnitTable, build_file_decl_index,
46 build_unit_table, consumes_span_of, detect_context_message_bundle, parsed_alias_span,
47 uses_span_of,
48};
49use bynk_project::{
50 AttributedError, LabelFile, ParsedFile, UnitKind, check_directory_kind_consistency,
51 check_directory_name_consistency, check_file_directory_conflicts, check_group_kind_consistency,
52 check_group_module_docs, check_path_name_alignment, detect_consumes_cycles,
53 discover_bynk_files, is_unpinned_range, normalize_rel, parse_sources, parse_sources_recovering,
54 read_adapter_binding, read_adapter_include, read_source,
55};
56use bynk_syntax::ast::*;
57pub use bynk_syntax::ast::{FnDecl, TypeDecl, Visibility};
65use bynk_syntax::error::CompileError;
66use bynk_syntax::lexer;
67use bynk_syntax::parser;
68use bynk_syntax::span::{FileId, Span};
69
70pub struct ErrorSink {
82 entries: Vec<AttributedError>,
83 warnings: Vec<AttributedError>,
88 files: HashMap<FileId, PathBuf>,
92}
93
94impl Default for ErrorSink {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100impl ErrorSink {
101 pub fn new() -> Self {
102 Self {
103 entries: Vec::new(),
104 warnings: Vec::new(),
105 files: HashMap::new(),
106 }
107 }
108 pub fn register_file(&mut self, file: FileId, identity: PathBuf) {
110 self.files.insert(file, identity);
111 }
112 pub fn register_parsed(&mut self, parsed: &[ParsedFile]) {
119 for pf in parsed {
120 if let Some(abs) = pf.abs_path() {
121 self.register_file(
122 bynk_project::parse_cache::file_id_for(&abs),
123 pf.identity_path(),
124 );
125 }
126 }
127 }
128 pub fn push_for(&mut self, file: Option<&Path>, mut error: CompileError) {
129 display_canonical_names(&mut error);
132 let attributed = AttributedError::new(file.map(Path::to_path_buf), error);
133 match bynk_syntax::Severity::for_error(&attributed.error) {
134 bynk_syntax::Severity::Warning => self.warnings.push(attributed),
135 bynk_syntax::Severity::Error => self.entries.push(attributed),
136 }
137 }
138 pub fn extend_for(
139 &mut self,
140 file: Option<&Path>,
141 errs: impl IntoIterator<Item = CompileError>,
142 ) {
143 for e in errs {
144 self.push_for(file, e);
145 }
146 }
147 pub fn extend_attributed_by_span(
158 &mut self,
159 parsed: &[ParsedFile],
160 errs: impl IntoIterator<Item = CompileError>,
161 ) {
162 self.register_parsed(parsed);
163 for e in errs {
164 let file = self.files.get(&e.span.file).cloned();
165 self.push_for(file.as_deref(), e);
166 }
167 }
168 pub fn is_empty(&self) -> bool {
171 self.entries.is_empty()
172 }
173 pub fn into_warnings(self) -> Vec<AttributedError> {
175 let mut warnings = self.warnings;
176 attribute_labels(&self.files, &mut warnings);
177 warnings
178 }
179 pub fn into_all(self) -> Vec<AttributedError> {
182 let mut all = self.entries;
183 all.extend(self.warnings);
184 attribute_labels(&self.files, &mut all);
185 all
186 }
187 pub fn len(&self) -> usize {
189 self.entries.len()
190 }
191}
192
193fn attribute_labels(files: &HashMap<FileId, PathBuf>, errors: &mut [AttributedError]) {
203 for ae in errors {
204 let primary = ae.error.span.file;
205 ae.label_files = ae
206 .error
207 .labels
208 .iter()
209 .map(|(span, _)| {
210 if span.file == primary || span.file == FileId::UNKNOWN {
211 return LabelFile::Primary;
212 }
213 match files.get(&span.file) {
214 Some(path) if Some(path) == ae.source_path.as_ref() => LabelFile::Primary,
215 Some(path) => LabelFile::Other(path.clone()),
216 None => match firstparty_unit_for(span.file) {
217 Some(unit) => LabelFile::FirstParty {
218 identity_path: unit.identity_path,
219 source: unit.source,
220 },
221 None => LabelFile::Unavailable,
222 },
223 }
224 })
225 .collect();
226 }
227}
228
229pub struct AdapterBinding {
234 pub output_path: PathBuf,
236 pub content: String,
238 pub includes: Vec<IncludedFile>,
240}
241
242#[derive(Clone)]
244pub struct IncludedFile {
245 pub output_path: PathBuf,
247 pub bytes: Vec<u8>,
248}
249
250#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
256pub enum BuildTarget {
257 #[default]
260 Bundle,
261 Workers,
265}
266
267pub fn normalize_service_defaults(parsed: &mut [ParsedFile]) {
268 for pf in parsed.iter_mut() {
269 let items = match pf.unit_mut() {
270 SourceUnit::Commons(c) => &mut c.items,
271 SourceUnit::Context(c) => &mut c.items,
272 SourceUnit::Adapter(a) => &mut a.items,
273 SourceUnit::Suite(_) => continue,
274 };
275 for item in items.iter_mut() {
276 if let CommonsItem::Service(svc) = item {
277 inject_service_defaults(svc);
278 }
279 }
280 }
281}
282
283pub fn inject_service_defaults(svc: &mut ServiceDecl) {
288 let default_by = svc.default_by.clone();
289 let default_given = svc.default_given.clone();
290 if default_by.is_none() && default_given.is_empty() {
291 return;
292 }
293 for handler in svc.handlers.iter_mut() {
294 if handler.kind == HandlerKind::Rejected {
297 continue;
298 }
299 if handler.by_clause.is_none()
300 && let Some(def) = &default_by
301 {
302 handler.by_clause = Some(def.clone());
303 }
304 if handler.given.is_empty() && !default_given.is_empty() {
305 handler.given = default_given.clone();
306 }
307 }
308}
309
310#[allow(clippy::result_unit_err)]
328pub fn phase_discovery(
329 trees: &[(PathBuf, PathBuf)],
330 excludes: &[PathBuf],
331 errors: &mut ErrorSink,
332) -> Result<Vec<Vec<PathBuf>>, ()> {
333 let mut out = Vec::with_capacity(trees.len());
334 for (i, (root, _prefix)) in trees.iter().enumerate() {
335 match discover_bynk_files(root, excludes) {
336 Ok(f) => out.push(f),
337 Err(e) if i > 0 && e.category == "bynk.project.no_root" => {
343 out.push(Vec::new());
344 }
345 Err(e) => {
346 errors.push_for(None, e);
347 return Err(());
348 }
349 }
350 }
351 Ok(out)
352}
353
354#[allow(clippy::result_unit_err)]
360pub fn check_discovered_files(
361 trees: &[(PathBuf, PathBuf)],
362 file_lists: &[Vec<PathBuf>],
363 errors: &mut ErrorSink,
364) -> Result<(), ()> {
365 if file_lists.iter().all(|f| f.is_empty()) {
366 errors.push_for(
367 None,
368 CompileError::new(
369 "bynk.project.no_sources",
370 Span::default(),
371 format!(
372 "no `.bynk` source files found under {}",
373 trees[0].0.display()
374 ),
375 ),
376 );
377 return Err(());
378 }
379 for ((root, _prefix), files) in trees.iter().zip(file_lists.iter()) {
380 if let Err(e) = check_file_directory_conflicts(root, files) {
381 errors.extend_for(None, e);
382 }
383 }
384 Ok(())
385}
386
387pub const FIRSTPARTY_ID_BLOCK: u32 = 1_000_000;
399pub const FIRSTPARTY_ID_BASE: u32 = 1_000_000_000;
400
401pub const FIRSTPARTY_FILE_ID_BASE: u32 = 0xFFFF_0000;
410
411pub fn firstparty_file_id(unit: &firstparty::Unit) -> FileId {
413 FileId(FIRSTPARTY_FILE_ID_BASE + unit.id_block)
414}
415
416fn firstparty_unit_for(file: FileId) -> Option<&'static firstparty::Unit> {
418 firstparty::UNITS
419 .iter()
420 .find(|unit| firstparty_file_id(unit) == file)
421}
422
423pub fn firstparty_parsed(
424 cache: &'static OnceLock<Result<ParsedFile, Vec<CompileError>>>,
425 identity_path: &'static str,
426 src: &'static str,
427 kind: UnitKind,
428 id_base: u32,
439 file: FileId,
442) -> Result<ParsedFile, Vec<CompileError>> {
443 cache
444 .get_or_init(|| {
445 lexer::tokenize_in(src, file)
446 .map_err(|e| vec![e])
447 .and_then(|toks| {
448 parser::parse_unit_with_warnings_from(&toks, src, &mut { id_base })
449 .map(|(unit, _warnings)| unit)
450 })
451 .map(|unit| {
452 ParsedFile::synthetic(
453 PathBuf::from(identity_path),
454 PathBuf::from(identity_path),
455 src.to_string(),
456 unit,
457 kind,
458 )
459 })
460 })
461 .clone()
462}
463
464fn all_syntax_errors(source: &str, strict: Vec<CompileError>) -> Vec<CompileError> {
468 let Ok(tokens) = bynk_syntax::lexer::tokenize(source) else {
469 return strict;
470 };
471 let recovered = bynk_syntax::parser::parse_units_recovering(&tokens, source).errors;
472 bynk_syntax::parser::merge_syntax_errors(strict, recovered)
473}
474
475#[allow(clippy::too_many_arguments)]
482#[allow(clippy::result_unit_err)]
483pub fn phase_parse(
484 trees: &[(PathBuf, PathBuf)],
487 file_lists: &[Vec<PathBuf>],
488 overlay: &HashMap<PathBuf, String>,
489 errors: &mut ErrorSink,
490 snapshots: &mut Vec<(PathBuf, String)>,
491) -> Result<(Vec<ParsedFile>, InjectedUnits, BrokenDeclNames), ()> {
492 let mut parsed: Vec<ParsedFile> = Vec::new();
493 let mut broken: BrokenDeclNames = HashMap::new();
494 let parse_tree = |root: &Path,
501 prefix: &Path,
502 files: &[PathBuf],
503 parsed: &mut Vec<ParsedFile>,
504 broken: &mut BrokenDeclNames,
505 errors: &mut ErrorSink,
506 snapshots: &mut Vec<(PathBuf, String)>| {
507 for path in files {
508 let rel = path.strip_prefix(root).unwrap_or(path).to_path_buf();
510 let id = prefix.join(&rel);
513 let source = match read_source(path, overlay) {
514 Ok(s) => s,
515 Err(e) => {
516 errors.push_for(
517 Some(&id),
518 CompileError::new(
519 "bynk.project.read_failed",
520 Span::default(),
521 format!("could not read `{}`: {e}", path.display()),
522 ),
523 );
524 continue;
525 }
526 };
527 snapshots.push((id.clone(), source.clone()));
528 match parse_sources(root, prefix, path, source) {
529 Ok((pfs, warnings)) => {
530 parsed.extend(pfs);
531 errors.extend_for(Some(&id), warnings);
534 }
535 Err(errs) => {
542 let source = snapshots.last().expect("pushed just above").1.clone();
543 match parse_sources_recovering(root, prefix, path, source.clone()) {
544 Some(recovered) => {
545 errors.extend_for(
546 Some(&id),
547 bynk_syntax::parser::merge_syntax_errors(errs, recovered.errors),
548 );
549 for pf in &recovered.files {
554 broken
555 .entry(pf.unit().name().joined())
556 .or_default()
557 .extend(recovered.broken_decl_names.iter().cloned());
558 }
559 parsed.extend(recovered.files);
560 }
561 None => errors.extend_for(Some(&id), all_syntax_errors(&source, errs)),
562 }
563 }
564 }
565 }
566 };
567 for ((root, prefix), files) in trees.iter().zip(file_lists.iter()) {
568 parse_tree(
569 root,
570 prefix,
571 files,
572 &mut parsed,
573 &mut broken,
574 errors,
575 snapshots,
576 );
577 }
578 errors.register_parsed(&parsed);
579 if !errors.is_empty() && parsed.is_empty() {
580 return Err(());
581 }
582
583 let injected = inject_firstparty(&mut parsed, errors);
584 Ok((parsed, injected, broken))
585}
586
587pub type InjectedUnits = Vec<&'static firstparty::Unit>;
594
595fn inject_firstparty(parsed: &mut Vec<ParsedFile>, errors: &mut ErrorSink) -> InjectedUnits {
606 const N: usize = firstparty::UNITS.len();
607 static CACHES: [OnceLock<Result<ParsedFile, Vec<CompileError>>>; N] =
608 [const { OnceLock::new() }; N];
609 let reaches = |pf: &ParsedFile, unit: &firstparty::Unit| match unit.kind {
610 firstparty::UnitKind::Adapter => {
611 pf.consumes().iter().any(|c| c.target.joined() == unit.name)
612 }
613 firstparty::UnitKind::Commons => pf.uses().iter().any(|u| u.target.joined() == unit.name),
614 };
615 let mut reached: Vec<Option<Option<ParsedFile>>> = vec![None; N];
616 loop {
617 let mut grew = false;
618 for (i, unit) in firstparty::UNITS.iter().enumerate() {
619 if reached[i].is_some() {
620 continue;
621 }
622 let injected = reached.iter().flatten().flatten();
623 if !parsed.iter().chain(injected).any(|pf| reaches(pf, unit)) {
624 continue;
625 }
626 let kind = match unit.kind {
627 firstparty::UnitKind::Adapter => UnitKind::Adapter,
628 firstparty::UnitKind::Commons => UnitKind::Commons,
629 };
630 let parse = firstparty_parsed(
631 &CACHES[i],
632 unit.identity_path,
633 unit.source,
634 kind,
635 FIRSTPARTY_ID_BASE + unit.id_block * FIRSTPARTY_ID_BLOCK,
636 firstparty_file_id(unit),
637 );
638 reached[i] = Some(match parse {
639 Ok(pf) => Some(pf),
640 Err(errs) => {
641 errors.extend_for(None, errs);
642 None
643 }
644 });
645 grew = true;
646 }
647 if !grew {
648 break;
649 }
650 }
651 let mut order: Vec<usize> = (0..N).filter(|&i| reached[i].is_some()).collect();
652 order.sort_by_key(|&i| firstparty::UNITS[i].id_block);
653 for &i in &order {
654 if let Some(Some(pf)) = reached[i].take() {
655 parsed.push(pf);
656 }
657 }
658 order.into_iter().map(|i| &firstparty::UNITS[i]).collect()
659}
660
661pub fn phase_resolve_derived(
669 groups: &BTreeMap<String, Vec<usize>>,
670 kinds: &BTreeMap<String, UnitKind>,
671 parsed: &[ParsedFile],
672 unit_tables: &mut HashMap<String, UnitTable>,
673 errors: &mut ErrorSink,
674) {
675 for (name, indices) in groups {
676 for &i in indices {
677 if parsed[i].is_synthetic() {
678 continue;
679 }
680 let path = parsed[i].identity_path();
681 for item in parsed[i].items() {
682 let CommonsItem::Capability(c) = item else {
683 continue;
684 };
685 let Some(d) = &c.derived else { continue };
686 if kinds.get(name) != Some(&UnitKind::Context) {
687 errors.push_for(
688 Some(&path),
689 CompileError::new(
690 "bynk.capability.derived_outside_context",
691 c.span,
692 format!(
693 "derived capability `{}` is declared outside a context",
694 c.name.name
695 ),
696 )
697 .with_note("a named instance of a platform resource belongs to the context that owns its data"),
698 );
699 continue;
700 }
701 let Some(table) = unit_tables.get(name) else {
702 continue;
703 };
704 let Some(owner) = table.flattened_caps.get(&d.base.name).cloned() else {
705 errors.push_for(
706 Some(&path),
707 CompileError::new(
708 "bynk.capability.unknown_base",
709 d.base.span,
710 format!(
711 "`{}` is not a capability this context selects with `consumes U {{ … }}`",
712 d.base.name
713 ),
714 )
715 .with_note(format!(
716 "a derived capability's base is a platform capability, selected by name: `consumes bynk.cloudflare {{ {} }}`",
717 d.base.name
718 )),
719 );
720 continue;
721 };
722 let Some(kind) = crate::firstparty::instantiable(&owner, &d.base.name) else {
723 errors.push_for(
724 Some(&path),
725 CompileError::new(
726 "bynk.capability.base_not_instantiable",
727 d.base.span,
728 format!(
729 "`{owner}.{}` has no named instances: a derived capability's base must be a platform resource kind",
730 d.base.name
731 ),
732 ),
733 );
734 continue;
735 };
736 let mut options: Vec<(String, crate::symbols::OptionValue)> = Vec::new();
738 for ann in &d.annotations {
739 if let Some(option_kind) = kind.option_kind(&ann.name.name) {
740 if options.iter().any(|(o, _)| *o == ann.name.name) {
741 errors.push_for(
742 Some(&path),
743 CompileError::new(
744 "bynk.capability.invalid_option",
745 ann.span,
746 format!("`@{}` is given twice", ann.name.name),
747 )
748 .with_note("an instance takes each option once"),
749 );
750 } else if let Some(value) =
751 instance_option_value(ann, option_kind, &d.base.name, &path, errors)
752 {
753 let own = match &value {
758 crate::symbols::OptionValue::Str(value)
759 if ann.name.name == firstparty::SERVICE_WORKER_OPTION =>
760 {
761 kinds.iter().find(|(unit, kind)| {
762 **kind == UnitKind::Context
763 && bynk_project::worker_dir_name(unit) == *value
764 })
765 }
766 _ => None,
767 };
768 if let Some((context, _)) = own {
769 errors.push_for(
770 Some(&path),
771 CompileError::new(
772 "bynk.capability.invalid_option",
773 ann.span,
774 format!(
775 "`{}` is this project's context `{context}`, not a Worker outside it",
776 bynk_project::worker_dir_name(context)
777 ),
778 )
779 .with_note(format!(
780 "reach a Bynk context with `consumes {context}`, which checks its contract"
781 )),
782 );
783 } else {
784 options.push((ann.name.name.clone(), value));
785 }
786 }
787 }
788 if kind.option_kind(&ann.name.name).is_none() {
789 let allowed = if kind.options.is_empty() {
790 format!("`{}` instances take no options", d.base.name)
791 } else {
792 format!(
793 "`{}` instances take {}",
794 d.base.name,
795 kind.option_names()
796 .map(|o| format!("`@{o}`"))
797 .collect::<Vec<_>>()
798 .join(", ")
799 )
800 };
801 errors.push_for(
802 Some(&path),
803 CompileError::new(
804 "bynk.capability.unknown_option",
805 ann.span,
806 format!(
807 "`@{}` is not an option of a `{}` instance",
808 ann.name.name, d.base.name
809 ),
810 )
811 .with_note(allowed),
812 );
813 }
814 }
815 for required in kind.required {
816 if !d.annotations.iter().any(|a| a.name.name == *required) {
817 errors.push_for(
818 Some(&path),
819 CompileError::new(
820 "bynk.capability.missing_option",
821 c.span,
822 format!("a `{}` instance needs `@{required}(…)`", d.base.name),
823 )
824 .with_note(format!("write `{}`", instance_example(kind))),
825 );
826 }
827 }
828 if table.exported_capabilities.contains(&c.name.name) {
829 errors.push_for(
830 Some(&path),
831 CompileError::new(
832 "bynk.exports.derived_capability",
833 c.span,
834 format!("derived capability `{}` cannot be exported", c.name.name),
835 )
836 .with_note("an instance's data belongs to this context; another context reaches it through this context's services"),
837 );
838 }
839 let ops = unit_tables
840 .get(&owner)
841 .and_then(|t| t.capabilities.get(&d.base.name))
842 .map(|base| base.ops.clone())
843 .unwrap_or_default();
844 let binding = crate::worker_bindings::instance_binding_name(
845 kind.binding_prefix,
846 &c.name.name,
847 );
848 let table = unit_tables.get_mut(name).expect("checked above");
849 if crate::symbols::is_edge_cache_base(&owner, &d.base.name)
852 && let Some((earlier, _)) =
853 table.derived.iter().find(|(_, inst)| inst.is_edge_cache())
854 {
855 errors.push_for(
856 Some(&path),
857 CompileError::new(
858 "bynk.cloudflare.edge_cache_duplicate",
859 c.span,
860 format!(
861 "context `{name}` already declares `EdgeCache` instance `{earlier}`"
862 ),
863 )
864 .with_note(format!(
865 "the edge cache is one setting for the whole Worker, so one instance turns it on and purges it; use `{earlier}`"
866 )),
867 );
868 continue;
869 }
870 if let Some(decl) = table.capabilities.get_mut(&c.name.name) {
871 decl.ops = ops;
872 }
873 table.derived.insert(
874 c.name.name.clone(),
875 crate::symbols::DerivedInstance {
876 base_unit: owner,
877 base_cap: d.base.name.clone(),
878 binding,
879 options,
880 span: c.span,
881 },
882 );
883 }
884 }
885 }
886 phase_edge_cache_reach(groups, parsed, unit_tables, errors);
887}
888
889pub fn phase_edge_cache_reach(
901 groups: &BTreeMap<String, Vec<usize>>,
902 parsed: &[ParsedFile],
903 unit_tables: &HashMap<String, UnitTable>,
904 errors: &mut ErrorSink,
905) {
906 for (name, indices) in groups {
907 let Some(table) = unit_tables.get(name) else {
908 continue;
909 };
910 let mut reaches: BTreeMap<String, String> = table
913 .derived
914 .iter()
915 .filter(|(_, d)| d.is_edge_cache())
916 .map(|(cap, _)| (cap.clone(), cap.clone()))
917 .collect();
918 if reaches.is_empty() {
919 continue;
920 }
921 loop {
922 let mut grew = false;
923 for (cap, provider) in &table.providers {
924 if reaches.contains_key(cap) {
925 continue;
926 }
927 let via = provider
928 .given
929 .iter()
930 .filter(|g| !g.is_cross_context())
931 .find_map(|g| reaches.get(g.key()).cloned());
932 if let Some(instance) = via {
933 reaches.insert(cap.clone(), instance);
934 grew = true;
935 }
936 }
937 if !grew {
938 break;
939 }
940 }
941 for &i in indices {
942 let path = parsed[i].identity_path();
943 for item in parsed[i].items() {
944 if let CommonsItem::Capability(c) = item
947 && c.derived.is_none()
948 && table.exported_capabilities.contains(&c.name.name)
949 && let Some(instance) = reaches.get(&c.name.name)
950 {
951 errors.push_for(
952 Some(&path),
953 CompileError::new(
954 "bynk.cloudflare.edge_cache_exported",
955 c.span,
956 format!(
957 "exported capability `{}` reaches `EdgeCache` instance `{instance}`, which purges only from this context's own Worker",
958 c.name.name
959 ),
960 )
961 .with_note(
962 "a context that consumes it would build its provider in its own Worker, whose purge misses this Worker's cache; export a service that purges instead",
963 ),
964 );
965 }
966 if let CommonsItem::Service(svc) = item
972 && matches!(svc.protocol, ServiceProtocol::Http)
973 {
974 for h in &svc.handlers {
975 let public = h.annotations.iter().any(|a| {
976 a.name.name == "cache"
977 && a.args.iter().any(|arg| {
978 arg.label.as_ref().is_some_and(|l| l.name == "scope")
979 && matches!(&arg.value.kind, ExprKind::Ident(id) if id.name == "public")
980 })
981 });
982 let Some(by) = h.by_clause.as_ref().or(svc.default_by.as_ref()) else {
983 continue;
984 };
985 let closed = by.actors.iter().find(|a| {
986 let single = ByClause {
987 binder: None,
988 actors: vec![(*a).clone()],
989 span: by.span,
990 };
991 !crate::actors::sum_has_open_member(&single, &table.actors)
992 });
993 if let (true, Some(actor)) = (public, closed) {
994 errors.push_for(
995 Some(&path),
996 CompileError::new(
997 "bynk.cloudflare.edge_cache_public_authenticated",
998 by.span,
999 format!(
1000 "a `@cache(scope: public)` route by `{}` would share one caller's response with every caller at Cloudflare's edge",
1001 actor.name
1002 ),
1003 )
1004 .with_note(format!(
1005 "context `{name}` declares `EdgeCache`, and the edge cache keys a response by its URL alone; use `scope: private`, or serve the route `by Visitor`"
1006 )),
1007 );
1008 }
1009 }
1010 }
1011 let (handlers, code, place, note) = match item {
1012 CommonsItem::Agent(a) => (
1013 &a.handlers,
1014 "bynk.cloudflare.edge_cache_in_agent",
1015 format!("agent `{}`", a.name.name),
1016 "an agent runs in a Durable Object, whose purge would not reach this Worker's cache; purge from the service handler that calls the agent",
1017 ),
1018 CommonsItem::Service(svc)
1019 if matches!(svc.protocol, ServiceProtocol::Queue { .. }) =>
1020 {
1021 (
1022 &svc.handlers,
1023 "bynk.cloudflare.edge_cache_in_queue",
1024 format!("queue consumer `{}`", svc.name.name),
1025 "a queue consumer is not the entrypoint the cache serves, and its purge is not known to reach that cache; purge from a service handler",
1026 )
1027 }
1028 CommonsItem::Service(svc) if matches!(svc.protocol, ServiceProtocol::Cron) => (
1029 &svc.handlers,
1030 "bynk.cloudflare.edge_cache_in_schedule",
1031 format!("`on schedule` service `{}`", svc.name.name),
1032 "Cloudflare accepts a purge from a cron run but it clears nothing from the cache the Worker's `fetch` serves; purge from a service handler",
1033 ),
1034 _ => continue,
1035 };
1036 for h in handlers {
1037 for g in h.given.iter().filter(|g| !g.is_cross_context()) {
1038 let Some(instance) = reaches.get(g.key()) else {
1039 continue;
1040 };
1041 let how = if g.key() == instance {
1042 String::new()
1043 } else {
1044 format!(" (through `{}`)", g.key())
1045 };
1046 errors.push_for(
1047 Some(&path),
1048 CompileError::new(
1049 code,
1050 g.span,
1051 format!(
1052 "{place} reaches `EdgeCache` instance `{instance}`{how}, which purges only from the Worker's `fetch`"
1053 ),
1054 )
1055 .with_note(note),
1056 );
1057 }
1058 }
1059 }
1060 }
1061 }
1062}
1063
1064pub(crate) fn instance_example(kind: &firstparty::Instantiable) -> String {
1068 let name = match kind.capability {
1069 "Service" => "Billing",
1070 "RateLimit" => "LoginLimit",
1071 "EdgeCache" => "Edge",
1072 _ => "Archives",
1073 };
1074 let mut out = format!("capability {name} = {}", kind.capability);
1075 for required in kind.required {
1076 out.push_str(&format!(" @{required}({})", option_sample(required)));
1077 }
1078 out
1079}
1080
1081fn option_sample(option: &str) -> &'static str {
1083 match option {
1084 firstparty::R2_JURISDICTION_OPTION => "\"eu\"",
1085 firstparty::RATELIMIT_LIMIT_OPTION => "100",
1086 firstparty::RATELIMIT_PERIOD_OPTION => "60.seconds",
1087 _ => "\"legacy-billing\"",
1088 }
1089}
1090
1091fn instance_option_value(
1099 ann: &bynk_syntax::ast::Annotation,
1100 kind: firstparty::OptionKind,
1101 base: &str,
1102 path: &std::path::Path,
1103 errors: &mut ErrorSink,
1104) -> Option<crate::symbols::OptionValue> {
1105 use crate::symbols::OptionValue;
1106 let option = ann.name.name.as_str();
1107 let invalid = |span, message: String| {
1108 CompileError::new("bynk.capability.invalid_option", span, message).with_note(format!(
1109 "write `@{option}({})`, for example",
1110 option_sample(option)
1111 ))
1112 };
1113 let [arg] = ann.args.as_slice() else {
1114 errors.push_for(
1115 Some(path),
1116 invalid(
1117 ann.span,
1118 format!("`@{option}` takes one positional argument"),
1119 ),
1120 );
1121 return None;
1122 };
1123 if arg.label.is_some() {
1124 errors.push_for(
1125 Some(path),
1126 invalid(
1127 ann.span,
1128 format!("`@{option}` takes one positional argument"),
1129 ),
1130 );
1131 return None;
1132 }
1133 let value = match (kind, &arg.value.kind) {
1134 (firstparty::OptionKind::Str, ExprKind::StrLit(value)) => OptionValue::Str(value.clone()),
1135 (firstparty::OptionKind::Str, _) => {
1136 errors.push_for(
1137 Some(path),
1138 invalid(arg.span, format!("`@{option}` takes a string literal")),
1139 );
1140 return None;
1141 }
1142 (firstparty::OptionKind::PositiveInt, ExprKind::IntLit { value, .. }) if *value > 0 => {
1143 OptionValue::Int(*value)
1144 }
1145 (firstparty::OptionKind::PositiveInt, _) => {
1146 errors.push_for(
1147 Some(path),
1148 invalid(
1149 arg.span,
1150 format!("`@{option}` takes a positive `Int` literal"),
1151 ),
1152 );
1153 return None;
1154 }
1155 (firstparty::OptionKind::Duration, ExprKind::DurationLit { millis, .. })
1156 if *millis % 1000 == 0 && firstparty::RATELIMIT_PERIODS.contains(&(*millis / 1000)) =>
1157 {
1158 OptionValue::Seconds(*millis / 1000)
1159 }
1160 (firstparty::OptionKind::Duration, _) => {
1161 errors.push_for(
1162 Some(path),
1163 CompileError::new(
1164 "bynk.capability.invalid_option",
1165 arg.span,
1166 format!("`@{option}` must be a window a `{base}` offers"),
1167 )
1168 .with_note(format!(
1169 "the windows are {}",
1170 firstparty::RATELIMIT_PERIODS
1171 .iter()
1172 .map(|s| format!("`{s}.seconds`"))
1173 .collect::<Vec<_>>()
1174 .join(" and ")
1175 )),
1176 );
1177 return None;
1178 }
1179 };
1180 let OptionValue::Str(text) = &value else {
1181 return Some(value);
1182 };
1183 if option == crate::firstparty::R2_JURISDICTION_OPTION
1184 && !crate::firstparty::R2_JURISDICTIONS.contains(&text.as_str())
1185 {
1186 errors.push_for(
1187 Some(path),
1188 CompileError::new(
1189 "bynk.capability.invalid_option",
1190 ann.span,
1191 format!("`{text}` is not a jurisdiction a `{base}` bucket can be pinned to"),
1192 )
1193 .with_note(format!(
1194 "the jurisdictions are {}",
1195 crate::firstparty::R2_JURISDICTIONS
1196 .iter()
1197 .map(|j| format!("`\"{j}\"`"))
1198 .collect::<Vec<_>>()
1199 .join(" and ")
1200 )),
1201 );
1202 return None;
1203 }
1204 if option == crate::firstparty::SERVICE_WORKER_OPTION && !is_worker_name(text) {
1205 errors.push_for(
1206 Some(path),
1207 CompileError::new(
1208 "bynk.capability.invalid_option",
1209 ann.span,
1210 format!("`{text}` is not a Worker name a `{base}` can reach"),
1211 )
1212 .with_note(
1213 "a Worker name is 1 to 63 lowercase letters, digits, `-` or `_`, and does not start or end with `-`",
1214 ),
1215 );
1216 return None;
1217 }
1218 Some(value)
1219}
1220
1221fn is_worker_name(name: &str) -> bool {
1223 (1..=63).contains(&name.len())
1224 && name
1225 .bytes()
1226 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
1227 && !name.starts_with('-')
1228 && !name.ends_with('-')
1229}
1230
1231pub type BrokenDeclNames = HashMap<String, Vec<String>>;
1236
1237pub fn tree_root_for<'a>(trees: &'a [(PathBuf, PathBuf)], pf: &ParsedFile) -> &'a Path {
1255 let Some(abs) = pf.abs_path() else {
1256 return trees[0].0.as_path();
1257 };
1258 trees
1259 .iter()
1260 .filter_map(|(root, _)| {
1261 std::path::absolute(root)
1262 .ok()
1263 .map(|abs_root| (root, abs_root))
1264 })
1265 .filter(|(_, abs_root)| abs.starts_with(abs_root))
1266 .max_by_key(|(root, _)| root.as_os_str().len())
1267 .map(|(root, _)| root.as_path())
1268 .unwrap_or_else(|| trees[0].0.as_path())
1269}
1270
1271fn resolve_binding_includes(
1278 pf: &ParsedFile,
1279 a: &AdapterDecl,
1280 b: &BindingDecl,
1281 adapter_dir: &Path,
1282 adapter_src_dir: &Path,
1283 errors: &mut ErrorSink,
1284) -> Vec<IncludedFile> {
1285 let adapter = a.name.joined();
1286 let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
1287 let mut out = Vec::new();
1288 for inc in &b.includes {
1289 let raw = Path::new(&inc.path);
1290 let escapes = raw.is_absolute()
1291 || raw.components().any(|c| {
1292 matches!(
1293 c,
1294 std::path::Component::ParentDir
1295 | std::path::Component::RootDir
1296 | std::path::Component::Prefix(_)
1297 )
1298 });
1299 if escapes {
1300 errors.push_for(
1301 Some(&pf.identity_path()),
1302 CompileError::new(
1303 "bynk.adapter.include_outside",
1304 inc.span,
1305 format!(
1306 "adapter `{adapter}` includes `{}`, which is outside the adapter's directory",
1307 inc.path
1308 ),
1309 )
1310 .with_note("an included path is relative to the adapter's file and may not be absolute or contain `..`"),
1311 );
1312 continue;
1313 }
1314 let rel = normalize_rel(raw);
1315 if rel.extension().and_then(|e| e.to_str()) != Some("wasm") {
1316 errors.push_for(
1317 Some(&pf.identity_path()),
1318 CompileError::new(
1319 "bynk.adapter.include_kind",
1320 inc.span,
1321 format!(
1322 "adapter `{adapter}` includes `{}`, which is not a `.wasm` file",
1323 inc.path
1324 ),
1325 )
1326 .with_note("only WebAssembly modules (`.wasm`) can be included beside a binding"),
1327 );
1328 continue;
1329 }
1330 if !seen.insert(rel.clone()) {
1331 errors.push_for(
1332 Some(&pf.identity_path()),
1333 CompileError::new(
1334 "bynk.adapter.include_duplicate",
1335 inc.span,
1336 format!("adapter `{adapter}` includes `{}` more than once", inc.path),
1337 )
1338 .with_note("remove the repeated entry"),
1339 );
1340 continue;
1341 }
1342 match read_adapter_include(&adapter_src_dir.join(&rel)) {
1343 Ok(bytes) => out.push(IncludedFile {
1344 output_path: normalize_rel(&adapter_dir.join(&rel)),
1345 bytes,
1346 }),
1347 Err(e) => errors.push_for(
1348 Some(&pf.identity_path()),
1349 CompileError::new(
1350 "bynk.adapter.include_missing",
1351 inc.span,
1352 format!(
1353 "adapter `{adapter}` includes `{}`, which could not be read ({e})",
1354 inc.path
1355 ),
1356 )
1357 .with_note("the path is resolved relative to the adapter's source file"),
1358 ),
1359 }
1360 }
1361 out
1362}
1363
1364#[allow(clippy::type_complexity)]
1372#[allow(clippy::too_many_arguments)]
1373pub fn phase_group(
1374 parsed: &[ParsedFile],
1375 trees: &[(PathBuf, PathBuf)],
1376 platform: Platform,
1377 injected: &[&'static firstparty::Unit],
1378 overlay: &HashMap<PathBuf, String>,
1379 errors: &mut ErrorSink,
1380) -> (
1381 BTreeMap<String, Vec<usize>>,
1382 BTreeMap<String, UnitKind>,
1383 BTreeMap<String, Vec<usize>>,
1384 BTreeMap<String, Vec<usize>>,
1385 HashMap<String, AdapterBinding>,
1386 std::collections::BTreeMap<String, String>,
1387) {
1388 let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1392 let mut kinds: BTreeMap<String, UnitKind> = BTreeMap::new();
1393 let mut test_groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1394 let mut integration_groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
1397 for (i, pf) in parsed.iter().enumerate() {
1398 let name = pf.unit().name().joined();
1399 if pf.kind() == UnitKind::Integration {
1400 integration_groups.entry(name).or_default().push(i);
1401 } else if pf.kind() == UnitKind::Test {
1402 test_groups.entry(name).or_default().push(i);
1403 } else {
1404 groups.entry(name.clone()).or_default().push(i);
1405 kinds.entry(name).or_insert(pf.kind());
1406 }
1407 }
1408 if let Err(e) = check_directory_name_consistency(parsed) {
1412 for (path, err) in e {
1413 errors.push_for(Some(&path), err);
1414 }
1415 }
1416 if let Err(e) = check_directory_kind_consistency(parsed) {
1417 errors.extend_for(None, e);
1418 }
1419 if let Err(e) = check_group_kind_consistency(parsed, &groups) {
1422 for (path, err) in e {
1423 errors.push_for(Some(&path), err);
1424 }
1425 }
1426 if let Err(e) = check_group_module_docs(parsed, &groups) {
1429 for (path, err) in e {
1430 errors.push_for(Some(&path), err);
1431 }
1432 }
1433 if let Err(e) = check_path_name_alignment(parsed) {
1437 for (path, err) in e {
1438 errors.push_for(Some(&path), err);
1439 }
1440 }
1441
1442 phase_function_type_boundaries(parsed, errors);
1449
1450 for pf in parsed {
1453 if pf.is_synthetic() {
1454 continue;
1455 }
1456 let qn = pf.unit().name();
1457 if qn.parts.first().is_some_and(|p| p.name == "bynk") {
1458 errors.push_for(Some(&pf.identity_path()),
1459 CompileError::new(
1460 "bynk.namespace.reserved",
1461 qn.span,
1462 format!(
1463 "`{}` uses the reserved `bynk` namespace — the `bynk` root is reserved for the toolchain's conformance surface",
1464 qn.joined()
1465 ),
1466 )
1467 .with_note("rename the unit so its first segment is not `bynk`"),
1468 );
1469 }
1470 }
1471
1472 for pf in parsed {
1476 if pf.is_synthetic() {
1477 continue;
1478 }
1479 if let Some(a) = pf.adapter() {
1480 let has_external = a
1481 .items
1482 .iter()
1483 .any(|it| matches!(it, CommonsItem::Provider(p) if p.external));
1484 if has_external && a.binding.is_none() {
1485 errors.push_for(Some(&pf.identity_path()),
1486 CompileError::new(
1487 "bynk.adapter.no_binding",
1488 a.span,
1489 format!(
1490 "adapter `{}` declares an external provider but has no `binding` clause to supply its implementation",
1491 a.name.joined()
1492 ),
1493 )
1494 .with_note(
1495 "add a `binding \"<module>\"` clause naming the TypeScript module that exports the provider symbols",
1496 ),
1497 );
1498 }
1499 }
1500 }
1501
1502 let mut adapter_bindings: HashMap<String, AdapterBinding> = HashMap::new();
1506 for unit in injected {
1510 if let Some((output_path, content)) = unit.binding_for(platform) {
1511 adapter_bindings.insert(
1512 unit.name.to_string(),
1513 AdapterBinding {
1514 output_path: PathBuf::from(output_path),
1515 content: content.to_string(),
1516 includes: Vec::new(),
1517 },
1518 );
1519 }
1520 }
1521 for pf in parsed {
1522 let Some(a) = pf.adapter() else { continue };
1523 let Some(b) = &a.binding else { continue };
1524 let pf_source_path = pf.source_path();
1525 let adapter_dir = pf_source_path.parent().unwrap_or(Path::new(""));
1526 let out_rel = normalize_rel(&adapter_dir.join(&b.module));
1527 let src_abs = match (pf.package(), pf.abs_path()) {
1531 (Some(_), Some(abs)) => {
1532 let dir = abs.parent().unwrap_or(Path::new("")).to_path_buf();
1533 dir.join(normalize_rel(Path::new(&b.module)))
1534 }
1535 _ => tree_root_for(trees, pf).join(&out_rel),
1536 };
1537 match read_adapter_binding(&src_abs, overlay) {
1538 Ok(content) => {
1539 let adapter_src_dir = match (pf.package(), pf.abs_path()) {
1542 (Some(_), Some(abs)) => abs.parent().unwrap_or(Path::new("")).to_path_buf(),
1543 _ => tree_root_for(trees, pf).join(adapter_dir),
1544 };
1545 let includes =
1546 resolve_binding_includes(pf, a, b, adapter_dir, &adapter_src_dir, errors);
1547 adapter_bindings.insert(
1548 a.name.joined(),
1549 AdapterBinding {
1550 output_path: out_rel,
1551 content,
1552 includes,
1553 },
1554 );
1555 }
1556 Err(e) => {
1557 errors.push_for(Some(&pf.identity_path()),
1558 CompileError::new(
1559 "bynk.adapter.no_binding",
1560 b.module_span,
1561 format!(
1562 "adapter `{}` names binding module `{}`, which could not be read ({e})",
1563 a.name.joined(),
1564 b.module
1565 ),
1566 )
1567 .with_note(
1568 "the binding path is resolved relative to the adapter's source file; author the `.binding.ts` there",
1569 ),
1570 );
1571 }
1572 }
1573 }
1574
1575 let mut npm_deps: std::collections::BTreeMap<String, String> =
1580 std::collections::BTreeMap::new();
1581 let mut first_dep: HashMap<String, (String, PathBuf, Span)> = HashMap::new();
1583 for pf in parsed {
1584 let Some(a) = pf.adapter() else { continue };
1585 let Some(b) = &a.binding else { continue };
1586 for dep in &b.requires {
1587 if is_unpinned_range(&dep.range) {
1588 errors.push_for(Some(&pf.identity_path()),
1589 CompileError::new(
1590 "bynk.requires.unpinned_dependency",
1591 dep.span,
1592 format!(
1593 "dependency `{}` has an unpinned version range `{}` — pin a concrete range (e.g. `^1.2.0`)",
1594 dep.package, dep.range
1595 ),
1596 )
1597 .with_note(
1598 "unpinned ranges (`*`, `latest`, …) make builds irreproducible and are rejected",
1599 ),
1600 );
1601 continue;
1602 }
1603 let adapter = a.name.joined();
1604 let identity = pf.identity_path();
1605 if let Some(range) = npm_deps.get(&dep.package) {
1606 if *range != dep.range {
1607 let (first_adapter, first_path, first_span) = &first_dep[&dep.package];
1608 let message = if *first_adapter == adapter {
1609 format!(
1610 "adapter `{adapter}` requires `{}` at both `{range}` and `{}`",
1611 dep.package, dep.range
1612 )
1613 } else {
1614 format!(
1615 "adapter `{adapter}` requires `{}` at `{}`, but adapter `{first_adapter}` requires it at `{range}`",
1616 dep.package, dep.range
1617 )
1618 };
1619 let mut err =
1620 CompileError::new("bynk.requires.conflicting_range", dep.span, message);
1621 if *first_path == identity {
1622 err = err.with_label(*first_span, "first required here");
1623 }
1624 errors.push_for(
1625 Some(&identity),
1626 err.with_note(format!(
1627 "`{}` is first required at `{range}` by adapter `{first_adapter}` (in `{}`); the project installs one version of each package, so every adapter must state the same range",
1628 dep.package,
1629 first_path
1630 .components()
1631 .map(|c| c.as_os_str().to_string_lossy())
1632 .collect::<Vec<_>>()
1633 .join("/")
1634 )),
1635 );
1636 }
1637 continue;
1638 }
1639 first_dep.insert(dep.package.clone(), (adapter, identity, dep.span));
1640 npm_deps.insert(dep.package.clone(), dep.range.clone());
1641 }
1642 }
1643
1644 (
1645 groups,
1646 kinds,
1647 test_groups,
1648 integration_groups,
1649 adapter_bindings,
1650 npm_deps,
1651 )
1652}
1653
1654pub fn phase_function_type_boundaries(parsed: &[ParsedFile], errors: &mut ErrorSink) {
1676 let types = collect_type_decls(parsed.iter().flat_map(|pf| pf.items()));
1681 for pf in parsed {
1682 let mut file_errors: Vec<CompileError> = Vec::new();
1683 check_function_type_boundary_items(pf.items(), &types, &mut file_errors);
1684 for err in file_errors {
1685 errors.push_for(Some(&pf.identity_path()), err);
1686 }
1687 }
1688}
1689
1690pub fn collect_type_decls<'a>(
1696 items: impl Iterator<Item = &'a CommonsItem>,
1697) -> HashMap<String, Arc<TypeDecl>> {
1698 let mut out = HashMap::new();
1699 for item in items {
1700 match item {
1701 CommonsItem::Type(t) => {
1702 out.entry(t.name.name.clone())
1703 .or_insert_with(|| Arc::new(t.clone()));
1704 }
1705 CommonsItem::Event(e) => {
1709 out.entry(e.name.name.clone())
1710 .or_insert_with(|| Arc::new(e.as_type_decl()));
1711 }
1712 _ => {}
1713 }
1714 }
1715 out
1716}
1717
1718pub fn check_function_type_boundary_items(
1722 items: &[CommonsItem],
1723 types: &HashMap<String, Arc<TypeDecl>>,
1724 errors: &mut Vec<CompileError>,
1725) {
1726 for item in items {
1727 match item {
1728 CommonsItem::Type(t) => match &t.body {
1729 TypeBody::Record(r) => {
1730 for f in &r.fields {
1731 reject_fn_types(&f.type_ref, "a record field", types, errors);
1732 }
1733 }
1734 TypeBody::Sum(s) => {
1735 for v in &s.variants {
1736 for p in &v.payload {
1737 reject_fn_types(&p.type_ref, "a sum-variant payload", types, errors);
1738 }
1739 }
1740 }
1741 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
1742 },
1743 CommonsItem::Event(e) => {
1747 for f in &e.body.fields {
1748 reject_fn_types(&f.type_ref, "an event field", types, errors);
1749 }
1750 }
1751 CommonsItem::Capability(c) => {
1752 for op in &c.ops {
1753 for p in &op.params {
1754 reject_fn_types(
1755 &p.type_ref,
1756 "a capability operation signature",
1757 types,
1758 errors,
1759 );
1760 }
1761 if !type_ref_is_held(&op.return_type) {
1765 reject_fn_types(
1766 &op.return_type,
1767 "a capability operation signature",
1768 types,
1769 errors,
1770 );
1771 }
1772 }
1773 }
1774 CommonsItem::Service(s) => {
1775 for h in &s.handlers {
1776 for p in &h.params {
1777 let http_headers = matches!(h.kind, HandlerKind::Http { .. })
1780 && p.name.name == "headers"
1781 && matches!(p.type_ref, TypeRef::RequestHeaders(_));
1782 let http_headers = http_headers
1785 || (h.kind == HandlerKind::Rejected
1786 && matches!(p.type_ref, TypeRef::BoundaryRejection(_)));
1787 if !type_ref_is_held(&p.type_ref) && !http_headers {
1792 reject_fn_types(
1793 &p.type_ref,
1794 "a service handler signature",
1795 types,
1796 errors,
1797 );
1798 }
1799 }
1800 reject_fn_types(&h.return_type, "a service handler signature", types, errors);
1801 }
1802 }
1803 CommonsItem::Agent(a) => {
1804 reject_fn_types(&a.key_type, "an agent key", types, errors);
1805 for f in &a.store_fields {
1806 validate_store_field_value_types(f, types, errors);
1807 }
1808 for h in &a.handlers {
1809 for p in &h.params {
1810 if !type_ref_is_held(&p.type_ref) {
1813 reject_fn_types(
1814 &p.type_ref,
1815 "an agent handler signature",
1816 types,
1817 errors,
1818 );
1819 }
1820 }
1821 reject_fn_types(&h.return_type, "an agent handler signature", types, errors);
1822 }
1823 }
1824 CommonsItem::Actor(a) => {
1825 if let Some(id) = &a.identity {
1826 reject_fn_types(id, "an actor identity type", types, errors);
1827 }
1828 }
1829 CommonsItem::Fn(_)
1832 | CommonsItem::Provider(_)
1833 | CommonsItem::Messages(_)
1834 | CommonsItem::Asset(_)
1835 | CommonsItem::Embed(_) => {}
1836 }
1837 }
1838}
1839
1840pub fn phase_symbol_tables(
1843 groups: &BTreeMap<String, Vec<usize>>,
1844 kinds: &BTreeMap<String, UnitKind>,
1845 parsed: &[ParsedFile],
1846 errors: &mut ErrorSink,
1847) -> HashMap<String, UnitTable> {
1848 let mut unit_tables: HashMap<String, UnitTable> = HashMap::new();
1849 for (name, indices) in groups {
1850 let kind = *kinds.get(name).expect("every group has a kind");
1851 let mut table_errors: Vec<(PathBuf, CompileError)> = Vec::new();
1853 let table = build_unit_table(name, kind, indices, parsed, &mut table_errors);
1854 for (path, err) in table_errors {
1855 errors.push_for(Some(&path), err);
1856 }
1857 unit_tables.insert(name.clone(), table);
1858 }
1859 unit_tables
1860}
1861
1862pub fn uses_first_order<'a, I>(
1872 names: I,
1873 unit_uses: &HashMap<String, Vec<String>>,
1874) -> Vec<&'a String>
1875where
1876 I: IntoIterator<Item = &'a String>,
1877{
1878 let names: BTreeSet<&String> = names.into_iter().collect();
1879 let mut placed: HashSet<&String> = HashSet::new();
1880 let mut order = Vec::with_capacity(names.len());
1881 loop {
1882 let ready: Vec<&String> = names
1883 .iter()
1884 .copied()
1885 .filter(|n| !placed.contains(n))
1886 .filter(|n| {
1887 unit_uses.get(*n).is_none_or(|deps| {
1888 deps.iter()
1889 .all(|d| !names.contains(d) || placed.iter().any(|p| *p == d))
1890 })
1891 })
1892 .collect();
1893 if ready.is_empty() {
1894 break;
1895 }
1896 for n in ready {
1897 placed.insert(n);
1898 order.push(n);
1899 }
1900 }
1901 order.extend(names.iter().copied().filter(|n| !placed.contains(n)));
1902 order
1903}
1904
1905pub fn phase_resolve_uses(
1906 groups: &BTreeMap<String, Vec<usize>>,
1907 kinds: &BTreeMap<String, UnitKind>,
1908 parsed: &[ParsedFile],
1909 unit_tables: &HashMap<String, UnitTable>,
1910 policy: &crate::stability::StabilityPolicy,
1912 errors: &mut ErrorSink,
1913) -> HashMap<String, Vec<String>> {
1914 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
1915 for (name, indices) in groups {
1916 let mut uses_targets: Vec<String> = Vec::new();
1917 for &i in indices {
1918 for u in parsed[i].uses() {
1919 let target = u.target.joined();
1920 if !unit_tables.contains_key(&target) {
1921 errors.push_for(
1922 Some(&parsed[i].identity_path()),
1923 CompileError::new(
1924 "bynk.uses.unknown_commons",
1925 u.span,
1926 format!("unknown commons `{target}`"),
1927 )
1928 .with_note(
1929 "the target of a `uses` clause must be a commons in the project",
1930 ),
1931 );
1932 continue;
1933 }
1934 if !parsed[i].is_synthetic()
1937 && let Some(err) = policy.check(&target, u.target.span)
1938 {
1939 errors.push_for(Some(&parsed[i].identity_path()), err);
1940 }
1941 let target_kind = *kinds.get(&target).unwrap();
1942 if target_kind != UnitKind::Commons {
1943 errors.push_for(Some(&parsed[i].identity_path()),
1944 CompileError::new(
1945 "bynk.uses.target_is_context",
1946 u.span,
1947 format!(
1948 "`uses {target}` targets a context — `uses` may only target a commons"
1949 ),
1950 )
1951 .with_note(
1952 "to declare a dependency on a context, use `consumes` instead",
1953 ),
1954 );
1955 continue;
1956 }
1957 if target == *name {
1958 errors.push_for(
1959 Some(&parsed[i].identity_path()),
1960 CompileError::new(
1961 "bynk.uses.self_reference",
1962 u.span,
1963 format!("`{name}` cannot `uses` itself"),
1964 ),
1965 );
1966 continue;
1967 }
1968 if !uses_targets.contains(&target) {
1969 uses_targets.push(target);
1970 }
1971 }
1972 }
1973 unit_uses.insert(name.clone(), uses_targets);
1974 }
1975 unit_uses
1976}
1977
1978#[allow(clippy::type_complexity)]
1984pub fn phase_resolve_consumes(
1985 groups: &BTreeMap<String, Vec<usize>>,
1986 kinds: &BTreeMap<String, UnitKind>,
1987 parsed: &[ParsedFile],
1988 unit_tables: &HashMap<String, UnitTable>,
1989 policy: &crate::stability::StabilityPolicy,
1992 errors: &mut ErrorSink,
1993 refs: &mut RefSink,
1994) -> (
1995 HashMap<String, Vec<String>>,
1996 HashMap<String, HashMap<String, String>>,
1997) {
1998 let mut unit_consumes: HashMap<String, Vec<String>> = HashMap::new();
1999 let mut unit_flattened: HashMap<String, HashMap<String, String>> = HashMap::new();
2002 for (name, indices) in groups {
2003 let kind = *kinds.get(name).unwrap();
2004 let mut consumes_targets: Vec<String> = Vec::new();
2005 let mut flattened: HashMap<String, String> = HashMap::new();
2006 let mut flattened_in: HashMap<String, usize> = HashMap::new();
2009 let local_caps: HashSet<String> = unit_tables
2010 .get(name)
2011 .map(|t| t.capabilities.keys().cloned().collect())
2012 .unwrap_or_default();
2013 for &i in indices {
2014 refs.enter_file(&parsed[i].identity_path(), name, parsed[i].is_synthetic());
2015 for c in parsed[i].consumes() {
2016 let target = c.target.joined();
2017 if kind == UnitKind::Adapter && c.selected.is_none() {
2021 errors.push_for(Some(&parsed[i].identity_path()),
2022 CompileError::new(
2023 "bynk.adapter.consumes_requires_selection",
2024 c.span,
2025 format!(
2026 "an adapter's `consumes` must select capabilities — write `consumes {target} {{ Cap, … }}`",
2027 ),
2028 )
2029 .with_note(
2030 "adapters depend on capabilities, never on services; the whole-unit and aliased forms are context-only",
2031 ),
2032 );
2033 continue;
2034 }
2035 if !unit_tables.contains_key(&target) {
2036 errors.push_for(
2037 Some(&parsed[i].identity_path()),
2038 CompileError::new(
2039 "bynk.consumes.unknown_context",
2040 c.span,
2041 format!("unknown context `{target}`"),
2042 )
2043 .with_note(
2044 "the target of a `consumes` clause must be a context in the project",
2045 ),
2046 );
2047 continue;
2048 }
2049 let target_kind = *kinds.get(&target).unwrap();
2050 if target_kind != UnitKind::Context && target_kind != UnitKind::Adapter {
2053 errors.push_for(Some(&parsed[i].identity_path()),
2054 CompileError::new(
2055 "bynk.consumes.target_is_commons",
2056 c.span,
2057 format!(
2058 "`consumes {target}` targets a commons — `consumes` may only target a context or adapter"
2059 ),
2060 )
2061 .with_note(
2062 "to mix in declarations from a commons, use `uses` instead",
2063 ),
2064 );
2065 continue;
2066 }
2067 if kind == UnitKind::Adapter && target_kind == UnitKind::Context {
2071 errors.push_for(Some(&parsed[i].identity_path()),
2072 CompileError::new(
2073 "bynk.adapter.consumes_context",
2074 c.span,
2075 format!(
2076 "adapter `{name}` cannot `consumes` the context `{target}` — adapter dependencies are adapter-to-adapter"
2077 ),
2078 )
2079 .with_note(
2080 "an adapter may only depend on capabilities exported by other adapters (e.g. the `bynk` surface)",
2081 ),
2082 );
2083 continue;
2084 }
2085 if target == *name {
2086 let kind_word = if kind == UnitKind::Adapter {
2087 "adapter"
2088 } else {
2089 "context"
2090 };
2091 errors.push_for(
2092 Some(&parsed[i].identity_path()),
2093 CompileError::new(
2094 "bynk.consumes.self_reference",
2095 c.span,
2096 format!("{kind_word} `{name}` cannot `consumes` itself"),
2097 ),
2098 );
2099 continue;
2100 }
2101 if !parsed[i].is_synthetic()
2102 && let Some(err) = policy.check(&target, c.target.span)
2103 {
2104 errors.push_for(Some(&parsed[i].identity_path()), err);
2105 }
2106 if let Some(names) = &c.selected {
2110 let exported = unit_tables
2111 .get(&target)
2112 .map(|t| &t.exported_capabilities)
2113 .cloned()
2114 .unwrap_or_default();
2115 for cap in names {
2116 if !parsed[i].is_synthetic()
2117 && let Some(err) =
2118 policy.check(&format!("{target}.{}", cap.name), cap.span)
2119 {
2120 errors.push_for(Some(&parsed[i].identity_path()), err);
2121 }
2122 if !exported.contains(&cap.name) {
2123 errors.push_for(
2124 Some(&parsed[i].identity_path()),
2125 CompileError::new(
2126 "bynk.given.cross_context_unknown_capability",
2127 cap.span,
2128 format!(
2129 "`{target}` does not export a capability named `{}`",
2130 cap.name
2131 ),
2132 ),
2133 );
2134 continue;
2135 }
2136 if local_caps.contains(&cap.name) {
2137 errors.push_for(Some(&parsed[i].identity_path()), CompileError::new(
2138 "bynk.consumes.capability_name_clash",
2139 cap.span,
2140 format!(
2141 "flattened capability `{}` clashes with a capability declared locally — use qualified `given {target}.{}` instead",
2142 cap.name, cap.name
2143 ),
2144 ));
2145 continue;
2146 }
2147 if let Some(prev) = flattened.get(&cap.name) {
2148 if *prev == target && flattened_in.get(&cap.name) != Some(&i) {
2151 refs.record_in_unit(
2152 cap.span,
2153 SymbolKind::Capability,
2154 &cap.name,
2155 &target,
2156 );
2157 continue;
2158 }
2159 errors.push_for(Some(&parsed[i].identity_path()), CompileError::new(
2160 "bynk.consumes.capability_name_clash",
2161 cap.span,
2162 format!(
2163 "capability `{}` is flattened from both `{prev}` and `{target}` — qualify one with `given U.{}`",
2164 cap.name, cap.name
2165 ),
2166 ));
2167 continue;
2168 }
2169 refs.record_in_unit(cap.span, SymbolKind::Capability, &cap.name, &target);
2172 flattened.insert(cap.name.clone(), target.clone());
2173 flattened_in.insert(cap.name.clone(), i);
2174 }
2175 }
2176 if !consumes_targets.contains(&target) {
2177 consumes_targets.push(target);
2178 }
2179 }
2180 }
2181 unit_consumes.insert(name.clone(), consumes_targets);
2182 unit_flattened.insert(name.clone(), flattened);
2183 }
2184 (unit_consumes, unit_flattened)
2185}
2186
2187pub fn phase_consumes_aliases(
2192 groups: &BTreeMap<String, Vec<usize>>,
2193 kinds: &BTreeMap<String, UnitKind>,
2194 parsed: &[ParsedFile],
2195 unit_tables: &HashMap<String, UnitTable>,
2196 errors: &mut ErrorSink,
2197) -> HashMap<String, HashMap<String, String>> {
2198 let mut unit_consumes_aliases: HashMap<String, HashMap<String, String>> = HashMap::new();
2199 for (name, indices) in groups {
2200 let kind = *kinds.get(name).unwrap();
2201 if kind != UnitKind::Context {
2202 continue;
2203 }
2204 let mut aliases: HashMap<String, String> = HashMap::new();
2205 let mut by_alias: HashMap<String, (String, usize, Span)> = HashMap::new();
2211 let mut by_target: HashMap<String, (String, usize, Span)> = HashMap::new();
2212 for &i in indices {
2213 for c in parsed[i].consumes() {
2214 let Some(alias) = &c.alias else { continue };
2215 let target = c.target.joined();
2216 if !unit_tables.contains_key(&target) {
2217 continue;
2219 }
2220 let first = by_alias
2223 .get(&alias.name)
2224 .filter(|(t, f, _)| *t != target || *f == i)
2225 .map(|(t, f, sp)| {
2226 let what = if *t == target {
2227 "the same clause appears earlier in this file".to_string()
2228 } else {
2229 format!("alias `{}` already names `{t}`", alias.name)
2230 };
2231 (what, *f, *sp)
2232 })
2233 .or_else(|| {
2234 by_target
2235 .get(&target)
2236 .filter(|(a, _, _)| *a != alias.name)
2237 .map(|(a, f, sp)| {
2238 (format!("`{target}` is already consumed as `{a}`"), *f, *sp)
2239 })
2240 });
2241 if let Some((what, prev_file, prev_span)) = first {
2242 let where_ = if prev_file == i {
2245 String::new()
2246 } else {
2247 format!(
2250 " (first stated in `{}`)",
2251 parsed[prev_file]
2252 .identity_path()
2253 .components()
2254 .map(|c| c.as_os_str().to_string_lossy())
2255 .collect::<Vec<_>>()
2256 .join("/")
2257 )
2258 };
2259 let mut err = CompileError::new(
2260 "bynk.consumes.alias_conflict",
2261 alias.span,
2262 format!(
2263 "`consumes {target} as {}` conflicts with another `consumes` clause in context `{name}`: {what}{where_}",
2264 alias.name
2265 ),
2266 );
2267 if prev_file == i {
2268 err = err.with_label(prev_span, "previously defined here");
2269 }
2270 errors.push_for(
2271 Some(&parsed[i].identity_path()),
2272 err.with_note(
2273 "a context may consume a unit under one alias, and an alias may name one unit; repeating the same clause in another file is fine",
2274 ),
2275 );
2276 continue;
2277 }
2278 if by_alias.contains_key(&alias.name) {
2279 continue;
2281 }
2282 aliases.insert(alias.name.clone(), target.clone());
2283 by_alias.insert(alias.name.clone(), (target.clone(), i, alias.span));
2284 by_target.insert(target, (alias.name.clone(), i, alias.span));
2285 }
2286 }
2287 unit_consumes_aliases.insert(name.clone(), aliases);
2288 }
2289
2290 for (name, aliases) in &unit_consumes_aliases {
2293 let Some(local) = unit_tables.get(name) else {
2294 continue;
2295 };
2296 for alias in aliases.keys() {
2297 let alias_site = parsed_alias_span(parsed, &groups[name], alias);
2298 let alias_span = alias_site.map(|(_, s)| s).unwrap_or_default();
2299 let alias_file = alias_site.map(|(i, _)| parsed[i].identity_path());
2300 let conflict_kind = if local.types.contains_key(alias) {
2301 Some("type")
2302 } else if local.fns.contains_key(alias) {
2303 Some("function")
2304 } else if local.capabilities.contains_key(alias) {
2305 Some("capability")
2306 } else if local.services.contains_key(alias) {
2307 Some("service")
2308 } else if local.agents.contains_key(alias) {
2309 Some("agent")
2310 } else {
2311 None
2312 };
2313 if let Some(kind) = conflict_kind {
2314 errors.push_for(alias_file.as_deref(),
2315 CompileError::new(
2316 "bynk.consumes.alias_conflict",
2317 alias_span,
2318 format!(
2319 "alias `{alias}` conflicts with a local {kind} of the same name in context `{name}`",
2320 ),
2321 )
2322 .with_note(
2323 "pick a different alias for the `consumes` clause, or rename the local declaration",
2324 ),
2325 );
2326 }
2327 }
2328 }
2329 unit_consumes_aliases
2330}
2331
2332pub fn phase_uses_name_conflicts(
2336 unit_uses: &HashMap<String, Vec<String>>,
2337 unit_tables: &HashMap<String, UnitTable>,
2338 parsed: &[ParsedFile],
2339 groups: &BTreeMap<String, Vec<usize>>,
2340 errors: &mut ErrorSink,
2341) {
2342 for (name, targets) in unit_uses {
2343 let local = unit_tables.get(name).expect("unit table present");
2344 let mut imported: HashMap<String, String> = HashMap::new();
2345 for t in targets {
2346 let used = unit_tables.get(t).expect("used unit table present");
2347 for type_name in used.types.keys() {
2348 if local.types.contains_key(type_name) || local.fns.contains_key(type_name) {
2355 continue;
2356 }
2357 if let Some(prev) = imported.get(type_name) {
2358 let site = uses_span_of(parsed, &groups[name], t);
2359 let span = site.map(|(_, s)| s).unwrap_or_default();
2360 let file = site.map(|(i, _)| parsed[i].identity_path());
2361 errors.push_for(file.as_deref(),
2362 CompileError::new(
2363 "bynk.uses.name_conflict",
2364 span,
2365 format!(
2366 "`{name}` uses two commons that both declare `{type_name}`: `{prev}` and `{t}`",
2367 ),
2368 )
2369 .with_note(
2370 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
2371 ),
2372 );
2373 } else {
2374 imported.insert(type_name.clone(), t.clone());
2375 }
2376 }
2377 for fn_name in used.fns.keys() {
2378 if crate::symbols::local_value_kind(local, fn_name).is_some() {
2381 continue;
2382 }
2383 if let Some(prev) = imported.get(fn_name) {
2384 let site = uses_span_of(parsed, &groups[name], t);
2385 let span = site.map(|(_, s)| s).unwrap_or_default();
2386 let file = site.map(|(i, _)| parsed[i].identity_path());
2387 errors.push_for(file.as_deref(),
2388 CompileError::new(
2389 "bynk.uses.name_conflict",
2390 span,
2391 format!(
2392 "`{name}` uses two commons that both declare `{fn_name}`: `{prev}` and `{t}`",
2393 ),
2394 )
2395 .with_note(
2396 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
2397 ),
2398 );
2399 } else {
2400 imported.insert(fn_name.clone(), t.clone());
2401 }
2402 }
2403 for asset_name in used.assets.keys() {
2406 if crate::symbols::local_value_kind(local, asset_name).is_some() {
2408 continue;
2409 }
2410 if let Some(prev) = imported.get(asset_name) {
2411 let site = uses_span_of(parsed, &groups[name], t);
2412 let span = site.map(|(_, s)| s).unwrap_or_default();
2413 let file = site.map(|(i, _)| parsed[i].identity_path());
2414 errors.push_for(file.as_deref(),
2415 CompileError::new(
2416 "bynk.uses.name_conflict",
2417 span,
2418 format!(
2419 "`{name}` uses two commons that both declare `{asset_name}`: `{prev}` and `{t}`",
2420 ),
2421 )
2422 .with_note(
2423 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
2424 ),
2425 );
2426 } else {
2427 imported.insert(asset_name.clone(), t.clone());
2428 }
2429 }
2430 for embed_name in used.embeds.keys() {
2433 if crate::symbols::local_value_kind(local, embed_name).is_some() {
2434 continue;
2435 }
2436 if let Some(prev) = imported.get(embed_name) {
2437 let site = uses_span_of(parsed, &groups[name], t);
2438 let span = site.map(|(_, s)| s).unwrap_or_default();
2439 let file = site.map(|(i, _)| parsed[i].identity_path());
2440 errors.push_for(file.as_deref(),
2441 CompileError::new(
2442 "bynk.uses.name_conflict",
2443 span,
2444 format!(
2445 "`{name}` uses two commons that both declare `{embed_name}`: `{prev}` and `{t}`",
2446 ),
2447 )
2448 .with_note(
2449 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
2450 ),
2451 );
2452 } else {
2453 imported.insert(embed_name.clone(), t.clone());
2454 }
2455 }
2456 }
2457 }
2458}
2459
2460pub fn phase_messages_bundles(
2480 parsed: &[ParsedFile],
2481 groups: &BTreeMap<String, Vec<usize>>,
2482 kinds: &BTreeMap<String, UnitKind>,
2483 unit_uses: &HashMap<String, Vec<String>>,
2484 errors: &mut ErrorSink,
2485) {
2486 for (name, indices) in groups {
2487 let mut first_messages: Option<(usize, Span)> = None;
2488 let mut reference_sites: Vec<(usize, Span)> = Vec::new();
2489 let mut reference_block: Option<(usize, &MessagesDecl)> = None;
2490 let mut by_tag: HashMap<&str, (usize, &MessagesDecl)> = HashMap::new();
2491 for &i in indices {
2492 for item in parsed[i].items() {
2493 let CommonsItem::Messages(m) = item else {
2494 continue;
2495 };
2496 if first_messages.is_none() {
2497 first_messages = Some((i, m.span));
2498 }
2499 if kinds.get(name) != Some(&UnitKind::Commons) {
2500 errors.push_for(
2501 Some(&parsed[i].identity_path()),
2502 CompileError::new(
2503 "bynk.messages.outside_commons",
2504 m.span,
2505 "`messages` declarations are only allowed inside a commons, not a context or adapter",
2506 ),
2507 );
2508 continue;
2509 }
2510 if !checker::locale_tag_accepts(&m.tag) {
2517 let pattern = checker::locale_tag_pattern().unwrap_or("");
2518 errors.push_for(
2519 Some(&parsed[i].identity_path()),
2520 CompileError::new(
2521 "bynk.messages.invalid_locale_tag",
2522 m.tag_span,
2523 format!(
2524 "\"{}\" is not a valid `LocaleTag` — it must match the pattern `{}`",
2525 m.tag, pattern
2526 ),
2527 ),
2528 );
2529 }
2530 if let Some(&(_, prev)) = by_tag.get(m.tag.as_str()) {
2539 errors.push_for(
2540 Some(&parsed[i].identity_path()),
2541 CompileError::new(
2542 "bynk.resolve.duplicate_message_locale",
2543 m.tag_span,
2544 format!("locale \"{}\" is already declared in this bundle", m.tag),
2545 )
2546 .with_label(prev.tag_span, "previously declared here"),
2547 );
2548 } else {
2549 by_tag.insert(m.tag.as_str(), (i, m));
2550 }
2551 for ann in &m.annotations {
2552 if ann.name.name == "reference" {
2553 reference_sites.push((i, ann.span));
2554 reference_block = Some((i, m));
2555 }
2556 }
2557 let mut seen: HashMap<&str, Span> = HashMap::new();
2558 for entry in &m.entries {
2559 if let Some(prev) = seen.get(entry.code.as_str()) {
2560 errors.push_for(
2561 Some(&parsed[i].identity_path()),
2562 CompileError::new(
2563 "bynk.resolve.duplicate_message_code",
2564 entry.code_span,
2565 format!(
2566 "message code \"{}\" is already declared in this block",
2567 entry.code
2568 ),
2569 )
2570 .with_label(*prev, "previously declared here"),
2571 );
2572 } else {
2573 seen.insert(entry.code.as_str(), entry.code_span);
2574 }
2575 check_entry_icu_syntax(entry, Some(&parsed[i].identity_path()), errors);
2580 }
2581 }
2582 }
2583 let Some((first_i, first_span)) = first_messages else {
2584 continue;
2585 };
2586 if kinds.get(name) != Some(&UnitKind::Commons) {
2587 continue;
2590 }
2591 match reference_sites.len() {
2592 0 => {
2593 errors.push_for(
2594 Some(&parsed[first_i].identity_path()),
2595 CompileError::new(
2596 "bynk.messages.missing_reference",
2597 first_span,
2598 "a message bundle must have exactly one `@reference` block; none found",
2599 ),
2600 );
2601 }
2602 1 => {
2603 let (_, reference) = reference_block
2608 .expect("reference_sites.len() == 1 implies reference_block is Some");
2609 let mut sorted_tags: Vec<&&str> = by_tag.keys().collect();
2612 sorted_tags.sort();
2613 for &&tag in &sorted_tags {
2614 let &(locale_i, locale_m) = &by_tag[tag];
2615 if tag == reference.tag.as_str() {
2616 continue;
2617 }
2618 for ref_entry in &reference.entries {
2619 let Some(locale_entry) =
2620 locale_m.entries.iter().find(|e| e.code == ref_entry.code)
2621 else {
2622 errors.push_for(
2623 Some(&parsed[locale_i].identity_path()),
2624 CompileError::new(
2625 "bynk.messages.incomplete",
2626 locale_m.span,
2627 format!(
2628 "locale \"{tag}\" is missing code \"{}\", declared by the reference locale \"{}\"",
2629 ref_entry.code, reference.tag
2630 ),
2631 ),
2632 );
2633 continue;
2634 };
2635 let ref_names = icu::placeholder_names(&ref_entry.template);
2636 let locale_names = icu::placeholder_names(&locale_entry.template);
2637 if ref_names != locale_names {
2638 errors.push_for(
2639 Some(&parsed[locale_i].identity_path()),
2640 CompileError::new(
2641 "bynk.messages.placeholder_mismatch",
2642 locale_entry.template_span,
2643 format!(
2644 "locale \"{tag}\"'s template for code \"{}\" uses placeholders {locale_names:?}, but the reference locale \"{}\"'s uses {ref_names:?}",
2645 ref_entry.code, reference.tag
2646 ),
2647 ),
2648 );
2649 }
2650 let ref_kinds = icu::template_format_kinds(&ref_entry.template);
2660 let locale_kinds = icu::template_format_kinds(&locale_entry.template);
2661 for (pname, ref_kind) in &ref_kinds {
2662 let Some(locale_kind) = locale_kinds.get(pname) else {
2663 continue;
2664 };
2665 if locale_kind != ref_kind {
2666 errors.push_for(
2667 Some(&parsed[locale_i].identity_path()),
2668 CompileError::new(
2669 "bynk.messages.format_mismatch",
2670 locale_entry.template_span,
2671 format!(
2672 "locale \"{tag}\"'s placeholder \"{pname}\" in code \"{}\" is formatted as {}, but the reference locale \"{}\"'s is {}",
2673 ref_entry.code,
2674 locale_kind.as_str(),
2675 reference.tag,
2676 ref_kind.as_str(),
2677 ),
2678 ),
2679 );
2680 }
2681 }
2682 }
2683 }
2684 }
2685 _ => {
2686 let (_, first_ref_span) = reference_sites[0];
2687 for &(i, span) in &reference_sites[1..] {
2688 errors.push_for(
2689 Some(&parsed[i].identity_path()),
2690 CompileError::new(
2691 "bynk.messages.multiple_reference",
2692 span,
2693 "a message bundle must have exactly one `@reference` block; found more than one",
2694 )
2695 .with_label(first_ref_span, "first `@reference` here"),
2696 );
2697 }
2698 }
2699 }
2700 let targets = unit_uses.get(name);
2709 let has_locale_uses =
2710 targets.is_some_and(|targets| targets.iter().any(|t| t == firstparty::LOCALE_UNIT));
2711 let has_locale_types_uses = targets
2712 .is_some_and(|targets| targets.iter().any(|t| t == firstparty::LOCALE_TYPES_UNIT));
2713 if !has_locale_uses || !has_locale_types_uses {
2714 let missing = match (has_locale_uses, has_locale_types_uses) {
2715 (false, false) => "`bynk.locale` and `bynk.locale.types`",
2716 (false, true) => "`bynk.locale`",
2717 (true, false) => "`bynk.locale.types`",
2718 (true, true) => unreachable!("at least one of the two is missing here"),
2719 };
2720 errors.push_for(
2721 Some(&parsed[first_i].identity_path()),
2722 CompileError::new(
2723 "bynk.messages.missing_locale_dependency",
2724 first_span,
2725 format!("a commons declaring `messages` must also `uses` {missing}"),
2726 ),
2727 );
2728 }
2729 }
2730}
2731
2732pub fn phase_locale_bundle_ambiguity(
2744 parsed: &[ParsedFile],
2745 groups: &BTreeMap<String, Vec<usize>>,
2746 kinds: &BTreeMap<String, UnitKind>,
2747 unit_uses: &HashMap<String, Vec<String>>,
2748 unit_flattened: &HashMap<String, HashMap<String, String>>,
2749 errors: &mut ErrorSink,
2750) {
2751 for (name, indices) in groups {
2752 if kinds.get(name) != Some(&UnitKind::Context) {
2753 continue;
2754 }
2755 let ContextMessageBundle::Many(bundles) =
2756 detect_context_message_bundle(name, unit_uses, groups, kinds, parsed)
2757 else {
2758 continue;
2759 };
2760 let consumes_locale = unit_flattened
2761 .get(name)
2762 .and_then(|m| m.get("Locale"))
2763 .is_some_and(|owner| owner == firstparty::BYNK_UNIT);
2764 if !consumes_locale {
2765 continue;
2766 }
2767 for &i in indices {
2768 for c in parsed[i].consumes() {
2769 if c.target.joined() != firstparty::BYNK_UNIT {
2770 continue;
2771 }
2772 let Some(locale_ident) = c.selected.iter().flatten().find(|id| id.name == "Locale")
2773 else {
2774 continue;
2775 };
2776 let mut err = CompileError::new(
2777 "bynk.locale.multiple_message_bundles",
2778 locale_ident.span,
2779 format!(
2780 "context `{name}` uses {} message bundles ({}) — `Locale.current()` has no single bundle to negotiate against",
2781 bundles.len(),
2782 bundles.join(", "),
2783 ),
2784 );
2785 for &j in indices {
2786 for u in parsed[j].uses() {
2787 if bundles.contains(&u.target.joined()) {
2788 err = err
2789 .with_label(u.span, format!("`{}` used here", u.target.joined()));
2790 }
2791 }
2792 }
2793 errors.push_for(Some(&parsed[i].identity_path()), err);
2794 }
2795 }
2796 }
2797}
2798
2799fn check_entry_icu_syntax(entry: &MessageEntry, file: Option<&Path>, errors: &mut ErrorSink) {
2803 for (inner_offset, inner) in icu::icu_dispatch_placeholders(&entry.template) {
2804 if let Err(e) = icu::parse_icu_placeholder(inner) {
2805 let decoded_start = inner_offset + e.offset;
2806 let decoded_span = Span::new(decoded_start, decoded_start + e.len);
2807 let raw_span = decoded_span.offset(entry.template_span.start + 1);
2808 errors.push_for(
2809 file,
2810 CompileError::new(
2811 "bynk.messages.malformed_icu_syntax",
2812 raw_span,
2813 e.kind.message(),
2814 ),
2815 );
2816 }
2817 }
2818}
2819
2820pub fn phase_event_subscriptions(
2834 parsed: &[ParsedFile],
2835 groups: &BTreeMap<String, Vec<usize>>,
2836 kinds: &BTreeMap<String, UnitKind>,
2837 unit_tables: &HashMap<String, UnitTable>,
2838 unit_consumes: &HashMap<String, Vec<String>>,
2839 unit_uses: &HashMap<String, Vec<String>>,
2840 errors: &mut ErrorSink,
2841) {
2842 for (name, indices) in groups {
2843 if kinds.get(name) != Some(&UnitKind::Context) {
2844 continue;
2845 }
2846 let consumed = unit_consumes.get(name).cloned().unwrap_or_default();
2847 for &i in indices {
2848 for item in parsed[i].items() {
2849 let CommonsItem::Service(s) = item else {
2850 continue;
2851 };
2852 let ServiceProtocol::Events {
2853 event_type,
2854 pattern,
2855 schema_dispatch,
2856 } = &s.protocol
2857 else {
2858 continue;
2859 };
2860 if let Some(dispatch) = schema_dispatch {
2865 check_schema_dispatch(dispatch, &parsed[i].identity_path(), errors);
2866 }
2867 let TypeRef::Named(id) = event_type else {
2868 continue;
2869 };
2870 let owner_locally = unit_tables
2871 .get(name)
2872 .filter(|t| t.events.contains_key(&id.name))
2873 .map(|_| name.clone());
2874 let owner_consumed = consumed.iter().find(|c| {
2875 unit_tables
2876 .get(*c)
2877 .is_some_and(|t| t.events.contains_key(&id.name))
2878 });
2879 let owner = owner_locally
2880 .as_deref()
2881 .or(owner_consumed.map(String::as_str));
2882 let Some(owner) = owner else {
2883 errors.push_for(
2884 Some(&parsed[i].identity_path()),
2885 CompileError::new(
2886 "bynk.event.unknown_subscription",
2887 id.span,
2888 format!(
2889 "`{}` is not a declared event in this context or any consumed context",
2890 id.name
2891 ),
2892 )
2893 .with_note(
2894 "check the spelling, or add `consumes <context>` for the context whose `event` this names — an unresolvable subscription never receives anything, silently",
2895 ),
2896 );
2897 continue;
2898 };
2899 let Some(pattern) = pattern else {
2904 continue;
2905 };
2906 let Some(event_decl) = unit_tables.get(owner).and_then(|t| t.events.get(&id.name))
2907 else {
2908 continue;
2909 };
2910 check_event_pattern(
2911 pattern,
2912 event_decl,
2913 owner,
2914 unit_tables,
2915 unit_uses,
2916 &parsed[i].identity_path(),
2917 errors,
2918 );
2919 }
2920 }
2921 }
2922}
2923
2924fn check_event_pattern(
2933 pattern: &EventPattern,
2934 event_decl: &EventDecl,
2935 owner: &str,
2936 unit_tables: &HashMap<String, UnitTable>,
2937 unit_uses: &HashMap<String, Vec<String>>,
2938 identity_path: &std::path::Path,
2939 errors: &mut ErrorSink,
2940) {
2941 let mut seen: HashSet<String> = HashSet::new();
2942 for field in &pattern.fields {
2943 if !seen.insert(field.name.name.clone()) {
2944 errors.push_for(
2945 Some(identity_path),
2946 CompileError::new(
2947 "bynk.event.pattern_duplicate_field",
2948 field.name.span,
2949 format!(
2950 "field `{}` is matched more than once in this subscription pattern",
2951 field.name.name
2952 ),
2953 ),
2954 );
2955 continue;
2956 }
2957 let Some(record_field) = event_decl
2958 .body
2959 .fields
2960 .iter()
2961 .find(|f| f.name.name == field.name.name)
2962 else {
2963 let known: Vec<&str> = event_decl
2964 .body
2965 .fields
2966 .iter()
2967 .map(|f| f.name.name.as_str())
2968 .collect();
2969 errors.push_for(
2970 Some(identity_path),
2971 CompileError::new(
2972 "bynk.event.pattern_unknown_field",
2973 field.name.span,
2974 format!(
2975 "`{}` has no field named `{}`",
2976 event_decl.name.name, field.name.name
2977 ),
2978 )
2979 .with_note(format!(
2980 "declared fields: {}",
2981 if known.is_empty() {
2982 "(none)".to_string()
2983 } else {
2984 known.join(", ")
2985 }
2986 )),
2987 );
2988 continue;
2989 };
2990 check_event_pattern_value(
2991 &field.value,
2992 record_field,
2993 owner,
2994 unit_tables,
2995 unit_uses,
2996 identity_path,
2997 errors,
2998 );
2999 }
3000}
3001
3002fn check_schema_dispatch(
3008 dispatch: &SchemaDispatch,
3009 identity_path: &std::path::Path,
3010 errors: &mut ErrorSink,
3011) {
3012 let SchemaVersionPattern::Literal(n) = &dispatch.pattern;
3013 if *n <= 0 {
3014 errors.push_for(
3015 Some(identity_path),
3016 CompileError::new(
3017 "bynk.event.bad_schema_dispatch",
3018 dispatch.span,
3019 "`via schema(...)`'s argument must be a positive `Int` literal",
3020 ),
3021 );
3022 }
3023}
3024
3025fn check_event_pattern_value(
3029 value: &EventPatternValue,
3030 record_field: &RecordField,
3031 owner: &str,
3032 unit_tables: &HashMap<String, UnitTable>,
3033 unit_uses: &HashMap<String, Vec<String>>,
3034 identity_path: &std::path::Path,
3035 errors: &mut ErrorSink,
3036) {
3037 match value {
3038 EventPatternValue::Literal { value: lit, span } => {
3039 let base = match &record_field.type_ref {
3047 TypeRef::Base(b, _) => Some(*b),
3048 TypeRef::Named(field_type_name) => {
3049 resolve_type_decl(unit_tables, unit_uses, owner, &field_type_name.name)
3050 .and_then(|d| match &d.body {
3051 TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => {
3052 Some(*base)
3053 }
3054 _ => None,
3055 })
3056 }
3057 _ => None,
3058 };
3059 let Some(base) = base else {
3060 errors.push_for(
3061 Some(identity_path),
3062 CompileError::new(
3063 "bynk.event.pattern_type_mismatch",
3064 *span,
3065 format!(
3066 "field `{}` is not a literal-kind type — a literal pattern value cannot match it",
3067 record_field.name.name
3068 ),
3069 ),
3070 );
3071 return;
3072 };
3073 let kind_matches = matches!(
3074 (lit, base),
3075 (LiteralValue::Int(_), BaseType::Int)
3076 | (LiteralValue::Str(_), BaseType::String)
3077 | (LiteralValue::Bool(_), BaseType::Bool)
3078 );
3079 if !kind_matches {
3080 errors.push_for(
3081 Some(identity_path),
3082 CompileError::new(
3083 "bynk.event.pattern_type_mismatch",
3084 *span,
3085 format!(
3086 "this literal does not match the type of field `{}` (`{}`)",
3087 record_field.name.name,
3088 type_ref_to_display(&record_field.type_ref)
3089 ),
3090 ),
3091 );
3092 }
3093 }
3094 EventPatternValue::Variant {
3095 type_name,
3096 variant,
3097 span,
3098 } => {
3099 let TypeRef::Named(field_type_name) = &record_field.type_ref else {
3100 errors.push_for(
3101 Some(identity_path),
3102 CompileError::new(
3103 "bynk.event.pattern_type_mismatch",
3104 *span,
3105 format!(
3106 "field `{}` is not a sum type — a variant pattern value cannot match it",
3107 record_field.name.name
3108 ),
3109 ),
3110 );
3111 return;
3112 };
3113 if let Some(qualifier) = type_name
3114 && qualifier.name != field_type_name.name
3115 {
3116 errors.push_for(
3117 Some(identity_path),
3118 CompileError::new(
3119 "bynk.event.pattern_type_mismatch",
3120 qualifier.span,
3121 format!(
3122 "field `{}` has type `{}`, not `{}`",
3123 record_field.name.name, field_type_name.name, qualifier.name
3124 ),
3125 ),
3126 );
3127 return;
3128 }
3129 let Some(decl) =
3130 resolve_type_decl(unit_tables, unit_uses, owner, &field_type_name.name)
3131 else {
3132 return;
3136 };
3137 let TypeBody::Sum(sum) = &decl.body else {
3138 errors.push_for(
3139 Some(identity_path),
3140 CompileError::new(
3141 "bynk.event.pattern_type_mismatch",
3142 *span,
3143 format!(
3144 "field `{}` has type `{}`, which is not a sum type",
3145 record_field.name.name, field_type_name.name
3146 ),
3147 ),
3148 );
3149 return;
3150 };
3151 let Some(member) = sum.variants.iter().find(|v| v.name.name == variant.name) else {
3152 errors.push_for(
3153 Some(identity_path),
3154 CompileError::new(
3155 "bynk.event.pattern_unknown_variant",
3156 variant.span,
3157 format!(
3158 "`{}` has no variant named `{}`",
3159 field_type_name.name, variant.name
3160 ),
3161 ),
3162 );
3163 return;
3164 };
3165 if !member.payload.is_empty() {
3166 errors.push_for(
3167 Some(identity_path),
3168 CompileError::new(
3169 "bynk.event.pattern_variant_payload",
3170 variant.span,
3171 format!(
3172 "`{}.{}` carries a payload — only a nullary variant may be matched here, since testing the tag alone would silently ignore the payload",
3173 field_type_name.name, variant.name
3174 ),
3175 ),
3176 );
3177 }
3178 }
3179 }
3180}
3181
3182fn resolve_type_decl<'a>(
3188 unit_tables: &'a HashMap<String, UnitTable>,
3189 unit_uses: &HashMap<String, Vec<String>>,
3190 owner: &str,
3191 name: &str,
3192) -> Option<&'a Arc<TypeDecl>> {
3193 if let Some(t) = unit_tables.get(owner).and_then(|t| t.types.get(name)) {
3194 return Some(t);
3195 }
3196 for used in unit_uses.get(owner).into_iter().flatten() {
3197 if let Some(t) = unit_tables.get(used).and_then(|t| t.types.get(name)) {
3198 return Some(t);
3199 }
3200 }
3201 None
3202}
3203
3204pub fn phase_validate_type_exports(
3210 groups: &BTreeMap<String, Vec<usize>>,
3211 kinds: &BTreeMap<String, UnitKind>,
3212 parsed: &[ParsedFile],
3213 unit_tables: &HashMap<String, UnitTable>,
3214 errors: &mut ErrorSink,
3215 refs: &mut RefSink,
3216) -> HashMap<String, HashMap<String, Visibility>> {
3217 let mut exports_visibility: HashMap<String, HashMap<String, Visibility>> = HashMap::new();
3218 for (name, indices) in groups {
3219 let kind = *kinds.get(name).unwrap();
3220 if kind != UnitKind::Context && kind != UnitKind::Adapter {
3221 continue;
3224 }
3225 let local = unit_tables.get(name).unwrap();
3226 let mut seen: HashMap<String, (Visibility, Span)> = HashMap::new();
3227 for &i in indices {
3228 refs.enter_file(&parsed[i].identity_path(), name, parsed[i].is_synthetic());
3229 for clause in parsed[i].exports() {
3230 let ExportKind::Type(clause_vis) = clause.kind else {
3233 continue;
3234 };
3235 let mut within: HashMap<String, Span> = HashMap::new();
3236 for n in clause.names.iter().map(|e| &e.name) {
3237 if let Some(prev) = within.get(&n.name) {
3238 errors.push_for(
3239 Some(&parsed[i].identity_path()),
3240 CompileError::new(
3241 "bynk.exports.duplicate_in_clause",
3242 n.span,
3243 format!(
3244 "type `{}` appears more than once in this exports clause",
3245 n.name
3246 ),
3247 )
3248 .with_label(*prev, "previously listed here"),
3249 );
3250 continue;
3251 }
3252 within.insert(n.name.clone(), n.span);
3253
3254 if !local.types.contains_key(&n.name) {
3255 errors.push_for(Some(&parsed[i].identity_path()),
3256 CompileError::new(
3257 "bynk.exports.undeclared_type",
3258 n.span,
3259 format!(
3260 "exports clause references `{}`, which is not a type declared in context `{}`",
3261 n.name, name
3262 ),
3263 )
3264 .with_note(
3265 "only types declared in the same context can appear in `exports` clauses",
3266 ),
3267 );
3268 continue;
3269 }
3270 refs.record(n.span, SymbolKind::Type, &n.name);
3272
3273 if let Some((prev_vis, prev_span)) = seen.get(&n.name) {
3274 if *prev_vis == clause_vis {
3275 errors.push_for(
3276 Some(&parsed[i].identity_path()),
3277 CompileError::new(
3278 "bynk.exports.duplicate_export",
3279 n.span,
3280 format!("type `{}` is exported more than once", n.name),
3281 )
3282 .with_label(*prev_span, "previously exported here"),
3283 );
3284 } else {
3285 errors.push_for(Some(&parsed[i].identity_path()),
3286 CompileError::new(
3287 "bynk.exports.conflicting_visibility",
3288 n.span,
3289 format!(
3290 "type `{}` is exported with conflicting visibilities — pick `opaque` or `transparent`",
3291 n.name,
3292 ),
3293 )
3294 .with_label(*prev_span, "previously exported here"),
3295 );
3296 }
3297 continue;
3298 }
3299 seen.insert(n.name.clone(), (clause_vis, n.span));
3300 }
3301 }
3302 }
3303 let mut visibility_map: HashMap<String, Visibility> = HashMap::new();
3304 for (n, (v, _)) in seen {
3305 visibility_map.insert(n, v);
3306 }
3307 exports_visibility.insert(name.clone(), visibility_map);
3308 }
3309 exports_visibility
3310}
3311
3312pub fn phase_validate_capability_exports(
3317 groups: &BTreeMap<String, Vec<usize>>,
3318 kinds: &BTreeMap<String, UnitKind>,
3319 parsed: &[ParsedFile],
3320 unit_tables: &HashMap<String, UnitTable>,
3321 errors: &mut ErrorSink,
3322 refs: &mut RefSink,
3323) {
3324 for (name, indices) in groups {
3325 if kinds.get(name) != Some(&UnitKind::Context)
3326 && kinds.get(name) != Some(&UnitKind::Adapter)
3327 {
3328 continue;
3329 }
3330 let local = unit_tables.get(name).unwrap();
3331 let mut seen: HashMap<String, Span> = HashMap::new();
3332 for &i in indices {
3333 refs.enter_file(&parsed[i].identity_path(), name, parsed[i].is_synthetic());
3334 for clause in parsed[i].exports() {
3335 if !matches!(clause.kind, ExportKind::Capability) {
3336 continue;
3337 }
3338 for n in clause.names.iter().map(|e| &e.name) {
3339 if let Some(prev) = seen.get(&n.name) {
3340 errors.push_for(
3341 Some(&parsed[i].identity_path()),
3342 CompileError::new(
3343 "bynk.exports.duplicate_export",
3344 n.span,
3345 format!("capability `{}` is exported more than once", n.name),
3346 )
3347 .with_label(*prev, "previously exported here"),
3348 );
3349 continue;
3350 }
3351 seen.insert(n.name.clone(), n.span);
3352 if local.capabilities.contains_key(&n.name) {
3353 refs.record(n.span, SymbolKind::Capability, &n.name);
3356 }
3357 if !local.capabilities.contains_key(&n.name) {
3358 errors.push_for(Some(&parsed[i].identity_path()),
3359 CompileError::new(
3360 "bynk.exports.undeclared_capability",
3361 n.span,
3362 format!(
3363 "`exports capability` references `{}`, which is not a capability declared in context `{}`",
3364 n.name, name
3365 ),
3366 )
3367 .with_note(
3368 "only capabilities declared in the same context can appear in `exports capability` clauses",
3369 ),
3370 );
3371 continue;
3372 }
3373 let derived = local
3377 .capabilities
3378 .get(&n.name)
3379 .is_some_and(|c| c.derived.is_some());
3380 if !derived && !local.providers.contains_key(&n.name) {
3381 errors.push_for(Some(&parsed[i].identity_path()),
3382 CompileError::new(
3383 "bynk.exports.capability_not_provided",
3384 n.span,
3385 format!(
3386 "exported capability `{}` has no provider in context `{}` — a consumer cannot instantiate it",
3387 n.name, name
3388 ),
3389 )
3390 .with_note(format!(
3391 "add a `provides {} = …` declaration so the capability can be wired into consumers",
3392 n.name
3393 )),
3394 );
3395 }
3396 }
3397 }
3398 }
3399 }
3400}
3401
3402pub fn phase_validate_providers(
3407 unit_tables: &HashMap<String, UnitTable>,
3408 groups: &BTreeMap<String, Vec<usize>>,
3412 parsed: &[ParsedFile],
3413 broken: &BrokenDeclNames,
3415 errors: &mut ErrorSink,
3416 tys: &Arc<Types>,
3417) {
3418 for (name, table) in unit_tables {
3419 let provider_files: HashMap<&str, PathBuf> = groups
3423 .get(name)
3424 .map(|indices| {
3425 indices
3426 .iter()
3427 .flat_map(|&i| {
3428 parsed[i].items().iter().filter_map(move |item| match item {
3429 CommonsItem::Provider(p) => {
3430 Some((p.capability.name.as_str(), parsed[i].identity_path()))
3431 }
3432 _ => None,
3433 })
3434 })
3435 .collect()
3436 })
3437 .unwrap_or_default();
3438 for (cap_name, provider) in &table.providers {
3439 let provider_file = provider_files.get(cap_name.as_str()).map(|p| p.as_path());
3440 if provider.external {
3443 continue;
3444 }
3445 let Some(cap) = table.capabilities.get(cap_name) else {
3446 if broken.get(name).is_some_and(|b| b.contains(cap_name)) {
3450 continue;
3451 }
3452 errors.push_for(provider_file,
3453 CompileError::new(
3454 "bynk.provider.unknown_capability",
3455 provider.capability.span,
3456 format!(
3457 "provider targets unknown capability `{}` — declare the capability in the same context",
3458 cap_name
3459 ),
3460 ),
3461 );
3462 continue;
3463 };
3464 for cap_op in &cap.ops {
3472 if !cap_op.type_params.is_empty() {
3473 errors.push_for(
3474 provider_file,
3475 CompileError::new(
3476 "bynk.provider.generic_op_requires_external",
3477 provider.span,
3478 format!(
3479 "provider `{}` for capability `{}` has a Bynk body, but operation `{}` declares its own type parameter(s) (`[{}]`) — a generic capability operation requires an external (bodiless) provider",
3480 provider.provider_name.name,
3481 cap_name,
3482 cap_op.name.name,
3483 cap_op
3484 .type_params
3485 .iter()
3486 .map(|p| p.name.name.as_str())
3487 .collect::<Vec<_>>()
3488 .join(", "),
3489 ),
3490 )
3491 .with_note(
3492 "write `provides Cap = Name` with no `{ … }` block, and supply the implementation as a hand-authored class in the adapter's binding file",
3493 ),
3494 );
3495 }
3496 }
3497 for cap_op in &cap.ops {
3499 if !provider.ops.iter().any(|o| o.name.name == cap_op.name.name) {
3500 errors.push_for(
3501 provider_file,
3502 CompileError::new(
3503 "bynk.provider.missing_operation",
3504 provider.span,
3505 format!(
3506 "provider `{}` for capability `{}` is missing operation `{}`",
3507 provider.provider_name.name, cap_name, cap_op.name.name
3508 ),
3509 ),
3510 );
3511 }
3512 }
3513 for prov_op in &provider.ops {
3516 let Some(cap_op) = cap.ops.iter().find(|o| o.name.name == prov_op.name.name) else {
3517 errors.push_for(provider_file, CompileError::new(
3518 "bynk.provider.extra_operation",
3519 prov_op.span,
3520 format!(
3521 "provider operation `{}.{}` does not match any operation in capability `{}`",
3522 provider.provider_name.name, prov_op.name.name, cap_name
3523 ),
3524 ));
3525 continue;
3526 };
3527 if cap_op.params.len() != prov_op.params.len() {
3528 errors.push_for(provider_file, CompileError::new(
3529 "bynk.provider.signature_mismatch",
3530 prov_op.span,
3531 format!(
3532 "provider operation `{}.{}` has {} parameter(s), but capability operation expects {}",
3533 provider.provider_name.name,
3534 prov_op.name.name,
3535 prov_op.params.len(),
3536 cap_op.params.len()
3537 ),
3538 ));
3539 continue;
3540 }
3541 let cap_info = build_capability_op_info(cap_op, &table.types, tys);
3552 let no_vars = HashSet::new();
3553 let prov_params: Vec<TyId> = prov_op
3554 .params
3555 .iter()
3556 .map(|p| {
3557 checker::resolve_type_ref_in(&p.type_ref, &table.types, &no_vars, tys)
3558 .unwrap_or(tys.intern(Ty::Unit))
3559 })
3560 .collect();
3561 let prov_return_ty =
3562 checker::resolve_type_ref_in(&prov_op.return_type, &table.types, &no_vars, tys)
3563 .unwrap_or(tys.intern(Ty::Unit));
3564 for (i, (cap_ty, (prov_p, prov_ty))) in cap_info
3565 .params
3566 .iter()
3567 .zip(prov_op.params.iter().zip(prov_params.iter()))
3568 .enumerate()
3569 {
3570 if cap_ty != prov_ty {
3571 errors.push_for(provider_file, CompileError::new(
3572 "bynk.provider.signature_mismatch",
3573 prov_p.span,
3574 format!(
3575 "provider operation `{}.{}` parameter {} has type `{}`, but capability declares `{}`",
3576 provider.provider_name.name,
3577 prov_op.name.name,
3578 i + 1,
3579 ts_type_ref_display(&prov_p.type_ref),
3580 ts_type_ref_display(&cap_op.params[i].type_ref)
3581 ),
3582 ));
3583 }
3584 }
3585 if cap_info.return_ty != prov_return_ty {
3586 errors.push_for(provider_file, CompileError::new(
3587 "bynk.provider.signature_mismatch",
3588 prov_op.return_type.span(),
3589 format!(
3590 "provider operation `{}.{}` returns `{}`, but capability declares `{}`",
3591 provider.provider_name.name,
3592 prov_op.name.name,
3593 ts_type_ref_display(&prov_op.return_type),
3594 ts_type_ref_display(&cap_op.return_type)
3595 ),
3596 ));
3597 }
3598 }
3599 }
3600 }
3601}
3602
3603fn lock_violation(
3612 native: &BTreeMap<Platform, String>,
3613 selected: Platform,
3614) -> Option<LockViolation> {
3615 let mut platforms = native.iter();
3616 let (first, first_unit) = platforms.next()?;
3617 if let Some((second, second_unit)) = platforms.next() {
3618 return Some(LockViolation::Conflict {
3619 a: (*first, first_unit.clone()),
3620 b: (*second, second_unit.clone()),
3621 });
3622 }
3623 if *first != selected {
3624 return Some(LockViolation::Required {
3625 needed: *first,
3626 unit: first_unit.clone(),
3627 });
3628 }
3629 None
3630}
3631
3632#[derive(Debug, PartialEq, Eq)]
3634enum LockViolation {
3635 Required { needed: Platform, unit: String },
3637 Conflict {
3639 a: (Platform, String),
3640 b: (Platform, String),
3641 },
3642}
3643
3644pub fn resolve_consume_prefix(
3655 prefix: &str,
3656 consumed: &[String],
3657 aliases: &HashMap<String, String>,
3658) -> Option<String> {
3659 if let Some(q) = aliases.get(prefix) {
3660 return Some(q.clone());
3661 }
3662 if consumed.iter().any(|c| c == prefix) {
3663 return Some(prefix.to_string());
3664 }
3665 None
3666}
3667
3668pub fn handler_cross_caps(
3672 table: &UnitTable,
3673 consumed: &[String],
3674 aliases: &HashMap<String, String>,
3675 flattened: &HashMap<String, String>,
3676) -> BTreeMap<String, String> {
3677 let mut out = BTreeMap::new();
3678 let mut scan = |given: &[CapRef]| {
3679 for c in given {
3680 if c.key() == "Events" && flattened.get(c.key()).map(String::as_str) == Some("bynk") {
3686 continue;
3687 }
3688 if let Some(p) = c.prefix() {
3689 if let Some(ctx) = resolve_consume_prefix(&p, consumed, aliases) {
3690 out.entry(c.key().to_string()).or_insert(ctx);
3691 }
3692 } else if let Some(unit) = flattened.get(c.key()) {
3693 out.entry(c.key().to_string())
3696 .or_insert_with(|| unit.clone());
3697 }
3698 }
3699 };
3700 for s in table.services.values() {
3701 for h in &s.handlers {
3702 scan(&h.given);
3703 }
3704 }
3705 for a in table.agents.values() {
3706 for h in &a.handlers {
3707 scan(&h.given);
3708 }
3709 }
3710 out
3711}
3712
3713fn collect_given_closure(
3724 provider_ctx: &str,
3725 cap: &str,
3726 unit_tables: &HashMap<String, UnitTable>,
3727 unit_consumes: &HashMap<String, Vec<String>>,
3728 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3729 unit_flattened: &HashMap<String, HashMap<String, String>>,
3730 reached: &mut BTreeSet<(String, String)>,
3731) {
3732 if !reached.insert((provider_ctx.to_string(), cap.to_string())) {
3736 return;
3737 }
3738 let Some(provider) = unit_tables
3739 .get(provider_ctx)
3740 .and_then(|t| t.providers.get(cap))
3741 else {
3742 return;
3743 };
3744 if provider.given.is_empty() {
3745 return;
3746 }
3747 let consumed = unit_consumes.get(provider_ctx).cloned().unwrap_or_default();
3748 let aliases = unit_consumes_aliases
3749 .get(provider_ctx)
3750 .cloned()
3751 .unwrap_or_default();
3752 let flattened = unit_flattened
3753 .get(provider_ctx)
3754 .cloned()
3755 .unwrap_or_default();
3756 for g in &provider.given {
3757 let target_ctx = match g.prefix() {
3758 Some(p) => resolve_consume_prefix(&p, &consumed, &aliases)
3759 .unwrap_or_else(|| provider_ctx.to_string()),
3760 None => flattened
3761 .get(g.key())
3762 .cloned()
3763 .unwrap_or_else(|| provider_ctx.to_string()),
3764 };
3765 collect_given_closure(
3766 &target_ctx,
3767 g.key(),
3768 unit_tables,
3769 unit_consumes,
3770 unit_consumes_aliases,
3771 unit_flattened,
3772 reached,
3773 );
3774 }
3775}
3776
3777fn native_platforms_of_context(
3789 ctx: &str,
3790 table: &UnitTable,
3791 unit_tables: &HashMap<String, UnitTable>,
3792 unit_consumes: &HashMap<String, Vec<String>>,
3793 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3794 unit_flattened: &HashMap<String, HashMap<String, String>>,
3795) -> BTreeMap<Platform, String> {
3796 let mut reached: BTreeSet<(String, String)> = BTreeSet::new();
3797 for cap in table.providers.keys() {
3798 collect_given_closure(
3799 ctx,
3800 cap,
3801 unit_tables,
3802 unit_consumes,
3803 unit_consumes_aliases,
3804 unit_flattened,
3805 &mut reached,
3806 );
3807 }
3808 let consumed = unit_consumes.get(ctx).cloned().unwrap_or_default();
3809 let aliases = unit_consumes_aliases.get(ctx).cloned().unwrap_or_default();
3810 let flattened = unit_flattened.get(ctx).cloned().unwrap_or_default();
3811 for (key, cctx) in handler_cross_caps(table, &consumed, &aliases, &flattened) {
3812 collect_given_closure(
3813 &cctx,
3814 &key,
3815 unit_tables,
3816 unit_consumes,
3817 unit_consumes_aliases,
3818 unit_flattened,
3819 &mut reached,
3820 );
3821 }
3822 let mut referenced: BTreeSet<String> = reached.into_iter().map(|(unit, _)| unit).collect();
3823 referenced.extend(table.derived.values().map(|d| d.base_unit.clone()));
3826 let mut out = BTreeMap::new();
3827 for unit in referenced {
3828 if let Some(p) = firstparty::platform_of(&unit) {
3829 out.entry(p).or_insert(unit);
3830 }
3831 }
3832 out
3833}
3834
3835#[allow(clippy::too_many_arguments)]
3848pub fn phase_platform_lock(
3849 target: BuildTarget,
3850 selected: Platform,
3851 parsed: &[ParsedFile],
3852 groups: &BTreeMap<String, Vec<usize>>,
3853 kinds: &BTreeMap<String, UnitKind>,
3854 unit_tables: &HashMap<String, UnitTable>,
3855 unit_consumes: &HashMap<String, Vec<String>>,
3856 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3857 unit_flattened: &HashMap<String, HashMap<String, String>>,
3858 errors: &mut ErrorSink,
3859) {
3860 if selected == Platform::Browser && target == BuildTarget::Workers {
3865 errors.push_for(
3866 None,
3867 CompileError::new(
3868 "bynk.target.browser_bundle_only",
3869 Span::default(),
3870 "`--platform browser` builds only the in-process `Bundle` topology, but `--target workers` was selected; a browser cannot run the Workers wire-call model",
3871 )
3872 .with_note("build the browser target with `--target bundle` (the default)"),
3873 );
3874 return;
3875 }
3876 let mut per_context: Vec<(String, BTreeMap<Platform, String>)> = Vec::new();
3881 let mut names: Vec<&String> = groups.keys().collect();
3882 names.sort();
3883 for name in names {
3884 if kinds.get(name.as_str()) != Some(&UnitKind::Context) {
3885 continue;
3886 }
3887 let Some(table) = unit_tables.get(name.as_str()) else {
3888 continue;
3889 };
3890 let native = native_platforms_of_context(
3891 name,
3892 table,
3893 unit_tables,
3894 unit_consumes,
3895 unit_consumes_aliases,
3896 unit_flattened,
3897 );
3898 if !native.is_empty() {
3899 per_context.push((name.clone(), native));
3900 }
3901 }
3902 let units: Vec<(String, BTreeMap<Platform, String>)> = match target {
3905 BuildTarget::Workers => per_context,
3906 BuildTarget::Bundle => {
3907 let mut union = BTreeMap::new();
3908 let mut owner: Option<String> = None;
3909 for (ctx, native) in per_context {
3910 owner.get_or_insert(ctx);
3911 for (p, unit) in native {
3912 union.entry(p).or_insert(unit);
3913 }
3914 }
3915 match owner {
3916 Some(ctx) if !union.is_empty() => vec![(ctx, union)],
3917 _ => Vec::new(),
3918 }
3919 }
3920 };
3921 for (ctx, native) in units {
3922 let Some(violation) = lock_violation(&native, selected) else {
3923 continue;
3924 };
3925 let span_for = |unit: &str| {
3926 groups
3927 .get(&ctx)
3928 .and_then(|idx| consumes_span_of(parsed, idx, unit))
3929 .map(|(_, s)| s)
3930 .unwrap_or_default()
3931 };
3932 match violation {
3933 LockViolation::Required { needed, unit } => {
3934 errors.push_for(
3935 None,
3936 CompileError::new(
3937 "bynk.target.vendor_required",
3938 span_for(&unit),
3939 format!(
3940 "context `{ctx}` uses the platform-native capabilities of `{unit}`, which run only on the `{}` platform, but the build selects `--platform {}`",
3941 needed.as_str(),
3942 selected.as_str(),
3943 ),
3944 )
3945 .with_note(
3946 "build with the matching `--platform`, or remove the platform-native dependency to stay portable",
3947 ),
3948 );
3949 }
3950 LockViolation::Conflict { a, b } => {
3951 errors.push_for(
3952 None,
3953 CompileError::new(
3954 "bynk.target.vendor_conflict",
3955 span_for(&a.1),
3956 format!(
3957 "one deployment unit (via context `{ctx}`) uses platform-native capabilities from two mutually-exclusive platforms: `{}` (from `{}`) and `{}` (from `{}`)",
3958 a.0.as_str(),
3959 a.1,
3960 b.0.as_str(),
3961 b.1,
3962 ),
3963 )
3964 .with_note(
3965 "split the consumers into separate deployment units (`--target workers`), or remove one of the platform-native dependencies",
3966 ),
3967 );
3968 }
3969 }
3970 }
3971}
3972
3973pub fn phase_secrets_computed_name(
3996 target: BuildTarget,
3997 parsed: &[ParsedFile],
3998 groups: &BTreeMap<String, Vec<usize>>,
3999 kinds: &BTreeMap<String, UnitKind>,
4000 unit_flattened: &HashMap<String, HashMap<String, String>>,
4001 errors: &mut ErrorSink,
4002) {
4003 if target != BuildTarget::Workers {
4004 return;
4005 }
4006 for (name, indices) in groups {
4007 if kinds.get(name) != Some(&UnitKind::Context) {
4008 continue;
4009 }
4010 let Some(flattened) = unit_flattened.get(name) else {
4011 continue;
4012 };
4013 for &i in indices {
4014 let SourceUnit::Context(ctx) = &parsed[i].unit() else {
4015 continue;
4016 };
4017 let items = &ctx.items;
4020 let handlers = items.iter().filter_map(|item| match item {
4021 CommonsItem::Service(s) => Some(s.handlers.iter()),
4022 CommonsItem::Agent(a) => Some(a.handlers.iter()),
4023 _ => None,
4024 });
4025 let providers = items.iter().filter_map(|item| match item {
4026 CommonsItem::Provider(p) => Some(p),
4027 _ => None,
4028 });
4029 let (_, mut warnings) = crate::secrets::secret_reads_of(handlers.flatten(), flattened);
4030 warnings.extend(crate::secrets::provider_secret_reads_of(providers, flattened).1);
4031 let rel = parsed[i].identity_path();
4032 errors.extend_for(Some(&rel), warnings);
4033 }
4034 }
4035}
4036
4037pub fn phase_workers_unprovided_capabilities(
4048 target: BuildTarget,
4049 parsed: &[ParsedFile],
4050 groups: &BTreeMap<String, Vec<usize>>,
4051 kinds: &BTreeMap<String, UnitKind>,
4052 unit_tables: &HashMap<String, UnitTable>,
4053 errors: &mut ErrorSink,
4054) {
4055 if target != BuildTarget::Workers {
4056 return;
4057 }
4058 for (name, indices) in groups {
4059 if kinds.get(name) != Some(&UnitKind::Context) {
4060 continue;
4061 }
4062 let Some(table) = unit_tables.get(name) else {
4063 continue;
4064 };
4065 let mut reported: HashSet<String> = HashSet::new();
4068 for &i in indices {
4069 let SourceUnit::Context(ctx) = &parsed[i].unit() else {
4070 continue;
4071 };
4072 let mut givens: Vec<&CapRef> = Vec::new();
4073 for item in &ctx.items {
4074 match item {
4075 CommonsItem::Service(s) => {
4076 givens.extend(s.handlers.iter().flat_map(|h| &h.given));
4077 }
4078 CommonsItem::Agent(a) => {
4079 givens.extend(a.handlers.iter().flat_map(|h| &h.given));
4080 }
4081 CommonsItem::Provider(p) => givens.extend(&p.given),
4082 _ => {}
4083 }
4084 }
4085 for cap in givens {
4086 if cap.context.is_some()
4090 || !table.capabilities.contains_key(&cap.name.name)
4091 || table.providers.contains_key(&cap.name.name)
4092 || table.derived.contains_key(&cap.name.name)
4094 || table.exported_capabilities.contains(&cap.name.name)
4095 || !reported.insert(cap.name.name.clone())
4096 {
4097 continue;
4098 }
4099 errors.push_for(
4100 Some(&parsed[i].identity_path()),
4101 CompileError::new(
4102 "bynk.capability.not_provided",
4103 cap.span,
4104 format!(
4105 "capability `{}` is required here but has no provider in context `{name}`; a Worker builds every capability its handlers are given",
4106 cap.name.name
4107 ),
4108 )
4109 .with_note(format!(
4110 "add `provides {} = …` to `{name}`, or build for `--target bundle`, where the host supplies it",
4111 cap.name.name
4112 )),
4113 );
4114 }
4115 }
4116 }
4117}
4118
4119pub fn phase_workers_ws_open_split(
4129 target: BuildTarget,
4130 parsed: &[ParsedFile],
4131 groups: &BTreeMap<String, Vec<usize>>,
4132 kinds: &BTreeMap<String, UnitKind>,
4133 unit_tables: &HashMap<String, UnitTable>,
4134 errors: &mut ErrorSink,
4135) {
4136 if target != BuildTarget::Workers {
4137 return;
4138 }
4139 for (name, indices) in groups {
4140 if kinds.get(name) != Some(&UnitKind::Context) || indices.len() < 2 {
4141 continue;
4142 }
4143 let Some(table) = unit_tables.get(name) else {
4144 continue;
4145 };
4146 let local_agents: HashSet<String> = table.agents.keys().cloned().collect();
4147 for &i in indices {
4148 let SourceUnit::Context(ctx) = &parsed[i].unit() else {
4149 continue;
4150 };
4151 for item in &ctx.items {
4152 let CommonsItem::Service(service) = item else {
4153 continue;
4154 };
4155 if !matches!(service.protocol, ServiceProtocol::WebSocket { .. }) {
4156 continue;
4157 }
4158 for open in service
4159 .handlers
4160 .iter()
4161 .filter(|h| matches!(h.kind, HandlerKind::Open))
4162 {
4163 let crate::websocket::WsOpenShape::One(target_agent) =
4164 crate::websocket::analyse_open_shape(&open.body, &local_agents)
4165 else {
4166 continue; };
4168 let declared_here = ctx.items.iter().any(|it| {
4169 matches!(it, CommonsItem::Agent(a) if a.name.name == target_agent.agent)
4170 });
4171 if declared_here {
4172 continue;
4173 }
4174 errors.push_for(
4175 Some(&parsed[i].identity_path()),
4176 CompileError::new(
4177 "bynk.ws.open_transfer_cross_file",
4178 open.span,
4179 format!(
4180 "this `on open` transfers its `connection` to agent `{}`, which is declared in a different file of context `{name}` than the service `{}`",
4181 target_agent.agent, service.name.name
4182 ),
4183 )
4184 .with_note(
4185 "on Workers the open is hosted inside the agent's Durable Object, which is emitted from the file that declares the agent; move the service into the agent's file, or build for `--target bundle`",
4186 ),
4187 );
4188 }
4189 }
4190 }
4191 }
4192}
4193
4194pub fn provider_constructs_agent(p: &ProviderDecl, is_agent: impl Fn(&str) -> bool) -> bool {
4204 fn walk(e: &Expr, is_agent: &dyn Fn(&str) -> bool) -> bool {
4205 if let ExprKind::Call { name, .. } = &e.kind
4206 && is_agent(&name.name)
4207 {
4208 return true;
4209 }
4210 expr_children(e).into_iter().any(|c| walk(c, is_agent))
4211 }
4212 p.ops.iter().any(|op| {
4213 let mut top = Vec::new();
4214 for s in &op.body.statements {
4215 statement_exprs(s, &mut top);
4216 }
4217 top.push(&op.body.tail);
4218 top.into_iter().any(|e| walk(e, &is_agent))
4219 })
4220}
4221
4222#[allow(clippy::too_many_arguments)]
4235pub fn phase_workers_provider_agent_cross_context(
4236 target: BuildTarget,
4237 parsed: &[ParsedFile],
4238 groups: &BTreeMap<String, Vec<usize>>,
4239 kinds: &BTreeMap<String, UnitKind>,
4240 unit_tables: &HashMap<String, UnitTable>,
4241 unit_consumes: &HashMap<String, Vec<String>>,
4242 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
4243 unit_flattened: &HashMap<String, HashMap<String, String>>,
4244 errors: &mut ErrorSink,
4245) {
4246 if target != BuildTarget::Workers {
4247 return;
4248 }
4249 for (name, indices) in groups {
4250 if kinds.get(name) != Some(&UnitKind::Context) {
4251 continue;
4252 }
4253 let consumed = unit_consumes.get(name).cloned().unwrap_or_default();
4254 let aliases = unit_consumes_aliases.get(name).cloned().unwrap_or_default();
4255 let flattened = unit_flattened.get(name).cloned().unwrap_or_default();
4256 let mut reported: HashSet<(String, String)> = HashSet::new();
4257 for &i in indices {
4258 let SourceUnit::Context(ctx) = &parsed[i].unit() else {
4259 continue;
4260 };
4261 let mut givens: Vec<&CapRef> = Vec::new();
4262 for item in &ctx.items {
4263 match item {
4264 CommonsItem::Service(s) => {
4265 givens.extend(s.handlers.iter().flat_map(|h| &h.given));
4266 }
4267 CommonsItem::Agent(a) => {
4268 givens.extend(a.handlers.iter().flat_map(|h| &h.given));
4269 }
4270 CommonsItem::Provider(p) => givens.extend(&p.given),
4271 _ => {}
4272 }
4273 }
4274 for cap in givens {
4275 let target_ctx = match cap.prefix() {
4277 Some(p) => resolve_consume_prefix(&p, &consumed, &aliases)
4278 .unwrap_or_else(|| name.clone()),
4279 None => flattened
4280 .get(cap.key())
4281 .cloned()
4282 .unwrap_or_else(|| name.clone()),
4283 };
4284 let mut reached: BTreeSet<(String, String)> = BTreeSet::new();
4285 collect_given_closure(
4286 &target_ctx,
4287 cap.key(),
4288 unit_tables,
4289 unit_consumes,
4290 unit_consumes_aliases,
4291 unit_flattened,
4292 &mut reached,
4293 );
4294 for (unit, key) in reached {
4295 if unit == *name {
4296 continue;
4297 }
4298 let Some(table) = unit_tables.get(&unit) else {
4299 continue;
4300 };
4301 let Some(provider) = table.providers.get(&key) else {
4302 continue;
4303 };
4304 if provider.external
4305 || !provider_constructs_agent(provider, |n| table.agents.contains_key(n))
4306 || !reported.insert((unit.clone(), key.clone()))
4307 {
4308 continue;
4309 }
4310 errors.push_for(
4311 Some(&parsed[i].identity_path()),
4312 CompileError::new(
4313 "bynk.capability.provider_agent_cross_context",
4314 cap.span,
4315 format!(
4316 "capability `{key}` is provided by `{}` in context `{unit}`, which constructs `{unit}`'s agents; on Workers context `{name}` would build that provider with its own `env`, which has no binding for those agents' Durable Objects",
4317 provider.provider_name.name
4318 ),
4319 )
4320 .with_note(format!(
4321 "call a `{unit}` service that uses `{key}` instead, or build for `--target bundle`, where every context shares one process"
4322 )),
4323 );
4324 }
4325 }
4326 }
4327 }
4328}
4329
4330pub fn phase_file_index(
4333 groups: &BTreeMap<String, Vec<usize>>,
4334 parsed: &[ParsedFile],
4335) -> HashMap<String, FileDeclIndex> {
4336 let mut unit_file_index: HashMap<String, FileDeclIndex> = HashMap::new();
4337 for (name, indices) in groups {
4338 unit_file_index.insert(name.clone(), build_file_decl_index(indices, parsed));
4339 }
4340 unit_file_index
4341}
4342
4343pub struct UnitInfo {
4351 pub kind: UnitKind,
4352 pub table: UnitTable,
4353 pub uses: Vec<String>,
4354 pub consumes: Vec<String>,
4355 pub flattened: HashMap<String, String>,
4356 pub aliases: HashMap<String, String>,
4357 pub exports: HashMap<String, Visibility>,
4358 pub file_index: FileDeclIndex,
4359 pub files: Vec<usize>,
4360}
4361
4362#[allow(clippy::too_many_arguments)]
4369pub fn assemble_unit_info(
4370 groups: &BTreeMap<String, Vec<usize>>,
4371 kinds: &BTreeMap<String, UnitKind>,
4372 unit_tables: &HashMap<String, UnitTable>,
4373 unit_uses: &HashMap<String, Vec<String>>,
4374 unit_consumes: &HashMap<String, Vec<String>>,
4375 unit_flattened: &HashMap<String, HashMap<String, String>>,
4376 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
4377 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
4378 unit_file_index: &HashMap<String, FileDeclIndex>,
4379) -> BTreeMap<String, UnitInfo> {
4380 groups
4381 .iter()
4382 .map(|(name, indices)| {
4383 let info = UnitInfo {
4384 kind: *kinds.get(name).unwrap(),
4385 table: unit_tables.get(name).unwrap().clone(),
4386 uses: unit_uses.get(name).cloned().unwrap_or_default(),
4387 consumes: unit_consumes.get(name).cloned().unwrap_or_default(),
4388 flattened: unit_flattened.get(name).cloned().unwrap_or_default(),
4389 aliases: unit_consumes_aliases.get(name).cloned().unwrap_or_default(),
4390 exports: exports_visibility.get(name).cloned().unwrap_or_default(),
4391 file_index: unit_file_index
4392 .get(name)
4393 .cloned()
4394 .unwrap_or_else(|| FileDeclIndex {
4395 types: HashMap::new(),
4396 fns: HashMap::new(),
4397 methods: HashMap::new(),
4398 agents: HashMap::new(),
4399 capabilities: HashMap::new(),
4400 }),
4401 files: indices.clone(),
4402 };
4403 (name.clone(), info)
4404 })
4405 .collect()
4406}
4407
4408pub fn collect_unit_methods(
4413 indices: &[usize],
4414 parsed: &[ParsedFile],
4415) -> HashMap<String, Vec<FnDecl>> {
4416 let mut local_methods_for_type: HashMap<String, Vec<FnDecl>> = HashMap::new();
4417 for &j in indices {
4418 for item in parsed[j].items() {
4419 if let CommonsItem::Fn(f) = item
4420 && let FnName::Method { type_name, .. } = &f.name
4421 {
4422 local_methods_for_type
4423 .entry(type_name.name.clone())
4424 .or_default()
4425 .push(f.clone());
4426 }
4427 }
4428 }
4429 local_methods_for_type
4430}
4431
4432#[allow(clippy::too_many_arguments)]
4438pub fn merge_consumed_exports(
4439 name: &str,
4440 parsed: &[ParsedFile],
4441 unit_info: &BTreeMap<String, UnitInfo>,
4442 combined_types: &mut HashMap<String, Arc<TypeDecl>>,
4443 combined_methods: &mut HashMap<String, ResolverMethodTable>,
4444 imported_from: &mut HashMap<String, String>,
4445 imported_from_kind: &mut HashMap<String, UnitKind>,
4446 errors: &mut ErrorSink,
4447) -> HashMap<String, ConsumedType> {
4448 let mut consumed_types: HashMap<String, ConsumedType> = HashMap::new();
4454
4455 for t in unit_info.get(name).into_iter().flat_map(|i| &i.consumes) {
4459 let used = &unit_info.get(t).expect("consumed unit present").table;
4460 let used_exports = &unit_info[t].exports;
4461 for (type_name, vis) in used_exports {
4462 let Some(decl) = used.types.get(type_name) else {
4463 continue;
4464 };
4465 let local_value = crate::symbols::local_value_kind(&unit_info[name].table, type_name)
4469 .map(|(kind, _)| kind)
4470 .filter(|kind| !matches!(*kind, "type" | "event"));
4471 if combined_types.contains_key(type_name) || local_value.is_some() {
4472 let consumes_site = consumes_span_of(parsed, &unit_info[name].files, t);
4474 let consumes_span = consumes_site.map(|(_, s)| s).unwrap_or_default();
4475 let consumes_file = consumes_site.map(|(i, _)| parsed[i].identity_path());
4476 let in_scope = match local_value {
4477 Some(kind) => format!("this context declares a {kind} of the same name"),
4478 None => "a type of the same name is already in scope".to_string(),
4479 };
4480 errors.push_for(consumes_file.as_deref(),
4481 CompileError::new(
4482 "bynk.consumes.name_conflict",
4483 consumes_span,
4484 format!(
4485 "context `{name}` consumes `{t}` which exports type `{type_name}`, but {in_scope}",
4486 ),
4487 )
4488 .with_note(
4489 "rename one of the conflicting declarations or restructure the import",
4490 ),
4491 );
4492 continue;
4493 }
4494 combined_types.insert(type_name.clone(), decl.clone());
4495 imported_from.insert(type_name.clone(), t.clone());
4496 imported_from_kind.insert(type_name.clone(), UnitKind::Context);
4497 consumed_types.insert(
4498 type_name.clone(),
4499 ConsumedType {
4500 owning_context: t.clone(),
4501 visibility: *vis,
4502 },
4503 );
4504 if let Some(mt) = used.methods.get(type_name) {
4512 let entry = combined_methods.entry(type_name.clone()).or_default();
4513 for (m, decl) in &mt.instance {
4514 entry
4515 .instance
4516 .entry(m.clone())
4517 .or_insert_with(|| decl.clone());
4518 }
4519 }
4523 }
4524 }
4525
4526 consumed_types
4527}
4528
4529#[allow(clippy::too_many_arguments)]
4567pub fn close_reachable_types(
4568 name: &str,
4569 parsed: &[ParsedFile],
4570 unit_info: &BTreeMap<String, UnitInfo>,
4571 combined_types: &mut HashMap<String, Arc<TypeDecl>>,
4572 combined_fns: &HashMap<String, Arc<FnDecl>>,
4573 combined_methods: &mut HashMap<String, ResolverMethodTable>,
4574 imported_from: &mut HashMap<String, String>,
4575 imported_from_kind: &mut HashMap<String, UnitKind>,
4576 errors: &mut ErrorSink,
4577) -> ReachedTypes {
4578 let mut conflicted: HashSet<String> = HashSet::new();
4579 let mut shadowed = ShadowedReaches::default();
4580 let consumed_signatures = consumed_service_seeds(name, unit_info);
4581 let hidden = close_imported(
4582 unit_info,
4583 combined_types,
4584 combined_fns,
4585 combined_methods,
4586 imported_from,
4587 imported_from_kind,
4588 consumed_signatures,
4589 |r, owner, bound| {
4590 if r.signature {
4594 return;
4595 }
4596 let bound = bound.unwrap_or(name);
4597 let directly_used = unit_info[name].uses.iter().any(|u| u == owner);
4602 if bound != owner && directly_used {
4606 shadowed.0.insert(
4607 (r.scope.clone(), r.name.clone()),
4608 ShadowedType {
4609 name: r.name.clone(),
4610 owner: owner.to_string(),
4611 bound: bound.to_string(),
4612 },
4613 );
4614 }
4615 if bound != owner && !directly_used && conflicted.insert(r.name.clone()) {
4616 let site = uses_span_of(parsed, &unit_info[name].files, &r.via)
4617 .or_else(|| consumes_span_of(parsed, &unit_info[name].files, &r.via));
4618 let span = site.map(|(_, s)| s).unwrap_or_default();
4619 let file = site.map(|(i, _)| parsed[i].identity_path());
4620 errors.push_for(
4621 file.as_deref(),
4622 CompileError::new(
4623 "bynk.uses.name_conflict",
4624 span,
4625 format!(
4626 "`{via}` brings in declarations that use type `{t}` from `{owner}`, \
4627 but `{t}` in `{name}` names `{bound}`'s type",
4628 via = r.via,
4629 t = r.name,
4630 ),
4631 )
4632 .with_note(
4633 "rename one of the two types; the imported declarations cannot be \
4634 checked against a different type of the same name",
4635 ),
4636 );
4637 }
4638 },
4639 );
4640 ReachedTypes { hidden, shadowed }
4641}
4642
4643pub fn close_view_types(
4663 imported: &[String],
4664 unit_tables: &HashMap<String, UnitTable>,
4665 unit_uses: &HashMap<String, Vec<String>>,
4666 types: &mut HashMap<String, Arc<TypeDecl>>,
4667 fns: &HashMap<String, Arc<FnDecl>>,
4668 methods: &mut HashMap<String, ResolverMethodTable>,
4669) -> ViewReach {
4670 let mut imported_from: HashMap<String, String> = HashMap::new();
4671 for u in imported {
4672 let Some(t) = unit_tables.get(u) else {
4673 continue;
4674 };
4675 for (n, d) in &t.types {
4676 if types.get(n).is_some_and(|v| Arc::ptr_eq(v, d)) {
4677 imported_from.entry(n.clone()).or_insert_with(|| u.clone());
4678 }
4679 }
4680 for (n, d) in &t.fns {
4681 if fns.get(n).is_some_and(|v| Arc::ptr_eq(v, d)) {
4682 imported_from.entry(n.clone()).or_insert_with(|| u.clone());
4683 }
4684 }
4685 }
4686 let mut conflicts: Vec<ViewConflict> = Vec::new();
4687 let mut seen: HashSet<String> = HashSet::new();
4688 let hidden = close_imported(
4689 &TableScopes {
4690 tables: unit_tables,
4691 uses: unit_uses,
4692 },
4693 types,
4694 fns,
4695 methods,
4696 &mut imported_from,
4697 &mut HashMap::new(),
4698 Vec::new(),
4699 |r, owner, bound| {
4700 if bound != Some(owner) && seen.insert(r.name.clone()) {
4701 conflicts.push(ViewConflict {
4702 name: r.name.clone(),
4703 owner: owner.to_string(),
4704 bound: bound.map(str::to_string),
4705 via: r.via.clone(),
4706 });
4707 }
4708 },
4709 );
4710 ViewReach { hidden, conflicts }
4711}
4712
4713#[derive(Debug, Clone, Default)]
4715pub struct ViewReach {
4716 pub hidden: BTreeMap<String, String>,
4718 pub conflicts: Vec<ViewConflict>,
4721}
4722
4723#[derive(Debug, Clone, PartialEq, Eq)]
4726pub struct ViewConflict {
4727 pub name: String,
4729 pub owner: String,
4731 pub bound: Option<String>,
4734 pub via: String,
4736}
4737
4738trait ScopeTables {
4744 fn scope(&self, unit: &str) -> Option<(&UnitTable, &[String], Option<UnitKind>)>;
4745}
4746
4747impl ScopeTables for BTreeMap<String, UnitInfo> {
4748 fn scope(&self, unit: &str) -> Option<(&UnitTable, &[String], Option<UnitKind>)> {
4749 self.get(unit)
4750 .map(|i| (&i.table, i.uses.as_slice(), Some(i.kind)))
4751 }
4752}
4753
4754struct TableScopes<'a> {
4756 tables: &'a HashMap<String, UnitTable>,
4757 uses: &'a HashMap<String, Vec<String>>,
4758}
4759
4760impl ScopeTables for TableScopes<'_> {
4761 fn scope(&self, unit: &str) -> Option<(&UnitTable, &[String], Option<UnitKind>)> {
4762 let table = self.tables.get(unit)?;
4763 let uses = self.uses.get(unit).map(Vec::as_slice).unwrap_or_default();
4764 Some((table, uses, table.kind))
4765 }
4766}
4767
4768struct Reached {
4773 name: String,
4774 scope: String,
4775 via: String,
4776 signature: bool,
4780}
4781
4782fn push_refs(names: Vec<String>, scope: &str, via: &str, queue: &mut Vec<Reached>) {
4783 push_refs_with(names, scope, via, false, queue);
4784}
4785
4786fn push_refs_with(
4787 names: Vec<String>,
4788 scope: &str,
4789 via: &str,
4790 signature: bool,
4791 queue: &mut Vec<Reached>,
4792) {
4793 queue.extend(names.into_iter().map(|n| Reached {
4794 name: n,
4795 scope: scope.to_string(),
4796 via: via.to_string(),
4797 signature,
4798 }));
4799}
4800
4801fn consumed_service_seeds(name: &str, unit_info: &BTreeMap<String, UnitInfo>) -> Vec<Reached> {
4808 let mut out = Vec::new();
4809 let Some(info) = unit_info.get(name) else {
4810 return out;
4811 };
4812 for t in &info.consumes {
4813 let Some(consumed) = unit_info.get(t) else {
4814 continue;
4815 };
4816 let mut services: Vec<&ServiceDecl> = consumed.table.services.values().collect();
4817 services.sort_by(|a, b| a.name.name.cmp(&b.name.name));
4818 for s in services {
4819 if let Some(h) = s.handlers.iter().find(|h| h.kind == HandlerKind::Call) {
4822 let mut names = Vec::new();
4823 for p in &h.params {
4824 type_ref_names(&p.type_ref, &HashSet::new(), &mut names);
4825 }
4826 type_ref_names(&h.return_type, &HashSet::new(), &mut names);
4827 push_refs_with(names, t, t, true, &mut out);
4828 }
4829 }
4830 }
4831 out
4832}
4833
4834#[allow(clippy::too_many_arguments)]
4841fn close_imported<S: ScopeTables + ?Sized>(
4842 scopes: &S,
4843 combined_types: &mut HashMap<String, Arc<TypeDecl>>,
4844 combined_fns: &HashMap<String, Arc<FnDecl>>,
4845 combined_methods: &mut HashMap<String, ResolverMethodTable>,
4846 imported_from: &mut HashMap<String, String>,
4847 imported_from_kind: &mut HashMap<String, UnitKind>,
4848 extra_seeds: Vec<Reached>,
4851 mut on_bound: impl FnMut(&Reached, &str, Option<&str>),
4852) -> BTreeMap<String, String> {
4853 let mut queue: Vec<Reached> = Vec::new();
4854 let mut type_names: Vec<&String> = combined_types.keys().collect();
4855 type_names.sort();
4856 for n in type_names {
4857 if let Some(unit) = imported_from.get(n) {
4858 push_refs(
4859 type_decl_ref_names(&combined_types[n]),
4860 unit,
4861 unit,
4862 &mut queue,
4863 );
4864 }
4865 }
4866 let mut fn_names: Vec<&String> = combined_fns.keys().collect();
4867 fn_names.sort();
4868 for n in fn_names {
4869 if let Some(unit) = imported_from.get(n) {
4870 push_refs(
4871 fn_sig_ref_names(&combined_fns[n], &[]),
4872 unit,
4873 unit,
4874 &mut queue,
4875 );
4876 }
4877 }
4878 let mut method_types: Vec<&String> = combined_methods.keys().collect();
4879 method_types.sort();
4880 for t in method_types {
4881 if let Some(unit) = imported_from.get(t) {
4882 let mt = &combined_methods[t];
4883 let attached = combined_types.get(t).map(|d| d.type_params.as_slice());
4884 let mut decls: Vec<&Arc<FnDecl>> =
4885 mt.instance.values().chain(mt.statics.values()).collect();
4886 decls.sort_by_key(|d| d.span.start);
4887 for d in decls {
4888 push_refs(
4889 fn_sig_ref_names(d, attached.unwrap_or_default()),
4890 unit,
4891 unit,
4892 &mut queue,
4893 );
4894 }
4895 }
4896 }
4897 queue.extend(extra_seeds);
4898 queue.reverse();
4901
4902 let mut hidden: BTreeMap<String, String> = BTreeMap::new();
4903 let mut visited: HashSet<(String, String)> = HashSet::new();
4904 while let Some(r) = queue.pop() {
4905 if !visited.insert((r.scope.clone(), r.name.clone())) {
4906 continue;
4907 }
4908 let Some(owner) = type_owner_in_scope(&r.scope, &r.name, scopes) else {
4909 continue;
4910 };
4911 let Some((owner_table, _, owner_kind)) = scopes.scope(owner) else {
4916 continue;
4917 };
4918 if owner_kind != Some(UnitKind::Commons) {
4919 continue;
4920 }
4921 if combined_types.contains_key(&r.name) {
4922 on_bound(&r, owner, imported_from.get(&r.name).map(String::as_str));
4923 continue;
4924 }
4925 let decl = owner_table.types[&r.name].clone();
4926 push_refs_with(
4927 type_decl_ref_names(&decl),
4928 owner,
4929 &r.via,
4930 r.signature,
4931 &mut queue,
4932 );
4933 combined_types.insert(r.name.clone(), decl.clone());
4934 imported_from.insert(r.name.clone(), owner.to_string());
4935 imported_from_kind.insert(r.name.clone(), UnitKind::Commons);
4936 hidden.insert(r.name.clone(), owner.to_string());
4937 if let Some(mt) = owner_table.methods.get(&r.name) {
4940 let entry = combined_methods.entry(r.name.clone()).or_default();
4941 let mut decls: Vec<(&String, &Arc<FnDecl>, bool)> = mt
4942 .instance
4943 .iter()
4944 .map(|(m, d)| (m, d, true))
4945 .chain(mt.statics.iter().map(|(m, d)| (m, d, false)))
4946 .collect();
4947 decls.sort_by_key(|(_, d, _)| d.span.start);
4948 for (m, d, is_instance) in decls {
4949 push_refs_with(
4950 fn_sig_ref_names(d, &decl.type_params),
4951 owner,
4952 &r.via,
4953 r.signature,
4954 &mut queue,
4955 );
4956 let table = if is_instance {
4957 &mut entry.instance
4958 } else {
4959 &mut entry.statics
4960 };
4961 table.entry(m.clone()).or_insert_with(|| d.clone());
4962 }
4963 }
4964 }
4965 hidden
4966}
4967
4968#[derive(Debug, Clone, Default)]
4970pub struct ReachedTypes {
4971 pub hidden: BTreeMap<String, String>,
4974 pub shadowed: ShadowedReaches,
4976}
4977
4978#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4981pub struct ShadowedType {
4982 pub name: String,
4984 pub owner: String,
4986 pub bound: String,
4989}
4990
4991#[derive(Debug, Clone, Default)]
5001pub struct ShadowedReaches(HashMap<(String, String), ShadowedType>);
5002
5003impl ShadowedReaches {
5004 pub fn is_empty(&self) -> bool {
5006 self.0.is_empty()
5007 }
5008
5009 pub fn in_type_ref(&self, scope: &str, r: &TypeRef, vars: &[TypeParam]) -> Vec<&ShadowedType> {
5013 let vars: HashSet<&str> = vars.iter().map(|p| p.name.name.as_str()).collect();
5014 let mut names = Vec::new();
5015 type_ref_names(r, &vars, &mut names);
5016 let mut out: Vec<&ShadowedType> = names
5017 .iter()
5018 .filter_map(|n| self.0.get(&(scope.to_string(), n.clone())))
5019 .collect();
5020 out.sort();
5021 out.dedup();
5022 out
5023 }
5024}
5025
5026fn type_owner_in_scope<'a, S: ScopeTables + ?Sized>(
5030 scope: &'a str,
5031 name: &str,
5032 scopes: &'a S,
5033) -> Option<&'a str> {
5034 let (table, uses, _) = scopes.scope(scope)?;
5035 if table.types.contains_key(name) {
5036 return Some(scope);
5037 }
5038 uses.iter()
5039 .find(|u| {
5040 scopes
5041 .scope(u)
5042 .is_some_and(|(t, _, _)| t.types.contains_key(name))
5043 })
5044 .map(String::as_str)
5045}
5046
5047pub(crate) fn type_decl_ref_names(d: &TypeDecl) -> Vec<String> {
5050 let vars: HashSet<&str> = d.type_params.iter().map(|p| p.name.name.as_str()).collect();
5051 let mut out = Vec::new();
5052 match &d.body {
5053 TypeBody::Record(r) => {
5054 for f in &r.fields {
5055 type_ref_names(&f.type_ref, &vars, &mut out);
5056 }
5057 }
5058 TypeBody::Sum(s) => {
5059 for v in &s.variants {
5060 for p in &v.payload {
5061 type_ref_names(&p.type_ref, &vars, &mut out);
5062 }
5063 }
5064 }
5065 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
5066 }
5067 out
5068}
5069
5070fn fn_sig_ref_names(f: &FnDecl, attached: &[TypeParam]) -> Vec<String> {
5074 let vars: HashSet<&str> = f
5075 .type_params
5076 .iter()
5077 .chain(attached)
5078 .map(|p| p.name.name.as_str())
5079 .collect();
5080 let mut out = Vec::new();
5081 for p in &f.params {
5082 type_ref_names(&p.type_ref, &vars, &mut out);
5083 }
5084 type_ref_names(&f.return_type, &vars, &mut out);
5085 out
5086}
5087
5088pub(crate) fn type_ref_names(r: &TypeRef, vars: &HashSet<&str>, out: &mut Vec<String>) {
5092 match r {
5093 TypeRef::Named(id) => {
5094 if !vars.contains(id.name.as_str()) {
5095 out.push(id.name.clone());
5096 }
5097 }
5098 TypeRef::App { name, args, .. } => {
5099 if !vars.contains(name.name.as_str()) {
5100 out.push(name.name.clone());
5101 }
5102 for a in args {
5103 type_ref_names(a, vars, out);
5104 }
5105 }
5106 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
5107 type_ref_names(a, vars, out);
5108 type_ref_names(b, vars, out);
5109 }
5110 TypeRef::Option(t, _)
5111 | TypeRef::Effect(t, _)
5112 | TypeRef::HttpResult(t, _)
5113 | TypeRef::List(t, _)
5114 | TypeRef::Query(t, _)
5115 | TypeRef::Stream(t, _)
5116 | TypeRef::Connection(t, _)
5117 | TypeRef::History(t, _) => type_ref_names(t, vars, out),
5118 TypeRef::Fn(params, ret, _) => {
5119 for p in params {
5120 type_ref_names(p, vars, out);
5121 }
5122 type_ref_names(ret, vars, out);
5123 }
5124 TypeRef::Base(..)
5125 | TypeRef::QueueResult(_)
5126 | TypeRef::Asset(_)
5127 | TypeRef::RequestHeaders(_)
5128 | TypeRef::TokenClaims(_)
5129 | TypeRef::BoundaryRejection(_)
5130 | TypeRef::ValidationError(_)
5131 | TypeRef::JsonError(_)
5132 | TypeRef::Unit(_) => {}
5133 }
5134}
5135
5136#[allow(clippy::type_complexity)]
5141pub fn compose_unit_symbols(
5142 name: &str,
5143 local_table: &UnitTable,
5144 unit_info: &BTreeMap<String, UnitInfo>,
5145) -> (
5146 HashMap<String, Arc<TypeDecl>>,
5147 HashMap<String, Arc<FnDecl>>,
5148 HashMap<String, ResolverMethodTable>,
5149 HashMap<String, String>,
5150 HashMap<String, UnitKind>,
5151) {
5152 let mut combined_types = local_table.types.clone();
5157 let mut combined_fns = local_table.fns.clone();
5158 let mut combined_methods = local_table.methods.clone();
5159 let mut imported_from: HashMap<String, String> = HashMap::new();
5160 let mut imported_from_kind: HashMap<String, UnitKind> = HashMap::new();
5161
5162 for t in unit_info.get(name).into_iter().flat_map(|i| &i.uses) {
5163 let used = &unit_info.get(t).expect("used unit present").table;
5164 for (type_name, decl) in &used.types {
5165 if !combined_types.contains_key(type_name) {
5166 combined_types.insert(type_name.clone(), decl.clone());
5167 imported_from.insert(type_name.clone(), t.clone());
5168 imported_from_kind.insert(type_name.clone(), UnitKind::Commons);
5169 }
5170 }
5171 for (fn_name, decl) in &used.fns {
5172 if !combined_fns.contains_key(fn_name)
5175 && crate::symbols::local_value_kind(local_table, fn_name).is_none()
5176 {
5177 combined_fns.insert(fn_name.clone(), decl.clone());
5178 imported_from.insert(fn_name.clone(), t.clone());
5179 imported_from_kind.insert(fn_name.clone(), UnitKind::Commons);
5180 }
5181 }
5182 for asset_name in used.assets.keys() {
5187 if crate::symbols::local_value_kind(local_table, asset_name).is_none()
5188 && !combined_fns.contains_key(asset_name)
5189 && !imported_from.contains_key(asset_name)
5190 {
5191 imported_from.insert(asset_name.clone(), t.clone());
5192 imported_from_kind.insert(asset_name.clone(), UnitKind::Commons);
5193 }
5194 }
5195 for embed_name in used.embeds.keys() {
5197 if crate::symbols::local_value_kind(local_table, embed_name).is_none()
5198 && !combined_fns.contains_key(embed_name)
5199 && !imported_from.contains_key(embed_name)
5200 {
5201 imported_from.insert(embed_name.clone(), t.clone());
5202 imported_from_kind.insert(embed_name.clone(), UnitKind::Commons);
5203 }
5204 }
5205 for (type_name, mt) in &used.methods {
5206 let entry = combined_methods.entry(type_name.clone()).or_default();
5207 for (m, decl) in &mt.instance {
5208 entry
5209 .instance
5210 .entry(m.clone())
5211 .or_insert_with(|| decl.clone());
5212 }
5213 for (m, decl) in &mt.statics {
5214 entry
5215 .statics
5216 .entry(m.clone())
5217 .or_insert_with(|| decl.clone());
5218 }
5219 }
5220 }
5221
5222 (
5223 combined_types,
5224 combined_fns,
5225 combined_methods,
5226 imported_from,
5227 imported_from_kind,
5228 )
5229}
5230
5231pub fn shadowed_imports(
5249 name: &str,
5250 unit_info: &BTreeMap<String, UnitInfo>,
5251) -> HashMap<String, crate::resolver::ShadowedImport> {
5252 let Some(info) = unit_info.get(name) else {
5253 return HashMap::new();
5254 };
5255 shadowed_imports_in(
5256 &info.table,
5257 info.uses
5258 .iter()
5259 .filter_map(|t| unit_info.get(t).map(|u| (t, &u.table))),
5260 )
5261}
5262
5263pub fn shadowed_imports_in<'a>(
5267 local: &UnitTable,
5268 uses: impl Iterator<Item = (&'a String, &'a UnitTable)>,
5269) -> HashMap<String, crate::resolver::ShadowedImport> {
5270 let mut out = HashMap::new();
5271 for (t, used) in uses {
5272 let imports = used
5275 .fns
5276 .keys()
5277 .map(|n| (n, "function"))
5278 .chain(used.assets.keys().map(|n| (n, "asset")))
5279 .chain(used.types.keys().map(|n| (n, "type")));
5280 for (n, imported) in imports {
5281 if let Some((kind, local_span)) = crate::symbols::local_value_kind(local, n)
5282 && kind != imported
5283 && !(imported == "type" && kind == "event")
5284 {
5285 let hidden = crate::resolver::ShadowedImport {
5286 unit: t.clone(),
5287 imported,
5288 local: kind,
5289 local_span,
5290 };
5291 match out.entry(n.clone()) {
5297 std::collections::hash_map::Entry::Vacant(e) => {
5298 e.insert(hidden);
5299 }
5300 std::collections::hash_map::Entry::Occupied(mut e) => {
5301 if imported == "type" && e.get().imported != "type" {
5302 e.insert(hidden);
5303 }
5304 }
5305 }
5306 }
5307 }
5308 }
5309 out
5310}
5311
5312pub fn phase_detect_consumes_cycles(
5318 groups: &BTreeMap<String, Vec<usize>>,
5319 parsed: &[ParsedFile],
5320 unit_consumes: &HashMap<String, Vec<String>>,
5321 errors: &mut ErrorSink,
5322) {
5323 let mut consumes_sites: HashMap<(String, String), (PathBuf, Span)> = HashMap::new();
5324 for (name, indices) in groups {
5325 for &i in indices {
5326 if parsed[i].is_synthetic() {
5327 continue;
5328 }
5329 for c in parsed[i].consumes() {
5330 consumes_sites
5331 .entry((name.clone(), c.target.joined()))
5332 .or_insert_with(|| (parsed[i].identity_path(), c.span));
5333 }
5334 }
5335 }
5336 let mut cycle_errors: Vec<(Option<PathBuf>, CompileError)> = Vec::new();
5337 detect_consumes_cycles(unit_consumes, &consumes_sites, &mut cycle_errors);
5338 for (path, err) in cycle_errors {
5339 errors.push_for(path.as_deref(), err);
5340 }
5341}
5342
5343#[cfg(test)]
5344mod platform_lock_tests {
5345 use super::{LockViolation, Platform, lock_violation};
5346 use std::collections::BTreeMap;
5347
5348 fn native(entries: &[(Platform, &str)]) -> BTreeMap<Platform, String> {
5349 entries
5350 .iter()
5351 .map(|(p, u)| (*p, (*u).to_string()))
5352 .collect()
5353 }
5354
5355 #[test]
5356 fn empty_closure_imposes_no_lock() {
5357 assert_eq!(lock_violation(&native(&[]), Platform::Node), None);
5358 }
5359
5360 #[test]
5361 fn matching_platform_is_fine() {
5362 let n = native(&[(Platform::Cloudflare, "bynk.cloudflare")]);
5363 assert_eq!(lock_violation(&n, Platform::Cloudflare), None);
5364 }
5365
5366 #[test]
5367 fn mismatched_platform_is_required() {
5368 let n = native(&[(Platform::Cloudflare, "bynk.cloudflare")]);
5369 assert_eq!(
5370 lock_violation(&n, Platform::Node),
5371 Some(LockViolation::Required {
5372 needed: Platform::Cloudflare,
5373 unit: "bynk.cloudflare".to_string(),
5374 })
5375 );
5376 }
5377
5378 #[test]
5383 fn two_platforms_conflict_regardless_of_selection() {
5384 let n = native(&[
5385 (Platform::Cloudflare, "bynk.cloudflare"),
5386 (Platform::Node, "bynk.synthetic"),
5387 ]);
5388 let v = lock_violation(&n, Platform::Cloudflare);
5389 assert_eq!(
5390 v,
5391 Some(LockViolation::Conflict {
5392 a: (Platform::Cloudflare, "bynk.cloudflare".to_string()),
5393 b: (Platform::Node, "bynk.synthetic".to_string()),
5394 })
5395 );
5396 }
5397}
5398
5399#[cfg(test)]
5400mod native_platform_closure_tests {
5401 use super::{HashMap, Platform, UnitTable, native_platforms_of_context};
5402 use bynk_syntax::ast::{CapRef, Ident, ProviderDecl, QualifiedName};
5403 use bynk_syntax::span::Span;
5404 use std::collections::HashMap as StdHashMap;
5405
5406 fn ident(name: &str) -> Ident {
5407 Ident {
5408 name: name.to_string(),
5409 span: Span::default(),
5410 }
5411 }
5412
5413 fn qualified(parts: &[&str]) -> QualifiedName {
5414 QualifiedName {
5415 parts: parts.iter().map(|p| ident(p)).collect(),
5416 span: Span::default(),
5417 }
5418 }
5419
5420 fn given_cap(prefix: Option<&[&str]>, name: &str) -> CapRef {
5421 CapRef {
5422 context: prefix.map(qualified),
5423 name: ident(name),
5424 span: Span::default(),
5425 }
5426 }
5427
5428 fn provider(capability: &str, given: Vec<CapRef>) -> ProviderDecl {
5429 ProviderDecl {
5430 capability: ident(capability),
5431 provider_name: ident(&format!("{capability}Impl")),
5432 given,
5433 ops: Vec::new(),
5434 external: false,
5435 documentation: None,
5436 span: Span::default(),
5437 trivia: Default::default(),
5438 }
5439 }
5440
5441 fn empty_table() -> UnitTable {
5442 UnitTable {
5443 kind: None,
5444 removed_agents: Vec::new(),
5445 types: StdHashMap::new(),
5446 fns: StdHashMap::new(),
5447 methods: StdHashMap::new(),
5448 capabilities: StdHashMap::new(),
5449 providers: StdHashMap::new(),
5450 services: StdHashMap::new(),
5451 agents: StdHashMap::new(),
5452 actors: StdHashMap::new(),
5453 exported_capabilities: Default::default(),
5454 events: StdHashMap::new(),
5455 flattened_caps: StdHashMap::new(),
5456 assets: StdHashMap::new(),
5457 embeds: StdHashMap::new(),
5458 derived: StdHashMap::new(),
5459 }
5460 }
5461
5462 #[test]
5475 fn a_providers_given_chain_into_a_platform_native_unit_is_recognised() {
5476 let mut table = empty_table();
5477 table.providers.insert(
5478 "Cache".to_string(),
5479 provider(
5480 "Cache",
5481 vec![given_cap(Some(&["bynk", "cloudflare"]), "Kv")],
5482 ),
5483 );
5484 let mut unit_tables = HashMap::new();
5485 unit_tables.insert("app.web".to_string(), table);
5486 let mut unit_consumes = HashMap::new();
5487 unit_consumes.insert("app.web".to_string(), vec!["bynk.cloudflare".to_string()]);
5488
5489 let native = native_platforms_of_context(
5490 "app.web",
5491 unit_tables.get("app.web").unwrap(),
5492 &unit_tables,
5493 &unit_consumes,
5494 &HashMap::new(),
5495 &HashMap::new(),
5496 );
5497 assert_eq!(
5498 native.get(&Platform::Cloudflare).map(String::as_str),
5499 Some("bynk.cloudflare"),
5500 "a provider's own `given` closure into a platform-native unit must be \
5501 walked recursively, not just a handler's direct `given` — got {native:?}"
5502 );
5503 }
5504
5505 #[test]
5509 fn a_providers_given_chain_into_an_ordinary_unit_is_not_native() {
5510 let mut table = empty_table();
5511 table.providers.insert(
5512 "Cache".to_string(),
5513 provider("Cache", vec![given_cap(None, "Clock")]),
5514 );
5515 let mut unit_tables = HashMap::new();
5516 unit_tables.insert("app.web".to_string(), table);
5517
5518 let native = native_platforms_of_context(
5519 "app.web",
5520 unit_tables.get("app.web").unwrap(),
5521 &unit_tables,
5522 &HashMap::new(),
5523 &HashMap::new(),
5524 &HashMap::new(),
5525 );
5526 assert!(
5527 native.is_empty(),
5528 "a `given` closure that never reaches a platform-native unit must not \
5529 report one — got {native:?}"
5530 );
5531 }
5532}
5533
5534#[cfg(test)]
5535mod view_closure_tests {
5536 use super::{HashMap, UnitTable, build_unit_table, close_view_types};
5537 use bynk_project::{ParsedFile, UnitKind};
5538 use bynk_syntax::{lexer, parser};
5539 use std::path::PathBuf;
5540
5541 fn table(name: &str, kind: UnitKind, src: &str) -> UnitTable {
5542 let tokens = lexer::tokenize(src).expect("lex");
5543 let unit = parser::parse_unit(&tokens, src).expect("parse");
5544 let path = PathBuf::from(format!("{}.bynk", name.replace('.', "/")));
5545 let pf = ParsedFile::new(path.clone(), path, None, src.to_string(), unit, kind, false);
5546 let mut errors = Vec::new();
5547 build_unit_table(name, kind, &[0], &[pf], &mut errors)
5548 }
5549
5550 #[test]
5556 fn a_kindless_table_is_still_searched_as_a_scope() {
5557 let core = table(
5558 "t.core",
5559 UnitKind::Commons,
5560 "commons t.core\n\ntype Repo = String where NonEmpty\n",
5561 );
5562 let mut model = table(
5563 "t.model",
5564 UnitKind::Commons,
5565 "commons t.model\n\nuses t.core\n\ntype Run = { repo: Repo }\n",
5566 );
5567 model.kind = None;
5568 let mut types = model.types.clone();
5569 let tables: HashMap<String, UnitTable> =
5570 [("t.core".to_string(), core), ("t.model".to_string(), model)].into();
5571 let uses: HashMap<String, Vec<String>> =
5572 [("t.model".to_string(), vec!["t.core".to_string()])].into();
5573 let reach = close_view_types(
5574 &["t.model".to_string()],
5575 &tables,
5576 &uses,
5577 &mut types,
5578 &HashMap::new(),
5579 &mut HashMap::new(),
5580 );
5581 assert_eq!(
5582 reach.hidden.get("Repo").map(String::as_str),
5583 Some("t.core"),
5584 "{:?}",
5585 reach.hidden
5586 );
5587 assert!(types.contains_key("Repo"));
5588 }
5589}
5590
5591#[cfg(test)]
5592mod label_attribution_tests {
5593 use super::*;
5594
5595 #[test]
5600 fn labels_are_attributed_to_the_file_their_span_is_in() {
5601 let (own, sibling, injected) = (FileId(10), FileId(11), FileId(12));
5602 let mut sink = ErrorSink::new();
5603 sink.register_file(own, PathBuf::from("src/app/z_more.bynk"));
5604 sink.register_file(sibling, PathBuf::from("src/app/a_web.bynk"));
5605 let error = CompileError::new(
5606 "bynk.resolve.duplicate_service",
5607 Span::new_in(own, 24, 27),
5608 "service `api` is already declared",
5609 )
5610 .with_label(Span::new_in(own, 0, 1), "same id")
5611 .with_label(Span::new(0, 1), "no id")
5612 .with_label(Span::new_in(sibling, 21, 24), "sibling")
5613 .with_label(Span::new_in(injected, 0, 1), "unregistered");
5614 sink.push_for(Some(Path::new("src/app/z_more.bynk")), error);
5615 let all = sink.into_all();
5616 assert_eq!(
5617 all[0].label_files,
5618 vec![
5619 LabelFile::Primary,
5620 LabelFile::Primary,
5621 LabelFile::Other(PathBuf::from("src/app/a_web.bynk")),
5622 LabelFile::Unavailable,
5623 ]
5624 );
5625 }
5626
5627 #[test]
5631 fn a_label_in_a_firstparty_unit_is_attributed_to_that_unit() {
5632 let unit = firstparty::unit("bynk").expect("the `bynk` unit is registered");
5633 let own = FileId(30);
5634 let mut sink = ErrorSink::new();
5635 sink.register_file(own, PathBuf::from("src/app/web.bynk"));
5636 let error = CompileError::new(
5637 "bynk.resolve.missing_field",
5638 Span::new_in(own, 0, 7),
5639 "missing required field `headers` for record `Request`",
5640 )
5641 .with_label(
5642 Span::new_in(firstparty_file_id(unit), 0, 7),
5643 "field declared here",
5644 );
5645 sink.push_for(Some(Path::new("src/app/web.bynk")), error);
5646 assert_eq!(
5647 sink.into_all()[0].label_files,
5648 vec![LabelFile::FirstParty {
5649 identity_path: unit.identity_path,
5650 source: unit.source,
5651 }]
5652 );
5653 }
5654
5655 #[test]
5658 fn a_label_naming_the_errors_own_path_is_primary() {
5659 let own = FileId(20);
5660 let mut sink = ErrorSink::new();
5661 sink.register_file(own, PathBuf::from("src/a.bynk"));
5662 let error = CompileError::new("bynk.test", Span::default(), "msg")
5663 .with_label(Span::new_in(own, 0, 1), "here");
5664 sink.push_for(Some(Path::new("src/a.bynk")), error);
5665 assert_eq!(sink.into_all()[0].label_files, vec![LabelFile::Primary]);
5666 }
5667}
5668
5669#[cfg(test)]
5676mod shadowed_import_tests {
5677 use crate::testkit::analyse;
5678
5679 const IMPORTED: [(&str, &str, &str, &str); 3] = [
5682 (
5683 "function",
5684 "fn thing() -> Int {\n 1\n}\n",
5685 "thing()",
5686 "Int",
5687 ),
5688 (
5689 "asset",
5690 "asset thing = \"assets/x.json\"\n",
5691 "thing.url",
5692 "String",
5693 ),
5694 (
5696 "type",
5697 "type thing = { n: Int }\n",
5698 "thing { n: 1 }.n",
5699 "Int",
5700 ),
5701 ];
5702
5703 const LOCAL: [(&str, &str); 7] = [
5704 ("function", "fn thing() -> Int {\n 2\n}\n"),
5705 ("asset", "asset thing = \"assets/x.json\"\n"),
5706 ("type", "type thing = { n: Int }\n"),
5707 ("event", "event thing = { n: Int }\n"),
5708 (
5709 "service",
5710 "service thing {\n on call() -> Effect[Int] {\n Effect.pure(1)\n }\n}\n",
5711 ),
5712 (
5713 "agent",
5714 "agent thing {\n key id: String\n\n store hits: Cell[Int]\n\n on call ping() -> Effect[Int] {\n 1\n }\n}\n",
5715 ),
5716 (
5717 "provider",
5718 "capability ProbeCap {\n fn go() -> Effect[Int]\n}\n\nprovides ProbeCap = thing {\n fn go() -> Effect[Int] {\n Effect.pure(1)\n }\n}\n",
5719 ),
5720 ];
5721
5722 #[derive(Clone, Copy, Debug)]
5723 enum Use {
5724 None,
5725 FnBody,
5726 HandlerBody,
5727 }
5728
5729 #[test]
5730 fn every_local_kind_against_every_import_and_use_site() {
5731 for (imported, idecl, expr, ty) in IMPORTED {
5732 for (local, ldecl) in LOCAL {
5733 for at in [Use::None, Use::FnBody, Use::HandlerBody] {
5734 let using = match at {
5735 Use::None => String::new(),
5736 Use::FnBody => format!("fn useIt() -> {ty} {{\n {expr}\n}}\n"),
5737 Use::HandlerBody => format!(
5738 "service user {{\n on call() -> Effect[{ty}] {{\n Effect.pure({expr})\n }}\n}}\n"
5739 ),
5740 };
5741 let lib = format!("commons lib.util\n\n{idecl}");
5742 let app = format!("context app\n\nuses lib.util\n\n{ldecl}\n{using}");
5743 let r = analyse(&[
5744 ("lib/util.bynk", &lib),
5745 ("app/main.bynk", &app),
5746 ("assets/x.json", "{}"),
5747 ]);
5748 let cats = r.error_categories();
5749 let case = format!("local {local} over imported {imported}, used {at:?}");
5750 let want: Vec<&str> = match at {
5751 Use::None => vec![],
5753 _ if local == imported => vec![],
5754 _ if imported == "type" && local == "event" => vec![],
5757 Use::FnBody if imported == "asset" && matches!(local, "type" | "event") => {
5761 vec!["bynk.resolve.unknown_static_member"]
5762 }
5763 Use::HandlerBody if imported == "function" && local == "agent" => {
5767 vec!["bynk.agent.construction_arity"]
5768 }
5769 _ => vec!["bynk.uses.name_conflict"],
5770 };
5771 assert_eq!(cats, want, "{case}");
5772 }
5773 }
5774 }
5775 }
5776
5777 #[test]
5781 fn a_local_agent_answers_its_own_call() {
5782 let r = analyse(&[
5783 (
5784 "lib/util.bynk",
5785 "commons lib.util\n\nfn thing() -> Int {\n 1\n}\n",
5786 ),
5787 (
5788 "app/main.bynk",
5789 "context app\n\nuses lib.util\n\nagent thing {\n key id: String\n\n store hits: Cell[Int]\n\n on call ping() -> Effect[Int] {\n 1\n }\n}\n\nservice web {\n on call() -> Effect[Int] {\n let n <- thing(\"k\").ping()\n Effect.pure(n)\n }\n}\n",
5790 ),
5791 ]);
5792 r.assert_clean();
5793 }
5794
5795 #[test]
5799 fn a_local_service_hiding_two_used_imports_is_no_ambiguity() {
5800 let r = analyse(&[
5801 (
5802 "lib/a.bynk",
5803 "commons lib.a\n\nfn thing() -> Int {\n 1\n}\n",
5804 ),
5805 (
5806 "lib/b.bynk",
5807 "commons lib.b\n\nfn thing() -> Int {\n 2\n}\n",
5808 ),
5809 (
5810 "app/main.bynk",
5811 "context app\n\nuses lib.a\nuses lib.b\n\nservice thing {\n on call() -> Effect[Int] {\n Effect.pure(1)\n }\n}\n",
5812 ),
5813 ]);
5814 r.assert_clean();
5815 }
5816
5817 #[test]
5821 fn a_bare_local_function_keeps_its_own_advice() {
5822 let lib = (
5823 "lib/util.bynk",
5824 "commons lib.util\n\nasset thing = \"assets/x.json\"\n",
5825 );
5826 let file = |body: &str| {
5827 format!(
5828 "context app\n\nuses lib.util\n\nfn thing() -> Int {{\n 1\n}}\n\nfn useIt() -> Int {{\n {body}\n}}\n"
5829 )
5830 };
5831 let bare = file("thing + 1");
5832 let r = analyse(&[lib, ("app/main.bynk", &bare), ("assets/x.json", "{}")]);
5833 assert_eq!(r.error_categories(), vec!["bynk.resolve.fn_without_call"]);
5834 let read = file("let u = thing.url\n 1");
5835 let r = analyse(&[lib, ("app/main.bynk", &read), ("assets/x.json", "{}")]);
5836 assert_eq!(r.error_categories(), vec!["bynk.uses.name_conflict"]);
5837 }
5838
5839 const STATUS_LIB: (&str, &str) = (
5842 "lib/st.bynk",
5843 "commons lib.st\n\ntype Status =\n | Done\n | Pending\n | Failed(reason: String)\n\nfn mk() -> Status {\n Done\n}\n\nfn isDone(s: Status) -> Bool {\n match s {\n Done => true\n _ => false\n }\n}\n",
5844 );
5845
5846 #[test]
5850 fn a_hidden_sums_bare_variant_constructs_only_by_conflict() {
5851 let unit = |body: &str| {
5852 format!("commons app\n\nuses lib.st\n\nfn Status() -> Int {{\n 1\n}}\n\n{body}")
5853 };
5854 let construct = unit("fn f() -> Bool {\n isDone(Done)\n}\n");
5855 let r = analyse(&[STATUS_LIB, ("app/main.bynk", &construct)]);
5856 assert_eq!(r.error_categories(), vec!["bynk.uses.name_conflict"]);
5857 let construct = unit("fn f() -> Bool {\n isDone(Failed(\"x\"))\n}\n");
5859 let r = analyse(&[STATUS_LIB, ("app/main.bynk", &construct)]);
5860 assert_eq!(r.error_categories(), vec!["bynk.uses.name_conflict"]);
5861 let matched = unit(
5862 "fn f() -> Bool {\n match mk() {\n Done => true\n Failed(r) => false\n _ => false\n }\n}\n",
5863 );
5864 let r = analyse(&[STATUS_LIB, ("app/main.bynk", &matched)]);
5865 r.assert_clean();
5866 }
5867
5868 #[test]
5872 fn a_hidden_types_values_still_flow() {
5873 let r = analyse(&[
5874 STATUS_LIB,
5875 (
5876 "app/main.bynk",
5877 "context app\n\nuses lib.st\n\nagent Status {\n key id: String\n\n store hits: Cell[Int]\n\n on call ping() -> Effect[Int] {\n 1\n }\n}\n\nfn f() -> Bool {\n let s = mk()\n isDone(s)\n}\n",
5878 ),
5879 ]);
5880 r.assert_clean();
5881 }
5882
5883 #[test]
5886 fn a_local_asset_answers_its_own_url_beside_a_hidden_type() {
5887 let r = analyse(&[
5888 STATUS_LIB,
5889 (
5890 "app/main.bynk",
5891 "commons app\n\nuses lib.st\n\nasset Status = \"assets/x.json\"\n\nfn f() -> String {\n Status.url\n}\n",
5892 ),
5893 ("assets/x.json", "{}"),
5894 ]);
5895 r.assert_clean();
5896 }
5897
5898 #[test]
5902 fn a_hidden_type_gates_whatever_the_uses_order() {
5903 let a = ("lib/a.bynk", "commons lib.a\n\ntype Status = { n: Int }\n");
5904 let b = (
5905 "lib/b.bynk",
5906 "commons lib.b\n\nfn Status() -> Int {\n 1\n}\n",
5907 );
5908 for (first, second) in [("lib.a", "lib.b"), ("lib.b", "lib.a")] {
5909 let app = format!(
5910 "context app\n\nuses {first}\nuses {second}\n\nservice Status {{\n on call() -> Effect[Int] {{\n Effect.pure(1)\n }}\n}}\n\nfn f(s: Status) -> Int {{\n s.n\n}}\n"
5911 );
5912 let r = analyse(&[a, b, ("app/main.bynk", &app)]);
5913 assert_eq!(
5914 r.error_categories(),
5915 vec!["bynk.uses.name_conflict"],
5916 "uses {first} then {second}"
5917 );
5918 }
5919 }
5920
5921 #[test]
5924 fn a_local_function_hiding_a_type_is_a_function_value() {
5925 let r = analyse(&[
5926 (
5927 "lib/util.bynk",
5928 "commons lib.util\n\ntype thing = { n: Int }\n",
5929 ),
5930 (
5931 "app/main.bynk",
5932 "commons app\n\nuses lib.util\n\nfn thing(x: Int) -> Int {\n x + 1\n}\n\nfn f() -> List[Int] {\n [1, 2].map(thing)\n}\n",
5933 ),
5934 ]);
5935 r.assert_clean();
5936 }
5937
5938 #[test]
5941 fn a_local_value_named_like_a_consumed_export_conflicts() {
5942 let r = analyse(&[
5943 (
5944 "shop.bynk",
5945 "context shop\n\nexports transparent { Order }\n\ntype Order = { id: String }\n",
5946 ),
5947 (
5948 "app.bynk",
5949 "context app\n\nconsumes shop\n\nservice Order {\n on call() -> Effect[Int] {\n Effect.pure(1)\n }\n}\n",
5950 ),
5951 ]);
5952 assert_eq!(r.error_categories(), vec!["bynk.consumes.name_conflict"]);
5953 }
5954
5955 #[test]
5957 fn the_conflict_names_both_declarations() {
5958 let r = analyse(&[
5959 (
5960 "lib/util.bynk",
5961 "commons lib.util\n\nfn thing() -> Int {\n 1\n}\n",
5962 ),
5963 (
5964 "app/main.bynk",
5965 "context app\n\nuses lib.util\n\ntype thing = { n: Int }\n\nfn useIt() -> Int {\n thing()\n}\n",
5966 ),
5967 ]);
5968 let e = r
5969 .analysis
5970 .errors
5971 .iter()
5972 .find(|e| e.error.category == "bynk.uses.name_conflict")
5973 .expect("a conflict");
5974 assert_eq!(
5975 e.error.message,
5976 "`thing` names this unit's type, which hides the function `thing` from `lib.util`"
5977 );
5978 }
5979}
5980
5981#[cfg(test)]
5982mod firstparty_injection_tests {
5983 use super::{
5984 ErrorSink, ParsedFile, UnitKind, firstparty, firstparty_file_id, inject_firstparty, lexer,
5985 parser,
5986 };
5987 use bynk_syntax::span::FileId;
5988 use std::collections::BTreeSet;
5989 use std::path::PathBuf;
5990
5991 const REFS: [&str; 7] = [
5994 "consumes bynk { Clock }",
5995 "consumes bynk.cloudflare { Kv }",
5996 "uses bynk.map",
5997 "uses bynk.list",
5998 "uses bynk.string",
5999 "uses bynk.locale",
6000 "uses bynk.locale.types",
6001 ];
6002
6003 #[test]
6007 fn injected_units_carry_their_reserved_file_id() {
6008 let ids: BTreeSet<FileId> = firstparty::UNITS.iter().map(firstparty_file_id).collect();
6009 assert_eq!(ids.len(), firstparty::UNITS.len());
6010 assert!(!ids.contains(&FileId::UNKNOWN));
6011 let mut parsed = project((1 << REFS.len()) - 1);
6012 let injected = inject_firstparty(&mut parsed, &mut ErrorSink::new());
6013 assert!(!injected.is_empty());
6014 for unit in injected {
6015 let pf = parsed
6016 .iter()
6017 .find(|pf| pf.identity_path() == std::path::Path::new(unit.identity_path))
6018 .expect("an injected unit is in `parsed`");
6019 assert_eq!(
6020 pf.unit().span().file,
6021 firstparty_file_id(unit),
6022 "{}",
6023 unit.name
6024 );
6025 }
6026 }
6027
6028 fn project(mask: u32) -> Vec<ParsedFile> {
6030 let mut uses = String::new();
6031 let mut consumes = String::new();
6032 for (bit, line) in REFS.iter().enumerate() {
6033 if mask & (1 << bit) != 0 {
6034 let into = if line.starts_with("uses") {
6035 &mut uses
6036 } else {
6037 &mut consumes
6038 };
6039 into.push_str(line);
6040 into.push('\n');
6041 }
6042 }
6043 let src = format!("context t\n\n{uses}{consumes}\nfn f() -> Int {{ 1 }}\n");
6044 let toks = lexer::tokenize(&src).expect("lexes");
6045 let (unit, _) =
6046 parser::parse_unit_with_warnings_from(&toks, &src, &mut { 0 }).expect("parses");
6047 vec![ParsedFile::synthetic(
6048 PathBuf::from("t.bynk"),
6049 PathBuf::from("t.bynk"),
6050 src,
6051 unit,
6052 UnitKind::Context,
6053 )]
6054 }
6055
6056 fn previous_injection(mask: u32) -> BTreeSet<&'static str> {
6059 let has = |bit: u32| mask & (1 << bit) != 0;
6060 let (bynk, cloudflare) = (has(0), has(1));
6061 let (map, list, string, locale, locale_types) = (has(2), has(3), has(4), has(5), has(6));
6062 let mut out = BTreeSet::new();
6063 let mut add = |cond: bool, name: &'static str| {
6064 if cond {
6065 out.insert(name);
6066 }
6067 };
6068 let bynk = bynk || cloudflare;
6072 add(bynk, "bynk");
6073 add(cloudflare, "bynk.cloudflare");
6074 add(map, "bynk.map");
6075 add(map || locale || list, "bynk.list");
6076 add(locale || string, "bynk.string");
6077 add(locale || locale_types || bynk, "bynk.locale.types");
6080 add(locale, "bynk.locale");
6081 out
6082 }
6083
6084 #[test]
6087 fn the_closure_matches_the_previous_gates_for_every_combination() {
6088 for mask in 0..(1u32 << REFS.len()) {
6089 let mut parsed = project(mask);
6090 let mut errors = ErrorSink::new();
6091 let injected = inject_firstparty(&mut parsed, &mut errors);
6092 assert!(
6093 errors.is_empty(),
6094 "mask {mask:#09b}: a first-party source failed to parse"
6095 );
6096 let names: BTreeSet<&str> = injected.iter().map(|u| u.name).collect();
6097 assert_eq!(names, previous_injection(mask), "mask {mask:#09b}");
6098 }
6099 }
6100
6101 #[test]
6105 fn injected_units_are_pushed_in_id_block_order() {
6106 let mut parsed = project((1 << REFS.len()) - 1);
6107 let mut errors = ErrorSink::new();
6108 let injected = inject_firstparty(&mut parsed, &mut errors);
6109 let pushed: Vec<String> = parsed[1..]
6110 .iter()
6111 .map(|pf| pf.unit().name().joined())
6112 .collect();
6113 assert_eq!(
6114 pushed,
6115 [
6116 "bynk",
6117 "bynk.cloudflare",
6118 "bynk.map",
6119 "bynk.list",
6120 "bynk.string",
6121 "bynk.locale.types",
6122 "bynk.locale",
6123 ]
6124 );
6125 let returned: Vec<&str> = injected.iter().map(|u| u.name).collect();
6126 assert_eq!(returned, pushed);
6127 assert!(injected.windows(2).all(|w| w[0].id_block < w[1].id_block));
6128 }
6129
6130 #[test]
6132 fn nothing_is_injected_unreferenced() {
6133 let mut parsed = project(0);
6134 let injected = inject_firstparty(&mut parsed, &mut ErrorSink::new());
6135 assert!(injected.is_empty());
6136 assert_eq!(parsed.len(), 1);
6137 }
6138
6139 #[test]
6143 fn injection_is_registry_driven() {
6144 let src = include_str!("project_model.rs");
6145 let start = src.find("pub fn phase_parse(").expect("phase_parse");
6146 let end = src[start..]
6147 .find("\n/// #1710: for each unit with a file the strict parse rejected")
6148 .map(|rel| start + rel)
6149 .expect("the item after inject_firstparty");
6150 let body = &src[start..end];
6151 for name in firstparty::UNITS.iter().map(|u| u.name) {
6152 assert!(!body.contains(&format!("\"{name}\"")), "names {name}");
6153 }
6154 let per_unit_constant = body
6155 .lines()
6156 .find(|l| l.contains("firstparty::") && (l.contains("_UNIT") || l.contains("_SRC")));
6157 assert_eq!(per_unit_constant, None, "references a per-unit constant");
6158 }
6159}
6160
6161fn display_canonical_names(error: &mut CompileError) {
6164 if error.message.contains('$') {
6165 error.message = bynk_project::display_names(&error.message);
6166 }
6167 for note in &mut error.notes {
6168 if note.contains('$') {
6169 *note = bynk_project::display_names(note);
6170 }
6171 }
6172 for (_, label) in &mut error.labels {
6173 if label.contains('$') {
6174 *label = bynk_project::display_names(label);
6175 }
6176 }
6177}