From 16a89a9047b55e311f331f6cd8bf8a3819ce2572 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 9 Aug 2026 15:05:51 +0900 Subject: [PATCH 1/3] Define the RISC-V 64 target contract Register riscv64-unknown-linux-gnu alongside the freestanding target and resolve effective CPU, ISA extensions, and LP64/LP64F/LP64D ABI settings before code generation. Apply the resolved ABI to LLVM target machines, expose the effective contract through target-spec and dry-run output, and preserve target CPU, features, ABI, and ISA metadata across LLVM IR and bitcode recompilation. Separate parser and LLVM architecture logic into x86_64, AArch64, and RISC-V modules. Canonicalize architecture aliases for target attributes and centralize per-architecture CPU lists, feature sets, register aliases, inline-assembly dialects, clobbers, register widths, and stack/control-flow analysis. Harden RISC-V inline assembly by rejecting zero, stack, global, and thread-pointer registers as value operands; supporting integer and floating-point ABI register aliases; accounting for all floating-point clobbers; recognizing non-returning jr, tail, ret, and jalr forms; requiring explicit stack and noreturn contracts; and reporting invalid assembly contracts as E3401 user diagnostics instead of internal compiler failures. Add C ABI export wrappers so Wave implementations keep native internal signatures while exported RISC-V functions use byval and sret lowering for large aggregates. Marshal direct, split, by-value, and structure-return values between wrapper and implementation paths and verify internal calls through exported aggregate functions. Make Linux cross-linking target-owned by selecting ABI-specific RISC-V loaders, preventing host CRT and generic library paths from leaking into cross builds, validating CRT ELF machine types, diagnosing incomplete sysroots, retaining CRT files for static links, requiring static archives in static mode, selecting rcrt1.o for static PIE, and avoiding dynamic loaders for static binaries. Add regression coverage for target option matrices, hosted and freestanding defaults, ELF ABI flags, IR and bitcode target preservation, RISC-V inline assembly contracts, C aggregate export lowering, cross-runtime isolation, static link plans, architecture target attributes, and WaveOS cross-codegen behavior. Add a Build Linux riscv64 GitHub Actions job that installs the RISC-V GNU sysroot and QEMU, runs the RISC-V contract tests, builds the release compiler, links an LP64D Linux executable, verifies its ELF machine, flags, and loader, and executes Hello World under qemu-riscv64 with a bounded timeout. Give every platform job a consistent OS and architecture name. --- .github/workflows/rust.yml | 81 ++++++ front/parser/src/arch/aarch64.rs | 17 ++ front/parser/src/arch/mod.rs | 70 +++++ front/parser/src/arch/riscv64.rs | 17 ++ front/parser/src/arch/x86_64.rs | 17 ++ front/parser/src/import.rs | 7 +- front/parser/src/lib.rs | 1 + llvm/src/backend.rs | 7 +- llvm/src/codegen/abi_c.rs | 8 +- llvm/src/codegen/arch/aarch64.rs | 96 ++++++ llvm/src/codegen/arch/mod.rs | 211 ++++++++++++++ llvm/src/codegen/arch/riscv64.rs | 213 ++++++++++++++ llvm/src/codegen/arch/x86_64.rs | 191 ++++++++++++ llvm/src/codegen/ir.rs | 361 +++++++++++++++++++++-- llvm/src/codegen/mod.rs | 1 + llvm/src/codegen/plan.rs | 466 +----------------------------- llvm/src/codegen/target.rs | 358 ++++++++++++++++++++--- llvm/src/expression/rvalue/asm.rs | 21 +- llvm/src/statement/asm.rs | 123 +------- src/cli.rs | 384 ++++++++++++++---------- src/flags.rs | 2 + src/runner.rs | 27 +- tests/codegen_regressions.rs | 452 +++++++++++++++++++++++++++-- 23 files changed, 2294 insertions(+), 837 deletions(-) create mode 100644 front/parser/src/arch/aarch64.rs create mode 100644 front/parser/src/arch/mod.rs create mode 100644 front/parser/src/arch/riscv64.rs create mode 100644 front/parser/src/arch/x86_64.rs create mode 100644 llvm/src/codegen/arch/aarch64.rs create mode 100644 llvm/src/codegen/arch/mod.rs create mode 100644 llvm/src/codegen/arch/riscv64.rs create mode 100644 llvm/src/codegen/arch/x86_64.rs 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..88c3e852 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, @@ -2382,8 +2445,10 @@ fn build_elf_lld_args( } 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 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))); } - 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")); + 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.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 +3090,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 +3181,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 +3191,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, + "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, - "target", - &json_string(&target_triple_for_global(global)), + "isa", + &json_optional_string(target_options.isa.as_deref()), ); text.push(','); append_json_field( @@ -3463,7 +3619,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 +3631,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 +3663,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 +3696,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 +3767,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..3c2542ba 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() { @@ -958,6 +971,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 +1321,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 +1440,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 +1488,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 +1515,130 @@ 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!( + stdout.contains("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] @@ -1559,13 +1727,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 +1744,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 +2108,7 @@ fun main() { "1:" } } + "#, ); let local_jump_dir = dir.join("local-jump"); @@ -2037,6 +2212,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"); From 73b6889ded0ebd07a3667699d4c81f3f88c358d5 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 9 Aug 2026 15:54:10 +0900 Subject: [PATCH 2/3] Fix RISC-V cross-prefix linking in CI Detect Debian-style cross-runtime prefixes whose libc or libm linker scripts reference prefix-qualified absolute paths. Keep Wave's target runtime discovery rooted in the requested prefix while passing the filesystem root to ld.lld, preventing the linker from prepending /usr/riscv64-linux-gnu twice to libc.so dependencies. Add a synthetic RISC-V LP64D cross-prefix regression that provides target CRT objects and GNU linker scripts, verifies the effective linker sysroot, and confirms that target library search paths remain isolated to the cross runtime. Make CRT path assertions accept both Unix separators and JSON-escaped Windows separators so the Windows amd64 Rust test no longer rejects a valid RISC-V static link plan. Validated with cargo fmt, all Rust targets, default Clippy, and the RISC-V-only feature configuration with warnings denied. --- src/cli.rs | 39 +++++++++++++++- tests/codegen_regressions.rs | 89 +++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 88c3e852..932dbe81 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2440,7 +2440,7 @@ 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; @@ -2755,6 +2755,43 @@ fn find_elf_runtime_file_any(target: &str, global: &Global, names: &[&str]) -> 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(); diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 3c2542ba..67628f94 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -141,6 +141,34 @@ fn json_string_for_test(value: &str) -> String { out } +fn json_contains_path_components(json: &str, components: &[&str]) -> bool { + json.contains(&components.join("/")) || json.contains(&components.join("\\\\")) +} + +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 + )); +} + fn run_link_tests_enabled() -> bool { std::env::var_os("WAVE_RUN_LINK_TESTS").is_some() } @@ -1617,7 +1645,7 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { stdout ); assert!( - stdout.contains("crt/riscv64-unknown-linux-gnu/crt1.o"), + json_contains_path_components(&stdout, &["crt", "riscv64-unknown-linux-gnu", "crt1.o"]), "static link plan must retain a CRT entry point:\n{}", stdout ); @@ -1641,6 +1669,65 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { ); } +#[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!( + stdout.contains(&format!("-L{}", runtime.display())), + "target runtime search path must remain isolated to the cross prefix:\n{}", + stdout + ); +} + #[test] fn advertised_target_options_reach_object_codegen_without_backend_diagnostics() { let dir = temp_case_dir("target-option-matrix"); From cbbbcacc9de8341a504343fc9c15561f99fb3760 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 9 Aug 2026 16:12:30 +0900 Subject: [PATCH 3/3] Make cross-prefix tests portable on Windows Normalize path-fragment assertions against both literal paths and JSON-escaped Windows separators. Apply the shared matcher to the RISC-V runtime -L search-path assertion that failed on Windows despite the generated linker plan being correct. Add a direct regression for a JSON-encoded Windows library search path and retain the existing Unix and Windows CRT path coverage. Validated with cargo fmt, cargo test --locked --all-targets, and Clippy with warnings denied. --- tests/codegen_regressions.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 67628f94..4a4e33b0 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -142,7 +142,12 @@ fn json_string_for_test(value: &str) -> String { } fn json_contains_path_components(json: &str, components: &[&str]) -> bool { - json.contains(&components.join("/")) || json.contains(&components.join("\\\\")) + 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) { @@ -167,6 +172,10 @@ fn json_path_matching_accepts_unix_and_escaped_windows_separators() { 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 { @@ -1722,7 +1731,7 @@ fn riscv64_debian_cross_prefix_does_not_double_apply_linker_sysroot() { stdout ); assert!( - stdout.contains(&format!("-L{}", runtime.display())), + json_contains_path_value(&stdout, &format!("-L{}", runtime.display())), "target runtime search path must remain isolated to the cross prefix:\n{}", stdout );