1use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use shape_ast::ast::FunctionDef;
12use shape_ast::module_utils::ModuleExportKind;
13use shape_ast::Program;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ModuleId(pub u32);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ModuleSourceKind {
30 ShapeSource,
32 NativeModule,
34 Hybrid,
36 CompiledBytecode,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ModuleExportVisibility {
47 Public,
49 ComptimeOnly,
51}
52
53#[derive(Debug, Clone)]
59pub struct ExportedSymbol {
60 pub kind: ModuleExportKind,
62 pub function_def: Option<Arc<FunctionDef>>,
64 pub visibility: ModuleExportVisibility,
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct ModuleInterface {
71 pub exports: HashMap<String, ExportedSymbol>,
73}
74
75#[derive(Debug, Clone)]
81pub struct NamedImportSymbol {
82 pub original_name: String,
84 pub local_name: String,
86 pub is_annotation: bool,
88 pub kind: ModuleExportKind,
90}
91
92#[derive(Debug, Clone)]
94pub enum ResolvedImport {
95 Namespace {
97 local_name: String,
99 canonical_path: String,
101 module_id: ModuleId,
103 },
104 Named {
106 canonical_path: String,
108 module_id: ModuleId,
110 symbols: Vec<NamedImportSymbol>,
112 },
113}
114
115#[derive(Debug, Clone)]
121pub struct ModuleNode {
122 pub id: ModuleId,
124 pub canonical_path: String,
126 pub source_kind: ModuleSourceKind,
128 pub ast: Option<Program>,
131 pub interface: ModuleInterface,
133 pub resolved_imports: Vec<ResolvedImport>,
135 pub dependencies: Vec<ModuleId>,
137}
138
139#[derive(Debug, Clone)]
148pub struct ModuleGraph {
149 nodes: Vec<ModuleNode>,
151 path_to_id: HashMap<String, ModuleId>,
153 topo_order: Vec<ModuleId>,
155 root_id: ModuleId,
157}
158
159impl ModuleGraph {
160 pub fn new(
165 nodes: Vec<ModuleNode>,
166 path_to_id: HashMap<String, ModuleId>,
167 topo_order: Vec<ModuleId>,
168 root_id: ModuleId,
169 ) -> Self {
170 Self {
171 nodes,
172 path_to_id,
173 topo_order,
174 root_id,
175 }
176 }
177
178 pub fn id_for_path(&self, path: &str) -> Option<ModuleId> {
180 self.path_to_id.get(path).copied()
181 }
182
183 pub fn node(&self, id: ModuleId) -> &ModuleNode {
185 &self.nodes[id.0 as usize]
186 }
187
188 pub fn node_mut(&mut self, id: ModuleId) -> &mut ModuleNode {
190 &mut self.nodes[id.0 as usize]
191 }
192
193 pub fn topo_order(&self) -> &[ModuleId] {
196 &self.topo_order
197 }
198
199 pub fn root_id(&self) -> ModuleId {
201 self.root_id
202 }
203
204 pub fn nodes(&self) -> &[ModuleNode] {
206 &self.nodes
207 }
208
209 pub fn len(&self) -> usize {
211 self.nodes.len()
212 }
213
214 pub fn is_empty(&self) -> bool {
216 self.nodes.is_empty()
217 }
218
219 pub fn contains(&self, path: &str) -> bool {
221 self.path_to_id.contains_key(path)
222 }
223}
224
225#[derive(Debug, Clone)]
231pub enum GraphBuildError {
232 CyclicDependency {
234 cycle: Vec<String>,
236 },
237 CompiledBytecodeNotSupported {
239 module_path: String,
240 },
241 ModuleNotFound {
243 module_path: String,
244 requested_by: String,
245 },
246 Other {
248 message: String,
249 },
250}
251
252impl std::fmt::Display for GraphBuildError {
253 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254 match self {
255 GraphBuildError::CyclicDependency { cycle } => {
256 write!(
257 f,
258 "Circular dependency detected: {}",
259 cycle.join(" → ")
260 )
261 }
262 GraphBuildError::CompiledBytecodeNotSupported { module_path } => {
263 write!(
264 f,
265 "Module '{}' is only available as pre-compiled bytecode. \
266 Graph-mode compilation requires source modules. Use \
267 `shape bundle --include-source` to include source in the \
268 package, or compile the dependency from source.",
269 module_path
270 )
271 }
272 GraphBuildError::ModuleNotFound {
273 module_path,
274 requested_by,
275 } => {
276 write!(
277 f,
278 "Module '{}' not found (imported by '{}')",
279 module_path, requested_by
280 )
281 }
282 GraphBuildError::Other { message } => write!(f, "{}", message),
283 }
284 }
285}
286
287impl std::error::Error for GraphBuildError {}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum ModuleSourceKindHint {
294 NativeExtension,
296 ShapeSource,
298 EmbeddedStdlib,
300 CompiledBundle,
302 NotFound,
304}
305
306pub fn resolve_module_source_kind(
308 loader: &shape_runtime::module_loader::ModuleLoader,
309 module_path: &str,
310) -> ModuleSourceKindHint {
311 if loader.has_extension_module(module_path) {
313 return ModuleSourceKindHint::NativeExtension;
314 }
315 if loader.embedded_stdlib_module_paths().contains(&module_path.to_string()) {
317 return ModuleSourceKindHint::EmbeddedStdlib;
318 }
319 if loader.resolve_module_path(module_path).is_ok() {
321 return ModuleSourceKindHint::ShapeSource;
322 }
323 ModuleSourceKindHint::NotFound
324}
325
326pub struct GraphBuilder {
328 nodes: Vec<ModuleNode>,
329 path_to_id: HashMap<String, ModuleId>,
330 visiting: HashSet<String>,
332 visited: HashSet<String>,
334}
335
336impl GraphBuilder {
337 pub fn new() -> Self {
339 Self {
340 nodes: Vec::new(),
341 path_to_id: HashMap::new(),
342 visiting: HashSet::new(),
343 visited: HashSet::new(),
344 }
345 }
346
347 pub fn get_or_create_node(&mut self, canonical_path: &str) -> ModuleId {
350 if let Some(&id) = self.path_to_id.get(canonical_path) {
351 return id;
352 }
353 let id = ModuleId(self.nodes.len() as u32);
354 self.nodes.push(ModuleNode {
355 id,
356 canonical_path: canonical_path.to_string(),
357 source_kind: ModuleSourceKind::ShapeSource, ast: None,
359 interface: ModuleInterface::default(),
360 resolved_imports: Vec::new(),
361 dependencies: Vec::new(),
362 });
363 self.path_to_id.insert(canonical_path.to_string(), id);
364 id
365 }
366
367 pub fn begin_visit(&mut self, canonical_path: &str) -> bool {
370 self.visiting.insert(canonical_path.to_string())
371 }
372
373 pub fn end_visit(&mut self, canonical_path: &str) {
375 self.visiting.remove(canonical_path);
376 self.visited.insert(canonical_path.to_string());
377 }
378
379 pub fn is_visited(&self, canonical_path: &str) -> bool {
381 self.visited.contains(canonical_path)
382 }
383
384 pub fn is_visiting(&self, canonical_path: &str) -> bool {
386 self.visiting.contains(canonical_path)
387 }
388
389 pub fn get_cycle_path(&self, target: &str) -> Vec<String> {
391 let mut cycle: Vec<String> = self.visiting.iter().cloned().collect();
394 cycle.push(target.to_string());
395 cycle
396 }
397
398 pub fn compute_topo_order(&self, root_id: ModuleId) -> Vec<ModuleId> {
401 let mut order = Vec::new();
402 let mut visited = HashSet::new();
403 for node in &self.nodes {
404 self.topo_dfs(node.id, root_id, &mut visited, &mut order);
405 }
406 order
407 }
408
409 fn topo_dfs(
410 &self,
411 current: ModuleId,
412 root_id: ModuleId,
413 visited: &mut HashSet<ModuleId>,
414 order: &mut Vec<ModuleId>,
415 ) {
416 if !visited.insert(current) {
417 return;
418 }
419 let node = &self.nodes[current.0 as usize];
420 for &dep in &node.dependencies {
421 self.topo_dfs(dep, root_id, visited, order);
422 }
423 if current != root_id {
425 order.push(current);
426 }
427 }
428
429 pub fn build(self, root_id: ModuleId) -> ModuleGraph {
431 let topo_order = self.compute_topo_order(root_id);
432 ModuleGraph::new(self.nodes, self.path_to_id, topo_order, root_id)
433 }
434}
435
436impl Default for GraphBuilder {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442fn extract_import_paths(ast: &Program) -> Vec<String> {
449 ast.items
450 .iter()
451 .filter_map(|item| {
452 if let shape_ast::ast::Item::Import(import_stmt, _) = item {
453 Some(import_stmt.from.clone())
454 } else {
455 None
456 }
457 })
458 .collect()
459}
460
461fn build_shape_interface(ast: &Program) -> ModuleInterface {
463 let symbols = match shape_ast::module_utils::collect_exported_symbols(ast) {
464 Ok(syms) => syms,
465 Err(_) => return ModuleInterface::default(),
466 };
467
468 let mut exports = HashMap::new();
469 for sym in symbols {
470 let name = sym.alias.unwrap_or(sym.name);
471 exports.insert(
472 name,
473 ExportedSymbol {
474 kind: sym.kind,
475 function_def: None,
476 visibility: ModuleExportVisibility::Public,
477 },
478 );
479 }
480
481 ModuleInterface { exports }
482}
483
484fn build_native_interface(
486 module: &shape_runtime::module_exports::ModuleExports,
487) -> ModuleInterface {
488 let mut exports = HashMap::new();
489 for name in module.export_names() {
490 let visibility = match module.export_visibility(name) {
491 shape_runtime::module_exports::ModuleExportVisibility::Public => {
492 ModuleExportVisibility::Public
493 }
494 shape_runtime::module_exports::ModuleExportVisibility::ComptimeOnly => {
495 ModuleExportVisibility::ComptimeOnly
496 }
497 shape_runtime::module_exports::ModuleExportVisibility::Internal => {
498 ModuleExportVisibility::Public
499 }
500 };
501 exports.insert(
502 name.to_string(),
503 ExportedSymbol {
504 kind: ModuleExportKind::Function,
505 function_def: None,
506 visibility,
507 },
508 );
509 }
510 ModuleInterface { exports }
511}
512
513fn resolve_imports_for_node(
515 ast: &Program,
516 builder: &GraphBuilder,
517) -> Vec<ResolvedImport> {
518 let mut resolved = Vec::new();
519
520 for item in &ast.items {
521 let shape_ast::ast::Item::Import(import_stmt, _) = item else {
522 continue;
523 };
524 let module_path = &import_stmt.from;
525 let Some(&dep_id) = builder.path_to_id.get(module_path) else {
526 continue;
527 };
528 let dep_node = &builder.nodes[dep_id.0 as usize];
529
530 match &import_stmt.items {
531 shape_ast::ast::ImportItems::Namespace { name, alias } => {
532 let local_name = alias
533 .as_ref()
534 .or(Some(name))
535 .cloned()
536 .unwrap_or_else(|| {
537 module_path
538 .split("::")
539 .last()
540 .unwrap_or(module_path)
541 .to_string()
542 });
543 resolved.push(ResolvedImport::Namespace {
544 local_name,
545 canonical_path: module_path.clone(),
546 module_id: dep_id,
547 });
548 }
549 shape_ast::ast::ImportItems::Named(specs) => {
550 let mut symbols = Vec::new();
551 for spec in specs {
552 let kind = dep_node
553 .interface
554 .exports
555 .get(&spec.name)
556 .map(|e| e.kind)
557 .unwrap_or(ModuleExportKind::Function);
558 symbols.push(NamedImportSymbol {
559 original_name: spec.name.clone(),
560 local_name: spec.alias.clone().unwrap_or_else(|| spec.name.clone()),
561 is_annotation: spec.is_annotation,
562 kind,
563 });
564 }
565 resolved.push(ResolvedImport::Named {
566 canonical_path: module_path.clone(),
567 module_id: dep_id,
568 symbols,
569 });
570 }
571 }
572 }
573
574 resolved
575}
576
577pub fn build_module_graph(
591 root_program: &Program,
592 loader: &mut shape_runtime::module_loader::ModuleLoader,
593 extensions: &[shape_runtime::module_exports::ModuleExports],
594 prelude_imports: &[String],
595) -> Result<ModuleGraph, GraphBuildError> {
596 let structured = collect_prelude_imports(loader);
598 build_module_graph_with_prelude_structure(
599 root_program,
600 loader,
601 extensions,
602 prelude_imports,
603 &structured,
604 )
605}
606
607fn build_module_graph_with_prelude_structure(
609 root_program: &Program,
610 loader: &mut shape_runtime::module_loader::ModuleLoader,
611 extensions: &[shape_runtime::module_exports::ModuleExports],
612 prelude_imports: &[String],
613 structured_prelude: &[PreludeImport],
614) -> Result<ModuleGraph, GraphBuildError> {
615 let mut builder = GraphBuilder::new();
616
617 for ext in extensions {
620 let ext_id = builder.get_or_create_node(&ext.name);
621 let node = &mut builder.nodes[ext_id.0 as usize];
622 node.source_kind = ModuleSourceKind::NativeModule;
623 node.interface = build_native_interface(ext);
624 builder.visited.insert(ext.name.clone());
625
626 if let Ok(module) = loader.load_module(&ext.name) {
636 let shape_interface = build_shape_interface(&module.ast);
637 let node = &mut builder.nodes[ext_id.0 as usize];
639 node.source_kind = ModuleSourceKind::Hybrid;
640 node.ast = Some(module.ast.clone());
641 for (name, sym) in shape_interface.exports {
643 node.interface.exports.insert(name, sym);
644 }
645 builder.visited.remove(&ext.name);
647 }
648 }
649
650 let root_id = builder.get_or_create_node("__root__");
652 {
653 let node = &mut builder.nodes[root_id.0 as usize];
654 node.source_kind = ModuleSourceKind::ShapeSource;
655 node.ast = Some(root_program.clone());
656 node.interface = build_shape_interface(root_program);
657 }
658
659 let mut root_deps = extract_import_paths(root_program);
662 for prelude_path in prelude_imports {
663 if !root_deps.contains(prelude_path) {
664 root_deps.push(prelude_path.clone());
665 }
666 }
667
668 visit_module(
669 "__root__",
670 &root_deps,
671 &mut builder,
672 loader,
673 extensions,
674 prelude_imports,
675 )?;
676
677 let node_count = builder.nodes.len();
680 for i in 0..node_count {
681 let ast = builder.nodes[i].ast.clone();
682 if let Some(ast) = &ast {
683 let resolved = resolve_imports_for_node(ast, &builder);
684 builder.nodes[i].resolved_imports = resolved;
685
686 let deps: Vec<ModuleId> = builder.nodes[i]
688 .resolved_imports
689 .iter()
690 .map(|ri| match ri {
691 ResolvedImport::Namespace { module_id, .. } => *module_id,
692 ResolvedImport::Named { module_id, .. } => *module_id,
693 })
694 .collect();
695 builder.nodes[i].dependencies = deps;
696 }
697 }
698
699 for i in 0..node_count {
702 let node_path = builder.nodes[i].canonical_path.clone();
703 if node_path.starts_with("std::core::prelude")
705 || prelude_imports.contains(&node_path)
706 {
707 continue;
708 }
709 for pi in structured_prelude {
710 let Some(&dep_id) = builder.path_to_id.get(pi.canonical_path.as_str()) else {
711 continue;
712 };
713
714 if pi.is_namespace || pi.named_symbols.is_empty() {
715 let has_namespace_import = builder.nodes[i].resolved_imports.iter().any(|ri| {
720 matches!(ri, ResolvedImport::Namespace { canonical_path, .. }
721 if canonical_path == &pi.canonical_path)
722 });
723 if has_namespace_import {
724 continue;
725 }
726
727 let local_name = pi
728 .canonical_path
729 .split("::")
730 .last()
731 .unwrap_or(&pi.canonical_path)
732 .to_string();
733 builder.nodes[i]
734 .resolved_imports
735 .push(ResolvedImport::Namespace {
736 local_name,
737 canonical_path: pi.canonical_path.clone(),
738 module_id: dep_id,
739 });
740 } else {
741 let existing_names: HashSet<String> = builder.nodes[i]
745 .resolved_imports
746 .iter()
747 .filter_map(|ri| match ri {
748 ResolvedImport::Named {
749 canonical_path,
750 symbols,
751 ..
752 } if canonical_path == &pi.canonical_path => {
753 Some(symbols.iter().map(|s| s.local_name.clone()))
754 }
755 _ => None,
756 })
757 .flatten()
758 .collect();
759
760 let dep_node = &builder.nodes[dep_id.0 as usize];
765 let mut symbols = Vec::new();
766 for sym in &pi.named_symbols {
767 if existing_names.contains(&sym.name) {
769 continue;
770 }
771 let kind = dep_node
773 .interface
774 .exports
775 .get(&sym.name)
776 .map(|e| e.kind)
777 .unwrap_or(ModuleExportKind::Function);
778
779 symbols.push(NamedImportSymbol {
780 original_name: sym.name.clone(),
781 local_name: sym.name.clone(),
782 is_annotation: sym.is_annotation,
783 kind,
784 });
785 }
786
787 if !symbols.is_empty() {
788 let existing_named_idx = builder.nodes[i]
790 .resolved_imports
791 .iter()
792 .position(|ri| matches!(ri,
793 ResolvedImport::Named { canonical_path, .. }
794 if canonical_path == &pi.canonical_path
795 ));
796
797 if let Some(idx) = existing_named_idx {
798 if let ResolvedImport::Named {
800 symbols: ref mut existing_symbols,
801 ..
802 } = builder.nodes[i].resolved_imports[idx]
803 {
804 existing_symbols.extend(symbols);
805 }
806 } else {
807 builder.nodes[i]
808 .resolved_imports
809 .push(ResolvedImport::Named {
810 canonical_path: pi.canonical_path.clone(),
811 module_id: dep_id,
812 symbols,
813 });
814 }
815 }
816
817 }
822
823 if !builder.nodes[i].dependencies.contains(&dep_id) {
824 builder.nodes[i].dependencies.push(dep_id);
825 }
826 }
827 }
828
829 Ok(builder.build(root_id))
831}
832
833fn visit_module(
835 current_path: &str,
836 dep_paths: &[String],
837 builder: &mut GraphBuilder,
838 loader: &mut shape_runtime::module_loader::ModuleLoader,
839 extensions: &[shape_runtime::module_exports::ModuleExports],
840 prelude_imports: &[String],
841) -> Result<(), GraphBuildError> {
842 if !builder.begin_visit(current_path) {
843 return Err(GraphBuildError::CyclicDependency {
844 cycle: builder.get_cycle_path(current_path),
845 });
846 }
847
848 for dep_path in dep_paths {
849 if builder.is_visited(dep_path) {
851 continue;
852 }
853
854 if builder.path_to_id.contains_key(dep_path.as_str()) && builder.is_visited(dep_path) {
856 continue;
857 }
858
859 let kind_hint = resolve_module_source_kind(loader, dep_path);
861
862 match kind_hint {
863 ModuleSourceKindHint::NativeExtension => {
864 if !builder.path_to_id.contains_key(dep_path.as_str()) {
868 let ext = extensions.iter().find(|e| e.name == *dep_path);
869 let dep_id = builder.get_or_create_node(dep_path);
870 let node = &mut builder.nodes[dep_id.0 as usize];
871 node.source_kind = ModuleSourceKind::NativeModule;
872 if let Some(ext) = ext {
873 node.interface = build_native_interface(ext);
874 }
875 builder.visited.insert(dep_path.clone());
876 }
877 }
878 ModuleSourceKindHint::ShapeSource
879 | ModuleSourceKindHint::EmbeddedStdlib => {
880 let module = loader
882 .load_module(dep_path)
883 .map_err(|e| GraphBuildError::Other {
884 message: format!(
885 "Failed to load module '{}': {}",
886 dep_path, e
887 ),
888 })?;
889
890 let dep_id = builder.get_or_create_node(dep_path);
891 let node = &mut builder.nodes[dep_id.0 as usize];
892
893 let is_native = extensions.iter().any(|e| e.name == *dep_path);
895 if is_native {
896 node.source_kind = ModuleSourceKind::Hybrid;
897 let shape_iface = build_shape_interface(&module.ast);
899 let native_ext = extensions.iter().find(|e| e.name == *dep_path).unwrap();
900 let mut native_iface = build_native_interface(native_ext);
901 for (name, sym) in shape_iface.exports {
903 native_iface.exports.insert(name, sym);
904 }
905 node.interface = native_iface;
906 } else {
907 node.source_kind = ModuleSourceKind::ShapeSource;
908 node.interface = build_shape_interface(&module.ast);
909 }
910 node.ast = Some(module.ast.clone());
911
912 let mut sub_deps = extract_import_paths(&module.ast);
914 if !prelude_imports.contains(dep_path) {
917 for pp in prelude_imports {
918 if !sub_deps.contains(pp) {
919 sub_deps.push(pp.clone());
920 }
921 }
922 }
923
924 visit_module(
925 dep_path,
926 &sub_deps,
927 builder,
928 loader,
929 extensions,
930 prelude_imports,
931 )?;
932 }
933 ModuleSourceKindHint::CompiledBundle => {
934 return Err(GraphBuildError::CompiledBytecodeNotSupported {
935 module_path: dep_path.clone(),
936 });
937 }
938 ModuleSourceKindHint::NotFound => {
939 }
944 }
945 }
946
947 builder.end_visit(current_path);
948 Ok(())
949}
950
951#[derive(Debug, Clone)]
953pub struct PreludeNamedSymbol {
954 pub name: String,
955 pub is_annotation: bool,
956}
957
958#[derive(Debug, Clone)]
960pub struct PreludeImport {
961 pub canonical_path: String,
962 pub named_symbols: Vec<PreludeNamedSymbol>,
963 pub is_namespace: bool,
964}
965
966pub fn collect_prelude_imports(
972 loader: &mut shape_runtime::module_loader::ModuleLoader,
973) -> Vec<PreludeImport> {
974 let prelude = match loader.load_module("std::core::prelude") {
983 Ok(m) => m,
984 Err(e) => {
985 eprintln!(
986 "warning: failed to load std::core::prelude — prelude items will be unavailable: {}",
987 e
988 );
989 return Vec::new();
990 }
991 };
992
993 let mut imports = Vec::new();
994 for item in &prelude.ast.items {
995 if let shape_ast::ast::Item::Import(import_stmt, _) = item {
996 if imports
998 .iter()
999 .any(|i: &PreludeImport| i.canonical_path == import_stmt.from)
1000 {
1001 continue;
1002 }
1003
1004 match &import_stmt.items {
1005 shape_ast::ast::ImportItems::Named(specs) => {
1006 let symbols = specs
1007 .iter()
1008 .map(|spec| PreludeNamedSymbol {
1009 name: spec.name.clone(),
1010 is_annotation: spec.is_annotation,
1011 })
1012 .collect();
1013 imports.push(PreludeImport {
1014 canonical_path: import_stmt.from.clone(),
1015 named_symbols: symbols,
1016 is_namespace: false,
1017 });
1018 }
1019 shape_ast::ast::ImportItems::Namespace { .. } => {
1020 imports.push(PreludeImport {
1021 canonical_path: import_stmt.from.clone(),
1022 named_symbols: Vec::new(),
1023 is_namespace: true,
1024 });
1025 }
1026 }
1027 }
1028 }
1029 imports
1030}
1031
1032pub fn collect_prelude_import_paths(
1038 loader: &mut shape_runtime::module_loader::ModuleLoader,
1039) -> Vec<String> {
1040 collect_prelude_imports(loader)
1041 .into_iter()
1042 .map(|pi| pi.canonical_path)
1043 .collect()
1044}
1045
1046#[cfg(test)]
1051mod tests {
1052 use super::*;
1053
1054 #[test]
1055 fn test_graph_builder_basic() {
1056 let mut builder = GraphBuilder::new();
1057 let a = builder.get_or_create_node("a");
1058 let b = builder.get_or_create_node("b");
1059 let c = builder.get_or_create_node("c");
1060
1061 builder.nodes[a.0 as usize].dependencies.push(b);
1063 builder.nodes[b.0 as usize].dependencies.push(c);
1064
1065 let graph = builder.build(a);
1066
1067 assert_eq!(graph.len(), 3);
1068 assert_eq!(graph.root_id(), a);
1069 assert_eq!(graph.topo_order(), &[c, b]);
1071 }
1072
1073 #[test]
1074 fn test_graph_builder_dedup() {
1075 let mut builder = GraphBuilder::new();
1076 let id1 = builder.get_or_create_node("std::core::math");
1077 let id2 = builder.get_or_create_node("std::core::math");
1078 assert_eq!(id1, id2);
1079 }
1080
1081 #[test]
1082 fn test_cycle_detection() {
1083 let mut builder = GraphBuilder::new();
1084 assert!(builder.begin_visit("a"));
1085 assert!(builder.begin_visit("b"));
1086 assert!(builder.is_visiting("a"));
1087 assert!(!builder.begin_visit("a")); }
1089
1090 #[test]
1091 fn test_graph_lookup() {
1092 let mut builder = GraphBuilder::new();
1093 let math_id = builder.get_or_create_node("std::core::math");
1094 builder.nodes[math_id.0 as usize].source_kind = ModuleSourceKind::NativeModule;
1095
1096 let graph = builder.build(math_id);
1097 assert_eq!(graph.id_for_path("std::core::math"), Some(math_id));
1098 assert_eq!(graph.id_for_path("nonexistent"), None);
1099 assert_eq!(
1100 graph.node(math_id).source_kind,
1101 ModuleSourceKind::NativeModule
1102 );
1103 }
1104
1105 #[test]
1106 fn test_diamond_dependency() {
1107 let mut builder = GraphBuilder::new();
1108 let root = builder.get_or_create_node("root");
1109 let a = builder.get_or_create_node("a");
1110 let b = builder.get_or_create_node("b");
1111 let c = builder.get_or_create_node("c");
1112
1113 builder.nodes[root.0 as usize].dependencies.push(a);
1115 builder.nodes[root.0 as usize].dependencies.push(b);
1116 builder.nodes[a.0 as usize].dependencies.push(c);
1117 builder.nodes[b.0 as usize].dependencies.push(c);
1118
1119 let graph = builder.build(root);
1120
1121 let order = graph.topo_order();
1123 assert_eq!(order.len(), 3); let c_pos = order.iter().position(|&id| id == c).unwrap();
1125 let a_pos = order.iter().position(|&id| id == a).unwrap();
1126 let b_pos = order.iter().position(|&id| id == b).unwrap();
1127 assert!(c_pos < a_pos);
1128 assert!(c_pos < b_pos);
1129 }
1130}