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        // W9: Probe the loader for a Shape overlay. The loader may carry the
627        // overlay from any of:
628        //   (a) the extension's own `module_artifacts` / `shape_sources`,
629        //   (b) the embedded `stdlib-src/` tree (`EMBEDDED_STDLIB_MODULES`),
630        //   (c) a registered extension module / on-disk file.
631        // Source (b) does NOT show up on the `ext.module_artifacts` /
632        // `ext.shape_sources` fields, so the prior heuristic missed shape
633        // overlays for the embedded stdlib (e.g. `pub annotation remote(addr)`
634        // in `std::core::remote`). Always probing the loader picks those up.
635        if let Ok(module) = loader.load_module(&ext.name) {
636            let shape_interface = build_shape_interface(&module.ast);
637            // Merge: Shape exports take priority over native
638            let node = &mut builder.nodes[ext_id.0 as usize];
639            node.source_kind = ModuleSourceKind::Hybrid;
640            node.ast = Some(module.ast.clone());
641            // Merge interfaces: Shape exports override native ones
642            for (name, sym) in shape_interface.exports {
643                node.interface.exports.insert(name, sym);
644            }
645            // Hybrid modules need their Shape source dependencies walked
646            builder.visited.remove(&ext.name);
647        }
648    }
649
650    // Step 2: Create root node.
651    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    // Step 3: Walk imports recursively.
660    // Collect the root's direct imports plus prelude imports.
661    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    // Step 4: Resolve imports per node (build ResolvedImport entries).
678    // Must be done after all nodes are created so dependency lookups work.
679    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            // Also set dependencies from resolved imports
687            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    // Step 5: Add prelude as synthetic low-priority imports to each module
700    // node that doesn't already have explicit imports for them.
701    for i in 0..node_count {
702        let node_path = builder.nodes[i].canonical_path.clone();
703        // Skip prelude modules themselves to avoid circular dependencies
704        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                // Namespace import: skip only if there is already an explicit
716                // Namespace import for this path. A Named import from the same
717                // module is not conflicting — it provides bare names while the
718                // namespace provides qualified access.
719                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                // Named import: per-symbol merge.
742                // Collect which local names already exist from explicit Named imports
743                // on this node for this module path.
744                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                // If already imported as namespace, the symbols are accessible
761                // via qualified names — but we still add named imports so bare
762                // names resolve. Only skip symbols whose bare name already exists.
763
764                let dep_node = &builder.nodes[dep_id.0 as usize];
765                let mut symbols = Vec::new();
766                for sym in &pi.named_symbols {
767                    // Skip if this specific name is already imported explicitly
768                    if existing_names.contains(&sym.name) {
769                        continue;
770                    }
771                    // Resolve the export kind from the dependency's interface
772                    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                    // Check if we already have a Named import for this path to merge into
789                    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                        // Merge symbols into existing Named import
799                        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                // Don't add a namespace binding for Named prelude imports.
818                // A namespace binding with the last segment (e.g., "snapshot" from
819                // "std::core::snapshot") would shadow the bare named symbol when
820                // module_binding_name resolution runs before find_function.
821            }
822
823            if !builder.nodes[i].dependencies.contains(&dep_id) {
824                builder.nodes[i].dependencies.push(dep_id);
825            }
826        }
827    }
828
829    // Step 6: Build final graph with topological order.
830    Ok(builder.build(root_id))
831}
832
833/// Recursively visit a module's dependencies and add them to the graph.
834fn 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        // Already fully processed?
850        if builder.is_visited(dep_path) {
851            continue;
852        }
853
854        // Already has a node (pre-registered native module)?
855        if builder.path_to_id.contains_key(dep_path.as_str()) && builder.is_visited(dep_path) {
856            continue;
857        }
858
859        // Classify the module
860        let kind_hint = resolve_module_source_kind(loader, dep_path);
861
862        match kind_hint {
863            ModuleSourceKindHint::NativeExtension => {
864                // Should have been caught in pre-registration step.
865                // If not, it's an extension module registered via loader
866                // but not in the extensions list. Create a native node.
867                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                // Load Shape source
881                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                // Check if this is also a native extension (Hybrid)
894                let is_native = extensions.iter().any(|e| e.name == *dep_path);
895                if is_native {
896                    node.source_kind = ModuleSourceKind::Hybrid;
897                    // Build merged interface
898                    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                    // Shape exports take priority
902                    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                // Recurse into this module's dependencies
913                let mut sub_deps = extract_import_paths(&module.ast);
914                // Also add prelude imports, but skip for prelude modules
915                // themselves to avoid circular dependencies among them.
916                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                // Module not found — might be a prelude module or just missing.
940                // We skip silently here; the compiler will emit proper errors.
941                // For prelude modules that don't resolve, this is expected
942                // (e.g., they may be virtual stdlib modules handled at compile time).
943            }
944        }
945    }
946
947    builder.end_visit(current_path);
948    Ok(())
949}
950
951/// A single symbol imported by the prelude.
952#[derive(Debug, Clone)]
953pub struct PreludeNamedSymbol {
954    pub name: String,
955    pub is_annotation: bool,
956}
957
958/// A prelude import preserving the named/namespace structure from prelude.shape.
959#[derive(Debug, Clone)]
960pub struct PreludeImport {
961    pub canonical_path: String,
962    pub named_symbols: Vec<PreludeNamedSymbol>,
963    pub is_namespace: bool,
964}
965
966/// Collect structured prelude imports by loading `std::core::prelude`.
967///
968/// Preserves the named/namespace import structure so the graph builder
969/// can generate appropriate `ResolvedImport::Named` entries (not just
970/// `ResolvedImport::Namespace` for everything).
971pub fn collect_prelude_imports(
972    loader: &mut shape_runtime::module_loader::ModuleLoader,
973) -> Vec<PreludeImport> {
974    // W6 imprecision 83 follow-up: previously this swallowed prelude load
975    // failures silently, which turned a parse error in any prelude-dep
976    // (e.g. `std::core::add` with a return-typeless trait method) into a
977    // mystery Vec.map SURFACE at the user program. The W6 grammar fix
978    // (return-typeless trait methods now parse) removes the proximate
979    // trigger, but we still surface load failures here loud-but-recovery
980    // so the failure mode is debuggable. Returning an empty Vec keeps the
981    // caller signatures stable; future work may propagate the Result up.
982    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            // Check for duplicate module path
997            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
1032/// Collect prelude import paths by loading `std::core::prelude`.
1033///
1034/// Returns the list of module paths that the prelude imports,
1035/// which should be added as synthetic imports to each module.
1036/// This is a thin wrapper over `collect_prelude_imports` for backward compatibility.
1037pub 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// ---------------------------------------------------------------------------
1047// Tests
1048// ---------------------------------------------------------------------------
1049
1050#[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        // a depends on b, b depends on c
1062        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        // Topo order: c, b (root excluded)
1070        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")); // cycle!
1088    }
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        // root -> a, root -> b, a -> c, b -> c
1114        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        // c must come before a and b
1122        let order = graph.topo_order();
1123        assert_eq!(order.len(), 3); // root excluded
1124        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}