use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::package_imports::{acquire_package_snapshots, resolve_import_path_with_snapshots};
use crate::package_snapshot::PackageSnapshot;
use harn_lexer::Span;
use harn_parser::{BindingPattern, Node, Parser, SNode};
pub mod asset_paths;
pub mod fingerprint;
pub mod package_execution;
mod package_imports;
pub mod package_snapshot;
pub mod personas;
mod stdlib;
pub use package_imports::{
resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DefKind {
Function,
Pipeline,
Tool,
Skill,
Struct,
Enum,
Interface,
Type,
Variable,
Parameter,
}
#[derive(Debug, Clone)]
pub struct DefSite {
pub name: String,
pub file: PathBuf,
pub kind: DefKind,
pub span: Span,
}
#[derive(Debug, Clone)]
pub enum WildcardResolution {
Resolved(HashSet<String>),
Unknown,
}
#[derive(Debug, Default)]
pub struct ModuleGraph {
modules: HashMap<PathBuf, ModuleInfo>,
_package_snapshots: Vec<PackageSnapshot>,
}
#[derive(Debug, Clone)]
pub struct ParsedModuleSource {
pub source: String,
pub program: Vec<SNode>,
}
#[derive(Debug, Default)]
pub struct ModuleGraphBuild {
pub graph: ModuleGraph,
pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
}
#[derive(Debug, Default)]
struct ModuleInfo {
declarations: HashMap<String, DefSite>,
exports: HashSet<String>,
own_exports: HashSet<String>,
selective_re_exports: HashMap<String, Vec<PathBuf>>,
wildcard_re_export_paths: Vec<PathBuf>,
selective_import_names: HashSet<String>,
imports: Vec<ImportRef>,
has_unresolved_wildcard_import: bool,
has_unresolved_selective_import: bool,
type_declarations: Vec<SNode>,
callable_declarations: Vec<SNode>,
load_error: Option<ModuleLoadError>,
}
#[derive(Debug, Clone)]
pub struct ModuleLoadError {
pub message: String,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ImportCompileFailure {
pub import_raw_path: String,
pub import_span: Span,
pub module_path: PathBuf,
pub error: ModuleLoadError,
}
#[derive(Debug, Clone)]
struct ImportRef {
raw_path: String,
path: Option<PathBuf>,
selective_names: Option<HashSet<String>>,
import_span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleImport {
pub raw_path: String,
pub resolved_path: Option<PathBuf>,
pub selective_names: Option<Vec<String>>,
}
pub fn read_module_source(path: &Path) -> Option<String> {
if let Some(stdlib_module) = stdlib_module_from_path(path) {
return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
}
std::fs::read_to_string(path).ok()
}
pub fn build(files: &[PathBuf]) -> ModuleGraph {
build_inner(files, None).graph
}
pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
build_inner(files, Some(&parsed_source_targets))
}
fn build_inner(
files: &[PathBuf],
parsed_source_targets: Option<&HashSet<PathBuf>>,
) -> ModuleGraphBuild {
let package_snapshots = acquire_package_snapshots(files);
let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
let mut seen: HashSet<PathBuf> = HashSet::new();
let mut wave: Vec<PathBuf> = Vec::new();
for file in files {
let canonical = normalize_path(file);
if seen.insert(canonical.clone()) {
wave.push(canonical);
}
}
while !wave.is_empty() {
let loaded = load_wave(&wave, &package_snapshots);
let mut next_wave: Vec<PathBuf> = Vec::new();
for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
let retain_parsed_source =
parsed_source_targets.is_some_and(|targets| targets.contains(&path));
if retain_parsed_source {
if let Some(parsed) = parsed {
parsed_sources.insert(path.clone(), parsed);
}
}
for import in &module.imports {
if let Some(import_path) = &import.path {
let canonical = normalize_path(import_path);
if seen.insert(canonical.clone()) {
next_wave.push(canonical);
}
}
}
modules.insert(path, module);
}
wave = next_wave;
}
resolve_re_exports(&mut modules);
ModuleGraphBuild {
graph: ModuleGraph {
modules,
_package_snapshots: package_snapshots,
},
parsed_sources,
}
}
pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
fn load_wave(
paths: &[PathBuf],
package_snapshots: &[PackageSnapshot],
) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
const MIN_PARALLEL_WAVE: usize = 8;
let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|&jobs| jobs > 0);
let workers = configured
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1)
})
.min(paths.len());
if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
return paths
.iter()
.map(|path| load_module(path, package_snapshots))
.collect();
}
let next = std::sync::atomic::AtomicUsize::new(0);
let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_| {
scope.spawn(|| {
let mut local = Vec::new();
loop {
let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Some(path) = paths.get(index) else {
break;
};
local.push((index, load_module(path, package_snapshots)));
}
local
})
})
.collect();
handles
.into_iter()
.flat_map(|handle| match handle.join() {
Ok(local) => local,
Err(panic) => std::panic::resume_unwind(panic),
})
.collect()
});
produced.sort_unstable_by_key(|(index, _)| *index);
produced.into_iter().map(|(_, loaded)| loaded).collect()
}
fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
let keys: Vec<PathBuf> = modules.keys().cloned().collect();
loop {
let mut changed = false;
for path in &keys {
let wildcard_paths = modules
.get(path)
.map(|m| m.wildcard_re_export_paths.clone())
.unwrap_or_default();
if wildcard_paths.is_empty() {
continue;
}
let mut additions: Vec<String> = Vec::new();
for src in &wildcard_paths {
let src_canonical = normalize_path(src);
if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
additions.extend(src_module.exports.iter().cloned());
}
}
if let Some(module) = modules.get_mut(path) {
for name in additions {
if module.exports.insert(name) {
changed = true;
}
}
}
}
if !changed {
break;
}
}
}
impl ModuleGraph {
pub fn module_paths(&self) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
paths.sort();
paths
}
pub fn contains_module(&self, path: &Path) -> bool {
self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
}
pub fn all_selective_import_names(&self) -> HashSet<&str> {
let mut names = HashSet::new();
for module in self.modules.values() {
for name in &module.selective_import_names {
names.insert(name.as_str());
}
}
names
}
pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
let target = normalize_path(target);
let mut out: Vec<PathBuf> = self
.modules
.iter()
.filter(|(_, info)| {
info.imports.iter().any(|import| {
import
.path
.as_ref()
.is_some_and(|p| normalize_path(p) == target)
})
})
.map(|(path, _)| path.clone())
.collect();
out.sort();
out
}
pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return Vec::new();
};
let mut imports: Vec<ModuleImport> = module
.imports
.iter()
.map(|import| {
let mut selective_names = import
.selective_names
.as_ref()
.map(|names| names.iter().cloned().collect::<Vec<_>>());
if let Some(names) = selective_names.as_mut() {
names.sort();
}
ModuleImport {
raw_path: import.raw_path.clone(),
resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
selective_names,
}
})
.collect();
imports.sort_by(|left, right| {
left.raw_path
.cmp(&right.raw_path)
.then_with(|| left.selective_names.cmp(&right.selective_names))
.then_with(|| left.resolved_path.cmp(&right.resolved_path))
});
imports
}
pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return Vec::new();
};
let mut exports: Vec<String> = module.exports.iter().cloned().collect();
exports.sort();
exports
}
pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return WildcardResolution::Unknown;
};
if module.has_unresolved_wildcard_import {
return WildcardResolution::Unknown;
}
let mut names = HashSet::new();
for import in module
.imports
.iter()
.filter(|import| import.selective_names.is_none())
{
let Some(import_path) = &import.path else {
return WildcardResolution::Unknown;
};
let imported = self.modules.get(import_path).or_else(|| {
let normalized = normalize_path(import_path);
self.modules.get(&normalized)
});
let Some(imported) = imported else {
return WildcardResolution::Unknown;
};
names.extend(imported.exports.iter().cloned());
}
WildcardResolution::Resolved(names)
}
#[must_use]
pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return Vec::new();
};
let mut failures = Vec::new();
for import in &module.imports {
let Some(import_path) = &import.path else {
continue;
};
let Some(target) = self
.modules
.get(import_path)
.or_else(|| self.modules.get(&normalize_path(import_path)))
else {
continue;
};
if let Some(error) = &target.load_error {
failures.push(ImportCompileFailure {
import_raw_path: import.raw_path.clone(),
import_span: import.import_span,
module_path: normalize_path(import_path),
error: error.clone(),
});
}
}
failures
}
pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
let file = normalize_path(file);
let module = self.modules.get(&file)?;
if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
return None;
}
let mut names = HashSet::new();
for import in &module.imports {
let import_path = import.path.as_ref()?;
let imported = self
.modules
.get(import_path)
.or_else(|| self.modules.get(&normalize_path(import_path)))?;
if imported.load_error.is_some() {
return None;
}
match &import.selective_names {
None => {
names.extend(imported.exports.iter().cloned());
}
Some(selective) => {
for name in selective {
if imported.declarations.contains_key(name)
|| imported.exports.contains(name)
{
names.insert(name.clone());
}
}
}
}
}
Some(names)
}
pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
let file = normalize_path(file);
let module = self.modules.get(&file)?;
if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
return None;
}
let mut decls = Vec::new();
for import in &module.imports {
let import_path = import.path.as_ref()?;
let imported = self
.modules
.get(import_path)
.or_else(|| self.modules.get(&normalize_path(import_path)))?;
if imported.load_error.is_some() {
return None;
}
let names_to_collect: Vec<String> = match &import.selective_names {
None => imported.exports.iter().cloned().collect(),
Some(selective) => selective.iter().cloned().collect(),
};
for name in &names_to_collect {
let mut visited = HashSet::new();
if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
decls.push(decl);
}
}
for ty_decl in &imported.type_declarations {
if type_decl_name(ty_decl).is_some() {
decls.push(ty_decl.clone());
}
}
}
Some(decls)
}
pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
let file = normalize_path(file);
let module = self.modules.get(&file)?;
if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
return None;
}
let mut decls = Vec::new();
for import in &module.imports {
let import_path = import.path.as_ref()?;
let imported = self
.modules
.get(import_path)
.or_else(|| self.modules.get(&normalize_path(import_path)))?;
if imported.load_error.is_some() {
return None;
}
let selective_import = import.selective_names.is_some();
let names_to_collect: Vec<String> = match &import.selective_names {
None => imported.exports.iter().cloned().collect(),
Some(selective) => selective.iter().cloned().collect(),
};
for name in &names_to_collect {
if selective_import || imported.own_exports.contains(name) {
if let Some(decl) = imported
.callable_declarations
.iter()
.find(|decl| callable_decl_name(decl) == Some(name.as_str()))
{
decls.push(decl.clone());
continue;
}
}
let mut visited = HashSet::new();
if let Some(decl) =
self.find_exported_callable_decl(import_path, name, &mut visited)
{
decls.push(decl);
}
}
}
Some(decls)
}
fn find_exported_type_decl(
&self,
path: &Path,
name: &str,
visited: &mut HashSet<PathBuf>,
) -> Option<SNode> {
let canonical = normalize_path(path);
if !visited.insert(canonical.clone()) {
return None;
}
let module = self
.modules
.get(&canonical)
.or_else(|| self.modules.get(path))?;
for decl in &module.type_declarations {
if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
return Some(decl.clone());
}
}
if let Some(sources) = module.selective_re_exports.get(name) {
for source in sources {
if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
return Some(decl);
}
}
}
for source in &module.wildcard_re_export_paths {
if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
return Some(decl);
}
}
None
}
fn find_exported_callable_decl(
&self,
path: &Path,
name: &str,
visited: &mut HashSet<PathBuf>,
) -> Option<SNode> {
let canonical = normalize_path(path);
if !visited.insert(canonical.clone()) {
return None;
}
let module = self
.modules
.get(&canonical)
.or_else(|| self.modules.get(path))?;
for decl in &module.callable_declarations {
if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
return Some(decl.clone());
}
}
if let Some(sources) = module.selective_re_exports.get(name) {
for source in sources {
if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
return Some(decl);
}
}
}
for source in &module.wildcard_re_export_paths {
if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
return Some(decl);
}
}
None
}
pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
let mut visited = HashSet::new();
self.definition_of_inner(file, name, &mut visited)
}
pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
let module = self.modules.get(&normalize_path(file))?;
let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
names.sort_unstable();
Some(names)
}
fn definition_of_inner(
&self,
file: &Path,
name: &str,
visited: &mut HashSet<PathBuf>,
) -> Option<DefSite> {
let file = normalize_path(file);
if !visited.insert(file.clone()) {
return None;
}
let current = self.modules.get(&file)?;
if let Some(local) = current.declarations.get(name) {
return Some(local.clone());
}
if let Some(sources) = current.selective_re_exports.get(name) {
for source in sources {
if let Some(def) = self.definition_of_inner(source, name, visited) {
return Some(def);
}
}
}
for source in ¤t.wildcard_re_export_paths {
if let Some(def) = self.definition_of_inner(source, name, visited) {
return Some(def);
}
}
for import in ¤t.imports {
let Some(selective_names) = &import.selective_names else {
continue;
};
if !selective_names.contains(name) {
continue;
}
if let Some(path) = &import.path {
if let Some(def) = self.definition_of_inner(path, name, visited) {
return Some(def);
}
}
}
for import in ¤t.imports {
if import.selective_names.is_some() {
continue;
}
if let Some(path) = &import.path {
if let Some(def) = self.definition_of_inner(path, name, visited) {
return Some(def);
}
}
}
None
}
pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return Vec::new();
};
let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
for (name, srcs) in &module.selective_re_exports {
sources
.entry(name.clone())
.or_default()
.extend(srcs.iter().cloned());
}
for src in &module.wildcard_re_export_paths {
let canonical = normalize_path(src);
let Some(src_module) = self
.modules
.get(&canonical)
.or_else(|| self.modules.get(src))
else {
continue;
};
for name in &src_module.exports {
sources
.entry(name.clone())
.or_default()
.push(canonical.clone());
}
}
for name in &module.own_exports {
if let Some(entry) = sources.get_mut(name) {
entry.push(file.clone());
}
}
let mut conflicts = Vec::new();
for (name, mut srcs) in sources {
srcs.sort();
srcs.dedup();
if srcs.len() > 1 {
conflicts.push(ReExportConflict {
name,
sources: srcs,
});
}
}
conflicts.sort_by(|a, b| a.name.cmp(&b.name));
conflicts
}
pub fn non_exported_selective_imports(&self, file: &Path) -> Vec<NonExportedImport> {
let file = normalize_path(file);
let Some(module) = self.modules.get(&file) else {
return Vec::new();
};
let mut out = Vec::new();
for import in &module.imports {
let Some(selective) = &import.selective_names else {
continue;
};
let Some(import_path) = &import.path else {
continue;
};
let Some(target) = self
.modules
.get(import_path)
.or_else(|| self.modules.get(&normalize_path(import_path)))
else {
continue;
};
for name in selective {
if target.declarations.contains_key(name) && !target.exports.contains(name) {
out.push(NonExportedImport {
name: name.clone(),
module: import.raw_path.clone(),
});
}
}
}
out.sort_by(|a, b| (&a.name, &a.module).cmp(&(&b.name, &b.module)));
out.dedup();
out
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReExportConflict {
pub name: String,
pub sources: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonExportedImport {
pub name: String,
pub module: String,
}
fn load_module(
path: &Path,
package_snapshots: &[PackageSnapshot],
) -> (ModuleInfo, Option<ParsedModuleSource>) {
let Some(source) = read_module_source(path) else {
return (ModuleInfo::default(), None);
};
let mut lexer = harn_lexer::Lexer::new(&source);
let tokens = match lexer.tokenize() {
Ok(tokens) => tokens,
Err(error) => {
let module = ModuleInfo {
load_error: Some(ModuleLoadError {
message: error.to_string(),
span: error.span(),
}),
..ModuleInfo::default()
};
return (module, None);
}
};
let mut parser = Parser::new(tokens);
let program = match parser.parse() {
Ok(program) => program,
Err(error) => {
let module = ModuleInfo {
load_error: Some(ModuleLoadError {
message: error.to_string(),
span: error.span(),
}),
..ModuleInfo::default()
};
return (module, None);
}
};
let mut module = ModuleInfo::default();
for node in &program {
collect_module_info(path, node, &mut module, package_snapshots);
collect_type_declarations(node, &mut module.type_declarations);
collect_callable_declarations(node, &mut module.callable_declarations);
}
module.exports.extend(module.own_exports.iter().cloned());
module
.exports
.extend(module.selective_re_exports.keys().cloned());
let parsed = ParsedModuleSource { source, program };
(module, Some(parsed))
}
fn stdlib_module_from_path(path: &Path) -> Option<&str> {
let s = path.to_str()?;
s.strip_prefix("<std>/")
}
fn collect_module_info(
file: &Path,
snode: &SNode,
module: &mut ModuleInfo,
package_snapshots: &[PackageSnapshot],
) {
match &snode.node {
Node::FnDecl {
name,
params,
is_pub,
..
} => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Function),
);
for param_name in params.iter().map(|param| param.name.clone()) {
module.declarations.insert(
param_name.clone(),
decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
);
}
}
Node::Pipeline { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Pipeline),
);
}
Node::ToolDecl { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Tool),
);
}
Node::SkillDecl { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Skill),
);
}
Node::StructDecl { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Struct),
);
}
Node::EnumDecl { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Enum),
);
}
Node::InterfaceDecl { name, .. } => {
module.own_exports.insert(name.clone());
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Interface),
);
}
Node::TypeDecl { name, is_pub, .. } => {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, name, DefKind::Type),
);
}
Node::LetBinding {
pattern, is_pub, ..
}
| Node::ConstBinding {
pattern, is_pub, ..
} => {
for name in pattern_names(pattern) {
if *is_pub {
module.own_exports.insert(name.clone());
}
module.declarations.insert(
name.clone(),
decl_site(file, snode.span, &name, DefKind::Variable),
);
}
}
Node::ImportDecl { path, is_pub } => {
let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
if import_path.is_none() {
module.has_unresolved_wildcard_import = true;
}
if *is_pub {
if let Some(resolved) = &import_path {
module
.wildcard_re_export_paths
.push(normalize_path(resolved));
}
}
module.imports.push(ImportRef {
raw_path: path.clone(),
path: import_path,
selective_names: None,
import_span: snode.span,
});
}
Node::SelectiveImport {
names,
path,
is_pub,
} => {
let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
if import_path.is_none() {
module.has_unresolved_selective_import = true;
}
if *is_pub {
if let Some(resolved) = &import_path {
let canonical = normalize_path(resolved);
for name in names {
module
.selective_re_exports
.entry(name.clone())
.or_default()
.push(canonical.clone());
}
}
}
let names: HashSet<String> = names.iter().cloned().collect();
module.selective_import_names.extend(names.iter().cloned());
module.imports.push(ImportRef {
raw_path: path.clone(),
path: import_path,
selective_names: Some(names),
import_span: snode.span,
});
}
Node::AttributedDecl { inner, .. } => {
collect_module_info(file, inner, module, package_snapshots);
}
_ => {}
}
}
fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
match &snode.node {
Node::TypeDecl { .. }
| Node::StructDecl { .. }
| Node::EnumDecl { .. }
| Node::InterfaceDecl { .. } => decls.push(snode.clone()),
Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
_ => {}
}
}
fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
match &snode.node {
Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
decls.push(snode.clone());
}
Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
_ => {}
}
}
fn type_decl_name(snode: &SNode) -> Option<&str> {
match &snode.node {
Node::TypeDecl { name, .. }
| Node::StructDecl { name, .. }
| Node::EnumDecl { name, .. }
| Node::InterfaceDecl { name, .. } => Some(name.as_str()),
_ => None,
}
}
fn callable_decl_name(snode: &SNode) -> Option<&str> {
match &snode.node {
Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
Some(name.as_str())
}
Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
_ => None,
}
}
fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
DefSite {
name: name.to_string(),
file: file.to_path_buf(),
kind,
span,
}
}
fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
match pattern {
BindingPattern::Identifier(name) => vec![name.clone()],
BindingPattern::Dict(fields) => fields
.iter()
.filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
.collect(),
BindingPattern::List(elements) => elements
.iter()
.map(|element| element.name.clone())
.collect(),
BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
}
}
fn normalize_path(path: &Path) -> PathBuf {
canonical_path(path)
}
pub fn canonical_path(path: &Path) -> PathBuf {
use std::sync::OnceLock;
if stdlib_module_from_path(path).is_some() {
return path.to_path_buf();
}
static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
if let Some(hit) = memo
.lock()
.expect("canonical path memo lock poisoned")
.get(path)
.cloned()
{
return hit;
}
match path.canonicalize() {
Ok(canonical) => {
memo.lock()
.expect("canonical path memo lock poisoned")
.insert(path.to_path_buf(), canonical.clone());
canonical
}
Err(_) => path.to_path_buf(),
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;