use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tishlang_ast::{ExportDeclaration, Expr, ImportSpecifier, Program, Statement};
#[derive(Debug, Clone)]
pub struct VirtualModule {
pub path: String,
pub program: Program,
}
const BUILTIN_ALIASES: &[(&str, &str)] = &[
("fs", "tish:fs"),
("http", "tish:http"),
("timers", "tish:timers"),
("process", "tish:process"),
("ws", "tish:ws"),
];
fn normalize_builtin_spec(spec: &str) -> Option<String> {
if spec.starts_with("tish:") {
return Some(spec.to_string());
}
BUILTIN_ALIASES
.iter()
.find(|(alias, _)| *alias == spec)
.map(|(_, canonical)| (*canonical).to_string())
}
fn is_native_import(spec: &str) -> bool {
spec.starts_with("tish:")
|| spec.starts_with("cargo:")
|| spec.starts_with('@')
|| matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
}
fn normalize_virtual_path(from_dir: &str, spec: &str) -> Result<String, String> {
if !spec.starts_with("./") && !spec.starts_with("../") {
return Err(format!(
"Only relative imports (./, ../) or native imports (tish:*, @scope/pkg) are supported. Got: {}",
spec
));
}
let combined = if from_dir.is_empty() {
spec.to_string()
} else {
format!("{}/{}", from_dir, spec)
};
let parts: Vec<&str> = combined.split('/').collect();
let mut stack: Vec<&str> = Vec::new();
for p in parts {
match p {
"" | "." => {}
".." => {
stack.pop();
}
_ => stack.push(p),
}
}
Ok(stack.join("/"))
}
fn parent_dir(path: &str) -> &str {
if let Some(slash) = path.rfind('/') {
&path[..slash]
} else {
""
}
}
fn resolve_import_to_key(
spec: &str,
from_dir: &str,
files: &HashMap<String, String>,
) -> Result<String, String> {
let normalized = normalize_virtual_path(from_dir, spec)?;
if files.contains_key(&normalized) {
return Ok(normalized);
}
if !normalized.ends_with(".tish") && !normalized.contains('.') {
let with_ext = format!("{}.tish", normalized);
if files.contains_key(&with_ext) {
return Ok(with_ext);
}
}
Err(format!(
"Cannot resolve import '{}' from {}: file not in virtual file map",
spec, from_dir
))
}
pub fn resolve_virtual(
entry_path: &str,
files: &HashMap<String, String>,
) -> Result<Vec<VirtualModule>, String> {
let entry_key = if files.contains_key(entry_path) {
entry_path.to_string()
} else if !entry_path.ends_with(".tish") {
let with_ext = format!("{}.tish", entry_path);
if files.contains_key(&with_ext) {
with_ext
} else {
return Err(format!(
"Entry file '{}' not in virtual file map",
entry_path
));
}
} else {
return Err(format!(
"Entry file '{}' not in virtual file map",
entry_path
));
};
let mut visited = HashSet::new();
let mut path_to_module: HashMap<String, Program> = HashMap::new();
let mut load_order: Vec<String> = Vec::new();
load_module_recursive(
&entry_key,
files,
&mut visited,
&mut path_to_module,
&mut load_order,
)?;
Ok(load_order
.into_iter()
.map(|p| {
let program = path_to_module.remove(&p).unwrap();
VirtualModule { path: p, program }
})
.collect())
}
fn load_module_recursive(
module_path: &str,
files: &HashMap<String, String>,
visited: &mut HashSet<String>,
path_to_module: &mut HashMap<String, Program>,
load_order: &mut Vec<String>,
) -> Result<(), String> {
if visited.contains(module_path) {
return Ok(());
}
visited.insert(module_path.to_string());
let source = files
.get(module_path)
.ok_or_else(|| format!("Module '{}' not in virtual file map", module_path))?;
let program = tishlang_parser::parse(source.trim())
.map_err(|e| format!("Parse error in {}: {}", module_path, e))?;
let from_dir = parent_dir(module_path);
for stmt in &program.statements {
if let Statement::Import { from, .. } = stmt {
if is_native_import(from) {
continue;
}
let dep_key = resolve_import_to_key(from, from_dir, files)?;
if !path_to_module.contains_key(&dep_key) {
load_module_recursive(&dep_key, files, visited, path_to_module, load_order)?;
}
}
}
path_to_module.insert(module_path.to_string(), program);
load_order.push(module_path.to_string());
Ok(())
}
pub fn detect_cycles_virtual(modules: &[VirtualModule]) -> Result<(), String> {
let path_to_idx: HashMap<_, _> = modules
.iter()
.enumerate()
.map(|(i, m)| (m.path.clone(), i))
.collect();
for (idx, module) in modules.iter().enumerate() {
let dir = parent_dir(&module.path);
let mut stack = vec![idx];
if has_cycle_from(
dir,
&module.program,
&path_to_idx,
modules,
&mut stack,
&mut HashSet::new(),
)? {
let path_names: Vec<_> = stack.iter().map(|&i| modules[i].path.clone()).collect();
return Err(format!(
"Circular import detected: {}",
path_names.join(" -> ")
));
}
}
Ok(())
}
fn has_cycle_from(
from_dir: &str,
program: &Program,
path_to_idx: &HashMap<String, usize>,
modules: &[VirtualModule],
stack: &mut Vec<usize>,
visiting: &mut HashSet<usize>,
) -> Result<bool, String> {
for stmt in &program.statements {
if let Statement::Import { from, .. } = stmt {
if is_native_import(from) {
continue;
}
let dep_key = resolve_import_to_key_for_cycle(from, from_dir, path_to_idx)?;
if let Some(&dep_idx) = path_to_idx.get(&dep_key) {
if stack.contains(&dep_idx) {
stack.push(dep_idx);
return Ok(true);
}
if !visiting.contains(&dep_idx) {
visiting.insert(dep_idx);
stack.push(dep_idx);
let dep = &modules[dep_idx];
let dep_dir = parent_dir(&dep.path);
if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
{
return Ok(true);
}
stack.pop();
visiting.remove(&dep_idx);
}
}
}
}
Ok(false)
}
fn resolve_import_to_key_for_cycle(
spec: &str,
from_dir: &str,
path_to_idx: &HashMap<String, usize>,
) -> Result<String, String> {
let normalized = normalize_virtual_path(from_dir, spec)?;
if path_to_idx.contains_key(&normalized) {
return Ok(normalized);
}
if !normalized.ends_with(".tish") && !normalized.contains('.') {
let with_ext = format!("{}.tish", normalized);
if path_to_idx.contains_key(&with_ext) {
return Ok(with_ext);
}
}
Err(format!(
"Cannot resolve import '{}' from {}: module not in resolved set",
spec, from_dir
))
}
pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, String> {
let path_to_idx: HashMap<String, usize> = modules
.iter()
.enumerate()
.map(|(i, m)| (m.path.clone(), i))
.collect();
let mut module_exports: Vec<HashMap<String, String>> = vec![HashMap::new(); modules.len()];
for (idx, module) in modules.iter().enumerate() {
for stmt in &module.program.statements {
if let Statement::Export { declaration, .. } = stmt {
match declaration.as_ref() {
ExportDeclaration::Named(s) => {
let name = match s.as_ref() {
Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
name.to_string()
}
_ => continue,
};
module_exports[idx].insert(name.clone(), name);
}
ExportDeclaration::Default(_) => {
let default_name = format!("__default_{}", idx);
module_exports[idx].insert("default".to_string(), default_name);
}
}
}
}
}
let mut statements = Vec::new();
for (idx, module) in modules.iter().enumerate() {
let dir = parent_dir(&module.path);
for stmt in &module.program.statements {
match stmt {
Statement::Import {
specifiers,
from,
span,
} => {
if is_native_import(from) {
let canonical_spec =
normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
for spec in specifiers {
match spec {
ImportSpecifier::Named {
name,
name_span,
alias,
alias_span,
} => {
let bind = alias.as_deref().unwrap_or(name.as_ref());
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
let init = Expr::NativeModuleLoad {
spec: Arc::from(canonical_spec.clone()),
export_name: name.clone(),
span: *span,
};
statements.push(Statement::VarDecl {
name: Arc::from(bind),
name_span: *decl_name_span,
mutable: false,
type_ann: None,
init: Some(init),
span: *span,
});
}
ImportSpecifier::Namespace { name, .. } => {
return Err(format!(
"Namespace import (* as {}) not supported for native module '{}'",
name.as_ref(),
from.as_ref()
));
}
ImportSpecifier::Default { name, .. } => {
return Err(format!(
"Default import '{}' not supported for native module '{}'. Use named import.",
name.as_ref(),
from.as_ref()
));
}
}
}
continue;
}
let dep_key = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)?;
let dep_idx = *path_to_idx
.get(&dep_key)
.ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
let dep_exports = &module_exports[dep_idx];
for spec in specifiers {
match spec {
ImportSpecifier::Named {
name,
name_span,
alias,
alias_span,
} => {
let source = dep_exports
.get(name.as_ref())
.cloned()
.unwrap_or_else(|| name.to_string());
let bind = alias.as_deref().unwrap_or(name.as_ref());
if bind != source {
let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
statements.push(Statement::VarDecl {
name: Arc::from(bind),
name_span: *decl_name_span,
mutable: false,
type_ann: None,
init: Some(Expr::Ident {
name: Arc::from(source),
span: *span,
}),
span: *span,
});
}
}
ImportSpecifier::Namespace { name, name_span } => {
let mut props = Vec::new();
for (k, v) in dep_exports {
props.push(tishlang_ast::ObjectProp::KeyValue(
Arc::from(k.clone()),
Expr::Ident {
name: Arc::from(v.clone()),
span: *span,
},
));
}
statements.push(Statement::VarDecl {
name: name.clone(),
name_span: *name_span,
mutable: false,
type_ann: None,
init: Some(Expr::Object { props, span: *span }),
span: *span,
});
}
ImportSpecifier::Default { name, name_span } => {
let source =
dep_exports.get("default").cloned().ok_or_else(|| {
format!("Module '{}' has no default export", from)
})?;
statements.push(Statement::VarDecl {
name: name.clone(),
name_span: *name_span,
mutable: false,
type_ann: None,
init: Some(Expr::Ident {
name: Arc::from(source),
span: *span,
}),
span: *span,
});
}
}
}
}
Statement::Export { declaration, .. } => match declaration.as_ref() {
ExportDeclaration::Named(s) => statements.push(*s.clone()),
ExportDeclaration::Default(e) => {
let default_name = format!("__default_{}", idx);
let espan = e.span();
statements.push(Statement::VarDecl {
name: Arc::from(default_name),
name_span: espan,
mutable: false,
type_ann: None,
init: Some((*e).clone()),
span: espan,
});
}
},
_ => statements.push(stmt.clone()),
}
}
}
Ok(Program { statements })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_virtual_simple_import() {
let mut files = HashMap::new();
files.insert(
"lib.tish".to_string(),
"export fn add(a, b) { return a + b }".to_string(),
);
files.insert(
"main.tish".to_string(),
"import { add } from \"./lib.tish\"\nconsole.log(add(1, 2))".to_string(),
);
let modules = resolve_virtual("main.tish", &files).unwrap();
assert_eq!(modules.len(), 2);
assert_eq!(modules[0].path, "lib.tish");
assert_eq!(modules[1].path, "main.tish");
detect_cycles_virtual(&modules).unwrap();
let program = merge_modules_virtual(modules).unwrap();
assert!(!program.statements.is_empty());
}
}