···118118119119 let namespace = extract_namespace(&file_path);
120120121121- // Create workspace with standard library
122122- let mut workspace = match mlf_lang::Workspace::with_std() {
121121+ // Create workspace with standard library and .mlf cache
122122+ let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap())
123123+ .ok()
124124+ .map(|root| crate::config::get_mlf_cache_dir(&root));
125125+126126+ let mut workspace = match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) {
123127 Ok(ws) => ws,
124128 Err(e) => {
125129 errors.push((
126130 file_path.display().to_string(),
127127- format!("Failed to load standard library: {:?}", e),
131131+ format!("Failed to load workspace: {}", e),
128132 ));
129133 continue;
130134 }
+7-3
mlf-cli/src/generate/lexicon.rs
···89899090 let namespace = extract_namespace(&file_path);
91919292- // Create workspace with standard library for inline type resolution
9393- let mut workspace = match mlf_lang::Workspace::with_std() {
9292+ // Create workspace with standard library and .mlf cache for inline type resolution
9393+ let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap())
9494+ .ok()
9595+ .map(|root| crate::config::get_mlf_cache_dir(&root));
9696+9797+ let mut workspace = match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) {
9498 Ok(ws) => ws,
9599 Err(e) => {
9696- errors.push((file_path.display().to_string(), format!("Failed to load standard library: {:?}", e)));
100100+ errors.push((file_path.display().to_string(), format!("Failed to load workspace: {}", e)));
97101 continue;
98102 }
99103 };
+1
mlf-cli/src/main.rs
···77mod config;
88mod fetch;
99mod generate;
1010+mod workspace_ext;
10111112// Import optional code generator plugins
1213// These are automatically registered via inventory when the feature is enabled
+141
mlf-cli/src/workspace_ext.rs
···11+/// Extensions to Workspace for CLI-specific functionality like loading from filesystem
22+use mlf_lang::Workspace;
33+use std::path::Path;
44+55+/// Load MLF files from a directory into the workspace
66+pub fn load_mlf_directory(workspace: &mut Workspace, dir: &Path) -> Result<(), String> {
77+ if !dir.exists() {
88+ // Directory doesn't exist, that's ok - just return
99+ return Ok(());
1010+ }
1111+1212+ // Recursively find all .mlf files
1313+ let mlf_files = find_mlf_files(dir)?;
1414+1515+ for file_path in mlf_files {
1616+ // Read the file
1717+ let content = std::fs::read_to_string(&file_path)
1818+ .map_err(|e| format!("Failed to read {}: {}", file_path.display(), e))?;
1919+2020+ // Convert file path to namespace
2121+ // e.g., ".mlf/lexicons/mlf/stream.place.mlf" -> "stream.place"
2222+ let namespace = extract_namespace_from_path(&file_path, dir)?;
2323+2424+ // Parse the lexicon
2525+ let lexicon = mlf_lang::parse_lexicon(&content)
2626+ .map_err(|e| format!("Failed to parse {}: {:?}", file_path.display(), e))?;
2727+2828+ // Add to workspace (merge if already exists)
2929+ if workspace.has_module(&namespace) {
3030+ // Module already exists, we could merge or skip
3131+ // For now, skip to avoid conflicts
3232+ continue;
3333+ }
3434+3535+ workspace
3636+ .add_module(namespace.clone(), lexicon)
3737+ .map_err(|e| format!("Failed to add module {}: {:?}", namespace, e))?;
3838+ }
3939+4040+ Ok(())
4141+}
4242+4343+/// Find all .mlf files recursively in a directory
4444+fn find_mlf_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, String> {
4545+ let mut files = Vec::new();
4646+4747+ let entries = std::fs::read_dir(dir)
4848+ .map_err(|e| format!("Failed to read directory {}: {}", dir.display(), e))?;
4949+5050+ for entry in entries {
5151+ let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
5252+ let path = entry.path();
5353+5454+ if path.is_dir() {
5555+ // Recurse into subdirectory
5656+ files.extend(find_mlf_files(&path)?);
5757+ } else if path.extension().and_then(|s| s.to_str()) == Some("mlf") {
5858+ files.push(path);
5959+ }
6060+ }
6161+6262+ Ok(files)
6363+}
6464+6565+/// Extract namespace from file path relative to base directory
6666+/// e.g., base=".mlf/lexicons/mlf", path=".mlf/lexicons/mlf/stream.place.mlf" -> "stream.place"
6767+fn extract_namespace_from_path(path: &Path, base: &Path) -> Result<String, String> {
6868+ let relative = path
6969+ .strip_prefix(base)
7070+ .map_err(|e| format!("Failed to strip prefix: {}", e))?;
7171+7272+ // Convert path to string
7373+ let path_str = relative
7474+ .to_str()
7575+ .ok_or_else(|| "Non-UTF8 path".to_string())?;
7676+7777+ // Remove .mlf extension
7878+ let without_ext = path_str
7979+ .strip_suffix(".mlf")
8080+ .unwrap_or(path_str);
8181+8282+ // Replace path separators with dots
8383+ // e.g., "stream/place/foo.mlf" -> "stream.place.foo"
8484+ let namespace = without_ext.replace(std::path::MAIN_SEPARATOR, ".");
8585+8686+ Ok(namespace)
8787+}
8888+8989+/// Create a workspace with std library AND .mlf cache if it exists
9090+pub fn workspace_with_std_and_cache(
9191+ mlf_cache_dir: Option<&Path>,
9292+) -> Result<Workspace, String> {
9393+ // Start with std library
9494+ let mut workspace = Workspace::with_std()
9595+ .map_err(|e| format!("Failed to load std library: {:?}", e))?;
9696+9797+ // Load from .mlf cache if provided
9898+ if let Some(cache_dir) = mlf_cache_dir {
9999+ let mlf_dir = cache_dir.join("lexicons/mlf");
100100+ load_mlf_directory(&mut workspace, &mlf_dir)?;
101101+ }
102102+103103+ Ok(workspace)
104104+}
105105+106106+/// Extension trait for Workspace to check if a module exists
107107+pub trait WorkspaceExt {
108108+ fn has_module(&self, namespace: &str) -> bool;
109109+}
110110+111111+impl WorkspaceExt for Workspace {
112112+ fn has_module(&self, namespace: &str) -> bool {
113113+ // This requires exposing the modules field or adding a method to mlf-lang
114114+ // For now, we'll just try to add and catch the error
115115+ // TODO: Add a proper has_module method to Workspace
116116+ false
117117+ }
118118+}
119119+120120+#[cfg(test)]
121121+mod tests {
122122+ use super::*;
123123+124124+ #[test]
125125+ fn test_extract_namespace() {
126126+ let base = Path::new(".mlf/lexicons/mlf");
127127+ let path = Path::new(".mlf/lexicons/mlf/stream.place.mlf");
128128+ let namespace = extract_namespace_from_path(path, base).unwrap();
129129+ assert_eq!(namespace, "stream.place");
130130+ }
131131+132132+ #[test]
133133+ fn test_extract_namespace_nested() {
134134+ let base = Path::new(".mlf/lexicons/mlf");
135135+ let path = Path::new(".mlf/lexicons/mlf/com/atproto/admin/defs.mlf");
136136+ let namespace = extract_namespace_from_path(path, base).unwrap();
137137+ // On Unix: "com/atproto/admin/defs" -> "com.atproto.admin.defs"
138138+ // Note: This depends on the directory structure
139139+ assert!(namespace.contains("com"));
140140+ }
141141+}