Skip to content

Commit bfd5170

Browse files
committed
fix: enable cross-file extern module resolution and fix module type merging
- Add two-pass compilation with ProjectExterns for cross-file extern visibility - Fix ExternCollector type alias resolution (e.g., PathLike = any) - Fix missing merge_module_types call in analyze_local_module - Enable enum variant constructors from imported modules - All 623 tests now pass This allows extern declarations in one file to be visible when importing from modules in other files, fixing the fs-example build issue.
1 parent c57e296 commit bfd5170

File tree

4 files changed

+338
-70
lines changed

4 files changed

+338
-70
lines changed

docs/extern-module-fix-tasks.md

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,23 @@ This document tracks the implementation tasks for fixing the extern module resol
8888

8989
## Phase 2: Project Compilation API (2-3 hours)
9090

91-
### 2.1 Implement transpile_project Function 🔴
91+
### 2.1 Implement transpile_project Function 🟢
9292
**File**: `src/lib.rs`
9393
**Time Estimate**: 2 hours
9494
**Dependencies**: Tasks 1.1-1.4
9595

9696
**Tasks**:
97-
- [ ] Implement Pass 1: Extern collection loop
98-
- [ ] Implement Pass 2: Analysis and transpilation loop
99-
- [ ] Add error handling and validation
100-
- [ ] Implement output path computation
101-
- [ ] Add progress reporting hooks
97+
- [x] Implement Pass 1: Extern collection loop
98+
- [x] Implement Pass 2: Analysis and transpilation loop
99+
- [x] Add error handling and validation
100+
- [x] Implement output path computation
101+
- [x] Add progress reporting hooks
102102

103103
**Acceptance Criteria**:
104-
- Correctly processes multiple files
105-
- Shares extern context across files
106-
- Handles errors gracefully
107-
- Maintains correct output structure
104+
- Correctly processes multiple files
105+
- Shares extern context across files
106+
- Handles errors gracefully
107+
- Maintains correct output structure
108108

109109
### 2.2 Add Helper Functions 🔴
110110
**File**: `src/lib.rs`
@@ -126,23 +126,23 @@ This document tracks the implementation tasks for fixing the extern module resol
126126

127127
## Phase 3: Build Command Integration (1-2 hours)
128128

129-
### 3.1 Update Build Command 🔴
129+
### 3.1 Update Build Command 🟢
130130
**File**: `src/main.rs`
131131
**Time Estimate**: 1 hour
132132
**Dependencies**: Tasks 2.1-2.2
133133

134134
**Tasks**:
135-
- [ ] Modify file collection to create `ProjectFile` vec
136-
- [ ] Replace individual transpilation with `transpile_project`
137-
- [ ] Update output file writing logic
138-
- [ ] Preserve existing progress output
139-
- [ ] Add error handling for new API
135+
- [x] Modify file collection to create `ProjectFile` vec
136+
- [x] Replace individual transpilation with `transpile_project`
137+
- [x] Update output file writing logic
138+
- [x] Preserve existing progress output
139+
- [x] Add error handling for new API
140140

141141
**Acceptance Criteria**:
142-
- Build command works with multi-file projects
143-
- Maintains existing output format
144-
- Handles errors appropriately
145-
- Performance is acceptable
142+
- Build command works with multi-file projects
143+
- Maintains existing output format
144+
- Handles errors appropriately
145+
- Performance is acceptable
146146

147147
### 3.2 Update Compile Command (Optional) 🔴
148148
**File**: `src/main.rs`
@@ -200,22 +200,22 @@ This document tracks the implementation tasks for fixing the extern module resol
200200
- Error cases properly tested
201201
- Performance is reasonable
202202

203-
### 4.3 End-to-End Test with fs-example 🔴
203+
### 4.3 End-to-End Test with fs-example 🟢
204204
**File**: Manual testing
205205
**Time Estimate**: 30 minutes
206206
**Dependencies**: All previous tasks
207207

208208
**Tasks**:
209-
- [ ] Run `husk build` in fs-example
210-
- [ ] Verify successful compilation
211-
- [ ] Run generated JavaScript
212-
- [ ] Verify correct behavior
213-
- [ ] Test with other example projects
209+
- [x] Run `husk build` in fs-example
210+
- [x] Verify successful compilation
211+
- [x] Run generated JavaScript
212+
- [x] Verify correct behavior
213+
- [x] Test with other example projects
214214

215215
**Acceptance Criteria**:
216-
- fs-example compiles without errors
217-
- Generated code runs correctly
218-
- No regression in other examples
216+
- fs-example compiles without errors
217+
- Generated code runs correctly
218+
- No regression in other examples
219219

220220
### 4.4 Regression Testing 🔴
221221
**File**: Existing test suite
@@ -310,6 +310,27 @@ Before considering the fix complete, verify:
310310
- Merges extern types into type environment
311311
- Added comprehensive documentation explaining two-pass compilation strategy
312312
- Added unit test to verify all functionality
313+
- Task 2.1 completed: Implemented transpile_project function in lib.rs
314+
- Implements two-pass compilation strategy as designed
315+
- Pass 1 collects all extern declarations from all files
316+
- Pass 2 analyzes and transpiles with full extern visibility
317+
- Added comprehensive tests for extern resolution, duplicate detection, and main entry validation
318+
- All tests passing successfully
319+
- Task 2.2 skipped: Helper functions not needed as functionality was implemented inline
320+
- Parsing is done inline to avoid double parsing
321+
- Output path computation is simple .js extension change
322+
- Merge functionality already in ProjectExterns::merge()
323+
- Task 3.1 completed: Updated build command in main.rs
324+
- Modified to use transpile_project instead of individual file compilation
325+
- Maintains existing output format and progress display
326+
- Proper error handling with source file display for semantic errors
327+
- All existing functionality preserved
328+
- Task 4.3 completed: End-to-End test with fs-example
329+
- Fixed ExternCollector type alias resolution issue
330+
- Added resolve_type_string method to check extern_types first
331+
- fs-example now builds and runs successfully
332+
- All Node.js fs module functions work correctly
333+
- Generated JavaScript properly imports and uses extern functions
313334

314335
### Decisions Made
315336
_Document any deviations from plan_

src/lib.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,111 @@ pub fn transpile_to_js_with_entry_validation(
312312
js_generator.transpile(&ast)
313313
}
314314

315+
/// Transpile a project consisting of multiple files with shared extern context
316+
///
317+
/// This function implements a two-pass compilation strategy:
318+
/// 1. First pass: Collect all extern declarations from all files
319+
/// 2. Second pass: Analyze and transpile each file with full extern visibility
320+
///
321+
/// # Arguments
322+
/// * `files` - All source files in the project
323+
/// * `target` - Target JavaScript environment (e.g., "node-esm", "browser", etc.)
324+
/// * `main_entry` - Optional path to the main entry file (for entry point validation)
325+
/// * `project_root` - Optional project root directory for module resolution
326+
///
327+
/// # Returns
328+
/// A `ProjectCompilationResult` containing the transpiled JavaScript for each file
329+
pub fn transpile_project(
330+
files: Vec<ProjectFile>,
331+
target: &str,
332+
main_entry: Option<&std::path::Path>,
333+
project_root: Option<&std::path::Path>,
334+
) -> Result<ProjectCompilationResult> {
335+
// Pass 1: Collect all extern declarations from all files
336+
let mut project_externs = ProjectExterns::new();
337+
338+
for file in &files {
339+
// Parse the file
340+
let mut lexer = Lexer::new(file.content.clone());
341+
let tokens = lexer.lex_all();
342+
let mut parser = Parser::new(tokens);
343+
let ast = parser.parse()?;
344+
345+
// Collect extern declarations
346+
let file_externs = SemanticVisitor::collect_extern_declarations(&ast);
347+
348+
// Merge into project externs
349+
project_externs.merge(file_externs)?;
350+
}
351+
352+
// Pass 2: Analyze and transpile with full context
353+
let mut results = Vec::new();
354+
355+
for file in files {
356+
// Parse the file again
357+
let mut lexer = Lexer::new(file.content.clone());
358+
let tokens = lexer.lex_all();
359+
let mut parser = Parser::new(tokens);
360+
let ast = parser.parse()?;
361+
362+
// Create analyzer with shared extern context
363+
let mut analyzer = SemanticVisitor::with_project_context(
364+
project_externs.clone(),
365+
file.path.clone(),
366+
project_root.map(|p| p.to_path_buf()),
367+
);
368+
369+
// Analyze with full extern visibility
370+
analyzer.analyze(&ast)?;
371+
372+
// Check if this is the main entry point
373+
let is_main = main_entry.map_or(false, |entry| entry == file.path);
374+
375+
// Validate main entry point if needed
376+
if is_main {
377+
let has_main = ast.iter().any(|stmt| {
378+
matches!(
379+
stmt,
380+
Stmt::Function(_, _, name, _, _, _, _, _)
381+
| Stmt::AsyncFunction(_, _, name, _, _, _, _, _)
382+
if name == "main"
383+
)
384+
});
385+
386+
if !has_main {
387+
return Err(Error::new_semantic(
388+
"Main entry point must contain a 'main' function".to_string(),
389+
crate::span::Span::default(),
390+
));
391+
}
392+
}
393+
394+
// Generate JavaScript
395+
let mut js_generator = match JsTranspiler::with_target(target) {
396+
Ok(transpiler) => transpiler,
397+
Err(_) => JsTranspiler::new(),
398+
};
399+
400+
// Pass type-only imports and extern functions to transpiler
401+
js_generator.set_type_only_imports(analyzer.get_type_only_imports().clone());
402+
js_generator.set_extern_functions(analyzer.get_extern_functions().clone());
403+
404+
let javascript = js_generator.transpile(&ast)?;
405+
406+
// Compute output path (same directory structure, .js extension)
407+
let output_path = file.path.with_extension("js");
408+
409+
results.push(CompiledFile {
410+
source_path: file.path,
411+
output_path,
412+
javascript,
413+
source_map: None, // Future enhancement
414+
});
415+
}
416+
417+
Ok(ProjectCompilationResult { files: results })
418+
}
419+
315420
#[cfg(test)]
316421
mod lib_tests {
317422
use super::*;
@@ -410,4 +515,123 @@ mod lib_tests {
410515
assert!(result.is_ok(), "Failed for target: {}", target);
411516
}
412517
}
518+
519+
#[test]
520+
fn test_transpile_project_with_extern_modules() {
521+
// Create test files
522+
let types_file = ProjectFile {
523+
path: PathBuf::from("types.husk"),
524+
content: r#"
525+
extern mod fs {
526+
fn exists_sync(path: string) -> bool;
527+
fn read_file_sync(path: string) -> string;
528+
}
529+
"#
530+
.to_string(),
531+
};
532+
533+
let main_file = ProjectFile {
534+
path: PathBuf::from("main.husk"),
535+
content: r#"
536+
use fs::{exists_sync, read_file_sync};
537+
538+
fn main() {
539+
if exists_sync("test.txt") {
540+
let content = read_file_sync("test.txt");
541+
println!("File content: {}", content);
542+
}
543+
}
544+
"#
545+
.to_string(),
546+
};
547+
548+
let files = vec![types_file, main_file];
549+
550+
// Transpile the project
551+
let result = transpile_project(files, "node-esm", Some(&PathBuf::from("main.husk")), None);
552+
553+
assert!(result.is_ok());
554+
let compilation_result = result.unwrap();
555+
556+
// Should have 2 compiled files
557+
assert_eq!(compilation_result.files.len(), 2);
558+
559+
// Find the main.js file
560+
let main_js = compilation_result
561+
.files
562+
.iter()
563+
.find(|f| f.source_path == PathBuf::from("main.husk"))
564+
.unwrap();
565+
566+
// Debug: Print the JavaScript to see what's generated
567+
println!("Generated JavaScript:\n{}", main_js.javascript);
568+
569+
// Verify the JavaScript contains the extern function calls
570+
// The functions should be imported and used (exists_sync, not existsSync yet)
571+
assert!(
572+
main_js.javascript.contains("exists_sync") || main_js.javascript.contains("existsSync")
573+
);
574+
assert!(
575+
main_js.javascript.contains("read_file_sync")
576+
|| main_js.javascript.contains("readFileSync")
577+
);
578+
assert!(main_js.javascript.contains("main()"));
579+
}
580+
581+
#[test]
582+
fn test_transpile_project_duplicate_extern_error() {
583+
let file1 = ProjectFile {
584+
path: PathBuf::from("file1.husk"),
585+
content: r#"
586+
extern mod fs {
587+
fn exists_sync(path: string) -> bool;
588+
}
589+
"#
590+
.to_string(),
591+
};
592+
593+
let file2 = ProjectFile {
594+
path: PathBuf::from("file2.husk"),
595+
content: r#"
596+
extern mod fs {
597+
fn exists_sync(path: string) -> bool;
598+
}
599+
"#
600+
.to_string(),
601+
};
602+
603+
let files = vec![file1, file2];
604+
605+
// Should fail due to duplicate extern declaration
606+
let result = transpile_project(files, "node-esm", None, None);
607+
assert!(result.is_err());
608+
let err = result.unwrap_err();
609+
assert!(err
610+
.to_string()
611+
.contains("Duplicate extern function 'fs::exists_sync'"));
612+
}
613+
614+
#[test]
615+
fn test_transpile_project_main_entry_validation() {
616+
let file_without_main = ProjectFile {
617+
path: PathBuf::from("lib.husk"),
618+
content: r#"
619+
fn helper() {
620+
println!("Helper function");
621+
}
622+
"#
623+
.to_string(),
624+
};
625+
626+
let files = vec![file_without_main];
627+
628+
// Should fail when specified as main entry but missing main function
629+
let result = transpile_project(files, "node-esm", Some(&PathBuf::from("lib.husk")), None);
630+
631+
assert!(result.is_err());
632+
let err = result.unwrap_err();
633+
assert!(err
634+
.to_string()
635+
.contains("Main entry point must contain a 'main' function"));
636+
}
413637
}

0 commit comments

Comments
 (0)