diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4a12653b..f245205a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,6 +19,7 @@ concurrency: jobs: build-linux-amd64: + name: Build Linux amd64 runs-on: ubuntu-latest timeout-minutes: 45 @@ -69,7 +70,86 @@ jobs: - name: Run Wave end-to-end tests run: python3 tools/run_tests.py + build-linux-riscv64: + name: Build Linux riscv64 + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.89.0 + + - name: Install LLVM 21 and RISC-V runtime tools + run: | + set -euo pipefail + + sudo apt-get update + sudo apt-get install -y wget software-properties-common + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 + sudo apt-get install -y \ + binutils-riscv64-linux-gnu \ + gcc-riscv64-linux-gnu \ + libc6-dev-riscv64-cross \ + lld-21 \ + qemu-user + + echo "LLVM_SYS_211_PREFIX=/usr/lib/llvm-21" >> "$GITHUB_ENV" + echo "LLVM_CONFIG_PATH=/usr/lib/llvm-21/bin/llvm-config" >> "$GITHUB_ENV" + echo "/usr/lib/llvm-21/bin" >> "$GITHUB_PATH" + + - name: Verify RISC-V toolchain + run: | + set -euo pipefail + + llvm-config --version + ld.lld --version + riscv64-linux-gnu-gcc --version + riscv64-linux-gnu-readelf --version + qemu-riscv64 --version + test -f /usr/riscv64-linux-gnu/lib/crt1.o + test -f /usr/riscv64-linux-gnu/lib/libc.so + test -f /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 + + - name: Run RISC-V contract tests + run: >- + cargo test --locked --test codegen_regressions riscv64_ --verbose + + - name: Build release compiler with RISC-V backend + run: cargo build --locked --release --verbose + + - name: Link and run Linux riscv64 binary + run: | + set -euo pipefail + + riscv_sysroot=/usr/riscv64-linux-gnu + output_dir="$RUNNER_TEMP/wave-linux-riscv64" + binary="$output_dir/test2" + mkdir -p "$output_dir" + + target/release/wavec build test/test2.wave \ + --target riscv64-unknown-linux-gnu \ + --sysroot "$riscv_sysroot" \ + --out-dir "$output_dir" + + test -x "$binary" + riscv64-linux-gnu-readelf -h "$binary" | tee "$RUNNER_TEMP/riscv64-elf-header.txt" + riscv64-linux-gnu-readelf -l "$binary" | tee "$RUNNER_TEMP/riscv64-program-headers.txt" + + grep -Eq 'Machine:[[:space:]]+RISC-V' "$RUNNER_TEMP/riscv64-elf-header.txt" + grep -Eq 'Flags:[[:space:]]+0x5.*double-float ABI' "$RUNNER_TEMP/riscv64-elf-header.txt" + grep -Fq '/lib/ld-linux-riscv64-lp64d.so.1' "$RUNNER_TEMP/riscv64-program-headers.txt" + + runtime_output="$(timeout --signal=TERM --kill-after=5s 30s \ + qemu-riscv64 -L "$riscv_sysroot" "$binary")" + printf '%s\n' "$runtime_output" + test "$runtime_output" = 'Hello World' + build-macos-arm64: + name: Build macOS arm64 runs-on: macos-latest timeout-minutes: 45 @@ -130,6 +210,7 @@ jobs: run: python3 tools/run_tests.py build-windows-amd64: + name: Build Windows amd64 runs-on: windows-latest timeout-minutes: 60 diff --git a/front/parser/src/arch/aarch64.rs b/front/parser/src/arch/aarch64.rs new file mode 100644 index 00000000..ee51fae3 --- /dev/null +++ b/front/parser/src/arch/aarch64.rs @@ -0,0 +1,17 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +pub(super) const NAME: &str = "aarch64"; + +pub(super) fn recognizes(value: &str) -> bool { + matches!(value, "aarch64" | "arm64") +} diff --git a/front/parser/src/arch/mod.rs b/front/parser/src/arch/mod.rs new file mode 100644 index 00000000..efd4b641 --- /dev/null +++ b/front/parser/src/arch/mod.rs @@ -0,0 +1,70 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +mod aarch64; +mod riscv64; +mod x86_64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Architecture { + X86_64, + Aarch64, + Riscv64, +} + +impl Architecture { + pub const fn name(self) -> &'static str { + match self { + Self::X86_64 => x86_64::NAME, + Self::Aarch64 => aarch64::NAME, + Self::Riscv64 => riscv64::NAME, + } + } + + pub fn from_name(value: &str) -> Option { + let value = value.trim().to_ascii_lowercase(); + if x86_64::recognizes(&value) { + Some(Self::X86_64) + } else if aarch64::recognizes(&value) { + Some(Self::Aarch64) + } else if riscv64::recognizes(&value) { + Some(Self::Riscv64) + } else { + None + } + } +} + +pub fn canonical_name(value: &str) -> String { + Architecture::from_name(value) + .map(|arch| arch.name().to_string()) + .unwrap_or_else(|| value.trim().to_ascii_lowercase()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn architecture_aliases_have_stable_canonical_names() { + for (input, expected) in [ + ("x86_64", "x86_64"), + ("AMD64", "x86_64"), + ("aarch64", "aarch64"), + ("arm64", "aarch64"), + ("riscv64", "riscv64"), + ("unknown-arch", "unknown-arch"), + ] { + assert_eq!(canonical_name(input), expected); + } + } +} diff --git a/front/parser/src/arch/riscv64.rs b/front/parser/src/arch/riscv64.rs new file mode 100644 index 00000000..7e5ca26c --- /dev/null +++ b/front/parser/src/arch/riscv64.rs @@ -0,0 +1,17 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +pub(super) const NAME: &str = "riscv64"; + +pub(super) fn recognizes(value: &str) -> bool { + value == NAME +} diff --git a/front/parser/src/arch/x86_64.rs b/front/parser/src/arch/x86_64.rs new file mode 100644 index 00000000..c0c9d75d --- /dev/null +++ b/front/parser/src/arch/x86_64.rs @@ -0,0 +1,17 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +pub(super) const NAME: &str = "x86_64"; + +pub(super) fn recognizes(value: &str) -> bool { + matches!(value, "x86_64" | "amd64") +} diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index 508f7eda..50eb5dc9 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.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::arch; use crate::ast::ASTNode; use crate::{parse_syntax_only, ParseError}; use error::error::{WaveError, WaveErrorKind}; @@ -74,11 +75,7 @@ impl<'a> TargetAttrCondition<'a> { fn normalize_target_value(key: &str, value: &str) -> String { let lower = value.trim().to_ascii_lowercase(); match key { - "arch" => match lower.as_str() { - "amd64" => "x86_64".to_string(), - "arm64" => "aarch64".to_string(), - other => other.to_string(), - }, + "arch" => arch::canonical_name(&lower), "os" => match lower.as_str() { "darwin" | "apple" => "macos".to_string(), "win32" | "win64" => "windows".to_string(), diff --git a/front/parser/src/lib.rs b/front/parser/src/lib.rs index 3339faf4..1173c7d4 100644 --- a/front/parser/src/lib.rs +++ b/front/parser/src/lib.rs @@ -33,6 +33,7 @@ macro_rules! println { }}; } +pub mod arch; pub mod ast; pub mod expr; pub mod format; diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index c94e726f..2a7df027 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -21,6 +21,7 @@ pub struct BackendOptions { pub cpu: Option, pub features: Option, pub abi: Option, + pub isa: Option, pub code_model: Option, pub relocation_model: Option, pub sysroot: Option, @@ -175,10 +176,10 @@ fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOpti let spec = target_spec_for_triple(target) .expect("Darwin linker configuration requires a registered target"); cmd.arg("-arch") - .arg(if spec.arch == "aarch64" { + .arg(if spec.architecture.name() == "aarch64" { "arm64" } else { - spec.arch + spec.architecture.name() }) .arg("-platform_version") .arg("macos") @@ -231,7 +232,7 @@ fn elf_lld_emulation(target: &str) -> Option<&'static str> { 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"), + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => Some("elf64lriscv"), _ => None, } } diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index 0e15250e..ea790b13 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -501,7 +501,9 @@ fn classify_param<'ctx>( CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::FreestandingArm64 => classify_param_arm64_darwin(td, t), - CodegenTarget::FreestandingRISCV64 => classify_param_riscv64(td, t), + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { + classify_param_riscv64(td, t) + } } } @@ -519,7 +521,9 @@ fn classify_ret<'ctx>( CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::FreestandingArm64 => classify_ret_arm64_darwin(td, t), - CodegenTarget::FreestandingRISCV64 => classify_ret_riscv64(td, t), + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { + classify_ret_riscv64(td, t) + } } } diff --git a/llvm/src/codegen/arch/aarch64.rs b/llvm/src/codegen/arch/aarch64.rs new file mode 100644 index 00000000..3b6553df --- /dev/null +++ b/llvm/src/codegen/arch/aarch64.rs @@ -0,0 +1,96 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +pub(crate) const CPUS: &[&str] = &["generic", "cortex-a53", "cortex-a72"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +pub(crate) const DARWIN_CPUS: &[&str] = &["generic", "apple-m1"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +pub(crate) const FEATURES: &[&str] = &["neon", "fp-armv8", "crypto"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +pub(crate) const DEFAULT_CPU: &str = "generic"; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +pub(crate) const DEFAULT_FEATURES: &[&str] = &[]; + +pub(crate) fn register_group(token: &str) -> Option { + match token { + "fp" => return Some("x29".to_string()), + "lr" => return Some("x30".to_string()), + "ip0" => return Some("x16".to_string()), + "ip1" => return Some("x17".to_string()), + "sp" => return Some("sp".to_string()), + "xzr" | "wzr" => return Some("xzr".to_string()), + _ => {} + } + let (prefix, num) = token.split_at_checked(1)?; + if !matches!(prefix, "x" | "w") || num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) { + return None; + } + let number = num.parse::().ok()?; + (number <= 30).then(|| format!("x{}", number)) +} + +pub(crate) fn register_width_bits(token: &str) -> Option { + let (prefix, num) = token.split_at_checked(1)?; + if num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) || num.parse::().ok()? > 30 { + return None; + } + match prefix { + "w" => Some(32), + "x" => Some(64), + _ => None, + } +} + +pub(crate) fn operand_register_group(token: &str) -> Option { + let group = register_group(token)?; + (!matches!(group.as_str(), "sp" | "xzr")).then_some(group) +} + +pub(crate) fn default_clobbers() -> Vec { + vec!["~{memory}".to_string(), "~{cc}".to_string()] +} + +pub(crate) fn allocatable_registers() -> Vec { + (0..=30) + .filter(|number| *number != 18) + .map(|number| format!("x{}", number)) + .collect() +} + +pub(crate) fn normalize_special_clobber(token: &str) -> Option { + match token { + "memory" => Some("~{memory}".to_string()), + "cc" | "flags" | "eflags" | "rflags" => Some("~{cc}".to_string()), + _ => None, + } +} + +pub(crate) fn stack_analysis(line: &str) -> super::StackAnalysis { + let code = super::instruction_text(line, false); + if code.is_empty() { + return super::StackAnalysis::default(); + } + let touches_stack = code == "ret" + || code.starts_with("ret ") + || code.starts_with("bl ") + || code.starts_with("blr ") + || code.contains(" sp,") + || code.contains(", sp") + || code.contains("[sp"); + super::StackAnalysis { + touches_stack, + unknown_stack_write: touches_stack && code.contains(" sp,"), + nonreturning_branch: super::mnemonic(&code) == "br", + ..Default::default() + } +} diff --git a/llvm/src/codegen/arch/mod.rs b/llvm/src/codegen/arch/mod.rs new file mode 100644 index 00000000..fc0c2a58 --- /dev/null +++ b/llvm/src/codegen/arch/mod.rs @@ -0,0 +1,211 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +pub(crate) mod aarch64; +pub(crate) mod riscv64; +pub(crate) mod x86_64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Architecture { + X86_64, + Aarch64, + Riscv64, +} + +impl Architecture { + pub const fn name(self) -> &'static str { + match self { + Self::X86_64 => "x86_64", + Self::Aarch64 => "aarch64", + Self::Riscv64 => "riscv64", + } + } +} + +pub(crate) fn register_group(architecture: Architecture, token: &str) -> Option { + match architecture { + Architecture::X86_64 => x86_64::register_group(token), + Architecture::Aarch64 => aarch64::register_group(token), + Architecture::Riscv64 => riscv64::register_group(token), + } +} + +pub(crate) fn operand_register_group(architecture: Architecture, token: &str) -> Option { + match architecture { + Architecture::X86_64 => x86_64::operand_register_group(token), + Architecture::Aarch64 => aarch64::operand_register_group(token), + Architecture::Riscv64 => riscv64::operand_register_group(token), + } +} + +pub(crate) fn register_width_bits(architecture: Architecture, token: &str) -> Option { + match architecture { + Architecture::X86_64 => x86_64::register_width_bits(token), + Architecture::Aarch64 => aarch64::register_width_bits(token), + Architecture::Riscv64 => riscv64::register_width_bits(token), + } +} + +pub(crate) const fn inline_asm_dialect(architecture: Architecture) -> inkwell::InlineAsmDialect { + match architecture { + Architecture::X86_64 => inkwell::InlineAsmDialect::Intel, + Architecture::Aarch64 | Architecture::Riscv64 => inkwell::InlineAsmDialect::ATT, + } +} + +pub(crate) fn default_clobbers(architecture: Architecture) -> Vec { + match architecture { + Architecture::X86_64 => x86_64::default_clobbers(), + Architecture::Aarch64 => aarch64::default_clobbers(), + Architecture::Riscv64 => riscv64::default_clobbers(), + } +} + +pub(crate) fn allocatable_registers(architecture: Architecture) -> Vec { + match architecture { + Architecture::X86_64 => x86_64::allocatable_registers(), + Architecture::Aarch64 => aarch64::allocatable_registers(), + Architecture::Riscv64 => riscv64::allocatable_registers(), + } +} + +pub(crate) fn normalize_special_clobber(architecture: Architecture, token: &str) -> Option { + match architecture { + Architecture::X86_64 => x86_64::normalize_special_clobber(token), + Architecture::Aarch64 => aarch64::normalize_special_clobber(token), + Architecture::Riscv64 => riscv64::normalize_special_clobber(token), + } +} + +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct StackAnalysis { + pub touches_stack: bool, + pub unknown_stack_write: bool, + pub unbalanced_delta: i64, + pub nonreturning_branch: bool, +} + +pub(crate) fn instruction_text(line: &str, hash_is_comment: bool) -> String { + let without_slash_comment = line.split_once("//").map(|(code, _)| code).unwrap_or(line); + let line = if hash_is_comment { + without_slash_comment + .split_once('#') + .map(|(code, _)| code) + .unwrap_or(without_slash_comment) + } else { + without_slash_comment + }; + let mut code = line.trim().to_ascii_lowercase(); + + while let Some((label, rest)) = code.split_once(':') { + let label = label.trim(); + if label.is_empty() + || !label + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.') + { + break; + } + code = rest.trim().to_string(); + } + code +} + +pub(crate) fn mnemonic(code: &str) -> &str { + code.split(|c: char| c.is_ascii_whitespace() || c == ';') + .next() + .unwrap_or("") +} + +pub(crate) fn stack_analysis(architecture: Architecture, line: &str) -> StackAnalysis { + match architecture { + Architecture::X86_64 => x86_64::stack_analysis(line), + Architecture::Aarch64 => aarch64::stack_analysis(line), + Architecture::Riscv64 => riscv64::stack_analysis(line), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn register_aliases_and_widths_are_architecture_local() { + assert_eq!( + register_group(Architecture::X86_64, "%eax").as_deref(), + None + ); + assert_eq!( + register_group(Architecture::X86_64, "eax").as_deref(), + Some("rax") + ); + assert_eq!(register_width_bits(Architecture::X86_64, "r8d"), Some(32)); + + assert_eq!( + register_group(Architecture::Aarch64, "fp").as_deref(), + Some("x29") + ); + assert_eq!(register_width_bits(Architecture::Aarch64, "w30"), Some(32)); + + assert_eq!( + register_group(Architecture::Riscv64, "a7").as_deref(), + Some("x17") + ); + assert_eq!( + register_group(Architecture::Riscv64, "fp").as_deref(), + Some("x8") + ); + assert_eq!(register_width_bits(Architecture::Riscv64, "x31"), Some(64)); + assert_eq!( + operand_register_group(Architecture::Riscv64, "fa0").as_deref(), + Some("f10") + ); + for reserved in ["zero", "x0", "sp", "x2", "gp", "x3", "tp", "x4"] { + assert_eq!( + operand_register_group(Architecture::Riscv64, reserved), + None, + "{} must not be accepted as a value operand", + reserved + ); + } + } + + #[test] + fn stack_contract_analysis_dispatches_by_architecture() { + let x86 = stack_analysis(Architecture::X86_64, "sub rsp, 16"); + assert!(x86.touches_stack); + assert_eq!(x86.unbalanced_delta, -16); + + let arm = stack_analysis(Architecture::Aarch64, "sub sp, sp, #16"); + assert!(arm.touches_stack); + assert!(arm.unknown_stack_write); + + let riscv = stack_analysis(Architecture::Riscv64, "addi sp, sp, -16"); + assert!(riscv.touches_stack); + assert!(riscv.unknown_stack_write); + assert!(stack_analysis(Architecture::Riscv64, "jr a0").nonreturning_branch); + assert!(stack_analysis(Architecture::Riscv64, "jalr x0, 0(a0)").nonreturning_branch); + assert!(stack_analysis(Architecture::Riscv64, "jalr zero, 0(a0)").nonreturning_branch); + } + + #[test] + fn architecture_comment_rules_preserve_aarch64_immediates() { + assert_eq!( + instruction_text("add x0, x0, #1 // note", false), + "add x0, x0, #1" + ); + assert_eq!( + instruction_text("addi a0, a0, 1 # note", true), + "addi a0, a0, 1" + ); + } +} diff --git a/llvm/src/codegen/arch/riscv64.rs b/llvm/src/codegen/arch/riscv64.rs new file mode 100644 index 00000000..4b50da0a --- /dev/null +++ b/llvm/src/codegen/arch/riscv64.rs @@ -0,0 +1,213 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const CPUS: &[&str] = &["generic", "generic-rv64", "rocket-rv64", "sifive-u74"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const FEATURES: &[&str] = &["m", "a", "f", "d", "c", "zicsr", "zifencei"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const ABIS: &[&str] = &["lp64", "lp64f", "lp64d"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const DEFAULT_CPU: &str = "generic-rv64"; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const LINUX_DEFAULT_FEATURES: &[&str] = &["m", "a", "f", "d", "c", "zicsr", "zifencei"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const FREESTANDING_DEFAULT_FEATURES: &[&str] = &["m", "a", "c"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const LINUX_DEFAULT_ABI: &str = "lp64d"; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +pub(crate) const FREESTANDING_DEFAULT_ABI: &str = "lp64"; + +pub(crate) fn isa_name( + m: bool, + a: bool, + f: bool, + d: bool, + c: bool, + zicsr: bool, + zifencei: bool, +) -> String { + if m && a && f && d && c && zicsr && zifencei { + return "rv64gc".to_string(); + } + + let mut isa = String::from("rv64i"); + for (enabled, extension) in [(m, 'm'), (a, 'a'), (f, 'f'), (d, 'd'), (c, 'c')] { + if enabled { + isa.push(extension); + } + } + if zicsr { + isa.push_str("_zicsr"); + } + if zifencei { + isa.push_str("_zifencei"); + } + isa +} + +pub(crate) fn register_group(token: &str) -> Option { + if let Some(number) = floating_register_number(token) { + return Some(format!("f{}", number)); + } + + let number = match token { + "zero" => 0, + "ra" => 1, + "sp" => 2, + "gp" => 3, + "tp" => 4, + "t0" => 5, + "t1" => 6, + "t2" => 7, + "s0" | "fp" => 8, + "s1" => 9, + "a0" => 10, + "a1" => 11, + "a2" => 12, + "a3" => 13, + "a4" => 14, + "a5" => 15, + "a6" => 16, + "a7" => 17, + "s2" => 18, + "s3" => 19, + "s4" => 20, + "s5" => 21, + "s6" => 22, + "s7" => 23, + "s8" => 24, + "s9" => 25, + "s10" => 26, + "s11" => 27, + "t3" => 28, + "t4" => 29, + "t5" => 30, + "t6" => 31, + _ => { + let raw = token.strip_prefix('x')?; + if raw.is_empty() || !raw.chars().all(|c| c.is_ascii_digit()) { + return None; + } + raw.parse::().ok()? + } + }; + (number <= 31).then(|| format!("x{}", number)) +} + +fn floating_register_number(token: &str) -> Option { + let number = match token { + "ft0" => 0, + "ft1" => 1, + "ft2" => 2, + "ft3" => 3, + "ft4" => 4, + "ft5" => 5, + "ft6" => 6, + "ft7" => 7, + "fs0" => 8, + "fs1" => 9, + "fa0" => 10, + "fa1" => 11, + "fa2" => 12, + "fa3" => 13, + "fa4" => 14, + "fa5" => 15, + "fa6" => 16, + "fa7" => 17, + "fs2" => 18, + "fs3" => 19, + "fs4" => 20, + "fs5" => 21, + "fs6" => 22, + "fs7" => 23, + "fs8" => 24, + "fs9" => 25, + "fs10" => 26, + "fs11" => 27, + "ft8" => 28, + "ft9" => 29, + "ft10" => 30, + "ft11" => 31, + _ => { + let raw = token.strip_prefix('f')?; + if raw.is_empty() || !raw.chars().all(|c| c.is_ascii_digit()) { + return None; + } + raw.parse::().ok()? + } + }; + (number <= 31).then_some(number) +} + +pub(crate) fn operand_register_group(token: &str) -> Option { + let group = register_group(token)?; + if group.starts_with('f') { + return Some(group); + } + (!matches!(group.as_str(), "x0" | "x2" | "x3" | "x4")).then_some(group) +} + +pub(crate) fn register_width_bits(token: &str) -> Option { + register_group(token).map(|_| 64) +} + +pub(crate) fn default_clobbers() -> Vec { + vec!["~{memory}".to_string()] +} + +pub(crate) fn allocatable_registers() -> Vec { + let mut registers = [ + 1u32, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, + ] + .into_iter() + .map(|number| format!("x{}", number)) + .collect::>(); + registers.extend((0..=31).map(|number| format!("f{}", number))); + registers +} + +pub(crate) fn normalize_special_clobber(token: &str) -> Option { + (token == "memory").then(|| "~{memory}".to_string()) +} + +pub(crate) fn stack_analysis(line: &str) -> super::StackAnalysis { + let code = super::instruction_text(line, true); + if code.is_empty() { + return super::StackAnalysis::default(); + } + let touches_stack = code == "ret" + || code.starts_with("call ") + || code.starts_with("jal ") + || code.starts_with("jalr ") + || code.contains(" sp,") + || code.contains(", sp") + || code.contains("(sp)"); + let mnemonic = super::mnemonic(&code); + let nonreturning_branch = match mnemonic { + "jr" | "tail" | "ret" => true, + "jalr" => code + .split(|ch: char| ch.is_ascii_whitespace() || ch == ',') + .filter(|part| !part.is_empty()) + .nth(1) + .is_some_and(|rd| matches!(rd, "x0" | "zero")), + _ => false, + }; + super::StackAnalysis { + touches_stack, + unknown_stack_write: touches_stack + && (code.contains(" sp,") || code.starts_with("addi sp")), + nonreturning_branch, + ..Default::default() + } +} diff --git a/llvm/src/codegen/arch/x86_64.rs b/llvm/src/codegen/arch/x86_64.rs new file mode 100644 index 00000000..f01e8b20 --- /dev/null +++ b/llvm/src/codegen/arch/x86_64.rs @@ -0,0 +1,191 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +pub(crate) const CPUS: &[&str] = &["generic", "x86-64", "x86-64-v2", "x86-64-v3"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +pub(crate) const FEATURES: &[&str] = &["sse2", "sse4.1", "avx", "avx2"]; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +pub(crate) const DEFAULT_CPU: &str = "generic"; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +pub(crate) const DEFAULT_FEATURES: &[&str] = &[]; + +pub(crate) fn register_group(token: &str) -> Option { + let group = match token { + "al" | "ah" | "ax" | "eax" | "rax" => "rax", + "bl" | "bh" | "bx" | "ebx" | "rbx" => "rbx", + "cl" | "ch" | "cx" | "ecx" | "rcx" => "rcx", + "dl" | "dh" | "dx" | "edx" | "rdx" => "rdx", + "sil" | "si" | "esi" | "rsi" => "rsi", + "dil" | "di" | "edi" | "rdi" => "rdi", + "bpl" | "bp" | "ebp" | "rbp" => "rbp", + "spl" | "sp" | "esp" | "rsp" => "rsp", + "r8b" | "r8w" | "r8d" | "r8" => "r8", + "r9b" | "r9w" | "r9d" | "r9" => "r9", + "r10b" | "r10w" | "r10d" | "r10" => "r10", + "r11b" | "r11w" | "r11d" | "r11" => "r11", + "r12b" | "r12w" | "r12d" | "r12" => "r12", + "r13b" | "r13w" | "r13d" | "r13" => "r13", + "r14b" | "r14w" | "r14d" | "r14" => "r14", + "r15b" | "r15w" | "r15d" | "r15" => "r15", + _ => return None, + }; + Some(group.to_string()) +} + +pub(crate) fn register_width_bits(token: &str) -> Option { + match token { + "al" | "bl" | "cl" | "dl" | "sil" | "dil" | "r8b" | "r9b" | "r10b" | "r11b" | "r12b" + | "r13b" | "r14b" | "r15b" => Some(8), + "ax" | "bx" | "cx" | "dx" | "si" | "di" | "r8w" | "r9w" | "r10w" | "r11w" | "r12w" + | "r13w" | "r14w" | "r15w" => Some(16), + "eax" | "ebx" | "ecx" | "edx" | "esi" | "edi" | "r8d" | "r9d" | "r10d" | "r11d" + | "r12d" | "r13d" | "r14d" | "r15d" => Some(32), + "rax" | "rbx" | "rcx" | "rdx" | "rsi" | "rdi" | "rbp" | "rsp" | "r8" | "r9" | "r10" + | "r11" | "r12" | "r13" | "r14" | "r15" => Some(64), + _ => None, + } +} + +pub(crate) fn operand_register_group(token: &str) -> Option { + let group = register_group(token)?; + (group != "rsp").then_some(group) +} + +pub(crate) fn default_clobbers() -> Vec { + ["~{memory}", "~{dirflag}", "~{fpsr}", "~{flags}"] + .into_iter() + .map(String::from) + .collect() +} + +pub(crate) fn allocatable_registers() -> Vec { + [ + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13", "r14", + "r15", + ] + .into_iter() + .map(String::from) + .collect() +} + +pub(crate) fn normalize_special_clobber(token: &str) -> Option { + match token { + "memory" => Some("~{memory}".to_string()), + "cc" | "flags" | "eflags" | "rflags" => Some("~{flags}".to_string()), + "dirflag" => Some("~{dirflag}".to_string()), + "fpsr" => Some("~{fpsr}".to_string()), + _ => None, + } +} + +fn parse_immediate(raw: &str) -> Option { + let mut value = raw + .trim() + .trim_start_matches('$') + .trim_start_matches('#') + .trim_end_matches(','); + let negative = value.starts_with('-'); + if negative { + value = value.trim_start_matches('-'); + } + if let Some(hex) = value.strip_prefix("0x") { + let parsed = i64::from_str_radix(hex, 16).ok()?; + Some(if negative { -parsed } else { parsed }) + } else { + let parsed = value.parse::().ok()?; + Some(if negative { -parsed } else { parsed }) + } +} + +fn stack_adjustment(code: &str) -> Option { + let mut parts = code + .split(|c: char| c.is_ascii_whitespace() || c == ',') + .filter(|part| !part.is_empty()); + let op = parts.next()?; + let first = parts.next()?; + let second = parts.next()?; + let sp_is_second = matches!(second, "rsp" | "%rsp" | "esp" | "%esp" | "sp" | "%sp"); + let sp_is_first = matches!(first, "rsp" | "%rsp" | "esp" | "%esp" | "sp" | "%sp"); + match op { + "sub" | "subq" | "subl" if sp_is_second => parse_immediate(first).map(|value| -value), + "add" | "addq" | "addl" if sp_is_second => parse_immediate(first), + "sub" | "subq" | "subl" if sp_is_first => parse_immediate(second).map(|value| -value), + "add" | "addq" | "addl" if sp_is_first => parse_immediate(second), + _ => None, + } +} + +fn jump_is_indirect(code: &str) -> bool { + let operand = code + .split(|c: char| c.is_ascii_whitespace() || c == ',') + .filter(|part| !part.is_empty()) + .nth(1); + let Some(operand) = operand else { + return false; + }; + let operand = operand.trim_start_matches('*').trim_start_matches('%'); + operand.starts_with('[') || register_group(operand).is_some() +} + +pub(crate) fn stack_analysis(line: &str) -> super::StackAnalysis { + let code = super::instruction_text(line, true); + if code.is_empty() { + return super::StackAnalysis::default(); + } + let mnemonic = super::mnemonic(&code); + let mut out = super::StackAnalysis::default(); + match mnemonic { + "call" | "callq" => out.touches_stack = true, + "push" | "pushq" => { + out.touches_stack = true; + out.unbalanced_delta = -8; + } + "pop" | "popq" | "ret" | "retq" => { + out.touches_stack = true; + out.unbalanced_delta = 8; + } + "retf" | "retfq" => { + out.touches_stack = true; + out.unbalanced_delta = 16; + } + "iret" | "iretq" | "leave" | "enter" => { + out.touches_stack = true; + out.unknown_stack_write = true; + } + "jmp" | "jmpq" => out.nonreturning_branch = jump_is_indirect(&code), + _ => { + if let Some(delta) = stack_adjustment(&code) { + out.touches_stack = true; + out.unbalanced_delta = delta; + } else { + let writes_sp = ["mov", "movq", "and", "andq", "xor", "lea"] + .iter() + .any(|op| { + code.starts_with(&format!("{} rsp", op)) + || code.starts_with(&format!("{} %rsp", op)) + }); + if writes_sp { + out.touches_stack = true; + out.unknown_stack_write = true; + } else { + out.touches_stack = code.contains("rsp") + || code.contains("esp") + || code.contains("[sp") + || code.contains(" sp,") + || code.contains(", sp"); + } + } + } + } + out +} diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index ea0552b7..b28bde64 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -12,15 +12,17 @@ use inkwell::attributes::{Attribute, AttributeLoc}; use inkwell::context::Context; -use inkwell::module::{Linkage, Module}; +use inkwell::module::{FlagBehavior, Linkage, Module}; use inkwell::passes::PassBuilderOptions; use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; -use inkwell::values::{BasicValue, BasicValueEnum, FunctionValue}; +use inkwell::values::{ + BasicMetadataValueEnum, BasicValue, BasicValueEnum, FunctionValue, PointerValue, ValueKind, +}; use inkwell::OptimizationLevel; use inkwell::targets::{ CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetData, TargetMachine, - TargetTriple, + TargetMachineOptions, TargetTriple, }; use parser::ast::{ ASTNode, EnumNode, ExternFunctionNode, FunctionNode, Mutability, ParameterNode, ProtoImplNode, @@ -36,7 +38,9 @@ use crate::statement::generate_statement_ir; use super::consts::{create_llvm_const_value, ConstEvalError}; use super::types::{wave_type_to_llvm_type, TypeFlavor, VariableInfo}; -use crate::codegen::abi_c::{apply_extern_c_attrs, lower_extern_c, ExternCInfo}; +use crate::codegen::abi_c::{ + apply_extern_c_attrs, lower_extern_c, ExternCInfo, ParamLowering, RetLowering, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CodegenFileKind { @@ -50,6 +54,238 @@ struct GeneratedModule { target_machine: TargetMachine, } +struct ExportCWrapper<'ctx> { + wrapper: FunctionValue<'ctx>, + implementation: FunctionValue<'ctx>, + info: ExternCInfo<'ctx>, + wave_param_types: Vec>, + wave_ret_type: Option>, +} + +fn reinterpret_abi_value<'ctx>( + context: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + td: &TargetData, + value: BasicValueEnum<'ctx>, + target: BasicTypeEnum<'ctx>, + tag: &str, +) -> BasicValueEnum<'ctx> { + if value.get_type() == target { + return value; + } + + let source = value.get_type(); + let source_size = td.get_store_size(&source); + let target_size = td.get_store_size(&target); + if source_size != target_size { + panic!( + "cannot reinterpret C ABI value '{}' from {} bytes to {} bytes", + tag, source_size, target_size + ); + } + + let source_ptr = builder + .build_alloca(source, &format!("{}_source", tag)) + .unwrap(); + builder.build_store(source_ptr, value).unwrap(); + let target_ptr = builder + .build_alloca(target, &format!("{}_target", tag)) + .unwrap(); + let size = context.i64_type().const_int(source_size, false); + builder + .build_memcpy( + target_ptr, + td.get_abi_alignment(&target), + source_ptr, + td.get_abi_alignment(&source), + size, + ) + .unwrap(); + builder + .build_load(target, target_ptr, &format!("{}_load", tag)) + .unwrap() + .as_basic_value_enum() +} + +fn rebuild_split_abi_value<'ctx>( + context: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + td: &TargetData, + parts: &[BasicValueEnum<'ctx>], + target: BasicTypeEnum<'ctx>, + tag: &str, +) -> BasicValueEnum<'ctx> { + let target_ptr = builder + .build_alloca(target, &format!("{}_target", tag)) + .unwrap(); + let mut offset = 0u64; + + for (index, part) in parts.iter().enumerate() { + let part_type = part.get_type(); + let part_size = td.get_store_size(&part_type); + let part_ptr = builder + .build_alloca(part_type, &format!("{}_part_{}", tag, index)) + .unwrap(); + builder.build_store(part_ptr, *part).unwrap(); + let offset_value = context.i64_type().const_int(offset, false); + let destination = unsafe { + builder + .build_gep( + context.i8_type(), + target_ptr, + &[offset_value], + &format!("{}_offset_{}", tag, index), + ) + .unwrap() + }; + builder + .build_memcpy( + destination, + 1, + part_ptr, + td.get_abi_alignment(&part_type), + context.i64_type().const_int(part_size, false), + ) + .unwrap(); + offset += part_size; + } + + if offset > td.get_store_size(&target) { + panic!("split C ABI value '{}' exceeds its Wave aggregate", tag); + } + builder + .build_load(target, target_ptr, &format!("{}_load", tag)) + .unwrap() + .as_basic_value_enum() +} + +fn build_export_c_wrapper<'ctx>( + context: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + td: &TargetData, + export: &ExportCWrapper<'ctx>, +) { + let entry = context.append_basic_block(export.wrapper, "entry"); + builder.position_at_end(entry); + + let mut llvm_index = 0u32; + let sret_ptr: Option> = + if matches!(export.info.ret, RetLowering::SRet { .. }) { + let pointer = export + .wrapper + .get_nth_param(0) + .expect("C ABI sret wrapper requires a hidden pointer") + .into_pointer_value(); + llvm_index += 1; + Some(pointer) + } else { + None + }; + + let mut implementation_args = Vec::>::new(); + for (wave_index, (lowering, wave_type)) in export + .info + .params + .iter() + .zip(export.wave_param_types.iter()) + .enumerate() + { + let value = match lowering { + ParamLowering::Direct(_) => { + let incoming = export + .wrapper + .get_nth_param(llvm_index) + .expect("missing direct C ABI wrapper argument"); + llvm_index += 1; + reinterpret_abi_value( + context, + builder, + td, + incoming, + *wave_type, + &format!("export_arg_{}", wave_index), + ) + } + ParamLowering::ByVal { .. } => { + let pointer = export + .wrapper + .get_nth_param(llvm_index) + .expect("missing byval C ABI wrapper argument") + .into_pointer_value(); + llvm_index += 1; + builder + .build_load(*wave_type, pointer, &format!("export_byval_{}", wave_index)) + .unwrap() + .as_basic_value_enum() + } + ParamLowering::Split(parts) => { + let mut incoming = Vec::with_capacity(parts.len()); + for _ in parts { + incoming.push( + export + .wrapper + .get_nth_param(llvm_index) + .expect("missing split C ABI wrapper argument"), + ); + llvm_index += 1; + } + rebuild_split_abi_value( + context, + builder, + td, + &incoming, + *wave_type, + &format!("export_split_{}", wave_index), + ) + } + }; + implementation_args.push(value.into()); + } + + let call = builder + .build_call( + export.implementation, + &implementation_args, + "export_implementation", + ) + .unwrap(); + + match &export.info.ret { + RetLowering::Void => { + builder.build_return(None).unwrap(); + } + RetLowering::SRet { .. } => { + let value = match call.try_as_basic_value() { + ValueKind::Basic(value) => value, + ValueKind::Instruction(_) => { + panic!("C ABI sret wrapper implementation returned void") + } + }; + builder + .build_store(sret_ptr.expect("missing C ABI sret pointer"), value) + .unwrap(); + builder.build_return(None).unwrap(); + } + RetLowering::Direct(lowered_type) => { + let value = match call.try_as_basic_value() { + ValueKind::Basic(value) => value, + ValueKind::Instruction(_) => { + panic!("C ABI direct wrapper implementation returned void") + } + }; + let wave_type = export + .wave_ret_type + .expect("direct C ABI wrapper requires a Wave return type"); + if value.get_type() != wave_type { + panic!("C ABI wrapper implementation return type changed unexpectedly"); + } + let lowered = + reinterpret_abi_value(context, builder, td, value, *lowered_type, "export_return"); + builder.build_return(Some(&lowered)).unwrap(); + } + } +} + fn is_implicit_i32_main(name: &str, return_type: &Option) -> bool { name == "main" && matches!(return_type, None | Some(WaveType::Void)) } @@ -176,7 +412,21 @@ fn apply_function_codegen_attrs<'ctx>( context: &'ctx Context, function: FunctionValue<'ctx>, disable_red_zone: bool, + cpu: &str, + features: &str, ) { + if !cpu.is_empty() { + function.add_attribute( + AttributeLoc::Function, + context.create_string_attribute("target-cpu", cpu), + ); + } + if !features.is_empty() { + function.add_attribute( + AttributeLoc::Function, + context.create_string_attribute("target-features", features), + ); + } if disable_red_zone { let no_red_zone = Attribute::get_named_enum_kind_id("noredzone"); let attr = context.create_enum_attribute(no_red_zone, 0); @@ -273,15 +523,17 @@ fn build_module( let code_model = code_model_from_backend(backend, abi_target); codegen_trace("create target machine"); + let mut target_options = TargetMachineOptions::new() + .set_cpu(cpu) + .set_features(features) + .set_level(target_opt_level_from_flag(opt_flag)) + .set_reloc_mode(reloc_mode) + .set_code_model(code_model); + if let Some(abi) = backend.abi.as_deref() { + target_options = target_options.set_abi(abi); + } let tm = target - .create_target_machine( - &triple, - cpu, - features, - target_opt_level_from_flag(opt_flag), - reloc_mode, - code_model, - ) + .create_target_machine_from_options(&triple, target_options) .unwrap(); codegen_trace("set target metadata"); @@ -289,6 +541,19 @@ fn build_module( let td_val: TargetData = tm.get_target_data(); module.set_data_layout(&td_val.get_data_layout()); + if abi_target.architecture() == super::arch::Architecture::Riscv64 { + if let Some(abi) = backend.abi.as_deref() { + module.add_metadata_flag( + "target-abi", + FlagBehavior::Error, + context.metadata_string(abi), + ); + } + if let Some(isa) = backend.isa.as_deref() { + let isa_node = context.metadata_node(&[context.metadata_string(isa).into()]); + module.add_metadata_flag("riscv-isa", FlagBehavior::AppendUnique, isa_node); + } + } let td: &'static TargetData = Box::leak(Box::new(td_val)); let mut extern_c_info: HashMap> = HashMap::new(); @@ -446,6 +711,7 @@ fn build_module( } let mut functions: HashMap = HashMap::new(); + let mut export_wrappers: Vec = Vec::new(); let function_nodes: Vec = ast_nodes .iter() @@ -512,14 +778,64 @@ fn build_module( } }; - let llvm_name = export - .as_ref() - .and_then(|export| export.symbol.as_deref()) - .unwrap_or(name.as_str()); - let linkage = export.as_ref().map(|_| Linkage::External); - let function = module.add_function(llvm_name, fn_type, linkage); - apply_function_codegen_attrs(context, function, disable_red_zone); - functions.insert(name.clone(), function); + if let Some(export) = export { + let wave_param_types = parameters + .iter() + .map(|parameter| { + wave_type_to_llvm_type( + context, + ¶meter.param_type, + &struct_types, + TypeFlavor::AbiC, + ) + }) + .collect::>(); + let wave_ret_type = return_type.as_ref().and_then(|return_type| { + if *return_type == WaveType::Void { + None + } else { + Some(wave_type_to_llvm_type( + context, + return_type, + &struct_types, + TypeFlavor::AbiC, + )) + } + }); + let export_decl = ExternFunctionNode { + name: name.clone(), + abi: export.abi.clone(), + symbol: export.symbol.clone(), + params: parameters + .iter() + .map(|parameter| (parameter.name.clone(), parameter.param_type.clone())) + .collect(), + return_type: return_type.clone().unwrap_or(WaveType::Void), + }; + let lowered = lower_extern_c(context, td, abi_target, &export_decl, &struct_types); + let wrapper = module.add_function(&lowered.llvm_name, lowered.fn_type, None); + apply_extern_c_attrs(context, wrapper, &lowered.info); + apply_function_codegen_attrs(context, wrapper, disable_red_zone, cpu, features); + + let implementation_name = format!("__wave_export_impl_{}", name); + let implementation = + module.add_function(&implementation_name, fn_type, Some(Linkage::Internal)); + apply_function_codegen_attrs(context, implementation, disable_red_zone, cpu, features); + + functions.insert(name.clone(), implementation); + extern_c_info.insert(name.clone(), lowered.info.clone()); + export_wrappers.push(ExportCWrapper { + wrapper, + implementation, + info: lowered.info, + wave_param_types, + wave_ret_type, + }); + } else { + let function = module.add_function(name, fn_type, None); + apply_function_codegen_attrs(context, function, disable_red_zone, cpu, features); + functions.insert(name.clone(), function); + } } for ext in &extern_functions { @@ -534,6 +850,7 @@ fn build_module( let f = module.add_function(&lowered.llvm_name, lowered.fn_type, None); apply_extern_c_attrs(context, f, &lowered.info); + apply_function_codegen_attrs(context, f, disable_red_zone, cpu, features); functions.insert(ext.name.clone(), f); @@ -620,6 +937,10 @@ fn build_module( } } + for export in &export_wrappers { + build_export_c_wrapper(context, builder, td, export); + } + if should_run_llvm_pass_pipeline() { let pbo = PassBuilderOptions::create(); let pipeline = pipeline_from_opt_flag(opt_flag); diff --git a/llvm/src/codegen/mod.rs b/llvm/src/codegen/mod.rs index b23ec842..386d2cec 100644 --- a/llvm/src/codegen/mod.rs +++ b/llvm/src/codegen/mod.rs @@ -12,6 +12,7 @@ pub mod abi_c; pub mod address; +pub mod arch; pub mod consts; pub mod format; pub mod ir; diff --git a/llvm/src/codegen/plan.rs b/llvm/src/codegen/plan.rs index 3c8a08f9..0d2e16db 100644 --- a/llvm/src/codegen/plan.rs +++ b/llvm/src/codegen/plan.rs @@ -11,6 +11,7 @@ // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. // codegen/asm/plan.rs +use crate::codegen::arch; use crate::codegen::target::CodegenTarget; use parser::ast::Expression; use std::collections::{HashMap, HashSet}; @@ -77,122 +78,10 @@ fn normalize_token(s: &str) -> String { s.trim().to_ascii_lowercase() } -fn reg_phys_group_x86_64(token: &str) -> Option<&'static str> { - match token { - "al" | "ah" | "ax" | "eax" | "rax" => Some("rax"), - "bl" | "bh" | "bx" | "ebx" | "rbx" => Some("rbx"), - "cl" | "ch" | "cx" | "ecx" | "rcx" => Some("rcx"), - "dl" | "dh" | "dx" | "edx" | "rdx" => Some("rdx"), - "sil" | "si" | "esi" | "rsi" => Some("rsi"), - "dil" | "di" | "edi" | "rdi" => Some("rdi"), - "bpl" | "bp" | "ebp" | "rbp" => Some("rbp"), - "spl" | "sp" | "esp" | "rsp" => Some("rsp"), - "r8b" | "r8w" | "r8d" | "r8" => Some("r8"), - "r9b" | "r9w" | "r9d" | "r9" => Some("r9"), - "r10b" | "r10w" | "r10d" | "r10" => Some("r10"), - "r11b" | "r11w" | "r11d" | "r11" => Some("r11"), - "r12b" | "r12w" | "r12d" | "r12" => Some("r12"), - "r13b" | "r13w" | "r13d" | "r13" => Some("r13"), - "r14b" | "r14w" | "r14d" | "r14" => Some("r14"), - "r15b" | "r15w" | "r15d" | "r15" => Some("r15"), - - _ => None, - } -} - -fn reg_phys_group_arm64(token: &str) -> Option { - match token { - "fp" => return Some("x29".to_string()), - "lr" => return Some("x30".to_string()), - "ip0" => return Some("x16".to_string()), - "ip1" => return Some("x17".to_string()), - "sp" => return Some("sp".to_string()), - "xzr" | "wzr" => return Some("xzr".to_string()), - _ => {} - } - - if token.len() >= 2 { - let (prefix, num) = token.split_at(1); - if (prefix == "x" || prefix == "w") - && num.chars().all(|c| c.is_ascii_digit()) - && !num.is_empty() - { - if let Ok(n) = num.parse::() { - if n <= 30 { - return Some(format!("x{}", n)); - } - } - } - } - - None -} - -fn reg_phys_group_riscv64(token: &str) -> Option { - match token { - "zero" => return Some("x0".to_string()), - "ra" => return Some("x1".to_string()), - "sp" => return Some("x2".to_string()), - "gp" => return Some("x3".to_string()), - "tp" => return Some("x4".to_string()), - "t0" => return Some("x5".to_string()), - "t1" => return Some("x6".to_string()), - "t2" => return Some("x7".to_string()), - "s0" | "fp" => return Some("x8".to_string()), - "s1" => return Some("x9".to_string()), - "a0" => return Some("x10".to_string()), - "a1" => return Some("x11".to_string()), - "a2" => return Some("x12".to_string()), - "a3" => return Some("x13".to_string()), - "a4" => return Some("x14".to_string()), - "a5" => return Some("x15".to_string()), - "a6" => return Some("x16".to_string()), - "a7" => return Some("x17".to_string()), - "s2" => return Some("x18".to_string()), - "s3" => return Some("x19".to_string()), - "s4" => return Some("x20".to_string()), - "s5" => return Some("x21".to_string()), - "s6" => return Some("x22".to_string()), - "s7" => return Some("x23".to_string()), - "s8" => return Some("x24".to_string()), - "s9" => return Some("x25".to_string()), - "s10" => return Some("x26".to_string()), - "s11" => return Some("x27".to_string()), - "t3" => return Some("x28".to_string()), - "t4" => return Some("x29".to_string()), - "t5" => return Some("x30".to_string()), - "t6" => return Some("x31".to_string()), - _ => {} - } - - if let Some(num) = token.strip_prefix('x') { - if num.chars().all(|c| c.is_ascii_digit()) && !num.is_empty() { - if let Ok(n) = num.parse::() { - if n <= 31 { - return Some(format!("x{}", n)); - } - } - } - } - - None -} - /// Decide whether user token is a real register or a constraint class. fn parse_token(target: CodegenTarget, raw: &str) -> RegToken { let raw_norm = normalize_token(raw); - 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()) - } - CodegenTarget::LinuxArm64 - | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => reg_phys_group_arm64(&raw_norm), - CodegenTarget::FreestandingRISCV64 => reg_phys_group_riscv64(&raw_norm), - }; + let phys_group = arch::operand_register_group(target.architecture(), &raw_norm); RegToken { raw_norm, phys_group, @@ -216,23 +105,7 @@ fn build_default_clobbers( ) -> Vec { match mode { AsmSafetyMode::ConservativeKernel => { - let mut clobbers = match target { - CodegenTarget::LinuxX86_64 - | CodegenTarget::DarwinX86_64 - | CodegenTarget::WindowsX86_64Gnu - | CodegenTarget::FreestandingX86_64 => vec![ - "~{memory}".to_string(), - "~{dirflag}".to_string(), - "~{fpsr}".to_string(), - "~{flags}".to_string(), - ], - CodegenTarget::LinuxArm64 - | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => { - vec!["~{memory}".to_string(), "~{cc}".to_string()] - } - CodegenTarget::FreestandingRISCV64 => vec!["~{memory}".to_string()], - }; + let mut clobbers = arch::default_clobbers(target.architecture()); // Empty barrier-like asm blocks must not implicitly clobber every GPR. // Users can still declare explicit register clobbers when needed. @@ -266,45 +139,9 @@ fn build_default_clobbers( return clobbers; } - match target { - CodegenTarget::LinuxX86_64 - | CodegenTarget::DarwinX86_64 - | CodegenTarget::WindowsX86_64Gnu - | CodegenTarget::FreestandingX86_64 => { - const GPRS: [&str; 14] = [ - "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", - "r13", "r14", "r15", - ]; - - for r in GPRS { - if !used_phys.contains(r) { - clobbers.push(format!("~{{{}}}", r)); - } - } - } - CodegenTarget::LinuxArm64 - | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => { - for n in 0..=30u32 { - if n == 18 { - continue; - } - let r = format!("x{}", n); - if !used_phys.contains(&r) { - clobbers.push(format!("~{{{}}}", r)); - } - } - } - CodegenTarget::FreestandingRISCV64 => { - for n in [ - 1u32, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, - ] { - let r = format!("x{}", n); - if !used_phys.contains(&r) { - clobbers.push(format!("~{{{}}}", r)); - } - } + for register in arch::allocatable_registers(target.architecture()) { + if !used_phys.contains(®ister) { + clobbers.push(format!("~{{{}}}", register)); } } @@ -348,29 +185,7 @@ fn gcc_percent_to_llvm_dollar(s: &str) -> String { } fn normalize_special_clobber(target: CodegenTarget, token: &str) -> Option { - match target { - CodegenTarget::LinuxX86_64 - | CodegenTarget::DarwinX86_64 - | CodegenTarget::WindowsX86_64Gnu - | CodegenTarget::FreestandingX86_64 => match token { - "memory" => Some("~{memory}".to_string()), - "cc" | "flags" | "eflags" | "rflags" => Some("~{flags}".to_string()), - "dirflag" => Some("~{dirflag}".to_string()), - "fpsr" => Some("~{fpsr}".to_string()), - _ => None, - }, - CodegenTarget::LinuxArm64 - | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => match token { - "memory" => Some("~{memory}".to_string()), - "cc" | "flags" | "eflags" | "rflags" => Some("~{cc}".to_string()), - _ => None, - }, - CodegenTarget::FreestandingRISCV64 => match token { - "memory" => Some("~{memory}".to_string()), - _ => None, - }, - } + arch::normalize_special_clobber(target.architecture(), token) } fn is_stack_pseudo_clobber(token: &str) -> bool { @@ -404,8 +219,7 @@ fn normalize_clobber_item(target: CodegenTarget, s: &str) -> String { return special; } - let reg = parse_token(target, &n); - if let Some(pg) = reg.phys_group { + if let Some(pg) = arch::register_group(target.architecture(), &n) { return format!("~{{{}}}", pg); } @@ -419,8 +233,7 @@ fn normalize_clobber_item(target: CodegenTarget, s: &str) -> String { return special; } - let reg = parse_token(target, &n); - if let Some(pg) = reg.phys_group { + if let Some(pg) = arch::register_group(target.architecture(), &n) { return format!("~{{{}}}", pg); } @@ -433,8 +246,7 @@ fn normalize_clobber_item(target: CodegenTarget, s: &str) -> String { return special; } - let rt = parse_token(target, t); - if let Some(pg) = rt.phys_group { + if let Some(pg) = arch::register_group(target.architecture(), &normalize_token(t)) { return format!("~{{{}}}", pg); } @@ -512,263 +324,11 @@ fn stack_contract_from_user_clobbers(user: &[String]) -> StackContract { } } -fn strip_inline_asm_comment(line: &str) -> &str { - line.split_once("//") - .map(|(code, _)| code) - .unwrap_or(line) - .split_once('#') - .map(|(code, _)| code) - .unwrap_or(line) -} - -fn asm_instruction_text(line: &str) -> String { - let mut code = strip_inline_asm_comment(line).trim().to_ascii_lowercase(); - - while let Some((label, rest)) = code.split_once(':') { - let label = label.trim(); - if label.is_empty() - || !label - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.') - { - break; - } - - code = rest.trim().to_string(); - } - - code -} - -fn asm_mnemonic(code: &str) -> &str { - code.split(|c: char| c.is_ascii_whitespace() || c == ';') - .next() - .unwrap_or("") -} - -fn parse_x86_imm(raw: &str) -> Option { - let mut s = raw.trim(); - s = s.trim_start_matches('$'); - s = s.trim_start_matches('#'); - s = s.trim_end_matches(','); - - if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("-0x")) { - let negative = s.starts_with('-'); - let value = i64::from_str_radix(hex, 16).ok()?; - return Some(if negative { -value } else { value }); - } - - s.parse::().ok() -} - -fn parse_x86_rsp_adjustment(code: &str) -> Option { - let mut parts = code - .split(|c: char| c.is_ascii_whitespace() || c == ',') - .filter(|p| !p.is_empty()); - - let op = parts.next()?; - let first = parts.next()?; - let second = parts.next()?; - - let sp_is_second = matches!(second, "rsp" | "%rsp" | "esp" | "%esp" | "sp" | "%sp"); - let sp_is_first = matches!(first, "rsp" | "%rsp" | "esp" | "%esp" | "sp" | "%sp"); - - match op { - "sub" | "subq" | "subl" if sp_is_second => parse_x86_imm(first).map(|v| -v), - "add" | "addq" | "addl" if sp_is_second => parse_x86_imm(first), - "sub" | "subq" | "subl" if sp_is_first => parse_x86_imm(second).map(|v| -v), - "add" | "addq" | "addl" if sp_is_first => parse_x86_imm(second), - _ => None, - } -} - -fn x86_jmp_operand_is_indirect(code: &str) -> bool { - let mut parts = code - .split(|c: char| c.is_ascii_whitespace() || c == ',') - .filter(|p| !p.is_empty()); - - let _op = parts.next(); - let Some(operand) = parts.next() else { - return false; - }; - let operand = operand.trim_start_matches('*').trim_start_matches('%'); - - operand.starts_with('[') - || matches!( - reg_phys_group_x86_64(operand), - Some( - "rax" - | "rbx" - | "rcx" - | "rdx" - | "rsi" - | "rdi" - | "rbp" - | "rsp" - | "r8" - | "r9" - | "r10" - | "r11" - | "r12" - | "r13" - | "r14" - | "r15" - ) - ) -} - -#[derive(Debug, Default, Clone, Copy)] -struct StackAnalysis { - touches_stack: bool, - unknown_stack_write: bool, - unbalanced_delta: i64, - nonreturning_branch: bool, -} - -fn x86_64_stack_analysis(line: &str) -> StackAnalysis { - let code = asm_instruction_text(line); - if code.is_empty() { - return StackAnalysis::default(); - } - - let first = asm_mnemonic(&code); - let mut out = StackAnalysis::default(); - - match first { - "call" | "callq" => { - out.touches_stack = true; - return out; - } - "push" | "pushq" => { - out.touches_stack = true; - out.unbalanced_delta = -8; - return out; - } - "pop" | "popq" => { - out.touches_stack = true; - out.unbalanced_delta = 8; - return out; - } - "ret" | "retq" => { - out.touches_stack = true; - out.unbalanced_delta = 8; - return out; - } - "retf" | "retfq" => { - out.touches_stack = true; - out.unbalanced_delta = 16; - return out; - } - "iret" | "iretq" => { - out.touches_stack = true; - out.unknown_stack_write = true; - return out; - } - "leave" | "enter" => { - out.touches_stack = true; - out.unknown_stack_write = true; - return out; - } - "jmp" | "jmpq" => { - out.nonreturning_branch = x86_jmp_operand_is_indirect(&code); - return out; - } - _ => {} - } - - if let Some(delta) = parse_x86_rsp_adjustment(&code) { - out.touches_stack = true; - out.unbalanced_delta = delta; - return out; - } - - let writes_sp = code.starts_with("mov rsp") - || code.starts_with("movq rsp") - || code.starts_with("mov %rsp") - || code.starts_with("movq %rsp") - || code.starts_with("and rsp") - || code.starts_with("andq rsp") - || code.starts_with("and %rsp") - || code.starts_with("andq %rsp") - || code.starts_with("xor rsp") - || code.starts_with("xor %rsp") - || code.starts_with("lea rsp") - || code.starts_with("lea %rsp"); - - if writes_sp { - out.touches_stack = true; - out.unknown_stack_write = true; - return out; - } - - out.touches_stack = code.contains("rsp") - || code.contains("esp") - || code.contains("[sp") - || code.contains(" sp,") - || code.contains(", sp"); - out -} - -fn aarch64_stack_analysis(line: &str) -> StackAnalysis { - let code = asm_instruction_text(line); - if code.is_empty() { - return StackAnalysis::default(); - } - - let mut out = StackAnalysis::default(); - let first = asm_mnemonic(&code); - - out.nonreturning_branch = matches!(first, "br"); - out.touches_stack = code == "ret" - || code.starts_with("ret ") - || code.starts_with("bl ") - || code.starts_with("blr ") - || code.contains(" sp,") - || code.contains(", sp") - || code.contains("[sp"); - if out.touches_stack && code.contains(" sp,") { - out.unknown_stack_write = true; - } - out -} - -fn riscv64_stack_analysis(line: &str) -> StackAnalysis { - let code = asm_instruction_text(line); - if code.is_empty() { - return StackAnalysis::default(); - } - - let mut out = StackAnalysis::default(); - let first = asm_mnemonic(&code); - - out.nonreturning_branch = matches!(first, "jr"); - out.touches_stack = code == "ret" - || code.starts_with("call ") - || code.starts_with("jal ") - || code.starts_with("jalr ") - || code.contains(" sp,") - || code.contains(", sp") - || code.contains("(sp)"); - if out.touches_stack && (code.contains(" sp,") || code.starts_with("addi sp")) { - out.unknown_stack_write = true; - } - out -} - -fn asm_stack_analysis(target: CodegenTarget, instructions: &[String]) -> StackAnalysis { - let mut total = StackAnalysis::default(); +fn asm_stack_analysis(target: CodegenTarget, instructions: &[String]) -> arch::StackAnalysis { + let mut total = arch::StackAnalysis::default(); for line in instructions { - let item = match target { - CodegenTarget::LinuxX86_64 - | CodegenTarget::DarwinX86_64 - | CodegenTarget::WindowsX86_64Gnu - | CodegenTarget::FreestandingX86_64 => x86_64_stack_analysis(line), - CodegenTarget::LinuxArm64 - | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => aarch64_stack_analysis(line), - CodegenTarget::FreestandingRISCV64 => riscv64_stack_analysis(line), - }; + let item = arch::stack_analysis(target.architecture(), line); total.touches_stack |= item.touches_stack; total.unknown_stack_write |= item.unknown_stack_write; diff --git a/llvm/src/codegen/target.rs b/llvm/src/codegen/target.rs index f5b4ce3b..8b38efaa 100644 --- a/llvm/src/codegen/target.rs +++ b/llvm/src/codegen/target.rs @@ -12,11 +12,15 @@ use inkwell::module::Module; use inkwell::targets::TargetTriple; +use std::collections::{BTreeMap, BTreeSet}; + +use super::arch::{self, Architecture}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CodegenTarget { LinuxX86_64, LinuxArm64, + LinuxRISCV64, DarwinX86_64, DarwinArm64, WindowsX86_64Gnu, @@ -29,7 +33,7 @@ pub enum CodegenTarget { pub struct TargetSpec { pub triple: &'static str, pub codegen: CodegenTarget, - pub arch: &'static str, + pub architecture: Architecture, pub vendor: &'static str, pub os: &'static str, pub env: &'static str, @@ -38,160 +42,373 @@ pub struct TargetSpec { pub cpus: &'static [&'static str], pub features: &'static [&'static str], pub abis: &'static [&'static str], + pub default_cpu: &'static str, + pub default_features: &'static [&'static str], + pub default_abi: Option<&'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"]; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EffectiveTargetOptions { + pub cpu: String, + pub features: String, + pub abi: Option, + pub isa: Option, +} -#[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"]; +pub fn resolve_target_options( + spec: &TargetSpec, + cpu: Option<&str>, + features: Option<&str>, + abi: Option<&str>, +) -> Result { + let cpu = cpu.unwrap_or(spec.default_cpu); + if !spec.cpus.contains(&cpu) { + return Err(format!( + "unsupported CPU '{}' for target '{}'; supported CPUs: {}", + cpu, + spec.triple, + spec.cpus.join(", ") + )); + } -#[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"]; + if let Some(abi) = abi { + if !spec.abis.contains(&abi) { + let supported = if spec.abis.is_empty() { + "no ABI overrides".to_string() + } else { + spec.abis.join(", ") + }; + return Err(format!( + "unsupported ABI '{}' for target '{}'; supported ABIs: {}", + abi, spec.triple, supported + )); + } + } + + let mut enabled = spec + .features + .iter() + .copied() + .map(|name| (name, spec.default_features.contains(&name))) + .collect::>(); + + if spec.architecture == Architecture::Riscv64 { + match abi.or(spec.default_abi) { + Some("lp64") => { + enabled.insert("f", false); + enabled.insert("d", false); + } + Some("lp64f") => { + enabled.insert("f", true); + enabled.insert("d", false); + } + Some("lp64d") => { + enabled.insert("f", true); + enabled.insert("d", true); + } + _ => {} + } + } + + let mut explicitly_set = BTreeSet::new(); + if let Some(features) = features { + for raw in features.split(',') { + let setting = raw.trim(); + if setting.is_empty() { + return Err(format!( + "invalid empty target feature in '{}' for target '{}'", + features, spec.triple + )); + } + let (value, name) = if let Some(name) = setting.strip_prefix('+') { + (true, name) + } else if let Some(name) = setting.strip_prefix('-') { + (false, name) + } else { + return Err(format!( + "invalid target feature '{}'; use '+feature' to enable or '-feature' to disable it", + setting + )); + }; + if name.is_empty() || !spec.features.contains(&name) { + return Err(format!( + "unsupported feature '{}' for target '{}'; supported features: {}", + name, + spec.triple, + spec.features.join(", ") + )); + } + if !explicitly_set.insert(name) { + return Err(format!( + "target feature '{}' is specified more than once for target '{}'", + name, spec.triple + )); + } + enabled.insert(name, value); + } + } + + let mut effective_abi = abi.or(spec.default_abi).map(str::to_string); + let mut isa = None; + if spec.architecture == Architecture::Riscv64 { + if enabled.get("f").copied().unwrap_or(false) && !explicitly_set.contains("zicsr") { + enabled.insert("zicsr", true); + } + let feature = |name| enabled.get(name).copied().unwrap_or(false); + if feature("d") && !feature("f") { + return Err(format!( + "invalid feature combination for target '{}': feature 'd' requires feature 'f'", + spec.triple + )); + } + if feature("f") && !feature("zicsr") { + return Err(format!( + "invalid feature combination for target '{}': feature 'f' requires feature 'zicsr'", + spec.triple + )); + } + + let derived_abi = if feature("d") { + "lp64d" + } else if feature("f") { + "lp64f" + } else { + "lp64" + }; + if let Some(requested) = abi { + if requested != derived_abi { + let requirement = match requested { + "lp64" => "features 'f' and 'd' to be disabled", + "lp64f" => "feature 'f' enabled and feature 'd' disabled", + "lp64d" => "features 'f' and 'd' enabled", + _ => unreachable!(), + }; + return Err(format!( + "ABI '{}' for target '{}' requires {}", + requested, spec.triple, requirement + )); + } + } else { + effective_abi = Some(derived_abi.to_string()); + } + + isa = Some(arch::riscv64::isa_name( + feature("m"), + feature("a"), + feature("f"), + feature("d"), + feature("c"), + feature("zicsr"), + feature("zifencei"), + )); + } + + let render_all_features = spec.architecture == Architecture::Riscv64; + let features = spec + .features + .iter() + .filter(|name| { + render_all_features + || explicitly_set.contains(**name) + || spec.default_features.contains(name) + }) + .map(|name| { + let sign = if enabled.get(name).copied().unwrap_or(false) { + '+' + } else { + '-' + }; + format!("{}{}", sign, name) + }) + .collect::>() + .join(","); + + Ok(EffectiveTargetOptions { + cpu: cpu.to_string(), + features, + abi: effective_abi, + isa, + }) +} #[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", + architecture: Architecture::X86_64, vendor: "unknown", os: "linux", env: "gnu", object_format: "elf", hosted: true, - cpus: X86_CPUS, - features: X86_FEATURES, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::X86_64, vendor: "apple", os: "macos", env: "", object_format: "macho", hosted: true, - cpus: X86_CPUS, - features: X86_FEATURES, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::X86_64, vendor: "w64", os: "windows", env: "gnu", object_format: "coff", hosted: true, - cpus: X86_CPUS, - features: X86_FEATURES, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::X86_64, vendor: "pc", os: "windows", env: "gnu", object_format: "coff", hosted: true, - cpus: X86_CPUS, - features: X86_FEATURES, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::X86_64, vendor: "unknown", os: "none", env: "none", object_format: "elf", hosted: false, - cpus: X86_CPUS, - features: X86_FEATURES, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::Aarch64, vendor: "unknown", os: "linux", env: "gnu", object_format: "elf", hosted: true, - cpus: AARCH64_CPUS, - features: AARCH64_FEATURES, + cpus: arch::aarch64::CPUS, + features: arch::aarch64::FEATURES, abis: &[], + default_cpu: arch::aarch64::DEFAULT_CPU, + default_features: arch::aarch64::DEFAULT_FEATURES, + default_abi: None, }; #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] const DARWIN_AARCH64: TargetSpec = TargetSpec { triple: "aarch64-apple-darwin", codegen: CodegenTarget::DarwinArm64, - arch: "aarch64", + architecture: Architecture::Aarch64, vendor: "apple", os: "macos", env: "", object_format: "macho", hosted: true, - cpus: DARWIN_AARCH64_CPUS, - features: AARCH64_FEATURES, + cpus: arch::aarch64::DARWIN_CPUS, + features: arch::aarch64::FEATURES, abis: &[], + default_cpu: arch::aarch64::DEFAULT_CPU, + default_features: arch::aarch64::DEFAULT_FEATURES, + default_abi: None, }; #[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", + architecture: Architecture::Aarch64, vendor: "unknown", os: "none", env: "none", object_format: "elf", hosted: false, - cpus: AARCH64_CPUS, - features: AARCH64_FEATURES, + cpus: arch::aarch64::CPUS, + features: arch::aarch64::FEATURES, abis: &[], + default_cpu: arch::aarch64::DEFAULT_CPU, + default_features: arch::aarch64::DEFAULT_FEATURES, + default_abi: None, +}; + +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] +const LINUX_RISCV64: TargetSpec = TargetSpec { + triple: "riscv64-unknown-linux-gnu", + codegen: CodegenTarget::LinuxRISCV64, + architecture: Architecture::Riscv64, + vendor: "unknown", + os: "linux", + env: "gnu", + object_format: "elf", + hosted: true, + cpus: arch::riscv64::CPUS, + features: arch::riscv64::FEATURES, + abis: arch::riscv64::ABIS, + default_cpu: arch::riscv64::DEFAULT_CPU, + default_features: arch::riscv64::LINUX_DEFAULT_FEATURES, + default_abi: Some(arch::riscv64::LINUX_DEFAULT_ABI), }; #[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", + architecture: Architecture::Riscv64, vendor: "unknown", os: "none", env: "none", object_format: "elf", hosted: false, - cpus: RISCV64_CPUS, - features: RISCV64_FEATURES, - abis: RISCV64_ABIS, + cpus: arch::riscv64::CPUS, + features: arch::riscv64::FEATURES, + abis: arch::riscv64::ABIS, + default_cpu: arch::riscv64::DEFAULT_CPU, + default_features: arch::riscv64::FREESTANDING_DEFAULT_FEATURES, + default_abi: Some(arch::riscv64::FREESTANDING_DEFAULT_ABI), }; pub fn supported_target_specs() -> Vec<&'static TargetSpec> { @@ -210,7 +427,7 @@ pub fn supported_target_specs() -> Vec<&'static TargetSpec> { specs.extend([&LINUX_AARCH64, &DARWIN_AARCH64, &FREESTANDING_AARCH64]); #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] - specs.push(&FREESTANDING_RISCV64); + specs.extend([&LINUX_RISCV64, &FREESTANDING_RISCV64]); specs.sort_unstable_by_key(|spec| spec.triple); specs @@ -223,6 +440,17 @@ pub fn target_spec_for_triple(triple: &str) -> Option<&'static TargetSpec> { } impl CodegenTarget { + pub const fn architecture(self) -> Architecture { + match self { + Self::LinuxX86_64 + | Self::DarwinX86_64 + | Self::WindowsX86_64Gnu + | Self::FreestandingX86_64 => Architecture::X86_64, + Self::LinuxArm64 | Self::DarwinArm64 | Self::FreestandingArm64 => Architecture::Aarch64, + Self::LinuxRISCV64 | Self::FreestandingRISCV64 => Architecture::Riscv64, + } + } + pub fn from_triple_str(triple: &str) -> Option { target_spec_for_triple(triple).map(|spec| spec.codegen) } @@ -246,6 +474,7 @@ impl CodegenTarget { Self::WindowsX86_64Gnu => "windows x86_64 gnu", Self::FreestandingX86_64 => "freestanding x86_64", Self::FreestandingArm64 => "freestanding arm64", + Self::LinuxRISCV64 => "linux riscv64", Self::FreestandingRISCV64 => "freestanding riscv64", } } @@ -301,7 +530,7 @@ mod tests { CodegenTarget::from_triple_str(spec.triple), Some(spec.codegen) ); - assert!(!spec.arch.is_empty()); + assert!(!spec.architecture.name().is_empty()); assert!(!spec.os.is_empty()); assert!(!spec.object_format.is_empty()); @@ -316,7 +545,7 @@ mod tests { for triple in [ "x86_64-garbage-linux-gnu", "prefix-x86_64-unknown-linux-gnu-suffix", - "riscv64-unknown-linux-gnu", + "riscv64-unknown-linux-musl", "x86_64-unknown-none-elf-waveabi", "", ] { @@ -324,4 +553,37 @@ mod tests { assert_eq!(CodegenTarget::from_triple_str(triple), None, "{triple}"); } } + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + #[test] + fn riscv64_defaults_define_distinct_hosted_and_freestanding_contracts() { + let linux = resolve_target_options(&LINUX_RISCV64, None, None, None).unwrap(); + assert_eq!(linux.cpu, "generic-rv64"); + assert_eq!(linux.features, "+m,+a,+f,+d,+c,+zicsr,+zifencei"); + assert_eq!(linux.abi.as_deref(), Some("lp64d")); + assert_eq!(linux.isa.as_deref(), Some("rv64gc")); + + let freestanding = resolve_target_options(&FREESTANDING_RISCV64, None, None, None).unwrap(); + assert_eq!(freestanding.cpu, "generic-rv64"); + assert_eq!(freestanding.features, "+m,+a,-f,-d,+c,-zicsr,-zifencei"); + assert_eq!(freestanding.abi.as_deref(), Some("lp64")); + assert_eq!(freestanding.isa.as_deref(), Some("rv64imac")); + } + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + #[test] + fn riscv64_feature_overrides_derive_or_validate_the_float_abi() { + let derived = + resolve_target_options(&FREESTANDING_RISCV64, None, Some("+f,-d"), None).unwrap(); + assert_eq!(derived.abi.as_deref(), Some("lp64f")); + assert_eq!(derived.isa.as_deref(), Some("rv64imafc_zicsr")); + + let error = + resolve_target_options(&FREESTANDING_RISCV64, None, Some("+f,-d"), Some("lp64d")) + .unwrap_err(); + assert!(error.contains("requires features 'f' and 'd' enabled")); + + let error = resolve_target_options(&LINUX_RISCV64, None, Some("-f"), None).unwrap_err(); + assert!(error.contains("feature 'd' requires feature 'f'")); + } } diff --git a/llvm/src/expression/rvalue/asm.rs b/llvm/src/expression/rvalue/asm.rs index 944b2352..686b58d2 100644 --- a/llvm/src/expression/rvalue/asm.rs +++ b/llvm/src/expression/rvalue/asm.rs @@ -11,29 +11,16 @@ // 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 super::ExprGenEnv; +use crate::codegen::arch; use crate::codegen::plan::*; -use crate::codegen::target::{require_supported_target_from_module, CodegenTarget}; +use crate::codegen::target::require_supported_target_from_module; use crate::codegen::types::{wave_type_to_llvm_type, TypeFlavor}; use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum, StringRadix}; use inkwell::values::{ AsValueRef, BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; -use inkwell::InlineAsmDialect; use parser::ast::{Expression, Literal, WaveType}; -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 - | CodegenTarget::FreestandingArm64 - | CodegenTarget::FreestandingRISCV64 => InlineAsmDialect::ATT, - } -} - pub(crate) fn gen<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, instructions: &[String], @@ -75,7 +62,7 @@ pub(crate) fn gen<'ctx, 'a>( constraints_str, plan.has_side_effects, plan.align_stack, - Some(inline_asm_dialect_for_target(target)), + Some(arch::inline_asm_dialect(target.architecture())), false, ); @@ -109,7 +96,7 @@ pub(crate) fn gen<'ctx, 'a>( constraints_str, plan.has_side_effects, plan.align_stack, - Some(inline_asm_dialect_for_target(target)), + Some(arch::inline_asm_dialect(target.architecture())), false, ); diff --git a/llvm/src/statement/asm.rs b/llvm/src/statement/asm.rs index 631e5cb9..02f92f16 100644 --- a/llvm/src/statement/asm.rs +++ b/llvm/src/statement/asm.rs @@ -10,18 +10,17 @@ // 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::arch; use crate::codegen::plan::*; -use crate::codegen::target::{require_supported_target_from_module, CodegenTarget}; +use crate::codegen::target::require_supported_target_from_module; use crate::codegen::types::{wave_type_to_llvm_type, TypeFlavor}; use crate::codegen::VariableInfo; use inkwell::module::Module; +use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum, StringRadix}; use inkwell::values::{ BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; -use inkwell::InlineAsmDialect; - -use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum, StringRadix}; use parser::ast::{Expression, Literal, WaveType}; use std::collections::HashMap; @@ -45,103 +44,6 @@ fn llvm_type_of_wave<'ctx>( wave_type_to_llvm_type(context, wt, struct_types, TypeFlavor::Value) } -fn reg_width_bits(reg: &str) -> Option { - match reg { - "al" | "bl" | "cl" | "dl" | "sil" | "dil" | "r8b" | "r9b" | "r10b" | "r11b" | "r12b" - | "r13b" | "r14b" | "r15b" => Some(8), - - "ax" | "bx" | "cx" | "dx" | "si" | "di" | "r8w" | "r9w" | "r10w" | "r11w" | "r12w" - | "r13w" | "r14w" | "r15w" => Some(16), - - "eax" | "ebx" | "ecx" | "edx" | "esi" | "edi" | "r8d" | "r9d" | "r10d" | "r11d" - | "r12d" | "r13d" | "r14d" | "r15d" => Some(32), - - "rax" | "rbx" | "rcx" | "rdx" | "rsi" | "rdi" | "rbp" | "rsp" | "r8" | "r9" | "r10" - | "r11" | "r12" | "r13" | "r14" | "r15" => Some(64), - - _ => None, - } -} - -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 - | CodegenTarget::FreestandingArm64 => { - if reg.len() >= 2 { - let (prefix, num) = reg.split_at(1); - if num.chars().all(|c| c.is_ascii_digit()) && !num.is_empty() { - if let Ok(n) = num.parse::() { - if n <= 30 { - return match prefix { - "w" => Some(32), - "x" => Some(64), - _ => None, - }; - } - } - } - } - None - } - CodegenTarget::FreestandingRISCV64 => { - if reg == "zero" { - return Some(64); - } - if matches!( - reg, - "ra" | "sp" - | "gp" - | "tp" - | "t0" - | "t1" - | "t2" - | "t3" - | "t4" - | "t5" - | "t6" - | "s0" - | "fp" - | "s1" - | "s2" - | "s3" - | "s4" - | "s5" - | "s6" - | "s7" - | "s8" - | "s9" - | "s10" - | "s11" - | "a0" - | "a1" - | "a2" - | "a3" - | "a4" - | "a5" - | "a6" - | "a7" - ) { - return Some(64); - } - if let Some(num) = reg.strip_prefix('x') { - if num.chars().all(|c| c.is_ascii_digit()) && !num.is_empty() { - if let Ok(n) = num.parse::() { - if n <= 31 { - return Some(64); - } - } - } - } - None - } - } -} - fn extract_reg_from_constraint(c: &str) -> Option { if let Some(inner) = c.strip_prefix('{').and_then(|s| s.strip_suffix('}')) { return Some(inner.to_ascii_lowercase()); @@ -155,19 +57,6 @@ fn extract_reg_from_constraint(c: &str) -> Option { } } -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 - | CodegenTarget::FreestandingArm64 - | CodegenTarget::FreestandingRISCV64 => InlineAsmDialect::ATT, - } -} - pub(super) fn gen_asm_stmt_ir<'ctx>( context: &'ctx inkwell::context::Context, builder: &'ctx inkwell::builder::Builder<'ctx>, @@ -206,7 +95,7 @@ pub(super) fn gen_asm_stmt_ir<'ctx>( // reg width forcing if let Some(reg) = extract_reg_from_constraint(&inp.constraint) { - if let Some(bits) = reg_width_bits_for_target(target, ®) { + if let Some(bits) = arch::register_width_bits(target.architecture(), ®) { if val.is_int_value() { let iv = val.into_int_value(); let target_ty = context.custom_width_int_type(bits); @@ -252,7 +141,7 @@ pub(super) fn gen_asm_stmt_ir<'ctx>( let mut asm_ty = dst_ty; if let Some(reg) = extract_reg_from_constraint(&o.reg_norm) { - if let Some(bits) = reg_width_bits_for_target(target, ®) { + if let Some(bits) = arch::register_width_bits(target.architecture(), ®) { if dst_ty.is_int_type() { asm_ty = context.custom_width_int_type(bits).as_basic_type_enum(); } @@ -278,7 +167,7 @@ pub(super) fn gen_asm_stmt_ir<'ctx>( constraints_str, plan.has_side_effects, plan.align_stack, - Some(inline_asm_dialect_for_target(target)), + Some(arch::inline_asm_dialect(target.architecture())), false, ); diff --git a/src/cli.rs b/src/cli.rs index ed8d3ecd..932dbe81 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,7 +18,8 @@ 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, + resolve_target_options, supported_target_specs, target_spec_for_triple, CodegenTarget, + EffectiveTargetOptions, TargetSpec, }; use std::collections::BTreeSet; use std::io::ErrorKind; @@ -280,8 +281,8 @@ 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 mut effective_global = effective_global_for_build(global, &build); + resolve_target_configuration(&mut effective_global.llvm)?; let classified = classify_inputs(&build)?; validate_build_request(&effective_global, &build, &classified)?; @@ -791,6 +792,7 @@ fn parse_llvm_backend_option( return Err(CliError::usage("missing value: --target=")); } llvm.target = Some(v.to_string()); + llvm.target_explicit = true; *i += 1; return Ok(true); } @@ -802,6 +804,7 @@ fn parse_llvm_backend_option( return Err(CliError::usage("missing value: --target ")); } llvm.target = Some(v.to_string()); + llvm.target_explicit = true; *i += 2; return Ok(true); } @@ -2132,14 +2135,22 @@ fn build_llc_lowering_args( } )); - if let Some(target) = &global.llvm.target { - args.push(format!("--mtriple={}", target)); - } - if let Some(cpu) = &global.llvm.cpu { - args.push(format!("--mcpu={}", cpu)); - } - if let Some(features) = &global.llvm.features { - args.push(format!("--mattr={}", features)); + // IR and bitcode carry their own target contract. Only override it when + // the user explicitly supplied --target; the implicit host target must + // not silently rewrite a cross-target artifact. + if global.llvm.target_explicit { + if let Some(target) = &global.llvm.target { + args.push(format!("--mtriple={}", target)); + } + if let Some(cpu) = &global.llvm.cpu { + args.push(format!("--mcpu={}", cpu)); + } + if let Some(features) = &global.llvm.features { + args.push(format!("--mattr={}", features)); + } + if let Some(abi) = &global.llvm.abi { + args.push(format!("--target-abi={}", abi)); + } } if let Some(model) = &global.llvm.code_model { args.push(format!("--code-model={}", model)); @@ -2147,10 +2158,6 @@ fn build_llc_lowering_args( if let Some(model) = &global.llvm.relocation_model { args.push(format!("--relocation-model={}", model)); } - if let Some(abi) = &global.llvm.abi { - args.push(format!("--target-abi={}", abi)); - } - if !global.opt.is_empty() { args.push(normalize_opt_for_llvm_tool(&global.opt).to_string()); } @@ -2194,6 +2201,10 @@ fn link_objects( ) -> Result<(), CliError> { ensure_parent_dir(output)?; + if global.llvm.linker.is_none() { + validate_default_elf_runtime(global, build)?; + } + let (bin, args) = build_linker_args(global, build, objects, output); let mut command = ProcessCommand::new(&bin); configure_bundled_llvm_tool_env(&mut command, &bin); @@ -2219,6 +2230,58 @@ fn link_objects( ))) } +fn validate_default_elf_runtime(global: &Global, build: &BuildRequest) -> Result<(), CliError> { + let target = target_triple_for_global(global); + if !is_linux_target(&target) || target == host_target_triple() || global.llvm.no_default_libs { + return Ok(()); + } + + let mut missing = Vec::new(); + if !build.shared && !build.no_start_files { + let start_name = elf_start_file_name(build); + let has_system_start = find_elf_runtime_file(&target, global, start_name).is_some() + && find_elf_runtime_file(&target, global, "crti.o").is_some(); + let has_bundled_start = + start_name == "crt1.o" && llvm::toolchain::find_bundled_linux_crt1(&target).is_some(); + if !has_system_start && !has_bundled_start { + missing.push(format!("{} and crti.o", start_name)); + } + } + let libc_names: &[&str] = if build.static_link { + &["libc.a"] + } else { + &["libc.so", "libc.a", "libc.so.6"] + }; + let libm_names: &[&str] = if build.static_link { + &["libm.a"] + } else { + &["libm.so", "libm.a", "libm.so.6"] + }; + if find_elf_runtime_file_any(&target, global, libc_names).is_none() { + missing.push("libc".to_string()); + } + if find_elf_runtime_file_any(&target, global, libm_names).is_none() { + missing.push("libm".to_string()); + } + + if missing.is_empty() { + return Ok(()); + } + + let location = global + .llvm + .sysroot + .as_deref() + .map(|sysroot| format!("sysroot '{}'", sysroot)) + .unwrap_or_else(|| "the target-specific system paths".to_string()); + Err(CliError::CommandFailed(format!( + "target runtime for '{}' is incomplete in {} (missing: {}); install the target C runtime or pass --sysroot= to a complete target sysroot; for a freestanding binary use --freestanding with an explicit entry point", + target, + location, + missing.join(", ") + ))) +} + fn build_linker_args( global: &Global, build: &BuildRequest, @@ -2377,13 +2440,15 @@ fn build_elf_lld_args( args.push("-m".to_string()); args.push(emulation.to_string()); } - if let Some(sysroot) = &global.llvm.sysroot { + if let Some(sysroot) = elf_lld_sysroot(target, global) { args.push(format!("--sysroot={}", sysroot)); } let mut uses_elf_end_files = false; if !global.llvm.no_default_libs && is_linux_target(target) && !build.shared { - if let Some(dynamic_linker) = linux_dynamic_linker(target) { - args.push(format!("--dynamic-linker={}", dynamic_linker)); + if !build.static_link { + if let Some(dynamic_linker) = linux_dynamic_linker(target, global.llvm.abi.as_deref()) { + args.push(format!("--dynamic-linker={}", dynamic_linker)); + } } uses_elf_end_files = append_elf_start_files(&mut args, target, global, build); } @@ -2502,15 +2567,21 @@ fn elf_lld_emulation(target: &str) -> Option<&'static str> { 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"), + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => Some("elf64lriscv"), _ => None, } } -fn linux_dynamic_linker(target: &str) -> Option<&'static str> { +fn linux_dynamic_linker(target: &str, abi: Option<&str>) -> Option<&'static str> { 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"), + CodegenTarget::LinuxRISCV64 => match abi { + Some("lp64") => Some("/lib/ld-linux-riscv64-lp64.so.1"), + Some("lp64f") => Some("/lib/ld-linux-riscv64-lp64f.so.1"), + Some("lp64d") | None => Some("/lib/ld-linux-riscv64-lp64d.so.1"), + Some(_) => None, + }, _ => None, } } @@ -2519,6 +2590,7 @@ fn linux_multiarch(target: &str) -> Option<&'static str> { match target_spec_for_triple(target)?.codegen { CodegenTarget::LinuxX86_64 => Some("x86_64-linux-gnu"), CodegenTarget::LinuxArm64 => Some("aarch64-linux-gnu"), + CodegenTarget::LinuxRISCV64 => Some("riscv64-linux-gnu"), _ => None, } } @@ -2533,11 +2605,7 @@ fn append_elf_start_files( return false; } - let start_name = if build.pie == Some(true) { - "Scrt1.o" - } else { - "crt1.o" - }; + let start_name = elf_start_file_name(build); let start_file = find_elf_runtime_file(target, global, start_name); let init_file = find_elf_runtime_file(target, global, "crti.o"); @@ -2547,10 +2615,23 @@ fn append_elf_start_files( return true; } + if start_name != "crt1.o" { + args.push(start_name.to_string()); + return false; + } + append_bundled_linux_crt1(args, target); false } +fn elf_start_file_name(build: &BuildRequest) -> &'static str { + match (build.static_link, build.pie) { + (true, Some(true)) => "rcrt1.o", + (false, Some(true)) => "Scrt1.o", + _ => "crt1.o", + } +} + fn append_elf_end_files(args: &mut Vec, target: &str, global: &Global) { if let Some(path) = find_elf_runtime_file(target, global, "crtn.o") { args.push(path); @@ -2656,7 +2737,9 @@ fn find_elf_runtime_file(target: &str, global: &Global, name: &str) -> Option O None } +fn elf_lld_sysroot(target: &str, global: &Global) -> Option { + let sysroot = global.llvm.sysroot.as_deref()?; + + // Debian-style cross runtime prefixes (for example + // /usr/riscv64-linux-gnu) are accepted as Wave sysroots so target CRT and + // libraries can be discovered inside them. Their libc/libm linker + // scripts, however, refer back to that prefix with absolute paths. Passing + // the same prefix to ld.lld as --sysroot would prepend it a second time. + // Root the linker at the host filesystem only for this detected layout; + // all search paths and CRT objects remain the target-owned paths selected + // by elf_runtime_dirs. + if linker_script_references_absolute_sysroot(target, global, sysroot) { + Some("/".to_string()) + } else { + Some(sysroot.to_string()) + } +} + +fn linker_script_references_absolute_sysroot(target: &str, global: &Global, sysroot: &str) -> bool { + if sysroot.is_empty() || !Path::new(sysroot).is_absolute() { + return false; + } + let normalized_sysroot = sysroot.trim_end_matches(['/', '\\']).replace('\\', "/"); + if normalized_sysroot.is_empty() { + return false; + } + let absolute_prefix = format!("{}/", normalized_sysroot); + + ["libc.so", "libm.so"].into_iter().any(|name| { + let Some(path) = find_elf_runtime_file(target, global, name) else { + return false; + }; + fs::read_to_string(path) + .is_ok_and(|script| script.replace('\\', "/").contains(&absolute_prefix)) + }) +} + fn elf_runtime_dirs(target: &str, global: &Global) -> Vec { let sysroot = global.llvm.sysroot.as_deref().unwrap_or(""); let mut dirs = Vec::new(); @@ -2679,14 +2799,60 @@ fn elf_runtime_dirs(target: &str, global: &Global) -> Vec { if let Some(multiarch) = linux_multiarch(target) { dirs.push(sysroot_path(sysroot, &format!("usr/lib/{}", multiarch))); dirs.push(sysroot_path(sysroot, &format!("lib/{}", multiarch))); + dirs.push(sysroot_path(sysroot, &format!("usr/{}/lib", multiarch))); + dirs.push(sysroot_path(sysroot, &format!("{}/lib", multiarch))); + } + if let Some(abi_dir) = riscv64_abi_lib_dir(target, global.llvm.abi.as_deref()) { + dirs.push(sysroot_path(sysroot, &format!("usr/{}", abi_dir))); + dirs.push(sysroot_path(sysroot, abi_dir)); + } + + // Generic lib directories are only target-owned when they are inside an + // explicit sysroot or when the target is the host. A cross target must + // never consume the host's crt objects or libc by accident. + if !sysroot.is_empty() || target == host_target_triple() { + dirs.push(sysroot_path(sysroot, "usr/lib64")); + dirs.push(sysroot_path(sysroot, "lib64")); + dirs.push(sysroot_path(sysroot, "usr/lib")); + dirs.push(sysroot_path(sysroot, "lib")); } - dirs.push(sysroot_path(sysroot, "usr/lib64")); - dirs.push(sysroot_path(sysroot, "lib64")); - dirs.push(sysroot_path(sysroot, "usr/lib")); - dirs.push(sysroot_path(sysroot, "lib")); + dirs.dedup(); dirs } +fn riscv64_abi_lib_dir(target: &str, abi: Option<&str>) -> Option<&'static str> { + match target_spec_for_triple(target)?.codegen { + CodegenTarget::LinuxRISCV64 => match abi { + Some("lp64") => Some("lib64/lp64"), + Some("lp64f") => Some("lib64/lp64f"), + Some("lp64d") | None => Some("lib64/lp64d"), + Some(_) => None, + }, + _ => None, + } +} + +fn elf_object_matches_target(path: &Path, target: &str) -> bool { + let expected_machine = match target_spec_for_triple(target).map(|spec| spec.architecture) { + Some(llvm::codegen::arch::Architecture::X86_64) => 62, + Some(llvm::codegen::arch::Architecture::Aarch64) => 183, + Some(llvm::codegen::arch::Architecture::Riscv64) => 243, + None => return false, + }; + let Ok(header) = fs::read(path) else { + return false; + }; + if header.len() < 20 || &header[..4] != b"\x7fELF" { + return false; + } + let machine = match header[5] { + 1 => u16::from_le_bytes([header[18], header[19]]), + 2 => u16::from_be_bytes([header[18], header[19]]), + _ => return false, + }; + machine == expected_machine +} + fn sysroot_path(sysroot: &str, suffix: &str) -> PathBuf { if sysroot.is_empty() { PathBuf::from("/").join(suffix) @@ -2961,8 +3127,16 @@ fn print_dry_run_human( classified: &[ClassifiedInput], plan: &BuildPlan, ) { + let target = target_triple_for_global(global); + let target_options = target_options_for(&target, &global.llvm) + .expect("dry-run requires validated target options"); println!("DRY-RUN PLAN"); println!(" mode: {}", build_mode_label(build)); + println!(" target: {}", target); + println!(" cpu: {}", target_options.cpu); + println!(" features: {}", target_options.features); + println!(" abi: {}", target_options.abi.as_deref().unwrap_or("")); + println!(" isa: {}", target_options.isa.as_deref().unwrap_or("")); println!(" emit: {}", render_emit_spec(&build.emit)); println!(" link-only: {}", build.link_only); println!(" run: {}", build.run); @@ -3044,6 +3218,9 @@ fn print_dry_run_json( classified: &[ClassifiedInput], plan: &BuildPlan, ) { + let target = target_triple_for_global(global); + let target_options = target_options_for(&target, &global.llvm) + .expect("dry-run requires validated target options"); let mut text = String::new(); text.push('{'); @@ -3051,10 +3228,26 @@ fn print_dry_run_json( text.push(','); append_json_field(&mut text, "mode", &json_string(build_mode_label(build))); text.push(','); + append_json_field(&mut text, "target", &json_string(&target)); + text.push(','); + append_json_field(&mut text, "cpu", &json_string(&target_options.cpu)); + text.push(','); append_json_field( &mut text, - "target", - &json_string(&target_triple_for_global(global)), + "features", + &json_string(&target_options.features), + ); + text.push(','); + append_json_field( + &mut text, + "abi", + &json_optional_string(target_options.abi.as_deref()), + ); + text.push(','); + append_json_field( + &mut text, + "isa", + &json_optional_string(target_options.isa.as_deref()), ); text.push(','); append_json_field( @@ -3463,7 +3656,10 @@ struct TargetSpecInfo { vendor: Option, os: Option, env: Option, + cpu: String, + features: String, abi: Option, + isa: Option, object_format: &'static str, hosted: bool, supported: bool, @@ -3472,14 +3668,24 @@ struct TargetSpecInfo { fn target_spec_info(global: &Global, target: &str) -> TargetSpecInfo { let spec = target_spec_for_triple(target) .expect("target spec rendering requires a validated target triple"); + let effective = resolve_target_options( + spec, + global.llvm.cpu.as_deref(), + global.llvm.features.as_deref(), + global.llvm.abi.as_deref(), + ) + .expect("target spec rendering requires validated target options"); TargetSpecInfo { triple: target.to_string(), - arch: spec.arch.to_string(), + arch: spec.architecture.name().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(), + cpu: effective.cpu, + features: effective.features, + abi: effective.abi, + isa: effective.isa, object_format: spec.object_format, hosted: spec.hosted, supported: true, @@ -3494,7 +3700,10 @@ fn print_target_spec_human(global: &Global, target: &str) { println!("vendor: {}", spec.vendor.as_deref().unwrap_or("")); println!("os: {}", spec.os.as_deref().unwrap_or("")); println!("env: {}", spec.env.as_deref().unwrap_or("")); + println!("cpu: {}", spec.cpu); + println!("features: {}", spec.features); println!("abi: {}", spec.abi.as_deref().unwrap_or("")); + println!("isa: {}", spec.isa.as_deref().unwrap_or("")); println!("object-format: {}", spec.object_format); println!("hosted: {}", spec.hosted); println!("freestanding: {}", !spec.hosted); @@ -3524,8 +3733,14 @@ fn target_spec_json(global: &Global, target: &str) -> String { out.push(','); append_json_field(&mut out, "env", &json_optional_string(spec.env.as_deref())); out.push(','); + append_json_field(&mut out, "cpu", &json_string(&spec.cpu)); + out.push(','); + append_json_field(&mut out, "features", &json_string(&spec.features)); + out.push(','); append_json_field(&mut out, "abi", &json_optional_string(spec.abi.as_deref())); out.push(','); + append_json_field(&mut out, "isa", &json_optional_string(spec.isa.as_deref())); + out.push(','); append_json_field(&mut out, "object_format", &json_string(spec.object_format)); out.push(','); append_json_field( @@ -3589,128 +3804,36 @@ fn ensure_supported_target(target: &str) -> Result<&'static TargetSpec, CliError }) } -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 validate_target_options_for(target: &str, llvm: &LlvmFlags) -> Result<(), CliError> { +fn target_options_for(target: &str, llvm: &LlvmFlags) -> Result { 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()) + resolve_target_options( + spec, + llvm.cpu.as_deref(), + llvm.features.as_deref(), + llvm.abi.as_deref(), + ) + .map_err(CliError::usage) } -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 - ))); - } - } +fn resolve_target_configuration(llvm: &mut LlvmFlags) -> Result<(), CliError> { + let target = llvm + .target + .clone() + .ok_or_else(|| CliError::usage("target resolution did not produce a target triple"))?; + let effective = target_options_for(&target, llvm)?; + llvm.cpu = Some(effective.cpu); + llvm.features = if effective.features.is_empty() { + None + } else { + Some(effective.features) + }; + llvm.abi = effective.abi; + llvm.isa = effective.isa; Ok(()) } -fn validate_target_feature_abi_compatibility( - spec: &TargetSpec, - features: Option<&str>, - abi: Option<&str>, -) -> Result<(), CliError> { - if spec.arch != "riscv64" { - return Ok(()); - } - - let disabled = |name: &str| { - features.is_some_and(|values| { - values - .split(',') - .map(str::trim) - .any(|value| value.strip_prefix('-') == Some(name)) - }) - }; - - 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 - ))); - } - - 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(()), - } +fn validate_target_options_for(target: &str, llvm: &LlvmFlags) -> Result<(), CliError> { + target_options_for(target, llvm).map(|_| ()) } fn detect_default_sysroot(target: &str) -> Option { diff --git a/src/flags.rs b/src/flags.rs index 098d4237..8fee895a 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -69,9 +69,11 @@ pub struct DepFlags { #[derive(Default, Clone)] pub struct LlvmFlags { pub target: Option, + pub target_explicit: bool, pub cpu: Option, pub features: Option, pub abi: Option, + pub isa: Option, pub code_model: Option, pub relocation_model: Option, pub sysroot: Option, diff --git a/src/runner.rs b/src/runner.rs index 6ba18e63..8e41c88d 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -33,7 +33,7 @@ fn target_condition_context_for_llvm(llvm: Option<&LlvmFlags>) -> TargetConditio if let Some(opts) = llvm { if let Some(triple) = opts.target.as_deref() { if let Some(spec) = target_spec_for_triple(triple) { - target.arch = Some(spec.arch.to_string()); + target.arch = Some(spec.architecture.name().to_string()); target.os = Some(spec.os.to_string()); target.env = Some(spec.env.to_string()); } @@ -453,6 +453,24 @@ fn classify_codegen_panic(panic_message: &str) -> (&'static str, &'static str, & ); } + if panic_message.contains("asm input register/constraint") + || panic_message.contains("asm output register/constraint") + || panic_message.contains("Invalid clobber token") + || panic_message.contains("asm touches the stack") + || panic_message.contains("asm contains a non-returning branch") + || panic_message.contains("asm stack delta is not balanced") + || panic_message.contains("asm writes the stack pointer") + || panic_message.contains("asm cannot declare both") + || panic_message.contains("conflicts with an input/output operand register") + || panic_message.contains("asm expression cannot declare") + { + return ( + "E3401", + "invalid inline assembly contract", + "use registers valid for the selected target and declare stack, clobber, and control-flow effects explicitly", + ); + } + ( "E9001", "compiler internal error during code generation", @@ -734,8 +752,10 @@ fn emit_codegen_panic_and_exit( err = err.with_note("no precise source span was available for this backend diagnostic"); } - if let Some(loc) = panic_location { - err = err.with_suggestion(format!("compiler panic location: {}", loc)); + if code == "E9001" { + if let Some(loc) = panic_location { + err = err.with_suggestion(format!("compiler panic location: {}", loc)); + } } err.display_auto(); @@ -919,6 +939,7 @@ fn build_backend_options(llvm: &LlvmFlags) -> BackendOptions { cpu: llvm.cpu.clone(), features: llvm.features.clone(), abi: llvm.abi.clone(), + isa: llvm.isa.clone(), code_model: llvm.code_model.clone(), relocation_model: llvm.relocation_model.clone(), sysroot: llvm.sysroot.clone(), diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 37fa8bf9..4a4e33b0 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -111,6 +111,19 @@ fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool { .any(|window| window == needle) } +fn riscv64_elf_flags(path: &Path) -> u32 { + let object = fs::read(path).unwrap(); + assert!( + object.len() >= 52, + "truncated ELF object: {}", + path.display() + ); + assert_eq!(&object[..4], b"\x7fELF", "{}", path.display()); + assert_eq!(object[4], 2, "expected ELF64 object: {}", path.display()); + assert_eq!(u16::from_le_bytes([object[18], object[19]]), 243); + u32::from_le_bytes([object[48], object[49], object[50], object[51]]) +} + fn json_string_for_test(value: &str) -> String { let mut out = String::from("\""); for ch in value.chars() { @@ -128,6 +141,43 @@ fn json_string_for_test(value: &str) -> String { out } +fn json_contains_path_components(json: &str, components: &[&str]) -> bool { + json_contains_path_value(json, &components.join("/")) + || json_contains_path_value(json, &components.join("\\")) +} + +fn json_contains_path_value(json: &str, value: &str) -> bool { + json.contains(value) || json.contains(&value.replace('\\', "\\\\")) +} + +fn write_minimal_elf64_object(path: &Path, machine: u16) { + let mut header = [0_u8; 64]; + header[..4].copy_from_slice(b"\x7fELF"); + header[4] = 2; + header[5] = 1; + header[6] = 1; + header[16..18].copy_from_slice(&1_u16.to_le_bytes()); + header[18..20].copy_from_slice(&machine.to_le_bytes()); + fs::write(path, header).unwrap(); +} + +#[test] +fn json_path_matching_accepts_unix_and_escaped_windows_separators() { + let components = ["crt", "riscv64-unknown-linux-gnu", "crt1.o"]; + assert!(json_contains_path_components( + r#"{"args":["/opt/wave/crt/riscv64-unknown-linux-gnu/crt1.o"]}"#, + &components + )); + assert!(json_contains_path_components( + r#"{"args":["C:\\wave\\crt\\riscv64-unknown-linux-gnu\\crt1.o"]}"#, + &components + )); + assert!(json_contains_path_value( + r#"{"args":["-LD:\\wave\\sysroot\\lib"]}"#, + r#"-LD:\wave\sysroot\lib"# + )); +} + fn run_link_tests_enabled() -> bool { std::env::var_os("WAVE_RUN_LINK_TESTS").is_some() } @@ -958,6 +1008,10 @@ fun main() -> i32 { "{}", plan ); + assert!(plan.contains("\"cpu\":\"generic\""), "{}", plan); + assert!(plan.contains("\"features\":\"\""), "{}", plan); + assert!(plan.contains("\"abi\":null"), "{}", plan); + assert!(plan.contains("\"isa\":null"), "{}", plan); assert!(plan.contains("\"mode\":\"compile-only\""), "{}", plan); assert!(plan.contains("\"emit_kinds\":[\"obj\"]"), "{}", plan); assert!(plan.contains("\"control_mode\":null"), "{}", plan); @@ -1304,8 +1358,8 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { "unsupported target 'x86_64-garbage-linux-gnu'", ), ( - &["--target", "riscv64-unknown-linux-gnu", "--emit=check"][..], - "unsupported target 'riscv64-unknown-linux-gnu'", + &["--target", "riscv64-unknown-linux-musl", "--emit=check"][..], + "unsupported target 'riscv64-unknown-linux-musl'", ), ( &[ @@ -1423,6 +1477,16 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { ][..], "ABI 'lp64f' for target 'riscv64-unknown-none-elf' requires feature 'f'", ), + ( + &[ + "--target", + "riscv64-unknown-linux-gnu", + "--features", + "-zicsr", + "--emit=check", + ][..], + "feature 'f' requires feature 'zicsr'", + ), ] { run_failure(options, expected); } @@ -1461,6 +1525,23 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { ]); assert!(riscv_out.join("main.o").is_file()); + let linux_riscv_out = dir.join("valid-linux-riscv"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + linux_riscv_out.as_os_str(), + ]); + let linux_riscv_object = linux_riscv_out.join("main.o"); + assert!(linux_riscv_object.is_file()); + assert_eq!(riscv64_elf_flags(&linux_riscv_object) & 0x7, 0x5); + let linux_riscv_bytes = fs::read(&linux_riscv_object).unwrap(); + assert!(bytes_contains(&linux_riscv_bytes, b"zicsr")); + assert!(bytes_contains(&linux_riscv_bytes, b"zifencei")); + let (stdout, stderr) = run_wavec_capture([ OsStr::new("print"), OsStr::new("target-spec"), @@ -1471,6 +1552,189 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { assert!(stderr.trim().is_empty(), "{}", stderr); assert!(stdout.contains("\"hosted\":false"), "{}", stdout); assert!(stdout.contains("\"freestanding\":true"), "{}", stdout); + assert!(stdout.contains("\"cpu\":\"generic-rv64\""), "{}", stdout); + assert!( + stdout.contains("\"features\":\"+m,+a,-f,-d,+c,-zicsr,-zifencei\""), + "{}", + stdout + ); + assert!(stdout.contains("\"abi\":\"lp64\""), "{}", stdout); + assert!(stdout.contains("\"isa\":\"rv64imac\""), "{}", stdout); + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("print"), + OsStr::new("target-spec"), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--format=json"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!(stdout.contains("\"hosted\":true"), "{}", stdout); + assert!(stdout.contains("\"freestanding\":false"), "{}", stdout); + assert!( + stdout.contains("\"features\":\"+m,+a,+f,+d,+c,+zicsr,+zifencei\""), + "{}", + stdout + ); + assert!(stdout.contains("\"abi\":\"lp64d\""), "{}", stdout); + assert!(stdout.contains("\"isa\":\"rv64gc\""), "{}", stdout); + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=obj"), + OsStr::new("--dry-run"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!(stdout.contains("\"cpu\":\"generic-rv64\""), "{}", stdout); + assert!( + stdout.contains("\"features\":\"+m,+a,+f,+d,+c,+zicsr,+zifencei\""), + "{}", + stdout + ); + assert!(stdout.contains("\"abi\":\"lp64d\""), "{}", stdout); + assert!(stdout.contains("\"isa\":\"rv64gc\""), "{}", stdout); + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--abi=lp64"), + OsStr::new("--emit=bin"), + OsStr::new("--dry-run"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!( + stdout.contains("--dynamic-linker=/lib/ld-linux-riscv64-lp64.so.1"), + "{}", + stdout + ); + assert!( + !stdout.contains("ld-linux-riscv64-lp64d.so.1"), + "{}", + stdout + ); + for host_path in [ + "/usr/lib64/crt1.o", + "/usr/lib64/crti.o", + "/usr/lib64/crtn.o", + "-L/usr/lib64", + "-L/lib64", + "-L/usr/lib ", + "-L/lib ", + ] { + assert!( + !stdout.contains(host_path), + "cross-target link plan consumed host runtime path '{}':\n{}", + host_path, + stdout + ); + } + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--static"), + OsStr::new("--emit=bin"), + OsStr::new("--dry-run"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!(stdout.contains("-static"), "{}", stdout); + assert!( + !stdout.contains("--dynamic-linker="), + "static link plan must not select a dynamic loader:\n{}", + stdout + ); + assert!( + json_contains_path_components(&stdout, &["crt", "riscv64-unknown-linux-gnu", "crt1.o"]), + "static link plan must retain a CRT entry point:\n{}", + stdout + ); + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--static"), + OsStr::new("--pie"), + OsStr::new("--emit=bin"), + OsStr::new("--dry-run"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!( + stdout.contains("rcrt1.o"), + "static PIE link plan must use the relocatable CRT entry point:\n{}", + stdout + ); +} + +#[test] +fn riscv64_debian_cross_prefix_does_not_double_apply_linker_sysroot() { + let dir = temp_case_dir("riscv64-debian-cross-prefix"); + let source = write_wave(&dir, "main.wave", "fun main() -> i32 { return 0; }\n"); + let sysroot = dir.join("usr").join("riscv64-linux-gnu"); + let runtime = sysroot.join("lib"); + fs::create_dir_all(&runtime).unwrap(); + + for crt in ["crt1.o", "crti.o", "crtn.o"] { + write_minimal_elf64_object(&runtime.join(crt), 243); + } + let runtime_prefix = runtime.to_string_lossy(); + fs::write( + runtime.join("libc.so"), + format!( + "GROUP ( {runtime_prefix}/libc.so.6 {runtime_prefix}/libc_nonshared.a \ + AS_NEEDED ( {runtime_prefix}/ld-linux-riscv64-lp64d.so.1 ) )\n" + ), + ) + .unwrap(); + fs::write( + runtime.join("libm.so"), + format!("GROUP ( {runtime_prefix}/libm.so.6 )\n"), + ) + .unwrap(); + for runtime_file in [ + "libc.so.6", + "libc_nonshared.a", + "libm.so.6", + "ld-linux-riscv64-lp64d.so.1", + ] { + fs::write(runtime.join(runtime_file), []).unwrap(); + } + + let (stdout, stderr) = run_wavec_capture([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--sysroot"), + sysroot.as_os_str(), + OsStr::new("--emit=bin"), + OsStr::new("--dry-run"), + ]); + assert!(stderr.trim().is_empty(), "{}", stderr); + assert!(stdout.contains("--sysroot=/"), "{}", stdout); + assert!( + !stdout.contains(&format!("--sysroot={}", sysroot.display())), + "cross prefix must not be applied twice by ld.lld:\n{}", + stdout + ); + assert!( + json_contains_path_value(&stdout, &format!("-L{}", runtime.display())), + "target runtime search path must remain isolated to the cross prefix:\n{}", + stdout + ); } #[test] @@ -1559,13 +1823,16 @@ fn advertised_target_options_reach_object_codegen_without_backend_diagnostics() for feature in features.lines().filter(|line| !line.is_empty()) { let feature_label = feature.replace(['-', '.'], "_"); for (sign, action) in [("+", "enable"), ("-", "disable")] { + let setting = match (target.starts_with("riscv64-"), feature, sign) { + (true, "f", "-") => "-f,-d".to_string(), + (true, "d", "+") => "+f,+d".to_string(), + (true, "zicsr", "-") => "-f,-d,-zicsr".to_string(), + _ => format!("{sign}{feature}"), + }; build_object( target, &format!("{target_label}_feature_{action}_{feature_label}"), - &[ - OsString::from("--features"), - OsString::from(format!("{sign}{feature}")), - ], + &[OsString::from("--features"), OsString::from(setting)], ); } } @@ -1573,21 +1840,24 @@ fn advertised_target_options_reach_object_codegen_without_backend_diagnostics() #[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"), + for (abi, features, expected_flags) in [ + ("lp64", "+m,+a,-f,-d,+c", 0x1), + ("lp64f", "+m,+a,+f,-d,+c", 0x3), + ("lp64d", "+m,+a,+f,+d,+c", 0x5), ] { - build_object( - "riscv64-unknown-none-elf", - &format!("riscv64_abi_{abi}"), - &[ - OsString::from("--features"), - OsString::from(features), - OsString::from("--abi"), - OsString::from(abi), - ], - ); + for target in ["riscv64-unknown-linux-gnu", "riscv64-unknown-none-elf"] { + let object = build_object( + target, + &format!("{}_abi_{abi}", target.replace('-', "_")), + &[ + OsString::from("--features"), + OsString::from(features), + OsString::from("--abi"), + OsString::from(abi), + ], + ); + assert_eq!(riscv64_elf_flags(&object) & 0x7, expected_flags); + } } } } @@ -1934,6 +2204,7 @@ fun main() { "1:" } } + "#, ); let local_jump_dir = dir.join("local-jump"); @@ -2037,6 +2308,243 @@ fun main() { ); } +#[test] +fn riscv64_inline_asm_enforces_reserved_registers_fprs_and_indirect_jumps() { + let dir = temp_case_dir("riscv64-asm-contract"); + + for reserved in ["sp", "zero"] { + let source = write_wave( + &dir, + &format!("reserved_{}.wave", reserved), + &format!( + r#" +fun bind(value: u64) {{ + asm {{ + "nop" + in("{reserved}") value + }} +}} + +fun main() {{}} +"# + ), + ); + let reserved_out = dir.join(format!("reserved-{}", reserved)); + let error = run_wavec_expect_failure([ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + reserved_out.as_os_str(), + ]); + assert!(error.contains("\"code\":\"E3401\""), "{}", error); + assert!(!error.contains("compiler internal error"), "{}", error); + } + + let float_source = write_wave( + &dir, + "float_register.wave", + r#" +fun consume(value: f64) { + asm { + "fmv.d fa0, fa0" + in("fa0") value + } +} + +fun main() {} +"#, + ); + let float_out = dir.join("float-register"); + run_wavec([ + OsStr::new("build"), + float_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--abi=lp64d"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + float_out.as_os_str(), + ]); + assert!(float_out.join("float_register.o").is_file()); + + let jump_source = write_wave( + &dir, + "jalr.wave", + r#" +fun jump(addr: u64) { + asm { + "jalr x0, 0(a0)" + in("a0") addr + clobber("stack") + } +} + +fun main() {} +"#, + ); + let jump_out = dir.join("jalr-missing-noreturn"); + let error = run_wavec_expect_failure([ + OsStr::new("build"), + jump_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-none-elf"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + jump_out.as_os_str(), + ]); + assert!(error.contains("clobber(\"noreturn\")"), "{}", error); + + let noreturn_source = write_wave( + &dir, + "jalr_noreturn.wave", + r#" +fun jump(addr: u64) { + asm { + "jalr x0, 0(a0)" + in("a0") addr + clobber("stack") + clobber("noreturn") + } +} + +fun main() {} +"#, + ); + let noreturn_out = dir.join("jalr-noreturn"); + run_wavec([ + OsStr::new("build"), + noreturn_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-none-elf"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + noreturn_out.as_os_str(), + ]); + let ir = fs::read_to_string(noreturn_out.join("jalr_noreturn.ll")).unwrap(); + assert!(ir.contains("unreachable"), "{}", ir); +} + +#[test] +fn riscv64_ir_and_bitcode_preserve_target_contract_when_recompiled() { + let dir = temp_case_dir("riscv64-artifact-contract"); + let source = write_wave(&dir, "main.wave", "fun main() -> i32 { return 0; }\n"); + let original = dir.join("original"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--abi=lp64f"), + OsStr::new("--emit=ir,bc,obj"), + OsStr::new("--out-dir"), + original.as_os_str(), + ]); + + let ir = fs::read_to_string(original.join("main.ll")).unwrap(); + assert!(ir.contains("!\"target-abi\", !\"lp64f\""), "{}", ir); + assert!(ir.contains("!\"riscv-isa\""), "{}", ir); + assert!(ir.contains("\"target-cpu\"=\"generic-rv64\""), "{}", ir); + assert!( + ir.contains("\"target-features\"=\"+m,+a,+f,-d,+c,+zicsr,+zifencei\""), + "{}", + ir + ); + + let from_ir = dir.join("from-ir"); + let ir_input = original.join("main.ll"); + run_wavec([ + OsStr::new("build"), + ir_input.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + from_ir.as_os_str(), + ]); + let from_bc = dir.join("from-bc"); + let bitcode_input = original.join("main.bc"); + run_wavec([ + OsStr::new("build"), + bitcode_input.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + from_bc.as_os_str(), + ]); + + for object in [ + original.join("main.o"), + from_ir.join("main.o"), + from_bc.join("main.o"), + ] { + assert_eq!( + riscv64_elf_flags(&object) & 0x7, + 0x3, + "{}", + object.display() + ); + let bytes = fs::read(&object).unwrap(); + assert!(bytes_contains(&bytes, b"zicsr"), "{}", object.display()); + assert!(bytes_contains(&bytes, b"zifencei"), "{}", object.display()); + } +} + +#[test] +fn riscv64_export_c_uses_indirect_aggregate_parameters_and_sret() { + let dir = temp_case_dir("riscv64-export-c-aggregate"); + let source = write_wave( + &dir, + "aggregate.wave", + r#" +struct Triple { + a: u64; + b: u64; + c: u64; +} + +export(c) fun wave_take(value: Triple) -> u64 { + return value.c; +} + +export(c) fun wave_make(a: u64, b: u64, c: u64) -> Triple { + return Triple { a: a, b: b, c: c }; +} + +fun main() -> i32 { + let value: Triple = wave_make(1, 2, 3); + if (wave_take(value) != 3) { + return 1; + } + return 0; +} +"#, + ); + let out = dir.join("out"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + let ir = fs::read_to_string(out.join("aggregate.ll")).unwrap(); + assert!( + ir.contains("define i64 @wave_take(ptr byval(%Triple) align 8"), + "{}", + ir + ); + assert!( + ir.contains("define void @wave_make(ptr sret(%Triple) align 8"), + "{}", + ir + ); + assert!(ir.contains("@__wave_export_impl_wave_take"), "{}", ir); + assert!(ir.contains("@__wave_export_impl_wave_make"), "{}", ir); +} + #[test] fn waveos_boot_smoke_builds_windows_freestanding_coff_object() { let dir = temp_case_dir("waveos-boot-smoke-coff");