1use std::collections::{BTreeSet, HashMap, HashSet};
24use std::fs;
25use std::path::{Component, Path, PathBuf};
26
27use crate::emitter;
28use bynk_check::checker;
29use bynk_check::checker::{CapabilityInfo, CapabilityOpInfo, Ty};
30use bynk_check::expr_types::{ExprTypeSink, FileExprTypes};
31use bynk_check::firstparty::{self, Platform};
32use bynk_check::hints::{FileHints, HintSink};
33use bynk_check::index::{IndexBuilder, ProjectIndex, RefSink, SiteRef, SymbolKind};
34use bynk_check::locals::{FileLocals, LocalsSink};
35use bynk_check::requirements::{FileRequirements, RequirementSink};
36use bynk_check::resolver::{self, MethodTable as ResolverMethodTable, ResolvedCommons};
37use bynk_syntax::ast::*;
38use bynk_syntax::error::CompileError;
39use bynk_syntax::lexer;
40use bynk_syntax::parser;
41use bynk_syntax::span::Span;
42
43mod consistency;
44mod diagnostics;
45mod discovery;
46mod graph;
47mod paths;
48mod symbols;
49mod tests_emit;
50mod validate;
51
52use consistency::*;
53use diagnostics::*;
54use discovery::*;
55use graph::*;
56use paths::*;
57use symbols::*;
58use tests_emit::*;
59use validate::*;
60
61pub use diagnostics::{AttributedError, ProjectAnalysis, ProjectFailure};
64pub use paths::{
65 ProjectPaths, read_project_paths, worker_dir_name, worker_handlers_output_path,
66 worker_handlers_source_path,
67};
68pub use symbols::{FileDeclIndex, UnitTable};
69pub use validate::check_function_type_boundary_items;
70pub(crate) use validate::type_ref_is_held;
71pub(crate) use validate::type_refs_match;
72
73pub struct CompiledFile {
75 pub source_path: PathBuf,
77 pub output_path: PathBuf,
80 pub typescript: String,
82 pub source_map: Option<String>,
90 pub debug_metadata: Option<String>,
95}
96
97pub struct ProjectOutput {
99 pub files: Vec<CompiledFile>,
100 pub warnings: Vec<AttributedError>,
103 pub discovered: Vec<DiscoveredSuite>,
110}
111
112#[derive(Debug, Clone, PartialEq)]
118pub struct DiscoveredSuite {
119 pub name: String,
120 pub kind: &'static str,
121 pub cases: Vec<DiscoveredCase>,
122}
123
124#[derive(Debug, Clone, PartialEq)]
128pub struct DiscoveredCase {
129 pub name: String,
130 pub location: Option<TestLocation>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
136pub struct TestLocation {
137 pub path: String,
138 pub line: u32,
139 pub col: u32,
140}
141
142struct AdapterBinding {
147 output_path: PathBuf,
149 content: String,
151}
152
153#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
159pub enum BuildTarget {
160 #[default]
163 Bundle,
164 Workers,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum UnitKind {
174 Commons,
175 Context,
176 Test,
177 Integration,
179 Adapter,
181}
182
183impl UnitKind {
184 pub fn display(self) -> &'static str {
185 match self {
186 UnitKind::Commons => "commons",
187 UnitKind::Context => "context",
188 UnitKind::Test => "test",
189 UnitKind::Integration => "integration test",
190 UnitKind::Adapter => "adapter",
191 }
192 }
193}
194
195pub enum Roots {
197 Single(PathBuf),
200 Split {
205 project_root: PathBuf,
206 paths: ProjectPaths,
207 },
208}
209
210impl Roots {
211 fn resolve(&self) -> (PathBuf, PathBuf) {
216 match self {
217 Roots::Single(root) => (root.clone(), root.clone()),
218 Roots::Split {
219 project_root,
220 paths,
221 } => {
222 let primary = project_root.join(paths.include.first().cloned().unwrap_or_default());
223 let secondary = paths
224 .include
225 .get(1)
226 .map(|p| project_root.join(p))
227 .unwrap_or_else(|| primary.clone());
228 (primary, secondary)
229 }
230 }
231 }
232
233 fn tests_prefix(&self) -> PathBuf {
238 match self {
239 Roots::Single(_) => PathBuf::new(),
240 Roots::Split { paths, .. } => paths.include.get(1).cloned().unwrap_or_default(),
241 }
242 }
243
244 fn excludes(&self) -> Vec<PathBuf> {
249 match self {
250 Roots::Single(_) => Vec::new(),
251 Roots::Split {
252 project_root,
253 paths,
254 } => {
255 let mut ex: Vec<PathBuf> =
256 paths.exclude.iter().map(|p| project_root.join(p)).collect();
257 for cache in ["out", "node_modules"] {
258 ex.push(project_root.join(cache));
259 }
260 ex
261 }
262 }
263 }
264}
265
266#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
275pub enum ImportExt {
276 #[default]
277 Js,
278 Ts,
279}
280
281impl ImportExt {
282 pub fn as_str(self) -> &'static str {
285 match self {
286 ImportExt::Js => "js",
287 ImportExt::Ts => "ts",
288 }
289 }
290}
291
292pub struct CompileOptions {
296 pub target: BuildTarget,
297 pub platform: Platform,
298 pub roots: Roots,
299 pub import_ext: ImportExt,
302}
303
304impl CompileOptions {
305 pub fn single(root: impl Into<PathBuf>) -> Self {
307 Self {
308 target: BuildTarget::Bundle,
309 platform: Platform::default(),
310 roots: Roots::Single(root.into()),
311 import_ext: ImportExt::default(),
312 }
313 }
314
315 pub fn split(project_root: impl Into<PathBuf>, paths: ProjectPaths) -> Self {
319 Self {
320 target: BuildTarget::Bundle,
321 platform: Platform::default(),
322 roots: Roots::Split {
323 project_root: project_root.into(),
324 paths,
325 },
326 import_ext: ImportExt::default(),
327 }
328 }
329
330 pub fn target(mut self, target: BuildTarget) -> Self {
333 self.target = target;
334 self
335 }
336
337 pub fn import_ext(mut self, ext: ImportExt) -> Self {
340 self.import_ext = ext;
341 self
342 }
343
344 pub fn platform(mut self, platform: Platform) -> Self {
347 self.platform = platform;
348 self
349 }
350}
351
352pub fn compile_project(options: &CompileOptions) -> Result<ProjectOutput, ProjectFailure> {
357 let (src_root, tests_root) = options.roots.resolve();
358 let tests_prefix = options.roots.tests_prefix();
359 let excludes = options.roots.excludes();
360 let run = run_checks(
361 &src_root,
362 &tests_root,
363 &tests_prefix,
364 options.target,
365 options.platform,
366 options.import_ext,
367 Mode::Build,
368 &HashMap::new(),
369 &excludes,
370 None,
371 );
372 finish_build(run, options.import_ext)
373}
374
375pub fn compile_in_memory(
388 source: &str,
389 target: BuildTarget,
390 platform: Platform,
391) -> Result<ProjectOutput, ProjectFailure> {
392 let root = PathBuf::from(".");
396 let path = in_memory_logical_path(source);
397 let mut overlay = HashMap::new();
398 overlay.insert(path.clone(), source.to_string());
399 let run = run_checks(
400 &root,
401 &root,
402 &root,
403 target,
404 platform,
405 ImportExt::Js,
406 Mode::Build,
407 &overlay,
408 &[],
409 Some((vec![path], Vec::new())),
410 );
411 finish_build(run, ImportExt::Js)
412}
413
414pub fn analyse_in_memory(
422 source: &str,
423 target: BuildTarget,
424 platform: Platform,
425) -> Vec<AttributedError> {
426 let root = PathBuf::from(".");
427 let path = in_memory_logical_path(source);
428 let mut overlay = HashMap::new();
429 overlay.insert(path.clone(), source.to_string());
430 let run = run_checks(
431 &root,
432 &root,
433 &root,
434 target,
435 platform,
436 ImportExt::Js,
437 Mode::Analyse,
438 &overlay,
439 &[],
440 Some((vec![path], Vec::new())),
441 );
442 match run {
443 RunChecks::Bailed { errors, .. } | RunChecks::Checked { errors, .. } => errors.into_all(),
444 }
445}
446
447fn in_memory_logical_path(source: &str) -> PathBuf {
452 let parts: Option<Vec<String>> = lexer::tokenize(source)
453 .ok()
454 .and_then(|tokens| parser::parse_unit(&tokens, source).ok())
455 .map(|unit| {
456 let name = match &unit {
457 SourceUnit::Commons(c) => &c.name,
458 SourceUnit::Context(c) => &c.name,
459 SourceUnit::Adapter(a) => &a.name,
460 SourceUnit::Suite(t) => &t.target,
461 SourceUnit::Integration(i) => &i.name,
462 };
463 name.parts.iter().map(|i| i.name.clone()).collect()
464 });
465 match parts {
466 Some(p) if !p.is_empty() => {
467 let mut path = PathBuf::from(p.join("/"));
468 path.set_extension("bynk");
469 path
470 }
471 _ => PathBuf::from("main.bynk"),
472 }
473}
474
475fn finish_build(run: RunChecks, import_ext: ImportExt) -> Result<ProjectOutput, ProjectFailure> {
479 match run {
480 RunChecks::Bailed {
481 errors, snapshots, ..
482 } => Err(ProjectFailure {
483 errors: errors.into_all(),
486 snapshots,
487 }),
488 RunChecks::Checked {
489 errors, snapshots, ..
490 } if !errors.is_empty() => Err(ProjectFailure {
491 errors: errors.into_all(),
492 snapshots,
493 }),
494 RunChecks::Checked {
495 errors,
496 compiled,
497 runnable_tests,
498 integration_outputs,
499 integration_runnables,
500 groups,
501 kinds,
502 unit_consumes,
503 unit_consumes_aliases,
504 unit_tables,
505 unit_flattened,
506 adapter_bindings,
507 npm_deps,
508 target,
509 ..
510 } => {
511 let mut out = build_output(
512 compiled,
513 runnable_tests,
514 integration_outputs,
515 integration_runnables,
516 groups,
517 kinds,
518 unit_consumes,
519 unit_consumes_aliases,
520 unit_tables,
521 unit_flattened,
522 adapter_bindings,
523 npm_deps,
524 target,
525 import_ext,
526 );
527 out.warnings = errors.into_warnings();
530 Ok(out)
531 }
532 }
533}
534
535pub fn analyse_project(root: &Path, overlay: &HashMap<PathBuf, String>) -> ProjectAnalysis {
539 match run_checks(
540 root,
541 root,
542 Path::new(""),
543 BuildTarget::Bundle,
544 Platform::default(),
545 ImportExt::Js,
546 Mode::Analyse,
547 overlay,
548 &[],
549 None,
550 ) {
551 RunChecks::Bailed {
552 errors,
553 snapshots,
554 mut hints,
555 mut locals,
556 mut exprs,
557 mut requirements,
558 } => ProjectAnalysis {
559 snapshots,
560 errors: errors.into_all(),
563 index: ProjectIndex::default(),
564 hints: hints.take_files(),
565 locals: locals.take_files(),
566 expr_types: exprs.take_files(),
567 requirements: requirements.take_files(),
568 unit_sources: HashMap::new(),
570 },
571 RunChecks::Checked {
572 errors,
573 snapshots,
574 mut refs,
575 mut hints,
576 mut locals,
577 mut exprs,
578 mut requirements,
579 parsed,
580 unit_uses,
581 unit_consumes,
582 ..
583 } => {
584 let index = assemble_index(
585 &parsed,
586 &unit_uses,
587 &unit_consumes,
588 std::mem::take(&mut refs),
589 );
590 let mut unit_sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
594 for pf in &parsed {
595 if pf.synthetic {
596 continue;
597 }
598 unit_sources
599 .entry(pf.unit.name().joined())
600 .or_default()
601 .push(pf.source_path.clone());
602 }
603 ProjectAnalysis {
604 snapshots,
605 errors: errors.into_all(),
606 index,
607 hints: hints.take_files(),
608 locals: locals.take_files(),
609 expr_types: exprs.take_files(),
610 requirements: requirements.take_files(),
611 unit_sources,
612 }
613 }
614 }
615}
616
617fn record_analyse_types(
623 exprs: &mut ExprTypeSink,
624 source_path: &Path,
625 synthetic: bool,
626 types: &HashMap<Span, Ty>,
627) {
628 exprs.enter_file(source_path, synthetic);
629 exprs.record_file(types);
630}
631
632fn phase_discovery(
638 src_root: &Path,
639 tests_root: &Path,
640 split_mode: bool,
641 excludes: &[PathBuf],
642 errors: &mut ErrorSink,
643) -> Result<(Vec<PathBuf>, Vec<PathBuf>), ()> {
644 let src_files = match discover_bynk_files(src_root, excludes) {
645 Ok(f) => f,
646 Err(e) => {
647 errors.push_for(None, e);
648 return Err(());
649 }
650 };
651 let tests_files = if split_mode {
652 if tests_root.exists() {
655 match discover_bynk_files(tests_root, excludes) {
656 Ok(f) => f,
657 Err(e) => {
658 errors.push_for(None, e);
659 return Err(());
660 }
661 }
662 } else {
663 Vec::new()
664 }
665 } else {
666 Vec::new()
667 };
668 if src_files.is_empty() && tests_files.is_empty() {
669 errors.push_for(
670 None,
671 CompileError::new(
672 "bynk.project.no_sources",
673 Span::default(),
674 format!("no `.bynk` source files found under {}", src_root.display()),
675 ),
676 );
677 return Err(());
678 }
679 if let Err(e) = check_file_directory_conflicts(src_root, &src_files) {
680 errors.extend_for(None, e);
681 }
682 if split_mode && let Err(e) = check_file_directory_conflicts(tests_root, &tests_files) {
683 errors.extend_for(None, e);
684 }
685 Ok((src_files, tests_files))
686}
687
688#[allow(clippy::too_many_arguments)]
696fn phase_parse(
697 src_root: &Path,
698 tests_root: &Path,
699 split_mode: bool,
700 src_files: &[PathBuf],
701 tests_files: &[PathBuf],
702 overlay: &HashMap<PathBuf, String>,
703 errors: &mut ErrorSink,
704 snapshots: &mut Vec<(PathBuf, String)>,
705) -> Result<(Vec<ParsedFile>, bool, bool), ()> {
706 let mut parsed: Vec<ParsedFile> = Vec::new();
707 let parse_tree = |root: &Path,
708 files: &[PathBuf],
709 parsed: &mut Vec<ParsedFile>,
710 errors: &mut ErrorSink,
711 snapshots: &mut Vec<(PathBuf, String)>| {
712 for path in files {
713 let rel = path.strip_prefix(root).unwrap_or(path).to_path_buf();
714 let source = match read_source(path, overlay) {
715 Ok(s) => s,
716 Err(e) => {
717 errors.push_for(
718 Some(&rel),
719 CompileError::new(
720 "bynk.project.read_failed",
721 Span::default(),
722 format!("could not read `{}`: {e}", path.display()),
723 ),
724 );
725 continue;
726 }
727 };
728 snapshots.push((rel.clone(), source.clone()));
729 match parse_sources(root, path, source) {
730 Ok(pfs) => parsed.extend(pfs),
731 Err(errs) => errors.extend_for(Some(&rel), errs),
732 }
733 }
734 };
735 parse_tree(src_root, src_files, &mut parsed, errors, snapshots);
736 if split_mode {
737 parse_tree(tests_root, tests_files, &mut parsed, errors, snapshots);
738 }
739 if !errors.is_empty() && parsed.is_empty() {
740 return Err(());
741 }
742
743 let consumes_bynk = parsed.iter().any(|pf| {
749 pf.consumes()
750 .iter()
751 .any(|c| c.target.joined() == firstparty::BYNK_UNIT)
752 });
753 if consumes_bynk {
754 match lexer::tokenize(firstparty::BYNK_ADAPTER_SRC)
755 .map_err(|e| vec![e])
756 .and_then(|toks| parser::parse_unit(&toks, firstparty::BYNK_ADAPTER_SRC))
757 {
758 Ok(unit) => parsed.push(ParsedFile {
759 source_path: PathBuf::from("bynk.bynk"),
760 abs_path: None,
761 source: firstparty::BYNK_ADAPTER_SRC.to_string(),
762 unit,
763 kind: UnitKind::Adapter,
764 synthetic: true,
765 }),
766 Err(errs) => errors.extend_for(None, errs),
767 }
768 }
769 let consumes_cloudflare = parsed.iter().any(|pf| {
773 pf.consumes()
774 .iter()
775 .any(|c| c.target.joined() == firstparty::CLOUDFLARE_UNIT)
776 });
777 if consumes_cloudflare {
778 match lexer::tokenize(firstparty::CLOUDFLARE_ADAPTER_SRC)
779 .map_err(|e| vec![e])
780 .and_then(|toks| parser::parse_unit(&toks, firstparty::CLOUDFLARE_ADAPTER_SRC))
781 {
782 Ok(unit) => parsed.push(ParsedFile {
783 source_path: PathBuf::from("bynk/cloudflare.bynk"),
784 abs_path: None,
785 source: firstparty::CLOUDFLARE_ADAPTER_SRC.to_string(),
786 unit,
787 kind: UnitKind::Adapter,
788 synthetic: true,
789 }),
790 Err(errs) => errors.extend_for(None, errs),
791 }
792 }
793 let uses_unit = |parsed: &[ParsedFile], unit: &str| {
800 parsed
801 .iter()
802 .any(|pf| pf.uses().iter().any(|u| u.target.joined() == unit))
803 };
804 let uses_map = uses_unit(&parsed, firstparty::MAP_UNIT);
805 if uses_map {
806 match lexer::tokenize(firstparty::BYNK_MAP_SRC)
807 .map_err(|e| vec![e])
808 .and_then(|toks| parser::parse_unit(&toks, firstparty::BYNK_MAP_SRC))
809 {
810 Ok(unit) => parsed.push(ParsedFile {
811 source_path: PathBuf::from("bynk/map.bynk"),
812 abs_path: None,
813 source: firstparty::BYNK_MAP_SRC.to_string(),
814 unit,
815 kind: UnitKind::Commons,
816 synthetic: true,
817 }),
818 Err(errs) => errors.extend_for(None, errs),
819 }
820 }
821 if uses_map || uses_unit(&parsed, firstparty::LIST_UNIT) {
822 match lexer::tokenize(firstparty::BYNK_LIST_SRC)
823 .map_err(|e| vec![e])
824 .and_then(|toks| parser::parse_unit(&toks, firstparty::BYNK_LIST_SRC))
825 {
826 Ok(unit) => parsed.push(ParsedFile {
827 source_path: PathBuf::from("bynk/list.bynk"),
828 abs_path: None,
829 source: firstparty::BYNK_LIST_SRC.to_string(),
830 unit,
831 kind: UnitKind::Commons,
832 synthetic: true,
833 }),
834 Err(errs) => errors.extend_for(None, errs),
835 }
836 }
837 if uses_unit(&parsed, firstparty::STRING_UNIT) {
840 match lexer::tokenize(firstparty::BYNK_STRING_SRC)
841 .map_err(|e| vec![e])
842 .and_then(|toks| parser::parse_unit(&toks, firstparty::BYNK_STRING_SRC))
843 {
844 Ok(unit) => parsed.push(ParsedFile {
845 source_path: PathBuf::from("bynk/string.bynk"),
846 abs_path: None,
847 source: firstparty::BYNK_STRING_SRC.to_string(),
848 unit,
849 kind: UnitKind::Commons,
850 synthetic: true,
851 }),
852 Err(errs) => errors.extend_for(None, errs),
853 }
854 }
855
856 Ok((parsed, consumes_bynk, consumes_cloudflare))
857}
858
859#[allow(clippy::type_complexity)]
867fn phase_group(
868 parsed: &[ParsedFile],
869 src_root: &Path,
870 platform: Platform,
871 consumes_bynk: bool,
872 consumes_cloudflare: bool,
873 errors: &mut ErrorSink,
874) -> (
875 HashMap<String, Vec<usize>>,
876 HashMap<String, UnitKind>,
877 HashMap<String, Vec<usize>>,
878 HashMap<String, Vec<usize>>,
879 HashMap<String, AdapterBinding>,
880 std::collections::BTreeMap<String, String>,
881) {
882 let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
886 let mut kinds: HashMap<String, UnitKind> = HashMap::new();
887 let mut test_groups: HashMap<String, Vec<usize>> = HashMap::new();
888 let mut integration_groups: HashMap<String, Vec<usize>> = HashMap::new();
891 for (i, pf) in parsed.iter().enumerate() {
892 let name = pf.unit.name().joined();
893 if pf.kind == UnitKind::Integration {
894 integration_groups.entry(name).or_default().push(i);
895 } else if pf.kind == UnitKind::Test {
896 test_groups.entry(name).or_default().push(i);
897 } else {
898 groups.entry(name.clone()).or_default().push(i);
899 kinds.entry(name).or_insert(pf.kind);
900 }
901 }
902 if let Err(e) = check_directory_name_consistency(parsed) {
903 errors.extend_for(None, e);
904 }
905 if let Err(e) = check_directory_kind_consistency(parsed) {
906 errors.extend_for(None, e);
907 }
908 if let Err(e) = check_group_kind_consistency(parsed, &groups) {
911 errors.extend_for(None, e);
912 }
913 if let Err(e) = check_path_name_alignment(parsed) {
917 errors.extend_for(None, e);
918 }
919
920 let mut fn_boundary_errors: Vec<CompileError> = Vec::new();
922 check_function_type_boundaries(parsed, &mut fn_boundary_errors);
923 errors.extend_for(None, fn_boundary_errors);
924
925 for pf in parsed {
928 if pf.synthetic {
929 continue;
930 }
931 let qn = pf.unit.name();
932 if qn.parts.first().is_some_and(|p| p.name == "bynk") {
933 errors.push_for(None,
934 CompileError::new(
935 "bynk.namespace.reserved",
936 qn.span,
937 format!(
938 "`{}` uses the reserved `bynk` namespace — the `bynk` root is reserved for the toolchain's conformance surface",
939 qn.joined()
940 ),
941 )
942 .with_note("rename the unit so its first segment is not `bynk`"),
943 );
944 }
945 }
946
947 for pf in parsed {
951 if pf.synthetic {
952 continue;
953 }
954 if let Some(a) = pf.adapter() {
955 let has_external = a
956 .items
957 .iter()
958 .any(|it| matches!(it, CommonsItem::Provider(p) if p.external));
959 if has_external && a.binding.is_none() {
960 errors.push_for(None,
961 CompileError::new(
962 "bynk.adapter.no_binding",
963 a.span,
964 format!(
965 "adapter `{}` declares an external provider but has no `binding` clause to supply its implementation",
966 a.name.joined()
967 ),
968 )
969 .with_note(
970 "add a `binding \"<module>\"` clause naming the TypeScript module that exports the provider symbols",
971 ),
972 );
973 }
974 }
975 }
976
977 let mut adapter_bindings: HashMap<String, AdapterBinding> = HashMap::new();
981 if consumes_bynk {
983 adapter_bindings.insert(
984 firstparty::BYNK_UNIT.to_string(),
985 AdapterBinding {
986 output_path: PathBuf::from(platform.bynk_binding_filename()),
987 content: platform.bynk_binding_source().to_string(),
988 },
989 );
990 }
991 if consumes_cloudflare {
994 adapter_bindings.insert(
995 firstparty::CLOUDFLARE_UNIT.to_string(),
996 AdapterBinding {
997 output_path: PathBuf::from(firstparty::CLOUDFLARE_BINDING_FILENAME),
998 content: firstparty::cloudflare_binding_source().to_string(),
999 },
1000 );
1001 }
1002 for pf in parsed {
1003 let Some(a) = pf.adapter() else { continue };
1004 let Some(b) = &a.binding else { continue };
1005 let adapter_dir = pf.source_path.parent().unwrap_or(Path::new(""));
1006 let out_rel = normalize_rel(&adapter_dir.join(&b.module));
1007 let src_abs = src_root.join(&out_rel);
1008 match fs::read_to_string(&src_abs) {
1009 Ok(content) => {
1010 adapter_bindings.insert(
1011 a.name.joined(),
1012 AdapterBinding {
1013 output_path: out_rel,
1014 content,
1015 },
1016 );
1017 }
1018 Err(e) => {
1019 errors.push_for(None,
1020 CompileError::new(
1021 "bynk.adapter.no_binding",
1022 b.module_span,
1023 format!(
1024 "adapter `{}` names binding module `{}`, which could not be read ({e})",
1025 a.name.joined(),
1026 b.module
1027 ),
1028 )
1029 .with_note(
1030 "the binding path is resolved relative to the adapter's source file; author the `.binding.ts` there",
1031 ),
1032 );
1033 }
1034 }
1035 }
1036
1037 let mut npm_deps: std::collections::BTreeMap<String, String> =
1040 std::collections::BTreeMap::new();
1041 for pf in parsed {
1042 let Some(a) = pf.adapter() else { continue };
1043 let Some(b) = &a.binding else { continue };
1044 for dep in &b.requires {
1045 if is_unpinned_range(&dep.range) {
1046 errors.push_for(None,
1047 CompileError::new(
1048 "bynk.requires.unpinned_dependency",
1049 dep.span,
1050 format!(
1051 "dependency `{}` has an unpinned version range `{}` — pin a concrete range (e.g. `^1.2.0`)",
1052 dep.package, dep.range
1053 ),
1054 )
1055 .with_note(
1056 "unpinned ranges (`*`, `latest`, …) make builds irreproducible and are rejected",
1057 ),
1058 );
1059 continue;
1060 }
1061 npm_deps.insert(dep.package.clone(), dep.range.clone());
1062 }
1063 }
1064
1065 (
1066 groups,
1067 kinds,
1068 test_groups,
1069 integration_groups,
1070 adapter_bindings,
1071 npm_deps,
1072 )
1073}
1074
1075fn phase_symbol_tables(
1078 groups: &HashMap<String, Vec<usize>>,
1079 kinds: &HashMap<String, UnitKind>,
1080 parsed: &[ParsedFile],
1081 errors: &mut ErrorSink,
1082) -> HashMap<String, UnitTable> {
1083 let mut unit_tables: HashMap<String, UnitTable> = HashMap::new();
1084 for (name, indices) in groups {
1085 let kind = *kinds.get(name).expect("every group has a kind");
1086 let mut table_errors: Vec<CompileError> = Vec::new();
1087 let table = build_unit_table(name, kind, indices, parsed, &mut table_errors);
1088 errors.extend_for(None, table_errors);
1089 unit_tables.insert(name.clone(), table);
1090 }
1091 unit_tables
1092}
1093
1094fn phase_resolve_uses(
1098 groups: &HashMap<String, Vec<usize>>,
1099 kinds: &HashMap<String, UnitKind>,
1100 parsed: &[ParsedFile],
1101 unit_tables: &HashMap<String, UnitTable>,
1102 errors: &mut ErrorSink,
1103) -> HashMap<String, Vec<String>> {
1104 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
1105 for (name, indices) in groups {
1106 let mut uses_targets: Vec<String> = Vec::new();
1107 for &i in indices {
1108 for u in parsed[i].uses() {
1109 let target = u.target.joined();
1110 if !unit_tables.contains_key(&target) {
1111 errors.push_for(
1112 None,
1113 CompileError::new(
1114 "bynk.uses.unknown_commons",
1115 u.span,
1116 format!("unknown commons `{target}`"),
1117 )
1118 .with_note(
1119 "the target of a `uses` clause must be a commons in the project",
1120 ),
1121 );
1122 continue;
1123 }
1124 let target_kind = *kinds.get(&target).unwrap();
1125 if target_kind != UnitKind::Commons {
1126 errors.push_for(None,
1127 CompileError::new(
1128 "bynk.uses.target_is_context",
1129 u.span,
1130 format!(
1131 "`uses {target}` targets a context — `uses` may only target a commons"
1132 ),
1133 )
1134 .with_note(
1135 "to declare a dependency on a context, use `consumes` instead",
1136 ),
1137 );
1138 continue;
1139 }
1140 if target == *name {
1141 errors.push_for(
1142 None,
1143 CompileError::new(
1144 "bynk.uses.self_reference",
1145 u.span,
1146 format!("`{name}` cannot `uses` itself"),
1147 ),
1148 );
1149 continue;
1150 }
1151 if !uses_targets.contains(&target) {
1152 uses_targets.push(target);
1153 }
1154 }
1155 }
1156 unit_uses.insert(name.clone(), uses_targets);
1157 }
1158 unit_uses
1159}
1160
1161#[allow(clippy::type_complexity)]
1167fn phase_resolve_consumes(
1168 groups: &HashMap<String, Vec<usize>>,
1169 kinds: &HashMap<String, UnitKind>,
1170 parsed: &[ParsedFile],
1171 unit_tables: &HashMap<String, UnitTable>,
1172 errors: &mut ErrorSink,
1173 refs: &mut RefSink,
1174) -> (
1175 HashMap<String, Vec<String>>,
1176 HashMap<String, HashMap<String, String>>,
1177) {
1178 let mut unit_consumes: HashMap<String, Vec<String>> = HashMap::new();
1179 let mut unit_flattened: HashMap<String, HashMap<String, String>> = HashMap::new();
1182 for (name, indices) in groups {
1183 let kind = *kinds.get(name).unwrap();
1184 let mut consumes_targets: Vec<String> = Vec::new();
1185 let mut flattened: HashMap<String, String> = HashMap::new();
1186 let local_caps: HashSet<String> = unit_tables
1187 .get(name)
1188 .map(|t| t.capabilities.keys().cloned().collect())
1189 .unwrap_or_default();
1190 for &i in indices {
1191 refs.enter_file(&parsed[i].source_path, name, parsed[i].synthetic);
1192 for c in parsed[i].consumes() {
1193 let target = c.target.joined();
1194 if kind != UnitKind::Context && kind != UnitKind::Adapter {
1195 errors.push_for(None,
1196 CompileError::new(
1197 "bynk.consumes.in_commons",
1198 c.span,
1199 format!(
1200 "`consumes` is only valid inside a context or adapter, not a commons `{name}`",
1201 ),
1202 )
1203 .with_note(
1204 "commons declare vocabulary; only contexts and adapters can declare behavioural dependencies",
1205 ),
1206 );
1207 continue;
1208 }
1209 if kind == UnitKind::Adapter && c.selected.is_none() {
1213 errors.push_for(None,
1214 CompileError::new(
1215 "bynk.adapter.consumes_requires_selection",
1216 c.span,
1217 format!(
1218 "an adapter's `consumes` must select capabilities — write `consumes {target} {{ Cap, … }}`",
1219 ),
1220 )
1221 .with_note(
1222 "adapters depend on capabilities, never on services; the whole-unit and aliased forms are context-only",
1223 ),
1224 );
1225 continue;
1226 }
1227 if !unit_tables.contains_key(&target) {
1228 errors.push_for(
1229 None,
1230 CompileError::new(
1231 "bynk.consumes.unknown_context",
1232 c.span,
1233 format!("unknown context `{target}`"),
1234 )
1235 .with_note(
1236 "the target of a `consumes` clause must be a context in the project",
1237 ),
1238 );
1239 continue;
1240 }
1241 let target_kind = *kinds.get(&target).unwrap();
1242 if target_kind != UnitKind::Context && target_kind != UnitKind::Adapter {
1245 errors.push_for(None,
1246 CompileError::new(
1247 "bynk.consumes.target_is_commons",
1248 c.span,
1249 format!(
1250 "`consumes {target}` targets a commons — `consumes` may only target a context or adapter"
1251 ),
1252 )
1253 .with_note(
1254 "to mix in declarations from a commons, use `uses` instead",
1255 ),
1256 );
1257 continue;
1258 }
1259 if kind == UnitKind::Adapter && target_kind == UnitKind::Context {
1263 errors.push_for(None,
1264 CompileError::new(
1265 "bynk.adapter.consumes_context",
1266 c.span,
1267 format!(
1268 "adapter `{name}` cannot `consumes` the context `{target}` — adapter dependencies are adapter-to-adapter"
1269 ),
1270 )
1271 .with_note(
1272 "an adapter may only depend on capabilities exported by other adapters (e.g. the `bynk` surface)",
1273 ),
1274 );
1275 continue;
1276 }
1277 if target == *name {
1278 let kind_word = if kind == UnitKind::Adapter {
1279 "adapter"
1280 } else {
1281 "context"
1282 };
1283 errors.push_for(
1284 None,
1285 CompileError::new(
1286 "bynk.consumes.self_reference",
1287 c.span,
1288 format!("{kind_word} `{name}` cannot `consumes` itself"),
1289 ),
1290 );
1291 continue;
1292 }
1293 if let Some(names) = &c.selected {
1297 let exported = unit_tables
1298 .get(&target)
1299 .map(|t| &t.exported_capabilities)
1300 .cloned()
1301 .unwrap_or_default();
1302 for cap in names {
1303 if !exported.contains(&cap.name) {
1304 errors.push_for(
1305 None,
1306 CompileError::new(
1307 "bynk.given.cross_context_unknown_capability",
1308 cap.span,
1309 format!(
1310 "`{target}` does not export a capability named `{}`",
1311 cap.name
1312 ),
1313 ),
1314 );
1315 continue;
1316 }
1317 if local_caps.contains(&cap.name) {
1318 errors.push_for(None, CompileError::new(
1319 "bynk.consumes.capability_name_clash",
1320 cap.span,
1321 format!(
1322 "flattened capability `{}` clashes with a capability declared locally — use qualified `given {target}.{}` instead",
1323 cap.name, cap.name
1324 ),
1325 ));
1326 continue;
1327 }
1328 if let Some(prev) = flattened.get(&cap.name) {
1329 errors.push_for(None, CompileError::new(
1330 "bynk.consumes.capability_name_clash",
1331 cap.span,
1332 format!(
1333 "capability `{}` is flattened from both `{prev}` and `{target}` — qualify one with `given U.{}`",
1334 cap.name, cap.name
1335 ),
1336 ));
1337 continue;
1338 }
1339 refs.record_in_unit(cap.span, SymbolKind::Capability, &cap.name, &target);
1342 flattened.insert(cap.name.clone(), target.clone());
1343 }
1344 }
1345 if !consumes_targets.contains(&target) {
1346 consumes_targets.push(target);
1347 }
1348 }
1349 }
1350 unit_consumes.insert(name.clone(), consumes_targets);
1351 unit_flattened.insert(name.clone(), flattened);
1352 }
1353 (unit_consumes, unit_flattened)
1354}
1355
1356fn phase_consumes_aliases(
1361 groups: &HashMap<String, Vec<usize>>,
1362 kinds: &HashMap<String, UnitKind>,
1363 parsed: &[ParsedFile],
1364 unit_tables: &HashMap<String, UnitTable>,
1365 errors: &mut ErrorSink,
1366) -> HashMap<String, HashMap<String, String>> {
1367 let mut unit_consumes_aliases: HashMap<String, HashMap<String, String>> = HashMap::new();
1368 for (name, indices) in groups {
1369 let kind = *kinds.get(name).unwrap();
1370 if kind != UnitKind::Context {
1371 continue;
1372 }
1373 let mut aliases: HashMap<String, String> = HashMap::new();
1374 let mut alias_spans: HashMap<String, Span> = HashMap::new();
1375 for &i in indices {
1376 for c in parsed[i].consumes() {
1377 let Some(alias) = &c.alias else { continue };
1378 let target = c.target.joined();
1379 if !unit_tables.contains_key(&target) {
1380 continue;
1382 }
1383 if let Some(prev_span) = alias_spans.get(&alias.name) {
1384 errors.push_for(None,
1385 CompileError::new(
1386 "bynk.consumes.alias_conflict",
1387 alias.span,
1388 format!(
1389 "alias `{}` is used by more than one `consumes` clause in context `{}`",
1390 alias.name, name
1391 ),
1392 )
1393 .with_label(*prev_span, "previously defined here")
1394 .with_note(
1395 "each `consumes` clause may introduce at most one alias, and aliases must be unique within a context",
1396 ),
1397 );
1398 continue;
1399 }
1400 aliases.insert(alias.name.clone(), target);
1401 alias_spans.insert(alias.name.clone(), alias.span);
1402 }
1403 }
1404 unit_consumes_aliases.insert(name.clone(), aliases);
1405 }
1406
1407 for (name, aliases) in &unit_consumes_aliases {
1410 let Some(local) = unit_tables.get(name) else {
1411 continue;
1412 };
1413 for alias in aliases.keys() {
1414 let alias_span = parsed_alias_span(parsed, &groups[name], alias).unwrap_or_default();
1415 let conflict_kind = if local.types.contains_key(alias) {
1416 Some("type")
1417 } else if local.fns.contains_key(alias) {
1418 Some("function")
1419 } else if local.capabilities.contains_key(alias) {
1420 Some("capability")
1421 } else if local.services.contains_key(alias) {
1422 Some("service")
1423 } else if local.agents.contains_key(alias) {
1424 Some("agent")
1425 } else {
1426 None
1427 };
1428 if let Some(kind) = conflict_kind {
1429 errors.push_for(None,
1430 CompileError::new(
1431 "bynk.consumes.alias_conflict",
1432 alias_span,
1433 format!(
1434 "alias `{alias}` conflicts with a local {kind} of the same name in context `{name}`",
1435 ),
1436 )
1437 .with_note(
1438 "pick a different alias for the `consumes` clause, or rename the local declaration",
1439 ),
1440 );
1441 }
1442 }
1443 }
1444 unit_consumes_aliases
1445}
1446
1447fn phase_uses_name_conflicts(
1451 unit_uses: &HashMap<String, Vec<String>>,
1452 unit_tables: &HashMap<String, UnitTable>,
1453 parsed: &[ParsedFile],
1454 groups: &HashMap<String, Vec<usize>>,
1455 errors: &mut ErrorSink,
1456) {
1457 for (name, targets) in unit_uses {
1458 let local = unit_tables.get(name).expect("unit table present");
1459 let mut imported: HashMap<String, String> = HashMap::new();
1460 for t in targets {
1461 let used = unit_tables.get(t).expect("used unit table present");
1462 for type_name in used.types.keys() {
1463 if local.types.contains_key(type_name) || local.fns.contains_key(type_name) {
1464 continue;
1465 }
1466 if let Some(prev) = imported.get(type_name) {
1467 let span = uses_span_of(parsed, &groups[name], t).unwrap_or_default();
1468 errors.push_for(None,
1469 CompileError::new(
1470 "bynk.uses.name_conflict",
1471 span,
1472 format!(
1473 "`{name}` uses two commons that both declare `{type_name}`: `{prev}` and `{t}`",
1474 ),
1475 )
1476 .with_note(
1477 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
1478 ),
1479 );
1480 } else {
1481 imported.insert(type_name.clone(), t.clone());
1482 }
1483 }
1484 for fn_name in used.fns.keys() {
1485 if local.types.contains_key(fn_name) || local.fns.contains_key(fn_name) {
1486 continue;
1487 }
1488 if let Some(prev) = imported.get(fn_name) {
1489 let span = uses_span_of(parsed, &groups[name], t).unwrap_or_default();
1490 errors.push_for(None,
1491 CompileError::new(
1492 "bynk.uses.name_conflict",
1493 span,
1494 format!(
1495 "`{name}` uses two commons that both declare `{fn_name}`: `{prev}` and `{t}`",
1496 ),
1497 )
1498 .with_note(
1499 "name conflicts at the use site are not yet renamable; remove or restructure one of the imports",
1500 ),
1501 );
1502 } else {
1503 imported.insert(fn_name.clone(), t.clone());
1504 }
1505 }
1506 }
1507 }
1508}
1509
1510fn phase_validate_type_exports(
1516 groups: &HashMap<String, Vec<usize>>,
1517 kinds: &HashMap<String, UnitKind>,
1518 parsed: &[ParsedFile],
1519 unit_tables: &HashMap<String, UnitTable>,
1520 errors: &mut ErrorSink,
1521 refs: &mut RefSink,
1522) -> HashMap<String, HashMap<String, Visibility>> {
1523 let mut exports_visibility: HashMap<String, HashMap<String, Visibility>> = HashMap::new();
1524 for (name, indices) in groups {
1525 let kind = *kinds.get(name).unwrap();
1526 if kind != UnitKind::Context && kind != UnitKind::Adapter {
1527 continue;
1530 }
1531 let local = unit_tables.get(name).unwrap();
1532 let mut seen: HashMap<String, (Visibility, Span)> = HashMap::new();
1533 for &i in indices {
1534 refs.enter_file(&parsed[i].source_path, name, parsed[i].synthetic);
1535 for clause in parsed[i].exports() {
1536 let ExportKind::Type(clause_vis) = clause.kind else {
1539 continue;
1540 };
1541 let mut within: HashMap<String, Span> = HashMap::new();
1542 for n in &clause.names {
1543 if let Some(prev) = within.get(&n.name) {
1544 errors.push_for(
1545 None,
1546 CompileError::new(
1547 "bynk.exports.duplicate_in_clause",
1548 n.span,
1549 format!(
1550 "type `{}` appears more than once in this exports clause",
1551 n.name
1552 ),
1553 )
1554 .with_label(*prev, "previously listed here"),
1555 );
1556 continue;
1557 }
1558 within.insert(n.name.clone(), n.span);
1559
1560 if !local.types.contains_key(&n.name) {
1561 errors.push_for(None,
1562 CompileError::new(
1563 "bynk.exports.undeclared_type",
1564 n.span,
1565 format!(
1566 "exports clause references `{}`, which is not a type declared in context `{}`",
1567 n.name, name
1568 ),
1569 )
1570 .with_note(
1571 "only types declared in the same context can appear in `exports` clauses",
1572 ),
1573 );
1574 continue;
1575 }
1576 refs.record(n.span, SymbolKind::Type, &n.name);
1578
1579 if let Some((prev_vis, prev_span)) = seen.get(&n.name) {
1580 if *prev_vis == clause_vis {
1581 errors.push_for(
1582 None,
1583 CompileError::new(
1584 "bynk.exports.duplicate_export",
1585 n.span,
1586 format!("type `{}` is exported more than once", n.name),
1587 )
1588 .with_label(*prev_span, "previously exported here"),
1589 );
1590 } else {
1591 errors.push_for(None,
1592 CompileError::new(
1593 "bynk.exports.conflicting_visibility",
1594 n.span,
1595 format!(
1596 "type `{}` is exported with conflicting visibilities — pick `opaque` or `transparent`",
1597 n.name,
1598 ),
1599 )
1600 .with_label(*prev_span, "previously exported here"),
1601 );
1602 }
1603 continue;
1604 }
1605 seen.insert(n.name.clone(), (clause_vis, n.span));
1606 }
1607 }
1608 }
1609 let mut visibility_map: HashMap<String, Visibility> = HashMap::new();
1610 for (n, (v, _)) in seen {
1611 visibility_map.insert(n, v);
1612 }
1613 exports_visibility.insert(name.clone(), visibility_map);
1614 }
1615 exports_visibility
1616}
1617
1618fn phase_validate_capability_exports(
1623 groups: &HashMap<String, Vec<usize>>,
1624 kinds: &HashMap<String, UnitKind>,
1625 parsed: &[ParsedFile],
1626 unit_tables: &HashMap<String, UnitTable>,
1627 errors: &mut ErrorSink,
1628 refs: &mut RefSink,
1629) {
1630 for (name, indices) in groups {
1631 if kinds.get(name) != Some(&UnitKind::Context)
1632 && kinds.get(name) != Some(&UnitKind::Adapter)
1633 {
1634 continue;
1635 }
1636 let local = unit_tables.get(name).unwrap();
1637 let mut seen: HashMap<String, Span> = HashMap::new();
1638 for &i in indices {
1639 refs.enter_file(&parsed[i].source_path, name, parsed[i].synthetic);
1640 for clause in parsed[i].exports() {
1641 if !matches!(clause.kind, ExportKind::Capability) {
1642 continue;
1643 }
1644 for n in &clause.names {
1645 if let Some(prev) = seen.get(&n.name) {
1646 errors.push_for(
1647 None,
1648 CompileError::new(
1649 "bynk.exports.duplicate_export",
1650 n.span,
1651 format!("capability `{}` is exported more than once", n.name),
1652 )
1653 .with_label(*prev, "previously exported here"),
1654 );
1655 continue;
1656 }
1657 seen.insert(n.name.clone(), n.span);
1658 if local.capabilities.contains_key(&n.name) {
1659 refs.record(n.span, SymbolKind::Capability, &n.name);
1662 }
1663 if !local.capabilities.contains_key(&n.name) {
1664 errors.push_for(None,
1665 CompileError::new(
1666 "bynk.exports.undeclared_capability",
1667 n.span,
1668 format!(
1669 "`exports capability` references `{}`, which is not a capability declared in context `{}`",
1670 n.name, name
1671 ),
1672 )
1673 .with_note(
1674 "only capabilities declared in the same context can appear in `exports capability` clauses",
1675 ),
1676 );
1677 continue;
1678 }
1679 if !local.providers.contains_key(&n.name) {
1680 errors.push_for(None,
1681 CompileError::new(
1682 "bynk.exports.capability_not_provided",
1683 n.span,
1684 format!(
1685 "exported capability `{}` has no provider in context `{}` — a consumer cannot instantiate it",
1686 n.name, name
1687 ),
1688 )
1689 .with_note(
1690 "add a `provides {n} = …` declaration so the capability can be wired into consumers",
1691 ),
1692 );
1693 }
1694 }
1695 }
1696 }
1697 }
1698}
1699
1700fn phase_validate_providers(unit_tables: &HashMap<String, UnitTable>, errors: &mut ErrorSink) {
1705 for (name, table) in unit_tables {
1706 let _ = name;
1707 for (cap_name, provider) in &table.providers {
1708 if provider.external {
1711 continue;
1712 }
1713 let Some(cap) = table.capabilities.get(cap_name) else {
1714 errors.push_for(None,
1715 CompileError::new(
1716 "bynk.provider.unknown_capability",
1717 provider.capability.span,
1718 format!(
1719 "provider targets unknown capability `{}` — declare the capability in the same context",
1720 cap_name
1721 ),
1722 ),
1723 );
1724 continue;
1725 };
1726 for cap_op in &cap.ops {
1728 if !provider.ops.iter().any(|o| o.name.name == cap_op.name.name) {
1729 errors.push_for(
1730 None,
1731 CompileError::new(
1732 "bynk.provider.missing_operation",
1733 provider.span,
1734 format!(
1735 "provider `{}` for capability `{}` is missing operation `{}`",
1736 provider.provider_name.name, cap_name, cap_op.name.name
1737 ),
1738 ),
1739 );
1740 }
1741 }
1742 for prov_op in &provider.ops {
1745 let Some(cap_op) = cap.ops.iter().find(|o| o.name.name == prov_op.name.name) else {
1746 errors.push_for(None, CompileError::new(
1747 "bynk.provider.extra_operation",
1748 prov_op.span,
1749 format!(
1750 "provider operation `{}.{}` does not match any operation in capability `{}`",
1751 provider.provider_name.name, prov_op.name.name, cap_name
1752 ),
1753 ));
1754 continue;
1755 };
1756 if cap_op.params.len() != prov_op.params.len() {
1757 errors.push_for(None, CompileError::new(
1758 "bynk.provider.signature_mismatch",
1759 prov_op.span,
1760 format!(
1761 "provider operation `{}.{}` has {} parameter(s), but capability operation expects {}",
1762 provider.provider_name.name,
1763 prov_op.name.name,
1764 prov_op.params.len(),
1765 cap_op.params.len()
1766 ),
1767 ));
1768 continue;
1769 }
1770 for (i, (cap_p, prov_p)) in
1771 cap_op.params.iter().zip(prov_op.params.iter()).enumerate()
1772 {
1773 if !type_refs_match(&cap_p.type_ref, &prov_p.type_ref) {
1774 errors.push_for(None, CompileError::new(
1775 "bynk.provider.signature_mismatch",
1776 prov_p.span,
1777 format!(
1778 "provider operation `{}.{}` parameter {} has type `{}`, but capability declares `{}`",
1779 provider.provider_name.name,
1780 prov_op.name.name,
1781 i + 1,
1782 ts_type_ref_display(&prov_p.type_ref),
1783 ts_type_ref_display(&cap_p.type_ref)
1784 ),
1785 ));
1786 }
1787 }
1788 if !type_refs_match(&cap_op.return_type, &prov_op.return_type) {
1789 errors.push_for(None, CompileError::new(
1790 "bynk.provider.signature_mismatch",
1791 prov_op.return_type.span(),
1792 format!(
1793 "provider operation `{}.{}` returns `{}`, but capability declares `{}`",
1794 provider.provider_name.name,
1795 prov_op.name.name,
1796 ts_type_ref_display(&prov_op.return_type),
1797 ts_type_ref_display(&cap_op.return_type)
1798 ),
1799 ));
1800 }
1801 }
1802 }
1803 }
1804}
1805
1806fn phase_file_index(
1809 groups: &HashMap<String, Vec<usize>>,
1810 parsed: &[ParsedFile],
1811) -> HashMap<String, FileDeclIndex> {
1812 let mut unit_file_index: HashMap<String, FileDeclIndex> = HashMap::new();
1813 for (name, indices) in groups {
1814 unit_file_index.insert(name.clone(), build_file_decl_index(indices, parsed));
1815 }
1816 unit_file_index
1817}
1818
1819struct UnitInfo {
1827 kind: UnitKind,
1828 table: UnitTable,
1829 uses: Vec<String>,
1830 consumes: Vec<String>,
1831 flattened: HashMap<String, String>,
1832 aliases: HashMap<String, String>,
1833 exports: HashMap<String, Visibility>,
1834 file_index: FileDeclIndex,
1835 files: Vec<usize>,
1836}
1837
1838#[allow(clippy::too_many_arguments)]
1845fn assemble_unit_info(
1846 groups: &HashMap<String, Vec<usize>>,
1847 kinds: &HashMap<String, UnitKind>,
1848 unit_tables: &HashMap<String, UnitTable>,
1849 unit_uses: &HashMap<String, Vec<String>>,
1850 unit_consumes: &HashMap<String, Vec<String>>,
1851 unit_flattened: &HashMap<String, HashMap<String, String>>,
1852 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1853 exports_visibility: &HashMap<String, HashMap<String, Visibility>>,
1854 unit_file_index: &HashMap<String, FileDeclIndex>,
1855) -> HashMap<String, UnitInfo> {
1856 groups
1857 .iter()
1858 .map(|(name, indices)| {
1859 let info = UnitInfo {
1860 kind: *kinds.get(name).unwrap(),
1861 table: unit_tables.get(name).unwrap().clone(),
1862 uses: unit_uses.get(name).cloned().unwrap_or_default(),
1863 consumes: unit_consumes.get(name).cloned().unwrap_or_default(),
1864 flattened: unit_flattened.get(name).cloned().unwrap_or_default(),
1865 aliases: unit_consumes_aliases.get(name).cloned().unwrap_or_default(),
1866 exports: exports_visibility.get(name).cloned().unwrap_or_default(),
1867 file_index: unit_file_index
1868 .get(name)
1869 .cloned()
1870 .unwrap_or_else(|| FileDeclIndex {
1871 types: HashMap::new(),
1872 fns: HashMap::new(),
1873 methods: HashMap::new(),
1874 }),
1875 files: indices.clone(),
1876 };
1877 (name.clone(), info)
1878 })
1879 .collect()
1880}
1881
1882fn collect_unit_methods(indices: &[usize], parsed: &[ParsedFile]) -> HashMap<String, Vec<FnDecl>> {
1887 let mut local_methods_for_type: HashMap<String, Vec<FnDecl>> = HashMap::new();
1888 for &j in indices {
1889 for item in parsed[j].items() {
1890 if let CommonsItem::Fn(f) = item
1891 && let FnName::Method { type_name, .. } = &f.name
1892 {
1893 local_methods_for_type
1894 .entry(type_name.name.clone())
1895 .or_default()
1896 .push(f.clone());
1897 }
1898 }
1899 }
1900 local_methods_for_type
1901}
1902
1903#[allow(clippy::too_many_arguments)]
1909fn merge_consumed_exports(
1910 name: &str,
1911 parsed: &[ParsedFile],
1912 unit_info: &HashMap<String, UnitInfo>,
1913 combined_types: &mut HashMap<String, TypeDecl>,
1914 combined_methods: &mut HashMap<String, ResolverMethodTable>,
1915 imported_from: &mut HashMap<String, String>,
1916 imported_from_kind: &mut HashMap<String, UnitKind>,
1917 errors: &mut ErrorSink,
1918) -> HashMap<String, ConsumedType> {
1919 let mut consumed_types: HashMap<String, ConsumedType> = HashMap::new();
1925
1926 for t in unit_info.get(name).into_iter().flat_map(|i| &i.consumes) {
1930 let used = &unit_info.get(t).expect("consumed unit present").table;
1931 let used_exports = &unit_info[t].exports;
1932 for (type_name, vis) in used_exports {
1933 let Some(decl) = used.types.get(type_name) else {
1934 continue;
1935 };
1936 if combined_types.contains_key(type_name) {
1937 let consumes_span =
1939 consumes_span_of(parsed, &unit_info[name].files, t).unwrap_or_default();
1940 errors.push_for(None,
1941 CompileError::new(
1942 "bynk.consumes.name_conflict",
1943 consumes_span,
1944 format!(
1945 "context `{name}` consumes `{t}` which exports type `{type_name}`, but a type of the same name is already in scope",
1946 ),
1947 )
1948 .with_note(
1949 "rename one of the conflicting declarations or restructure the import",
1950 ),
1951 );
1952 continue;
1953 }
1954 combined_types.insert(type_name.clone(), decl.clone());
1955 imported_from.insert(type_name.clone(), t.clone());
1956 imported_from_kind.insert(type_name.clone(), UnitKind::Context);
1957 consumed_types.insert(
1958 type_name.clone(),
1959 ConsumedType {
1960 owning_context: t.clone(),
1961 visibility: *vis,
1962 },
1963 );
1964 if let Some(mt) = used.methods.get(type_name) {
1972 let entry = combined_methods.entry(type_name.clone()).or_default();
1973 for (m, decl) in &mt.instance {
1974 entry
1975 .instance
1976 .entry(m.clone())
1977 .or_insert_with(|| decl.clone());
1978 }
1979 }
1983 }
1984 }
1985
1986 consumed_types
1987}
1988
1989#[allow(clippy::type_complexity)]
1994fn compose_unit_symbols(
1995 name: &str,
1996 local_table: &UnitTable,
1997 unit_info: &HashMap<String, UnitInfo>,
1998) -> (
1999 HashMap<String, TypeDecl>,
2000 HashMap<String, FnDecl>,
2001 HashMap<String, ResolverMethodTable>,
2002 HashMap<String, String>,
2003 HashMap<String, UnitKind>,
2004) {
2005 let mut combined_types = local_table.types.clone();
2010 let mut combined_fns = local_table.fns.clone();
2011 let mut combined_methods = local_table.methods.clone();
2012 let mut imported_from: HashMap<String, String> = HashMap::new();
2013 let mut imported_from_kind: HashMap<String, UnitKind> = HashMap::new();
2014
2015 for t in unit_info.get(name).into_iter().flat_map(|i| &i.uses) {
2016 let used = &unit_info.get(t).expect("used unit present").table;
2017 for (type_name, decl) in &used.types {
2018 if !combined_types.contains_key(type_name) {
2019 combined_types.insert(type_name.clone(), decl.clone());
2020 imported_from.insert(type_name.clone(), t.clone());
2021 imported_from_kind.insert(type_name.clone(), UnitKind::Commons);
2022 }
2023 }
2024 for (fn_name, decl) in &used.fns {
2025 if !combined_fns.contains_key(fn_name) {
2026 combined_fns.insert(fn_name.clone(), decl.clone());
2027 imported_from.insert(fn_name.clone(), t.clone());
2028 imported_from_kind.insert(fn_name.clone(), UnitKind::Commons);
2029 }
2030 }
2031 for (type_name, mt) in &used.methods {
2032 let entry = combined_methods.entry(type_name.clone()).or_default();
2033 for (m, decl) in &mt.instance {
2034 entry
2035 .instance
2036 .entry(m.clone())
2037 .or_insert_with(|| decl.clone());
2038 }
2039 for (m, decl) in &mt.statics {
2040 entry
2041 .statics
2042 .entry(m.clone())
2043 .or_insert_with(|| decl.clone());
2044 }
2045 }
2046 }
2047
2048 (
2049 combined_types,
2050 combined_fns,
2051 combined_methods,
2052 imported_from,
2053 imported_from_kind,
2054 )
2055}
2056
2057#[allow(clippy::too_many_arguments)]
2062fn emit_unit(
2063 name: &str,
2064 kind: UnitKind,
2065 i: usize,
2066 pf: &ParsedFile,
2067 indices: &[usize],
2068 parsed: &[ParsedFile],
2069 unit_info: &HashMap<String, UnitInfo>,
2070 imported_from: &HashMap<String, String>,
2071 imported_from_kind: &HashMap<String, UnitKind>,
2072 owning_context_for_emit: &Option<String>,
2073 consumed_types: &HashMap<String, ConsumedType>,
2074 cross_context_for_file: &resolver::CrossContextInfo,
2075 typed: &checker::TypedCommons,
2076 target: BuildTarget,
2077 import_ext: ImportExt,
2078 compiled: &mut Vec<CompiledFile>,
2079) {
2080 let info = &unit_info[name];
2082 let mut imported_decl_paths: HashMap<String, HashMap<String, PathBuf>> = HashMap::new();
2083 for t in &info.uses {
2084 if let Some(target_info) = unit_info.get(t) {
2085 let target_index = &target_info.file_index;
2086 let mut paths: HashMap<String, PathBuf> = HashMap::new();
2087 for (n, p) in &target_index.types {
2088 paths.insert(n.clone(), p.clone());
2089 }
2090 for (n, p) in &target_index.fns {
2091 paths.insert(n.clone(), p.clone());
2092 }
2093 imported_decl_paths.insert(t.clone(), paths);
2094 }
2095 }
2096 for t in &info.consumes {
2097 if let Some(target_info) = unit_info.get(t) {
2098 let target_index = &target_info.file_index;
2099 let mut paths: HashMap<String, PathBuf> = HashMap::new();
2100 let exports_for_target = &target_info.exports;
2103 for n in exports_for_target.keys() {
2104 if let Some(p) = target_index.types.get(n) {
2105 paths.insert(n.clone(), p.clone());
2106 }
2107 }
2108 imported_decl_paths.insert(t.clone(), paths);
2109 }
2110 }
2111
2112 let exports_local = info.exports.clone();
2113 let exports_for_consumed = info
2114 .consumes
2115 .iter()
2116 .map(|t| {
2117 (
2118 t.clone(),
2119 unit_info
2120 .get(t)
2121 .map(|i| i.exports.clone())
2122 .unwrap_or_default(),
2123 )
2124 })
2125 .collect();
2126 let cross_context_info = cross_context_for_file.clone();
2127
2128 let workers_mode = matches!(target, BuildTarget::Workers);
2133 let emit_source_path = if workers_mode && kind == UnitKind::Context {
2134 worker_handlers_source_path(name)
2135 } else {
2136 pf.source_path.clone()
2137 };
2138 let emit_local_files = if workers_mode && kind == UnitKind::Context {
2139 Vec::new()
2142 } else {
2143 indices
2144 .iter()
2145 .filter_map(|&j| {
2146 if j == i {
2147 None
2148 } else {
2149 Some(parsed[j].source_path.clone())
2150 }
2151 })
2152 .collect()
2153 };
2154
2155 let mut imported_decl_paths_emit = imported_decl_paths.clone();
2158 if workers_mode {
2159 for (unit, decls) in imported_decl_paths.iter() {
2160 let target_kind = unit_info.get(unit).map(|i| i.kind);
2161 if target_kind == Some(UnitKind::Context) {
2162 let handlers_path = worker_handlers_source_path(unit);
2163 let mut rewritten = HashMap::new();
2164 for n in decls.keys() {
2165 rewritten.insert(n.clone(), handlers_path.clone());
2166 }
2167 imported_decl_paths_emit.insert(unit.clone(), rewritten);
2168 }
2169 }
2170 }
2171
2172 let boundary_type_owners = if workers_mode && kind == UnitKind::Context {
2176 compute_boundary_type_owners(name, unit_info, parsed)
2177 } else {
2178 HashMap::new()
2179 };
2180
2181 let emit_ctx = EmitProjectCtx {
2182 source_path: emit_source_path,
2183 commons_name: name.to_string(),
2184 local_files: emit_local_files,
2185 file_decl_index: info.file_index.clone(),
2186 imported_from: imported_from.clone(),
2187 imported_from_kind: imported_from_kind.clone(),
2188 imported_decl_paths: imported_decl_paths_emit,
2189 commons_dir: commons_dir_for(name),
2190 unit_kind: kind,
2191 owning_context: owning_context_for_emit.clone(),
2192 exports_local,
2193 exports_for_consumed,
2194 consumed_types: consumed_types.clone(),
2195 cross_context: cross_context_info,
2196 is_consumed_by_others: unit_info
2197 .values()
2198 .any(|i| i.consumes.iter().any(|t| t == name)),
2199 target,
2200 boundary_type_owners,
2201 local_agents: info.table.agents.keys().cloned().collect(),
2202 actors: info.table.actors.clone(),
2206 consumed_adapters: info
2207 .consumes
2208 .iter()
2209 .filter(|t| unit_info.get(*t).map(|i| i.kind) == Some(UnitKind::Adapter))
2210 .cloned()
2211 .collect(),
2212 import_ext,
2213 };
2214 let source_name = pf.map_source_name();
2219 let (ts, source_map) = emitter::emit_project(typed, &emit_ctx, &pf.source, &source_name);
2220 let debug_metadata = emitter::collect_handler_labels(typed);
2223 let output_path = if workers_mode && kind == UnitKind::Context {
2224 worker_handlers_output_path(name)
2225 } else {
2226 ts_output_path(&pf.source_path)
2227 };
2228 compiled.push(CompiledFile {
2229 source_path: pf.source_path.clone(),
2230 output_path,
2231 typescript: ts,
2232 source_map,
2233 debug_metadata,
2234 });
2235}
2236
2237#[allow(clippy::too_many_arguments)]
2241#[allow(clippy::type_complexity)]
2242fn check_unit_files(
2243 name: &str,
2244 kind: UnitKind,
2245 indices: &[usize],
2246 parsed: &[ParsedFile],
2247 unit_info: &HashMap<String, UnitInfo>,
2248 combined_types: &HashMap<String, TypeDecl>,
2249 combined_fns: &HashMap<String, FnDecl>,
2250 combined_methods: &HashMap<String, ResolverMethodTable>,
2251 local_names: &HashSet<String>,
2252 local_methods_for_type: &HashMap<String, Vec<FnDecl>>,
2253 consumed_types: &HashMap<String, ConsumedType>,
2254 imported_from: &HashMap<String, String>,
2255 imported_from_kind: &HashMap<String, UnitKind>,
2256 owning_context_for_emit: &Option<String>,
2257 target: BuildTarget,
2258 import_ext: ImportExt,
2259 mode: Mode,
2260 errors: &mut ErrorSink,
2261 refs: &mut RefSink,
2262 hints: &mut HintSink,
2263 locals: &mut LocalsSink,
2264 exprs: &mut ExprTypeSink,
2265 requirements: &mut RequirementSink,
2266 compiled: &mut Vec<CompiledFile>,
2267) {
2268 let unit_tables: HashMap<String, UnitTable> = unit_info
2274 .iter()
2275 .map(|(n, i)| (n.clone(), i.table.clone()))
2276 .collect();
2277 let unit_uses: HashMap<String, Vec<String>> = unit_info
2278 .iter()
2279 .map(|(n, i)| (n.clone(), i.uses.clone()))
2280 .collect();
2281 let unit_consumes: HashMap<String, Vec<String>> = unit_info
2282 .iter()
2283 .map(|(n, i)| (n.clone(), i.consumes.clone()))
2284 .collect();
2285 let unit_consumes_aliases: HashMap<String, HashMap<String, String>> = unit_info
2286 .iter()
2287 .map(|(n, i)| (n.clone(), i.aliases.clone()))
2288 .collect();
2289
2290 for &i in indices {
2291 let pf = &parsed[i];
2292
2293 let mut emit_items: Vec<CommonsItem> = Vec::new();
2294 let types_in_this_file: HashSet<String> = pf
2295 .items()
2296 .iter()
2297 .filter_map(|it| match it {
2298 CommonsItem::Type(t) => Some(t.name.name.clone()),
2299 _ => None,
2300 })
2301 .collect();
2302 for item in pf.items() {
2303 match item {
2304 CommonsItem::Type(t) => {
2305 emit_items.push(CommonsItem::Type(t.clone()));
2306 }
2307 CommonsItem::Fn(f) => match &f.name {
2308 FnName::Free(_) => emit_items.push(CommonsItem::Fn(f.clone())),
2309 FnName::Method { type_name, .. } => {
2310 if types_in_this_file.contains(&type_name.name) {
2311 emit_items.push(CommonsItem::Fn(f.clone()));
2312 }
2313 }
2314 },
2315 CommonsItem::Capability(c) => {
2316 emit_items.push(CommonsItem::Capability(c.clone()));
2317 }
2318 CommonsItem::Provider(p) => {
2319 emit_items.push(CommonsItem::Provider(p.clone()));
2320 }
2321 CommonsItem::Service(s) => {
2322 emit_items.push(CommonsItem::Service(s.clone()));
2323 }
2324 CommonsItem::Agent(a) => {
2325 emit_items.push(CommonsItem::Agent(a.clone()));
2326 }
2327 CommonsItem::Actor(a) => {
2328 emit_items.push(CommonsItem::Actor(a.clone()));
2331 }
2332 }
2333 }
2334 for type_name in &types_in_this_file {
2335 if let Some(methods) = local_methods_for_type.get(type_name) {
2336 for m in methods {
2337 let already = emit_items.iter().any(|it| match it {
2338 CommonsItem::Fn(existing) => match &existing.name {
2339 FnName::Method {
2340 type_name: t,
2341 method_name: n,
2342 } => match &m.name {
2343 FnName::Method {
2344 type_name: t2,
2345 method_name: n2,
2346 } => t.name == t2.name && n.name == n2.name,
2347 _ => false,
2348 },
2349 _ => false,
2350 },
2351 _ => false,
2352 });
2353 if !already {
2354 emit_items.push(CommonsItem::Fn(m.clone()));
2355 }
2356 }
2357 }
2358 }
2359
2360 let synthetic_commons = pf.as_synthetic_commons(emit_items);
2363
2364 let cross_context_for_file = if kind == UnitKind::Context || kind == UnitKind::Adapter {
2370 let mut cci = build_cross_context_info(
2371 name,
2372 &unit_consumes,
2373 &unit_consumes_aliases,
2374 &unit_uses,
2375 &unit_tables,
2376 );
2377 cci.flattened_caps = unit_info[name].flattened.clone();
2378 cci
2379 } else {
2380 resolver::CrossContextInfo::default()
2381 };
2382
2383 let resolved = ResolvedCommons {
2384 commons: synthetic_commons,
2385 types: combined_types.clone(),
2386 fns: combined_fns.clone(),
2387 methods: combined_methods.clone(),
2388 local_type_names: local_names.clone(),
2389 cross_context: cross_context_for_file.clone(),
2390 agents: HashMap::new(),
2391 imported_from: imported_from.clone(),
2393 };
2394 refs.enter_file(&pf.source_path, name, pf.synthetic);
2395 hints.enter_file(
2398 &pf.source_path,
2399 pf.synthetic || matches!(pf.kind, UnitKind::Test | UnitKind::Integration),
2400 );
2401 locals.enter_file(&pf.source_path, pf.synthetic);
2404 requirements.enter_file(
2407 &pf.source_path,
2408 pf.synthetic || matches!(pf.kind, UnitKind::Test | UnitKind::Integration),
2409 );
2410 if let Err(errs) = resolver::resolve_file_record(&resolved, refs) {
2411 errors.extend_for(Some(&pf.source_path), errs);
2412 continue;
2413 }
2414 let rc = checker::check_record(resolved, refs, hints, locals, requirements);
2415 let typed = match rc.result {
2416 Ok(t) => {
2417 if !t.warnings.is_empty() {
2421 errors.extend_for(Some(&pf.source_path), t.warnings.clone());
2422 }
2423 t
2424 }
2425 Err(errs) => {
2426 errors.extend_for(Some(&pf.source_path), errs);
2427 if mode == Mode::Analyse {
2432 record_analyse_types(
2433 exprs,
2434 &pf.source_path,
2435 pf.synthetic,
2436 &rc.partial_expr_types,
2437 );
2438 }
2439 continue;
2440 }
2441 };
2442
2443 if kind == UnitKind::Context {
2446 let context_check_errs = check_context_constraints(&typed, consumed_types, local_names);
2447 if !context_check_errs.is_empty() {
2448 errors.extend_for(Some(&pf.source_path), context_check_errs);
2449 if mode == Mode::Analyse {
2450 record_analyse_types(exprs, &pf.source_path, pf.synthetic, &typed.expr_types);
2451 }
2452 continue;
2453 }
2454 }
2455
2456 let mut typed = typed;
2461 let unit_table_owned = unit_info.get(name).map(|i| i.table.clone());
2462 if (kind == UnitKind::Context || kind == UnitKind::Adapter)
2463 && let Some(table) = unit_table_owned.as_ref()
2464 {
2465 let decl_errs = check_context_declarations(
2466 &mut typed,
2467 table,
2468 &cross_context_for_file,
2469 refs,
2470 hints,
2471 locals,
2472 requirements,
2473 );
2474 if !decl_errs.is_empty() {
2475 let blocks_emission = decl_errs.iter().any(|e| {
2479 matches!(
2480 bynk_syntax::Severity::for_error(e),
2481 bynk_syntax::Severity::Error
2482 )
2483 });
2484 errors.extend_for(Some(&pf.source_path), decl_errs);
2485 if blocks_emission {
2486 if mode == Mode::Analyse {
2490 record_analyse_types(
2491 exprs,
2492 &pf.source_path,
2493 pf.synthetic,
2494 &typed.expr_types,
2495 );
2496 }
2497 continue;
2498 }
2499 }
2501 }
2502
2503 if mode == Mode::Analyse {
2507 record_analyse_types(exprs, &pf.source_path, pf.synthetic, &typed.expr_types);
2508 continue;
2509 }
2510 emit_unit(
2511 name,
2512 kind,
2513 i,
2514 pf,
2515 indices,
2516 parsed,
2517 unit_info,
2518 imported_from,
2519 imported_from_kind,
2520 owning_context_for_emit,
2521 consumed_types,
2522 &cross_context_for_file,
2523 &typed,
2524 target,
2525 import_ext,
2526 compiled,
2527 );
2528 }
2529}
2530
2531#[allow(clippy::large_enum_variant)]
2536enum RunChecks {
2537 Bailed {
2540 errors: ErrorSink,
2541 snapshots: Vec<(PathBuf, String)>,
2542 hints: HintSink,
2543 locals: LocalsSink,
2544 exprs: ExprTypeSink,
2545 requirements: RequirementSink,
2546 },
2547 Checked {
2549 errors: ErrorSink,
2550 snapshots: Vec<(PathBuf, String)>,
2551 refs: RefSink,
2552 hints: HintSink,
2553 locals: LocalsSink,
2554 exprs: ExprTypeSink,
2555 requirements: RequirementSink,
2556 parsed: Vec<ParsedFile>,
2557 compiled: Vec<CompiledFile>,
2558 runnable_tests: Vec<RunnableTest>,
2559 integration_outputs: Vec<CompiledFile>,
2560 integration_runnables: Vec<RunnableTest>,
2561 groups: HashMap<String, Vec<usize>>,
2562 kinds: HashMap<String, UnitKind>,
2563 unit_uses: HashMap<String, Vec<String>>,
2564 unit_consumes: HashMap<String, Vec<String>>,
2565 unit_consumes_aliases: HashMap<String, HashMap<String, String>>,
2566 unit_tables: HashMap<String, UnitTable>,
2567 unit_flattened: HashMap<String, HashMap<String, String>>,
2568 adapter_bindings: HashMap<String, AdapterBinding>,
2569 npm_deps: std::collections::BTreeMap<String, String>,
2570 target: BuildTarget,
2571 },
2572}
2573
2574#[allow(clippy::too_many_arguments)]
2575fn run_checks(
2576 src_root: &Path,
2577 tests_root: &Path,
2578 tests_prefix: &Path,
2579 target: BuildTarget,
2580 platform: Platform,
2581 import_ext: ImportExt,
2582 mode: Mode,
2583 overlay: &HashMap<PathBuf, String>,
2584 excludes: &[PathBuf],
2587 discovered: Option<(Vec<PathBuf>, Vec<PathBuf>)>,
2593) -> RunChecks {
2594 let mut errors = ErrorSink::new();
2595 let mut refs = RefSink::new();
2598 let mut hints = HintSink::new();
2602 let mut locals = LocalsSink::new();
2603 let mut requirements = RequirementSink::new();
2606 let mut exprs = ExprTypeSink::new();
2609 let mut snapshots: Vec<(PathBuf, String)> = Vec::new();
2610 let split_mode = src_root != tests_root;
2611
2612 let (src_files, tests_files) = match discovered {
2614 Some(files) => files,
2615 None => match phase_discovery(src_root, tests_root, split_mode, excludes, &mut errors) {
2616 Ok(files) => files,
2617 Err(()) => {
2618 return RunChecks::Bailed {
2619 errors,
2620 snapshots,
2621 hints,
2622 locals,
2623 exprs,
2624 requirements,
2625 };
2626 }
2627 },
2628 };
2629
2630 let (parsed, consumes_bynk, consumes_cloudflare) = match phase_parse(
2632 src_root,
2633 tests_root,
2634 split_mode,
2635 &src_files,
2636 &tests_files,
2637 overlay,
2638 &mut errors,
2639 &mut snapshots,
2640 ) {
2641 Ok(out) => out,
2642 Err(()) => {
2643 return RunChecks::Bailed {
2644 errors,
2645 snapshots,
2646 hints,
2647 locals,
2648 exprs,
2649 requirements,
2650 };
2651 }
2652 };
2653
2654 let (groups, kinds, test_groups, integration_groups, adapter_bindings, npm_deps) = phase_group(
2656 &parsed,
2657 src_root,
2658 platform,
2659 consumes_bynk,
2660 consumes_cloudflare,
2661 &mut errors,
2662 );
2663
2664 let unit_tables = phase_symbol_tables(&groups, &kinds, &parsed, &mut errors);
2666
2667 let unit_uses = phase_resolve_uses(&groups, &kinds, &parsed, &unit_tables, &mut errors);
2669
2670 let (unit_consumes, unit_flattened) = phase_resolve_consumes(
2672 &groups,
2673 &kinds,
2674 &parsed,
2675 &unit_tables,
2676 &mut errors,
2677 &mut refs,
2678 );
2679
2680 let unit_consumes_aliases =
2685 phase_consumes_aliases(&groups, &kinds, &parsed, &unit_tables, &mut errors);
2686
2687 let mut cycle_errors: Vec<CompileError> = Vec::new();
2689 detect_consumes_cycles(&unit_consumes, &mut cycle_errors);
2690 errors.extend_for(None, cycle_errors);
2691
2692 phase_uses_name_conflicts(&unit_uses, &unit_tables, &parsed, &groups, &mut errors);
2694
2695 let exports_visibility = phase_validate_type_exports(
2698 &groups,
2699 &kinds,
2700 &parsed,
2701 &unit_tables,
2702 &mut errors,
2703 &mut refs,
2704 );
2705
2706 phase_validate_capability_exports(
2709 &groups,
2710 &kinds,
2711 &parsed,
2712 &unit_tables,
2713 &mut errors,
2714 &mut refs,
2715 );
2716
2717 phase_validate_providers(&unit_tables, &mut errors);
2719
2720 if !errors.is_empty() && mode == Mode::Build {
2721 return RunChecks::Bailed {
2722 errors,
2723 snapshots,
2724 hints,
2725 locals,
2726 exprs,
2727 requirements,
2728 };
2729 }
2730
2731 let unit_file_index = phase_file_index(&groups, &parsed);
2733
2734 let unit_info = assemble_unit_info(
2741 &groups,
2742 &kinds,
2743 &unit_tables,
2744 &unit_uses,
2745 &unit_consumes,
2746 &unit_flattened,
2747 &unit_consumes_aliases,
2748 &exports_visibility,
2749 &unit_file_index,
2750 );
2751
2752 let mut compiled: Vec<CompiledFile> = Vec::new();
2755
2756 for (name, info) in &unit_info {
2757 let kind = info.kind;
2758 let indices = info.files.as_slice();
2759 let local_table = &info.table;
2760 let group_error_baseline = errors.len();
2766
2767 let (
2768 mut combined_types,
2769 combined_fns,
2770 mut combined_methods,
2771 mut imported_from,
2772 mut imported_from_kind,
2773 ) = compose_unit_symbols(name, local_table, &unit_info);
2774 let consumed_types = merge_consumed_exports(
2775 name,
2776 &parsed,
2777 &unit_info,
2778 &mut combined_types,
2779 &mut combined_methods,
2780 &mut imported_from,
2781 &mut imported_from_kind,
2782 &mut errors,
2783 );
2784
2785 if errors.len() > group_error_baseline {
2786 continue;
2787 }
2788
2789 let local_names: HashSet<String> = local_table.types.keys().cloned().collect();
2790
2791 let local_methods_for_type = collect_unit_methods(indices, &parsed);
2792
2793 let owning_context_for_emit = if kind == UnitKind::Context {
2795 Some(name.clone())
2796 } else {
2797 None
2798 };
2799
2800 check_unit_files(
2801 name,
2802 kind,
2803 indices,
2804 &parsed,
2805 &unit_info,
2806 &combined_types,
2807 &combined_fns,
2808 &combined_methods,
2809 &local_names,
2810 &local_methods_for_type,
2811 &consumed_types,
2812 &imported_from,
2813 &imported_from_kind,
2814 &owning_context_for_emit,
2815 target,
2816 import_ext,
2817 mode,
2818 &mut errors,
2819 &mut refs,
2820 &mut hints,
2821 &mut locals,
2822 &mut exprs,
2823 &mut requirements,
2824 &mut compiled,
2825 );
2826 }
2827
2828 let mut test_errors: Vec<CompileError> = Vec::new();
2833 let (test_outputs, runnable_tests) = process_tests(
2834 &test_groups,
2835 &parsed,
2836 &kinds,
2837 &unit_tables,
2838 &exports_visibility,
2839 &unit_consumes,
2840 &unit_consumes_aliases,
2841 &unit_uses,
2842 tests_prefix,
2843 import_ext,
2844 &mut test_errors,
2845 &mut refs,
2846 );
2847 errors.extend_for(None, test_errors);
2848
2849 compiled.extend(test_outputs);
2850
2851 let mut integration_errors: Vec<CompileError> = Vec::new();
2857 let (integration_outputs, integration_runnables) = process_integration_tests(
2858 &integration_groups,
2859 &parsed,
2860 &kinds,
2861 &unit_tables,
2862 &unit_consumes,
2863 &unit_consumes_aliases,
2864 &unit_uses,
2865 tests_prefix,
2866 &mut integration_errors,
2867 &mut refs,
2868 );
2869 errors.extend_for(None, integration_errors);
2870
2871 if errors.is_empty() {
2877 let mut lock_errors: Vec<CompileError> = Vec::new();
2878 check_platform_lock(
2879 target,
2880 platform,
2881 &parsed,
2882 &groups,
2883 &kinds,
2884 &unit_tables,
2885 &unit_consumes,
2886 &unit_consumes_aliases,
2887 &unit_flattened,
2888 &mut lock_errors,
2889 );
2890 errors.extend_for(None, lock_errors);
2891 }
2892
2893 if target == BuildTarget::Workers {
2898 let mut bytes_boundary_errors: Vec<CompileError> = Vec::new();
2899 check_bytes_workers_boundaries(&parsed, &mut bytes_boundary_errors);
2900 errors.extend_for(None, bytes_boundary_errors);
2901 }
2902
2903 RunChecks::Checked {
2904 errors,
2905 snapshots,
2906 refs,
2907 hints,
2908 locals,
2909 exprs,
2910 requirements,
2911 parsed,
2912 compiled,
2913 runnable_tests,
2914 integration_outputs,
2915 integration_runnables,
2916 groups,
2917 kinds,
2918 unit_uses,
2919 unit_consumes,
2920 unit_consumes_aliases,
2921 unit_tables,
2922 unit_flattened,
2923 adapter_bindings,
2924 npm_deps,
2925 target,
2926 }
2927}
2928
2929#[allow(clippy::too_many_arguments)]
2934fn build_output(
2935 mut compiled: Vec<CompiledFile>,
2936 mut runnable_tests: Vec<RunnableTest>,
2937 integration_outputs: Vec<CompiledFile>,
2938 integration_runnables: Vec<RunnableTest>,
2939 groups: HashMap<String, Vec<usize>>,
2940 kinds: HashMap<String, UnitKind>,
2941 unit_consumes: HashMap<String, Vec<String>>,
2942 unit_consumes_aliases: HashMap<String, HashMap<String, String>>,
2943 unit_tables: HashMap<String, UnitTable>,
2944 unit_flattened: HashMap<String, HashMap<String, String>>,
2945 adapter_bindings: HashMap<String, AdapterBinding>,
2946 npm_deps: std::collections::BTreeMap<String, String>,
2947 target: BuildTarget,
2948 import_ext: ImportExt,
2949) -> ProjectOutput {
2950 compiled.extend(integration_outputs);
2951 runnable_tests.extend(integration_runnables);
2952
2953 let discovered = discovery_manifest(&runnable_tests);
2957
2958 if !runnable_tests.is_empty() {
2961 let main_ts = emit_test_main(&runnable_tests, import_ext);
2962 compiled.push(CompiledFile {
2963 source_path: PathBuf::from("tests/main.test.bynk"),
2964 output_path: PathBuf::from("tests/main.ts"),
2965 typescript: main_ts,
2966 source_map: None,
2967 debug_metadata: None,
2968 });
2969 }
2970
2971 let context_native: HashMap<String, std::collections::BTreeMap<Platform, String>> = kinds
2975 .iter()
2976 .filter(|(_, k)| **k == UnitKind::Context)
2977 .filter_map(|(name, _)| {
2978 let table = unit_tables.get(name)?;
2979 let native = native_platforms_of_context(
2980 name,
2981 table,
2982 &unit_tables,
2983 &unit_consumes,
2984 &unit_consumes_aliases,
2985 &unit_flattened,
2986 );
2987 (!native.is_empty()).then(|| (name.clone(), native))
2988 })
2989 .collect();
2990
2991 match target {
2992 BuildTarget::Bundle => {
2993 if let Some(compose_ts) = emit_composition_root(
2999 &groups,
3000 &kinds,
3001 &unit_consumes,
3002 &unit_consumes_aliases,
3003 &unit_tables,
3004 &adapter_bindings,
3005 &unit_flattened,
3006 !context_native.is_empty(),
3010 ) {
3011 compiled.push(CompiledFile {
3012 source_path: PathBuf::from("compose.bynk"),
3013 output_path: PathBuf::from("compose.ts"),
3014 typescript: compose_ts,
3015 source_map: None,
3016 debug_metadata: None,
3017 });
3018 }
3019 }
3020 BuildTarget::Workers => {
3021 for (ctx_name, kind) in &kinds {
3024 if *kind != UnitKind::Context {
3025 continue;
3026 }
3027 let Some(table) = unit_tables.get(ctx_name) else {
3028 continue;
3029 };
3030 let dashes = worker_dir_name(ctx_name);
3031 let consumes_targets = unit_consumes.get(ctx_name).cloned().unwrap_or_default();
3032 let aliases = unit_consumes_aliases
3033 .get(ctx_name)
3034 .cloned()
3035 .unwrap_or_default();
3036 let entry_ts = emitter::emit_worker_entry(ctx_name, table);
3037 let binding_modules: HashMap<String, String> = adapter_bindings
3038 .iter()
3039 .map(|(n, b)| {
3040 (
3041 n.clone(),
3042 emitter::ts_specifier(&b.output_path.with_extension("js")),
3043 )
3044 })
3045 .collect();
3046 let flattened = unit_flattened.get(ctx_name).cloned().unwrap_or_default();
3047 let needs_kv = context_native
3050 .get(ctx_name)
3051 .is_some_and(|n| n.values().any(|u| u == firstparty::CLOUDFLARE_UNIT));
3052 let compose_ts = emitter::emit_worker_compose(
3053 ctx_name,
3054 table,
3055 &consumes_targets,
3056 &aliases,
3057 &unit_tables,
3058 &binding_modules,
3059 &flattened,
3060 &unit_consumes,
3061 &unit_consumes_aliases,
3062 &unit_flattened,
3063 needs_kv,
3064 );
3065 let service_consumes: Vec<String> = consumes_targets
3068 .iter()
3069 .filter(|t| !binding_modules.contains_key(*t))
3070 .cloned()
3071 .collect();
3072 let wrangler =
3073 emitter::emit_wrangler_toml(ctx_name, table, &service_consumes, needs_kv);
3074 compiled.push(CompiledFile {
3075 source_path: PathBuf::from(format!("workers/{dashes}/<index>")),
3076 output_path: PathBuf::from(format!("workers/{dashes}/index.ts")),
3077 typescript: entry_ts,
3078 source_map: None,
3079 debug_metadata: None,
3080 });
3081 compiled.push(CompiledFile {
3082 source_path: PathBuf::from(format!("workers/{dashes}/<compose>")),
3083 output_path: PathBuf::from(format!("workers/{dashes}/compose.ts")),
3084 typescript: compose_ts,
3085 source_map: None,
3086 debug_metadata: None,
3087 });
3088 compiled.push(CompiledFile {
3089 source_path: PathBuf::from(format!("workers/{dashes}/<wrangler>")),
3090 output_path: PathBuf::from(format!("workers/{dashes}/wrangler.toml")),
3091 typescript: wrangler,
3092 source_map: None,
3093 debug_metadata: None,
3094 });
3095 }
3096 }
3097 }
3098
3099 let mut binding_names: Vec<&String> = adapter_bindings.keys().collect();
3103 binding_names.sort();
3104 for name in binding_names {
3105 let b = &adapter_bindings[name];
3106 compiled.push(CompiledFile {
3107 source_path: b.output_path.clone(),
3108 output_path: b.output_path.clone(),
3109 typescript: b.content.clone(),
3110 source_map: None,
3111 debug_metadata: None,
3112 });
3113 }
3114
3115 if !npm_deps.is_empty() {
3118 compiled.push(CompiledFile {
3119 source_path: PathBuf::from("<package.json>"),
3120 output_path: PathBuf::from("package.json"),
3121 typescript: render_package_json(&npm_deps),
3122 source_map: None,
3123 debug_metadata: None,
3124 });
3125 }
3126
3127 compiled.push(CompiledFile {
3132 source_path: PathBuf::from("<runtime>"),
3133 output_path: PathBuf::from("runtime.ts"),
3134 typescript: emitter::emit_runtime_module(),
3135 source_map: None,
3136 debug_metadata: None,
3137 });
3138 compiled.push(CompiledFile {
3139 source_path: PathBuf::from("<tsconfig>"),
3140 output_path: PathBuf::from("tsconfig.json"),
3141 typescript: emitter::emit_tsconfig(),
3142 source_map: None,
3143 debug_metadata: None,
3144 });
3145
3146 compiled.sort_by(|a, b| a.source_path.cmp(&b.source_path));
3147 ProjectOutput {
3148 files: compiled,
3149 discovered,
3150 warnings: Vec::new(),
3152 }
3153}
3154
3155fn resolve_consume_prefix(
3161 prefix: &str,
3162 consumed: &[String],
3163 aliases: &HashMap<String, String>,
3164) -> Option<String> {
3165 if let Some(q) = aliases.get(prefix) {
3166 return Some(q.clone());
3167 }
3168 if consumed.iter().any(|c| c == prefix) {
3169 return Some(prefix.to_string());
3170 }
3171 None
3172}
3173
3174fn handler_cross_caps(
3177 table: &UnitTable,
3178 consumed: &[String],
3179 aliases: &HashMap<String, String>,
3180 flattened: &HashMap<String, String>,
3181) -> std::collections::BTreeMap<String, String> {
3182 let mut out = std::collections::BTreeMap::new();
3183 let mut scan = |given: &[CapRef]| {
3184 for c in given {
3185 if let Some(p) = c.prefix() {
3186 if let Some(ctx) = resolve_consume_prefix(&p, consumed, aliases) {
3187 out.entry(c.key().to_string()).or_insert(ctx);
3188 }
3189 } else if let Some(unit) = flattened.get(c.key()) {
3190 out.entry(c.key().to_string())
3193 .or_insert_with(|| unit.clone());
3194 }
3195 }
3196 };
3197 for s in table.services.values() {
3198 for h in &s.handlers {
3199 scan(&h.given);
3200 }
3201 }
3202 for a in table.agents.values() {
3203 for h in &a.handlers {
3204 scan(&h.given);
3205 }
3206 }
3207 out
3208}
3209
3210#[allow(clippy::too_many_arguments)]
3218fn native_platforms_of_context(
3219 ctx: &str,
3220 table: &UnitTable,
3221 unit_tables: &HashMap<String, UnitTable>,
3222 unit_consumes: &HashMap<String, Vec<String>>,
3223 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3224 unit_flattened: &HashMap<String, HashMap<String, String>>,
3225) -> std::collections::BTreeMap<Platform, String> {
3226 let mut referenced: BTreeSet<String> = BTreeSet::new();
3227 for cap in table.providers.keys() {
3228 let _ = instantiate_provider_expr(
3229 ctx,
3230 cap,
3231 unit_tables,
3232 unit_consumes,
3233 unit_consumes_aliases,
3234 unit_flattened,
3235 false,
3236 None,
3237 &mut referenced,
3238 );
3239 }
3240 let consumed = unit_consumes.get(ctx).cloned().unwrap_or_default();
3241 let aliases = unit_consumes_aliases.get(ctx).cloned().unwrap_or_default();
3242 let flattened = unit_flattened.get(ctx).cloned().unwrap_or_default();
3243 for (key, cctx) in handler_cross_caps(table, &consumed, &aliases, &flattened) {
3244 let _ = instantiate_provider_expr(
3245 &cctx,
3246 &key,
3247 unit_tables,
3248 unit_consumes,
3249 unit_consumes_aliases,
3250 unit_flattened,
3251 false,
3252 None,
3253 &mut referenced,
3254 );
3255 }
3256 let mut out = std::collections::BTreeMap::new();
3257 for unit in referenced {
3258 if let Some(p) = bynk_check::firstparty::platform_of(&unit) {
3259 out.entry(p).or_insert(unit);
3260 }
3261 }
3262 out
3263}
3264
3265#[allow(clippy::too_many_arguments)]
3283pub(crate) fn instantiate_provider_expr(
3284 provider_ctx: &str,
3285 cap: &str,
3286 unit_tables: &HashMap<String, UnitTable>,
3287 unit_consumes: &HashMap<String, Vec<String>>,
3288 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3289 unit_flattened: &HashMap<String, HashMap<String, String>>,
3290 workers_ns: bool,
3291 env_ident: Option<&str>,
3292 referenced_units: &mut BTreeSet<String>,
3293) -> String {
3294 let ns = provider_ctx.replace('.', "_");
3295 let bodied_ns = if workers_ns {
3296 format!("handlers_{ns}")
3297 } else {
3298 ns.clone()
3299 };
3300 referenced_units.insert(provider_ctx.to_string());
3301 let Some(provider) = unit_tables
3302 .get(provider_ctx)
3303 .and_then(|t| t.providers.get(cap))
3304 else {
3305 return format!("new {bodied_ns}.{cap}()");
3306 };
3307 let deps_obj = if provider.given.is_empty() {
3309 None
3310 } else {
3311 let consumed = unit_consumes.get(provider_ctx).cloned().unwrap_or_default();
3312 let aliases = unit_consumes_aliases
3313 .get(provider_ctx)
3314 .cloned()
3315 .unwrap_or_default();
3316 let flattened = unit_flattened
3317 .get(provider_ctx)
3318 .cloned()
3319 .unwrap_or_default();
3320 let deps: Vec<String> = provider
3321 .given
3322 .iter()
3323 .map(|g| {
3324 let target_ctx = match g.prefix() {
3325 Some(p) => resolve_consume_prefix(&p, &consumed, &aliases)
3326 .unwrap_or_else(|| provider_ctx.to_string()),
3327 None => flattened
3328 .get(g.key())
3329 .cloned()
3330 .unwrap_or_else(|| provider_ctx.to_string()),
3331 };
3332 let expr = instantiate_provider_expr(
3333 &target_ctx,
3334 g.key(),
3335 unit_tables,
3336 unit_consumes,
3337 unit_consumes_aliases,
3338 unit_flattened,
3339 workers_ns,
3340 env_ident,
3341 referenced_units,
3342 );
3343 format!("{}: {}", g.key(), expr)
3344 })
3345 .collect();
3346 Some(format!("{{ {} }}", deps.join(", ")))
3347 };
3348 let mut args: Vec<String> = deps_obj.into_iter().collect();
3349 if provider.external
3353 && bynk_check::firstparty::provider_takes_env(provider_ctx, &provider.provider_name.name)
3354 && let Some(env) = env_ident
3355 {
3356 args.push(env.to_string());
3357 }
3358 let class = &provider.provider_name.name;
3359 let args = args.join(", ");
3360 if provider.external {
3364 format!("new {ns}__binding.{class}({args})")
3365 } else {
3366 format!("new {bodied_ns}.{class}({args})")
3367 }
3368}
3369
3370#[allow(clippy::too_many_arguments)]
3371fn emit_composition_root(
3372 groups: &HashMap<String, Vec<usize>>,
3373 kinds: &HashMap<String, UnitKind>,
3374 unit_consumes: &HashMap<String, Vec<String>>,
3375 unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
3376 unit_tables: &HashMap<String, UnitTable>,
3377 adapter_bindings: &HashMap<String, AdapterBinding>,
3378 unit_flattened: &HashMap<String, HashMap<String, String>>,
3379 thread_env: bool,
3385) -> Option<String> {
3386 let mut needs_compose = false;
3388 for (name, targets) in unit_consumes {
3389 if !targets.is_empty()
3390 && let Some(UnitKind::Context) = kinds.get(name)
3391 {
3392 for t in targets {
3393 if let Some(other) = unit_tables.get(t)
3394 && !other.services.is_empty()
3395 {
3396 needs_compose = true;
3397 }
3398 }
3399 }
3400 }
3401 if !needs_compose {
3405 for (name, kind) in kinds {
3406 if *kind != UnitKind::Context {
3407 continue;
3408 }
3409 let Some(table) = unit_tables.get(name) else {
3410 continue;
3411 };
3412 let consumed = unit_consumes.get(name).cloned().unwrap_or_default();
3413 let aliases = unit_consumes_aliases.get(name).cloned().unwrap_or_default();
3414 let flattened = unit_flattened.get(name).cloned().unwrap_or_default();
3415 if !handler_cross_caps(table, &consumed, &aliases, &flattened).is_empty()
3416 || table.providers.values().any(|p| {
3417 p.given.iter().any(|g| {
3418 g.is_cross_context()
3419 || (g.prefix().is_none() && flattened.contains_key(g.key()))
3423 })
3424 })
3425 {
3426 needs_compose = true;
3427 break;
3428 }
3429 }
3430 }
3431 if !needs_compose {
3432 return None;
3433 }
3434
3435 let mut contexts: Vec<&String> = groups
3436 .keys()
3437 .filter(|n| kinds.get(*n) == Some(&UnitKind::Context))
3438 .collect();
3439 contexts.sort();
3440
3441 let mut referenced_units: BTreeSet<String> = BTreeSet::new();
3446 let mut out = String::new();
3447
3448 let (compose_params, env_ident) = if thread_env {
3449 ("env?: unknown", Some("env"))
3450 } else {
3451 ("", None)
3452 };
3453 out.push_str(&format!(
3454 "export function composeApp({compose_params}) {{\n"
3455 ));
3456
3457 let mut ordered: Vec<String> = Vec::new();
3461 let mut visited: HashSet<String> = HashSet::new();
3462 fn visit(
3463 node: &str,
3464 unit_consumes: &HashMap<String, Vec<String>>,
3465 visited: &mut HashSet<String>,
3466 out: &mut Vec<String>,
3467 ) {
3468 if visited.contains(node) {
3469 return;
3470 }
3471 visited.insert(node.to_string());
3472 if let Some(targets) = unit_consumes.get(node) {
3473 for t in targets {
3474 visit(t, unit_consumes, visited, out);
3475 }
3476 }
3477 out.push(node.to_string());
3478 }
3479 for c in &contexts {
3480 visit(c, unit_consumes, &mut visited, &mut ordered);
3481 }
3482
3483 for ctx_name in &ordered {
3484 if kinds.get(ctx_name.as_str()) != Some(&UnitKind::Context) {
3485 continue;
3486 }
3487 let Some(table) = unit_tables.get(ctx_name.as_str()) else {
3488 continue;
3489 };
3490 if table.services.is_empty() {
3493 continue;
3494 }
3495 let ns = ctx_name.replace('.', "_");
3496
3497 let mut deps_entries: Vec<String> = table
3498 .providers
3499 .keys()
3500 .map(|cap| {
3501 format!(
3502 "{cap}: {}",
3503 instantiate_provider_expr(
3504 ctx_name,
3505 cap,
3506 unit_tables,
3507 unit_consumes,
3508 unit_consumes_aliases,
3509 unit_flattened,
3510 false,
3511 env_ident,
3512 &mut referenced_units,
3513 )
3514 )
3515 })
3516 .collect();
3517 {
3520 let consumed = unit_consumes
3521 .get(ctx_name.as_str())
3522 .cloned()
3523 .unwrap_or_default();
3524 let aliases = unit_consumes_aliases
3525 .get(ctx_name.as_str())
3526 .cloned()
3527 .unwrap_or_default();
3528 let flattened = unit_flattened
3529 .get(ctx_name.as_str())
3530 .cloned()
3531 .unwrap_or_default();
3532 for (key, cctx) in handler_cross_caps(table, &consumed, &aliases, &flattened) {
3533 deps_entries.push(format!(
3534 "{key}: {}",
3535 instantiate_provider_expr(
3536 &cctx,
3537 &key,
3538 unit_tables,
3539 unit_consumes,
3540 unit_consumes_aliases,
3541 unit_flattened,
3542 false,
3543 env_ident,
3544 &mut referenced_units,
3545 )
3546 ));
3547 }
3548 }
3549 deps_entries.sort();
3550
3551 let mut surface_entries: Vec<String> = Vec::new();
3552 if let Some(targets) = unit_consumes.get(ctx_name.as_str()) {
3553 let aliases = unit_consumes_aliases
3554 .get(ctx_name.as_str())
3555 .cloned()
3556 .unwrap_or_default();
3557 let mut alias_for: HashMap<String, String> = HashMap::new();
3558 for (alias, target) in &aliases {
3559 alias_for.insert(target.clone(), alias.clone());
3560 }
3561 let mut sorted_targets = targets.clone();
3562 sorted_targets.sort();
3563 for t in &sorted_targets {
3564 let Some(other) = unit_tables.get(t) else {
3565 continue;
3566 };
3567 if other.services.is_empty() {
3568 continue;
3569 }
3570 let surface_key = alias_for
3571 .get(t)
3572 .cloned()
3573 .unwrap_or_else(|| t.rsplit('.').next().unwrap_or(t.as_str()).to_string());
3574 surface_entries.push(format!("{surface_key}: {}Surface", t.replace('.', "_")));
3575 }
3576 }
3577 if !surface_entries.is_empty() {
3578 deps_entries.push(format!("surface: {{ {} }}", surface_entries.join(", ")));
3579 }
3580 out.push_str(&format!(
3581 " const {ns}Deps = {{ {} }};\n",
3582 deps_entries.join(", ")
3583 ));
3584 if !table.services.is_empty() {
3585 out.push_str(&format!(
3586 " const {ns}Surface = {ns}.makeSurface({ns}Deps);\n",
3587 ));
3588 }
3589 }
3590 out.push('\n');
3591
3592 out.push_str(" return {\n");
3594 for ctx_name in &contexts {
3595 let Some(table) = unit_tables.get(ctx_name.as_str()) else {
3596 continue;
3597 };
3598 if table.services.is_empty() {
3599 continue;
3600 }
3601 let ns = ctx_name.replace('.', "_");
3602 let key = ctx_name.rsplit('.').next().unwrap_or(ctx_name.as_str());
3603 out.push_str(&format!(" {key}: {ns}Surface,\n"));
3604 }
3605 out.push_str(" };\n");
3606 out.push_str("}\n");
3607
3608 let mut header = String::new();
3611 header.push_str("// Generated by bynkc — do not edit by hand.\n");
3612 header.push_str("// composition root\n\n");
3613
3614 for ctx_name in &contexts {
3616 let dir = emitter::ts_specifier(&commons_dir_for(ctx_name));
3617 let ns = ctx_name.replace('.', "_");
3618 header.push_str(&format!("import * as {ns} from \"./{dir}.js\";\n"));
3619 }
3620 let mut consumed_adapters: Vec<String> = unit_consumes
3626 .iter()
3627 .filter(|(name, _)| kinds.get(*name) == Some(&UnitKind::Context))
3628 .flat_map(|(_, targets)| targets.iter().cloned())
3629 .chain(referenced_units.iter().cloned())
3630 .filter(|t| adapter_bindings.contains_key(t))
3631 .collect();
3632 consumed_adapters.sort();
3633 consumed_adapters.dedup();
3634 for adapter in &consumed_adapters {
3635 let ns = adapter.replace('.', "_");
3636 let module =
3637 emitter::ts_specifier(&adapter_bindings[adapter].output_path.with_extension("js"));
3638 header.push_str(&format!("import * as {ns}__binding from \"./{module}\";\n"));
3639 }
3640 header.push('\n');
3641
3642 let out = format!("{header}{out}");
3643
3644 Some(out)
3645}
3646
3647fn compute_boundary_type_owners(
3654 consumer: &str,
3655 unit_info: &HashMap<String, UnitInfo>,
3656 parsed: &[ParsedFile],
3657) -> HashMap<String, BoundaryOwner> {
3658 let mut out: HashMap<String, BoundaryOwner> = HashMap::new();
3659 let Some(consumer_info) = unit_info.get(consumer) else {
3660 return out;
3661 };
3662 let _ = parsed;
3663 for t in &consumer_info.consumes {
3664 let Some(target_info) = unit_info.get(t) else {
3665 continue;
3666 };
3667 for type_name in target_info.table.types.keys() {
3670 out.insert(
3671 type_name.clone(),
3672 BoundaryOwner::Context { context: t.clone() },
3673 );
3674 }
3675 }
3678 let _ = &consumer_info.file_index;
3681 out
3682}
3683
3684pub struct EmitProjectCtx {
3687 pub source_path: PathBuf,
3689 pub commons_name: String,
3691 pub local_files: Vec<PathBuf>,
3693 pub file_decl_index: FileDeclIndex,
3695 pub imported_from: HashMap<String, String>,
3697 pub imported_from_kind: HashMap<String, UnitKind>,
3699 pub imported_decl_paths: HashMap<String, HashMap<String, PathBuf>>,
3701 pub commons_dir: PathBuf,
3703 pub unit_kind: UnitKind,
3705 pub owning_context: Option<String>,
3708 pub exports_local: HashMap<String, Visibility>,
3710 pub exports_for_consumed: HashMap<String, HashMap<String, Visibility>>,
3713 pub consumed_types: HashMap<String, ConsumedType>,
3716 pub cross_context: resolver::CrossContextInfo,
3720 pub is_consumed_by_others: bool,
3723 pub target: BuildTarget,
3726 pub boundary_type_owners: HashMap<String, BoundaryOwner>,
3731 pub local_agents: HashSet<String>,
3735 pub actors: HashMap<String, bynk_syntax::ast::ActorDecl>,
3739 pub consumed_adapters: HashSet<String>,
3743 pub import_ext: ImportExt,
3747}
3748
3749#[derive(Debug, Clone)]
3751pub enum BoundaryOwner {
3752 Commons { source_path: PathBuf },
3754 Context { context: String },
3756}
3757
3758impl EmitProjectCtx {
3759 pub fn commons_path(name: &str) -> PathBuf {
3760 commons_dir_for(name)
3761 }
3762}
3763
3764#[allow(dead_code)]
3765fn _ensure_components_used(_p: &Path) {
3766 let _ = Component::CurDir;
3767}
3768
3769#[cfg(test)]
3770mod tests {
3771 use super::*;
3772
3773 #[test]
3778 fn assemble_unit_info_yields_one_record_per_group_with_all_facets() {
3779 let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
3780 groups.insert("a.commons".to_string(), vec![0, 1]);
3781 groups.insert("a.context".to_string(), vec![2]);
3782
3783 let mut kinds: HashMap<String, UnitKind> = HashMap::new();
3784 kinds.insert("a.commons".to_string(), UnitKind::Commons);
3785 kinds.insert("a.context".to_string(), UnitKind::Context);
3786
3787 let mut unit_tables: HashMap<String, UnitTable> = HashMap::new();
3788 unit_tables.insert("a.commons".to_string(), UnitTable::default());
3789 unit_tables.insert("a.context".to_string(), UnitTable::default());
3790
3791 let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
3792 unit_uses.insert("a.context".to_string(), vec!["a.commons".to_string()]);
3793
3794 let mut unit_consumes: HashMap<String, Vec<String>> = HashMap::new();
3795 unit_consumes.insert("a.context".to_string(), vec![]);
3796
3797 let mut unit_flattened: HashMap<String, HashMap<String, String>> = HashMap::new();
3800 unit_flattened.insert("a.context".to_string(), HashMap::new());
3801 let unit_consumes_aliases: HashMap<String, HashMap<String, String>> = HashMap::new();
3802 let mut exports_visibility: HashMap<String, HashMap<String, Visibility>> = HashMap::new();
3803 exports_visibility.insert("a.context".to_string(), HashMap::new());
3804
3805 let mut unit_file_index: HashMap<String, FileDeclIndex> = HashMap::new();
3806 unit_file_index.insert(
3807 "a.commons".to_string(),
3808 FileDeclIndex {
3809 types: HashMap::new(),
3810 fns: HashMap::new(),
3811 methods: HashMap::new(),
3812 },
3813 );
3814 let info = assemble_unit_info(
3817 &groups,
3818 &kinds,
3819 &unit_tables,
3820 &unit_uses,
3821 &unit_consumes,
3822 &unit_flattened,
3823 &unit_consumes_aliases,
3824 &exports_visibility,
3825 &unit_file_index,
3826 );
3827
3828 assert_eq!(info.len(), 2);
3830 assert!(info.contains_key("a.commons"));
3831 assert!(info.contains_key("a.context"));
3832
3833 assert_eq!(info["a.commons"].files, vec![0, 1]);
3835 assert_eq!(info["a.context"].files, vec![2]);
3836
3837 assert_eq!(info["a.commons"].kind, UnitKind::Commons);
3839 assert_eq!(info["a.context"].kind, UnitKind::Context);
3840 assert_eq!(info["a.context"].uses, vec!["a.commons".to_string()]);
3841
3842 assert!(info["a.commons"].exports.is_empty());
3844 assert!(info["a.commons"].aliases.is_empty());
3845 assert!(info["a.commons"].flattened.is_empty());
3846 assert!(info["a.context"].file_index.types.is_empty());
3848 assert!(info["a.context"].file_index.fns.is_empty());
3849 assert!(info["a.context"].file_index.methods.is_empty());
3850 }
3851}