diff --git a/Cargo.toml b/Cargo.toml index 95af7d2d..531f8d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,12 @@ utils = { path = "utils" } lexer = { path = "front/lexer" } parser = { path = "front/parser" } error = { path = "front/error" } -llvm = { path = "./llvm" } +llvm = { path = "./llvm", default-features = false } + +[features] +default = ["llvm-target-all"] +llvm-target-all = ["llvm/llvm-target-all"] +llvm-target-x86 = ["llvm/llvm-target-x86"] [workspace] members = [ diff --git a/README.md b/README.md index 8b32eb43..497cbe95 100644 --- a/README.md +++ b/README.md @@ -101,22 +101,14 @@ Wave follows a tiered platform policy to set clear expectations for stability, C
- 🥉 Tier 3 · Experimental — OpenBSD + 🥉 Tier 3 · Experimental — OpenBSD, Windows (MinGW/GNU)
-
- 🪦 Tier 4 · Unofficial — Windows - -
- --- ## CLI Usage diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index b8ca1445..6d9425ee 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.rs @@ -31,7 +31,7 @@ fn parse_target_os_attr(line: &str) -> Option> { let start = "#[target(os=\"".len(); let end = trimmed.len() - 3; // ")]" let os = &trimmed[start..end]; - if os == "linux" || os == "macos" { + if os == "linux" || os == "macos" || os == "windows" { Some(TargetAttr::Supported(os)) } else { Some(TargetAttr::Unsupported) @@ -182,8 +182,8 @@ fn consume_target_item(lines: &[&str], mut idx: usize, keep: bool, out: &mut Vec idx } -fn preprocess_target_attrs(source: &str) -> String { - let host = std::env::consts::OS; +fn preprocess_target_attrs(source: &str, target_os: Option<&str>) -> String { + let host = target_os.unwrap_or(std::env::consts::OS); let lines: Vec<&str> = source.lines().collect(); let mut out: Vec = Vec::with_capacity(lines.len()); let mut idx: usize = 0; @@ -257,6 +257,7 @@ pub struct ImportedUnit { pub struct ImportConfig { pub dep_roots: Vec, pub dep_packages: HashMap, + pub target_os: Option, } pub fn local_import_unit( @@ -284,7 +285,7 @@ pub fn local_import_unit_with_config( } if path.starts_with("std::") { - return std_import_unit(path, already_imported); + return std_import_unit(path, already_imported, config); } if path.contains("::") { @@ -308,7 +309,7 @@ pub fn local_import_unit_with_config( )); } - parse_wave_file(&found_path, &target_file_name, already_imported) + parse_wave_file(&found_path, &target_file_name, already_imported, config) } pub fn local_import( @@ -462,7 +463,7 @@ fn external_import_unit( for candidate in &candidates { if candidate.exists() && candidate.is_file() { - return parse_wave_file(candidate, path, already_imported); + return parse_wave_file(candidate, path, already_imported, config); } } @@ -489,6 +490,7 @@ fn external_import_unit( fn std_import_unit( path: &str, already_imported: &mut HashSet, + config: &ImportConfig, ) -> Result { let rel = path.strip_prefix("std::").unwrap(); if rel.trim().is_empty() { @@ -520,7 +522,7 @@ fn std_import_unit( )); } - parse_wave_file(&found_path, path, already_imported) + parse_wave_file(&found_path, path, already_imported, config) } fn std_root_dir(import_path: &str) -> Result { @@ -541,6 +543,7 @@ fn parse_wave_file( found_path: &Path, display_name: &str, already_imported: &mut HashSet, + config: &ImportConfig, ) -> Result { let abs_path = found_path.canonicalize().map_err(|e| { WaveError::new( @@ -582,7 +585,7 @@ fn parse_wave_file( 0, ) })?; - let content = preprocess_target_attrs(&raw_content); + let content = preprocess_target_attrs(&raw_content, config.target_os.as_deref()); let mut lexer = Lexer::new_with_file(&content, abs_path.display().to_string()); let tokens = lexer.tokenize()?; diff --git a/llvm/Cargo.toml b/llvm/Cargo.toml index 9b1339ac..db4da564 100644 --- a/llvm/Cargo.toml +++ b/llvm/Cargo.toml @@ -7,5 +7,10 @@ edition = "2021" parser = { path = "../front/parser" } lexer = { path = "../front/lexer" } error = { path = "../front/error" } -inkwell = { version = "0.8.0", features = ["llvm21-1"] } +inkwell = { version = "0.8.0", default-features = false, features = ["llvm21-1"] } llvm-sys = { version = "211.0.0", features = ["prefer-dynamic"] } + +[features] +default = ["llvm-target-all"] +llvm-target-all = ["inkwell/target-all"] +llvm-target-x86 = ["inkwell/target-x86"] diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index b14f9f72..9de75bb3 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -26,6 +26,14 @@ pub struct BackendOptions { pub no_default_libs: bool, } +fn is_windows_gnu_target(target: Option<&str>) -> bool { + let Some(target) = target else { + return false; + }; + let t = target.to_ascii_lowercase(); + t.starts_with("x86_64-") && t.contains("windows") && !t.contains("msvc") +} + fn normalize_clang_opt_flag(opt_flag: &str) -> &str { match opt_flag { // LLVM pass pipeline currently has no dedicated Ofast preset, so keep @@ -131,7 +139,7 @@ pub fn link_objects( cmd.arg("-o").arg(output); - if !backend.no_default_libs { + if !backend.no_default_libs && !is_windows_gnu_target(backend.target.as_deref()) { cmd.arg("-lc").arg("-lm"); } diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index a9cd7ef4..0e15250e 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -351,6 +351,55 @@ fn classify_ret_x86_64_sysv<'ctx>( RetLowering::Direct(t) } +fn classify_param_x86_64_windows<'ctx>( + context: &'ctx Context, + td: &TargetData, + t: BasicTypeEnum<'ctx>, +) -> ParamLowering<'ctx> { + let size = td.get_store_size(&t) as u64; + + match t { + BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_) => match size { + 1 | 2 | 4 | 8 => ParamLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ), + _ => ParamLowering::ByVal { + ty: t.as_any_type_enum(), + align: td.get_abi_alignment(&t) as u32, + }, + }, + _ => ParamLowering::Direct(t), + } +} + +fn classify_ret_x86_64_windows<'ctx>( + context: &'ctx Context, + td: &TargetData, + t: Option>, +) -> RetLowering<'ctx> { + let Some(t) = t else { + return RetLowering::Void; + }; + let size = td.get_store_size(&t) as u64; + + match t { + BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_) => match size { + 1 | 2 | 4 | 8 => RetLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ), + _ => RetLowering::SRet { + ty: t.as_any_type_enum(), + align: td.get_abi_alignment(&t) as u32, + }, + }, + _ => RetLowering::Direct(t), + } +} + fn classify_param_arm64_darwin<'ctx>( td: &TargetData, t: BasicTypeEnum<'ctx>, @@ -448,6 +497,7 @@ fn classify_param<'ctx>( CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 | CodegenTarget::FreestandingX86_64 => classify_param_x86_64_sysv(context, td, t), + CodegenTarget::WindowsX86_64Gnu => classify_param_x86_64_windows(context, td, t), CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::FreestandingArm64 => classify_param_arm64_darwin(td, t), @@ -465,6 +515,7 @@ fn classify_ret<'ctx>( CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 | CodegenTarget::FreestandingX86_64 => classify_ret_x86_64_sysv(context, td, t), + CodegenTarget::WindowsX86_64Gnu => classify_ret_x86_64_windows(context, td, t), CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::FreestandingArm64 => classify_ret_arm64_darwin(td, t), diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 0d7ac1f6..256ee9d6 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -60,6 +60,20 @@ fn target_opt_level_from_flag(opt_flag: &str) -> OptimizationLevel { } } +fn initialize_llvm_targets() { + let config = InitializationConfig::default(); + + #[cfg(feature = "llvm-target-all")] + { + Target::initialize_all(&config); + } + + #[cfg(all(not(feature = "llvm-target-all"), feature = "llvm-target-x86"))] + { + Target::initialize_x86(&config); + } +} + pub unsafe fn generate_ir( ast_nodes: &[ASTNode], opt_flag: &str, @@ -75,7 +89,7 @@ pub unsafe fn generate_ir( .map(|n| resolve_ast_node(n, &named_types)) .collect(); - Target::initialize_all(&InitializationConfig::default()); + initialize_llvm_targets(); let triple = if let Some(raw) = &backend.target { TargetTriple::create(raw) } else { diff --git a/llvm/src/codegen/plan.rs b/llvm/src/codegen/plan.rs index 4e8c8e68..600011d7 100644 --- a/llvm/src/codegen/plan.rs +++ b/llvm/src/codegen/plan.rs @@ -182,6 +182,7 @@ fn parse_token(target: CodegenTarget, raw: &str) -> RegToken { let phys_group = match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => { reg_phys_group_x86_64(&raw_norm).map(|s| s.to_string()) } @@ -216,6 +217,7 @@ fn build_default_clobbers( let mut clobbers = match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => vec![ "~{memory}".to_string(), "~{dirflag}".to_string(), @@ -265,6 +267,7 @@ fn build_default_clobbers( match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => { const GPRS: [&str; 16] = [ "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10", @@ -346,6 +349,7 @@ fn normalize_special_clobber(target: CodegenTarget, token: &str) -> Option match token { "memory" => Some("~{memory}".to_string()), "cc" | "flags" | "eflags" | "rflags" => Some("~{flags}".to_string()), diff --git a/llvm/src/codegen/target.rs b/llvm/src/codegen/target.rs index c2249b79..3d7bd9bc 100644 --- a/llvm/src/codegen/target.rs +++ b/llvm/src/codegen/target.rs @@ -19,6 +19,7 @@ pub enum CodegenTarget { LinuxArm64, DarwinX86_64, DarwinArm64, + WindowsX86_64Gnu, FreestandingX86_64, FreestandingArm64, FreestandingRISCV64, @@ -33,6 +34,7 @@ impl CodegenTarget { let is_riscv64 = t.starts_with("riscv64"); let is_linux = t.contains("linux"); let is_darwin = t.contains("darwin"); + let is_windows_gnu = t.contains("windows") && !t.contains("msvc"); let is_freestanding = t.contains("-none-") || t.ends_with("-none") || t.contains("elf"); if is_x86_64 && is_linux { @@ -47,6 +49,9 @@ impl CodegenTarget { if is_arm64 && is_darwin { return Some(Self::DarwinArm64); } + if is_x86_64 && is_windows_gnu { + return Some(Self::WindowsX86_64Gnu); + } if is_x86_64 && is_freestanding { return Some(Self::FreestandingX86_64); } @@ -76,6 +81,7 @@ impl CodegenTarget { Self::LinuxArm64 => "linux arm64", Self::DarwinX86_64 => "darwin x86_64", Self::DarwinArm64 => "darwin arm64", + Self::WindowsX86_64Gnu => "windows x86_64 gnu", Self::FreestandingX86_64 => "freestanding x86_64", Self::FreestandingArm64 => "freestanding arm64", Self::FreestandingRISCV64 => "freestanding riscv64", @@ -90,7 +96,7 @@ pub fn require_supported_target_from_triple(triple: &TargetTriple) -> CodegenTar let raw = triple.as_str().to_string_lossy(); panic!( - "unsupported target triple '{}': Wave currently supports linux x86_64/arm64, darwin x86_64/arm64, and freestanding x86_64/arm64/riscv64", + "unsupported target triple '{}': Wave currently supports linux x86_64/arm64, darwin x86_64/arm64, windows x86_64 gnu, and freestanding x86_64/arm64/riscv64", raw ); } @@ -103,7 +109,7 @@ pub fn require_supported_target_from_module(module: &Module<'_>) -> CodegenTarge let triple = module.get_triple(); let raw = triple.as_str().to_string_lossy(); panic!( - "unsupported target triple '{}': Wave currently supports linux x86_64/arm64, darwin x86_64/arm64, and freestanding x86_64/arm64/riscv64", + "unsupported target triple '{}': Wave currently supports linux x86_64/arm64, darwin x86_64/arm64, windows x86_64 gnu, and freestanding x86_64/arm64/riscv64", raw ); } diff --git a/llvm/src/expression/rvalue/asm.rs b/llvm/src/expression/rvalue/asm.rs index bb0369d6..0153ab8d 100644 --- a/llvm/src/expression/rvalue/asm.rs +++ b/llvm/src/expression/rvalue/asm.rs @@ -25,6 +25,7 @@ fn inline_asm_dialect_for_target(target: CodegenTarget) -> InlineAsmDialect { match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => InlineAsmDialect::Intel, CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 diff --git a/llvm/src/statement/asm.rs b/llvm/src/statement/asm.rs index 1c227208..e880e1ef 100644 --- a/llvm/src/statement/asm.rs +++ b/llvm/src/statement/asm.rs @@ -67,6 +67,7 @@ fn reg_width_bits_for_target(target: CodegenTarget, reg: &str) -> Option { match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => reg_width_bits(reg), CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 @@ -158,6 +159,7 @@ fn inline_asm_dialect_for_target(target: CodegenTarget) -> InlineAsmDialect { match target { CodegenTarget::LinuxX86_64 | CodegenTarget::DarwinX86_64 + | CodegenTarget::WindowsX86_64Gnu | CodegenTarget::FreestandingX86_64 => InlineAsmDialect::Intel, CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 diff --git a/src/cli.rs b/src/cli.rs index 43156d2c..3e449641 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -224,7 +224,7 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> let classified = classify_inputs(build)?; validate_build_request(&effective_global, build, &classified)?; - let plan = create_build_plan(build, &classified)?; + let plan = create_build_plan(&effective_global, build, &classified)?; if build.dry_run { print_dry_run(&effective_global, build, &classified, &plan); @@ -238,6 +238,7 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> &input.path, &effective_global.debug, &effective_global.dep, + &effective_global.llvm, ); } } @@ -1460,6 +1461,7 @@ fn set_link_sysroot_arg(link_args: &mut Vec, value: &str) { } fn create_build_plan( + global: &Global, build: &BuildRequest, classified: &[ClassifiedInput], ) -> Result { @@ -1517,7 +1519,7 @@ fn create_build_plan( let primary = classified .first() .ok_or_else(|| CliError::usage("build requires at least one input"))?; - plan.link_output = Some(resolve_binary_output_path(build, primary)); + plan.link_output = Some(resolve_binary_output_path(global, build, primary)); } Ok(plan) @@ -1556,7 +1558,11 @@ fn resolve_object_output_path( PathBuf::from("target").join(file_name) } -fn resolve_binary_output_path(build: &BuildRequest, primary: &ClassifiedInput) -> PathBuf { +fn resolve_binary_output_path( + global: &Global, + build: &BuildRequest, + primary: &ClassifiedInput, +) -> PathBuf { if let Some(path) = &build.output { return path.clone(); } @@ -1566,8 +1572,12 @@ fn resolve_binary_output_path(build: &BuildRequest, primary: &ClassifiedInput) - .file_stem() .and_then(|s| s.to_str()) .filter(|s| !s.is_empty()) - .unwrap_or("a.out") - .to_string(); + .unwrap_or("a.out"); + let stem = if is_windows_gnu_target_global(global) { + format!("{}.exe", stem) + } else { + stem.to_string() + }; if let Some(out_dir) = &build.out_dir { return out_dir.join(&stem); @@ -1710,7 +1720,12 @@ fn execute_explicit_emit_artifacts( match kind { EmitKind::Ast => { let text = unsafe { - runner::emit_wave_ast_text(&input.path, &global.debug, &global.dep) + runner::emit_wave_ast_text( + &input.path, + &global.debug, + &global.dep, + &global.llvm, + ) }; fs::write(output, text)?; } @@ -2022,7 +2037,7 @@ fn build_linker_args( args.push("-o".to_string()); args.push(output.to_string_lossy().to_string()); - if !global.llvm.no_default_libs { + if !global.llvm.no_default_libs && !is_windows_gnu_target_global(global) { args.push("-lc".to_string()); args.push("-lm".to_string()); } @@ -2453,17 +2468,44 @@ fn host_target_triple() -> String { } fn supported_targets() -> &'static [&'static str] { + #[cfg(all(feature = "llvm-target-x86", not(feature = "llvm-target-all")))] + { + return &[ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-w64-windows-gnu", + "x86_64-pc-windows-gnu", + "x86_64-unknown-none-elf", + ]; + } + + #[cfg(not(all(feature = "llvm-target-x86", not(feature = "llvm-target-all"))))] &[ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "aarch64-apple-darwin", + "x86_64-w64-windows-gnu", + "x86_64-pc-windows-gnu", "x86_64-unknown-none-elf", "aarch64-unknown-none-elf", "riscv64-unknown-none-elf", ] } +fn is_windows_gnu_target(target: &str) -> bool { + let t = target.to_ascii_lowercase(); + t.starts_with("x86_64-") && t.contains("windows") && !t.contains("msvc") +} + +fn is_windows_gnu_target_global(global: &Global) -> bool { + global + .llvm + .target + .as_deref() + .is_some_and(is_windows_gnu_target) +} + fn ensure_supported_target(target: &str) -> Result<(), CliError> { if target == host_target_triple() || supported_targets().iter().any(|t| *t == target) { return Ok(()); diff --git a/src/runner.rs b/src/runner.rs index 0e1d41f0..233f88c2 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -40,7 +40,7 @@ fn parse_target_os_attr(line: &str) -> Option> { let start = "#[target(os=\"".len(); let end = trimmed.len() - 3; // ")]" let os = &trimmed[start..end]; - if os == "linux" || os == "macos" { + if os == "linux" || os == "macos" || os == "windows" { Some(TargetAttr::Supported(os)) } else { Some(TargetAttr::Unsupported) @@ -191,8 +191,27 @@ fn consume_target_item(lines: &[&str], mut idx: usize, keep: bool, out: &mut Vec idx } -fn preprocess_target_attrs(source: &str) -> String { - let host = std::env::consts::OS; +fn target_os_from_triple(triple: &str) -> Option<&'static str> { + let lower = triple.to_ascii_lowercase(); + if lower.contains("windows") { + Some("windows") + } else if lower.contains("darwin") || lower.contains("apple") { + Some("macos") + } else if lower.contains("linux") { + Some("linux") + } else { + None + } +} + +fn target_os_for_llvm(llvm: Option<&LlvmFlags>) -> Option { + llvm.and_then(|opts| opts.target.as_deref()) + .and_then(target_os_from_triple) + .map(str::to_string) +} + +fn preprocess_target_attrs(source: &str, target_os: Option<&str>) -> String { + let host = target_os.unwrap_or(std::env::consts::OS); let lines: Vec<&str> = source.lines().collect(); let mut out: Vec = Vec::with_capacity(lines.len()); let mut idx: usize = 0; @@ -739,8 +758,9 @@ fn emit_codegen_panic_and_exit( process::exit(1); } -fn build_import_config(dep: &DepFlags) -> ImportConfig { +fn build_import_config(dep: &DepFlags, target_os: Option) -> ImportConfig { let mut config = ImportConfig::default(); + config.target_os = target_os; for root in &dep.roots { config.dep_roots.push(PathBuf::from(root)); @@ -956,6 +976,7 @@ fn frontend_prepare_wave_ast( file_path: &Path, debug: &DebugFlags, dep: &DepFlags, + llvm: Option<&LlvmFlags>, ) -> (String, Vec) { let raw_code = match fs::read_to_string(file_path) { Ok(c) => c, @@ -972,7 +993,8 @@ fn frontend_prepare_wave_ast( process::exit(1); } }; - let code = preprocess_target_attrs(&raw_code); + let target_os = target_os_for_llvm(llvm); + let code = preprocess_target_attrs(&raw_code, target_os.as_deref()); let mut lexer = Lexer::new_with_file(&code, file_path.display().to_string()); let tokens = lexer.tokenize().unwrap_or_else(|e| { @@ -993,7 +1015,7 @@ fn frontend_prepare_wave_ast( println!("\n===== AST =====\n{:#?}", parsed_ast); } - let import_config = build_import_config(dep); + let import_config = build_import_config(dep, target_os); let ast = match expand_imports_for_codegen(file_path, parsed_ast, &import_config) { Ok(a) => a, Err(e) => { @@ -1026,16 +1048,22 @@ fn frontend_prepare_wave_ast( (code, ast) } -pub(crate) unsafe fn check_wave_file(file_path: &Path, debug: &DebugFlags, dep: &DepFlags) { - let _ = frontend_prepare_wave_ast(file_path, debug, dep); +pub(crate) unsafe fn check_wave_file( + file_path: &Path, + debug: &DebugFlags, + dep: &DepFlags, + llvm: &LlvmFlags, +) { + let _ = frontend_prepare_wave_ast(file_path, debug, dep, Some(llvm)); } pub(crate) unsafe fn emit_wave_ast_text( file_path: &Path, debug: &DebugFlags, dep: &DepFlags, + llvm: &LlvmFlags, ) -> String { - let (_, ast) = frontend_prepare_wave_ast(file_path, debug, dep); + let (_, ast) = frontend_prepare_wave_ast(file_path, debug, dep, Some(llvm)); format!("{:#?}\n", ast) } @@ -1046,7 +1074,7 @@ pub(crate) unsafe fn emit_wave_ir_text( dep: &DepFlags, llvm: &LlvmFlags, ) -> String { - let (code, ast) = frontend_prepare_wave_ast(file_path, debug, dep); + let (code, ast) = frontend_prepare_wave_ast(file_path, debug, dep, Some(llvm)); let backend_opts = build_backend_options(llvm); let ir = match run_panic_guarded(|| unsafe { generate_ir(&ast, opt_flag, &backend_opts) }) { @@ -1087,7 +1115,8 @@ pub(crate) unsafe fn run_wave_file( process::exit(1); } }; - let code = preprocess_target_attrs(&raw_code); + let target_os = target_os_for_llvm(Some(llvm)); + let code = preprocess_target_attrs(&raw_code, target_os.as_deref()); let mut lexer = Lexer::new_with_file(&code, file_path.display().to_string()); let tokens = lexer.tokenize().unwrap_or_else(|e| { @@ -1108,7 +1137,7 @@ pub(crate) unsafe fn run_wave_file( println!("\n===== AST =====\n{:#?}", ast); } - let import_config = build_import_config(dep); + let import_config = build_import_config(dep, target_os); let ast = match expand_imports_for_codegen(file_path, ast, &import_config) { Ok(a) => a, @@ -1227,7 +1256,8 @@ pub(crate) unsafe fn object_build_wave_file( .display(); process::exit(1); }); - let code = preprocess_target_attrs(&raw_code); + let target_os = target_os_for_llvm(Some(llvm)); + let code = preprocess_target_attrs(&raw_code, target_os.as_deref()); let mut lexer = Lexer::new_with_file(&code, file_path.display().to_string()); let tokens = lexer.tokenize().unwrap_or_else(|e| { @@ -1248,7 +1278,7 @@ pub(crate) unsafe fn object_build_wave_file( println!("\n===== AST =====\n{:#?}", ast); } - let import_config = build_import_config(dep); + let import_config = build_import_config(dep, target_os); let ast = expand_imports_for_codegen(file_path, ast, &import_config).unwrap_or_else(|e| { e.display(); diff --git a/x.py b/x.py index 7df07d76..dcc70d0b 100644 --- a/x.py +++ b/x.py @@ -31,6 +31,14 @@ TARGET_DIR = ROOT / "target" BINARY_NAME = "wavec" NAME = "wave" +WINDOWS_GNU_TARGET = "x86_64-pc-windows-gnu" +WINDOWS_LLVM_PREFIX = ROOT / "tools" / "llvm-win-prefix" +WINDOWS_LLVM_CONFIG_EXE = Path(os.environ.get( + "LLVM_CONFIG_EXE", + "/opt/llvm-win/bin/llvm-config.exe", +)) +MINGW_CC = "x86_64-w64-mingw32-gcc" +MINGW_CXX = "x86_64-w64-mingw32-g++" TARGET_MATRIX = { "x86_64-unknown-linux-gnu": ["Linux"], @@ -42,7 +50,7 @@ "x86_64-unknown-redox": ["Redox"], "x86_64-unknown-fuchsia": ["Fuchsia"], "x86_64-unknown-haiku": ["Haiku"], - "x86_64-pc-windows-gnu": ["Windows"], + WINDOWS_GNU_TARGET: ["Linux", "Windows"], "aarch64-apple-darwin": ["Darwin"], "x86_64-apple-darwin": ["Darwin"], } @@ -73,6 +81,71 @@ def get_version(): VERSION = get_version() +def is_windows_gnu_target(target): + return target == WINDOWS_GNU_TARGET + +def require_tool(tool): + if shutil.which(tool) is None: + print(f"[!] Missing required tool: {tool}") + sys.exit(1) + +def configure_windows_gnu_env(env): + if not WINDOWS_LLVM_PREFIX.exists(): + print(f"[!] Missing Windows LLVM prefix wrapper: {WINDOWS_LLVM_PREFIX}") + print(" Expected tools/llvm-win-prefix/bin/llvm-config to exist.") + sys.exit(1) + + if not WINDOWS_LLVM_CONFIG_EXE.exists(): + print(f"[!] Missing Windows llvm-config.exe: {WINDOWS_LLVM_CONFIG_EXE}") + print(" Set LLVM_CONFIG_EXE=/path/to/llvm-config.exe if it is installed elsewhere.") + sys.exit(1) + + require_tool(MINGW_CC) + require_tool(MINGW_CXX) + require_tool("wine") + + env["LLVM_SYS_211_PREFIX"] = str(WINDOWS_LLVM_PREFIX) + env["LLVM_CONFIG_EXE"] = str(WINDOWS_LLVM_CONFIG_EXE) + env["CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER"] = MINGW_CC + env["CC_x86_64_pc_windows_gnu"] = MINGW_CC + env["CXX_x86_64_pc_windows_gnu"] = MINGW_CXX + +def cargo_build_args(target): + args = ["cargo", "build", "--target", target, "--release"] + if is_windows_gnu_target(target): + args.extend(["--no-default-features", "--features", "llvm-target-x86"]) + return args + +def mingw_print_file_name(name): + fallback = Path("/usr/x86_64-w64-mingw32/sys-root/mingw/bin") / name + if shutil.which(MINGW_CC) is None: + return fallback if fallback.exists() else None + + result = subprocess.run( + [MINGW_CC, f"-print-file-name={name}"], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0: + return None + + path = Path(result.stdout.strip()) + if path.exists() and path.name.lower() == name.lower(): + return path + if fallback.exists(): + return fallback + return None + +def windows_package_inputs(exe_path): + files = [exe_path] + for dll in ["libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"]: + path = mingw_print_file_name(dll) + if path is not None: + files.append(path) + return files + # ------------------------------------------------------ # rustup target add # ------------------------------------------------------ @@ -93,14 +166,12 @@ def cmd_build(): env = os.environ.copy() - if platform.system() == "Linux" and t == "x86_64-pc-windows-gnu": - print(" [*] Applying MinGW LLVM environment") - - env["LLVM_SYS_211_PREFIX"] = "/opt/llvm-win" - env["LLVM_CONFIG_PATH"] = "/opt/llvm-win/bin/llvm-config.exe" + if is_windows_gnu_target(t): + print(" [*] Applying MinGW + Windows LLVM environment") + configure_windows_gnu_env(env) subprocess.run( - ["cargo", "build", "--target", t, "--release"], + cargo_build_args(t), check=True, env=env ) @@ -127,10 +198,16 @@ def cmd_package(): print(f"[!] Missing binary: {bin_path}") continue - shutil.copy(bin_path, ROOT / f"{BINARY_NAME}.exe") + package_files = [] + for src in windows_package_inputs(bin_path): + dst = ROOT / src.name + shutil.copy(src, dst) + package_files.append(dst.name) + zip_path = ROOT / f"{out_name}.zip" - subprocess.run(["zip", "-q", zip_path, f"{BINARY_NAME}.exe"], check=True) - os.remove(ROOT / f"{BINARY_NAME}.exe") + subprocess.run(["zip", "-q", zip_path, *package_files], check=True) + for name in package_files: + os.remove(ROOT / name) print(f"[+] Windows packaged → {zip_path}") @@ -349,11 +426,23 @@ def cmd_clean(): # ------------------------------------------------------ def main(): if len(sys.argv) < 2: - print("Usage: x.py [install | build | package | release | clean | gui]") + print("Usage: x.py [install | build | package | release | clean | gui] [target...]") return cmd = sys.argv[1] + selected_targets = sys.argv[2:] + + global TARGETS + if selected_targets: + unknown = [t for t in selected_targets if t not in ALL_TARGETS] + if unknown: + print("Unknown target(s):", ", ".join(unknown)) + print("Known targets:") + for target in ALL_TARGETS: + print(" ", target) + sys.exit(1) + TARGETS = selected_targets if cmd == "install": cmd_install() @@ -369,7 +458,7 @@ def main(): cmd_gui() else: print("Unknown command:", cmd) - print("Usage: x.py [install | build | package | release | clean | gui]") + print("Usage: x.py [install | build | package | release | clean | gui] [target...]") if __name__ == "__main__": main()