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 let has_shape_source = ext
629 .module_artifacts
630 .iter()
631 .any(|a| a.source.is_some() && a.module_path == ext.name)
632 || !ext.shape_sources.is_empty();
633
634 if has_shape_source {
635 if let Ok(module) = loader.load_module(&ext.name) {
637 let shape_interface = build_shape_interface(&module.ast);
638 let node = &mut builder.nodes[ext_id.0 as usize];
640 node.source_kind = ModuleSourceKind::Hybrid;
641 node.ast = Some(module.ast.clone());
642 for (name, sym) in shape_interface.exports {
644 node.interface.exports.insert(name, sym);
645 }
646 builder.visited.remove(&ext.name);
648 }
649 }
650 }
651
652 let root_id = builder.get_or_create_node("__root__");
654 {
655 let node = &mut builder.nodes[root_id.0 as usize];
656 node.source_kind = ModuleSourceKind::ShapeSource;
657 node.ast = Some(root_program.clone());
658 node.interface = build_shape_interface(root_program);
659 }
660
661 let mut root_deps = extract_import_paths(root_program);
664 for prelude_path in prelude_imports {
665 if !root_deps.contains(prelude_path) {
666 root_deps.push(prelude_path.clone());
667 }
668 }
669
670 visit_module(
671 "__root__",
672 &root_deps,
673 &mut builder,
674 loader,
675 extensions,
676 prelude_imports,
677 )?;
678
679 let node_count = builder.nodes.len();
682 for i in 0..node_count {
683 let ast = builder.nodes[i].ast.clone();
684 if let Some(ast) = &ast {
685 let resolved = resolve_imports_for_node(ast, &builder);
686 builder.nodes[i].resolved_imports = resolved;
687
688 let deps: Vec<ModuleId> = builder.nodes[i]
690 .resolved_imports
691 .iter()
692 .map(|ri| match ri {
693 ResolvedImport::Namespace { module_id, .. } => *module_id,
694 ResolvedImport::Named { module_id, .. } => *module_id,
695 })
696 .collect();
697 builder.nodes[i].dependencies = deps;
698 }
699 }
700
701 for i in 0..node_count {
704 let node_path = builder.nodes[i].canonical_path.clone();
705 if node_path.starts_with("std::core::prelude")
707 || prelude_imports.contains(&node_path)
708 {
709 continue;
710 }
711 for pi in structured_prelude {
712 let Some(&dep_id) = builder.path_to_id.get(pi.canonical_path.as_str()) else {
713 continue;
714 };
715
716 if pi.is_namespace || pi.named_symbols.is_empty() {
717 let has_namespace_import = builder.nodes[i].resolved_imports.iter().any(|ri| {
722 matches!(ri, ResolvedImport::Namespace { canonical_path, .. }
723 if canonical_path == &pi.canonical_path)
724 });
725 if has_namespace_import {
726 continue;
727 }
728
729 let local_name = pi
730 .canonical_path
731 .split("::")
732 .last()
733 .unwrap_or(&pi.canonical_path)
734 .to_string();
735 builder.nodes[i]
736 .resolved_imports
737 .push(ResolvedImport::Namespace {
738 local_name,
739 canonical_path: pi.canonical_path.clone(),
740 module_id: dep_id,
741 });
742 } else {
743 let existing_names: HashSet<String> = builder.nodes[i]
747 .resolved_imports
748 .iter()
749 .filter_map(|ri| match ri {
750 ResolvedImport::Named {
751 canonical_path,
752 symbols,
753 ..
754 } if canonical_path == &pi.canonical_path => {
755 Some(symbols.iter().map(|s| s.local_name.clone()))
756 }
757 _ => None,
758 })
759 .flatten()
760 .collect();
761
762 let dep_node = &builder.nodes[dep_id.0 as usize];
767 let mut symbols = Vec::new();
768 for sym in &pi.named_symbols {
769 if existing_names.contains(&sym.name) {
771 continue;
772 }
773 let kind = dep_node
775 .interface
776 .exports
777 .get(&sym.name)
778 .map(|e| e.kind)
779 .unwrap_or(ModuleExportKind::Function);
780
781 symbols.push(NamedImportSymbol {
782 original_name: sym.name.clone(),
783 local_name: sym.name.clone(),
784 is_annotation: sym.is_annotation,
785 kind,
786 });
787 }
788
789 if !symbols.is_empty() {
790 let existing_named_idx = builder.nodes[i]
792 .resolved_imports
793 .iter()
794 .position(|ri| matches!(ri,
795 ResolvedImport::Named { canonical_path, .. }
796 if canonical_path == &pi.canonical_path
797 ));
798
799 if let Some(idx) = existing_named_idx {
800 if let ResolvedImport::Named {
802 symbols: ref mut existing_symbols,
803 ..
804 } = builder.nodes[i].resolved_imports[idx]
805 {
806 existing_symbols.extend(symbols);
807 }
808 } else {
809 builder.nodes[i]
810 .resolved_imports
811 .push(ResolvedImport::Named {
812 canonical_path: pi.canonical_path.clone(),
813 module_id: dep_id,
814 symbols,
815 });
816 }
817 }
818
819 }
824
825 if !builder.nodes[i].dependencies.contains(&dep_id) {
826 builder.nodes[i].dependencies.push(dep_id);
827 }
828 }
829 }
830
831 Ok(builder.build(root_id))
833}
834
835fn visit_module(
837 current_path: &str,
838 dep_paths: &[String],
839 builder: &mut GraphBuilder,
840 loader: &mut shape_runtime::module_loader::ModuleLoader,
841 extensions: &[shape_runtime::module_exports::ModuleExports],
842 prelude_imports: &[String],
843) -> Result<(), GraphBuildError> {
844 if !builder.begin_visit(current_path) {
845 return Err(GraphBuildError::CyclicDependency {
846 cycle: builder.get_cycle_path(current_path),
847 });
848 }
849
850 for dep_path in dep_paths {
851 if builder.is_visited(dep_path) {
853 continue;
854 }
855
856 if builder.path_to_id.contains_key(dep_path.as_str()) && builder.is_visited(dep_path) {
858 continue;
859 }
860
861 let kind_hint = resolve_module_source_kind(loader, dep_path);
863
864 match kind_hint {
865 ModuleSourceKindHint::NativeExtension => {
866 if !builder.path_to_id.contains_key(dep_path.as_str()) {
870 let ext = extensions.iter().find(|e| e.name == *dep_path);
871 let dep_id = builder.get_or_create_node(dep_path);
872 let node = &mut builder.nodes[dep_id.0 as usize];
873 node.source_kind = ModuleSourceKind::NativeModule;
874 if let Some(ext) = ext {
875 node.interface = build_native_interface(ext);
876 }
877 builder.visited.insert(dep_path.clone());
878 }
879 }
880 ModuleSourceKindHint::ShapeSource
881 | ModuleSourceKindHint::EmbeddedStdlib => {
882 let module = loader
884 .load_module(dep_path)
885 .map_err(|e| GraphBuildError::Other {
886 message: format!(
887 "Failed to load module '{}': {}",
888 dep_path, e
889 ),
890 })?;
891
892 let dep_id = builder.get_or_create_node(dep_path);
893 let node = &mut builder.nodes[dep_id.0 as usize];
894
895 let is_native = extensions.iter().any(|e| e.name == *dep_path);
897 if is_native {
898 node.source_kind = ModuleSourceKind::Hybrid;
899 let shape_iface = build_shape_interface(&module.ast);
901 let native_ext = extensions.iter().find(|e| e.name == *dep_path).unwrap();
902 let mut native_iface = build_native_interface(native_ext);
903 for (name, sym) in shape_iface.exports {
905 native_iface.exports.insert(name, sym);
906 }
907 node.interface = native_iface;
908 } else {
909 node.source_kind = ModuleSourceKind::ShapeSource;
910 node.interface = build_shape_interface(&module.ast);
911 }
912 node.ast = Some(module.ast.clone());
913
914 let mut sub_deps = extract_import_paths(&module.ast);
916 if !prelude_imports.contains(dep_path) {
919 for pp in prelude_imports {
920 if !sub_deps.contains(pp) {
921 sub_deps.push(pp.clone());
922 }
923 }
924 }
925
926 visit_module(
927 dep_path,
928 &sub_deps,
929 builder,
930 loader,
931 extensions,
932 prelude_imports,
933 )?;
934 }
935 ModuleSourceKindHint::CompiledBundle => {
936 return Err(GraphBuildError::CompiledBytecodeNotSupported {
937 module_path: dep_path.clone(),
938 });
939 }
940 ModuleSourceKindHint::NotFound => {
941 }
946 }
947 }
948
949 builder.end_visit(current_path);
950 Ok(())
951}
952
953#[derive(Debug, Clone)]
955pub struct PreludeNamedSymbol {
956 pub name: String,
957 pub is_annotation: bool,
958}
959
960#[derive(Debug, Clone)]
962pub struct PreludeImport {
963 pub canonical_path: String,
964 pub named_symbols: Vec<PreludeNamedSymbol>,
965 pub is_namespace: bool,
966}
967
968pub fn collect_prelude_imports(
974 loader: &mut shape_runtime::module_loader::ModuleLoader,
975) -> Vec<PreludeImport> {
976 let prelude = match loader.load_module("std::core::prelude") {
977 Ok(m) => m,
978 Err(_) => return Vec::new(),
979 };
980
981 let mut imports = Vec::new();
982 for item in &prelude.ast.items {
983 if let shape_ast::ast::Item::Import(import_stmt, _) = item {
984 if imports
986 .iter()
987 .any(|i: &PreludeImport| i.canonical_path == import_stmt.from)
988 {
989 continue;
990 }
991
992 match &import_stmt.items {
993 shape_ast::ast::ImportItems::Named(specs) => {
994 let symbols = specs
995 .iter()
996 .map(|spec| PreludeNamedSymbol {
997 name: spec.name.clone(),
998 is_annotation: spec.is_annotation,
999 })
1000 .collect();
1001 imports.push(PreludeImport {
1002 canonical_path: import_stmt.from.clone(),
1003 named_symbols: symbols,
1004 is_namespace: false,
1005 });
1006 }
1007 shape_ast::ast::ImportItems::Namespace { .. } => {
1008 imports.push(PreludeImport {
1009 canonical_path: import_stmt.from.clone(),
1010 named_symbols: Vec::new(),
1011 is_namespace: true,
1012 });
1013 }
1014 }
1015 }
1016 }
1017 imports
1018}
1019
1020pub fn collect_prelude_import_paths(
1026 loader: &mut shape_runtime::module_loader::ModuleLoader,
1027) -> Vec<String> {
1028 collect_prelude_imports(loader)
1029 .into_iter()
1030 .map(|pi| pi.canonical_path)
1031 .collect()
1032}
1033
1034#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1043 fn test_graph_builder_basic() {
1044 let mut builder = GraphBuilder::new();
1045 let a = builder.get_or_create_node("a");
1046 let b = builder.get_or_create_node("b");
1047 let c = builder.get_or_create_node("c");
1048
1049 builder.nodes[a.0 as usize].dependencies.push(b);
1051 builder.nodes[b.0 as usize].dependencies.push(c);
1052
1053 let graph = builder.build(a);
1054
1055 assert_eq!(graph.len(), 3);
1056 assert_eq!(graph.root_id(), a);
1057 assert_eq!(graph.topo_order(), &[c, b]);
1059 }
1060
1061 #[test]
1062 fn test_graph_builder_dedup() {
1063 let mut builder = GraphBuilder::new();
1064 let id1 = builder.get_or_create_node("std::core::math");
1065 let id2 = builder.get_or_create_node("std::core::math");
1066 assert_eq!(id1, id2);
1067 }
1068
1069 #[test]
1070 fn test_cycle_detection() {
1071 let mut builder = GraphBuilder::new();
1072 assert!(builder.begin_visit("a"));
1073 assert!(builder.begin_visit("b"));
1074 assert!(builder.is_visiting("a"));
1075 assert!(!builder.begin_visit("a")); }
1077
1078 #[test]
1079 fn test_graph_lookup() {
1080 let mut builder = GraphBuilder::new();
1081 let math_id = builder.get_or_create_node("std::core::math");
1082 builder.nodes[math_id.0 as usize].source_kind = ModuleSourceKind::NativeModule;
1083
1084 let graph = builder.build(math_id);
1085 assert_eq!(graph.id_for_path("std::core::math"), Some(math_id));
1086 assert_eq!(graph.id_for_path("nonexistent"), None);
1087 assert_eq!(
1088 graph.node(math_id).source_kind,
1089 ModuleSourceKind::NativeModule
1090 );
1091 }
1092
1093 #[test]
1094 fn test_diamond_dependency() {
1095 let mut builder = GraphBuilder::new();
1096 let root = builder.get_or_create_node("root");
1097 let a = builder.get_or_create_node("a");
1098 let b = builder.get_or_create_node("b");
1099 let c = builder.get_or_create_node("c");
1100
1101 builder.nodes[root.0 as usize].dependencies.push(a);
1103 builder.nodes[root.0 as usize].dependencies.push(b);
1104 builder.nodes[a.0 as usize].dependencies.push(c);
1105 builder.nodes[b.0 as usize].dependencies.push(c);
1106
1107 let graph = builder.build(root);
1108
1109 let order = graph.topo_order();
1111 assert_eq!(order.len(), 3); let c_pos = order.iter().position(|&id| id == c).unwrap();
1113 let a_pos = order.iter().position(|&id| id == a).unwrap();
1114 let b_pos = order.iter().position(|&id| id == b).unwrap();
1115 assert!(c_pos < a_pos);
1116 assert!(c_pos < b_pos);
1117 }
1118}