Skip to main content

shape_vm/
module_graph.rs

1//! Canonical module graph for dependency-ordered compilation.
2//!
3//! Replaces AST import inlining with a directed acyclic graph where each
4//! module is a node with its own resolved imports and public interface.
5//! Modules compile in topological order using the graph for cross-module
6//! name resolution.
7
8use 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// ---------------------------------------------------------------------------
16// Core identifiers
17// ---------------------------------------------------------------------------
18
19/// Opaque module identity — index into the graph's node array.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ModuleId(pub u32);
22
23// ---------------------------------------------------------------------------
24// Source classification
25// ---------------------------------------------------------------------------
26
27/// How a module's implementation is provided.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ModuleSourceKind {
30    /// Has `.shape` source, compiles to bytecode.
31    ShapeSource,
32    /// Rust-backed `ModuleExports`, runtime dispatch only.
33    NativeModule,
34    /// Both native exports AND Shape source overlay.
35    Hybrid,
36    /// Pre-compiled, no source available (deferred — emits hard error).
37    CompiledBytecode,
38}
39
40// ---------------------------------------------------------------------------
41// Export visibility
42// ---------------------------------------------------------------------------
43
44/// Visibility of a module export.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ModuleExportVisibility {
47    /// Available at both compile time and runtime.
48    Public,
49    /// Available only during comptime evaluation.
50    ComptimeOnly,
51}
52
53// ---------------------------------------------------------------------------
54// Module interface
55// ---------------------------------------------------------------------------
56
57/// Metadata for a single exported symbol.
58#[derive(Debug, Clone)]
59pub struct ExportedSymbol {
60    /// What kind of symbol this is (function, type, annotation, etc.).
61    pub kind: ModuleExportKind,
62    /// The full function definition, if this export is a Shape function.
63    pub function_def: Option<Arc<FunctionDef>>,
64    /// Visibility level.
65    pub visibility: ModuleExportVisibility,
66}
67
68/// The public interface of a module — what it exports.
69#[derive(Debug, Clone, Default)]
70pub struct ModuleInterface {
71    /// Exported symbols keyed by their public name (after alias resolution).
72    pub exports: HashMap<String, ExportedSymbol>,
73}
74
75// ---------------------------------------------------------------------------
76// Resolved imports (per-node)
77// ---------------------------------------------------------------------------
78
79/// A single symbol from a named import (`from m use { a, b as c }`).
80#[derive(Debug, Clone)]
81pub struct NamedImportSymbol {
82    /// Name as it appears in the source module.
83    pub original_name: String,
84    /// Name bound in the importing module (may differ via `as` alias).
85    pub local_name: String,
86    /// Whether this import targets an annotation definition.
87    pub is_annotation: bool,
88    /// Resolved kind from the dependency's interface.
89    pub kind: ModuleExportKind,
90}
91
92/// A resolved import — how the importing module accesses a dependency.
93#[derive(Debug, Clone)]
94pub enum ResolvedImport {
95    /// Namespace import: `use std::core::math` or `use std::core::math as m`
96    Namespace {
97        /// Local name bound in the importing module (e.g. `math` or `m`).
98        local_name: String,
99        /// Canonical module path (e.g. `std::core::math`).
100        canonical_path: String,
101        /// Graph node of the imported module.
102        module_id: ModuleId,
103    },
104    /// Named import: `from std::core::math use { sqrt, PI }`
105    Named {
106        /// Canonical module path.
107        canonical_path: String,
108        /// Graph node of the imported module.
109        module_id: ModuleId,
110        /// Individual symbols being imported.
111        symbols: Vec<NamedImportSymbol>,
112    },
113}
114
115// ---------------------------------------------------------------------------
116// Graph nodes
117// ---------------------------------------------------------------------------
118
119/// A single module in the dependency graph.
120#[derive(Debug, Clone)]
121pub struct ModuleNode {
122    /// Unique identity within this graph.
123    pub id: ModuleId,
124    /// Canonical module path (e.g. `std::core::math`, `mypackage::utils`).
125    pub canonical_path: String,
126    /// How this module is implemented.
127    pub source_kind: ModuleSourceKind,
128    /// Parsed AST (present for `ShapeSource` and `Hybrid`, absent for
129    /// `NativeModule` and `CompiledBytecode`).
130    pub ast: Option<Program>,
131    /// Public interface (exports).
132    pub interface: ModuleInterface,
133    /// Resolved imports for this module.
134    pub resolved_imports: Vec<ResolvedImport>,
135    /// Direct dependencies (modules this one imports).
136    pub dependencies: Vec<ModuleId>,
137}
138
139// ---------------------------------------------------------------------------
140// The graph
141// ---------------------------------------------------------------------------
142
143/// Canonical module graph — the single source of truth for import resolution.
144///
145/// Built before compilation; modules compile in `topo_order` so that
146/// dependencies are always available when a module is compiled.
147#[derive(Debug, Clone)]
148pub struct ModuleGraph {
149    /// All module nodes, indexed by `ModuleId`.
150    nodes: Vec<ModuleNode>,
151    /// Canonical path → node id lookup.
152    path_to_id: HashMap<String, ModuleId>,
153    /// Topological compilation order (dependencies before dependents).
154    topo_order: Vec<ModuleId>,
155    /// The root module (entry point / user script).
156    root_id: ModuleId,
157}
158
159impl ModuleGraph {
160    /// Create a new graph from pre-built components.
161    ///
162    /// Used by the graph builder after all nodes, interfaces, and edges
163    /// have been constructed and topologically sorted.
164    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    /// Look up a module by its canonical path.
179    pub fn id_for_path(&self, path: &str) -> Option<ModuleId> {
180        self.path_to_id.get(path).copied()
181    }
182
183    /// Get a module node by id.
184    pub fn node(&self, id: ModuleId) -> &ModuleNode {
185        &self.nodes[id.0 as usize]
186    }
187
188    /// Get a mutable module node by id.
189    pub fn node_mut(&mut self, id: ModuleId) -> &mut ModuleNode {
190        &mut self.nodes[id.0 as usize]
191    }
192
193    /// Topological compilation order (dependencies before dependents).
194    /// Does NOT include the root module — that is compiled separately.
195    pub fn topo_order(&self) -> &[ModuleId] {
196        &self.topo_order
197    }
198
199    /// The root module id.
200    pub fn root_id(&self) -> ModuleId {
201        self.root_id
202    }
203
204    /// All nodes in the graph.
205    pub fn nodes(&self) -> &[ModuleNode] {
206        &self.nodes
207    }
208
209    /// Number of modules in the graph.
210    pub fn len(&self) -> usize {
211        self.nodes.len()
212    }
213
214    /// Whether the graph is empty.
215    pub fn is_empty(&self) -> bool {
216        self.nodes.is_empty()
217    }
218
219    /// Check if a canonical path is registered in the graph.
220    pub fn contains(&self, path: &str) -> bool {
221        self.path_to_id.contains_key(path)
222    }
223}
224
225// ---------------------------------------------------------------------------
226// Graph builder
227// ---------------------------------------------------------------------------
228
229/// Errors that can occur during graph construction.
230#[derive(Debug, Clone)]
231pub enum GraphBuildError {
232    /// Circular dependency detected.
233    CyclicDependency {
234        /// The cycle path, e.g. `["a", "b", "c", "a"]`.
235        cycle: Vec<String>,
236    },
237    /// A module is only available as pre-compiled bytecode.
238    CompiledBytecodeNotSupported {
239        module_path: String,
240    },
241    /// Module not found.
242    ModuleNotFound {
243        module_path: String,
244        requested_by: String,
245    },
246    /// Other error during graph construction.
247    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/// Classification hint for how a module path should be resolved.
290///
291/// Used during graph construction to decide how to handle each dependency.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum ModuleSourceKindHint {
294    /// Module is backed by a native extension (Rust `ModuleExports`).
295    NativeExtension,
296    /// Module has `.shape` source code available.
297    ShapeSource,
298    /// Module is an embedded stdlib module.
299    EmbeddedStdlib,
300    /// Module is available only as a pre-compiled bundle.
301    CompiledBundle,
302    /// Module could not be found.
303    NotFound,
304}
305
306/// Classify a module path by probing the module loader's resolvers.
307pub fn resolve_module_source_kind(
308    loader: &shape_runtime::module_loader::ModuleLoader,
309    module_path: &str,
310) -> ModuleSourceKindHint {
311    // Check if it's a registered extension module (native)
312    if loader.has_extension_module(module_path) {
313        return ModuleSourceKindHint::NativeExtension;
314    }
315    // Check if it's an embedded stdlib module
316    if loader.embedded_stdlib_module_paths().contains(&module_path.to_string()) {
317        return ModuleSourceKindHint::EmbeddedStdlib;
318    }
319    // Check if we can resolve a file path for it
320    if loader.resolve_module_path(module_path).is_ok() {
321        return ModuleSourceKindHint::ShapeSource;
322    }
323    ModuleSourceKindHint::NotFound
324}
325
326/// Intermediate builder state used during graph construction.
327pub struct GraphBuilder {
328    nodes: Vec<ModuleNode>,
329    path_to_id: HashMap<String, ModuleId>,
330    /// Tracks modules currently being visited for cycle detection.
331    visiting: HashSet<String>,
332    /// Tracks modules that have been fully processed.
333    visited: HashSet<String>,
334}
335
336impl GraphBuilder {
337    /// Create a new empty graph builder.
338    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    /// Allocate a new node with the given canonical path and return its id.
348    /// If a node with this path already exists, returns its existing id.
349    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, // default, overwritten later
358            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    /// Mark a module as currently being visited (for cycle detection).
368    /// Returns `false` if the module is already being visited (cycle!).
369    pub fn begin_visit(&mut self, canonical_path: &str) -> bool {
370        self.visiting.insert(canonical_path.to_string())
371    }
372
373    /// Mark a module as fully visited.
374    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    /// Check if a module has been fully visited.
380    pub fn is_visited(&self, canonical_path: &str) -> bool {
381        self.visited.contains(canonical_path)
382    }
383
384    /// Check if a module is currently being visited (would form a cycle).
385    pub fn is_visiting(&self, canonical_path: &str) -> bool {
386        self.visiting.contains(canonical_path)
387    }
388
389    /// Get the cycle path when a cycle is detected.
390    pub fn get_cycle_path(&self, target: &str) -> Vec<String> {
391        // The visiting set doesn't preserve order, so we just report
392        // the modules involved. The caller can provide more context.
393        let mut cycle: Vec<String> = self.visiting.iter().cloned().collect();
394        cycle.push(target.to_string());
395        cycle
396    }
397
398    /// Compute topological order via DFS post-order.
399    /// The root module is excluded from the topo order (compiled separately).
400    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        // Exclude root from topo order — it is compiled separately
424        if current != root_id {
425            order.push(current);
426        }
427    }
428
429    /// Finalize into a `ModuleGraph`.
430    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
442// ---------------------------------------------------------------------------
443// Graph building
444// ---------------------------------------------------------------------------
445
446/// Extract import paths from a program's AST (same logic as
447/// `shape_runtime::module_loader::resolution::extract_dependencies`).
448fn 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
461/// Build a module interface from a Shape source AST.
462fn 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
484/// Build a module interface from a native `ModuleExports`.
485fn 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
513/// Resolve imports for a module node against the graph's dependency interfaces.
514fn 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
577/// Build a complete module graph from a root program.
578///
579/// Algorithm:
580/// 1. Pre-register native modules from `extensions`
581/// 2. Create root node from the user's program
582/// 3. Walk imports recursively (DFS), loading Shape sources via `loader`
583/// 4. Build interfaces per node
584/// 5. Resolve per-node imports against dependency interfaces
585/// 6. Cycle detection via visiting set
586/// 7. Topological sort
587///
588/// Prelude modules are included as synthetic low-priority imports on
589/// each module node.
590pub 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    // Collect structured prelude imports from the loader
597    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
607/// Build a module graph with pre-collected structured prelude import data.
608fn 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    // Step 1: Pre-register native extension modules.
618    // These have no source artifact in the loader; they are Rust-backed.
619    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        // Also check if the extension provides Shape source overlays
627        // (module_artifacts or shape_sources), making it Hybrid.
628        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            // Try to load the Shape overlay AST
636            if let Ok(module) = loader.load_module(&ext.name) {
637                let shape_interface = build_shape_interface(&module.ast);
638                // Merge: Shape exports take priority over native
639                let node = &mut builder.nodes[ext_id.0 as usize];
640                node.source_kind = ModuleSourceKind::Hybrid;
641                node.ast = Some(module.ast.clone());
642                // Merge interfaces: Shape exports override native ones
643                for (name, sym) in shape_interface.exports {
644                    node.interface.exports.insert(name, sym);
645                }
646                // Hybrid modules need their Shape source dependencies walked
647                builder.visited.remove(&ext.name);
648            }
649        }
650    }
651
652    // Step 2: Create root node.
653    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    // Step 3: Walk imports recursively.
662    // Collect the root's direct imports plus prelude imports.
663    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    // Step 4: Resolve imports per node (build ResolvedImport entries).
680    // Must be done after all nodes are created so dependency lookups work.
681    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            // Also set dependencies from resolved imports
689            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    // Step 5: Add prelude as synthetic low-priority imports to each module
702    // node that doesn't already have explicit imports for them.
703    for i in 0..node_count {
704        let node_path = builder.nodes[i].canonical_path.clone();
705        // Skip prelude modules themselves to avoid circular dependencies
706        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                // Namespace import: skip only if there is already an explicit
718                // Namespace import for this path. A Named import from the same
719                // module is not conflicting — it provides bare names while the
720                // namespace provides qualified access.
721                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                // Named import: per-symbol merge.
744                // Collect which local names already exist from explicit Named imports
745                // on this node for this module path.
746                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                // If already imported as namespace, the symbols are accessible
763                // via qualified names — but we still add named imports so bare
764                // names resolve. Only skip symbols whose bare name already exists.
765
766                let dep_node = &builder.nodes[dep_id.0 as usize];
767                let mut symbols = Vec::new();
768                for sym in &pi.named_symbols {
769                    // Skip if this specific name is already imported explicitly
770                    if existing_names.contains(&sym.name) {
771                        continue;
772                    }
773                    // Resolve the export kind from the dependency's interface
774                    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                    // Check if we already have a Named import for this path to merge into
791                    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                        // Merge symbols into existing Named import
801                        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                // Don't add a namespace binding for Named prelude imports.
820                // A namespace binding with the last segment (e.g., "snapshot" from
821                // "std::core::snapshot") would shadow the bare named symbol when
822                // module_binding_name resolution runs before find_function.
823            }
824
825            if !builder.nodes[i].dependencies.contains(&dep_id) {
826                builder.nodes[i].dependencies.push(dep_id);
827            }
828        }
829    }
830
831    // Step 6: Build final graph with topological order.
832    Ok(builder.build(root_id))
833}
834
835/// Recursively visit a module's dependencies and add them to the graph.
836fn 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        // Already fully processed?
852        if builder.is_visited(dep_path) {
853            continue;
854        }
855
856        // Already has a node (pre-registered native module)?
857        if builder.path_to_id.contains_key(dep_path.as_str()) && builder.is_visited(dep_path) {
858            continue;
859        }
860
861        // Classify the module
862        let kind_hint = resolve_module_source_kind(loader, dep_path);
863
864        match kind_hint {
865            ModuleSourceKindHint::NativeExtension => {
866                // Should have been caught in pre-registration step.
867                // If not, it's an extension module registered via loader
868                // but not in the extensions list. Create a native node.
869                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                // Load Shape source
883                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                // Check if this is also a native extension (Hybrid)
896                let is_native = extensions.iter().any(|e| e.name == *dep_path);
897                if is_native {
898                    node.source_kind = ModuleSourceKind::Hybrid;
899                    // Build merged interface
900                    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                    // Shape exports take priority
904                    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                // Recurse into this module's dependencies
915                let mut sub_deps = extract_import_paths(&module.ast);
916                // Also add prelude imports, but skip for prelude modules
917                // themselves to avoid circular dependencies among them.
918                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                // Module not found — might be a prelude module or just missing.
942                // We skip silently here; the compiler will emit proper errors.
943                // For prelude modules that don't resolve, this is expected
944                // (e.g., they may be virtual stdlib modules handled at compile time).
945            }
946        }
947    }
948
949    builder.end_visit(current_path);
950    Ok(())
951}
952
953/// A single symbol imported by the prelude.
954#[derive(Debug, Clone)]
955pub struct PreludeNamedSymbol {
956    pub name: String,
957    pub is_annotation: bool,
958}
959
960/// A prelude import preserving the named/namespace structure from prelude.shape.
961#[derive(Debug, Clone)]
962pub struct PreludeImport {
963    pub canonical_path: String,
964    pub named_symbols: Vec<PreludeNamedSymbol>,
965    pub is_namespace: bool,
966}
967
968/// Collect structured prelude imports by loading `std::core::prelude`.
969///
970/// Preserves the named/namespace import structure so the graph builder
971/// can generate appropriate `ResolvedImport::Named` entries (not just
972/// `ResolvedImport::Namespace` for everything).
973pub 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            // Check for duplicate module path
985            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
1020/// Collect prelude import paths by loading `std::core::prelude`.
1021///
1022/// Returns the list of module paths that the prelude imports,
1023/// which should be added as synthetic imports to each module.
1024/// This is a thin wrapper over `collect_prelude_imports` for backward compatibility.
1025pub 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// ---------------------------------------------------------------------------
1035// Tests
1036// ---------------------------------------------------------------------------
1037
1038#[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        // a depends on b, b depends on c
1050        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        // Topo order: c, b (root excluded)
1058        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")); // cycle!
1076    }
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        // root -> a, root -> b, a -> c, b -> c
1102        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        // c must come before a and b
1110        let order = graph.topo_order();
1111        assert_eq!(order.len(), 3); // root excluded
1112        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}