diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml index e613abbe..109a8536 100644 --- a/.github/workflows/changelog.yaml +++ b/.github/workflows/changelog.yaml @@ -27,6 +27,8 @@ jobs: - 'riscv-rt/**' riscv-semihosting: - 'riscv-semihosting/**' + riscv-target-parser: + - 'riscv-target-parser/**' - name: Check for CHANGELOG.md (riscv) if: steps.changes.outputs.riscv == 'true' @@ -43,7 +45,15 @@ jobs: changeLogPath: ./riscv-pac/CHANGELOG.md skipLabels: 'skip changelog' missingUpdateErrorMessage: 'Please add a changelog entry in the riscv-pac/CHANGELOG.md file.' - + + - name: Check for CHANGELOG.md (riscv-peripheral) + if: steps.changes.outputs.riscv-peripheral == 'true' + uses: dangoslen/changelog-enforcer@v3 + with: + changeLogPath: ./riscv-peripheral/CHANGELOG.md + skipLabels: 'skip changelog' + missingUpdateErrorMessage: 'Please add a changelog entry in the riscv-peripheral/CHANGELOG.md file.' + - name: Check for CHANGELOG.md (riscv-rt) if: steps.changes.outputs.riscv-rt == 'true' uses: dangoslen/changelog-enforcer@v3 @@ -60,10 +70,10 @@ jobs: skipLabels: 'skip changelog' missingUpdateErrorMessage: 'Please add a changelog entry in the riscv-semihosting/CHANGELOG.md file.' - - name: Check for CHANGELOG.md (riscv-peripheral) - if: steps.changes.outputs.riscv-peripheral == 'true' + - name: Check for CHANGELOG.md (riscv-target-parser) + if: steps.changes.outputs.riscv-target-parser == 'true' uses: dangoslen/changelog-enforcer@v3 with: - changeLogPath: ./riscv-peripheral/CHANGELOG.md + changeLogPath: ./riscv-target-parser/CHANGELOG.md skipLabels: 'skip changelog' - missingUpdateErrorMessage: 'Please add a changelog entry in the riscv-peripheral/CHANGELOG.md file.' + missingUpdateErrorMessage: 'Please add a changelog entry in the riscv-target-parser/CHANGELOG.md file.' diff --git a/.github/workflows/riscv-target-parser.yaml b/.github/workflows/riscv-target-parser.yaml new file mode 100644 index 00000000..f6f52a39 --- /dev/null +++ b/.github/workflows/riscv-target-parser.yaml @@ -0,0 +1,37 @@ +on: + push: + branches: [ master ] + pull_request: + merge_group: + +name: Run tests (riscv-target-parser) + +jobs: + run-tests: + strategy: + matrix: + os: [ macos-latest, ubuntu-latest, windows-latest ] # windows shows weird linking errors + toolchain: [ stable, nightly, 1.61.0 ] + include: + # Nightly is only for reference and allowed to fail + - rust: nightly + experimental: true + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental || false }} + steps: + - uses: actions/checkout@v4 + - name: Update Rust toolchain + run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + - name: Build + run: cargo build --package riscv-target-parser + - name: Run tests + run: cargo test --package riscv-target-parser + + # Job to check that all the builds succeeded + tests-check: + needs: + - run-tests + runs-on: ubuntu-latest + if: always() + steps: + - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' diff --git a/Cargo.toml b/Cargo.toml index ba28eae6..a10c4830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ members = [ "riscv-peripheral", "riscv-rt", "riscv-semihosting", + "riscv-target-parser", "tests", ] diff --git a/README.md b/README.md index 5bfbda80..3314bbee 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This repository contains various crates useful for writing Rust programs on RISC * [`riscv-peripheral`]: Interfaces for standard RISC-V peripherals * [`riscv-rt`]: Startup code and interrupt handling * [`riscv-semihosting`]: Semihosting for RISC-V processors +* [`riscv-target-parser`]: Utility crate for parsing RISC-V targets in build scripts This project is developed and maintained by the [RISC-V team][team]. @@ -27,5 +28,6 @@ to intervene to uphold that code of conduct. [`riscv-peripheral`]: https://crates.io/crates/riscv-peripheral [`riscv-rt`]: https://crates.io/crates/riscv-rt [`riscv-semihosting`]: https://crates.io/crates/riscv-semihosting +[`riscv-target-parser`]: https://crates.io/crates/riscv-target-parser [team]: https://github.com/rust-embedded/wg#the-risc-v-team [CoC]: CODE_OF_CONDUCT.md diff --git a/riscv-rt/Cargo.toml b/riscv-rt/Cargo.toml index d5973f33..92f4055e 100644 --- a/riscv-rt/Cargo.toml +++ b/riscv-rt/Cargo.toml @@ -19,6 +19,9 @@ targets = [ "riscv64imac-unknown-none-elf", "riscv64gc-unknown-none-elf", ] +[build-dependencies] +riscv-target-parser = { path = "../riscv-target-parser", version = "0.1.0" } + [dependencies] riscv = { path = "../riscv", version = "0.12.0" } riscv-pac = { path = "../riscv-pac", version = "0.2.0" } diff --git a/riscv-rt/build.rs b/riscv-rt/build.rs index 1d787a98..59be6a7b 100644 --- a/riscv-rt/build.rs +++ b/riscv-rt/build.rs @@ -1,6 +1,7 @@ // NOTE: Adapted from cortex-m/build.rs -use std::{collections::HashSet, env, fs, io, path::PathBuf}; +use riscv_target_parser::RiscvTarget; +use std::{env, fs, io, path::PathBuf}; fn add_linker_script(arch_width: u32) -> io::Result<()> { // Read the file to a string and replace all occurrences of ${ARCH_WIDTH} with the arch width @@ -17,96 +18,28 @@ fn add_linker_script(arch_width: u32) -> io::Result<()> { Ok(()) } -/// Parse the target RISC-V architecture and returns its bit width and the extension set -fn parse_target(target: &str, cargo_flags: &str) -> (u32, HashSet) { - // isolate bit width and extensions from the rest of the target information - let arch = target - .trim_start_matches("riscv") - .split('-') - .next() - .unwrap(); - - let bits = arch - .chars() - .take_while(|c| c.is_ascii_digit()) - .collect::() - .parse::() - .unwrap(); - - let mut extensions: HashSet = arch.chars().skip_while(|c| c.is_ascii_digit()).collect(); - // expand the 'g' shorthand extension - if extensions.contains(&'g') { - extensions.insert('i'); - extensions.insert('m'); - extensions.insert('a'); - extensions.insert('f'); - extensions.insert('d'); - } - - let cargo_flags = cargo_flags - .split(0x1fu8 as char) - .filter(|arg| !arg.is_empty()); - - cargo_flags - .filter(|k| k.starts_with("target-feature=")) - .flat_map(|str| { - let flags = str.split('=').collect::>()[1]; - flags.split(',') - }) - .for_each(|feature| { - let chars = feature.chars().collect::>(); - match chars[0] { - '+' => { - extensions.insert(chars[1]); - } - '-' => { - extensions.remove(&chars[1]); - } - _ => { - panic!("Unsupported target feature operation"); - } - } - }); - - (bits, extensions) -} - fn main() { - println!("cargo:rustc-check-cfg=cfg(riscv)"); - println!("cargo:rustc-check-cfg=cfg(riscv32)"); - println!("cargo:rustc-check-cfg=cfg(riscv64)"); + // Required until target_feature risc-v is stable and in-use (rust 1.75) for ext in ['i', 'e', 'm', 'a', 'f', 'd', 'g', 'c'] { println!("cargo:rustc-check-cfg=cfg(riscv{})", ext); } let target = env::var("TARGET").unwrap(); let cargo_flags = env::var("CARGO_ENCODED_RUSTFLAGS").unwrap(); - let _name = env::var("CARGO_PKG_NAME").unwrap(); - - // set configuration flags depending on the target - if target.starts_with("riscv") { - println!("cargo:rustc-cfg=riscv"); - // This is required until target_arch & target_feature risc-v work is - // stable and in-use (rust 1.75.0) - let (bits, extensions) = parse_target(&target, &cargo_flags); - // generate the linker script and expose the ISA width - let arch_width = match bits { - 32 => { - println!("cargo:rustc-cfg=riscv32"); - 4 - } - 64 => { - println!("cargo:rustc-cfg=riscv64"); - 8 - } - _ => panic!("Unsupported bit width"), - }; - add_linker_script(arch_width).unwrap(); - - // expose the ISA extensions - for ext in &extensions { - println!("cargo:rustc-cfg=riscv{}", ext); + if let Ok(target) = RiscvTarget::build(&target, &cargo_flags) { + let width = target.width(); + if matches!(width, riscv_target_parser::Width::W128) { + panic!("Unsupported RISC-V target: {width}"); + } + if target.base_extension().is_none() { + panic!("Unsupported RISC-V target: no base extension"); + } + for flag in target.rustc_flags() { + // Required until target_feature risc-v is stable and in-use + println!("cargo:rustc-check-cfg=cfg({flag})"); + println!("cargo:rustc-cfg={flag}"); } + add_linker_script(width.into()).unwrap(); } } diff --git a/riscv-rt/src/asm.rs b/riscv-rt/src/asm.rs index 1969c4d4..fbf87448 100644 --- a/riscv-rt/src/asm.rs +++ b/riscv-rt/src/asm.rs @@ -31,11 +31,11 @@ macro_rules! cfg_global_asm { // - https://github.com/llvm/llvm-project/issues/61991 cfg_global_asm!( "// Provisional patch to avoid LLVM spurious errors when compiling in release mode.", - #[cfg(all(riscv32, riscvm))] + #[cfg(all(target_arch = "riscv32", riscvm))] ".attribute arch, \"rv32im\"", - #[cfg(all(riscv64, riscvm, not(riscvg)))] + #[cfg(all(target_arch = "riscv64", riscvm, not(riscvg)))] ".attribute arch, \"rv64im\"", - #[cfg(all(riscv64, riscvg))] + #[cfg(all(target_arch = "riscv64", riscvg))] ".attribute arch, \"rv64g\"", ); @@ -47,10 +47,10 @@ cfg_global_asm!( .global _start _start:", - #[cfg(riscv32)] + #[cfg(target_arch = "riscv32")] "lui ra, %hi(_abs_start) jr %lo(_abs_start)(ra)", - #[cfg(riscv64)] + #[cfg(target_arch = "riscv64")] ".option push .option norelax // to prevent an unsupported R_RISCV_ALIGN relocation from being generated 1: @@ -84,7 +84,9 @@ _abs_start: // ZERO OUT GENERAL-PURPOSE REGISTERS riscv_rt_macros::loop_global_asm!(" li x{}, 0", 1, 10); // a0..a2 (x10..x12) skipped -riscv_rt_macros::loop_global_asm!(" li x{}, 0", 13, 32); +riscv_rt_macros::loop_global_asm!(" li x{}, 0", 13, 16); +#[cfg(not(riscve))] +riscv_rt_macros::loop_global_asm!(" li x{}, 0", 16, 32); // INITIALIZE GLOBAL POINTER, STACK POINTER, AND FRAME POINTER cfg_global_asm!( @@ -125,12 +127,12 @@ cfg_global_asm!( // STORE A0..A2 IN THE STACK, AS THEY WILL BE NEEDED LATER BY main cfg_global_asm!( - #[cfg(riscv32)] + #[cfg(target_arch = "riscv32")] "addi sp, sp, -4 * 3 sw a0, 4 * 0(sp) sw a1, 4 * 1(sp) sw a2, 4 * 2(sp)", - #[cfg(riscv64)] + #[cfg(target_arch = "riscv64")] "addi sp, sp, -8 * 3 sd a0, 8 * 0(sp) sd a1, 8 * 1(sp) @@ -202,9 +204,9 @@ cfg_global_asm!( "fscsr x0", ); // ZERO OUT FLOATING POINT REGISTERS -#[cfg(all(riscv32, riscvd))] +#[cfg(all(target_arch = "riscv32", riscvd))] riscv_rt_macros::loop_global_asm!(" fcvt.d.w f{}, x0", 32); -#[cfg(all(riscv64, riscvd))] +#[cfg(all(target_arch = "riscv64", riscvd))] riscv_rt_macros::loop_global_asm!(" fmv.d.x f{}, x0", 32); #[cfg(all(riscvf, not(riscvd)))] riscv_rt_macros::loop_global_asm!(" fmv.w.x f{}, x0", 32); @@ -212,12 +214,12 @@ riscv_rt_macros::loop_global_asm!(" fmv.w.x f{}, x0", 32); // SET UP INTERRUPTS, RESTORE a0..a2, AND JUMP TO MAIN RUST FUNCTION cfg_global_asm!( "call _setup_interrupts", - #[cfg(riscv32)] + #[cfg(target_arch = "riscv32")] "lw a0, 4 * 0(sp) lw a1, 4 * 1(sp) lw a2, 4 * 2(sp) addi sp, sp, 4 * 3", - #[cfg(riscv64)] + #[cfg(target_arch = "riscv64")] "ld a0, 8 * 0(sp) ld a1, 8 * 1(sp) ld a2, 8 * 2(sp) @@ -276,14 +278,14 @@ _pre_init_trap: j _pre_init_trap", ); -#[cfg(riscv32)] +#[cfg(target_arch = "riscv32")] riscv_rt_macros::weak_start_trap_riscv32!(); -#[cfg(riscv64)] +#[cfg(target_arch = "riscv64")] riscv_rt_macros::weak_start_trap_riscv64!(); -#[cfg(all(riscv32, feature = "v-trap"))] +#[cfg(all(target_arch = "riscv32", feature = "v-trap"))] riscv_rt_macros::vectored_interrupt_trap_riscv32!(); -#[cfg(all(riscv64, feature = "v-trap"))] +#[cfg(all(target_arch = "riscv64", feature = "v-trap"))] riscv_rt_macros::vectored_interrupt_trap_riscv64!(); #[rustfmt::skip] diff --git a/riscv-rt/src/interrupts.rs b/riscv-rt/src/interrupts.rs index 6fe900e5..b7ab89a2 100644 --- a/riscv-rt/src/interrupts.rs +++ b/riscv-rt/src/interrupts.rs @@ -71,7 +71,10 @@ pub unsafe extern "C" fn _dispatch_core_interrupt(code: usize) { } // In vectored mode, we also must provide a vector table -#[cfg(all(riscv, feature = "v-trap"))] +#[cfg(all( + any(target_arch = "riscv32", target_arch = "riscv64"), + feature = "v-trap" +))] core::arch::global_asm!( r#" .section .trap, "ax" .weak _vector_table diff --git a/riscv-rt/src/lib.rs b/riscv-rt/src/lib.rs index 68ca2a6c..e550b229 100644 --- a/riscv-rt/src/lib.rs +++ b/riscv-rt/src/lib.rs @@ -532,7 +532,7 @@ #![no_std] #![deny(missing_docs)] -#[cfg(riscv)] +#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] mod asm; #[cfg(not(feature = "no-exceptions"))] @@ -551,11 +551,11 @@ pub use riscv_rt_macros::{entry, exception, external_interrupt, pre_init}; pub use riscv_pac::*; -#[cfg(riscv32)] +#[cfg(target_arch = "riscv32")] pub use riscv_rt_macros::core_interrupt_riscv32 as core_interrupt; -#[cfg(riscv64)] +#[cfg(target_arch = "riscv64")] pub use riscv_rt_macros::core_interrupt_riscv64 as core_interrupt; -#[cfg(not(riscv))] +#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] pub use riscv_rt_macros::core_interrupt_riscv64 as core_interrupt; // just for docs, tests, etc. /// We export this static with an informative name so that if an application attempts to link @@ -579,12 +579,16 @@ pub struct TrapFrame { /// `x7`: temporary register `t2`, used for intermediate values. pub t2: usize, /// `x28`: temporary register `t3`, used for intermediate values. + #[cfg(not(riscve))] pub t3: usize, /// `x29`: temporary register `t4`, used for intermediate values. + #[cfg(not(riscve))] pub t4: usize, /// `x30`: temporary register `t5`, used for intermediate values. + #[cfg(not(riscve))] pub t5: usize, /// `x31`: temporary register `t6`, used for intermediate values. + #[cfg(not(riscve))] pub t6: usize, /// `x10`: argument register `a0`. Used to pass the first argument to a function. pub a0: usize, @@ -598,10 +602,16 @@ pub struct TrapFrame { pub a4: usize, /// `x15`: argument register `a5`. Used to pass the sixth argument to a function. pub a5: usize, + #[cfg(not(riscve))] /// `x16`: argument register `a6`. Used to pass the seventh argument to a function. pub a6: usize, + #[cfg(not(riscve))] /// `x17`: argument register `a7`. Used to pass the eighth argument to a function. pub a7: usize, + #[cfg(riscve)] + _reserved0: usize, + #[cfg(riscve)] + _reserved1: usize, } /// Trap entry point rust (_start_trap_rust) @@ -634,7 +644,10 @@ pub struct TrapFrame { /// /// This function must be called only from assembly `_start_trap` function. /// Do **NOT** call this function directly. -#[cfg_attr(riscv, link_section = ".trap.rust")] +#[cfg_attr( + any(target_arch = "riscv32", target_arch = "riscv64"), + link_section = ".trap.rust" +)] #[export_name = "_start_trap_rust"] pub unsafe extern "C" fn start_trap_rust(trap_frame: *const TrapFrame) { extern "C" { diff --git a/riscv-target-parser/CHANGELOG.md b/riscv-target-parser/CHANGELOG.md new file mode 100644 index 00000000..4a26855b --- /dev/null +++ b/riscv-target-parser/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + diff --git a/riscv-target-parser/Cargo.toml b/riscv-target-parser/Cargo.toml new file mode 100644 index 00000000..1035521e --- /dev/null +++ b/riscv-target-parser/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "riscv-target-parser" +version = "0.1.0" +rust-version = "1.61" +repository = "https://github.com/rust-embedded/riscv" +authors = ["The RISC-V Team "] +categories = ["embedded", "no-std"] +description = "Parser for RISC-V target specifications" +documentation = "https://docs.rs/riscv-target-parser" +keywords = ["riscv", "runtime", "startup"] +license = "ISC" +edition = "2021" diff --git a/riscv-target-parser/README.md b/riscv-target-parser/README.md new file mode 100644 index 00000000..0e8a8f04 --- /dev/null +++ b/riscv-target-parser/README.md @@ -0,0 +1,40 @@ +[![crates.io](https://img.shields.io/crates/d/riscv-target-parser.svg)](https://crates.io/crates/riscv-target-parser) +[![crates.io](https://img.shields.io/crates/v/riscv-target-parser.svg)](https://crates.io/crates/riscv-target-parser) + +# `riscv-target-parser` + +> Utility crate for parsing RISC-V targets in build scripts + +This project is developed and maintained by the [RISC-V team][team]. + +## [Documentation](https://docs.rs/crate/riscv-target-parser) + +## Minimum Supported Rust Version (MSRV) + +This crate is guaranteed to compile on stable Rust 1.61 and up. It *might* +compile with older versions but that may change in any new patch release. + +## License + +Copyright 2024-2025 [RISC-V team][team] + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +## Code of Conduct + +Contribution to this crate is organized under the terms of the [Rust Code of +Conduct][CoC], the maintainer of this crate, the [RISC-V team][team], promises +to intervene to uphold that code of conduct. + +[CoC]: CODE_OF_CONDUCT.md +[team]: https://github.com/rust-embedded/wg#the-risc-v-team diff --git a/riscv-target-parser/src/extension.rs b/riscv-target-parser/src/extension.rs new file mode 100644 index 00000000..16afdfc9 --- /dev/null +++ b/riscv-target-parser/src/extension.rs @@ -0,0 +1,494 @@ +use crate::Error; +use std::collections::HashSet; + +/// RISC-V standard extensions +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Extension { + /// Base Integer Instruction Set + I, + /// Base Integer Instruction Set (embedded, only 16 registers) + E, + /// Integer Multiplication and Division + M, + /// Atomic Instructions + A, + /// Single-Precision Floating-Point + F, + /// Double-Precision Floating-Point + D, + /// Quad-Precision Floating-Point + Q, + /// Compressed Instructions + C, + /// Bit Manipulation + B, + /// Packed-SIMD Instructions + P, + /// Vector Operations + V, + /// Hypervisor + H, + /// Standard Z-type extension + Z(String), + /// Standard S-type extension + S(String), + /// Vendor extension + X(String), +} + +impl Extension { + /// Determines if the extension is a base extension. + pub const fn is_base(&self) -> bool { + matches!(self, Self::I | Self::E) + } +} + +impl std::fmt::Display for Extension { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let repr = match self { + Self::I => "i", + Self::E => "e", + Self::M => "m", + Self::A => "a", + Self::F => "f", + Self::D => "d", + Self::Q => "q", + Self::C => "c", + Self::B => "b", + Self::P => "p", + Self::V => "v", + Self::H => "h", + Self::Z(s) | Self::S(s) | Self::X(s) => s, + }; + write!(f, "{repr}") + } +} + +impl<'a> TryFrom<&'a str> for Extension { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + match value { + "i" => Ok(Extension::I), + "e" => Ok(Extension::E), + "m" => Ok(Extension::M), + "a" => Ok(Extension::A), + "f" => Ok(Extension::F), + "d" => Ok(Extension::D), + "q" => Ok(Extension::Q), + "c" => Ok(Extension::C), + "b" => Ok(Extension::B), + "p" => Ok(Extension::P), + "v" => Ok(Extension::V), + "h" => Ok(Extension::H), + _ => { + if value.starts_with('Z') { + Ok(Extension::Z(value.to_string())) + } else if value.starts_with('S') { + Ok(Extension::S(value.to_string())) + } else if value.starts_with('X') { + Ok(Extension::X(value.to_string())) + } else { + Err(Self::Error::UnknownExtension(value)) + } + } + } + } +} + +/// Collection of RISC-V extensions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Extensions { + extensions: HashSet, +} + +impl Extensions { + /// Returns a vector with the list of extensions. Extensions are sorted in canonical order. + /// + /// The canonical order is defined as follows: + /// 1. Base ISA (I or E) + /// 2. Standard non-base extensions (M, A, F, D, Q, C, B, P, V, H) + /// 3. Standard Z-type extensions (e.g., Zicsr) + /// 4. Standard S-type extensions (e.g., Ssccfg) + /// 5. Vendor X-type extensions (e.g., XSifivecdiscarddlone) + /// + /// Z, S, and X-type extensions are sorted by their string representation. + pub fn extensions(&self) -> Vec { + let mut res = self.extensions.iter().cloned().collect::>(); + res.sort(); + res + } + + /// Returns the base extension (I or E) if present. + pub fn base_extension(&self) -> Option { + if self.extensions.contains(&Extension::I) { + Some(Extension::I) + } else if self.extensions.contains(&Extension::E) { + Some(Extension::E) + } else { + None + } + } + + /// Returns `true` if the collection contains the given extension. + pub fn contains(&self, extension: &Extension) -> bool { + self.extensions.contains(extension) + } + + pub fn is_g(&self) -> bool { + self.extensions.contains(&Extension::I) + && self.extensions.contains(&Extension::M) + && self.extensions.contains(&Extension::A) + && self.extensions.contains(&Extension::F) + && self.extensions.contains(&Extension::D) + } + + /// Adds an extension to the collection. Returns `true` if the extension was not present. + pub fn insert(&mut self, extension: Extension) -> bool { + self.extensions.insert(extension) + } + + /// Removes an extension from the collection. Returns `true` if the extension was present. + pub fn remove(&mut self, extension: &Extension) -> bool { + self.extensions.remove(extension) + } +} + +impl<'a> TryFrom<&'a str> for Extensions { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + let mut value = value; + let mut extensions = HashSet::new(); + + while !value.is_empty() { + let extension = + if value.starts_with("Z") || value.starts_with("S") || value.starts_with("X") { + match value.find('_') { + Some(pos) => { + let (ext, _) = value.split_at(pos); + ext + } + None => value, + } + } else { + &value[0..1] // single character extension + }; + value = value.trim_start_matches(extension).trim_start_matches("_"); + + match Extension::try_from(extension) { + Ok(ext) => { + extensions.insert(ext); + } + Err(Self::Error::UnknownExtension(ext)) => { + if ext == "g" { + // G is a shorthand for IMAFD + extensions.insert(Extension::I); + extensions.insert(Extension::M); + extensions.insert(Extension::A); + extensions.insert(Extension::F); + extensions.insert(Extension::D); + } else { + return Err(Self::Error::UnknownExtension(ext)); + } + } + _ => unreachable!(), + } + } + Ok(Extensions { extensions }) + } +} + +impl std::fmt::Display for Extensions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut extensions = String::new(); + let mut prev_zsx = false; + for ext in &self.extensions() { + if prev_zsx { + extensions.push('_'); + } + extensions.push_str(ext.to_string().as_str()); + prev_zsx = matches!(ext, Extension::Z(_) | Extension::S(_) | Extension::X(_)); + } + match extensions.strip_prefix("imafd") { + Some(extensions) => write!(f, "g{}", extensions), + None => match extensions.strip_prefix("iemafd") { + Some(extensions) => write!(f, "ge{}", extensions), + None => write!(f, "{}", extensions), + }, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_extension_try_from() { + assert_eq!(Extension::try_from("i"), Ok(Extension::I)); + assert_eq!(Extension::try_from("e"), Ok(Extension::E)); + assert_eq!(Extension::try_from("m"), Ok(Extension::M)); + assert_eq!(Extension::try_from("a"), Ok(Extension::A)); + assert_eq!(Extension::try_from("f"), Ok(Extension::F)); + assert_eq!(Extension::try_from("d"), Ok(Extension::D)); + assert_eq!(Extension::try_from("q"), Ok(Extension::Q)); + assert_eq!(Extension::try_from("c"), Ok(Extension::C)); + assert_eq!(Extension::try_from("b"), Ok(Extension::B)); + assert_eq!(Extension::try_from("p"), Ok(Extension::P)); + assert_eq!(Extension::try_from("v"), Ok(Extension::V)); + assert_eq!(Extension::try_from("h"), Ok(Extension::H)); + assert_eq!( + Extension::try_from("Zicsr"), + Ok(Extension::Z("Zicsr".to_string())) + ); + assert_eq!( + Extension::try_from("Ssccfg"), + Ok(Extension::S("Ssccfg".to_string())) + ); + assert_eq!( + Extension::try_from("XSifivecdiscarddlone"), + Ok(Extension::X("XSifivecdiscarddlone".to_string())) + ); + assert_eq!( + Extension::try_from("unknown"), + Err(Error::UnknownExtension("unknown")) + ); + } + + #[test] + fn test_extension_to_string() { + assert_eq!(Extension::I.to_string(), "i"); + assert_eq!(Extension::E.to_string(), "e"); + assert_eq!(Extension::M.to_string(), "m"); + assert_eq!(Extension::A.to_string(), "a"); + assert_eq!(Extension::F.to_string(), "f"); + assert_eq!(Extension::D.to_string(), "d"); + assert_eq!(Extension::Q.to_string(), "q"); + assert_eq!(Extension::C.to_string(), "c"); + assert_eq!(Extension::B.to_string(), "b"); + assert_eq!(Extension::P.to_string(), "p"); + assert_eq!(Extension::V.to_string(), "v"); + assert_eq!(Extension::H.to_string(), "h"); + assert_eq!(Extension::Z("Zicsr".to_string()).to_string(), "Zicsr"); + assert_eq!(Extension::S("Ssccfg".to_string()).to_string(), "Ssccfg"); + assert_eq!( + Extension::X("XSifivecdiscarddlone".to_string()).to_string(), + "XSifivecdiscarddlone" + ); + } + + #[test] + fn test_extension_cmp() { + let mut extensions = vec![ + Extension::I, + Extension::M, + Extension::A, + Extension::F, + Extension::D, + Extension::Q, + Extension::C, + Extension::B, + Extension::P, + Extension::V, + Extension::H, + Extension::Z("Zicsr".to_string()), + Extension::S("Ssccfg".to_string()), + Extension::X("XSifivecdiscarddlone".to_string()), + ]; + extensions.reverse(); + extensions.sort(); + assert_eq!( + extensions, + vec![ + Extension::I, + Extension::M, + Extension::A, + Extension::F, + Extension::D, + Extension::Q, + Extension::C, + Extension::B, + Extension::P, + Extension::V, + Extension::H, + Extension::Z("Zicsr".to_string()), + Extension::S("Ssccfg".to_string()), + Extension::X("XSifivecdiscarddlone".to_string()), + ] + ); + } + + #[test] + fn test_extensions_try_from() { + let mut try_extensions = Extensions::try_from(""); + assert!(try_extensions.is_ok()); + let mut extensions = try_extensions.unwrap(); + assert!(extensions.extensions().is_empty()); + assert!(extensions.base_extension().is_none()); + + try_extensions = + Extensions::try_from("giemafdqcbpvhXSifivecdiscarddlone_Ssccfg_Zicsr_Zaamo_u"); + assert!(try_extensions.is_err()); + assert_eq!(try_extensions, Err(Error::UnknownExtension("u"))); + + try_extensions = Extensions::try_from("geqcbpvhXSifivecdiscarddlone_Ssccfg_Zicsr_Zaamo_"); + assert!(try_extensions.is_ok()); + extensions = try_extensions.unwrap(); + assert_eq!( + extensions.extensions(), + vec![ + Extension::I, + Extension::E, + Extension::M, + Extension::A, + Extension::F, + Extension::D, + Extension::Q, + Extension::C, + Extension::B, + Extension::P, + Extension::V, + Extension::H, + Extension::Z("Zaamo".to_string()), + Extension::Z("Zicsr".to_string()), + Extension::S("Ssccfg".to_string()), + Extension::X("XSifivecdiscarddlone".to_string()), + ] + ); + assert_eq!(extensions.base_extension(), Some(Extension::I)); + + try_extensions = + Extensions::try_from("iemafdqcbpvhXSifivecdiscarddlone_Ssccfg_Zicsr_Zaamo_"); + assert!(try_extensions.is_ok()); + extensions = try_extensions.unwrap(); + assert_eq!( + extensions.extensions(), + vec![ + Extension::I, + Extension::E, + Extension::M, + Extension::A, + Extension::F, + Extension::D, + Extension::Q, + Extension::C, + Extension::B, + Extension::P, + Extension::V, + Extension::H, + Extension::Z("Zaamo".to_string()), + Extension::Z("Zicsr".to_string()), + Extension::S("Ssccfg".to_string()), + Extension::X("XSifivecdiscarddlone".to_string()), + ] + ); + assert_eq!(extensions.base_extension(), Some(Extension::I)); + + try_extensions = + Extensions::try_from("emafdqcbpvhXSifivecdiscarddlone_Ssccfg_Zicsr_Zaamo_"); + assert!(try_extensions.is_ok()); + extensions = try_extensions.unwrap(); + assert_eq!( + extensions.extensions(), + vec![ + Extension::E, + Extension::M, + Extension::A, + Extension::F, + Extension::D, + Extension::Q, + Extension::C, + Extension::B, + Extension::P, + Extension::V, + Extension::H, + Extension::Z("Zaamo".to_string()), + Extension::Z("Zicsr".to_string()), + Extension::S("Ssccfg".to_string()), + Extension::X("XSifivecdiscarddlone".to_string()), + ] + ); + assert_eq!(extensions.base_extension(), Some(Extension::E)); + } + + #[test] + fn test_extensions_insert_remove() { + let mut extensions = Extensions::try_from("gc").unwrap(); + + assert_eq!(extensions.extensions.len(), 6); + assert!(extensions.contains(&Extension::I)); + assert!(extensions.contains(&Extension::M)); + assert!(extensions.contains(&Extension::A)); + assert!(extensions.contains(&Extension::F)); + assert!(extensions.contains(&Extension::D)); + assert!(extensions.contains(&Extension::C)); + assert!(!extensions.contains(&Extension::E)); + assert!(!extensions.contains(&Extension::Q)); + assert_eq!(extensions.base_extension(), Some(Extension::I)); + + assert!(!extensions.insert(Extension::I)); + assert!(!extensions.remove(&Extension::E)); + assert_eq!(extensions.extensions.len(), 6); + + assert!(extensions.insert(Extension::E)); + assert_eq!(extensions.extensions.len(), 7); + assert!(extensions.contains(&Extension::E)); + assert_eq!(extensions.base_extension(), Some(Extension::I)); + + assert!(extensions.remove(&Extension::I)); + assert_eq!(extensions.extensions.len(), 6); + assert!(!extensions.contains(&Extension::I)); + assert_eq!(extensions.base_extension(), Some(Extension::E)); + + assert!(extensions.remove(&Extension::E)); + assert_eq!(extensions.extensions.len(), 5); + assert!(!extensions.contains(&Extension::E)); + assert_eq!(extensions.base_extension(), None); + } + + #[test] + fn test_extensions_to_string() { + let mut extensions = Extensions::try_from("imafdc").unwrap(); + assert_eq!(extensions.to_string(), "gc"); + + extensions.insert(Extension::try_from("Ssccfg").unwrap()); + assert_eq!(extensions.to_string(), "gcSsccfg"); + + extensions.insert(Extension::try_from("Zicsr").unwrap()); + assert_eq!(extensions.to_string(), "gcZicsr_Ssccfg"); + + extensions.insert(Extension::try_from("Zaamo").unwrap()); + assert_eq!(extensions.to_string(), "gcZaamo_Zicsr_Ssccfg"); + + extensions.insert(Extension::try_from("XSifivecdiscarddlone").unwrap()); + assert_eq!( + extensions.to_string(), + "gcZaamo_Zicsr_Ssccfg_XSifivecdiscarddlone" + ); + + extensions.insert(Extension::try_from("e").unwrap()); + assert_eq!( + extensions.to_string(), + "gecZaamo_Zicsr_Ssccfg_XSifivecdiscarddlone" + ); + + extensions.remove(&Extension::I); + assert_eq!( + extensions.to_string(), + "emafdcZaamo_Zicsr_Ssccfg_XSifivecdiscarddlone" + ); + + extensions.remove(&Extension::E); + assert_eq!( + extensions.to_string(), + "mafdcZaamo_Zicsr_Ssccfg_XSifivecdiscarddlone" + ); + + extensions.insert(Extension::I); + assert_eq!( + extensions.to_string(), + "gcZaamo_Zicsr_Ssccfg_XSifivecdiscarddlone" + ); + } +} diff --git a/riscv-target-parser/src/lib.rs b/riscv-target-parser/src/lib.rs new file mode 100644 index 00000000..5e4b812c --- /dev/null +++ b/riscv-target-parser/src/lib.rs @@ -0,0 +1,222 @@ +pub mod extension; +pub use extension::{Extension, Extensions}; + +/// Error variants for the RISC-V target parser. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Error<'a> { + InvalidTriple(&'a str), + InvalidArch(&'a str), + InvalidWidth(usize), + UnknownExtension(&'a str), + UnknownTargetFeature(&'a str), +} + +/// Helper struct to parse and store a target triple. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TargetTriple<'a> { + arch: &'a str, + vendor: &'a str, + os: &'a str, + bin: Option<&'a str>, +} + +impl<'a> TryFrom<&'a str> for TargetTriple<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + let mut parts = value.split('-'); + + let arch = parts.next().ok_or(Error::InvalidTriple(value))?; + let vendor = parts.next().ok_or(Error::InvalidTriple(value))?; + let os = parts.next().ok_or(Error::InvalidTriple(value))?; + let bin = parts.next(); + + Ok(Self { + arch, + vendor, + os, + bin, + }) + } +} + +impl std::fmt::Display for TargetTriple<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.arch, self.vendor, self.os)?; + if let Some(bin) = self.bin { + write!(f, "-{}", bin)?; + } + Ok(()) + } +} + +/// The width of the RISC-V architecture. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Width { + /// 32-bit RISC-V architecture. + W32, + /// 64-bit RISC-V architecture. + W64, + /// 128-bit RISC-V architecture. + W128, +} + +impl std::fmt::Display for Width { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::W32 => write!(f, "32"), + Self::W64 => write!(f, "64"), + Self::W128 => write!(f, "128"), + } + } +} + +macro_rules! impl_try_from_width { + ($($t:ty),*) => { + $( + impl TryFrom<$t> for Width { + type Error = Error<'static>; + fn try_from(bits: $t) -> Result { + match bits { + 32 => Ok(Self::W32), + 64 => Ok(Self::W64), + 128 => Ok(Self::W128), + _ => Err(Self::Error::InvalidWidth(bits as usize)), + } + } + } + impl From for $t { + fn from(width: Width) -> Self { + match width { + Width::W32 => 32, + Width::W64 => 64, + Width::W128 => 128, + } + } + } + )* + }; +} +impl_try_from_width!(u8, u16, u32, u64, u128, usize, i16, i32, i64, i128, isize); + +/// Struct that represents a RISC-V target. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RiscvTarget { + width: Width, + extensions: Extensions, +} + +impl RiscvTarget { + /// Builds a RISC-V target from a target triple and cargo flags. + /// This function is expected to be called from a build script. + /// + /// The target triple is expected to be in the form `riscv{width}{extensions}-vendor-os[-bin]`. + /// If the target triple is invalid, an error is returned. + /// + /// # Example + /// + /// ```no_run + /// + /// // In build.rs + /// let target = std::env::var("TARGET").unwrap(); + /// let cargo_flags = std::env::var("CARGO_ENCODED_RUSTFLAGS").unwrap(); + /// let target = riscv_target_parser::RiscvTarget::build(&target, &cargo_flags).unwrap(); // This will panic if the target is invalid + /// ``` + pub fn build<'a>(target: &'a str, cargo_flags: &'a str) -> Result> { + let triple = TargetTriple::try_from(target)?; + let mut target = Self::try_from(triple)?; + + for target_feature in cargo_flags + .split(0x1fu8 as char) + .filter(|arg| arg.starts_with("target-feature=")) + .flat_map(|arg| { + let arg = arg.trim_start_matches("target-feature="); + arg.split(',') + }) + { + if let Some(feature) = target_feature.strip_prefix('+') { + let extension = Extension::try_from(feature)?; + target.extensions.insert(extension); + } else if let Some(feature) = target_feature.strip_prefix('-') { + let extension = Extension::try_from(feature)?; + target.extensions.remove(&extension); + } else { + return Err(Error::UnknownTargetFeature(target_feature)); + } + } + Ok(target) + } + + /// Returns a list of flags to pass to `rustc` for the given RISC-V target. + /// This function is expected to be called from a build script. + /// + /// # Example + /// + /// ```no_run + /// let target = std::env::var("TARGET").unwrap(); + /// let cargo_flags = std::env::var("CARGO_ENCODED_RUSTFLAGS").unwrap(); + /// let target = riscv_target_parser::RiscvTarget::build(&target, &cargo_flags).unwrap(); + /// for flag in target.rustc_flags() { + /// println!("cargo:rustc-check-cfg=cfg({})", flag); + /// println!("cargo:rustc-cfg={}", flag); + /// } + /// + pub fn rustc_flags(&self) -> Vec { + let mut res = self + .extensions + .extensions() + .iter() + .map(|e| format!("riscv{e}")) + .collect::>(); + if self.extensions.is_g() { + res.push("riscvg".to_string()); + } + res + } + + /// Returns the width of the RISC-V architecture. + pub fn width(&self) -> Width { + self.width + } + + /// Returns the base extension of the RISC-V architecture (if any). + pub fn base_extension(&self) -> Option { + self.extensions.base_extension() + } +} + +impl<'a> TryFrom> for RiscvTarget { + type Error = Error<'a>; + + fn try_from(triple: TargetTriple<'a>) -> Result { + match triple.arch.strip_prefix("riscv") { + Some(arch) => { + match arch + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(arch.len()) + { + 0 => Err(Error::InvalidArch(arch)), + digit_end => { + let (width_str, extensions_str) = arch.split_at(digit_end); + let width = width_str.parse::().unwrap().try_into()?; + let extensions = extensions_str.try_into()?; + Ok(Self { width, extensions }) + } + } + } + None => Err(Error::InvalidArch(triple.arch)), + } + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_parse_target() { + let target = "riscv32imac-unknown-none-elf"; + let cargo_flags = "target-feature=+m,-a,+f"; + let target = super::RiscvTarget::build(target, cargo_flags).unwrap(); + let rustc_flags = target.rustc_flags(); + assert_eq!(rustc_flags, vec!["riscvi", "riscvm", "riscvf", "riscvc"]); + } +}