use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use shape_ast::ast::FunctionDef;
use shape_ast::module_utils::ModuleExportKind;
use shape_ast::Program;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModuleId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleSourceKind {
ShapeSource,
NativeModule,
Hybrid,
CompiledBytecode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleExportVisibility {
Public,
ComptimeOnly,
}
#[derive(Debug, Clone)]
pub struct ExportedSymbol {
pub kind: ModuleExportKind,
pub function_def: Option<Arc<FunctionDef>>,
pub visibility: ModuleExportVisibility,
}
#[derive(Debug, Clone, Default)]
pub struct ModuleInterface {
pub exports: HashMap<String, ExportedSymbol>,
}
#[derive(Debug, Clone)]
pub struct NamedImportSymbol {
pub original_name: String,
pub local_name: String,
pub is_annotation: bool,
pub kind: ModuleExportKind,
}
#[derive(Debug, Clone)]
pub enum ResolvedImport {
Namespace {
local_name: String,
canonical_path: String,
module_id: ModuleId,
},
Named {
canonical_path: String,
module_id: ModuleId,
symbols: Vec<NamedImportSymbol>,
},
}
#[derive(Debug, Clone)]
pub struct ModuleNode {
pub id: ModuleId,
pub canonical_path: String,
pub source_kind: ModuleSourceKind,
pub ast: Option<Program>,
pub interface: ModuleInterface,
pub resolved_imports: Vec<ResolvedImport>,
pub dependencies: Vec<ModuleId>,
}
#[derive(Debug, Clone)]
pub struct ModuleGraph {
nodes: Vec<ModuleNode>,
path_to_id: HashMap<String, ModuleId>,
topo_order: Vec<ModuleId>,
root_id: ModuleId,
}
impl ModuleGraph {
pub fn new(
nodes: Vec<ModuleNode>,
path_to_id: HashMap<String, ModuleId>,
topo_order: Vec<ModuleId>,
root_id: ModuleId,
) -> Self {
Self {
nodes,
path_to_id,
topo_order,
root_id,
}
}
pub fn id_for_path(&self, path: &str) -> Option<ModuleId> {
self.path_to_id.get(path).copied()
}
pub fn node(&self, id: ModuleId) -> &ModuleNode {
&self.nodes[id.0 as usize]
}
pub fn node_mut(&mut self, id: ModuleId) -> &mut ModuleNode {
&mut self.nodes[id.0 as usize]
}
pub fn topo_order(&self) -> &[ModuleId] {
&self.topo_order
}
pub fn root_id(&self) -> ModuleId {
self.root_id
}
pub fn nodes(&self) -> &[ModuleNode] {
&self.nodes
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn contains(&self, path: &str) -> bool {
self.path_to_id.contains_key(path)
}
}
#[derive(Debug, Clone)]
pub enum GraphBuildError {
CyclicDependency {
cycle: Vec<String>,
},
CompiledBytecodeNotSupported {
module_path: String,
},
ModuleNotFound {
module_path: String,
requested_by: String,
},
Other {
message: String,
},
}
impl std::fmt::Display for GraphBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphBuildError::CyclicDependency { cycle } => {
write!(
f,
"Circular dependency detected: {}",
cycle.join(" → ")
)
}
GraphBuildError::CompiledBytecodeNotSupported { module_path } => {
write!(
f,
"Module '{}' is only available as pre-compiled bytecode. \
Graph-mode compilation requires source modules. Use \
`shape bundle --include-source` to include source in the \
package, or compile the dependency from source.",
module_path
)
}
GraphBuildError::ModuleNotFound {
module_path,
requested_by,
} => {
write!(
f,
"Module '{}' not found (imported by '{}')",
module_path, requested_by
)
}
GraphBuildError::Other { message } => write!(f, "{}", message),
}
}
}
impl std::error::Error for GraphBuildError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleSourceKindHint {
NativeExtension,
ShapeSource,
EmbeddedStdlib,
CompiledBundle,
NotFound,
}
pub fn resolve_module_source_kind(
loader: &shape_runtime::module_loader::ModuleLoader,
module_path: &str,
) -> ModuleSourceKindHint {
if loader.has_extension_module(module_path) {
return ModuleSourceKindHint::NativeExtension;
}
if loader.embedded_stdlib_module_paths().contains(&module_path.to_string()) {
return ModuleSourceKindHint::EmbeddedStdlib;
}
if loader.resolve_module_path(module_path).is_ok() {
return ModuleSourceKindHint::ShapeSource;
}
ModuleSourceKindHint::NotFound
}
pub struct GraphBuilder {
nodes: Vec<ModuleNode>,
path_to_id: HashMap<String, ModuleId>,
visiting: HashSet<String>,
visited: HashSet<String>,
}
impl GraphBuilder {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
path_to_id: HashMap::new(),
visiting: HashSet::new(),
visited: HashSet::new(),
}
}
pub fn get_or_create_node(&mut self, canonical_path: &str) -> ModuleId {
if let Some(&id) = self.path_to_id.get(canonical_path) {
return id;
}
let id = ModuleId(self.nodes.len() as u32);
self.nodes.push(ModuleNode {
id,
canonical_path: canonical_path.to_string(),
source_kind: ModuleSourceKind::ShapeSource, ast: None,
interface: ModuleInterface::default(),
resolved_imports: Vec::new(),
dependencies: Vec::new(),
});
self.path_to_id.insert(canonical_path.to_string(), id);
id
}
pub fn begin_visit(&mut self, canonical_path: &str) -> bool {
self.visiting.insert(canonical_path.to_string())
}
pub fn end_visit(&mut self, canonical_path: &str) {
self.visiting.remove(canonical_path);
self.visited.insert(canonical_path.to_string());
}
pub fn is_visited(&self, canonical_path: &str) -> bool {
self.visited.contains(canonical_path)
}
pub fn is_visiting(&self, canonical_path: &str) -> bool {
self.visiting.contains(canonical_path)
}
pub fn get_cycle_path(&self, target: &str) -> Vec<String> {
let mut cycle: Vec<String> = self.visiting.iter().cloned().collect();
cycle.push(target.to_string());
cycle
}
pub fn compute_topo_order(&self, root_id: ModuleId) -> Vec<ModuleId> {
let mut order = Vec::new();
let mut visited = HashSet::new();
for node in &self.nodes {
self.topo_dfs(node.id, root_id, &mut visited, &mut order);
}
order
}
fn topo_dfs(
&self,
current: ModuleId,
root_id: ModuleId,
visited: &mut HashSet<ModuleId>,
order: &mut Vec<ModuleId>,
) {
if !visited.insert(current) {
return;
}
let node = &self.nodes[current.0 as usize];
for &dep in &node.dependencies {
self.topo_dfs(dep, root_id, visited, order);
}
if current != root_id {
order.push(current);
}
}
pub fn build(self, root_id: ModuleId) -> ModuleGraph {
let topo_order = self.compute_topo_order(root_id);
ModuleGraph::new(self.nodes, self.path_to_id, topo_order, root_id)
}
}
impl Default for GraphBuilder {
fn default() -> Self {
Self::new()
}
}
fn extract_import_paths(ast: &Program) -> Vec<String> {
ast.items
.iter()
.filter_map(|item| {
if let shape_ast::ast::Item::Import(import_stmt, _) = item {
Some(import_stmt.from.clone())
} else {
None
}
})
.collect()
}
fn build_shape_interface(ast: &Program) -> ModuleInterface {
let symbols = match shape_ast::module_utils::collect_exported_symbols(ast) {
Ok(syms) => syms,
Err(_) => return ModuleInterface::default(),
};
let mut exports = HashMap::new();
for sym in symbols {
let name = sym.alias.unwrap_or(sym.name);
exports.insert(
name,
ExportedSymbol {
kind: sym.kind,
function_def: None,
visibility: ModuleExportVisibility::Public,
},
);
}
ModuleInterface { exports }
}
fn build_native_interface(
module: &shape_runtime::module_exports::ModuleExports,
) -> ModuleInterface {
let mut exports = HashMap::new();
for name in module.export_names() {
let visibility = match module.export_visibility(name) {
shape_runtime::module_exports::ModuleExportVisibility::Public => {
ModuleExportVisibility::Public
}
shape_runtime::module_exports::ModuleExportVisibility::ComptimeOnly => {
ModuleExportVisibility::ComptimeOnly
}
shape_runtime::module_exports::ModuleExportVisibility::Internal => {
ModuleExportVisibility::Public
}
};
exports.insert(
name.to_string(),
ExportedSymbol {
kind: ModuleExportKind::Function,
function_def: None,
visibility,
},
);
}
ModuleInterface { exports }
}
fn resolve_imports_for_node(
ast: &Program,
builder: &GraphBuilder,
) -> Vec<ResolvedImport> {
let mut resolved = Vec::new();
for item in &ast.items {
let shape_ast::ast::Item::Import(import_stmt, _) = item else {
continue;
};
let module_path = &import_stmt.from;
let Some(&dep_id) = builder.path_to_id.get(module_path) else {
continue;
};
let dep_node = &builder.nodes[dep_id.0 as usize];
match &import_stmt.items {
shape_ast::ast::ImportItems::Namespace { name, alias } => {
let local_name = alias
.as_ref()
.or(Some(name))
.cloned()
.unwrap_or_else(|| {
module_path
.split("::")
.last()
.unwrap_or(module_path)
.to_string()
});
resolved.push(ResolvedImport::Namespace {
local_name,
canonical_path: module_path.clone(),
module_id: dep_id,
});
}
shape_ast::ast::ImportItems::Named(specs) => {
let mut symbols = Vec::new();
for spec in specs {
let kind = dep_node
.interface
.exports
.get(&spec.name)
.map(|e| e.kind)
.unwrap_or(ModuleExportKind::Function);
symbols.push(NamedImportSymbol {
original_name: spec.name.clone(),
local_name: spec.alias.clone().unwrap_or_else(|| spec.name.clone()),
is_annotation: spec.is_annotation,
kind,
});
}
resolved.push(ResolvedImport::Named {
canonical_path: module_path.clone(),
module_id: dep_id,
symbols,
});
}
}
}
resolved
}
pub fn build_module_graph(
root_program: &Program,
loader: &mut shape_runtime::module_loader::ModuleLoader,
extensions: &[shape_runtime::module_exports::ModuleExports],
prelude_imports: &[String],
) -> Result<ModuleGraph, GraphBuildError> {
let structured = collect_prelude_imports(loader);
build_module_graph_with_prelude_structure(
root_program,
loader,
extensions,
prelude_imports,
&structured,
)
}
fn build_module_graph_with_prelude_structure(
root_program: &Program,
loader: &mut shape_runtime::module_loader::ModuleLoader,
extensions: &[shape_runtime::module_exports::ModuleExports],
prelude_imports: &[String],
structured_prelude: &[PreludeImport],
) -> Result<ModuleGraph, GraphBuildError> {
let mut builder = GraphBuilder::new();
for ext in extensions {
let ext_id = builder.get_or_create_node(&ext.name);
let node = &mut builder.nodes[ext_id.0 as usize];
node.source_kind = ModuleSourceKind::NativeModule;
node.interface = build_native_interface(ext);
builder.visited.insert(ext.name.clone());
if let Ok(module) = loader.load_module(&ext.name) {
let shape_interface = build_shape_interface(&module.ast);
let node = &mut builder.nodes[ext_id.0 as usize];
node.source_kind = ModuleSourceKind::Hybrid;
node.ast = Some(module.ast.clone());
for (name, sym) in shape_interface.exports {
node.interface.exports.insert(name, sym);
}
builder.visited.remove(&ext.name);
}
}
let root_id = builder.get_or_create_node("__root__");
{
let node = &mut builder.nodes[root_id.0 as usize];
node.source_kind = ModuleSourceKind::ShapeSource;
node.ast = Some(root_program.clone());
node.interface = build_shape_interface(root_program);
}
let mut root_deps = extract_import_paths(root_program);
for prelude_path in prelude_imports {
if !root_deps.contains(prelude_path) {
root_deps.push(prelude_path.clone());
}
}
visit_module(
"__root__",
&root_deps,
&mut builder,
loader,
extensions,
prelude_imports,
)?;
let node_count = builder.nodes.len();
for i in 0..node_count {
let ast = builder.nodes[i].ast.clone();
if let Some(ast) = &ast {
let resolved = resolve_imports_for_node(ast, &builder);
builder.nodes[i].resolved_imports = resolved;
let deps: Vec<ModuleId> = builder.nodes[i]
.resolved_imports
.iter()
.map(|ri| match ri {
ResolvedImport::Namespace { module_id, .. } => *module_id,
ResolvedImport::Named { module_id, .. } => *module_id,
})
.collect();
builder.nodes[i].dependencies = deps;
}
}
for i in 0..node_count {
let node_path = builder.nodes[i].canonical_path.clone();
if node_path.starts_with("std::core::prelude")
|| prelude_imports.contains(&node_path)
{
continue;
}
for pi in structured_prelude {
let Some(&dep_id) = builder.path_to_id.get(pi.canonical_path.as_str()) else {
continue;
};
if pi.is_namespace || pi.named_symbols.is_empty() {
let has_namespace_import = builder.nodes[i].resolved_imports.iter().any(|ri| {
matches!(ri, ResolvedImport::Namespace { canonical_path, .. }
if canonical_path == &pi.canonical_path)
});
if has_namespace_import {
continue;
}
let local_name = pi
.canonical_path
.split("::")
.last()
.unwrap_or(&pi.canonical_path)
.to_string();
builder.nodes[i]
.resolved_imports
.push(ResolvedImport::Namespace {
local_name,
canonical_path: pi.canonical_path.clone(),
module_id: dep_id,
});
} else {
let existing_names: HashSet<String> = builder.nodes[i]
.resolved_imports
.iter()
.filter_map(|ri| match ri {
ResolvedImport::Named {
canonical_path,
symbols,
..
} if canonical_path == &pi.canonical_path => {
Some(symbols.iter().map(|s| s.local_name.clone()))
}
_ => None,
})
.flatten()
.collect();
let dep_node = &builder.nodes[dep_id.0 as usize];
let mut symbols = Vec::new();
for sym in &pi.named_symbols {
if existing_names.contains(&sym.name) {
continue;
}
let kind = dep_node
.interface
.exports
.get(&sym.name)
.map(|e| e.kind)
.unwrap_or(ModuleExportKind::Function);
symbols.push(NamedImportSymbol {
original_name: sym.name.clone(),
local_name: sym.name.clone(),
is_annotation: sym.is_annotation,
kind,
});
}
if !symbols.is_empty() {
let existing_named_idx = builder.nodes[i]
.resolved_imports
.iter()
.position(|ri| matches!(ri,
ResolvedImport::Named { canonical_path, .. }
if canonical_path == &pi.canonical_path
));
if let Some(idx) = existing_named_idx {
if let ResolvedImport::Named {
symbols: ref mut existing_symbols,
..
} = builder.nodes[i].resolved_imports[idx]
{
existing_symbols.extend(symbols);
}
} else {
builder.nodes[i]
.resolved_imports
.push(ResolvedImport::Named {
canonical_path: pi.canonical_path.clone(),
module_id: dep_id,
symbols,
});
}
}
}
if !builder.nodes[i].dependencies.contains(&dep_id) {
builder.nodes[i].dependencies.push(dep_id);
}
}
}
Ok(builder.build(root_id))
}
fn visit_module(
current_path: &str,
dep_paths: &[String],
builder: &mut GraphBuilder,
loader: &mut shape_runtime::module_loader::ModuleLoader,
extensions: &[shape_runtime::module_exports::ModuleExports],
prelude_imports: &[String],
) -> Result<(), GraphBuildError> {
if !builder.begin_visit(current_path) {
return Err(GraphBuildError::CyclicDependency {
cycle: builder.get_cycle_path(current_path),
});
}
for dep_path in dep_paths {
if builder.is_visited(dep_path) {
continue;
}
if builder.path_to_id.contains_key(dep_path.as_str()) && builder.is_visited(dep_path) {
continue;
}
let kind_hint = resolve_module_source_kind(loader, dep_path);
match kind_hint {
ModuleSourceKindHint::NativeExtension => {
if !builder.path_to_id.contains_key(dep_path.as_str()) {
let ext = extensions.iter().find(|e| e.name == *dep_path);
let dep_id = builder.get_or_create_node(dep_path);
let node = &mut builder.nodes[dep_id.0 as usize];
node.source_kind = ModuleSourceKind::NativeModule;
if let Some(ext) = ext {
node.interface = build_native_interface(ext);
}
builder.visited.insert(dep_path.clone());
}
}
ModuleSourceKindHint::ShapeSource
| ModuleSourceKindHint::EmbeddedStdlib => {
let module = loader
.load_module(dep_path)
.map_err(|e| GraphBuildError::Other {
message: format!(
"Failed to load module '{}': {}",
dep_path, e
),
})?;
let dep_id = builder.get_or_create_node(dep_path);
let node = &mut builder.nodes[dep_id.0 as usize];
let is_native = extensions.iter().any(|e| e.name == *dep_path);
if is_native {
node.source_kind = ModuleSourceKind::Hybrid;
let shape_iface = build_shape_interface(&module.ast);
let native_ext = extensions.iter().find(|e| e.name == *dep_path).unwrap();
let mut native_iface = build_native_interface(native_ext);
for (name, sym) in shape_iface.exports {
native_iface.exports.insert(name, sym);
}
node.interface = native_iface;
} else {
node.source_kind = ModuleSourceKind::ShapeSource;
node.interface = build_shape_interface(&module.ast);
}
node.ast = Some(module.ast.clone());
let mut sub_deps = extract_import_paths(&module.ast);
if !prelude_imports.contains(dep_path) {
for pp in prelude_imports {
if !sub_deps.contains(pp) {
sub_deps.push(pp.clone());
}
}
}
visit_module(
dep_path,
&sub_deps,
builder,
loader,
extensions,
prelude_imports,
)?;
}
ModuleSourceKindHint::CompiledBundle => {
return Err(GraphBuildError::CompiledBytecodeNotSupported {
module_path: dep_path.clone(),
});
}
ModuleSourceKindHint::NotFound => {
}
}
}
builder.end_visit(current_path);
Ok(())
}
#[derive(Debug, Clone)]
pub struct PreludeNamedSymbol {
pub name: String,
pub is_annotation: bool,
}
#[derive(Debug, Clone)]
pub struct PreludeImport {
pub canonical_path: String,
pub named_symbols: Vec<PreludeNamedSymbol>,
pub is_namespace: bool,
}
pub fn collect_prelude_imports(
loader: &mut shape_runtime::module_loader::ModuleLoader,
) -> Vec<PreludeImport> {
let prelude = match loader.load_module("std::core::prelude") {
Ok(m) => m,
Err(e) => {
eprintln!(
"warning: failed to load std::core::prelude — prelude items will be unavailable: {}",
e
);
return Vec::new();
}
};
let mut imports = Vec::new();
for item in &prelude.ast.items {
if let shape_ast::ast::Item::Import(import_stmt, _) = item {
if imports
.iter()
.any(|i: &PreludeImport| i.canonical_path == import_stmt.from)
{
continue;
}
match &import_stmt.items {
shape_ast::ast::ImportItems::Named(specs) => {
let symbols = specs
.iter()
.map(|spec| PreludeNamedSymbol {
name: spec.name.clone(),
is_annotation: spec.is_annotation,
})
.collect();
imports.push(PreludeImport {
canonical_path: import_stmt.from.clone(),
named_symbols: symbols,
is_namespace: false,
});
}
shape_ast::ast::ImportItems::Namespace { .. } => {
imports.push(PreludeImport {
canonical_path: import_stmt.from.clone(),
named_symbols: Vec::new(),
is_namespace: true,
});
}
}
}
}
imports
}
pub fn collect_prelude_import_paths(
loader: &mut shape_runtime::module_loader::ModuleLoader,
) -> Vec<String> {
collect_prelude_imports(loader)
.into_iter()
.map(|pi| pi.canonical_path)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_builder_basic() {
let mut builder = GraphBuilder::new();
let a = builder.get_or_create_node("a");
let b = builder.get_or_create_node("b");
let c = builder.get_or_create_node("c");
builder.nodes[a.0 as usize].dependencies.push(b);
builder.nodes[b.0 as usize].dependencies.push(c);
let graph = builder.build(a);
assert_eq!(graph.len(), 3);
assert_eq!(graph.root_id(), a);
assert_eq!(graph.topo_order(), &[c, b]);
}
#[test]
fn test_graph_builder_dedup() {
let mut builder = GraphBuilder::new();
let id1 = builder.get_or_create_node("std::core::math");
let id2 = builder.get_or_create_node("std::core::math");
assert_eq!(id1, id2);
}
#[test]
fn test_cycle_detection() {
let mut builder = GraphBuilder::new();
assert!(builder.begin_visit("a"));
assert!(builder.begin_visit("b"));
assert!(builder.is_visiting("a"));
assert!(!builder.begin_visit("a")); }
#[test]
fn test_graph_lookup() {
let mut builder = GraphBuilder::new();
let math_id = builder.get_or_create_node("std::core::math");
builder.nodes[math_id.0 as usize].source_kind = ModuleSourceKind::NativeModule;
let graph = builder.build(math_id);
assert_eq!(graph.id_for_path("std::core::math"), Some(math_id));
assert_eq!(graph.id_for_path("nonexistent"), None);
assert_eq!(
graph.node(math_id).source_kind,
ModuleSourceKind::NativeModule
);
}
#[test]
fn test_diamond_dependency() {
let mut builder = GraphBuilder::new();
let root = builder.get_or_create_node("root");
let a = builder.get_or_create_node("a");
let b = builder.get_or_create_node("b");
let c = builder.get_or_create_node("c");
builder.nodes[root.0 as usize].dependencies.push(a);
builder.nodes[root.0 as usize].dependencies.push(b);
builder.nodes[a.0 as usize].dependencies.push(c);
builder.nodes[b.0 as usize].dependencies.push(c);
let graph = builder.build(root);
let order = graph.topo_order();
assert_eq!(order.len(), 3); let c_pos = order.iter().position(|&id| id == c).unwrap();
let a_pos = order.iter().position(|&id| id == a).unwrap();
let b_pos = order.iter().position(|&id| id == b).unwrap();
assert!(c_pos < a_pos);
assert!(c_pos < b_pos);
}
}