Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions debug_fs.husk
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Test extern function resolution

extern mod fs {
fn exists_sync(path: string) -> bool;
}

use fs::{exists_sync};

fn main() {
let result = exists_sync("test.txt");
println!("File exists: {}", result);
}
368 changes: 368 additions & 0 deletions docs/extern-module-fix-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
# Extern Module Resolution Fix - Implementation Plan

## Executive Summary

This document outlines the comprehensive plan to fix the extern module resolution issue in the Husk compiler. The core problem is that extern module declarations in one file are not visible when importing from those modules in other files, due to each file being compiled in isolation.

## Problem Analysis

### Current State
- Each `.husk` file is compiled independently with its own `SemanticVisitor` instance
- Extern module declarations (e.g., `extern mod fs { ... }`) are only visible within the file they're declared
- When another file tries to import from an extern module (e.g., `use fs::{exists_sync}`), the import fails because the extern declarations aren't available

### Root Cause
The compilation pipeline processes files individually without sharing semantic context, preventing cross-file visibility of extern declarations.

## Solution Architecture

### Overview
Implement a two-pass compilation strategy with shared semantic context across all files in a project.

### Key Components

#### 1. Project-Level Compilation API
- New `transpile_project` function that processes multiple files together
- Maintains shared semantic context across all files
- Two-pass approach: collect externs first, then analyze with full context

#### 2. Enhanced SemanticVisitor
- New constructor to accept pre-populated extern declarations
- Extract extern collection into a separate method
- Maintain backward compatibility for single-file compilation

#### 3. Updated Build Process
- Collect all project files before compilation
- Use project-level API instead of file-by-file compilation
- Preserve output file structure and naming

## Detailed Implementation Plan

### Phase 1: Core Data Structures

```rust
// In lib.rs
pub struct ProjectFile {
pub path: PathBuf,
pub content: String,
}

pub struct ProjectCompilationResult {
pub files: Vec<CompiledFile>,
}

pub struct CompiledFile {
pub source_path: PathBuf,
pub output_path: PathBuf,
pub javascript: String,
pub source_map: Option<String>,
}
```

### Phase 2: Project Compilation Function

```rust
pub fn transpile_project(
files: Vec<ProjectFile>,
target: &str,
main_entry: Option<&Path>,
project_root: Option<&Path>,
) -> Result<ProjectCompilationResult> {
// Pass 1: Collect all extern declarations
let mut project_externs = ProjectExterns::new();

for file in &files {
let ast = parse_file(&file.content)?;
project_externs.collect_from_ast(&ast);
}

// Pass 2: Analyze and transpile with full context
let mut results = Vec::new();

for file in files {
let ast = parse_file(&file.content)?;

// Create analyzer with shared extern context
let mut analyzer = SemanticVisitor::with_project_context(
project_externs.clone(),
file.path.clone(),
project_root.cloned(),
);

// Analyze with full extern visibility
analyzer.analyze(&ast)?;

// Check if this is the main entry point
let is_main = main_entry.map_or(false, |entry| entry == file.path);

// Generate JavaScript
let js = transpile_ast(&ast, &analyzer, target, is_main)?;

results.push(CompiledFile {
source_path: file.path,
output_path: compute_output_path(&file.path, project_root),
javascript: js,
source_map: None, // Future enhancement
});
}

Ok(ProjectCompilationResult { files: results })
}
```

### Phase 3: SemanticVisitor Enhancements

```rust
// In semantic.rs

pub struct ProjectExterns {
pub extern_modules: HashMap<String, HashMap<String, Vec<FunctionSignature>>>,
pub extern_functions: HashMap<String, String>, // name -> js_name mapping
pub extern_types: HashMap<String, Type>,
}

impl SemanticVisitor {
/// Create a visitor with pre-populated project context
pub fn with_project_context(
project_externs: ProjectExterns,
current_file: PathBuf,
project_root: Option<PathBuf>,
) -> Self {
let mut visitor = Self::new();
visitor.extern_modules = project_externs.extern_modules;
visitor.extern_functions = project_externs.extern_functions;
visitor.current_file = Some(current_file);
visitor.project_root = project_root;
visitor
}

/// Extract only extern declarations without full analysis
pub fn collect_extern_declarations(ast: &[Stmt]) -> ProjectExterns {
let mut collector = ExternCollector::new();
for stmt in ast {
collector.visit_stmt(stmt);
}
collector.into_project_externs()
}
}

/// Specialized visitor that only collects extern declarations
struct ExternCollector {
extern_modules: HashMap<String, HashMap<String, Vec<FunctionSignature>>>,
extern_functions: HashMap<String, String>,
extern_types: HashMap<String, Type>,
}
```

### Phase 4: Build Command Updates

```rust
// In main.rs

fn build_command(cli: Build, no_color: bool) -> anyhow::Result<()> {
// ... existing setup code ...

// Collect all Husk files
let husk_files = find_husk_files(&src_dir)?;

// Read all files into memory
let mut project_files = Vec::new();
for path in husk_files {
let content = fs::read_to_string(&path)?;
project_files.push(ProjectFile {
path: path.clone(),
content,
});
}

// Determine main entry point
let main_entry_path = project_root.join(config.get_main_entry());

// Compile project as a whole
let compilation_result = husk_lang::transpile_project(
project_files,
target,
Some(&main_entry_path),
Some(&project_root),
)?;

// Write output files
for compiled_file in compilation_result.files {
let output_path = out_dir.join(
compiled_file.source_path
.strip_prefix(&src_dir)?
.with_extension("js")
);

if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}

fs::write(&output_path, &compiled_file.javascript)?;

println!(" {} -> {}",
compiled_file.source_path.display(),
output_path.display()
);
}

Ok(())
}
```

### Phase 5: Backward Compatibility

The existing single-file compilation functions remain unchanged:
- `transpile_to_js`
- `transpile_to_js_with_packages`
- `transpile_to_js_with_entry_validation`

These continue to work for:
- REPL/interactive mode
- Single-file scripts
- Testing individual files
- Existing tooling integration

### Phase 6: Error Handling

#### Duplicate Extern Declarations
```rust
fn merge_extern_modules(
target: &mut HashMap<String, HashMap<String, Vec<FunctionSignature>>>,
source: HashMap<String, HashMap<String, Vec<FunctionSignature>>>,
) -> Result<()> {
for (module_name, functions) in source {
match target.entry(module_name.clone()) {
Entry::Vacant(e) => {
e.insert(functions);
}
Entry::Occupied(mut e) => {
// Check for conflicts
for (func_name, signatures) in functions {
if e.get().contains_key(&func_name) {
return Err(Error::new_semantic(
format!("Duplicate extern function '{}::{}' found",
module_name, func_name),
Span::default(),
));
}
e.get_mut().insert(func_name, signatures);
}
}
}
}
Ok(())
}
```

#### Circular Dependencies
The two-pass approach naturally handles circular dependencies:
1. All externs are collected before any analysis
2. During analysis, all extern information is available
3. Module imports can reference each other without ordering issues

### Phase 7: Testing Strategy

#### Integration Tests
```rust
#[test]
fn test_multi_file_extern_resolution() {
let files = vec![
ProjectFile {
path: PathBuf::from("types.husk"),
content: r#"
extern mod fs {
fn exists_sync(path: string) -> bool;
}
"#.to_string(),
},
ProjectFile {
path: PathBuf::from("main.husk"),
content: r#"
use fs::{exists_sync};

fn main() {
if exists_sync("test.txt") {
println!("File exists");
}
}
"#.to_string(),
},
];

let result = transpile_project(files, "node-esm", Some(&PathBuf::from("main.husk")), None);
assert!(result.is_ok());

let compiled = result.unwrap();
assert_eq!(compiled.files.len(), 2);

// Verify main.js contains the exists_sync call
let main_js = compiled.files.iter()
.find(|f| f.source_path == PathBuf::from("main.husk"))
.unwrap();
assert!(main_js.javascript.contains("existsSync"));
}
```

#### Edge Cases to Test
1. Empty extern modules
2. Conflicting extern declarations
3. Missing extern modules in imports
4. Nested module structures
5. Type-only imports with externs
6. Generic extern functions
7. Extern implementations

## Performance Considerations

### Memory Usage
- Files are loaded into memory during compilation
- Extern declarations are cloned for each file's analysis
- For large projects, consider streaming approach

### Compilation Speed
- Two-pass approach adds minimal overhead
- Extern collection pass is fast (no type checking)
- Parallel analysis possible in future enhancement

### Optimization Opportunities
1. Cache parsed ASTs between passes
2. Parallelize file analysis in pass 2
3. Incremental compilation support
4. Lazy loading of extern declarations

## Migration Guide

### For End Users
No changes required - the `husk build` command works the same way but now correctly handles extern modules across files.

### For Tool Authors
- Single-file compilation APIs unchanged
- New `transpile_project` API available for multi-file compilation
- Consider migrating to project API for better extern support

## Future Enhancements

1. **Incremental Compilation**
- Track file dependencies
- Only recompile changed files and dependents

2. **Parallel Compilation**
- Analyze independent files concurrently
- Maintain deterministic output

3. **Source Maps**
- Generate source maps for debugging
- Include in `CompiledFile` structure

4. **Module Resolution**
- Support for node_modules
- Custom module resolution strategies

5. **Watch Mode**
- Efficient recompilation on file changes
- Maintain semantic context between builds

## Conclusion

This implementation plan provides a robust solution to the extern module resolution issue while maintaining backward compatibility and setting the foundation for future enhancements. The two-pass compilation strategy ensures all extern declarations are available during import resolution, solving the core problem comprehensively.
Loading
Loading