use std::collections::BTreeMap;
use super::*;
pub(crate) struct ModuleResolver {
pub(crate) root_dir: PathBuf,
pub(crate) stdlib_dir: Option<PathBuf>,
pub(crate) os_dir: Option<PathBuf>,
pub(crate) dep_dirs: Vec<PathBuf>,
pub(crate) modules: BTreeMap<String, ModuleInfo>,
queue: Vec<String>,
diagnostics: Vec<Diagnostic>,
}
impl ModuleResolver {
pub(crate) fn new(entry_path: &Path) -> Result<Self, Vec<Diagnostic>> {
let root_dir = entry_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let source = std::fs::read_to_string(entry_path).map_err(|e| {
vec![Diagnostic::error(
format!("cannot read '{}': {}", entry_path.display(), e),
Span::dummy(),
)
.with_help("check that the file exists and is readable".to_string())]
})?;
let (name, deps) = scan_module_header(&source);
let entry_name = name.unwrap_or_else(|| "main".to_string());
let info = ModuleInfo {
name: entry_name.clone(),
file_path: entry_path.to_path_buf(),
source,
dependencies: deps.clone(),
};
let mut modules = BTreeMap::new();
modules.insert(entry_name.clone(), info);
Ok(Self {
root_dir,
stdlib_dir: find_stdlib_dir(),
os_dir: find_os_dir(),
dep_dirs: Vec::new(),
modules,
queue: deps,
diagnostics: Vec::new(),
})
}
pub(crate) fn find_vm_dir(&self) -> Option<PathBuf> {
if let Some(ref stdlib_dir) = self.stdlib_dir {
if let Some(parent) = stdlib_dir.parent() {
let vm_dir = parent.join("vm");
if vm_dir.is_dir() {
return Some(vm_dir);
}
}
}
let cwd_vm = PathBuf::from("vm");
if cwd_vm.is_dir() {
return Some(cwd_vm);
}
None
}
pub(crate) fn discover_all(&mut self) -> Result<(), Vec<Diagnostic>> {
while let Some(module_name) = self.queue.pop() {
if self.modules.contains_key(&module_name) {
continue;
}
let file_path = self.resolve_path(&module_name);
let source = match std::fs::read_to_string(&file_path) {
Ok(s) => s,
Err(e) => {
self.diagnostics.push(
Diagnostic::error(
format!(
"cannot find module '{}' (looked at '{}'): {}",
module_name,
file_path.display(),
e
),
Span::dummy(),
)
.with_help(format!(
"create the file '{}' or check the module name in the `use` statement",
file_path.display()
)),
);
continue;
}
};
let (_name, deps) = scan_module_header(&source);
for dep in &deps {
if !self.modules.contains_key(dep) {
self.queue.push(dep.clone());
}
}
self.modules.insert(
module_name.clone(),
ModuleInfo {
name: module_name,
file_path,
source,
dependencies: deps,
},
);
}
if self.diagnostics.is_empty() {
Ok(())
} else {
Err(self.diagnostics.clone())
}
}
pub(crate) fn resolve_path(&self, module_name: &str) -> PathBuf {
let raw_parts: Vec<&str> = module_name.split('.').collect();
for part in &raw_parts {
if part.is_empty()
|| *part == ".."
|| part.starts_with('.')
|| part.contains('/')
|| part.contains('\\')
{
return self.root_dir.join("<invalid-module-name>");
}
}
if let Some(rest) = module_name.strip_prefix("os.") {
if let Some(ref os_dir) = self.os_dir {
let parts: Vec<&str> = rest.split('.').collect();
if parts.len() >= 2 {
let os_name = parts[0];
let target_dir = os_dir.join(os_name);
if target_dir.is_dir() {
let mut path = target_dir;
for part in &parts[1..] {
path = path.join(part);
}
return path.with_extension("tri");
}
}
}
}
if let Some(rest) = module_name.strip_prefix("vm.") {
if let Some(ref vm_dir) = self.find_vm_dir() {
let parts: Vec<&str> = rest.split('.').collect();
let mut path = vm_dir.clone();
for part in &parts {
path = path.join(part);
}
return path.with_extension("tri");
}
}
if let Some(ext_pos) = raw_parts.iter().position(|&p| p == "ext") {
if ext_pos > 0 && ext_pos + 1 < raw_parts.len() {
if let Some(ref os_dir) = self.os_dir {
let os_name = &raw_parts[..ext_pos].join("/");
let rest = &raw_parts[ext_pos + 1..];
let mut path = os_dir.join(os_name);
for part in rest {
path = path.join(part);
}
return path.with_extension("tri");
}
}
}
if let Some(rest) = module_name.strip_prefix("ext.") {
if let Some(ref os_dir) = self.os_dir {
let parts: Vec<&str> = rest.split('.').collect();
let mut path = os_dir.clone();
for part in &parts {
path = path.join(part);
}
return path.with_extension("tri");
}
}
if let Some(rest) = module_name.strip_prefix("std.") {
if let Some(ref stdlib_dir) = self.stdlib_dir {
let parts: Vec<&str> = rest.split('.').collect();
let mut path = stdlib_dir.clone();
for part in &parts {
path = path.join(part);
}
let candidate = path.with_extension("tri");
if candidate.exists() {
return candidate;
}
if let Some(new_name) = legacy_stdlib_fallback(module_name) {
return self.resolve_path(new_name);
}
return candidate;
}
}
for dep_dir in &self.dep_dirs {
let parts: Vec<&str> = module_name.split('.').collect();
let mut path = dep_dir.clone();
for part in &parts {
path = path.join(part);
}
let candidate = path.with_extension("tri");
if candidate.exists() {
return candidate;
}
if path.is_dir() {
let main_tri = path.join("main.tri");
if main_tri.exists() {
return main_tri;
}
}
}
let parts: Vec<&str> = module_name.split('.').collect();
let mut path = self.root_dir.clone();
for part in &parts {
path = path.join(part);
}
path.with_extension("tri")
}
pub(crate) fn topological_sort(&self) -> Result<Vec<ModuleInfo>, Vec<Diagnostic>> {
let mut visited: BTreeSet<String> = BTreeSet::new();
let mut in_progress: BTreeSet<String> = BTreeSet::new();
let mut order: Vec<String> = Vec::new();
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for name in self.modules.keys() {
if !visited.contains(name) {
self.dfs(
name,
&mut visited,
&mut in_progress,
&mut order,
&mut diagnostics,
);
}
}
if !diagnostics.is_empty() {
return Err(diagnostics);
}
let result: Vec<ModuleInfo> = order
.into_iter()
.filter_map(|name| self.modules.get(&name).cloned())
.collect();
Ok(result)
}
fn dfs(
&self,
name: &str,
visited: &mut BTreeSet<String>,
in_progress: &mut BTreeSet<String>,
order: &mut Vec<String>,
diagnostics: &mut Vec<Diagnostic>,
) {
if visited.contains(name) {
return;
}
if in_progress.contains(name) {
diagnostics.push(
Diagnostic::error(
format!("circular dependency detected involving module '{}'", name),
Span::dummy(),
)
.with_help(
"break the cycle by extracting shared definitions into a separate module"
.to_string(),
),
);
return;
}
in_progress.insert(name.to_string());
if let Some(info) = self.modules.get(name) {
for dep in &info.dependencies {
self.dfs(dep, visited, in_progress, order, diagnostics);
}
}
in_progress.remove(name);
visited.insert(name.to_string());
order.push(name.to_string());
}
}
pub(crate) fn scan_module_header(source: &str) -> (Option<String>, Vec<String>) {
let mut name = None;
let mut deps = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") {
continue;
}
if let Some(rest) = trimmed.strip_prefix("program ") {
name = Some(rest.trim().to_string());
} else if let Some(rest) = trimmed.strip_prefix("module ") {
name = Some(rest.trim().to_string());
} else if let Some(rest) = trimmed.strip_prefix("use ") {
let dep = rest.trim().to_string();
deps.push(dep);
} else {
if trimmed.starts_with("fn ")
|| trimmed.starts_with("pub ")
|| trimmed.starts_with("const ")
|| trimmed.starts_with("struct ")
{
break;
}
}
}
(name, deps)
}