use crate::manifest::ResolvedDeps;
use crate::parser::ast::{ImportStatement, Program, Statement};
use std::path::{Path, PathBuf};
use thiserror::Error;
pub fn package_import_names_from_program(program: &Program) -> Vec<String> {
let mut out = Vec::new();
for stmt in &program.statements {
if let Statement::Import(imp) = stmt {
let p = imp.path.trim();
if p.is_empty() {
continue;
}
if p.starts_with("stdlib::")
|| p.starts_with("./")
|| p.starts_with("../")
|| p.contains('/')
|| p.ends_with(".dal")
{
continue;
}
if KNOWN_STDLIB.contains(&p) {
continue;
}
out.push(p.to_string());
}
}
out
}
const KNOWN_STDLIB: &[&str] = &[
"add_sol",
"admin",
"agent",
"ai",
"assist", "aml",
"auth",
"chain",
"cloudadmin",
"config",
"evolve",
"fs",
"cross_chain_security",
"crypto",
"crypto_signatures",
"database",
"desktop",
"iot",
"key",
"kyc",
"log",
"mobile",
"mold",
"oracle",
"rag",
"secure_auth",
"service",
"sh",
"sync",
"test",
"trust",
"web",
];
#[derive(Debug, Clone, PartialEq)]
pub enum ResolvedImport {
Stdlib(String),
RelativeFile(PathBuf),
Package { name: String, path: PathBuf },
}
#[derive(Debug, Error)]
pub enum ResolveError {
#[error("unknown module: {0}")]
UnknownModule(String),
#[error("file not found: {0}")]
FileNotFound(PathBuf),
#[error("import cycle detected: {0}")]
CycleDetected(String),
#[error("relative import requires an entry path")]
RelativeWithoutEntryPath,
#[error("invalid path: {0}")]
InvalidPath(String),
#[error("package resolution not available (M3): {0}")]
PackageNotAvailable(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error in {path}: {message}")]
ParseError { path: PathBuf, message: String },
}
#[derive(Debug, Clone)]
pub struct ResolvedImportEntry {
pub import: ImportStatement,
pub resolved: ResolvedImport,
}
#[derive(Debug, Default)]
pub struct ModuleResolver {
pub root_dir: Option<PathBuf>,
pub dependencies: Option<ResolvedDeps>,
}
impl ModuleResolver {
pub fn new() -> Self {
Self::default()
}
pub fn with_root_dir(mut self, root: PathBuf) -> Self {
self.root_dir = Some(root);
self
}
pub fn with_dependencies(mut self, deps: ResolvedDeps) -> Self {
self.dependencies = Some(deps);
self
}
pub fn resolve(
&self,
import_path: &str,
current_file_dir: Option<&Path>,
) -> Result<ResolvedImport, ResolveError> {
let path = import_path.trim();
if path.starts_with("stdlib::") {
let name = path.strip_prefix("stdlib::").unwrap().trim();
if name.is_empty() {
return Err(ResolveError::UnknownModule(import_path.to_string()));
}
let namespace = name.split("::").next().unwrap_or(name);
if KNOWN_STDLIB.contains(&namespace) {
return Ok(ResolvedImport::Stdlib(namespace.to_string()));
}
return Err(ResolveError::UnknownModule(format!(
"unknown stdlib namespace '{}' (known: {})",
namespace,
KNOWN_STDLIB[..KNOWN_STDLIB.len().min(5)].join(", ")
)));
}
if path.starts_with("./") || path.starts_with("../") {
let base = current_file_dir
.or(self.root_dir.as_deref())
.ok_or(ResolveError::RelativeWithoutEntryPath)?;
let joined = base.join(path);
let canonical = joined
.canonicalize()
.map_err(|_| ResolveError::FileNotFound(joined.clone()))?;
if !canonical.is_file() {
return Err(ResolveError::FileNotFound(canonical));
}
return Ok(ResolvedImport::RelativeFile(canonical));
}
if path.contains('/') || path.ends_with(".dal") {
let base = current_file_dir
.or(self.root_dir.as_deref())
.ok_or(ResolveError::RelativeWithoutEntryPath)?;
let joined = base.join(path);
let canonical = joined
.canonicalize()
.map_err(|_| ResolveError::FileNotFound(joined))?;
if !canonical.is_file() {
return Err(ResolveError::FileNotFound(canonical));
}
return Ok(ResolvedImport::RelativeFile(canonical));
}
if let Some(ref deps) = self.dependencies {
if let Some(package_root) = deps.get(path) {
return Ok(ResolvedImport::Package {
name: path.to_string(),
path: package_root.clone(),
});
}
}
Err(ResolveError::PackageNotAvailable(format!(
"package '{}' not in [dependencies]; run `dal install` or add it to dal.toml",
path
)))
}
pub fn resolve_program_imports(
&self,
program: &Program,
entry_path: Option<&Path>,
) -> Result<Vec<ResolvedImportEntry>, ResolveError> {
let current_dir = entry_path.and_then(|p| p.parent());
let mut out = Vec::new();
for stmt in &program.statements {
if let Statement::Import(import) = stmt {
let resolved = self.resolve(&import.path, current_dir)?;
out.push(ResolvedImportEntry {
import: import.clone(),
resolved,
});
}
}
Ok(out)
}
pub fn resolve_program_with_cycles(
&self,
program: &Program,
entry_path: Option<&Path>,
parse_fn: impl Fn(&str) -> Result<Program, String>,
) -> Result<Vec<ResolvedImportEntry>, ResolveError> {
let mut stack: Vec<PathBuf> = vec![];
let mut result = Vec::new();
self.resolve_program_recursive(program, entry_path, &mut stack, &mut result, &parse_fn)?;
Ok(result)
}
fn resolve_program_recursive(
&self,
program: &Program,
current_file: Option<&Path>,
stack: &mut Vec<PathBuf>,
result: &mut Vec<ResolvedImportEntry>,
parse_fn: &impl Fn(&str) -> Result<Program, String>,
) -> Result<(), ResolveError> {
let current_dir = current_file.and_then(|p| p.parent());
for stmt in &program.statements {
if let Statement::Import(import) = stmt {
let resolved = self.resolve(&import.path, current_dir)?;
match &resolved {
ResolvedImport::RelativeFile(path) => {
let canonical = path.clone();
if stack.contains(&canonical) {
let chain: Vec<String> = stack
.iter()
.chain(std::iter::once(&canonical))
.map(|p| p.display().to_string())
.collect();
return Err(ResolveError::CycleDetected(chain.join(" -> ")));
}
let source = std::fs::read_to_string(&canonical)
.map_err(|_| ResolveError::FileNotFound(canonical.clone()))?;
let dep_program =
parse_fn(&source).map_err(|msg| ResolveError::ParseError {
path: canonical.clone(),
message: msg,
})?;
stack.push(canonical.clone());
self.resolve_program_recursive(
&dep_program,
Some(&canonical),
stack,
result,
parse_fn,
)?;
stack.pop();
}
ResolvedImport::Package {
path: package_root, ..
} => {
let entry_path = package_entry_path(package_root).ok_or_else(|| {
ResolveError::FileNotFound(package_root.join("main.dal"))
})?;
if stack.contains(&entry_path) {
let chain: Vec<String> = stack
.iter()
.chain(std::iter::once(&entry_path))
.map(|p| p.display().to_string())
.collect();
return Err(ResolveError::CycleDetected(chain.join(" -> ")));
}
let source = std::fs::read_to_string(&entry_path)
.map_err(|_| ResolveError::FileNotFound(entry_path.clone()))?;
let dep_program =
parse_fn(&source).map_err(|msg| ResolveError::ParseError {
path: entry_path.clone(),
message: msg,
})?;
stack.push(entry_path.clone());
self.resolve_program_recursive(
&dep_program,
Some(&entry_path),
stack,
result,
parse_fn,
)?;
stack.pop();
}
ResolvedImport::Stdlib(_) => {}
}
result.push(ResolvedImportEntry {
import: import.clone(),
resolved,
});
}
}
Ok(())
}
}
fn package_entry_path(package_root: &Path) -> Option<PathBuf> {
let main = package_root.join("main.dal");
let lib = package_root.join("lib.dal");
if main.exists() {
Some(main)
} else if lib.exists() {
Some(lib)
} else {
None
}
}
pub fn resolve_imports(
program: &Program,
entry_path: Option<&Path>,
) -> Result<Vec<ResolvedImportEntry>, ResolveError> {
ModuleResolver::new().resolve_program_imports(program, entry_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_stdlib() {
let r = ModuleResolver::new();
let out = r.resolve("stdlib::chain", None).unwrap();
assert!(matches!(out, ResolvedImport::Stdlib(s) if s == "chain"));
let out = r.resolve("stdlib::ai", None).unwrap();
assert!(matches!(out, ResolvedImport::Stdlib(s) if s == "ai"));
}
#[test]
fn test_resolve_stdlib_unknown() {
let r = ModuleResolver::new();
let e = r.resolve("stdlib::nonexistent", None).unwrap_err();
assert!(matches!(e, ResolveError::UnknownModule(_)));
}
#[test]
fn test_resolve_relative_without_entry() {
let r = ModuleResolver::new();
let e = r.resolve("./foo.dal", None).unwrap_err();
assert!(matches!(e, ResolveError::RelativeWithoutEntryPath));
}
#[test]
fn test_resolve_package_not_available() {
let r = ModuleResolver::new();
let e = r.resolve("some_package", None).unwrap_err();
assert!(matches!(e, ResolveError::PackageNotAvailable(_)));
}
}