use std::collections::BTreeMap;
use super::*;
pub(crate) struct ModuleResolver {
/// Root directory of the project.
pub(crate) root_dir: PathBuf,
/// Standard library directory (if found).
pub(crate) stdlib_dir: Option<PathBuf>,
/// OS library directory โ OS-specific extension code (if found).
pub(crate) os_dir: Option<PathBuf>,
/// Additional directories to search for modules (from locked dependencies).
pub(crate) dep_dirs: Vec<PathBuf>,
/// All discovered modules by name.
pub(crate) modules: BTreeMap<String, ModuleInfo>,
/// Queue of modules to process.
queue: Vec<String>,
/// Diagnostics.
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())]
})?;
// Quick-parse the entry file to get its name and dependencies
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(),
})
}
/// Find the VM intrinsic library directory.
/// Mirrors stdlib_dir search but looks for `vm/` sibling to `std/`.
pub(crate) fn find_vm_dir(&self) -> Option<PathBuf> {
// If we have a stdlib_dir, look for vm/ as a sibling
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);
}
}
}
// Fallback: vm/ in current working directory
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;
}
// Resolve module name to file path
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);
// Queue newly discovered dependencies
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())
}
}
/// Resolve a dotted module name to a file path.
///
/// Four-tier namespace:
/// "vm.core.field" โ vm_dir/core/field.tri (VM intrinsics)
/// "std.crypto.sha256" โ stdlib_dir/crypto/sha256.tri (real libraries)
/// "os.neptune.kernel" โ os_dir/neptune/kernel.tri (OS-specific)
/// "crypto.sponge" โ root_dir/crypto/sponge.tri (local)
///
/// Legacy backward compatibility still supported:
/// "neptune.ext.kernel" โ os_dir/neptune/kernel.tri
/// "ext.neptune.kernel" โ os_dir/neptune/kernel.tri
/// "std.crypto.hash" โ vm_dir/crypto/hash.tri (intrinsics moved)
/// "std.hash" โ vm_dir/crypto/hash.tri (flat โ layered โ vm)
pub(crate) fn resolve_path(&self, module_name: &str) -> PathBuf {
// Validate: reject path traversal components
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>");
}
}
// OS-specific extension modules: os.<os>.<module> โ os/<os>/<module>.tri
// Distinguishes os.neptune.kernel (extension) from os.neuron (portable).
// An OS name is recognized if os/<os_name>/ exists as a directory.
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");
}
}
}
}
// VM intrinsic modules: vm.* โ vm/<rest>.tri
if let Some(rest) = module_name.strip_prefix("vm.") {
// Look for vm/ directory using same search strategy as stdlib
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");
}
}
// Legacy: <os>.ext.<module> โ os/<os>/<module>.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");
}
}
}
// Legacy: ext.<os>.<module> โ os/<os>/<module>.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");
}
}
// Standard library modules resolve from stdlib_dir
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 the layered path exists, use it
if candidate.exists() {
return candidate;
}
// Legacy fallback: try remapped path for old flat names
if let Some(new_name) = legacy_stdlib_fallback(module_name) {
return self.resolve_path(new_name);
}
// Return the original candidate (will fail with good error)
return candidate;
}
}
// Check dependency cache directories
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;
}
// Also check for main.tri inside a directory matching the name
if path.is_dir() {
let main_tri = path.join("main.tri");
if main_tri.exists() {
return main_tri;
}
}
}
// Default: local project path
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")
}
/// Topological sort of the module DAG. Returns Err if circular.
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());
}
}
/// Quick scan of a source file to extract module name and `use` dependencies.
/// Does not fully parse โ just looks for `program X` / `module X` and `use Y` lines.
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();
// Skip comments and empty lines
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 {
// Once we hit a non-header line, stop scanning for use statements
// (use must come before items per the grammar)
if trimmed.starts_with("fn ")
|| trimmed.starts_with("pub ")
|| trimmed.starts_with("const ")
|| trimmed.starts_with("struct ")
{
break;
}
}
}
(name, deps)
}
trident/src/config/resolve/resolver.rs
ฯ 0.0%
use BTreeMap;
use *;
pub
/// Quick scan of a source file to extract module name and `use` dependencies.
/// Does not fully parse โ just looks for `program X` / `module X` and `use Y` lines.
pub