From 3a2f4f9cda49c5fbf885dd4c8663ace99e7261e8 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Mon, 18 May 2026 21:10:57 -0500 Subject: [PATCH] fix: fix import resolution in check and generate commands. --- mlf-cli/src/generate/code.rs | 67 +++++++++--------- mlf-cli/src/generate/lexicon.rs | 75 ++++++++++++--------- mlf-lang/src/workspace.rs | 116 +++++++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 62 deletions(-) diff --git a/mlf-cli/src/generate/code.rs b/mlf-cli/src/generate/code.rs index bc297b3..7088026 100644 --- a/mlf-cli/src/generate/code.rs +++ b/mlf-cli/src/generate/code.rs @@ -164,11 +164,29 @@ pub fn run( source, })?; + // Create shared workspace with standard library and .mlf cache + let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) + .ok() + .map(|root| crate::config::get_mlf_cache_dir(&root)); + + let mut workspace = + match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { + Ok(ws) => ws, + Err(e) => { + return Err(GenerateError::ParseLexicon { + path: "workspace".to_string(), + help: Some(format!("Failed to load workspace: {}", e)), + }); + } + }; + + // Phase 1: Parse all input files and add them to the shared workspace let mut errors = Vec::new(); let mut success_count = 0; + let mut parsed_files: Vec<(PathBuf, String, String, mlf_lang::Lexicon)> = Vec::new(); - for file_path in file_paths { - let source = match std::fs::read_to_string(&file_path) { + for file_path in &file_paths { + let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(e) => { errors.push(( @@ -187,7 +205,7 @@ pub fn run( } }; - let namespace = match extract_namespace(&file_path, &root_dir) { + let namespace = match extract_namespace(file_path, &root_dir) { Ok(ns) => ns, Err(e) => { errors.push(( @@ -198,24 +216,7 @@ pub fn run( } }; - // Create workspace with standard library and .mlf cache - let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) - .ok() - .map(|root| crate::config::get_mlf_cache_dir(&root)); - - let mut workspace = - match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { - Ok(ws) => ws, - Err(e) => { - errors.push(( - file_path.display().to_string(), - format!("Failed to load workspace: {}", e), - )); - continue; - } - }; - - // Add the module to the workspace + // Add the module to the shared workspace if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { errors.push(( file_path.display().to_string(), @@ -224,19 +225,23 @@ pub fn run( continue; } - // Resolve types - if let Err(e) = workspace.resolve() { - errors.push(( - file_path.display().to_string(), - format!("Type resolution error: {:?}", e), - )); - continue; - } + parsed_files.push((file_path.clone(), namespace, source, lexicon)); + } + + // Phase 2: Resolve all types in the shared workspace + if let Err(e) = workspace.resolve() { + errors.push(( + "workspace".to_string(), + format!("Type resolution error: {:?}", e), + )); + } + // Phase 3: Generate code for each file using the resolved workspace + for (file_path, namespace, _source, lexicon) in &parsed_files { // Generate code using the selected generator let ctx = GeneratorContext { - namespace: &namespace, - lexicon: &lexicon, + namespace, + lexicon, workspace: &workspace, }; diff --git a/mlf-cli/src/generate/lexicon.rs b/mlf-cli/src/generate/lexicon.rs index c8e7680..02eeb15 100644 --- a/mlf-cli/src/generate/lexicon.rs +++ b/mlf-cli/src/generate/lexicon.rs @@ -117,11 +117,30 @@ pub fn run( source, })?; + // Create shared workspace with standard library and .mlf cache for inline type resolution + let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) + .ok() + .map(|root| crate::config::get_mlf_cache_dir(&root)); + + let mut workspace = + match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { + Ok(ws) => ws, + Err(e) => { + return Err(GenerateError::ParseLexicon { + path: "workspace".to_string(), + help: Some(format!("Failed to load workspace: {}", e)), + }); + } + }; + let mut errors = Vec::new(); let mut success_count = 0; - for file_path in file_paths { - let source = match std::fs::read_to_string(&file_path) { + // Phase 1: Parse all input files and add them to the shared workspace + let mut parsed_files: Vec<(PathBuf, String, mlf_lang::Lexicon)> = Vec::new(); + + for file_path in &file_paths { + let source = match std::fs::read_to_string(file_path) { Ok(s) => s, Err(source) => { errors.push(( @@ -140,26 +159,18 @@ pub fn run( } }; - let namespace = extract_namespace(&file_path, &root_dir)?; - - // Create workspace with standard library and .mlf cache for inline type resolution - let mlf_cache_dir = crate::config::find_project_root(&std::env::current_dir().unwrap()) - .ok() - .map(|root| crate::config::get_mlf_cache_dir(&root)); - - let mut workspace = - match crate::workspace_ext::workspace_with_std_and_cache(mlf_cache_dir.as_deref()) { - Ok(ws) => ws, - Err(e) => { - errors.push(( - file_path.display().to_string(), - format!("Failed to load workspace: {}", e), - )); - continue; - } - }; + let namespace = match extract_namespace(file_path, &root_dir) { + Ok(ns) => ns, + Err(e) => { + errors.push(( + file_path.display().to_string(), + format!("Failed to extract namespace: {}", e), + )); + continue; + } + }; - // Add the module to the workspace + // Add the module to the shared workspace if let Err(e) = workspace.add_module(namespace.clone(), lexicon.clone()) { errors.push(( file_path.display().to_string(), @@ -168,16 +179,20 @@ pub fn run( continue; } - // Resolve types - if let Err(e) = workspace.resolve() { - errors.push(( - file_path.display().to_string(), - format!("Type resolution error: {:?}", e), - )); - continue; - } + parsed_files.push((file_path.clone(), namespace, lexicon)); + } + + // Phase 2: Resolve all types in the shared workspace + if let Err(e) = workspace.resolve() { + errors.push(( + "workspace".to_string(), + format!("Type resolution error: {:?}", e), + )); + } - let output = mlf_codegen::generate_lexicon(&namespace, &lexicon, &workspace); + // Phase 3: Generate lexicon JSON for each file using the resolved workspace + for (file_path, namespace, lexicon) in &parsed_files { + let output = mlf_codegen::generate_lexicon(namespace, lexicon, &workspace); for warning in &output.warnings { eprintln!(" warning: {}: {}", warning.namespace, warning.message); diff --git a/mlf-lang/src/workspace.rs b/mlf-lang/src/workspace.rs index 8d49b59..a2c16b7 100644 --- a/mlf-lang/src/workspace.rs +++ b/mlf-lang/src/workspace.rs @@ -1065,7 +1065,32 @@ impl Workspace { .keys() .any(|ns| ns.starts_with(&alloc::format!("{}.", target_namespace))); - if !namespace_exists && !has_children { + // When `use namespace.typename;` (no `as`, no `.{`) is parsed, the + // entire path becomes the target_namespace under UseImports::All. + // If that doesn't exist as a module, check whether the parent path + // (all segments except the last) is a module whose symbol table + // contains the last segment as a type name. If so, this is really + // a single-type import, not a namespace alias. + let mut is_implicit_type_import = false; + let mut type_import_name = String::new(); + + if !namespace_exists && !has_children && use_stmt.path.segments.len() >= 2 { + let parent_namespace = use_stmt.path.segments[..use_stmt.path.segments.len() - 1] + .iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("."); + let candidate_type = &use_stmt.path.segments.last().unwrap().name; + + if let Some(parent_module) = self.modules.get(&parent_namespace) { + if parent_module.symbols.types.contains_key(candidate_type) { + is_implicit_type_import = true; + type_import_name = candidate_type.clone(); + } + } + } + + if !namespace_exists && !has_children && !is_implicit_type_import { errors.push(ValidationError::UndefinedReference { name: target_namespace.clone(), span: use_stmt.path.span, @@ -1095,6 +1120,11 @@ impl Workspace { // e.g., "use com.example;" creates alias "example" -> "com.example" Some((namespace_suffix.to_string(), target_namespace.clone())) } + } else if is_implicit_type_import { + // Module doesn't exist, but the parent module has a type + // matching the last segment — this is a type import, not + // a namespace alias. + None } else { // Module doesn't exist but has children - create namespace alias // e.g., "use com.example;" with only "com.example.defs" module @@ -1135,6 +1165,21 @@ impl Workspace { // Namespace alias only, no type imports vec![] } + } else if is_implicit_type_import { + // The entire path was parsed as a namespace, but it's + // actually `use namespace.typename;` — import that type. + // Build original_path from the full path segments. + let imported = ImportedSymbol { + original_path: use_stmt + .path + .segments + .iter() + .map(|s| s.name.clone()) + .collect(), + local_name: type_import_name.clone(), + span: use_stmt.path.span, + }; + vec![(type_import_name.clone(), imported)] } else { // Module doesn't exist - namespace alias only vec![] @@ -2645,4 +2690,73 @@ mod tests { let imports = ws.get_imports("com.example.app"); assert_eq!(imports.len(), 0); } + + #[test] + fn test_use_bare_single_type_import() { + let mut ws = Workspace::new(); + + // Create a source module with a type definition + let defs = parse_lexicon( + r#" + def type exampleDef = string; + "#, + ) + .unwrap(); + ws.add_module("lol.mlf.defs".into(), defs).unwrap(); + + // Import the type using the bare `use namespace.typename;` syntax + // (no `as` alias, no `.{` brace syntax) + let app = parse_lexicon( + r#" + use lol.mlf.defs.exampleDef; + + record package { + example!: exampleDef, + } + "#, + ) + .unwrap(); + ws.add_module("lol.mlf.package".into(), app).unwrap(); + + let result = ws.resolve(); + if let Err(ref e) = result { + eprintln!("Errors: {:?}", e); + } + assert!(result.is_ok()); + + // Verify the type was imported + let imports = ws.get_imports("lol.mlf.package"); + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].0, "exampleDef"); + } + + #[test] + fn test_use_bare_single_type_import_undefined() { + let mut ws = Workspace::new(); + + // Create a source module WITHOUT the requested type + let defs = parse_lexicon( + r#" + def type someOtherType = string; + "#, + ) + .unwrap(); + ws.add_module("lol.mlf.defs".into(), defs).unwrap(); + + // Try to import a type that doesn't exist in the source module + let app = parse_lexicon( + r#" + use lol.mlf.defs.exampleDef; + + record package { + example!: exampleDef, + } + "#, + ) + .unwrap(); + ws.add_module("lol.mlf.package".into(), app).unwrap(); + + let result = ws.resolve(); + assert!(result.is_err()); + } } -- 2.51.2