From 188ef1c6eeb850c4a90bd2c09f7e23ac66ec3075 Mon Sep 17 00:00:00 2001 From: David Wood Date: Thu, 7 May 2026 13:13:19 +0100 Subject: [PATCH 1/8] intrinsic-test: document arm intrinsic filtering Improve documentation for Arm intrinsic filtering - why we test some intrinsics and not others. --- crates/intrinsic-test/src/arm/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/intrinsic-test/src/arm/mod.rs b/crates/intrinsic-test/src/arm/mod.rs index 9bf6c95ffd..80f5ae17d7 100644 --- a/crates/intrinsic-test/src/arm/mod.rs +++ b/crates/intrinsic-test/src/arm/mod.rs @@ -46,15 +46,19 @@ impl SupportedArchitectureTest for ArmArchitectureTest { let intrinsics = intrinsics .into_iter() - // Not sure how we would compare intrinsic that returns void. + // Skip intrinsics that don't return a value. .filter(|i| i.results.kind() != TypeKind::Void) + // Skip bfloat intrinsics - not currently supported .filter(|i| i.results.kind() != TypeKind::BFloat) .filter(|i| !i.arguments.iter().any(|a| a.ty.kind() == TypeKind::BFloat)) // Skip pointers for now, we would probably need to look at the return // type to work out how many elements we need to point to. .filter(|i| !i.arguments.iter().any(|a| a.is_ptr())) + // Skip intrinsics with 128-bit elements (e.g. `p128`) .filter(|i| !i.arguments.iter().any(|a| a.ty.inner_size() == 128)) + // Skip intrinsics from `--skip` .filter(|i| !cli_options.skip.contains(&i.name)) + // Skip A64-specific intrinsics on A32 .filter(|i| !(a32 && i.arch_tags == vec!["A64".to_string()])) .take(sample_size) .collect::>(); From a3a3eedfe30a18833c081d48c20e088a16ded164 Mon Sep 17 00:00:00 2001 From: David Wood Date: Fri, 8 May 2026 11:07:23 +0000 Subject: [PATCH 2/8] intrinsic-test: document `--sample-percentage` This flag was missing documentation but the other flags had documentation --- crates/intrinsic-test/src/common/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/intrinsic-test/src/common/cli.rs b/crates/intrinsic-test/src/common/cli.rs index f407b5ceb7..07f94eba18 100644 --- a/crates/intrinsic-test/src/common/cli.rs +++ b/crates/intrinsic-test/src/common/cli.rs @@ -19,6 +19,7 @@ pub struct Cli { #[arg(long)] pub target: String, + /// Percentage of intrinsics to test (used to limit testing to keep CI times manageable) #[arg(long, default_value_t = 100u8)] pub sample_percentage: u8, } From 998fe202d254a1db921046640f65d8d051528f6f Mon Sep 17 00:00:00 2001 From: David Wood Date: Fri, 8 May 2026 12:54:38 +0000 Subject: [PATCH 3/8] intrinsic-test: document `iter_specializations` It isn't necessarily obvious what this function does --- crates/intrinsic-test/src/common/constraint.rs | 6 +++--- crates/intrinsic-test/src/common/intrinsic.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/intrinsic-test/src/common/constraint.rs b/crates/intrinsic-test/src/common/constraint.rs index 5984e0fcc2..c78eb3541c 100644 --- a/crates/intrinsic-test/src/common/constraint.rs +++ b/crates/intrinsic-test/src/common/constraint.rs @@ -1,10 +1,10 @@ use serde::Deserialize; use std::ops::Range; -/// Describes the values to test for a const generic parameter. +/// Describes the values to test for a const generic parameter #[derive(Debug, PartialEq, Clone, Deserialize)] pub enum Constraint { - /// Test a single value. + /// Test a single value Equal(i64), /// Test a range of values, e.g. `0..16`. Range(Range), @@ -13,7 +13,7 @@ pub enum Constraint { } impl Constraint { - /// Iterate over the values of this constraint. + /// Returns an iterator over the values of this constraint pub fn iter<'a>(&'a self) -> impl Iterator + 'a { match self { Constraint::Equal(i) => std::slice::Iter::default().copied().chain(*i..*i + 1), diff --git a/crates/intrinsic-test/src/common/intrinsic.rs b/crates/intrinsic-test/src/common/intrinsic.rs index 76e5959153..d69644388a 100644 --- a/crates/intrinsic-test/src/common/intrinsic.rs +++ b/crates/intrinsic-test/src/common/intrinsic.rs @@ -19,6 +19,10 @@ pub struct Intrinsic { pub arch_tags: Vec, } +/// Invokes `f` for each combination of the values in the constraint ranges. +/// +/// For example, given `constraints=[Equal(0), Range(1..2), Set([3, 4])]` and `imm_values=[]`, this +/// produces the four calls to `f`: `f([0, 1, 3])`, `f([0, 1, 4])`, `f([0, 2, 3])`, `f([0, 2, 4])`. fn recurse_specializations<'a, E>( constraints: &mut (impl Iterator + Clone), imm_values: &mut Vec, @@ -37,6 +41,13 @@ fn recurse_specializations<'a, E>( } impl Intrinsic { + /// Invokes `f` for "specialisation" of the intrinsic - a specific instantiation of the + /// constant generics of the intrinsic. `f` takes a slice where the `i`th element corresponds + /// to the value of the `i`th const generic argument of the intrinsic. + /// + /// For an intrinsic with three arguments with constraints `Equal(0)`, `Range(1..2)`, + /// `Set([3, 4])` respectively, this would produce four calls to `f`: `f(0, 1, 3)`, + /// `f(0, 1, 4)`, `f(0, 2, 3)`, `f(0, 2, 4)`. pub fn iter_specializations( &self, mut f: impl FnMut(&[i64]) -> Result<(), E>, From 19fc11feb57bd825b4f099097d14a9ed9759bfab Mon Sep 17 00:00:00 2001 From: David Wood Date: Fri, 8 May 2026 12:54:38 +0000 Subject: [PATCH 4/8] intrinsic-test: document `common::argument` module --- crates/intrinsic-test/src/common/argument.rs | 69 ++++++++++++++++++-- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/crates/intrinsic-test/src/common/argument.rs b/crates/intrinsic-test/src/common/argument.rs index 7a08a01246..25207a8c45 100644 --- a/crates/intrinsic-test/src/common/argument.rs +++ b/crates/intrinsic-test/src/common/argument.rs @@ -52,7 +52,8 @@ where self.constraint.is_some() } - /// The name (e.g. "A_VALS" or "a_vals") for the array of possible test inputs. + /// Returns a string with the name of the static variable containing test values for intrinsic + /// arguments of this type. pub(crate) fn rust_vals_array_name(&self) -> impl std::fmt::Display { let loads = crate::common::gen_rust::PASSES; format!( @@ -62,12 +63,15 @@ where ) } + /// Should this argument be passed by reference in C wrapper function declarations? + /// + /// SIMD types and `f16` are currently passed by reference. pub(crate) fn pass_by_ref(&self) -> bool { - // pass SIMD types and `f16` by reference self.is_simd() || (self.ty.kind() == TypeKind::Float && self.ty.inner_size() == 16) } } +/// Arguments of an intrinsic - including parameters that end up being const generics. #[derive(Debug, PartialEq, Clone)] pub struct ArgumentList { pub args: Vec>, @@ -77,6 +81,11 @@ impl ArgumentList where T: IntrinsicTypeDefinition, { + /// Returns a string with the arguments in `self` as a parameter list for a wrapper fn + /// definition in C (e.g. `$ty1 $arg1, $ty2 $arg2`). + /// + /// Skips arguments with constraints - which correspond to arguments that must take immediates - + /// as a different C definition will be generated for each value of these being tested. pub fn as_non_imm_arglist_c(&self) -> String { self.iter() .filter(|arg| !arg.has_constraint()) @@ -90,6 +99,11 @@ where .to_string() } + /// Returns a string with the arguments in `self` as a parameter list for a Rust declaration of + /// a C wrapper fn (e.g. `$arg1: $ty1, $arg2: $ty2`). + /// + /// Skips arguments with constraints - which correspond to arguments that must take immediates - + /// as a different C definition will be generated for each value of these being tested. pub fn as_non_imm_arglist_rust(&self) -> String { self.iter() .filter(|arg| !arg.has_constraint()) @@ -107,6 +121,8 @@ where .to_string() } + /// Returns a string with the arguments in `self` being passed to an intrinsic call in C + /// (e.g. `$arg1, 2 /* imm_args[0] */, $arg3` where `$arg2` has a constraint). pub fn as_call_params_c(&self, imm_args: &[i64]) -> String { let mut imm_args = imm_args.iter(); self.iter() @@ -123,8 +139,9 @@ where .to_string() } - /// Converts the argument list into the call parameters for a Rust function. - /// e.g. this would generate something like `a, b, c` + /// Returns a string with the arguments in `self` being passed to an intrinsic call in Rust. + /// (e.g. `$arg1, $arg3` where `$arg2` has a constraint and so corresponds to a const generic + /// parameter). pub fn as_call_param_rust(&self) -> String { self.iter() .filter(|a| !a.has_constraint()) @@ -132,6 +149,9 @@ where .join(", ") } + /// Returns a string with the arguments in `self` being passed to the declaration of a C wrapper + /// fn from Rust (e.g. `$arg1, $arg3` (where `$arg2` has a constraint and so corresponds to a + /// const generic parameter). pub fn as_c_call_param_rust(&self) -> String { self.iter() .filter(|a| !a.has_constraint()) @@ -145,6 +165,26 @@ where .join("") } + /// Returns a string defining a static variable with test values used for all intrinsics with + /// arguments of `arg`'s type. + /// + /// e.g. + /// ```rust,ignore + /// static U8_20: [u8; 20] = [ + /// 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xf0, + /// 0x80, 0x3b, 0xff, + /// ]; + /// ``` + /// + /// `num_lanes * num_vectors + loads - 1` elements are present in the array, which is sufficient + /// for a `loads` number of `num_lanes * num_vectors` windows into the array to be loaded: + /// + /// ```text + /// [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xf0, 0x80, 0x3b, 0xff] + /// ^^^^^^^^^^^^^^^^^^^ first window of `num_lanes * num_vectors` elements (e.g. four elements) + /// ^^^^^^^^^^^^^^^^^^ second window + /// `loads`th window ^^^^^^^^^^^^^^^^^^^^^^ + /// ``` pub fn gen_arg_rust( arg: &Argument, w: &mut impl std::io::Write, @@ -160,9 +200,23 @@ where ) } - /// Creates a line for each argument that initializes the argument from array `[ARG]_VALS` at - /// an offset `i` using a load intrinsic, in Rust. - /// e.g `let a = vld1_u8(A_VALS.as_ptr().offset(i));` + /// Returns a string defining a local variable for each argument and loading a value into each + /// using a load intrinsic. + /// + /// e.g. + /// ```rust,ignore + /// let a = vld1_u8(I16_23.as_ptr().offset((i + 0 /* idx */) % 20 /* PASSES */)); + /// ```` + /// + /// The generator will have already generated arrays of appropriate length with values that can + /// be used for testing (see the `gen_args_rust` function). + /// + /// Each load is assumed to have a variable `i` in scope which comes from a loop which repeats + /// the testing of the intrinsic for different values - each subsequent `i` shifts the window + /// of values being loaded along the pre-prepared array. + /// + /// Each subsequent argument's first window is started one element further into the array + /// then the previous. pub fn load_values_rust(&self) -> String { self.iter() .filter(|&arg| !arg.has_constraint()) @@ -186,6 +240,7 @@ where .collect() } + /// Returns an iterator over the contained arguments pub fn iter(&self) -> std::slice::Iter<'_, Argument> { self.args.iter() } From 7f312b48cb9c3a7e70da3ccc887e3253c41c8b91 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 19 May 2026 20:20:15 +0000 Subject: [PATCH 5/8] intrinsic-test: document `common::values` --- crates/intrinsic-test/src/common/values.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/intrinsic-test/src/common/values.rs b/crates/intrinsic-test/src/common/values.rs index 6c94ef2c22..01dc0713f0 100644 --- a/crates/intrinsic-test/src/common/values.rs +++ b/crates/intrinsic-test/src/common/values.rs @@ -1,6 +1,12 @@ -/// Get a single value for an argument values array in a determistic way. -/// * `bits`: The number of bits for the type, only 8, 16, 32, 64 are valid values -/// * `index`: The position in the array we are generating for +/// Returns a bit pattern for a value being output into a array of test values. Bit patterns come +/// from one of many constant arrays of test values. The specific constant array used depends on +/// the number of bits - `bits` - of the type having test values generated for it. This function +/// is called repeatedly with incrementing values of `index` to produce an entire array of test +/// values. +/// +/// Each constant array of bit patterns should ideally be at least the length of the largest array +/// of test values that will be requested (e.g. 51 for a `poly8x8x4` when `PASSES=20`: +/// `(8 * 4) + 20 - 1`), otherwise values will be repeated. pub fn value_for_array(bits: u32, index: u32) -> u64 { let index = index as usize; match bits { From de670c17949d6e83e55dbfe0140ceb2a529055c9 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 19 May 2026 20:20:15 +0000 Subject: [PATCH 6/8] intrinsic-test: document `common::intrinsic_helpers` --- .../src/common/intrinsic_helpers.rs | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/crates/intrinsic-test/src/common/intrinsic_helpers.rs b/crates/intrinsic-test/src/common/intrinsic_helpers.rs index 9d09eff645..cb8740fa07 100644 --- a/crates/intrinsic-test/src/common/intrinsic_helpers.rs +++ b/crates/intrinsic-test/src/common/intrinsic_helpers.rs @@ -68,7 +68,7 @@ impl fmt::Display for TypeKind { } impl TypeKind { - /// Gets the type part of a c typedef for a type that's in the form of {type}{size}_t. + /// Returns the type component of a C typedef for a type of the form of `{type}{size}_t` pub fn c_prefix(&self) -> &str { match self { Self::Float => "float", @@ -82,7 +82,7 @@ impl TypeKind { } } - /// Gets the rust prefix for the type kind i.e. i, u, f. + /// Returns the Rust prefix for this type kind i.e. `i`, `u`, or `f`. pub fn rust_prefix(&self) -> &str { match self { Self::BFloat => "bf", @@ -100,35 +100,43 @@ impl TypeKind { #[derive(Debug, PartialEq, Clone)] pub struct IntrinsicType { + /// Is this an immediate? pub constant: bool, - /// whether this object is a const pointer + /// Is this is a const pointer to the type? pub ptr_constant: bool, + /// Is this is a pointer to the type? pub ptr: bool, + /// Element type (e.g. `TypeKind::Int(Sign::Unsigned)` for `uint64x2_t`). pub kind: TypeKind, - /// The bit length of this type (e.g. 32 for u32). + + /// Number of bits of this type (e.g. 32 for `u32`). pub bit_len: Option, - /// Length of the SIMD vector (i.e. 4 for uint32x4_t), A value of `None` - /// means this is not a simd type. A `None` can be assumed to be 1, - /// although in some places a distinction is needed between `u64` and - /// `uint64x1_t` this signals that. + /// Length of a SIMD vector (i.e. 4 for `uint32x4_t`). + /// + /// A value of `None` means this is not a SIMD type. The number of lanes of a type with + /// `simd_len=None` can be assumed to be one, though it is important to maintain a distinction + /// between `simd_len=None` and `simd_len=Some(1)` so as to differentiate between `u64` and + /// `uint64x1_t`. pub simd_len: Option, - /// The number of rows for SIMD matrices (i.e. 2 for uint8x8x2_t). - /// A value of `None` represents a type that does not contain any - /// rows encoded in the type (e.g. uint8x8_t). - /// A value of `None` can be assumed to be 1 though. + /// Number of rows of a SIMD matrix (i.e. 2 for `uint8x8x2_t`). + /// + /// A value of `None` means this is not a SIMD matrix (e.g. `uint8x8_t`). The number of rows of + /// a type with `vec_len=None` can be assumed to be one. pub vec_len: Option, } impl IntrinsicType { + /// Returns the element type pub fn kind(&self) -> TypeKind { self.kind } + /// Returns the number of bits of the type (with a minimum of `8`) pub fn inner_size(&self) -> u32 { if let Some(bl) = self.bit_len { cmp::max(bl, 8) @@ -137,22 +145,34 @@ impl IntrinsicType { } } + /// Returns the number of lanes of the type pub fn num_lanes(&self) -> u32 { self.simd_len.unwrap_or(1) } + /// Returns the number of vectors of the type pub fn num_vectors(&self) -> u32 { self.vec_len.unwrap_or(1) } + /// Returns `true` if this represents a SIMD vector pub fn is_simd(&self) -> bool { self.simd_len.is_some() || self.vec_len.is_some() } + /// Returns `true` if this is a pointer pub fn is_ptr(&self) -> bool { self.ptr } + /// Returns the elements used in the test value arrays in `gen_arg_rust`. Uses the same + /// `num_lanes * num_vectors + loads - 1` arithmetic to produce the number of values that + /// `ArgumentList::gen_arg_rust` expects and `ArgumentList::load_values_rust` needs. + /// + /// Each value in the array starts as a bit pattern from `common::values::value_from_array` + /// which is then printed as a hex value in the generated code (and if identified as a negative + /// value, with the appropriate minus and corrected hex pattern). Calls to `fN::from_bits` are + /// generated for floats. pub fn populate_random(&self, loads: u32) -> String { match self { IntrinsicType { @@ -231,18 +251,16 @@ impl IntrinsicType { pub trait IntrinsicTypeDefinition: Deref { /// Determines the load function for this type. - /// can be implemented in an `impl` block fn get_load_function(&self) -> String; - /// Gets a string containing the typename for this type in C format. - /// can be directly defined in `impl` blocks + /// Gets a string containing the typename for this type in C. fn c_type(&self) -> String; - /// Gets a string containing the typename for this type in Rust format. - /// can be directly defined in `impl` blocks + /// Gets a string containing the typename for this type in Rust. fn rust_type(&self) -> String; - /// To enable architecture-specific logic + /// Gets a string containing the name of the scalar type corresponding to this type if it is a + /// vector. fn rust_scalar_type(&self) -> String { if self.is_simd() { format!( From 7503329e9113d2b78dfc07a3e836f6d4210f9643 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 19 May 2026 20:20:15 +0000 Subject: [PATCH 7/8] intrinsic-test: document `common::gen_c` --- crates/intrinsic-test/src/common/gen_c.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/intrinsic-test/src/common/gen_c.rs b/crates/intrinsic-test/src/common/gen_c.rs index bdf6f68d58..24756324c4 100644 --- a/crates/intrinsic-test/src/common/gen_c.rs +++ b/crates/intrinsic-test/src/common/gen_c.rs @@ -4,6 +4,16 @@ use crate::common::intrinsic::Intrinsic; use super::intrinsic_helpers::IntrinsicTypeDefinition; +/// Generates a C source file containing wrapper functions around each specialisation of each +/// intrinsic (that is, intrinsics with specific values for the the immediate arguments). Each +/// wrapper function is invoked via FFI from the Rust binary doing the testing. +/// +/// e.g. +/// ```c +/// void __crc32cd_wrapper(uint32_t* __dst, uint32_t a, uint64_t b) { +/// *__dst = __crc32cd(a, b); +/// } +/// ``` pub fn write_wrapper_c( w: &mut impl std::io::Write, notice: &str, From 770f7ceb9caad2e7c6f7bfe107398a010ff16c09 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 19 May 2026 20:20:15 +0000 Subject: [PATCH 8/8] intrinsic-test: document `common::gen_rust` --- crates/intrinsic-test/src/common/gen_rust.rs | 31 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/intrinsic-test/src/common/gen_rust.rs b/crates/intrinsic-test/src/common/gen_rust.rs index 15d7f0bec2..039e78f577 100644 --- a/crates/intrinsic-test/src/common/gen_rust.rs +++ b/crates/intrinsic-test/src/common/gen_rust.rs @@ -7,10 +7,13 @@ use crate::common::argument::ArgumentList; use crate::common::intrinsic::Intrinsic; use crate::common::intrinsic_helpers::TypeKind; -// The number of times each intrinsic will be called. +// The number of times each intrinsic will be called - influences the generation of the +// test arrays to minimise repeated testing of the same test values. pub(crate) const PASSES: u32 = 20; -// we need a reflexive equality relation, so treat NaNs as equal +/// Rust definitions that are included verbatim in the generated source. In particular, defines +/// a wrapper around float types that defines `NaN`s to be equal reflexively to enable +/// comparison of results that use floats types. const COMMON_RUST_DEFINITIONS: &str = r#" macro_rules! wrap_partialeq { ($($wrapper:ident ($inner:ty)),*) => {$( @@ -53,6 +56,13 @@ pub fn run_rustfmt(source_path: &str) { } } +/// Writes a `Cargo.toml` containing a workspace with `module_count` members to `w`. +/// +/// e.g. +/// ```toml +/// [workspace] +/// members = [ "mod_0", "mod_1" ] +/// ``` pub fn write_bin_cargo_toml( w: &mut impl std::io::Write, module_count: usize, @@ -64,6 +74,8 @@ pub fn write_bin_cargo_toml( writeln!(w, "]") } +/// Writes a `Cargo.toml` for a crate with name `name` to `w` that will contain a single Rust source +/// file with a subset of the testing being generated. pub fn write_lib_cargo_toml(w: &mut impl std::io::Write, name: &str) -> std::io::Result<()> { write!( w, @@ -90,6 +102,8 @@ pub fn write_lib_cargo_toml(w: &mut impl std::io::Write, name: &str) -> std::io: ) } +/// Writes a Rust source file into `w` with common definitions, static arrays with test values, +/// declarations of C wrapper functions for FFI and Rust test functions. pub fn write_lib_rs( w: &mut impl std::io::Write, notice: &str, @@ -138,6 +152,13 @@ pub fn write_lib_rs( Ok(()) } +/// Writes the body of an intrinsic test to `w` for `intrinsic`. +/// +/// Each specialisation of the intrinsic (i.e. specific instantiations of the immediate arguments +/// of the intrinsic) is added to an array of specialisations. Each specialisation is tested +/// (first loop) `PASSES` number of times (second loop). For a given iteration of a given +/// specialisation, test values are loaded for each argument and passed to the Rust intrinsic +/// and the C wrapper function, and the results are compared. fn generate_rust_test_loop( w: &mut impl std::io::Write, intrinsic: &Intrinsic, @@ -231,6 +252,8 @@ fn generate_rust_test_loop( ) } +/// Writes a test function for an given intrinsic to `w`, with a body generated by +/// `generate_rust_test_loop`. fn create_rust_test( w: &mut impl std::io::Write, intrinsic: &Intrinsic, @@ -250,6 +273,8 @@ fn create_rust_test( Ok(()) } +/// Writes an `extern "C"` block with function declarations for each of the C wrapper functions into +/// `w`. pub fn write_bindings_rust( w: &mut impl std::io::Write, i: usize, @@ -283,6 +308,8 @@ pub fn write_bindings_rust( writeln!(w, "}}") } +/// Writes a `build.rs` into `w` for each test crate that compiles the corresponding C source code +/// with wrapper functions. pub fn write_build_rs( w: &mut impl std::io::Write, i: usize,