modular-module extension framework continued

This commit is contained in:
jess 2026-05-30 14:54:53 -07:00
parent 853a9fd0cc
commit 9f2e2ff687
9 changed files with 227 additions and 70 deletions

View File

@ -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<String>,
pub segments: Vec<String>,
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<Depende
}
indent(out, depth, "}");
}
Stmt::Use(module, item) => {
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"]);
}
}

View File

@ -156,7 +156,7 @@ pub fn evaluate_modules(sources: &[ModuleSource]) -> Vec<ModuleResult> {
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<ModuleResult> {
}
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);
}
}
}

View File

@ -21,7 +21,7 @@ pub enum Stmt {
body: Vec<Stmt>,
},
Return(Expr),
Use(String, Option<String>),
Use(Vec<String>, bool),
CellAssign {
block: Option<String>,
table: String,

View File

@ -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)
}

View File

@ -18,8 +18,14 @@ pub struct ModuleExports {
#[derive(Debug, Clone, PartialEq)]
pub struct UseDecl {
pub module: String,
pub item: Option<String>,
pub segments: Vec<String>,
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<UseDecl> {
}
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<bool, String> {
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<ModuleExports, String> {
let mut sub = Interpreter::new();
sub.module_paths = self.module_paths.clone();

View File

@ -290,22 +290,22 @@ impl Parser {
pub(crate) fn parse_use(&mut self) -> Result<Stmt, String> {
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<Stmt, String> {

View File

@ -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();

View File

@ -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]

View File

@ -485,14 +485,15 @@ impl super::EditorState {
visited: &mut std::collections::HashSet<String>,
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);
}
}