modular-module extension framework continued
This commit is contained in:
parent
853a9fd0cc
commit
9f2e2ff687
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue