From c137a36e36f43121649f166bbe23512bb1c04c9b Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 9 Aug 2026 10:53:14 +0900 Subject: [PATCH] Harden target configuration validation Centralize Wave target support in a feature-gated TargetSpec registry with exact triple matching, architecture metadata, hosted and freestanding classification, object formats, CPUs, features, and ABI overrides. Reject unsupported triples, target CPUs, unsigned or unknown features, duplicate and conflicting feature settings, and invalid RISC-V ABI combinations before check, build, object generation, or dry-run can reach LLVM. Keep usage failures machine-readable with exit code 2 and without leaking backend panics or internal compiler errors. Replace target substring heuristics in the CLI, runner, linker selection, target attributes, and LLVM backend with the shared registry and CodegenTarget mappings. Align advertised options with LLVM 21 by using rocket-rv64, permitting the generic RV64 CPU, and exposing fp-armv8 for AArch64. Add regression coverage for malformed targets, CPU and feature errors, LP64F and LP64D requirements, exact target lookup, JSON diagnostics, target metadata, every advertised target/CPU/feature combination, feature-specific LLVM builds, warning-free object generation, and ELF, Mach-O, and COFF object-format consistency. --- llvm/src/backend.rs | 27 +-- llvm/src/codegen/target.rs | 292 ++++++++++++++++++++++++---- src/cli.rs | 340 ++++++++++++++++++--------------- src/runner.rs | 60 +----- tests/codegen_regressions.rs | 356 +++++++++++++++++++++++++++++++++-- 5 files changed, 799 insertions(+), 276 deletions(-) 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]