diff --git a/Cargo.toml b/Cargo.toml index 816c05a6..11bb8905 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,11 @@ required-features = ["std"] name = "codec_mode_histogram" required-features = ["codec"] +# W1a-#9 masking-primitive codegen probe imports `ndarray::simd` (std-gated). +[[example]] +name = "w1a9_codegen_probe" +required-features = ["std"] + [[example]] name = "mc_via_shader" required-features = ["codec"] diff --git a/examples/w1a9_codegen_probe.rs b/examples/w1a9_codegen_probe.rs new file mode 100644 index 00000000..0e28d844 --- /dev/null +++ b/examples/w1a9_codegen_probe.rs @@ -0,0 +1,26 @@ +use ndarray::simd::U64x8; +use std::hint::black_box; + +#[inline(never)] +pub fn probe_and3(a: U64x8, b: U64x8, c: U64x8) -> U64x8 { + a.ternlog::<0x80>(b, c) +} + +#[inline(never)] +pub fn probe_and2_andnot(a: U64x8, b: U64x8, c: U64x8) -> U64x8 { + a.ternlog::<0x40>(b, c) +} + +#[inline(never)] +pub fn probe_andnot(a: U64x8, b: U64x8) -> U64x8 { + a.andnot(b) +} + +fn main() { + let a = black_box(U64x8::splat(0xF0F0_F0F0_F0F0_F0F0)); + let b = black_box(U64x8::splat(0xCCCC_CCCC_CCCC_CCCC)); + let c = black_box(U64x8::splat(0xAAAA_AAAA_AAAA_AAAA)); + println!("{:x}", black_box(probe_and3(a, b, c)).to_array()[0]); + println!("{:x}", black_box(probe_and2_andnot(a, b, c)).to_array()[0]); + println!("{:x}", black_box(probe_andnot(a, b)).to_array()[0]); +} diff --git a/src/simd.rs b/src/simd.rs index 92c40283..20fc5f15 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -556,6 +556,36 @@ pub fn simd_ln_f32(x: F32x16) -> F32x16 { // Without `hpc-extras`, consumers still get the SIMD polyfill types above // (F32x16, I8x32, etc.) but NOT the domain-specific functions below. +/// Truth-table immediates for `U64x8::ternlog` / `U32x16::ternlog`. +/// +/// A ternlog immediate IS the truth table of a 3-input boolean function: for +/// each bit position, `index = (a << 2) | (b << 1) | c`, and the result bit is +/// `(IMM >> index) & 1`. Intel's VPTERNLOG convention, reproduced exactly by +/// every backend in this crate. +/// +/// Lives on the facade — NOT in a backend — so `ndarray::simd::ternlog::AND3` +/// resolves on every dispatch arm (a backend-resident module is compiled out +/// whenever another backend is selected, which is exactly what happened to +/// this module's first home in the scalar backend). +pub mod ternlog { + /// `a & b & c` — stack three prerequisite masks. + pub const AND3: i32 = 0x80; + /// `a & b & !c` — stack two prerequisites, exclude a third. + pub const AND2_ANDNOT: i32 = 0x40; + /// `a & !b & !c` — one base mask, two exclusions. + pub const AND_ANDNOT2: i32 = 0x10; + /// `(a | b) & c` — either of two prerequisites, gated by a third. + pub const OR2_AND: i32 = 0xA8; + /// `a ^ b ^ c` — three-way parity. + pub const XOR3: i32 = 0x96; + /// `(a & b) | (a & c) | (b & c)` — two-of-three majority. + pub const MAJ3: i32 = 0xE8; + /// `a & b` — two-input AND, `c` ignored. + pub const AND2: i32 = 0xC0; + /// `a | b | c` — union of three masks. + pub const OR3: i32 = 0xFE; +} + pub use crate::hpc::bitwise::{hamming_distance_raw, popcount_raw}; pub use crate::hpc::bnn_cross_plane::CollapseGate; pub use crate::hpc::fft::{wht_f32, wht_f32_new}; @@ -1266,4 +1296,221 @@ mod tests { assert!(v.is_finite(), "exp(200) must saturate, got {}", v); } } + + // ── W1a-#9 parity: U64x8/U32x16 andnot + ternlog ──────────────────────── + // + // The backends are compile-time exclusive, so "all three agree" is proven + // by asserting the ACTIVE backend against an independent scalar reference + // computed inline here, then running this suite under each cargo config + // (.cargo/config.toml = AVX2, config-avx512.toml = AVX-512, and the + // aarch64/wasm configs, which resolve U64x8 to the scalar backend). + // The reference below is written from the Intel truth-table definition, + // NOT by calling the primitive it checks. + + /// SplitMix64 — fixed seed, deterministic corpus (no dev-dependency). + fn splitmix64(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Independent reference: bit i of the result is `(imm >> idx) & 1` where + /// `idx = (a_i << 2) | (b_i << 1) | c_i`. Computed bit-by-bit on purpose — + /// deliberately NOT the same shape as the implementation. + fn ref_ternlog_u64(a: u64, b: u64, c: u64, imm: i32) -> u64 { + let mut out = 0u64; + for bit in 0..64 { + let ab = (a >> bit) & 1; + let bb = (b >> bit) & 1; + let cb = (c >> bit) & 1; + let idx = (ab << 2) | (bb << 1) | cb; + if (imm as u64 >> idx) & 1 == 1 { + out |= 1u64 << bit; + } + } + out + } + + /// Corpus: edge cases first, then a fixed-seed random tail. 8 lanes each. + fn corpus_u64(n: usize) -> Vec<[u64; 8]> { + let mut v: Vec<[u64; 8]> = vec![ + [0u64; 8], + [u64::MAX; 8], + [0x5555_5555_5555_5555; 8], + [0xAAAA_AAAA_AAAA_AAAA; 8], + [1, 0, u64::MAX, 0x8000_0000_0000_0000, 0xFFFF_FFFF, 0, 1 << 63, 7], + ]; + let mut st = 0x0DDB_1A5E_5EED_1234u64; + while v.len() < n { + let mut lanes = [0u64; 8]; + for l in lanes.iter_mut() { + *l = splitmix64(&mut st); + } + v.push(lanes); + } + v + } + + /// G1 — `andnot` is set difference in the documented direction, and it is + /// NOT the raw intrinsic's `!a & b`. The anti-half is the asymmetry + /// assertion: a constant-zero or argument-swapped implementation fails. + #[test] + fn w1a9_andnot_is_self_minus_other_u64x8() { + let corpus = corpus_u64(40); + let mut asymmetric_seen = 0usize; + for w in corpus.windows(2) { + let (a, b) = (w[0], w[1]); + let got = U64x8::from_array(a).andnot(U64x8::from_array(b)).to_array(); + for i in 0..8 { + assert_eq!(got[i], a[i] & !b[i], "andnot lane {i}: {a:?} \\ {b:?}"); + } + // Direction check: `self & !other` differs from `!self & other` + // whenever the two masks are not equal-and-symmetric. + let swapped: Vec = (0..8).map(|i| !a[i] & b[i]).collect(); + if (0..8).any(|i| got[i] != swapped[i]) { + asymmetric_seen += 1; + } + } + // Anti-vacuity: the direction must actually be observable on this + // corpus, or the test above proves nothing about argument order. + assert!( + asymmetric_seen * 3 > corpus.len(), + "andnot direction is unobservable on this corpus ({asymmetric_seen} asymmetric)" + ); + // Identities. + let x = U64x8::from_array(corpus[4]); + assert!(x.andnot(x).to_array().iter().all(|&v| v == 0)); + assert_eq!(x.andnot(U64x8::splat(0)).to_array(), corpus[4]); + } + + /// G2 — `ternlog` matches the independent truth-table reference for ALL + /// 256 immediates over the whole corpus. Any collapsed arm, wrong index + /// order, or dropped term fails. + #[test] + fn w1a9_ternlog_matches_truth_table_reference_all_256_imms() { + let corpus = corpus_u64(24); + // A hand-written subset of immediates is not enough — sweep all 256 + // via a macro-expanded const, since IMM is a const generic. + macro_rules! sweep { + ($($imm:literal),* $(,)?) => {$({ + for w in corpus.windows(3) { + let (a, b, c) = (w[0], w[1], w[2]); + let got = U64x8::from_array(a) + .ternlog::<$imm>(U64x8::from_array(b), U64x8::from_array(c)) + .to_array(); + for i in 0..8 { + assert_eq!( + got[i], + ref_ternlog_u64(a[i], b[i], c[i], $imm), + "ternlog imm={} lane={}", $imm, i + ); + } + } + })*}; + } + // All 256 truth tables. + sweep!( + 0, 1, 2, 3, 4, 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, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, + 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, + 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, + 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, + 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, + 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255 + ); + } + + /// G3 — the named truth-table constants mean what their docs say, and each + /// one is distinguishable from the others on real input (anti-vacuity: a + /// table of aliases would pass a weaker test). + #[test] + fn w1a9_named_immediates_have_their_documented_meaning() { + use super::ternlog; + let a = U64x8::from_array([0xF0F0_F0F0_F0F0_F0F0; 8]); + let b = U64x8::from_array([0xCCCC_CCCC_CCCC_CCCC; 8]); + let c = U64x8::from_array([0xAAAA_AAAA_AAAA_AAAA; 8]); + let (av, bv, cv) = (0xF0F0_F0F0_F0F0_F0F0u64, 0xCCCC_CCCC_CCCC_CCCCu64, 0xAAAA_AAAA_AAAA_AAAAu64); + + let and3 = a.ternlog::<{ ternlog::AND3 }>(b, c).to_array()[0]; + assert_eq!(and3, av & bv & cv, "AND3"); + let and2_andnot = a.ternlog::<{ ternlog::AND2_ANDNOT }>(b, c).to_array()[0]; + assert_eq!(and2_andnot, av & bv & !cv, "AND2_ANDNOT"); + let and_andnot2 = a.ternlog::<{ ternlog::AND_ANDNOT2 }>(b, c).to_array()[0]; + assert_eq!(and_andnot2, av & !bv & !cv, "AND_ANDNOT2"); + let or2_and = a.ternlog::<{ ternlog::OR2_AND }>(b, c).to_array()[0]; + assert_eq!(or2_and, (av | bv) & cv, "OR2_AND"); + let xor3 = a.ternlog::<{ ternlog::XOR3 }>(b, c).to_array()[0]; + assert_eq!(xor3, av ^ bv ^ cv, "XOR3"); + let maj3 = a.ternlog::<{ ternlog::MAJ3 }>(b, c).to_array()[0]; + assert_eq!(maj3, (av & bv) | (av & cv) | (bv & cv), "MAJ3"); + let and2 = a.ternlog::<{ ternlog::AND2 }>(b, c).to_array()[0]; + assert_eq!(and2, av & bv, "AND2 (c ignored)"); + let or3 = a.ternlog::<{ ternlog::OR3 }>(b, c).to_array()[0]; + assert_eq!(or3, av | bv | cv, "OR3"); + + // Anti-vacuity: all eight are pairwise distinct on this input, so the + // assertions above cannot be passing by aliasing. + let all = [and3, and2_andnot, and_andnot2, or2_and, xor3, maj3, and2, or3]; + for i in 0..all.len() { + for j in (i + 1)..all.len() { + assert_ne!(all[i], all[j], "named immediates {i} and {j} alias"); + } + } + } + + /// G4 — `ternlog::<0xC0>` (AND2, c ignored) equals plain `BitAnd`, and + /// `ternlog` composed with `andnot` agrees with the two-step form. This is + /// the bridge assertion: the new three-input primitive must not disagree + /// with the operators already shipping on these types. + #[test] + fn w1a9_ternlog_agrees_with_existing_operators() { + let corpus = corpus_u64(30); + for w in corpus.windows(3) { + let (a, b, c) = (U64x8::from_array(w[0]), U64x8::from_array(w[1]), U64x8::from_array(w[2])); + assert_eq!(a.ternlog::<0xC0>(b, c).to_array(), (a & b).to_array(), "AND2 vs BitAnd"); + assert_eq!(a.ternlog::<0xFE>(b, c).to_array(), (a | b | c).to_array(), "OR3 vs BitOr"); + assert_eq!(a.ternlog::<0x96>(b, c).to_array(), (a ^ b ^ c).to_array(), "XOR3 vs BitXor"); + // Three-layer stack: (a & b) \ c, one instruction vs two steps. + assert_eq!( + a.ternlog::<0x40>(b, c).to_array(), + (a & b).andnot(c).to_array(), + "AND2_ANDNOT vs (a & b).andnot(c)" + ); + } + } + + /// G5 — the 32-bit-lane sibling carries the same semantics. + #[test] + fn w1a9_u32x16_andnot_and_ternlog() { + let mut st = 0xC0FF_EE00_1234_5678u64; + for _ in 0..40 { + let mut a = [0u32; 16]; + let mut b = [0u32; 16]; + let mut c = [0u32; 16]; + for i in 0..16 { + a[i] = splitmix64(&mut st) as u32; + b[i] = splitmix64(&mut st) as u32; + c[i] = splitmix64(&mut st) as u32; + } + let got = U32x16::from_array(a) + .andnot(U32x16::from_array(b)) + .to_array(); + for i in 0..16 { + assert_eq!(got[i], a[i] & !b[i], "u32 andnot lane {i}"); + } + let t = U32x16::from_array(a) + .ternlog::<0x40>(U32x16::from_array(b), U32x16::from_array(c)) + .to_array(); + for i in 0..16 { + assert_eq!(t[i], a[i] & b[i] & !c[i], "u32 ternlog lane {i}"); + } + } + } } diff --git a/src/simd_avx2.rs b/src/simd_avx2.rs index 1d145353..9f372b6b 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -3500,3 +3500,149 @@ mod f16_precision_tests { } } } + +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (portable backend) ─────────── +// +// Masked projection, never traversal. The geometry is fixed and identical on +// every architecture, so these are whole-register operations composed from the +// `BitAnd` / `BitOr` / `Not` this type already carries — there is no lane +// index anywhere below. LLVM lowers the same source to `vpand`/`vpandn` on +// ymm (v3), `vandq_u64`/`vbicq_u64` on NEON, and `v128_and`/`v128_andnot` on +// wasm; the `repr(align(64))` backing is what earns the aligned moves. +// +// `IMM` is a const generic, so each `if IMM & bit` folds at compile time and +// only the minterms the truth table names survive. `AND3` (0x80) reduces to +// two ANDs of the whole register. + +impl U64x8 { + /// Set difference: `self & !other`. + /// + /// **Argument order differs from the raw Intel intrinsic.** + /// `_mm*_andnot_si*(a, b)` computes `!a & b`; this computes + /// `self & !other` — "self minus other". Every backend, same direction. + /// + /// Total function: no saturation, no overflow, no UB. `x.andnot(x)` is + /// zero; `x.andnot(U64x8::splat(0))` is `x`. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U64x8; + /// let a = U64x8::splat(0b1100); + /// let b = U64x8::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + self & !other + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM`. + /// + /// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit = + /// `(IMM >> index) & 1` — Intel's VPTERNLOG convention, matched exactly by + /// every backend. `IMM` is `i32` to mirror the intrinsic's signature; only + /// `0..=255` is legal, enforced at compile time on the AVX-512 backend by + /// the intrinsic's own static assert. Within that domain: total function, + /// no lane interaction. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U64x8}; + /// let (a, b, c) = (U64x8::splat(0b1100), U64x8::splat(0b1010), U64x8::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; + } + if IMM & 0x02 != 0 { + r = r | !a & !b & c; + } + if IMM & 0x04 != 0 { + r = r | !a & b & !c; + } + if IMM & 0x08 != 0 { + r = r | !a & b & c; + } + if IMM & 0x10 != 0 { + r = r | a & !b & !c; + } + if IMM & 0x20 != 0 { + r = r | a & !b & c; + } + if IMM & 0x40 != 0 { + r = r | a & b & !c; + } + if IMM & 0x80 != 0 { + r = r | a & b & c; + } + r + } +} + +impl U32x16 { + /// Set difference: `self & !other`. See [`U64x8::andnot`]. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + self & !other + } + + /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; + } + if IMM & 0x02 != 0 { + r = r | !a & !b & c; + } + if IMM & 0x04 != 0 { + r = r | !a & b & !c; + } + if IMM & 0x08 != 0 { + r = r | !a & b & c; + } + if IMM & 0x10 != 0 { + r = r | a & !b & !c; + } + if IMM & 0x20 != 0 { + r = r | a & !b & c; + } + if IMM & 0x40 != 0 { + r = r | a & b & !c; + } + if IMM & 0x80 != 0 { + r = r | a & b & c; + } + r + } +} diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index c92c5a58..89dc104b 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -4873,3 +4873,105 @@ mod int_simd_tests { assert_eq!(got, [10, 30, 50, 70, 20, 40, 60, 80]); } } + +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (AVX-512 backend) ──────────── +// +// The masking primitives, native. `andnot` is VPANDNQ/VPANDND; `ternlog` is +// VPTERNLOGQ/VPTERNLOGD — ONE instruction for ANY three-input boolean function +// of three 512-bit registers, selected by an 8-bit immediate that IS the +// function's truth table. Stacking three prerequisite masks therefore costs a +// single instruction, independent of how many bits are set. + +impl U64x8 { + /// Set difference: `self & !other`, lane-wise (VPANDNQ). + /// + /// **Argument order differs from the raw Intel intrinsic.** + /// `_mm512_andnot_si512(a, b)` computes `!a & b`; this method computes + /// `self & !other` — "self minus other" — so the arguments are swapped at + /// the call below. Every backend implements this same direction. + /// + /// Total function: no saturation, no overflow, no UB. `x.andnot(x)` is + /// zero; `x.andnot(U64x8::splat(0))` is `x`. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U64x8; + /// let a = U64x8::splat(0b1100); + /// let b = U64x8::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + // SAFETY: this module is only reachable under `target_feature = + // "avx512f"`, so the intrinsic's required feature is enabled at + // compile time. Arguments are swapped because `_mm512_andnot_si512` + // computes `!a & b` and this method's contract is `self & !other`. + U64x8(unsafe { _mm512_andnot_si512(other.0, self.0) }) + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM` — a single VPTERNLOGQ. + /// + /// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit = + /// `(IMM >> index) & 1`. Named immediates live in `crate::simd::ternlog` + /// (`AND3`, `AND2_ANDNOT`, `OR2_AND`, `MAJ3`, …). + /// + /// `IMM` is `i32` to match the intrinsic's signature; only `0..=255` is + /// legal, and the intrinsic's own `static_assert_uimm_bits!` rejects + /// anything wider **at compile time**. Within that domain this is a total + /// function: no saturation, no overflow, no UB, no lane interaction. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U64x8}; + /// let (a, b, c) = (U64x8::splat(0b1100), U64x8::splat(0b1010), U64x8::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + // SAFETY: avx512f is enabled at compile time (see `andnot`). IMM is a + // compile-time constant validated by the intrinsic's own static assert. + U64x8(unsafe { _mm512_ternarylogic_epi64::(self.0, b.0, c.0) }) + } +} + +impl U32x16 { + /// Set difference: `self & !other`, lane-wise (VPANDND). See + /// [`U64x8::andnot`] for the argument-order note. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + // SAFETY: avx512f enabled at compile time; arguments swapped so this + // computes `self & !other`, not the intrinsic's `!a & b`. + U32x16(unsafe { _mm512_andnot_si512(other.0, self.0) }) + } + + /// Any 3-input boolean function, 32-bit lanes — a single VPTERNLOGD. + /// See [`U64x8::ternlog`] for the truth-table convention and IMM domain. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + // SAFETY: avx512f enabled at compile time; IMM validated by the + // intrinsic's own static assert. + U32x16(unsafe { _mm512_ternarylogic_epi32::(self.0, b.0, c.0) }) + } +} diff --git a/src/simd_neon.rs b/src/simd_neon.rs index 4301d23e..9c95f3e1 100644 --- a/src/simd_neon.rs +++ b/src/simd_neon.rs @@ -1837,6 +1837,85 @@ impl core::ops::Add for U32x16 { } } +#[cfg(target_arch = "aarch64")] +impl U32x16 { + /// Set difference: `self & !other`, lane-wise. Same direction as every + /// other backend (`simd_avx512::U32x16::andnot` is the reference doc). + /// + /// Elementwise over the fanned array — the codegen shape this file's own + /// oracle blessed: LLVM vectorizes the loop over the aligned array, no + /// intrinsic override earned. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + let (a, b) = (self.to_array(), other.to_array()); + let mut o = [0u32; 16]; + for i in 0..16 { + o[i] = a[i] & !b[i]; + } + Self::from_array(o) + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM` — Intel's VPTERNLOG convention, + /// matched exactly by every backend. Named immediates: + /// `crate::simd::ternlog`. Only `0..=255` is legal, enforced at compile + /// time on every backend. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, b, c) = (self.to_array(), b.to_array(), c.to_array()); + let mut o = [0u32; 16]; + for i in 0..16 { + let (x, y, z) = (a[i], b[i], c[i]); + let mut r = 0u32; + if IMM & 0x01 != 0 { + r |= !x & !y & !z; + } + if IMM & 0x02 != 0 { + r |= !x & !y & z; + } + if IMM & 0x04 != 0 { + r |= !x & y & !z; + } + if IMM & 0x08 != 0 { + r |= !x & y & z; + } + if IMM & 0x10 != 0 { + r |= x & !y & !z; + } + if IMM & 0x20 != 0 { + r |= x & !y & z; + } + if IMM & 0x40 != 0 { + r |= x & y & !z; + } + if IMM & 0x80 != 0 { + r |= x & y & z; + } + o[i] = r; + } + Self::from_array(o) + } +} + #[cfg(target_arch = "aarch64")] impl core::ops::BitXor for U32x16 { type Output = Self; diff --git a/src/simd_nightly/u_word_types.rs b/src/simd_nightly/u_word_types.rs index e4e379b9..00f03b6e 100644 --- a/src/simd_nightly/u_word_types.rs +++ b/src/simd_nightly/u_word_types.rs @@ -882,3 +882,131 @@ mod tests { assert_eq!(a.cmpgt_mask(b), 0x05u8); } } + +impl U64x8 { + /// Set difference: `self & !other`, lane-wise. Same direction as every + /// other backend (`simd_avx512::U64x8::andnot` is the reference doc). + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U64x8; + /// let a = U64x8::splat(0b1100); + /// let b = U64x8::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + Self(self.0 & !other.0) + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM` — Intel's VPTERNLOG convention, + /// matched exactly by every backend (64-bit lanes). Named immediates: + /// `crate::simd::ternlog`. Only `0..=255` is legal, enforced at compile + /// time on every backend. The minterm branches fold at compile time. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U64x8}; + /// let (a, b, c) = (U64x8::splat(0b1100), U64x8::splat(0b1010), U64x8::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, b, c) = (self.0, b.0, c.0); + let mut r = a ^ a; + if IMM & 0x01 != 0 { + r |= !a & !b & !c; + } + if IMM & 0x02 != 0 { + r |= !a & !b & c; + } + if IMM & 0x04 != 0 { + r |= !a & b & !c; + } + if IMM & 0x08 != 0 { + r |= !a & b & c; + } + if IMM & 0x10 != 0 { + r |= a & !b & !c; + } + if IMM & 0x20 != 0 { + r |= a & !b & c; + } + if IMM & 0x40 != 0 { + r |= a & b & !c; + } + if IMM & 0x80 != 0 { + r |= a & b & c; + } + Self(r) + } +} + +impl U32x16 { + /// Set difference: `self & !other`, lane-wise. Same direction as every + /// other backend (`simd_avx512::U32x16::andnot` is the reference doc). + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + Self(self.0 & !other.0) + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM` — Intel's VPTERNLOG convention, + /// matched exactly by every backend (32-bit lanes). Named immediates: + /// `crate::simd::ternlog`. Only `0..=255` is legal, enforced at compile + /// time on every backend. The minterm branches fold at compile time. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, b, c) = (self.0, b.0, c.0); + let mut r = a ^ a; + if IMM & 0x01 != 0 { + r |= !a & !b & !c; + } + if IMM & 0x02 != 0 { + r |= !a & !b & c; + } + if IMM & 0x04 != 0 { + r |= !a & b & !c; + } + if IMM & 0x08 != 0 { + r |= !a & b & c; + } + if IMM & 0x10 != 0 { + r |= a & !b & !c; + } + if IMM & 0x20 != 0 { + r |= a & !b & c; + } + if IMM & 0x40 != 0 { + r |= a & b & !c; + } + if IMM & 0x80 != 0 { + r |= a & b & c; + } + Self(r) + } +} diff --git a/src/simd_scalar.rs b/src/simd_scalar.rs index 9004ebf9..a63810b9 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -2028,3 +2028,148 @@ pub type u64x4 = U64x4; pub type i32x8 = I32x8; #[allow(non_camel_case_types)] pub type i64x4 = I64x4; + +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (portable backend) ─────────── +// +// Masked projection, never traversal. The geometry is fixed and identical on +// every architecture, so these are whole-register operations composed from the +// `BitAnd` / `BitOr` / `Not` this type already carries — there is no lane +// index anywhere below. LLVM lowers the same source to `vpand`/`vpandn` on +// ymm (v3), `vandq_u64`/`vbicq_u64` on NEON, and `v128_and`/`v128_andnot` on +// wasm; the `repr(align(64))` backing is what earns the aligned moves. +// +// `IMM` is a const generic, so each `if IMM & bit` folds at compile time and +// only the minterms the truth table names survive. `AND3` (0x80) reduces to +// two ANDs of the whole register. + +impl U64x8 { + /// Set difference: `self & !other`. + /// + /// **Argument order differs from the raw Intel intrinsic.** + /// `_mm*_andnot_si*(a, b)` computes `!a & b`; this computes + /// `self & !other` — "self minus other". Every backend, same direction. + /// + /// Total function: no saturation, no overflow, no UB. `x.andnot(x)` is + /// zero; `x.andnot(U64x8::splat(0))` is `x`. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U64x8; + /// let a = U64x8::splat(0b1100); + /// let b = U64x8::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + self & !other + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM`. + /// + /// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit = + /// `(IMM >> index) & 1` — Intel's VPTERNLOG convention, matched exactly by + /// every backend. `IMM` is `i32` to mirror the intrinsic's signature; only + /// `0..=255` is legal, enforced at compile time on the AVX-512 backend by + /// the intrinsic's own static assert. Within that domain: total function, + /// no lane interaction. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U64x8}; + /// let (a, b, c) = (U64x8::splat(0b1100), U64x8::splat(0b1010), U64x8::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; + } + if IMM & 0x02 != 0 { + r = r | !a & !b & c; + } + if IMM & 0x04 != 0 { + r = r | !a & b & !c; + } + if IMM & 0x08 != 0 { + r = r | !a & b & c; + } + if IMM & 0x10 != 0 { + r = r | a & !b & !c; + } + if IMM & 0x20 != 0 { + r = r | a & !b & c; + } + if IMM & 0x40 != 0 { + r = r | a & b & !c; + } + if IMM & 0x80 != 0 { + r = r | a & b & c; + } + r + } +} + +impl U32x16 { + /// Set difference: `self & !other`. See [`U64x8::andnot`]. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + self & !other + } + + /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; + } + if IMM & 0x02 != 0 { + r = r | !a & !b & c; + } + if IMM & 0x04 != 0 { + r = r | !a & b & !c; + } + if IMM & 0x08 != 0 { + r = r | !a & b & c; + } + if IMM & 0x10 != 0 { + r = r | a & !b & !c; + } + if IMM & 0x20 != 0 { + r = r | a & !b & c; + } + if IMM & 0x40 != 0 { + r = r | a & b & !c; + } + if IMM & 0x80 != 0 { + r = r | a & b & c; + } + r + } +} diff --git a/src/simd_wasm.rs b/src/simd_wasm.rs index 75dd6ce9..5fb9f082 100644 --- a/src/simd_wasm.rs +++ b/src/simd_wasm.rs @@ -957,6 +957,79 @@ pub mod wasm32_simd { /// no intrinsic override earned. See /// `.claude/knowledge/blake3-on-ndarray-simd.md`. impl U32x16 { + /// Set difference: `self & !other`, lane-wise — one `v128.andnot` per + /// 128-bit part (the wasm intrinsic's argument order already matches + /// this crate's direction). Reference doc: `simd_avx512::U32x16::andnot`. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::U32x16; + /// let a = U32x16::splat(0b1100); + /// let b = U32x16::splat(0b1010); + /// assert_eq!(a.andnot(b).to_array()[0], 0b0100); // a & !b + /// ``` + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + Self([ + U32x4(v128_andnot(self.0[0].0, other.0[0].0)), + U32x4(v128_andnot(self.0[1].0, other.0[1].0)), + U32x4(v128_andnot(self.0[2].0, other.0[2].0)), + U32x4(v128_andnot(self.0[3].0, other.0[3].0)), + ]) + } + + /// Any 3-input boolean function of `self`, `b` and `c` — Intel's + /// VPTERNLOG convention, matched exactly by every backend. Named + /// immediates: `crate::simd::ternlog`. Only `0..=255` is legal, + /// enforced at compile time on every backend. Composed per 128-bit + /// part from `v128` bit ops; the minterm branches fold at compile + /// time, so only the truth table's terms survive. + /// + /// # Examples + /// + /// ``` + /// use ndarray::simd::{ternlog, U32x16}; + /// let (a, b, c) = (U32x16::splat(0b1100), U32x16::splat(0b1010), U32x16::splat(0b1001)); + /// let maj = a.ternlog::<{ ternlog::MAJ3 }>(b, c); // two-of-three majority + /// assert_eq!(maj.to_array()[0], 0b1000); + /// ``` + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + const { assert!(IMM >= 0 && IMM <= 255, "ternlog IMM is an 8-bit truth table") } + let mut parts = [self.0[0].0; 4]; + for p in 0..4 { + let (x, y, z) = (self.0[p].0, b.0[p].0, c.0[p].0); + let mut r = v128_xor(x, x); // zero + if IMM & 0x01 != 0 { + r = v128_or(r, v128_and(v128_not(x), v128_andnot(v128_not(y), z))); + } + if IMM & 0x02 != 0 { + r = v128_or(r, v128_and(v128_not(x), v128_andnot(z, y))); + } + if IMM & 0x04 != 0 { + r = v128_or(r, v128_and(v128_not(x), v128_andnot(y, z))); + } + if IMM & 0x08 != 0 { + r = v128_or(r, v128_and(v128_not(x), v128_and(y, z))); + } + if IMM & 0x10 != 0 { + r = v128_or(r, v128_and(x, v128_andnot(v128_not(y), z))); + } + if IMM & 0x20 != 0 { + r = v128_or(r, v128_and(x, v128_andnot(z, y))); + } + if IMM & 0x40 != 0 { + r = v128_or(r, v128_and(x, v128_andnot(y, z))); + } + if IMM & 0x80 != 0 { + r = v128_or(r, v128_and(x, v128_and(y, z))); + } + parts[p] = r; + } + Self([U32x4(parts[0]), U32x4(parts[1]), U32x4(parts[2]), U32x4(parts[3])]) + } + /// `_mm256_unpacklo_epi32` per 256-bit half: within each 128-bit quad, /// interleave the low two `u32` of each operand. #[inline(always)]