diff --git a/compile/src/lib.rs b/compile/src/lib.rs index 97f2a05..262fa1f 100644 --- a/compile/src/lib.rs +++ b/compile/src/lib.rs @@ -23,10 +23,14 @@ impl DecomposeHook for NoHook {} /// external module dependency discovered during decomposition. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Dependency { - /// module name (note filename stem, hyphen-form). - pub module: String, - /// specific item imported, or None / Some("*") for the whole module. - pub item: Option, + pub segments: Vec, + pub wildcard: bool, +} + +impl Dependency { + pub fn root(&self) -> &str { + self.segments.first().map(|s| s.as_str()).unwrap_or("") + } } /// result of decomposing a single cordial source. @@ -155,13 +159,20 @@ fn emit_stmt(out: &mut String, stmt: &Stmt, depth: usize, deps: &mut Vec { - let mod_ident = module.replace('-', "_"); - deps.push(Dependency { module: module.clone(), item: item.clone() }); + Stmt::Use(segments, wildcard) => { + deps.push(Dependency { segments: segments.clone(), wildcard: *wildcard }); + let Some(root) = segments.first() else { return Ok(()); }; + let mod_ident = root.replace('-', "_"); indent(out, depth, &format!("mod {};", mod_ident)); - match item.as_deref() { - None | Some("*") => indent(out, depth, &format!("use {}::*;", mod_ident)), - Some(name) => indent(out, depth, &format!("use {}::{};", mod_ident, ident(name))), + let mut path = mod_ident.clone(); + for seg in segments.iter().skip(1) { + path.push_str("::"); + path.push_str(&ident(seg)); + } + if *wildcard || segments.len() == 1 { + indent(out, depth, &format!("use {}::*;", path)); + } else { + indent(out, depth, &format!("use {};", path)); } } Stmt::CellAssign { table, cell, value, .. } => { @@ -578,8 +589,8 @@ mod tests { assert!(r.code.contains("mod math;")); assert!(r.code.contains("use math::*;")); assert_eq!(r.deps.len(), 1); - assert_eq!(r.deps[0].module, "math"); - assert_eq!(r.deps[0].item, None); + assert_eq!(r.deps[0].segments, vec!["math".to_string()]); + assert!(!r.deps[0].wildcard); } #[test] @@ -587,7 +598,7 @@ mod tests { let r = dec_full("use utils::double"); assert!(r.code.contains("mod utils;")); assert!(r.code.contains("use utils::double;")); - assert_eq!(r.deps[0].item.as_deref(), Some("double")); + assert_eq!(r.deps[0].segments, vec!["utils".to_string(), "double".to_string()]); } #[test] @@ -595,6 +606,14 @@ mod tests { let r = dec_full("use my_lib"); assert!(r.code.contains("mod my_lib;")); assert!(r.code.contains("use my_lib::*;")); - assert_eq!(r.deps[0].module, "my_lib"); + assert_eq!(r.deps[0].root(), "my_lib"); + } + + #[test] + fn use_n_depth_path_emits_full_chain() { + let r = dec_full("use sitter::scene::primitives"); + assert!(r.code.contains("mod sitter;")); + assert!(r.code.contains("use sitter::scene::primitives;")); + assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]); } } diff --git a/core/src/eval.rs b/core/src/eval.rs index 90e8bed..b29d815 100644 --- a/core/src/eval.rs +++ b/core/src/eval.rs @@ -156,7 +156,7 @@ pub fn evaluate_modules(sources: &[ModuleSource]) -> Vec { for (i, decls) in use_decls.iter().enumerate() { for decl in decls { - if let Some(&dep_idx) = name_to_idx.get(decl.module.as_str()) { + if let Some(&dep_idx) = name_to_idx.get(decl.root()) { if dep_idx != i { dependents[dep_idx].push(i); in_degree[i] += 1; @@ -211,17 +211,11 @@ pub fn evaluate_modules(sources: &[ModuleSource]) -> Vec { } for decl in &use_decls[idx] { - if let Some(module_exports) = exports_by_name.get(&decl.module) { - match &decl.item { - Some(s) if s == "*" => { - interp.import_all(module_exports); - } - None => { - interp.import_all(module_exports); - } - Some(item) => { - interp.import_item(module_exports, item); - } + if let Some(module_exports) = exports_by_name.get(decl.root()) { + if decl.wildcard || decl.segments.len() < 2 { + interp.import_all(module_exports); + } else if let Some(item) = decl.segments.last() { + interp.import_item(module_exports, item); } } } diff --git a/core/src/interp/ast.rs b/core/src/interp/ast.rs index b599bec..c502e3f 100644 --- a/core/src/interp/ast.rs +++ b/core/src/interp/ast.rs @@ -21,7 +21,7 @@ pub enum Stmt { body: Vec, }, Return(Expr), - Use(String, Option), + Use(Vec, bool), CellAssign { block: Option, table: String, diff --git a/core/src/interp/eval/stmt.rs b/core/src/interp/eval/stmt.rs index cde1ced..a171beb 100644 --- a/core/src/interp/eval/stmt.rs +++ b/core/src/interp/eval/stmt.rs @@ -154,19 +154,36 @@ impl Interpreter { self.return_slot = Some(val); Err("\x00return".to_string()) } - Stmt::Use(module, item) => { - if let Some(name) = item.as_deref().filter(|s| *s != "*") { - let has_sub = self.module_paths.borrow().has_sub(module, name); + Stmt::Use(segments, wildcard) => { + if segments.is_empty() { + return Ok(Value::Void); + } + let segs: Vec<&str> = segments.iter().map(|s| s.as_str()).collect(); + if *wildcard { + if self.resolve_module_path(&segs).is_some() { + self.load_module(&segs, None)?; + } + return Ok(Value::Void); + } + if segments.len() >= 2 { + let (parent, last) = segs.split_at(segs.len() - 1); + let last = last[0]; + let has_sub = parent.len() == 1 + && self.module_paths.borrow().has_sub(parent[0], last); if has_sub { - let sub = [module.as_str(), name]; - self.load_module(&sub, None)?; + self.load_module(&segs, None)?; return Ok(Value::Void); } + if self.try_load_submodule(&segs)? { + return Ok(Value::Void); + } + if self.resolve_module_path(parent).is_some() { + self.load_module(parent, Some(last))?; + } + return Ok(Value::Void); } - let top = [module.as_str()]; - if self.resolve_module_path(&top).is_some() { - let filter = item.as_deref().filter(|s| *s != "*"); - self.load_module(&top, filter)?; + if self.resolve_module_path(&segs).is_some() { + self.load_module(&segs, None)?; } Ok(Value::Void) } diff --git a/core/src/interp/modules.rs b/core/src/interp/modules.rs index 1c9ccc0..df64670 100644 --- a/core/src/interp/modules.rs +++ b/core/src/interp/modules.rs @@ -18,8 +18,14 @@ pub struct ModuleExports { #[derive(Debug, Clone, PartialEq)] pub struct UseDecl { - pub module: String, - pub item: Option, + pub segments: Vec, + pub wildcard: bool, +} + +impl UseDecl { + pub fn root(&self) -> &str { + self.segments.first().map(|s| s.as_str()).unwrap_or("") + } } impl Interpreter { @@ -89,8 +95,8 @@ pub fn extract_use_declarations(text: &str) -> Vec { } let Ok(tokens) = tokenize(trimmed, false) else { continue }; let mut parser = Parser::new(tokens); - if let Ok(Stmt::Use(module, item)) = parser.parse_use() { - decls.push(UseDecl { module, item }); + if let Ok(Stmt::Use(segments, wildcard)) = parser.parse_use() { + decls.push(UseDecl { segments, wildcard }); } } decls @@ -115,6 +121,20 @@ impl Interpreter { Ok(()) } + pub fn try_load_submodule(&mut self, segments: &[&str]) -> Result { + let path = match self.resolve_module_path(segments) { + Some(p) => p, + None => return Ok(false), + }; + let source = match read_module_source(&path) { + Ok(s) => s, + Err(_) => return Ok(false), + }; + let exports = self.exec_in_subinterp(&source)?; + self.import_all(&exports); + Ok(true) + } + fn exec_in_subinterp(&self, source: &str) -> Result { let mut sub = Interpreter::new(); sub.module_paths = self.module_paths.clone(); diff --git a/core/src/interp/parse/stmt.rs b/core/src/interp/parse/stmt.rs index 473a5e2..de7c472 100644 --- a/core/src/interp/parse/stmt.rs +++ b/core/src/interp/parse/stmt.rs @@ -290,22 +290,22 @@ impl Parser { pub(crate) fn parse_use(&mut self) -> Result { self.expect(&Token::Use)?; - let module = match self.advance() { - Token::Ident(name) => name, + let mut segments = Vec::new(); + let mut wildcard = false; + match self.advance() { + Token::Ident(name) => segments.push(name), other => return Err(format!("expected module name after 'use', got {:?}", other)), - }; - let item = if self.peek() == &Token::ColonColon { + } + while self.peek() == &Token::ColonColon { self.advance(); match self.advance() { - Token::Ident(name) => Some(name), - Token::Star => Some("*".to_string()), - other => return Err(format!("expected item name after '::', got {:?}", other)), + Token::Ident(name) => segments.push(name), + Token::Star => { wildcard = true; break; } + other => return Err(format!("expected name or '*' after '::', got {:?}", other)), } - } else { - None - }; + } self.skip_newlines(); - Ok(Stmt::Use(module, item)) + Ok(Stmt::Use(segments, wildcard)) } fn parse_trait_def(&mut self) -> Result { diff --git a/core/src/interp/tests/loader.rs b/core/src/interp/tests/loader.rs index 60a36b0..2893b07 100644 --- a/core/src/interp/tests/loader.rs +++ b/core/src/interp/tests/loader.rs @@ -96,6 +96,41 @@ fn use_wildcard_imports_everything() { fs::remove_dir_all(&dir).ok(); } +#[test] +fn use_module_colon_submodule_loads_sibling_cord_file() { + let dir = tmp_dir("sibling"); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("primitives.cord"), "let standard_outer = 9.8\nfn make_toroid() { 42 }").unwrap(); + fs::write(dir.join("coils.cord"), "let standard_turns = 100").unwrap(); + + let mut i = Interpreter::new(); + i.add_module_path("sitter", &dir); + i.exec("use sitter::primitives").unwrap(); + + assert!(matches!(i.get_var("standard_outer"), Some(Value::Number(n)) if (n - 9.8).abs() < 1e-9)); + let v = i.eval("make_toroid()").unwrap(); + assert!(matches!(v, Value::Number(n) if n == 42.0)); + // coils.cord should NOT be loaded by `use sitter::primitives` + assert!(i.get_var("standard_turns").is_none()); + + fs::remove_dir_all(&dir).ok(); +} + +#[test] +fn use_falls_back_to_item_filter_when_no_submodule_on_disk() { + let dir = tmp_dir("itemfilter"); + fs::write(dir.join("utils.cord"), "let alpha = 1\nlet beta = 2").unwrap(); + + let mut i = Interpreter::new(); + i.add_module_path("utils", dir.join("utils.cord")); + i.exec("use utils::beta").unwrap(); + + assert!(matches!(i.get_var("beta"), Some(Value::Number(n)) if n == 2.0)); + assert!(i.get_var("alpha").is_none()); + + fs::remove_dir_all(&dir).ok(); +} + #[test] fn sub_namespace_override_loads_distinct_source() { let dir = tmp_dir("subns"); @@ -113,6 +148,56 @@ fn sub_namespace_override_loads_distinct_source() { fs::remove_dir_all(&dir).ok(); } +#[test] +fn use_three_segment_path_loads_nested_submodule() { + let dir = tmp_dir("ndepth3"); + let scene = dir.join("scene"); + fs::create_dir_all(&scene).unwrap(); + fs::write(scene.join("primitives.cord"), "let toroid_outer = 12.5").unwrap(); + + let mut i = Interpreter::new(); + i.add_module_path("sitter", &dir); + i.exec("use sitter::scene::primitives").unwrap(); + + assert!(matches!(i.get_var("toroid_outer"), Some(Value::Number(n)) if (n - 12.5).abs() < 1e-9)); + + fs::remove_dir_all(&dir).ok(); +} + +#[test] +fn use_four_segment_path_walks_hierarchy() { + let dir = tmp_dir("ndepth4"); + let nested = dir.join("a").join("b").join("c"); + fs::create_dir_all(&nested).unwrap(); + fs::write(nested.join("d.cord"), "let final_value = 7").unwrap(); + + let mut i = Interpreter::new(); + i.add_module_path("a", &dir.join("a")); + i.exec("use a::b::c::d").unwrap(); + + assert!(matches!(i.get_var("final_value"), Some(Value::Number(n)) if n == 7.0)); + + fs::remove_dir_all(&dir).ok(); +} + +#[test] +fn use_wildcard_at_depth_imports_all() { + let dir = tmp_dir("wilddepth"); + let scene = dir.join("scene"); + fs::create_dir_all(&scene).unwrap(); + fs::write(scene.join("a.cord"), "let alpha = 1").unwrap(); + fs::write(scene.join("b.cord"), "let beta = 2").unwrap(); + + let mut i = Interpreter::new(); + i.add_module_path("sitter", &dir); + i.exec("use sitter::scene::*").unwrap(); + + assert!(matches!(i.get_var("alpha"), Some(Value::Number(n)) if n == 1.0)); + assert!(matches!(i.get_var("beta"), Some(Value::Number(n)) if n == 2.0)); + + fs::remove_dir_all(&dir).ok(); +} + #[test] fn unregistered_module_no_ops() { let mut i = Interpreter::new(); diff --git a/core/src/interp/tests/modules.rs b/core/src/interp/tests/modules.rs index f95fa55..631da84 100644 --- a/core/src/interp/tests/modules.rs +++ b/core/src/interp/tests/modules.rs @@ -15,10 +15,10 @@ use super::helpers::*; let text = "let x = 5\nuse calculations\nSome prose\nuse budget::ramp\n/= x"; let decls = extract_use_declarations(text); assert_eq!(decls.len(), 2); - assert_eq!(decls[0].module, "calculations"); - assert_eq!(decls[0].item, None); - assert_eq!(decls[1].module, "budget"); - assert_eq!(decls[1].item, Some("ramp".to_string())); + assert_eq!(decls[0].segments, vec!["calculations"]); + assert!(!decls[0].wildcard); + assert_eq!(decls[1].segments, vec!["budget", "ramp"]); + assert!(!decls[1].wildcard); } #[test] @@ -26,7 +26,20 @@ use super::helpers::*; let text = "use\nuse 123\nuse valid_module"; let decls = extract_use_declarations(text); assert_eq!(decls.len(), 1); - assert_eq!(decls[0].module, "valid_module"); + assert_eq!(decls[0].segments, vec!["valid_module"]); + } + + #[test] + fn extract_use_supports_n_depth_paths() { + let text = "use a::b::c\nuse a::b::*\nuse a::b::c::d"; + let decls = extract_use_declarations(text); + assert_eq!(decls.len(), 3); + assert_eq!(decls[0].segments, vec!["a", "b", "c"]); + assert!(!decls[0].wildcard); + assert_eq!(decls[1].segments, vec!["a", "b"]); + assert!(decls[1].wildcard); + assert_eq!(decls[2].segments, vec!["a", "b", "c", "d"]); + assert!(!decls[2].wildcard); } #[test] diff --git a/viewport/src/editor/eval.rs b/viewport/src/editor/eval.rs index 7f37b39..76e68cd 100644 --- a/viewport/src/editor/eval.rs +++ b/viewport/src/editor/eval.rs @@ -485,14 +485,15 @@ impl super::EditorState { visited: &mut std::collections::HashSet, target: &mut acord_core::interp::Interpreter, ) { - if Self::is_builtin_use(&decl.module) { return; } + let root = decl.root(); + if Self::is_builtin_use(root) { return; } - if let Some(local) = self.modules.iter().find(|m| m.name == decl.module) { + if let Some(local) = self.modules.iter().find(|m| m.name == root) { let exports = self.resolve_module_exports(local, visited); - match &decl.item { - None => target.import_all(&exports), - Some(s) if s == "*" => target.import_all(&exports), - Some(item) => { target.import_item(&exports, item); } + if decl.wildcard || decl.segments.len() < 2 { + target.import_all(&exports); + } else if let Some(item) = decl.segments.last() { + target.import_item(&exports, item); } return; } @@ -509,11 +510,12 @@ impl super::EditorState { ) { use acord_core::interp; - let cycle_key = format!("__ext__{}", decl.module); + let root = decl.root(); + let cycle_key = format!("__ext__{}", root); if !visited.insert(cycle_key) { return; } let dir = self.notes_dir(); - let path = dir.join(format!("{}.md", decl.module)); + let path = dir.join(format!("{}.md", root)); let Ok(bytes) = std::fs::read(&path) else { return }; let (text_bytes, _archive) = crate::sidecar::extract_from_md(&bytes); let Ok(raw) = String::from_utf8(text_bytes) else { return }; @@ -526,8 +528,15 @@ impl super::EditorState { let ext_infos = build_block_infos_from(&ext_blocks); let ext_modules = crate::module::compute_modules(&ext_infos); - let submodule_match = decl.item.as_deref().and_then(|item| { - if item == "*" { return None; } + let item_name: Option<&str> = if decl.wildcard { + None + } else if decl.segments.len() >= 2 { + Some(decl.segments[1].as_str()) + } else { + None + }; + + let submodule_match = item_name.and_then(|item| { ext_modules.iter().find(|m| m.name == item) }); @@ -554,10 +563,10 @@ impl super::EditorState { } crate::eval::evaluate_document_with_interp(&mut nested, &clean); let exports = nested.exports(); - match &decl.item { - None => target.import_all(&exports), - Some(s) if s == "*" => target.import_all(&exports), - Some(item) => { target.import_item(&exports, item); } + if decl.wildcard || item_name.is_none() { + target.import_all(&exports); + } else if let Some(item) = item_name { + target.import_item(&exports, item); } }