diff --git a/Cargo.toml b/Cargo.toml index acce65c..36b6573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["keelhaul", "register-selftest"] +members = ["keelhaul", "keelhaul-cli", "register-selftest"] resolver = "2" [workspace.dependencies] diff --git a/keelhaul-cli/Cargo.toml b/keelhaul-cli/Cargo.toml new file mode 100644 index 0000000..5771b2e --- /dev/null +++ b/keelhaul-cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "keelhaul-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "keelhaul" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.81" +clap = { version = "4.5.3", features = ["derive"] } +keelhaul = { version = "0.1.0", path = "../keelhaul", features = ["rustfmt"] } diff --git a/keelhaul-cli/README.md b/keelhaul-cli/README.md new file mode 100644 index 0000000..e69de29 diff --git a/keelhaul-cli/src/main.rs b/keelhaul-cli/src/main.rs new file mode 100644 index 0000000..f0fe455 --- /dev/null +++ b/keelhaul-cli/src/main.rs @@ -0,0 +1,294 @@ +use std::{env, io, path}; + +use anyhow::{anyhow, Context}; +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + /// CMSIS-SVD source file for memory map metadata + #[arg(group = "input", long, required = true, action = clap::ArgAction::Append)] + svd: Vec, + + /// IEEE-1685 source file for memory map metadata + #[arg(group = "input", long, required = true, action = clap::ArgAction::Append)] + ipxact: Vec, + + /// Number of bits used to represent addresses on the target CPUs architecture + #[arg(long, value_enum, required = true)] + arch: ArchWidth, + + #[arg(long = "validate", default_value = ValidateLevel(keelhaul::ValidateLevel::Disabled), requires = "svd")] + validate_level: ValidateLevel, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Run the Keelhaul parser without doing anything + DryRun, + /// Lists all top level items (peripherals or subsystems) in the supplied sources + /// + /// Peripherals or subsystems containing zero registers are omitted. + LsTop { + /// Only list peripherals without register counts + #[arg(long, action = clap::ArgAction::SetTrue)] + no_count: bool, + #[arg(long, default_value = "alpha")] + sorting: Sorting, + }, + CountRegisters {}, + Coverage { + /// Describes how many of the input registers supply a known reset value which can be tested + /// for + #[arg(long)] + has_reset_value: bool, + }, + /// Generate metadata tests + Generate { + /// Type of test to be generated. Chain multiple for more kinds. + #[arg(short = 't', long = "test", required = true, action = clap::ArgAction::Append)] + tests_to_generate: Vec, + /// What to do when a test fails + #[arg(long)] + on_fail: Option, + /// Derive debug on possible errors + /// + /// May improve output for failing tests but also increases binary size. + #[arg(long, action = clap::ArgAction::SetTrue)] + derive_debug: bool, + /// Ignore the reset mask field when evaluating reset values for correctness + /// + /// Can be useful when the reset masks are misconfigured and it's good enough to just check + /// that the register values match with reset values on reset. + #[arg(long, action = clap::ArgAction::SetTrue)] + ignore_reset_masks: bool, + }, +} + +#[derive(Clone)] +enum Sorting { + Preserve, + Alpha, +} + +impl ValueEnum for Sorting { + fn value_variants<'a>() -> &'a [Self] { + &[Sorting::Alpha, Sorting::Preserve] + } + + fn to_possible_value(&self) -> Option { + use clap::builder::PossibleValue; + match self { + Sorting::Alpha => Some(PossibleValue::new("alpha")), + Sorting::Preserve => Some(PossibleValue::new("preserve")), + } + } +} + +#[derive(Clone)] +struct FailureImplKind(keelhaul::FailureImplKind); + +impl From for keelhaul::FailureImplKind { + fn from(value: FailureImplKind) -> Self { + value.0 + } +} + +impl ValueEnum for FailureImplKind { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self(keelhaul::FailureImplKind::None), + Self(keelhaul::FailureImplKind::Panic), + Self(keelhaul::FailureImplKind::ReturnError), + ] + } + + fn to_possible_value(&self) -> Option { + use clap::builder::PossibleValue; + match self.0 { + keelhaul::FailureImplKind::None => Some(PossibleValue::new("ignore")), + keelhaul::FailureImplKind::Panic => Some(PossibleValue::new("panic")), + keelhaul::FailureImplKind::ReturnError => Some(PossibleValue::new("error")), + } + } +} + +#[derive(Clone)] +struct ValidateLevel(keelhaul::ValidateLevel); + +impl From for keelhaul::ValidateLevel { + fn from(value: ValidateLevel) -> Self { + value.0 + } +} + +impl From for clap::builder::OsStr { + fn from(value: ValidateLevel) -> Self { + match value.0 { + keelhaul::ValidateLevel::Disabled => "disabled".into(), + keelhaul::ValidateLevel::Weak => "weak".into(), + keelhaul::ValidateLevel::Strict => "strict".into(), + } + } +} + +impl ValueEnum for ValidateLevel { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self(keelhaul::ValidateLevel::Disabled), + Self(keelhaul::ValidateLevel::Weak), + Self(keelhaul::ValidateLevel::Strict), + ] + } + + fn to_possible_value(&self) -> Option { + use clap::builder::PossibleValue; + match self.0 { + keelhaul::ValidateLevel::Disabled => Some(PossibleValue::new("disabled")), + keelhaul::ValidateLevel::Weak => Some(PossibleValue::new("weak")), + keelhaul::ValidateLevel::Strict => Some(PossibleValue::new("strict")), + } + } +} + +#[derive(Clone)] +struct TestKind(keelhaul::TestKind); + +impl From for keelhaul::TestKind { + fn from(value: TestKind) -> Self { + value.0 + } +} + +impl ValueEnum for TestKind { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self(keelhaul::TestKind::Read), + Self(keelhaul::TestKind::ReadIsResetVal), + ] + } + + fn to_possible_value(&self) -> Option { + use clap::builder::PossibleValue; + match self.0 { + keelhaul::TestKind::Read => Some(PossibleValue::new("read")), + keelhaul::TestKind::ReadIsResetVal => Some(PossibleValue::new("reset")), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum ArchWidth { + #[value(name = "32")] + U32, + #[value(name = "64")] + U64, +} + +impl From for keelhaul::ArchWidth { + fn from(value: ArchWidth) -> Self { + match value { + ArchWidth::U32 => keelhaul::ArchWidth::U32, + ArchWidth::U64 => keelhaul::ArchWidth::U64, + } + } +} + +fn string_to_path(s: &String) -> Result { + env::current_dir() + .expect("cannot access current working dir") + .join(s) + // Canonicalize paths for clear output + .canonicalize() +} + +fn get_sources(cli: &Cli) -> anyhow::Result> { + let mut sources = Vec::with_capacity(cli.svd.len() + cli.ipxact.len()); + + sources.extend(cli.svd.iter().map(|s| (s, keelhaul::SourceFormat::Svd))); + sources.extend( + cli.ipxact + .iter() + .map(|s| (s, keelhaul::SourceFormat::Ieee1685)), + ); + + let sources = sources + .into_iter() + .map(|(s, f)| string_to_path(s).map(|p| keelhaul::ModelSource::new(p, f))) + .collect::, _>>()?; + + // Make sure all source paths correspond to a file + for s in sources.iter() { + if !s.path().is_file() { + return Err(anyhow!("file does not exist: {}", s.path().display())); + } + } + + Ok(sources) +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let sources = get_sources(&cli)?; + let arch = cli.arch.into(); + + match &cli.command { + Some(Commands::DryRun) => { + match keelhaul::dry_run(&sources, arch).with_context(|| { + format!("could not execute dry run for arch: {arch:?}, sources: {sources:?}") + }) { + Ok(_) => println!("keelhaul: dry run completed successfully"), + Err(e) => println!("keelhaul: exited unsuccessfully: {e:?}"), + } + } + Some(Commands::LsTop { no_count, sorting }) => { + let mut top_and_count = keelhaul::list_top(&sources, arch)?; + if top_and_count.is_empty() { + println!("keelhaul: no peripherals found in input"); + } + match sorting { + Sorting::Preserve => { /* do nothing */ } + Sorting::Alpha => top_and_count.sort(), + }; + let longest = top_and_count.iter().map(|(s, _)| s.len()).max().unwrap(); + for (top, count) in top_and_count { + if *no_count { + println!("{top}"); + } else { + println!("{top: { + let output = keelhaul::count_registers_svd(&sources, arch, &keelhaul::Filters::all())?; + println!("{output}"); + } + Some(Commands::Generate { + tests_to_generate, + on_fail, + derive_debug, + ignore_reset_masks, + }) => { + let mut config = keelhaul::TestConfig::new(arch) + .tests_to_generate(tests_to_generate.iter().cloned().map(|tk| tk.0).collect())? + .derive_debug(*derive_debug) + .ignore_reset_masks(*ignore_reset_masks); + if let Some(on_fail) = on_fail.as_ref() { + config = config.on_fail(on_fail.clone().into()); + } + let output = + keelhaul::generate_tests(&sources, arch, &config, &keelhaul::Filters::all())?; + println!("{output}"); + } + Some(Commands::Coverage { .. }) => { + todo!() + } + None => {} + } + + Ok(()) +} diff --git a/keelhaul/src/analysis.rs b/keelhaul/src/analysis.rs new file mode 100644 index 0000000..dd3f6aa --- /dev/null +++ b/keelhaul/src/analysis.rs @@ -0,0 +1,13 @@ +//! Provides information about the input files + +use std::fmt; + +use crate::model; + +/// Trait for types that provide information about parsed registers +pub(crate) trait AnalyzeRegister: model::UniquePath {} + +impl AnalyzeRegister + for model::Register +{ +} diff --git a/keelhaul/src/api.rs b/keelhaul/src/api.rs index feb228e..efa9462 100644 --- a/keelhaul/src/api.rs +++ b/keelhaul/src/api.rs @@ -9,8 +9,11 @@ mod error; use std::{path, str}; -use crate::{codegen, error::SvdParseError, model, Filters, ParseTestKindError, TestConfig}; +use crate::{ + analysis, codegen, error::SvdParseError, model, Filters, ParseTestKindError, TestConfig, +}; use error::NotImplementedError; +use itertools::Itertools; use log::info; use strum::EnumIter; @@ -140,6 +143,25 @@ where Ok(registers.into_iter().next().unwrap()) } +fn parse_registers_for_analysis( + sources: &[ModelSource], + filters: &Filters, + arch: ArchWidth, +) -> Result>, ApiError> { + Ok(match arch { + ArchWidth::U32 => parse_registers::(sources, filters)? + .clone() + .into_iter() + .map(|reg| Box::new(reg) as Box) + .collect(), + ArchWidth::U64 => parse_registers::(sources, filters)? + .clone() + .into_iter() + .map(|reg| Box::new(reg) as Box) + .collect(), + }) +} + pub fn generate_tests( sources: &[ModelSource], arch_ptr_size: ArchWidth, @@ -186,3 +208,40 @@ pub fn generate_tests( Ok(s) } + +pub fn count_registers_svd( + sources: &[ModelSource], + arch: ArchWidth, + filters: &Filters, +) -> Result { + let registers = parse_registers_for_analysis(sources, filters, arch)?; + Ok(registers.len()) +} + +/// Returns top level containers (peripherals or subsystems) and the number of registers in each +/// +/// `Vec<(container, register count)>` +pub fn list_top( + sources: &[ModelSource], + arch: ArchWidth, +) -> Result, ApiError> { + let registers = parse_registers_for_analysis(sources, &Filters::all(), arch)?; + + let tops = registers + .iter() + .map(|reg| reg.top_container_name()) + .unique() + .collect_vec(); + let tops_and_counts = tops + .into_iter() + .map(|top| { + let reg_count = registers + .iter() + .filter(|reg| reg.top_container_name() == top) + .count(); + (top, reg_count) + }) + .collect_vec(); + + Ok(tops_and_counts) +} diff --git a/keelhaul/src/codegen.rs b/keelhaul/src/codegen.rs index dfc2eef..5a591cb 100644 --- a/keelhaul/src/codegen.rs +++ b/keelhaul/src/codegen.rs @@ -14,7 +14,7 @@ use std::{ use self::test_register::RegTestGenerator; use crate::{ api, - model::{self, ArchPtr, Registers}, + model::{self, ArchPtr, Registers, UniquePath}, TestKind, }; use itertools::Itertools; diff --git a/keelhaul/src/codegen/test_register.rs b/keelhaul/src/codegen/test_register.rs index 4509fcf..b2ceb65 100644 --- a/keelhaul/src/codegen/test_register.rs +++ b/keelhaul/src/codegen/test_register.rs @@ -8,29 +8,7 @@ use quote::{format_ident, quote}; use thiserror::Error; /// Type that a test can be generated for -pub trait TestRegister

{ - /// The path of the register used for human readable identification of the register as part of a - /// larger design. Might comprise components such as "peripheral, register cluster, register - /// name". - fn path(&self) -> Vec; - - /// A human-readable unique identifier for the register, usually the the path that is used to - /// access the register. - fn binding_id(&self) -> String { - self.path().join("_") - } - - /// Name of the top-level element containing this register, usually the peripheral or subsystem - /// depending on system architecture - fn top_container_name(&self) -> String { - self.path().first().unwrap().to_owned() - } - - /// The name of the register, usually the final element of the `path` - fn name(&self) -> String { - self.path().last().unwrap().to_owned() - } - +pub trait TestRegister

: model::UniquePath { /// Get the absolute memory address of the register fn addr(&self) -> P; @@ -42,6 +20,17 @@ pub trait TestRegister

{ /// An optional, known reset value fn reset_value(&self) -> Option>; + + /// A human-readable unique identifier for the register, usually the the path that is used to + /// access the register. + fn binding_id(&self) -> String { + self.path().join("_") + } + + /// The name of the register, usually the final element of the `path` + fn name(&self) -> String { + self.path().last().unwrap().to_owned() + } } #[derive(Clone, Debug)] diff --git a/keelhaul/src/frontend/svd_legacy.rs b/keelhaul/src/frontend/svd_legacy.rs index 150637d..9f04580 100644 --- a/keelhaul/src/frontend/svd_legacy.rs +++ b/keelhaul/src/frontend/svd_legacy.rs @@ -8,7 +8,10 @@ use std::{ use crate::{ bit_count_to_rust_uint_type_str, error::{self, Error, PositionalError, SvdParseError}, - model::{self, AddrRepr, ArchPtr, PtrSize, RegPath, RegValue, Register, Registers, ResetValue}, + model::{ + self, AddrRepr, ArchPtr, PtrSize, RegPath, RegValue, Register, Registers, ResetValue, + UniquePath, + }, util, Filters, IsAllowedOrBlocked, ItemFilter, TestRegister, }; use itertools::Itertools; @@ -528,7 +531,7 @@ fn try_dim_element_from_xml_node( fn check_node_count( node: &XmlNode, node_name: &str, - vector: &Vec, + vector: &[XmlNode], expected_count: RangeInclusive, ) -> Result<(), PositionalError> { let actual_count = vector.len(); diff --git a/keelhaul/src/lib.rs b/keelhaul/src/lib.rs index b95d98e..981218b 100644 --- a/keelhaul/src/lib.rs +++ b/keelhaul/src/lib.rs @@ -3,6 +3,7 @@ // Export full API at crate root pub use api::*; +pub(crate) mod analysis; mod api; pub(crate) mod codegen; pub mod error; diff --git a/keelhaul/src/model.rs b/keelhaul/src/model.rs index 419d358..85f2bfb 100644 --- a/keelhaul/src/model.rs +++ b/keelhaul/src/model.rs @@ -10,6 +10,23 @@ use std::{fmt, ops}; use self::schema::svd; +/// Trait for types that have a unique path within a given system +/// +/// This could be the "peripheral-cluster-register" chain on CMSIS-SVD or the bus traverse path on +/// IP-XACT. +pub trait UniquePath { + /// The path of the register used for human readable identification of the register as part of a + /// larger design. Might comprise components such as "peripheral, register cluster, register + /// name". + fn path(&self) -> Vec; + + /// Name of the top-level element containing this register, usually the peripheral or subsystem + /// depending on system architecture + fn top_container_name(&self) -> String { + self.path().first().unwrap().to_owned() + } +} + /// Reference schema /// /// Trait for signaling a reference schema. A type implementing `Schema` can be used to indicate diff --git a/keelhaul/src/model/register.rs b/keelhaul/src/model/register.rs index 3a923e8..26bf384 100644 --- a/keelhaul/src/model/register.rs +++ b/keelhaul/src/model/register.rs @@ -5,7 +5,7 @@ use std::{cmp, fmt, hash, marker::PhantomData, str}; use crate::{ bit_count_to_rust_uint_type_str, codegen, error, - model::{RefSchema, RefSchemaSvdV1_2}, + model::{RefSchema, RefSchemaSvdV1_2, UniquePath}, }; use itertools::Itertools; @@ -90,7 +90,7 @@ where } } -impl codegen::TestRegister

for Register +impl UniquePath for Register where P: num::CheckedAdd + Copy + fmt::Debug, S: RefSchema, @@ -102,7 +102,13 @@ where .map(|path| path.name.clone()) .collect_vec() } +} +impl codegen::TestRegister

for Register +where + P: num::CheckedAdd + Copy + fmt::Debug, + S: RefSchema, +{ /// Get the absolute memory address of the register /// /// # Panics