use crate::domain::DomainCtx;
use crate::error::CompileError;
use crate::hash_hex;
use crate::ir::UnusedImport;
use crate::subst::collect_prefixes;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use elenchus_parser::Statement;
pub trait Resolver {
fn load(&self, path: &str) -> Result<String, CompileError>;
fn resolve(&self, _base: &str, relative: &str) -> String {
relative.to_string()
}
}
#[derive(Default)]
pub struct MemoryResolver {
sources: BTreeMap<String, String>,
}
impl MemoryResolver {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, path: &str, content: &str) -> &mut Self {
self.sources.insert(path.to_string(), content.to_string());
self
}
}
pub fn normalize_import_path(base: &str, relative: &str) -> String {
fn is_sep(c: char) -> bool {
c == '/' || c == '\\'
}
fn push<'a>(parts: &mut Vec<&'a str>, seg: &'a str) {
match seg {
"" | "." => {}
".." => {
parts.pop();
}
_ => parts.push(seg),
}
}
let mut absolute = base.starts_with(is_sep);
let mut parts: Vec<&str> = Vec::new();
let base_segs: Vec<&str> = base.split(is_sep).collect();
for seg in base_segs.iter().take(base_segs.len().saturating_sub(1)) {
push(&mut parts, seg);
}
if relative.starts_with(is_sep) {
parts.clear();
absolute = true;
}
for seg in relative.split(is_sep) {
push(&mut parts, seg);
}
let joined = parts.join("/");
if absolute {
alloc::format!("/{joined}")
} else {
joined
}
}
impl Resolver for MemoryResolver {
fn load(&self, path: &str) -> Result<String, CompileError> {
self.sources
.get(path)
.cloned()
.ok_or_else(|| CompileError::ImportNotFound(path.to_string()))
}
fn resolve(&self, base: &str, relative: &str) -> String {
normalize_import_path(base, relative)
}
}
#[cfg(feature = "std")]
pub struct FileResolver;
#[cfg(feature = "std")]
impl Resolver for FileResolver {
fn load(&self, path: &str) -> Result<String, CompileError> {
std::fs::read_to_string(path)
.map_err(|e| CompileError::ImportNotFound(alloc::format!("{}: {}", path, e)))
}
fn resolve(&self, base: &str, relative: &str) -> String {
normalize_import_path(base, relative)
}
}
pub(crate) struct ResolvedFile {
pub(crate) path: String,
pub(crate) content: String,
pub(crate) ctx: DomainCtx,
}
pub(crate) struct ImportEdge {
pub(crate) alias: Option<String>,
pub(crate) child_path: String,
pub(crate) line: u32,
}
pub(crate) struct DiscoveredFile {
pub(crate) path: String,
pub(crate) content: String,
pub(crate) domain: String,
pub(crate) edges: Vec<ImportEdge>,
pub(crate) used_prefixes: BTreeSet<Option<String>>,
}
pub(crate) fn resolve_graph<R: Resolver>(
root: &str,
resolver: &R,
) -> Result<(Vec<ResolvedFile>, Vec<UnusedImport>), CompileError> {
enum Step {
Enter(String),
Exit(String),
}
let mut discovered: BTreeMap<String, DiscoveredFile> = BTreeMap::new(); let mut path_hash: BTreeMap<String, String> = BTreeMap::new(); let mut order: Vec<String> = Vec::new(); let mut active: BTreeSet<String> = BTreeSet::new(); let mut work: Vec<Step> = vec![Step::Enter(root.to_string())];
while let Some(step) = work.pop() {
match step {
Step::Exit(hash) => {
active.remove(&hash);
order.push(hash);
}
Step::Enter(path) => {
let content = resolver.load(&path)?;
let hash = hash_hex(content.as_bytes());
path_hash.insert(path.clone(), hash.clone());
if active.contains(&hash) {
return Err(CompileError::CircularImport(path)); }
if discovered.contains_key(&hash) {
continue; }
let program = parse_tagged(&path, &content)?;
let domain = extract_domain(&program, &path)?;
let mut edges = Vec::new();
let mut used_prefixes = BTreeSet::new();
for stmt in &program.statements {
if let Statement::Import { path: p, alias } = stmt {
edges.push(ImportEdge {
alias: alias.as_ref().map(|a| a.data.to_string()),
child_path: resolver.resolve(&path, p.data),
line: p.span.location_line(),
});
} else {
collect_prefixes(stmt, &mut used_prefixes);
}
}
drop(program); active.insert(hash.clone());
work.push(Step::Exit(hash.clone()));
for e in edges.iter().rev() {
work.push(Step::Enter(e.child_path.clone()));
}
discovered.insert(
hash,
DiscoveredFile {
path,
content,
domain,
edges,
used_prefixes,
},
);
}
}
}
let domain_of: BTreeMap<&str, &str> = discovered
.iter()
.map(|(h, f)| (h.as_str(), f.domain.as_str()))
.collect();
let mut out = Vec::with_capacity(order.len());
let mut unused: Vec<UnusedImport> = Vec::new();
for hash in &order {
let file = &discovered[hash];
let mut aliases = BTreeMap::new();
aliases.insert(file.domain.clone(), file.domain.clone());
for edge in &file.edges {
let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
let bind = edge
.alias
.clone()
.unwrap_or_else(|| child_domain.to_string());
match aliases.get(&bind) {
Some(existing) if existing != child_domain => {
return Err(CompileError::DomainAliasClash { alias: bind });
}
_ => {
aliases.insert(bind, child_domain.to_string());
}
}
}
let referenced: BTreeSet<&str> = file
.used_prefixes
.iter()
.filter_map(|p| match p {
None => Some(file.domain.as_str()),
Some(name) => aliases.get(name).map(|d| d.as_str()),
})
.collect();
for edge in &file.edges {
let child_domain = domain_of[path_hash[&edge.child_path].as_str()];
if !referenced.contains(child_domain) {
unused.push(UnusedImport {
file: file.path.clone(),
domain: child_domain.to_string(),
alias: edge.alias.clone(),
line: edge.line,
});
}
}
let ctx = DomainCtx {
current: file.domain.clone(),
aliases,
};
out.push((hash.clone(), ctx));
}
unused.sort();
let files = out
.into_iter()
.map(|(hash, ctx)| {
let file = discovered.remove(&hash).expect("hash was discovered");
ResolvedFile {
path: file.path,
content: file.content,
ctx,
}
})
.collect();
Ok((files, unused))
}
pub(crate) fn parse_tagged<'a>(
file: &str,
content: &'a str,
) -> Result<elenchus_parser::Program<'a>, CompileError> {
elenchus_parser::parse(content).map_err(|mut diag| {
diag.set_file(file);
CompileError::Parse(diag)
})
}
pub(crate) fn extract_domain(
program: &elenchus_parser::Program,
source: &str,
) -> Result<String, CompileError> {
let mut found: Option<String> = None;
for stmt in &program.statements {
if let Statement::Domain(name) = stmt {
if found.is_some() {
return Err(CompileError::DuplicateDomain {
file: source.to_string(),
});
}
found = Some(name.data.to_string());
}
}
found.ok_or_else(|| CompileError::MissingDomain {
file: source.to_string(),
})
}