use crate::analyzer::{Analyzer, SignatureRegistry};
use crate::codegen::rust::CodeGenerator;
use crate::lexer::Lexer;
use crate::linter::rust_leakage::RustLeakageLinter;
use crate::metadata::{
meta_cache_path, metadata_function_sig_from_analyzer, CrateMetadata, FunctionSignature,
ModuleMetadata,
};
use crate::parser::ast::core::Item;
use crate::parser::Parser;
use crate::type_inference::{FloatInference, IntInference};
use crate::CompilationTarget;
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_library_multipass(
wj_files: &[PathBuf],
base_path: &Path,
output: &Path,
target: CompilationTarget,
library: bool,
enable_lint: bool,
external_paths: &HashMap<String, PathBuf>,
mut crate_metadata: CrateMetadata,
) -> Result<()> {
let mut sources: Vec<(PathBuf, String)> = Vec::new();
for file in wj_files {
let canon = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
let source = std::fs::read_to_string(&canon)?;
sources.push((canon, source));
}
let mut removed_stems: HashSet<PathBuf> = HashSet::new();
let mut shader_count = 0usize;
let mut filtered_modules_by_dir: HashMap<PathBuf, HashSet<String>> = HashMap::new();
sources.retain(|(file, source)| {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
if let Ok(program) = parser.parse() {
if super::is_shader_file(&program) {
removed_stems.insert(file.clone());
if let Some(parent) = file.parent() {
if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
filtered_modules_by_dir
.entry(parent.to_path_buf())
.or_default()
.insert(stem.to_string());
}
}
shader_count += 1;
return false;
}
}
true
});
if !removed_stems.is_empty() {
sources.retain(|(file, source)| {
let is_mod = file
.file_name()
.and_then(|n| n.to_str())
.map(|n| n == "mod.wj")
.unwrap_or(false);
if !is_mod {
return true;
}
let parent = match file.parent() {
Some(p) => p,
None => return true,
};
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = match parser.parse() {
Ok(p) => p,
Err(_) => return true,
};
let has_non_mod_items = program
.items
.iter()
.any(|item| !matches!(item, Item::Mod { .. }));
if has_non_mod_items {
return true;
}
let all_subs_removed = program.items.iter().all(|item| {
if let Item::Mod { name, .. } = item {
let sub_file = parent.join(format!("{}.wj", name));
let sub_dir_mod = parent.join(name.as_str()).join("mod.wj");
removed_stems.contains(&sub_file) || removed_stems.contains(&sub_dir_mod)
} else {
true
}
});
if all_subs_removed {
if let Some(dir_name) = parent.file_name().and_then(|n| n.to_str()) {
if let Some(grandparent) = parent.parent() {
filtered_modules_by_dir
.entry(grandparent.to_path_buf())
.or_default()
.insert(dir_name.to_string());
}
}
shader_count += 1;
false
} else {
true
}
});
}
if shader_count > 0 {
eprintln!(
" Skipped {} shader file(s) from Rust pipeline (use WJSL target for GPU shaders)",
shader_count
);
}
if sources.is_empty() {
return Ok(());
}
let src_base: PathBuf = {
let raw = if base_path.is_file() {
base_path.parent().unwrap_or(base_path).to_path_buf()
} else {
base_path.to_path_buf()
};
std::fs::canonicalize(&raw).unwrap_or(raw)
};
let (mut global_copy_structs, local_struct_names) =
super::library_copy_registry::collect_global_copy_structs_for_library(&sources);
let dep_roots =
super::dependency_resolution::find_dependency_metadata_roots(&src_base, external_paths);
let mut dep_registry = SignatureRegistry::new();
{
let mut dep_copy_structs = Vec::new();
let mut dep_struct_fields: HashMap<String, Vec<Vec<String>>> = HashMap::new();
for root in &dep_roots {
crate::metadata::merge_wj_meta_signatures_from_dir_inner_pub(
root,
&mut dep_registry,
&mut dep_copy_structs,
&mut dep_struct_fields,
);
}
crate::metadata::infer_copy_from_metadata_structs_pub(
&dep_struct_fields,
&mut dep_copy_structs,
);
for name in dep_copy_structs {
if !local_struct_names.contains(&name) {
global_copy_structs.insert(name);
}
}
}
let mut global_registry = dep_registry;
crate::metadata::merge_wj_meta_signatures_from_dir(&src_base, &mut global_registry);
let mut global_float_signatures: HashMap<
String,
(
Vec<crate::parser::ast::types::Type>,
Option<crate::parser::ast::types::Type>,
),
> = HashMap::new();
let mut global_struct_fields: HashMap<
String,
HashMap<String, crate::parser::ast::types::Type>,
> = HashMap::new();
let mut struct_defining_module_paths: HashMap<String, Vec<Vec<String>>> = HashMap::new();
for (file, source) in &sources {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error in {}: {}", file.display(), e))?;
let mut module_meta = ModuleMetadata::new(
file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string(),
);
for item in &program.items {
match item {
Item::Struct { decl, .. } => {
let mut fields = HashMap::new();
for field in &decl.fields {
fields.insert(
field.name.clone(),
ModuleMetadata::serialize_type(&field.field_type),
);
}
module_meta.structs.insert(decl.name.clone(), fields);
}
Item::Function { decl, .. } => {
module_meta.functions.insert(
decl.name.clone(),
FunctionSignature {
params: decl
.parameters
.iter()
.map(|p| ModuleMetadata::serialize_type(&p.type_))
.collect(),
return_type: decl
.return_type
.as_ref()
.map(ModuleMetadata::serialize_type),
is_associated: false,
parent_type: None,
param_ownership: vec![],
has_self_receiver: false,
is_extern: decl.is_extern,
},
);
}
Item::Impl { block, .. } => {
for func_decl in &block.functions {
let full_name = format!("{}::{}", block.type_name, func_decl.name);
module_meta.functions.insert(
full_name,
FunctionSignature {
params: func_decl
.parameters
.iter()
.map(|p| ModuleMetadata::serialize_type(&p.type_))
.collect(),
return_type: func_decl
.return_type
.as_ref()
.map(ModuleMetadata::serialize_type),
is_associated: true,
parent_type: Some(block.type_name.clone()),
param_ownership: vec![],
has_self_receiver: false,
is_extern: false,
},
);
}
}
_ => {}
}
}
crate_metadata.merge_module(&module_meta);
for item in &program.items {
match item {
Item::Function { decl, .. } => {
let param_types: Vec<crate::parser::ast::types::Type> =
decl.parameters.iter().map(|p| p.type_.clone()).collect();
global_float_signatures
.insert(decl.name.clone(), (param_types, decl.return_type.clone()));
}
Item::Impl { block, .. } => {
for func_decl in &block.functions {
let param_types: Vec<crate::parser::ast::types::Type> = func_decl
.parameters
.iter()
.map(|p| p.type_.clone())
.collect();
let full_name = format!("{}::{}", block.type_name, func_decl.name);
global_float_signatures
.insert(full_name, (param_types, func_decl.return_type.clone()));
}
}
_ => {}
}
}
fn merge_struct_fields_from_items(
items: &[crate::parser::ast::core::Item<'_>],
module_prefix: &[String],
global_struct_fields: &mut HashMap<
String,
HashMap<String, crate::parser::ast::types::Type>,
>,
struct_defining_module_paths: &mut HashMap<String, Vec<Vec<String>>>,
) {
use crate::parser::ast::core::Item;
use crate::type_inference::struct_field_registry;
for item in items {
match item {
Item::Struct { decl, .. } => {
let qualified =
struct_field_registry::qualify_struct_key(module_prefix, &decl.name);
let mut fields = HashMap::new();
for field in &decl.fields {
fields.insert(field.name.clone(), field.field_type.clone());
}
global_struct_fields.insert(qualified, fields);
struct_defining_module_paths
.entry(decl.name.clone())
.or_default()
.push(module_prefix.to_vec());
}
Item::Mod { name, items, .. } => {
let mut next = module_prefix.to_vec();
next.push(name.clone());
merge_struct_fields_from_items(
items,
&next,
global_struct_fields,
struct_defining_module_paths,
);
}
_ => {}
}
}
}
let file_module = crate::analyzer::type_collector::wj_file_to_module_path(&src_base, file)
.unwrap_or_default();
merge_struct_fields_from_items(
&program.items,
&file_module,
&mut global_struct_fields,
&mut struct_defining_module_paths,
);
let mut analyzer = Analyzer::new_with_copy_structs(global_copy_structs.clone());
let (_, registry, _) = analyzer
.analyze_program(&program)
.map_err(|e| anyhow::anyhow!("Analysis error in {}: {}", file.display(), e))?;
global_registry.merge(®istry);
let file_stem = file.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let module_path = file_module.join("::");
if !file_stem.is_empty() {
for (name, sig) in ®istry.signatures {
if !name.contains("::") {
let qualified = format!("{}::{}", file_stem, name);
global_registry.add_function(qualified, sig.clone());
}
if !module_path.is_empty() {
let full_qualified = format!("{}::{}", module_path, name);
global_registry.add_function(full_qualified, sig.clone());
}
}
}
}
const MAX_GLOBAL_PASSES: usize = 10;
let mut pass_number = 1;
loop {
let mut new_registry = global_registry.clone();
for (file, source) in &sources {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
let mut analyzer = Analyzer::new_with_copy_structs(global_copy_structs.clone());
analyzer.set_global_struct_field_types(global_struct_fields.clone());
let (_, file_registry, _) = analyzer
.analyze_program_with_global_signatures(&program, &global_registry)
.map_err(|e| anyhow::anyhow!("Analysis error in pass {}: {}", pass_number, e))?;
let file_stem = file.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let file_module =
crate::analyzer::type_collector::wj_file_to_module_path(&src_base, file)
.unwrap_or_default();
let module_path = file_module.join("::");
for (name, sig) in &file_registry.signatures {
match global_registry.signatures.get(name) {
None => {
new_registry.signatures.insert(name.clone(), sig.clone());
}
Some(old_sig) => {
if sig.param_ownership != old_sig.param_ownership
|| sig.return_ownership != old_sig.return_ownership
|| sig.has_self_receiver != old_sig.has_self_receiver
{
new_registry.signatures.insert(name.clone(), sig.clone());
if !file_stem.is_empty() {
let qualified = format!("{}::{}", file_stem, name);
new_registry.signatures.insert(qualified, sig.clone());
if !module_path.is_empty() {
let full_qualified = format!("{}::{}", module_path, name);
new_registry.signatures.insert(full_qualified, sig.clone());
}
}
}
}
}
}
}
let mut changed = false;
for (name, sig) in &new_registry.signatures {
match global_registry.signatures.get(name) {
None => {
changed = true;
break;
}
Some(old_sig) => {
if sig.param_ownership != old_sig.param_ownership
|| sig.return_ownership != old_sig.return_ownership
|| sig.has_self_receiver != old_sig.has_self_receiver
{
changed = true;
break;
}
}
}
}
if !changed || pass_number >= MAX_GLOBAL_PASSES {
global_registry = new_registry;
break;
}
global_registry = new_registry;
pass_number += 1;
}
let mut module_re_exports: HashMap<String, HashMap<String, String>> = HashMap::new();
for (file, source) in &sources {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error in {}: {}", file.display(), e))?;
let file_module = crate::analyzer::type_collector::wj_file_to_module_path(&src_base, file)
.unwrap_or_default();
crate::type_inference::struct_field_registry::merge_module_reexports_from_items(
&program.items,
&file_module,
&global_struct_fields,
&struct_defining_module_paths,
&mut module_re_exports,
);
if crate::type_inference::struct_field_registry::debug_struct_import_trace()
&& file.to_string_lossy().contains("dialogue")
{
eprintln!(
"=== WJ_DEBUG: file={} file_module_path={:?}",
file.display(),
file_module
);
}
}
if crate::type_inference::struct_field_registry::debug_struct_import_trace() {
eprintln!("=== GLOBAL MODULE_RE_EXPORTS (post pre-pass) ===");
let mut mods: Vec<_> = module_re_exports.keys().cloned().collect();
mods.sort();
for m in &mods {
if !m.contains("dialogue") && !m.is_empty() {
continue;
}
let exports = &module_re_exports[m];
eprintln!(" module {:?}: {} exports", m, exports.len());
for (name, key) in exports {
if name.contains("Dialogue") {
eprintln!(" {} → {}", name, key);
}
}
}
}
let mut global_float_inference = FloatInference::new();
if !external_paths.is_empty() {
global_float_inference.set_external_crate_metadata_paths(external_paths);
}
global_float_inference.set_global_function_signatures(global_float_signatures.clone());
global_float_inference.set_global_struct_field_types(&global_struct_fields);
global_float_inference.set_struct_defining_module_paths(struct_defining_module_paths.clone());
global_float_inference.set_module_re_exports(module_re_exports.clone());
for (file, source) in &sources {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error in {}: {}", file.display(), e))?;
let file_module = crate::analyzer::type_collector::wj_file_to_module_path(&src_base, file)
.unwrap_or_default();
global_float_inference.set_current_file_module_path(file_module);
global_float_inference.infer_program(&program);
}
if !global_float_inference.errors.is_empty() {
for error in &global_float_inference.errors {
eprintln!("Float inference error: {}", error);
}
return Err(anyhow::anyhow!(
"Float type inference failed: {} error(s)",
global_float_inference.errors.len()
));
}
let mut global_int_inference = IntInference::new();
global_int_inference.set_global_function_signatures(global_float_signatures.clone());
global_int_inference.set_global_struct_field_types(&global_struct_fields);
global_int_inference.set_struct_defining_module_paths(struct_defining_module_paths);
global_int_inference.set_module_re_exports(module_re_exports);
for (file, source) in &sources {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error in {}: {}", file.display(), e))?;
let file_module = crate::analyzer::type_collector::wj_file_to_module_path(&src_base, file)
.unwrap_or_default();
global_int_inference.set_current_file_module_path(file_module);
global_int_inference.infer_program(&program);
}
if !global_int_inference.errors.is_empty() {
for error in &global_int_inference.errors {
eprintln!("Int inference error: {}", error);
}
return Err(anyhow::anyhow!(
"Int type inference failed: {} error(s)",
global_int_inference.errors.len()
));
}
let type_defining_modules =
super::dependency_resolution::build_type_defining_modules_for_library(&sources, &src_base)?;
let extern_submodule_qualifiers =
super::dependency_resolution::build_extern_submodule_qualifier_map(&sources, &src_base)?;
let global_analyzed_trait_methods = {
let global_copy_structs_clone = global_copy_structs.clone();
let sources_for_thread: Vec<(std::path::PathBuf, String)> = sources
.iter()
.map(|(p, s)| (p.clone(), s.clone()))
.collect();
let handle = std::thread::Builder::new()
.name("trait-inference".to_string())
.stack_size(64 * 1024 * 1024)
.spawn(move || -> Result<HashMap<String, HashMap<String, crate::analyzer::AnalyzedFunction<'static>>>, String> {
let mut shared_analyzer = Analyzer::new_with_copy_structs(global_copy_structs_clone);
let mut parsers: Vec<Parser> = Vec::with_capacity(sources_for_thread.len());
let mut programs: Vec<crate::parser::Program<'_>> = Vec::with_capacity(sources_for_thread.len());
for (_file, source) in &sources_for_thread {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let parser = Parser::new_with_source(
tokens,
String::new(),
source.clone(),
);
parsers.push(parser);
}
for parser in &mut parsers {
if let Ok(program) = parser.parse() {
programs.push(program);
}
}
for program in &programs {
shared_analyzer.register_traits_from_program(program)
.unwrap_or_else(|e| eprintln!("Trait registration warning: {}", e));
}
let mut all_items = Vec::new();
for program in programs {
all_items.extend(program.items);
}
let merged_program = crate::parser::Program { items: all_items };
shared_analyzer.infer_trait_signatures_from_impls(&merged_program)?;
Ok(shared_analyzer.analyzed_trait_methods.clone())
})
.map_err(|e| anyhow::anyhow!("Failed to spawn trait inference thread: {}", e))?;
match handle.join() {
Ok(Ok(methods)) => methods,
Ok(Err(e)) => {
eprintln!("Cross-file trait inference warning: {}", e);
HashMap::new()
}
Err(_) => {
eprintln!("⚠️ Global trait inference thread panicked (stack overflow?) — skipping cross-file trait methods.");
HashMap::new()
}
}
};
for (file, source) in sources.iter() {
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize_with_locations();
let mut parser =
Parser::new_with_source(tokens, file.to_string_lossy().to_string(), source.clone());
let mut program = parser
.parse()
.map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
if let Some(parent_dir) = file.parent() {
if let Some(filtered_names) = filtered_modules_by_dir.get(parent_dir) {
program.items = super::strip_filtered_mod_items(program.items, filtered_names);
}
}
let mut analyzer = Analyzer::new_with_copy_structs(global_copy_structs.clone());
analyzer.set_global_struct_field_types(global_struct_fields.clone());
if enable_lint {
let file_name = file.to_string_lossy().to_string();
let mut rust_leakage = RustLeakageLinter::new(&file_name);
rust_leakage.lint_program(&program);
for diag in rust_leakage.diagnostics() {
eprintln!("{}", diag);
}
}
analyzer
.register_traits_from_program(&program)
.unwrap_or_else(|e| eprintln!("Trait registration warning: {}", e));
let (analyzed_functions, registry, _) = analyzer
.analyze_program_with_global_signatures(&program, &global_registry)
.map_err(|e| anyhow::anyhow!("Final analysis error: {}", e))?;
analyzer
.infer_trait_signatures_from_impls(&program)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let mut merged_trait_methods = analyzer.analyzed_trait_methods.clone();
for (trait_name, methods) in &global_analyzed_trait_methods {
let entry = merged_trait_methods.entry(trait_name.clone()).or_default();
for (method_name, method_analysis) in methods {
entry.insert(method_name.clone(), method_analysis.clone());
}
}
let output_file =
crate::project_paths::resolve_wj_output_path_library(&src_base, file, output)?;
if let Some(parent) = output_file.parent() {
std::fs::create_dir_all(parent)?;
}
let mut full_registry = global_registry.clone();
full_registry.merge(®istry);
let registry_snapshot = full_registry.clone();
let mut codegen = CodeGenerator::new_for_module(full_registry, target);
codegen.set_copy_types_registry(global_copy_structs.clone());
codegen.set_global_struct_field_types(global_struct_fields.clone());
codegen.set_output_file(&output_file);
codegen.set_source_file(file);
codegen.set_library_source_root(src_base.clone());
codegen.set_type_defining_modules(type_defining_modules.clone());
codegen.set_extern_submodule_qualifiers(extern_submodule_qualifiers.clone());
codegen.set_analyzed_trait_methods(merged_trait_methods);
codegen.set_float_inference(global_float_inference.clone());
codegen.set_int_inference(global_int_inference.clone());
let mut trait_inference = crate::inference::InferenceEngine::new();
let mut inferred_bounds_map = std::collections::HashMap::new();
for item in &program.items {
if let Item::Function { decl: func, .. } = item {
let bounds = trait_inference.infer_function_bounds(func);
if !bounds.is_empty() {
inferred_bounds_map.insert(func.name.clone(), bounds);
}
}
if let Item::Impl { block, .. } = item {
for func in &block.functions {
let bounds = trait_inference.infer_function_bounds(func);
if !bounds.is_empty() {
inferred_bounds_map.insert(func.name.clone(), bounds);
}
}
}
}
codegen.set_inferred_bounds(inferred_bounds_map);
let rust_code = codegen.generate_program(&program, &analyzed_functions);
super::cache_management::write_if_changed(&output_file, &rust_code)?;
if target == CompilationTarget::Rust {
let module_name = file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let mut meta = ModuleMetadata::new(module_name.to_string());
for item in &program.items {
match item {
Item::Function { decl, .. } => {
if let Some(sig) = registry_snapshot.get_signature(&decl.name) {
meta.functions.insert(
decl.name.clone(),
metadata_function_sig_from_analyzer(sig, false, None),
);
}
}
Item::Impl { block, .. } => {
for func_decl in &block.functions {
let full_name = format!("{}::{}", block.type_name, func_decl.name);
if let Some(sig) = registry_snapshot.get_signature(&full_name) {
meta.functions.insert(
full_name,
metadata_function_sig_from_analyzer(
sig,
true,
Some(block.type_name.clone()),
),
);
}
}
}
Item::Struct { decl, .. } => {
let mut fields = std::collections::HashMap::new();
for field in &decl.fields {
fields.insert(
field.name.clone(),
ModuleMetadata::serialize_type(&field.field_type),
);
}
meta.structs.insert(decl.name.clone(), fields);
}
_ => {}
}
}
meta.copy_structs = analyzer.get_copy_structs();
let meta_path = meta_cache_path(file);
if let Some(parent) = meta_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string_pretty(&meta) {
let _ = super::cache_management::write_if_changed(&meta_path, &json);
}
}
}
if library && (!crate_metadata.structs.is_empty() || !crate_metadata.functions.is_empty()) {
let metadata_path = output.join("metadata.json");
let metadata_json = serde_json::to_string_pretty(&crate_metadata)?;
super::cache_management::write_if_changed(&metadata_path, &metadata_json)?;
}
if target == CompilationTarget::Rust {
crate::build_utils::generate_mod_file_with_layout(
output,
Some((output, src_base.as_path())),
)?;
}
if target == CompilationTarget::Rust {
super::cache_management::clean_nested_cargo_toml(output);
let source_dir = if base_path.is_file() {
base_path.parent().unwrap_or(base_path)
} else {
base_path
};
crate::cargo_toml::generate_single_file_cargo_toml(output, source_dir, target)?;
}
if target == CompilationTarget::Wasm {
let source_dir = if base_path.is_file() {
base_path.parent().unwrap_or(base_path)
} else {
base_path
};
crate::cargo_toml::generate_wasm_cargo_toml(output, source_dir)?;
}
Ok(())
}