use voltaic_ast::Item;
use voltaic_ast::Program;
use voltaic_lattice::Declarations;
use voltaic_lattice::MemberKind;
use std::path::Path;
use std::path::PathBuf;
pub const FOREIGN: &[&str] = &["std", "core", "alloc", "crate", "super", "self"];
pub fn crate_of(path: &str) -> Option<String> {
let pair = path.split_once("::").unwrap_or((path, ""));
let head = pair.0;
if head.is_empty() || FOREIGN.contains(&head) {
return None;
}
Some(head.replace("_", "-"))
}
pub fn workspace_above(from: &Path) -> Option<PathBuf> {
let mut here = from.canonicalize().unwrap_or_else(|_| from.to_path_buf());
let mut climbed = 0;
while climbed < 8 {
if here.join("crates").is_dir() && here.join("Cargo.toml").is_file() {
return Some(here);
}
here = here.parent()?.to_path_buf();
climbed += 1;
}
None
}
pub fn sidecars_of(root: &Path, named: &str) -> Vec<PathBuf> {
let mut found = Vec::new();
let dir = root.join("crates").join(named).join("vlt");
gather(&dir, &mut found);
found.sort();
found
}
fn gather(dir: &Path, into: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return ();
};
for found in entries.flatten() {
let path = found.path();
if path.is_dir() {
gather(&path, into);
continue;
}
if path.to_string_lossy().ends_with(".vlt.sig") {
into.push(path);
}
}
();
}
pub fn upstream(program: &Program, from: &Path) -> Declarations {
let mut known = Declarations::empty();
let Some(root) = workspace_above(from) else {
return known;
};
let mut seen: Vec<String> = Vec::new();
for item in program.items.iter() {
let Item::Import(decl) = item else {
continue;
};
let Some(named) = crate_of(decl.path.as_str()) else {
continue;
};
if seen.as_slice().contains(&named) {
continue;
}
seen.push(named.clone());
for path in sidecars_of(&root, named.as_str()) {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let published = voltaic_lattice::read_sidecar(&text);
for m in published.members.iter() {
let kind = if m.is_field {
MemberKind::Field
} else {
MemberKind::Method
};
known.members.push(voltaic_lattice::Member {
owner: m.owner.clone(),
name: m.name.clone(),
ty: voltaic_lattice::Ty::Nil,
kind,
});
if !known.types.as_slice().contains(&m.owner.clone()) {
known.types.push(m.owner.clone());
}
}
}
}
known
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn an_import_names_a_candidate_crate_unless_its_head_is_one_of_rust_s_own_roots() {
assert_eq!(
crate_of("voltaic_ast::TypeExpr"),
Some("voltaic-ast".to_string())
);
assert_eq!(
crate_of("voltaic_lattice::Ty"),
Some("voltaic-lattice".to_string())
);
assert_eq!(
crate_of("anything_else::Thing"),
Some("anything-else".to_string())
);
for root in FOREIGN {
assert_eq!(crate_of(&format!("{}::Thing", root)), None);
}
}
#[test]
fn the_workspace_is_found_by_climbing_to_the_directory_holding_crates() {
let here = std::env::current_dir().expect("a test runs somewhere");
assert!(workspace_above(&here).is_some());
}
#[test]
fn a_relative_path_that_does_not_exist_cannot_climb_and_says_so() {
assert_eq!(workspace_above(Path::new("no/such/place")), None);
}
#[test]
fn a_path_outside_any_workspace_finds_none_rather_than_climbing_forever() {
assert_eq!(workspace_above(Path::new("/")), None);
}
#[test]
fn a_program_with_no_crate_qualified_import_resolves_nothing() {
let source = "import std::path::Path\n\nf : u64 -> u64\nf n = n\n";
let program = voltaic_parser::parse_source(source).expect("should parse");
let known = upstream(&program, Path::new("crates/voltaic-driver/vlt/resolve.vlt"));
assert!(known.is_empty());
}
}