use crate::using::errors::UsingError; use crate::using::policy::UsingPolicy; use crate::using::spec::{PackageKind, UsingPackage}; use std::collections::HashMap; /// Populate using context vectors from nyash.toml (if present). /// Keeps behavior aligned with existing runner pipeline: /// - Adds [using.paths] entries to `using_paths` /// - Flattens [modules] into (name, path) pairs appended to `pending_modules` /// - Reads optional [aliases] table (k -> v) pub fn populate_from_toml( using_paths: &mut Vec, pending_modules: &mut Vec<(String, String)>, aliases: &mut HashMap, packages: &mut HashMap, ) -> Result { let mut policy = UsingPolicy::default(); // Prefer CWD nyash.toml; if missing, honor NYASH_ROOT/nyash.toml for tools that run from subdirs let (text, toml_path) = { // Prefer hakorune.toml, fallback to nyash.toml; check CWD then NYASH_ROOT let candidates = ["hakorune.toml", "nyash.toml"]; let mut found: Option<(String, std::path::PathBuf)> = None; // 1) Try current directory for name in candidates.iter() { let p = std::path::Path::new(name); if p.exists() { let txt = std::fs::read_to_string(p).map_err(|e| UsingError::ReadToml(e.to_string()))?; found = Some((txt, p.to_path_buf())); break; } } // 2) Try NYASH_ROOT if not found yet if found.is_none() { if let Ok(root) = std::env::var("NYASH_ROOT") { for name in candidates.iter() { let alt = std::path::Path::new(&root).join(name); if alt.exists() { let txt = std::fs::read_to_string(&alt) .map_err(|e| UsingError::ReadToml(e.to_string()))?; found = Some((txt, alt)); break; } } } } // 3) Fallback: empty content and path Ok(found.unwrap_or((String::new(), std::path::PathBuf::from("")))) }?; let doc = toml::from_str::(&text).map_err(|e| UsingError::ParseToml(e.to_string()))?; let toml_dir = toml_path .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| std::path::PathBuf::from(".")); // [modules] table flatten: supports nested namespaces (a.b.c = "path") if let Some(mods) = doc.get("modules").and_then(|v| v.as_table()) { fn visit(prefix: &str, tbl: &toml::value::Table, out: &mut Vec<(String, String)>) { for (k, v) in tbl.iter() { let name = if prefix.is_empty() { k.to_string() } else { format!("{}.{}", prefix, k) }; if let Some(s) = v.as_str() { out.push((name, s.to_string())); } else if let Some(t) = v.as_table() { visit(&name, t, out); } } } visit("", mods, pending_modules); if let Some(workspace_tbl) = mods.get("workspace").and_then(|v| v.as_table()) { load_workspace_modules(&toml_dir, workspace_tbl, pending_modules, aliases)?; } } // [using.paths] array if let Some(using_tbl) = doc.get("using").and_then(|v| v.as_table()) { // paths if let Some(paths_arr) = using_tbl.get("paths").and_then(|v| v.as_array()) { for p in paths_arr { if let Some(s) = p.as_str() { let s = s.trim(); if !s.is_empty() { using_paths.push(s.to_string()); policy.search_paths.push(s.to_string()); } } } } // aliases if let Some(alias_tbl) = using_tbl.get("aliases").and_then(|v| v.as_table()) { for (k, v) in alias_tbl.iter() { if let Some(target) = v.as_str() { aliases.insert(k.to_string(), target.to_string()); } } } // named packages: any subtable not paths/aliases is a package for (k, v) in using_tbl.iter() { if k == "paths" || k == "aliases" { continue; } if let Some(tbl) = v.as_table() { let kind = tbl .get("kind") .and_then(|x| x.as_str()) .map(PackageKind::from_str) .unwrap_or(PackageKind::Package); // path is required if let Some(path_s) = tbl.get("path").and_then(|x| x.as_str()) { let path = path_s.to_string(); let main = tbl .get("main") .and_then(|x| x.as_str()) .map(|s| s.to_string()); let bid = tbl .get("bid") .and_then(|x| x.as_str()) .map(|s| s.to_string()); packages.insert( k.to_string(), UsingPackage { kind, path, main, bid, }, ); } } } } // legacy top-level [aliases] also accepted (migration) if let Some(alias_tbl) = doc.get("aliases").and_then(|v| v.as_table()) { for (k, v) in alias_tbl.iter() { if let Some(target) = v.as_str() { aliases.insert(k.to_string(), target.to_string()); } } } Ok(policy) } /// Resolve a using target name into a concrete path token. /// - Returns plain file path for modules/package files /// - Returns a marker token `dylib:` for kind="dylib" packages /// - Searches relative to `context_dir` then `using_paths` for bare names /// - When `strict` and multiple candidates exist, returns Err pub fn resolve_using_target_common( tgt: &str, modules: &[(String, String)], using_paths: &[String], packages: &HashMap, context_dir: Option<&std::path::Path>, strict: bool, verbose: bool, ) -> Result { // 1) modules mapping if let Some((_, p)) = modules.iter().find(|(n, _)| n == tgt) { if verbose { eprintln!("[using/resolve] modules '{}' -> '{}'", tgt, p); } return Ok(p.clone()); } // 2) named packages if let Some(pkg) = packages.get(tgt) { match pkg.kind { PackageKind::Dylib => { let out = format!("dylib:{}", pkg.path); if verbose { eprintln!("[using/resolve] dylib '{}' -> '{}'", tgt, out); } return Ok(out); } PackageKind::Package => { let base = std::path::Path::new(&pkg.path); let out = if let Some(m) = &pkg.main { if matches!( base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako") ) { pkg.path.clone() } else { base.join(m).to_string_lossy().to_string() } } else { if matches!( base.extension().and_then(|s| s.to_str()), Some("nyash") | Some("hako") ) { pkg.path.clone() } else { let leaf = base.file_name().and_then(|s| s.to_str()).unwrap_or(tgt); let hako = base.join(format!("{}.hako", leaf)); if hako.exists() { hako.to_string_lossy().to_string() } else { base.join(format!("{}.hako", leaf)) .to_string_lossy() .to_string() } } }; if verbose { eprintln!("[using/resolve] package '{}' -> '{}'", tgt, out); } return Ok(out); } } } // 3) relative: prefer cwd > using_paths; .hako first then .nyash let rel_hako = tgt.replace('.', "/") + ".hako"; let rel_ny = tgt.replace('.', "/") + ".nyash"; let mut cand: Vec = Vec::new(); if let Some(dir) = context_dir { let c1 = dir.join(&rel_hako); if c1.exists() { cand.push(c1.to_string_lossy().to_string()); } let c2 = dir.join(&rel_ny); if c2.exists() { cand.push(c2.to_string_lossy().to_string()); } } for base in using_paths { let p = std::path::Path::new(base); let c1 = p.join(&rel_hako); if c1.exists() { cand.push(c1.to_string_lossy().to_string()); } let c2 = p.join(&rel_ny); if c2.exists() { cand.push(c2.to_string_lossy().to_string()); } } if cand.is_empty() { if verbose { eprintln!("[using] unresolved '{}' (searched: rel+paths)", tgt); } return Err(format!( "using: unresolved '{}': searched relative and using.paths", tgt )); } if cand.len() > 1 && strict { return Err(format!("ambiguous using '{}': {}", tgt, cand.join(", "))); } let out = cand.remove(0); if verbose { eprintln!("[using/resolve] '{}' -> '{}'", tgt, out); } Ok(out) } fn load_workspace_modules( nyash_dir: &std::path::Path, workspace_tbl: &toml::value::Table, pending_modules: &mut Vec<(String, String)>, aliases: &mut HashMap, ) -> Result<(), UsingError> { let members = workspace_tbl .get("members") .and_then(|v| v.as_array()) .ok_or_else(|| { UsingError::ParseWorkspaceModule( "modules.workspace".into(), "expected members array".into(), ) })?; for entry in members { let raw_path = entry.as_str().ok_or_else(|| { UsingError::ParseWorkspaceModule( "modules.workspace".into(), "members must be string paths".into(), ) })?; let module_path = if std::path::Path::new(raw_path).is_absolute() { std::path::PathBuf::from(raw_path) } else { nyash_dir.join(raw_path) }; let module_dir = module_path .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| nyash_dir.to_path_buf()); let module_text = std::fs::read_to_string(&module_path).map_err(|e| { UsingError::ReadWorkspaceModule( module_path.to_string_lossy().to_string(), e.to_string(), ) })?; let module_doc = toml::from_str::(&module_text).map_err(|e| { UsingError::ParseWorkspaceModule( module_path.to_string_lossy().to_string(), e.to_string(), ) })?; let module_name = module_doc .get("module") .and_then(|v| v.get("name")) .and_then(|v| v.as_str()) .ok_or_else(|| { UsingError::WorkspaceModuleMissingName(module_path.to_string_lossy().to_string()) })?; if let Some(exports_tbl) = module_doc.get("exports").and_then(|v| v.as_table()) { for (export_key, export_value) in exports_tbl { if let Some(rel_path) = export_value.as_str() { let mut full_name = module_name.to_string(); if !export_key.is_empty() { full_name.push('.'); full_name.push_str(export_key); } if pending_modules.iter().any(|(name, _)| name == &full_name) { continue; } let resolved_path = module_dir.join(rel_path); let resolved_str = resolved_path .canonicalize() .unwrap_or(resolved_path) .to_string_lossy() .to_string(); pending_modules.push((full_name, resolved_str)); } } } if let Some(alias_tbl) = module_doc.get("aliases").and_then(|v| v.as_table()) { for (alias, target) in alias_tbl { if let Some(target_str) = target.as_str() { aliases.insert(alias.to_string(), target_str.to_string()); } } } } Ok(()) }