use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::ast::{AstNode, AstToken, CallExpr, MacroCall};
use crate::incremental::normalize_path;
use crate::index::ModuleUsing;
use crate::resolve::{ModulePath, PackageSource, module_path_unresolvable};
use crate::semantic::{BindingKind, LoadKind, SemanticModel};
use crate::syntax::SyntaxNode;
use smol_str::SmolStr;
use super::{IncludeEdge, include_edges, include_target};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DefinitionEffects {
pub calls_eval: bool,
pub literal_include: bool,
pub dynamic_include: bool,
}
impl DefinitionEffects {
pub(crate) fn observe(&mut self, node: &SyntaxNode) {
if let Some(call) = MacroCall::cast(node.clone()) {
if call
.name()
.and_then(|name| name.macro_token())
.is_some_and(|t| t.text() == "eval")
{
self.calls_eval = true;
}
} else if let Some(call) = CallExpr::cast(node.clone()) {
if let Some(callee) = call.callee_ident() {
match callee.text() {
"eval" => self.calls_eval = true,
"include" if include_target(&call).is_some() => self.literal_include = true,
"include" => self.dynamic_include = true,
_ => {}
}
} else if call.callee().is_some_and(|callee| {
callee
.syntax()
.last_token()
.is_some_and(|token| matches!(token.text(), "include" | "eval"))
}) {
self.dynamic_include = true;
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScriptModule {
pub bindings: BTreeSet<SmolStr>,
pub usings: Vec<ModuleUsing>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScriptFile {
pub modules: BTreeMap<ModulePath, ScriptModule>,
pub includes: Vec<IncludeEdge>,
pub incomplete: bool,
events: Vec<ScriptEvent>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ScriptEvent {
Using(ModulePath, ModuleUsing),
Include(IncludeEdge),
}
impl ScriptFile {
pub fn project(
root: &SyntaxNode,
model: &SemanticModel,
path: &Path,
parse_clean: bool,
) -> Self {
let mut out = Self {
includes: include_edges(root, path.parent()),
incomplete: !parse_clean,
..Self::default()
};
let mut events = Vec::new();
for (site, edge) in super::include_sites(root).into_iter().zip(&out.includes) {
events.push((site.call.start(), ScriptEvent::Include(edge.clone())));
}
for binding in model.bindings() {
if model.scope(binding.scope).kind.is_global() {
let name = if binding.kind == BindingKind::Macro {
format!("@{}", binding.name).into()
} else {
binding.name.clone()
};
out.modules
.entry(model.enclosing_module_path(binding.scope))
.or_default()
.bindings
.insert(name);
}
}
for load in model.module_loads() {
if load.kind == LoadKind::Using && load.items.is_none() {
events.push((
load.range.start(),
ScriptEvent::Using(
model.enclosing_module_path(load.scope),
ModuleUsing {
leading_dots: load.path.leading_dots,
components: load
.path
.components
.iter()
.map(ToString::to_string)
.collect(),
},
),
));
}
}
events.sort_by_key(|(offset, _)| *offset);
out.events = events.into_iter().map(|(_, event)| event).collect();
let mut effects = DefinitionEffects::default();
for node in root.descendants() {
effects.observe(&node);
}
out.incomplete |= effects.calls_eval || effects.dynamic_include;
out
}
pub fn parse(path: &Path, text: &str) -> Self {
let parsed = crate::parser::parse(text);
Self::project(
&parsed.cst,
&SemanticModel::build(&parsed.cst),
path,
parsed.diagnostics.is_empty(),
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScriptProgram {
pub entry: PathBuf,
pub modules: BTreeMap<ModulePath, ScriptModule>,
pub members: BTreeMap<PathBuf, BTreeSet<ModulePath>>,
pub incomplete: bool,
}
impl ScriptProgram {
pub fn build(entry: &Path, mut file: impl FnMut(&Path) -> Option<Arc<ScriptFile>>) -> Self {
let mut out = Self {
entry: normalize_path(entry),
..Self::default()
};
let mut active = BTreeSet::new();
out.visit(&normalize_path(entry), &Vec::new(), &mut file, &mut active);
out
}
fn visit(
&mut self,
path: &Path,
host: &ModulePath,
file: &mut impl FnMut(&Path) -> Option<Arc<ScriptFile>>,
active: &mut BTreeSet<PathBuf>,
) {
if active.contains(path) {
self.incomplete = true;
return;
}
if !self
.members
.entry(path.to_path_buf())
.or_default()
.insert(host.clone())
{
return;
}
let Some(projection) = file(path) else {
self.incomplete = true;
return;
};
active.insert(path.to_path_buf());
self.incomplete |= projection.incomplete;
for (suffix, module) in &projection.modules {
let path: ModulePath = host.iter().chain(suffix).cloned().collect();
let dest = self.modules.entry(path).or_default();
dest.bindings.extend(module.bindings.iter().cloned());
dest.usings.extend(module.usings.iter().cloned());
}
for event in &projection.events {
let edge = match event {
ScriptEvent::Using(suffix, using) => {
let path = host.iter().chain(suffix).cloned().collect();
self.modules
.entry(path)
.or_default()
.usings
.push(using.clone());
continue;
}
ScriptEvent::Include(edge) => edge,
};
let Some(target) = &edge.target else {
self.incomplete = true;
continue;
};
let mut child_host = host.clone();
child_host.extend(edge.host_suffix.iter().map(SmolStr::new));
self.visit(&normalize_path(target), &child_host, file, active);
}
active.remove(path);
}
pub fn complete(&self, packages: &dyn PackageSource) -> bool {
!self.incomplete
&& self.modules.values().all(|module| {
module
.usings
.iter()
.all(|u| !module_path_unresolvable(u.leading_dots, &u.components, packages))
})
}
}
#[derive(Clone)]
pub struct ScriptContext<'a> {
pub program: &'a ScriptProgram,
pub host: ModulePath,
}
impl ScriptContext<'_> {
pub fn label(&self) -> String {
let module = std::iter::once("Main")
.chain(self.host.iter().map(SmolStr::as_str))
.collect::<Vec<_>>()
.join(".");
format!("{} ({module})", self.program.entry.display())
}
}
#[derive(Debug, Clone, Default)]
pub struct ScriptAnalysis {
pub programs: Vec<Arc<ScriptProgram>>,
pub pending: bool,
}
impl ScriptAnalysis {
pub fn contexts(&self, path: &Path) -> Vec<ScriptContext<'_>> {
let path = normalize_path(path);
self.programs
.iter()
.flat_map(|program| {
program
.members
.get(&path)
.into_iter()
.flatten()
.map(|host| ScriptContext {
program,
host: host.clone(),
})
})
.collect()
}
pub fn applies(&self, path: &Path) -> bool {
self.pending
|| self.programs.iter().any(|program| {
program.incomplete || program.members.contains_key(&normalize_path(path))
})
}
pub fn from_sources(entries: &[PathBuf], sources: &ScriptSources) -> Self {
let files: BTreeMap<_, _> = sources
.iter()
.filter_map(|(path, text)| {
text.as_ref()
.ok()
.map(|text| (path.clone(), Arc::new(ScriptFile::parse(path, text))))
})
.collect();
Self {
programs: entries
.iter()
.map(|entry| Arc::new(ScriptProgram::build(entry, |path| files.get(path).cloned())))
.collect(),
pending: false,
}
}
}
pub type ScriptSources = BTreeMap<PathBuf, Result<Arc<str>, String>>;
#[cfg(test)]
mod tests {
use super::*;
use crate::index::{PackageIndex, harvest_tree};
use crate::resolve::{Namespace, Resolution, Resolver};
#[test]
fn using_order_follows_includes_and_disagreeing_contexts_are_ambiguous() {
let dir = tempfile::tempdir().unwrap();
let first = dir.path().join("first.jl");
let second = dir.path().join("second.jl");
let helper = dir.path().join("helper.jl");
let source = "f() = shared\n";
let sources = BTreeMap::from([
(
first.clone(),
Ok(Arc::from(
"include(\"imports.jl\")\nusing B\ninclude(\"helper.jl\")\n",
)),
),
(
second.clone(),
Ok(Arc::from(
"using B\ninclude(\"imports.jl\")\ninclude(\"helper.jl\")\n",
)),
),
(dir.path().join("imports.jl"), Ok(Arc::from("using A\n"))),
(helper.clone(), Ok(Arc::from(source))),
]);
let scripts = ScriptAnalysis::from_sources(&[first, second], &sources);
let library: BTreeMap<_, _> = ["A", "B"]
.into_iter()
.map(|name| {
let mut root = harvest_tree(&crate::parser::parse("export shared\n").cst);
root.name = name.into();
(
name.to_string(),
Arc::new(PackageIndex {
name: name.into(),
root,
members: Vec::new(),
member_modules: BTreeMap::new(),
diagnostics: Vec::new(),
}),
)
})
.collect();
let parsed = crate::parser::parse(source);
let model = SemanticModel::build(&parsed.cst);
let contexts = scripts.contexts(&helper);
let resolver = Resolver::new(&model, &library).with_scripts(contexts.clone());
let offset = (source.find("shared").unwrap() as u32).into();
for (context, expected) in contexts.iter().zip(["A", "B"]) {
assert!(context.program.complete(&library));
assert_eq!(
resolver.resolve_in_script("shared", offset, Namespace::Value, context),
Resolution::Using {
module: expected.into(),
name: "shared".into()
}
);
}
assert_eq!(
resolver.resolve("shared", offset, Namespace::Value),
Resolution::Ambiguous
);
}
}