diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index b5ec7f4c..c94e726f 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -10,6 +10,7 @@ // SPDX-License-Identifier: MPL-2.0 // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. +use crate::codegen::target::{target_spec_for_triple, CodegenTarget}; use std::env; use std::path::PathBuf; use std::process::Command; @@ -30,11 +31,9 @@ pub struct BackendOptions { } 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") + target + .and_then(target_spec_for_triple) + .is_some_and(|spec| spec.codegen == CodegenTarget::WindowsX86_64Gnu) } fn normalize_llvm_opt_flag(opt_flag: &str) -> &str { @@ -173,11 +172,13 @@ fn default_lld_for_target(target: &str) -> String { fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOptions) { if is_darwin_target(target) { + let spec = target_spec_for_triple(target) + .expect("Darwin linker configuration requires a registered target"); cmd.arg("-arch") - .arg(if target.starts_with("x86_64-") { - "x86_64" - } else { + .arg(if spec.arch == "aarch64" { "arm64" + } else { + spec.arch }) .arg("-platform_version") .arg("macos") @@ -223,14 +224,14 @@ fn expand_lld_link_args(link_args: &[String]) -> Vec { } fn is_darwin_target(target: &str) -> bool { - target.contains("apple-darwin") + target_spec_for_triple(target).is_some_and(|spec| spec.os == "macos") } fn elf_lld_emulation(target: &str) -> Option<&'static str> { - match target.split('-').next().unwrap_or(target) { - "x86_64" => Some("elf_x86_64"), - "aarch64" => Some("aarch64elf"), - "riscv64" => Some("elf64lriscv"), + match target_spec_for_triple(target)?.codegen { + CodegenTarget::LinuxX86_64 | CodegenTarget::FreestandingX86_64 => Some("elf_x86_64"), + CodegenTarget::LinuxArm64 | CodegenTarget::FreestandingArm64 => Some("aarch64elf"), + CodegenTarget::FreestandingRISCV64 => Some("elf64lriscv"), _ => None, } } diff --git a/llvm/src/codegen/target.rs b/llvm/src/codegen/target.rs index 3d7bd9bc..f5b4ce3b 100644 --- a/llvm/src/codegen/target.rs +++ b/llvm/src/codegen/target.rs @@ -25,44 +25,206 @@ pub enum CodegenTarget { FreestandingRISCV64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TargetSpec { + pub triple: &'static str, + pub codegen: CodegenTarget, + pub arch: &'static str, + pub vendor: &'static str, + pub os: &'static str, + pub env: &'static str, + pub object_format: &'static str, + pub hosted: bool, + pub cpus: &'static [&'static str], + pub features: &'static [&'static str], + pub abis: &'static [&'static str], +} + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const X86_CPUS: &[&str] = &["generic", "x86-64", "x86-64-v2", "x86-64-v3"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const X86_FEATURES: &[&str] = &["sse2", "sse4.1", "avx", "avx2"]; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const AARCH64_CPUS: &[&str] = &["generic", "cortex-a53", "cortex-a72"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const DARWIN_AARCH64_CPUS: &[&str] = &["generic", "apple-m1"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const AARCH64_FEATURES: &[&str] = &["neon", "fp-armv8", "crypto"]; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +const RISCV64_CPUS: &[&str] = &["generic", "generic-rv64", "rocket-rv64", "sifive-u74"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +const RISCV64_FEATURES: &[&str] = &["m", "a", "f", "d", "c"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +const RISCV64_ABIS: &[&str] = &["lp64", "lp64f", "lp64d"]; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const LINUX_X86_64: TargetSpec = TargetSpec { + triple: "x86_64-unknown-linux-gnu", + codegen: CodegenTarget::LinuxX86_64, + arch: "x86_64", + vendor: "unknown", + os: "linux", + env: "gnu", + object_format: "elf", + hosted: true, + cpus: X86_CPUS, + features: X86_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const DARWIN_X86_64: TargetSpec = TargetSpec { + triple: "x86_64-apple-darwin", + codegen: CodegenTarget::DarwinX86_64, + arch: "x86_64", + vendor: "apple", + os: "macos", + env: "", + object_format: "macho", + hosted: true, + cpus: X86_CPUS, + features: X86_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const WINDOWS_W64_X86_64_GNU: TargetSpec = TargetSpec { + triple: "x86_64-w64-windows-gnu", + codegen: CodegenTarget::WindowsX86_64Gnu, + arch: "x86_64", + vendor: "w64", + os: "windows", + env: "gnu", + object_format: "coff", + hosted: true, + cpus: X86_CPUS, + features: X86_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const WINDOWS_PC_X86_64_GNU: TargetSpec = TargetSpec { + triple: "x86_64-pc-windows-gnu", + codegen: CodegenTarget::WindowsX86_64Gnu, + arch: "x86_64", + vendor: "pc", + os: "windows", + env: "gnu", + object_format: "coff", + hosted: true, + cpus: X86_CPUS, + features: X86_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const FREESTANDING_X86_64: TargetSpec = TargetSpec { + triple: "x86_64-unknown-none-elf", + codegen: CodegenTarget::FreestandingX86_64, + arch: "x86_64", + vendor: "unknown", + os: "none", + env: "none", + object_format: "elf", + hosted: false, + cpus: X86_CPUS, + features: X86_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const LINUX_AARCH64: TargetSpec = TargetSpec { + triple: "aarch64-unknown-linux-gnu", + codegen: CodegenTarget::LinuxArm64, + arch: "aarch64", + vendor: "unknown", + os: "linux", + env: "gnu", + object_format: "elf", + hosted: true, + cpus: AARCH64_CPUS, + features: AARCH64_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const DARWIN_AARCH64: TargetSpec = TargetSpec { + triple: "aarch64-apple-darwin", + codegen: CodegenTarget::DarwinArm64, + arch: "aarch64", + vendor: "apple", + os: "macos", + env: "", + object_format: "macho", + hosted: true, + cpus: DARWIN_AARCH64_CPUS, + features: AARCH64_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const FREESTANDING_AARCH64: TargetSpec = TargetSpec { + triple: "aarch64-unknown-none-elf", + codegen: CodegenTarget::FreestandingArm64, + arch: "aarch64", + vendor: "unknown", + os: "none", + env: "none", + object_format: "elf", + hosted: false, + cpus: AARCH64_CPUS, + features: AARCH64_FEATURES, + abis: &[], +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +const FREESTANDING_RISCV64: TargetSpec = TargetSpec { + triple: "riscv64-unknown-none-elf", + codegen: CodegenTarget::FreestandingRISCV64, + arch: "riscv64", + vendor: "unknown", + os: "none", + env: "none", + object_format: "elf", + hosted: false, + cpus: RISCV64_CPUS, + features: RISCV64_FEATURES, + abis: RISCV64_ABIS, +}; + +pub fn supported_target_specs() -> Vec<&'static TargetSpec> { + let mut specs: Vec<&'static TargetSpec> = Vec::new(); + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] + specs.extend([ + &LINUX_X86_64, + &DARWIN_X86_64, + &WINDOWS_W64_X86_64_GNU, + &WINDOWS_PC_X86_64_GNU, + &FREESTANDING_X86_64, + ]); + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] + specs.extend([&LINUX_AARCH64, &DARWIN_AARCH64, &FREESTANDING_AARCH64]); + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + specs.push(&FREESTANDING_RISCV64); + + specs.sort_unstable_by_key(|spec| spec.triple); + specs +} + +pub fn target_spec_for_triple(triple: &str) -> Option<&'static TargetSpec> { + supported_target_specs() + .into_iter() + .find(|spec| spec.triple == triple) +} + impl CodegenTarget { pub fn from_triple_str(triple: &str) -> Option { - let t = triple.to_ascii_lowercase(); - - let is_x86_64 = t.starts_with("x86_64"); - let is_arm64 = t.starts_with("arm64") || t.starts_with("aarch64"); - 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 { - return Some(Self::LinuxX86_64); - } - if is_arm64 && is_linux { - return Some(Self::LinuxArm64); - } - if is_x86_64 && is_darwin { - return Some(Self::DarwinX86_64); - } - 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); - } - if is_arm64 && is_freestanding { - return Some(Self::FreestandingArm64); - } - if is_riscv64 && is_freestanding { - return Some(Self::FreestandingRISCV64); - } - - None + target_spec_for_triple(triple).map(|spec| spec.codegen) } pub fn from_target_triple(triple: &TargetTriple) -> Option { @@ -95,9 +257,14 @@ pub fn require_supported_target_from_triple(triple: &TargetTriple) -> CodegenTar } let raw = triple.as_str().to_string_lossy(); + let supported = supported_target_specs() + .into_iter() + .map(|spec| spec.triple) + .collect::>() + .join(", "); panic!( - "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 + "unsupported target triple '{}': Wave currently supports {}", + raw, supported ); } @@ -108,8 +275,53 @@ pub fn require_supported_target_from_module(module: &Module<'_>) -> CodegenTarge let triple = module.get_triple(); let raw = triple.as_str().to_string_lossy(); + let supported = supported_target_specs() + .into_iter() + .map(|spec| spec.triple) + .collect::>() + .join(", "); panic!( - "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 + "unsupported target triple '{}': Wave currently supports {}", + raw, supported ); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registered_targets_round_trip_through_exact_lookup() { + let specs = supported_target_specs(); + assert!(!specs.is_empty()); + + for (index, spec) in specs.iter().enumerate() { + assert_eq!(target_spec_for_triple(spec.triple), Some(*spec)); + assert_eq!( + CodegenTarget::from_triple_str(spec.triple), + Some(spec.codegen) + ); + assert!(!spec.arch.is_empty()); + assert!(!spec.os.is_empty()); + assert!(!spec.object_format.is_empty()); + + for other in specs.iter().skip(index + 1) { + assert_ne!(spec.triple, other.triple, "duplicate target triple"); + } + } + } + + #[test] + fn malformed_or_unregistered_triples_do_not_match_by_substring() { + for triple in [ + "x86_64-garbage-linux-gnu", + "prefix-x86_64-unknown-linux-gnu-suffix", + "riscv64-unknown-linux-gnu", + "x86_64-unknown-none-elf-waveabi", + "", + ] { + assert_eq!(target_spec_for_triple(triple), None, "{triple}"); + assert_eq!(CodegenTarget::from_triple_str(triple), None, "{triple}"); + } + } +} diff --git a/src/cli.rs b/src/cli.rs index 00364fcc..ed8d3ecd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,6 +17,9 @@ use crate::flags::{ use crate::{runner, std as wave_std, version}; use crate::version::get_os_pretty_name; +use llvm::codegen::target::{ + supported_target_specs, target_spec_for_triple, CodegenTarget, TargetSpec, +}; use std::collections::BTreeSet; use std::io::ErrorKind; use std::path::{Path, PathBuf}; @@ -278,6 +281,7 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> configure_wave_error_format(build.error_format); let effective_global = effective_global_for_build(global, &build); + validate_target_configuration(&effective_global.llvm)?; let classified = classify_inputs(&build)?; validate_build_request(&effective_global, &build, &classified)?; @@ -417,10 +421,12 @@ fn dispatch_print_human(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "host" => { + validate_target_options_for(&host_target_triple(), &global.llvm)?; print_target_spec_human(global, &host_target_triple()); Ok(()) } "target-spec" => { + validate_target_options_for(target, &global.llvm)?; print_target_spec_human(global, target); Ok(()) } @@ -431,6 +437,7 @@ fn dispatch_print_human(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "sysroot" => { + ensure_supported_target(target)?; if let Some(s) = detect_default_sysroot(target) { println!("{}", s); } else { @@ -453,6 +460,7 @@ fn dispatch_print_human(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "default-linker" => { + ensure_supported_target(target)?; let target_global = global_with_target(global, target); println!("{}", default_linker_name(&target_global)); Ok(()) @@ -477,15 +485,15 @@ fn dispatch_print_human(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "cpu-list" => { - ensure_supported_target(target)?; - for cpu in cpu_list_for_target(target) { + let spec = ensure_supported_target(target)?; + for cpu in spec.cpus { println!("{}", cpu); } Ok(()) } "target-features" => { - ensure_supported_target(target)?; - for feat in target_features_for_target(target) { + let spec = ensure_supported_target(target)?; + for feat in spec.features { println!("{}", feat); } Ok(()) @@ -501,10 +509,12 @@ fn dispatch_print_json(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "host" => { + validate_target_options_for(&host_target_triple(), &global.llvm)?; println!("{}", target_spec_json(global, &host_target_triple())); Ok(()) } "target-spec" => { + validate_target_options_for(target, &global.llvm)?; println!("{}", target_spec_json(global, target)); Ok(()) } @@ -513,6 +523,7 @@ fn dispatch_print_json(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "sysroot" => { + ensure_supported_target(target)?; println!( "{}", json_optional_string(detect_default_sysroot(target).as_deref()) @@ -529,6 +540,7 @@ fn dispatch_print_json(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "default-linker" => { + ensure_supported_target(target)?; let target_global = global_with_target(global, target); println!("{}", json_string(&default_linker_name(&target_global))); Ok(()) @@ -548,13 +560,13 @@ fn dispatch_print_json(global: &Global, item: &str, target: &str) -> Result<(), Ok(()) } "cpu-list" => { - ensure_supported_target(target)?; - println!("{}", json_string_array(cpu_list_for_target(target))); + let spec = ensure_supported_target(target)?; + println!("{}", json_string_array(spec.cpus.to_vec())); Ok(()) } "target-features" => { - ensure_supported_target(target)?; - println!("{}", json_string_array(target_features_for_target(target))); + let spec = ensure_supported_target(target)?; + println!("{}", json_string_array(spec.features.to_vec())); Ok(()) } _ => Err(CliError::usage(format!("unknown print item: {}", item))), @@ -2471,48 +2483,42 @@ fn target_triple_for_global(global: &Global) -> String { } fn is_darwin_target(target: &str) -> bool { - target.contains("apple-darwin") + target_spec_for_triple(target).is_some_and(|spec| spec.os == "macos") } fn is_linux_target(target: &str) -> bool { - target.contains("linux") -} - -fn target_arch(target: &str) -> &str { - target.split('-').next().unwrap_or(target) + target_spec_for_triple(target).is_some_and(|spec| spec.os == "linux") } fn darwin_arch(target: &str) -> &'static str { - match target_arch(target) { - "aarch64" => "arm64", - "x86_64" => "x86_64", - _ => "arm64", + match target_spec_for_triple(target).map(|spec| spec.codegen) { + Some(CodegenTarget::DarwinArm64) => "arm64", + Some(CodegenTarget::DarwinX86_64) => "x86_64", + _ => unreachable!("Darwin linker requires a registered Darwin target"), } } fn elf_lld_emulation(target: &str) -> Option<&'static str> { - match target_arch(target) { - "x86_64" => Some("elf_x86_64"), - "aarch64" => Some("aarch64elf"), - "riscv64" => Some("elf64lriscv"), + match target_spec_for_triple(target)?.codegen { + CodegenTarget::LinuxX86_64 | CodegenTarget::FreestandingX86_64 => Some("elf_x86_64"), + CodegenTarget::LinuxArm64 | CodegenTarget::FreestandingArm64 => Some("aarch64elf"), + CodegenTarget::FreestandingRISCV64 => Some("elf64lriscv"), _ => None, } } fn linux_dynamic_linker(target: &str) -> Option<&'static str> { - match target_arch(target) { - "x86_64" => Some("/lib64/ld-linux-x86-64.so.2"), - "aarch64" => Some("/lib/ld-linux-aarch64.so.1"), - "riscv64" => Some("/lib/ld-linux-riscv64-lp64d.so.1"), + match target_spec_for_triple(target)?.codegen { + CodegenTarget::LinuxX86_64 => Some("/lib64/ld-linux-x86-64.so.2"), + CodegenTarget::LinuxArm64 => Some("/lib/ld-linux-aarch64.so.1"), _ => None, } } fn linux_multiarch(target: &str) -> Option<&'static str> { - match target_arch(target) { - "x86_64" => Some("x86_64-linux-gnu"), - "aarch64" => Some("aarch64-linux-gnu"), - "riscv64" => Some("riscv64-linux-gnu"), + match target_spec_for_triple(target)?.codegen { + CodegenTarget::LinuxX86_64 => Some("x86_64-linux-gnu"), + CodegenTarget::LinuxArm64 => Some("aarch64-linux-gnu"), _ => None, } } @@ -3416,30 +3422,10 @@ fn host_target_triple() -> String { } fn supported_targets() -> Vec<&'static str> { - let mut targets = Vec::new(); - - #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] - targets.extend([ - "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(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] - targets.extend([ - "aarch64-unknown-linux-gnu", - "aarch64-apple-darwin", - "aarch64-unknown-none-elf", - ]); - - #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] - targets.extend(["riscv64-unknown-none-elf"]); - - targets.sort_unstable(); - targets.dedup(); - targets + supported_target_specs() + .into_iter() + .map(|spec| spec.triple) + .collect() } fn supported_input_types() -> Vec<&'static str> { @@ -3479,37 +3465,24 @@ struct TargetSpecInfo { env: Option, abi: Option, object_format: &'static str, + hosted: bool, supported: bool, } fn target_spec_info(global: &Global, target: &str) -> TargetSpecInfo { - let parts = target.split('-').collect::>(); - let arch = parts - .first() - .map(|s| canonical_target_arch(s)) - .unwrap_or_else(|| "unknown".to_string()); - let vendor = parts.get(1).map(|s| (*s).to_string()); - let os = target_os_name(target); - let env = target_env_name(target); - let abi = global.llvm.abi.clone().or_else(|| target_abi_name(target)); - let object_format = if is_windows_gnu_target(target) { - "coff" - } else if is_darwin_target(target) { - "macho" - } else { - "elf" - }; - let supported = target == host_target_triple() || supported_targets().contains(&target); + let spec = target_spec_for_triple(target) + .expect("target spec rendering requires a validated target triple"); TargetSpecInfo { triple: target.to_string(), - arch, - vendor, - os, - env, - abi, - object_format, - supported, + arch: spec.arch.to_string(), + vendor: Some(spec.vendor.to_string()), + os: Some(spec.os.to_string()), + env: Some(spec.env.to_string()), + abi: global.llvm.abi.clone(), + object_format: spec.object_format, + hosted: spec.hosted, + supported: true, } } @@ -3523,6 +3496,8 @@ fn print_target_spec_human(global: &Global, target: &str) { println!("env: {}", spec.env.as_deref().unwrap_or("")); println!("abi: {}", spec.abi.as_deref().unwrap_or("")); println!("object-format: {}", spec.object_format); + println!("hosted: {}", spec.hosted); + println!("freestanding: {}", !spec.hosted); println!("supported: {}", spec.supported); println!("default-linker: {}", default_linker_name(&target_global)); println!( @@ -3553,6 +3528,18 @@ fn target_spec_json(global: &Global, target: &str) -> String { out.push(','); append_json_field(&mut out, "object_format", &json_string(spec.object_format)); out.push(','); + append_json_field( + &mut out, + "hosted", + if spec.hosted { "true" } else { "false" }, + ); + out.push(','); + append_json_field( + &mut out, + "freestanding", + if spec.hosted { "false" } else { "true" }, + ); + out.push(','); append_json_field( &mut out, "supported", @@ -3581,99 +3568,148 @@ fn global_with_target(global: &Global, target: &str) -> Global { } 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") + target_spec_for_triple(target).is_some_and(|spec| spec.os == "windows" && spec.env == "gnu") } -fn canonical_target_arch(arch: &str) -> String { - match arch.to_ascii_lowercase().as_str() { - "amd64" => "x86_64".to_string(), - "arm64" => "aarch64".to_string(), - other => other.to_string(), - } +fn is_windows_gnu_target_global(global: &Global) -> bool { + global + .llvm + .target + .as_deref() + .is_some_and(is_windows_gnu_target) } -fn target_os_name(target: &str) -> Option { - let t = target.to_ascii_lowercase(); - if t.contains("windows") { - Some("windows".to_string()) - } else if t.contains("darwin") || t.contains("apple") { - Some("macos".to_string()) - } else if t.contains("linux") { - Some("linux".to_string()) - } else if t.contains("none") { - Some("none".to_string()) - } else { - None - } +fn ensure_supported_target(target: &str) -> Result<&'static TargetSpec, CliError> { + target_spec_for_triple(target).ok_or_else(|| { + CliError::usage(format!( + "unsupported target '{}'; supported targets: {}; see `wavec print target-list`", + target, + supported_targets().join(", ") + )) + }) } -fn target_env_name(target: &str) -> Option { - let t = target.to_ascii_lowercase(); - if t.contains("windows") && t.contains("gnu") { - Some("gnu".to_string()) - } else if t.contains("windows") && t.contains("msvc") { - Some("msvc".to_string()) - } else if t.contains("linux") && t.contains("musl") { - Some("musl".to_string()) - } else if t.contains("linux") && t.contains("gnu") { - Some("gnu".to_string()) - } else if t.contains("none") { - Some("none".to_string()) - } else { - None - } +fn validate_target_configuration(llvm: &LlvmFlags) -> Result<(), CliError> { + let target = llvm + .target + .as_deref() + .ok_or_else(|| CliError::usage("target resolution did not produce a target triple"))?; + validate_target_options_for(target, llvm) } -fn target_abi_name(target: &str) -> Option { - let parts = target.split('-').collect::>(); - if parts.len() >= 5 { - parts.last().map(|s| (*s).to_string()) - } else { - None +fn validate_target_options_for(target: &str, llvm: &LlvmFlags) -> Result<(), CliError> { + let spec = ensure_supported_target(target)?; + + if let Some(cpu) = llvm.cpu.as_deref() { + if !spec.cpus.contains(&cpu) { + return Err(CliError::usage(format!( + "unsupported CPU '{}' for target '{}'; supported CPUs: {}", + cpu, + target, + spec.cpus.join(", ") + ))); + } + } + + if let Some(features) = llvm.features.as_deref() { + validate_target_features(spec, features)?; + } + + if let Some(abi) = llvm.abi.as_deref() { + if !spec.abis.contains(&abi) { + let supported = if spec.abis.is_empty() { + "no ABI overrides".to_string() + } else { + spec.abis.join(", ") + }; + return Err(CliError::usage(format!( + "unsupported ABI '{}' for target '{}'; supported ABIs: {}", + abi, target, supported + ))); + } } + + validate_target_feature_abi_compatibility(spec, llvm.features.as_deref(), llvm.abi.as_deref()) } -fn is_windows_gnu_target_global(global: &Global) -> bool { - global - .llvm - .target - .as_deref() - .is_some_and(is_windows_gnu_target) +fn validate_target_features(spec: &TargetSpec, features: &str) -> Result<(), CliError> { + let mut seen = BTreeSet::new(); + for raw in features.split(',') { + let setting = raw.trim(); + if setting.is_empty() { + return Err(CliError::usage(format!( + "invalid empty target feature in '{}' for target '{}'", + features, spec.triple + ))); + } + let name = setting + .strip_prefix('+') + .or_else(|| setting.strip_prefix('-')) + .ok_or_else(|| { + CliError::usage(format!( + "invalid target feature '{}'; use '+feature' to enable or '-feature' to disable it", + setting + )) + })?; + if name.is_empty() || !spec.features.contains(&name) { + return Err(CliError::usage(format!( + "unsupported feature '{}' for target '{}'; supported features: {}", + name, + spec.triple, + spec.features.join(", ") + ))); + } + if !seen.insert(name) { + return Err(CliError::usage(format!( + "target feature '{}' is specified more than once for target '{}'", + name, spec.triple + ))); + } + } + Ok(()) } -fn ensure_supported_target(target: &str) -> Result<(), CliError> { - if target == host_target_triple() || supported_targets().contains(&target) { +fn validate_target_feature_abi_compatibility( + spec: &TargetSpec, + features: Option<&str>, + abi: Option<&str>, +) -> Result<(), CliError> { + if spec.arch != "riscv64" { return Ok(()); } - Err(CliError::usage(format!( - "unsupported target '{}': see `wavec print target-list`", - target - ))) -} + let disabled = |name: &str| { + features.is_some_and(|values| { + values + .split(',') + .map(str::trim) + .any(|value| value.strip_prefix('-') == Some(name)) + }) + }; -fn cpu_list_for_target(target: &str) -> Vec<&'static str> { - if target.starts_with("x86_64-") { - vec!["generic", "x86-64", "x86-64-v2", "x86-64-v3"] - } else if target.starts_with("aarch64-") { - vec!["generic", "cortex-a53", "cortex-a72", "apple-m1"] - } else if target.starts_with("riscv64-") { - vec!["generic-rv64", "rocket", "sifive-u74"] - } else { - vec!["generic"] + if features.is_some_and(|values| { + values + .split(',') + .map(str::trim) + .any(|value| value == "+d" || value == "d") + }) && disabled("f") + { + return Err(CliError::usage(format!( + "invalid feature combination for target '{}': feature 'd' requires feature 'f'", + spec.triple + ))); } -} -fn target_features_for_target(target: &str) -> Vec<&'static str> { - if target.starts_with("x86_64-") { - vec!["sse2", "sse4.1", "avx", "avx2"] - } else if target.starts_with("aarch64-") { - vec!["neon", "fp", "crypto"] - } else if target.starts_with("riscv64-") { - vec!["m", "a", "f", "d", "c"] - } else { - vec![] + match abi { + Some("lp64d") if disabled("f") || disabled("d") => Err(CliError::usage(format!( + "ABI 'lp64d' for target '{}' requires features 'f' and 'd'", + spec.triple + ))), + Some("lp64f") if disabled("f") => Err(CliError::usage(format!( + "ABI 'lp64f' for target '{}' requires feature 'f'", + spec.triple + ))), + _ => Ok(()), } } diff --git a/src/runner.rs b/src/runner.rs index 000ee769..6ba18e63 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -19,6 +19,7 @@ use ::parser::verification::{validate_program_detailed, SemanticSpanHint, Semant use ::parser::*; use lexer::Lexer; use llvm::backend::*; +use llvm::codegen::target::target_spec_for_triple; use llvm::codegen::*; use std::collections::HashSet; use std::path::{Path, PathBuf}; @@ -26,56 +27,16 @@ use std::process::Stdio; use std::sync::{Arc, Mutex}; use std::{fs, process, process::Command}; -fn target_os_from_triple(triple: &str) -> Option { - let lower = triple.to_ascii_lowercase(); - if lower.contains("windows") { - Some("windows".to_string()) - } else if lower.contains("darwin") || lower.contains("apple") { - Some("macos".to_string()) - } else if lower.contains("linux") { - Some("linux".to_string()) - } else if lower.contains("none") { - Some("none".to_string()) - } else { - None - } -} - -fn target_env_from_triple(triple: &str) -> Option { - let lower = triple.to_ascii_lowercase(); - if lower.contains("windows") && lower.contains("gnu") { - Some("gnu".to_string()) - } else if lower.contains("windows") && lower.contains("msvc") { - Some("msvc".to_string()) - } else if lower.contains("linux") && lower.contains("musl") { - Some("musl".to_string()) - } else if lower.contains("linux") && lower.contains("gnu") { - Some("gnu".to_string()) - } else if lower.contains("none") { - Some("none".to_string()) - } else { - None - } -} - -fn target_abi_from_triple(triple: &str) -> Option { - let parts = triple.split('-').collect::>(); - if parts.len() >= 5 { - parts.last().map(|s| (*s).to_string()) - } else { - None - } -} - fn target_condition_context_for_llvm(llvm: Option<&LlvmFlags>) -> TargetConditionContext { let mut target = TargetConditionContext::default(); if let Some(opts) = llvm { if let Some(triple) = opts.target.as_deref() { - target.arch = triple.split('-').next().map(canonical_target_arch); - target.os = target_os_from_triple(triple); - target.env = target_env_from_triple(triple); - target.abi = target_abi_from_triple(triple); + if let Some(spec) = target_spec_for_triple(triple) { + target.arch = Some(spec.arch.to_string()); + target.os = Some(spec.os.to_string()); + target.env = Some(spec.env.to_string()); + } } if opts.abi.is_some() { target.abi = opts.abi.clone(); @@ -84,15 +45,6 @@ fn target_condition_context_for_llvm(llvm: Option<&LlvmFlags>) -> TargetConditio target } - -fn canonical_target_arch(arch: &str) -> String { - match arch.to_ascii_lowercase().as_str() { - "amd64" => "x86_64".to_string(), - "arm64" => "aarch64".to_string(), - other => other.to_string(), - } -} - fn parse_wave_tokens_or_exit( file_path: &Path, source: &str, diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 647b47ce..37fa8bf9 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -10,7 +10,7 @@ // SPDX-License-Identifier: MPL-2.0 // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -1246,28 +1246,350 @@ fun main() -> i32 { ]); let riscv_ir = fs::read_to_string(riscv_dir.join("select.ll")).unwrap(); assert!(riscv_ir.contains("ret i32 255"), "{}", riscv_ir); - - let abi_src = write_wave( - &dir, - "abi_from_triple.wave", - r#" -#[target(arch="x86_64", os="none", env="none", abi="waveabi")] -fun selected() -> i32 { - return 7; } -fun main() -> i32 { - return selected(); -} -"#, - ); +#[test] +fn target_configuration_is_rejected_before_frontend_or_backend_work() { + let dir = temp_case_dir("target-validation"); + let source = write_wave(&dir, "main.wave", "fun main() -> i32 { return 0; }\n"); + + let run_failure = |options: &[&str], expected: &str| { + let mut args = vec![ + OsString::from("--error-format=json"), + OsString::from("build"), + source.as_os_str().to_os_string(), + ]; + args.extend(options.iter().map(OsString::from)); + let output = run_wavec_raw(args); + assert_eq!( + output.status.code(), + Some(2), + "target validation should fail with usage exit code 2\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.trim().is_empty(), + "JSON usage errors must not write to stdout: {stdout}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("\"kind\":\"usage\""), "{}", stderr); + assert!(stderr.contains(expected), "{}", stderr); + assert!( + !stderr.contains("E9001") + && !stderr.contains("compiler internal error") + && !stderr.contains("panic location"), + "target validation leaked a backend failure: {}", + stderr + ); + }; + + for mode in [ + &["--target", "mips64-unknown-linux-gnu", "--emit=check"][..], + &["--target", "mips64-unknown-linux-gnu", "--emit=obj"][..], + &[ + "--target", + "mips64-unknown-linux-gnu", + "--emit=obj", + "--dry-run", + ][..], + ] { + run_failure(mode, "unsupported target 'mips64-unknown-linux-gnu'"); + } + + for (options, expected) in [ + ( + &["--target", "x86_64-garbage-linux-gnu", "--emit=check"][..], + "unsupported target 'x86_64-garbage-linux-gnu'", + ), + ( + &["--target", "riscv64-unknown-linux-gnu", "--emit=check"][..], + "unsupported target 'riscv64-unknown-linux-gnu'", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--cpu", + "sifive-u74", + "--emit=check", + ][..], + "unsupported CPU 'sifive-u74'", + ), + ( + &[ + "--target", + "riscv64-unknown-none-elf", + "--cpu", + "rocket", + "--emit=obj", + ][..], + "unsupported CPU 'rocket'", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--features", + "+m", + "--emit=check", + ][..], + "unsupported feature 'm'", + ), + ( + &[ + "--target", + "aarch64-unknown-linux-gnu", + "--features", + "+fp", + "--emit=obj", + ][..], + "unsupported feature 'fp'", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--abi", + "lp64d", + "--emit=check", + ][..], + "unsupported ABI 'lp64d'", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--features", + "sse2", + "--emit=check", + ][..], + "invalid target feature 'sse2'", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--features", + "+sse2,-sse2", + "--emit=check", + ][..], + "target feature 'sse2' is specified more than once", + ), + ( + &[ + "--target", + "x86_64-unknown-linux-gnu", + "--features", + "+sse2,,+avx", + "--emit=check", + ][..], + "invalid empty target feature", + ), + ( + &[ + "--target", + "riscv64-unknown-none-elf", + "--features", + "+m,+a,-f,+d,+c", + "--abi", + "lp64d", + "--emit=check", + ][..], + "feature 'd' requires feature 'f'", + ), + ( + &[ + "--target", + "riscv64-unknown-none-elf", + "--features", + "+f,-d", + "--abi", + "lp64d", + "--emit=check", + ][..], + "ABI 'lp64d' for target 'riscv64-unknown-none-elf' requires features 'f' and 'd'", + ), + ( + &[ + "--target", + "riscv64-unknown-none-elf", + "--features", + "-f", + "--abi", + "lp64f", + "--emit=check", + ][..], + "ABI 'lp64f' for target 'riscv64-unknown-none-elf' requires feature 'f'", + ), + ] { + run_failure(options, expected); + } + + let x86_out = dir.join("valid-x86"); run_wavec([ OsStr::new("build"), - abi_src.as_os_str(), + source.as_os_str(), OsStr::new("--target"), - OsStr::new("x86_64-unknown-none-elf-waveabi"), - OsStr::new("--emit=check"), + OsStr::new("x86_64-unknown-linux-gnu"), + OsStr::new("--cpu"), + OsStr::new("x86-64-v2"), + OsStr::new("--features"), + OsStr::new("+sse2,-avx"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + x86_out.as_os_str(), + ]); + assert!(x86_out.join("main.o").is_file()); + + let riscv_out = dir.join("valid-riscv"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-none-elf"), + OsStr::new("--cpu"), + OsStr::new("sifive-u74"), + OsStr::new("--features"), + OsStr::new("+m,+a,+f,+d,+c"), + OsStr::new("--abi"), + OsStr::new("lp64d"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + riscv_out.as_os_str(), + ]); + assert!(riscv_out.join("main.o").is_file()); + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("print"), + OsStr::new("target-spec"), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-none-elf"), + OsStr::new("--format=json"), ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!(stdout.contains("\"hosted\":false"), "{}", stdout); + assert!(stdout.contains("\"freestanding\":true"), "{}", stdout); +} + +#[test] +fn advertised_target_options_reach_object_codegen_without_backend_diagnostics() { + let dir = temp_case_dir("target-option-matrix"); + let source = write_wave(&dir, "matrix.wave", "fun main() -> i32 { return 0; }\n"); + + let build_object = |target: &str, label: &str, options: &[OsString]| -> PathBuf { + let out_dir = dir.join(label); + let mut args = vec![ + OsString::from("build"), + source.as_os_str().to_os_string(), + OsString::from("--target"), + OsString::from(target), + ]; + args.extend_from_slice(options); + args.extend([ + OsString::from("--emit=obj"), + OsString::from("--out-dir"), + out_dir.as_os_str().to_os_string(), + ]); + + let (stdout, stderr) = run_wavec_capture(args); + assert!(stdout.trim().is_empty(), "{}", stdout); + assert!( + stderr.trim().is_empty(), + "advertised target option emitted a backend diagnostic for {target}: {stderr}" + ); + let object = out_dir.join("matrix.o"); + assert!(object.is_file(), "{target} {label}"); + object + }; + + let (targets, stderr) = run_wavec_capture([OsStr::new("print"), OsStr::new("target-list")]); + assert!(stderr.trim().is_empty(), "{}", stderr); + + for target in targets.lines().filter(|line| !line.is_empty()) { + let target_label = target.replace('-', "_"); + let object = build_object(target, &format!("{target_label}_default"), &[]); + + let (target_spec, stderr) = run_wavec_capture([ + OsStr::new("print"), + OsStr::new("target-spec"), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--format=json"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + let object = fs::read(object).unwrap(); + if target_spec.contains("\"object_format\":\"elf\"") { + assert!(object.starts_with(b"\x7fELF"), "{target}: {target_spec}"); + } else if target_spec.contains("\"object_format\":\"macho\"") { + assert!( + object.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]), + "{target}: {target_spec}" + ); + } else if target_spec.contains("\"object_format\":\"coff\"") { + assert!(object.starts_with(&[0x64, 0x86]), "{target}: {target_spec}"); + } else { + panic!("target spec has an unknown object format: {target_spec}"); + } + + let (cpus, stderr) = run_wavec_capture([ + OsStr::new("print"), + OsStr::new("cpu-list"), + OsStr::new("--target"), + OsStr::new(target), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + for cpu in cpus.lines().filter(|line| !line.is_empty()) { + let cpu_label = cpu.replace('-', "_"); + build_object( + target, + &format!("{target_label}_cpu_{cpu_label}"), + &[OsString::from("--cpu"), OsString::from(cpu)], + ); + } + + let (features, stderr) = run_wavec_capture([ + OsStr::new("print"), + OsStr::new("target-features"), + OsStr::new("--target"), + OsStr::new(target), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + for feature in features.lines().filter(|line| !line.is_empty()) { + let feature_label = feature.replace(['-', '.'], "_"); + for (sign, action) in [("+", "enable"), ("-", "disable")] { + build_object( + target, + &format!("{target_label}_feature_{action}_{feature_label}"), + &[ + OsString::from("--features"), + OsString::from(format!("{sign}{feature}")), + ], + ); + } + } + } + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + { + for (abi, features) in [ + ("lp64", "+m,+a,+c"), + ("lp64f", "+m,+a,+f,+c"), + ("lp64d", "+m,+a,+f,+d,+c"), + ] { + build_object( + "riscv64-unknown-none-elf", + &format!("riscv64_abi_{abi}"), + &[ + OsString::from("--features"), + OsString::from(features), + OsString::from("--abi"), + OsString::from(abi), + ], + ); + } + } } #[test]