use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use wolf_ast::{ExternDeclKind, ItemKind};
use wolf_diag::{Diagnostic, codes};
use wolf_span::Span;
use crate::{ModuleInput, Role};
struct Externs {
header: Span,
names: BTreeMap<String, BTreeSet<String>>,
}
impl Externs {
fn missing(&self, target: &str) -> Vec<String> {
let all: BTreeSet<&String> = self.names.values().flatten().collect();
let declared = self.names.get(target);
all.into_iter()
.filter(|n| !declared.is_some_and(|d| d.contains(*n)))
.cloned()
.collect()
}
}
struct Graph {
imports: Vec<Vec<usize>>,
import_spans: BTreeMap<(usize, usize), Span>,
externs: BTreeMap<usize, Externs>,
}
fn graph(modules: &[ModuleInput<'_>]) -> Graph {
let index: BTreeMap<&[String], usize> = modules
.iter()
.enumerate()
.map(|(i, m)| (m.path.as_slice(), i))
.collect();
let mut imports: Vec<Vec<usize>> = vec![Vec::new(); modules.len()];
let mut import_spans: BTreeMap<(usize, usize), Span> = BTreeMap::new();
let mut externs: BTreeMap<usize, Externs> = BTreeMap::new();
for (i, m) in modules.iter().enumerate() {
if matches!(m.role, Role::Std | Role::Prelude) {
continue;
}
for section in &m.ast.sections {
for item in §ion.items {
match &item.kind {
ItemKind::Import(import) => {
let path: Vec<String> = import
.path
.segments
.iter()
.map(|s| s.name.clone())
.collect();
if let (Some(j), Some(list)) =
(index.get(path.as_slice()), imports.get_mut(i))
{
list.push(*j);
import_spans.entry((i, *j)).or_insert(import.path.span);
}
}
ItemKind::ExternTarget(block) => {
let e = externs.entry(i).or_insert_with(|| Externs {
header: section.name_span,
names: BTreeMap::new(),
});
let names = e.names.entry(block.target.name.clone()).or_default();
for decl in &block.decls {
let name = match &decl.kind {
ExternDeclKind::Fn(sig) => &sig.name.name,
ExternDeclKind::Type(name) => &name.name,
ExternDeclKind::Struct(_) => continue,
};
names.insert(name.clone());
}
}
_ => {}
}
}
}
}
Graph {
imports,
import_spans,
externs,
}
}
impl Graph {
fn reach(&self, groups: Vec<Vec<usize>>) -> BTreeMap<usize, Option<usize>> {
let mut parent: BTreeMap<usize, Option<usize>> = BTreeMap::new();
for group in groups {
let mut queue: VecDeque<usize> = VecDeque::new();
for r in group {
if let Entry::Vacant(v) = parent.entry(r) {
v.insert(None);
queue.push_back(r);
}
}
while let Some(i) = queue.pop_front() {
for &j in self.imports.get(i).map_or(&[][..], Vec::as_slice) {
if let Entry::Vacant(v) = parent.entry(j) {
v.insert(Some(i));
queue.push_back(j);
}
}
}
}
parent
}
}
fn chain(
modules: &[ModuleInput<'_>],
parent: &BTreeMap<usize, Option<usize>>,
i: usize,
) -> Vec<String> {
let path_of = |k: usize| modules.get(k).map(|m| m.path.join(".")).unwrap_or_default();
let mut chain = vec![path_of(i)];
let mut cur = i;
while let Some(Some(p)) = parent.get(&cur) {
chain.push(path_of(*p));
cur = *p;
}
chain.reverse();
chain
}
pub fn check_vm(
modules: &[ModuleInput<'_>],
root: impl Fn(&ModuleInput<'_>) -> bool,
help: &str,
) -> Vec<Diagnostic> {
let g = graph(modules);
let roots: Vec<usize> = modules
.iter()
.enumerate()
.filter(|(_, m)| !matches!(m.role, Role::Std | Role::Prelude) && root(m))
.map(|(i, _)| i)
.collect();
let parent = g.reach(vec![roots]);
let mut out = Vec::new();
for (&i, e) in &g.externs {
if !parent.contains_key(&i) {
continue;
}
let chain = chain(modules, &parent, i);
let name = modules.get(i).map(|m| m.path.join(".")).unwrap_or_default();
let native = modules.get(i).is_some_and(|m| m.role == Role::Native);
let mut d = if native {
let importer = parent.get(&i).copied().flatten();
let mut d = Diagnostic::new(
codes::E0807,
format!("the native module `{name}` cannot run in the VM"),
);
if let Some(span) = importer.and_then(|p| g.import_spans.get(&(p, i)).copied()) {
d = d.primary(span, "code of the targets is imported here");
}
d
} else {
Diagnostic::new(
codes::E0807,
format!("module `{name}` cannot run in the VM"),
)
.primary(e.header, "code of the targets is called here")
};
if chain.len() > 1 {
d = d.note(format!("imported through {}", chain.join(" -> ")));
}
out.push(d.help(help.to_string()));
}
out
}
pub fn check_target(
modules: &[ModuleInput<'_>],
target: &str,
built: impl Fn(Role) -> bool,
) -> Vec<Diagnostic> {
let g = graph(modules);
let (import_spans, externs) = (&g.import_spans, &g.externs);
let roots: Vec<usize> = modules
.iter()
.enumerate()
.filter(|(_, m)| {
matches!(m.role, Role::Entry | Role::Example | Role::Test) && built(m.role)
})
.map(|(i, _)| i)
.collect();
let rest: Vec<usize> = modules
.iter()
.enumerate()
.filter(|(_, m)| m.role == Role::Src)
.map(|(i, _)| i)
.collect();
let parent = g.reach(vec![roots, rest]);
let mut out = Vec::new();
for (&i, e) in externs {
if !parent.contains_key(&i) {
continue;
}
let missing = e.missing(target);
if missing.is_empty() {
continue;
}
let name = modules.get(i).map(|m| m.path.join(".")).unwrap_or_default();
let chain = chain(modules, &parent, i);
let bound: Vec<&str> = e
.names
.keys()
.filter(|t| e.missing(t).is_empty())
.map(String::as_str)
.collect();
let list = missing
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ");
if modules.get(i).is_some_and(|m| m.role == Role::Native) {
let importer = parent.get(&i).copied().flatten();
let span = importer.and_then(|p| import_spans.get(&(p, i)).copied());
let mut d = Diagnostic::new(
codes::E0704,
format!("the native module `{name}` is not available for `{target}`"),
);
if let Some(span) = span {
d = d.primary(span, format!("no `{target}` code for {list}"));
}
if chain.len() > 2 {
d = d.note(format!("imported through {}", chain.join(" -> ")));
}
let others = bound
.iter()
.map(|t| format!("`{t}`"))
.collect::<Vec<_>>()
.join(" or ");
out.push(d.help(format!(
"add the `{target}` code of the module, or build for {others}"
)));
continue;
}
let mut d = Diagnostic::new(
codes::E0704,
format!("module `{name}` is not available for `{target}`"),
)
.primary(e.header, format!("no `{target}` declaration for {list}"));
if chain.len() > 1 {
d = d.note(format!("imported through {}", chain.join(" -> ")));
}
d = if bound.is_empty() {
d.help(format!("declare {list} in a `{target}` block"))
} else {
d.help(format!(
"declare {list} in a `{target}` block, or build for {}",
bound
.iter()
.map(|t| format!("`{t}`"))
.collect::<Vec<_>>()
.join(" or ")
))
};
out.push(d);
}
out
}