From ba2db74e20e5548ad60ce5995a9593cae5d7ecd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 20:55:06 +0000 Subject: [PATCH 1/5] =?UTF-8?q?simd(W1a-#9):=20U64x8/U32x16=20andnot=20+?= =?UTF-8?q?=20ternlog=20=E2=80=94=20the=20masking=20primitives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the two operators the polyfill lacked for mask composition: set difference, and any 3-input boolean function selected by a truth-table immediate. No new type — U64x8 (8 x u64 = 512 bits) and U32x16 already exist in every backend and are re-exported from all six arms of simd.rs, so simd.rs needs no dispatch change; the methods ride along. Why these two: FieldMask-style bitsets had AND/OR/XOR and no and-not, which is the operator 'stack this prerequisite, exclude that one' needs, and no three-input form at all. Stacking N prerequisite masks cost N-1 ops; with ternlog it costs ceil((N-1)/2), and on AVX-512 each of those is one instruction over 512 positions regardless of how many bits are set. Semantics, identical on every backend: a.andnot(b) = a & !b -- NOTE the argument order differs from the raw intrinsic: _mm*_andnot_si*(a, b) computes !a & b. Documented at every definition; the AVX-512 path swaps its arguments accordingly. a.ternlog::(b, c): per bit, index = (a<<2)|(b<<1)|c, result bit = (IMM >> index) & 1 (Intel VPTERNLOG convention). IMM is i32 to match the intrinsic; 0..=255 legal, enforced at compile time by the intrinsic's own static assert. Named immediates (AND3, AND2_ANDNOT, OR2_AND, MAJ3, ...) in the scalar backend's ternlog module. Total functions: no saturation, no overflow, no UB, no lane interaction. Backends. AVX-512 uses the native intrinsics; that module compiles only under a global target_feature = avx512f, which is the guard -- no added CPU check, no runtime detection. AVX2/NEON/wasm/scalar share ONE portable body: an element-wise loop over the repr(align(64)) backing array, the same idiom this file's existing BitAnd/BitOr/BitXor use. That is not a scalar fallback -- measured codegen below. Measured codegen (examples/w1a9_codegen_probe.rs, black_box'd inputs, release): v4 (config-avx512.toml): ternlog::<0x80> -> vpternlogq /bin/bashx80,%zmm2,%zmm1,%zmm0 (1 insn) ternlog::<0x40> -> vpternlogq /bin/bashx40,%zmm2,%zmm1,%zmm0 (1 insn) andnot -> vandnps %zmm0,%zmm1,%zmm0 (1 insn) v3 default (.cargo/config.toml): ternlog::<0x80> -> 2 x [vmovaps ymm; vandps; vandps; vmovaps] aligned moves, 512 bits in 8 insns The portable body auto-vectorises to real ymm work; repr(align(64)) is what earns the aligned vmovaps. Matches the storage documented for the HSW/ARL profiles in .claude/knowledge/agnostic-surface-cpu-matrix.md. Tests (5, at the simd.rs facade so they exercise whichever backend the build selected): all 256 immediates against an independent bit-by-bit truth-table reference; andnot direction with an anti-vacuity assertion that the self-minus-other vs not-self-and-other distinction is actually observable on the corpus; named-immediate meanings with a pairwise distinctness check so aliasing cannot pass; agreement with the existing BitAnd/BitOr/BitXor operators; the 32-bit-lane sibling. Fixed-seed SplitMix64 corpus with edge cases (0, MAX, 0x5555.., 0xAAAA..), no dev-dependency added. Verification: lib suite 2207 passed / 0 failed on the v3 arm; cargo check clean on the v4 arm; fmt clean; clippy adds no finding (3 pre-existing warnings remain in property_mask.rs / bitwise.rs / palette_codec.rs, untouched here). Known gap, stated rather than papered over: the v4 arm's *test* build is broken on main independently of this change (15 errors on a clean tree, missing I8x16/U64x8/U16x8 types in unrelated test modules), so the parity tests could not be RUN under AVX-512 -- only compiled. The native path is verified by the disassembly above, not by a test execution. Consumer site: AdaWorldAPI/lance-graph crates/lance-graph-contract/src/ class_view.rs -- FieldMask/WideFieldMask carry intersect/union/is_disjoint and lack difference/is_subset_of (D-MAR-1); and graph/blasgraph/ typed_graph.rs masked_traverse, which filters a materialised result per-entry instead of masking during the operation. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp --- examples/w1a9_codegen_probe.rs | 26 ++++ src/simd.rs | 216 +++++++++++++++++++++++++++++++++ src/simd_avx2.rs | 132 ++++++++++++++++++++ src/simd_avx512.rs | 66 ++++++++++ src/simd_scalar.rs | 158 ++++++++++++++++++++++++ 5 files changed, 598 insertions(+) create mode 100644 examples/w1a9_codegen_probe.rs 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..e3b6a139 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -1266,4 +1266,220 @@ 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() { + 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::<0x80>(b, c).to_array()[0]; + assert_eq!(and3, av & bv & cv, "AND3"); + let and2_andnot = a.ternlog::<0x40>(b, c).to_array()[0]; + assert_eq!(and2_andnot, av & bv & !cv, "AND2_ANDNOT"); + let and_andnot2 = a.ternlog::<0x10>(b, c).to_array()[0]; + assert_eq!(and_andnot2, av & !bv & !cv, "AND_ANDNOT2"); + let or2_and = a.ternlog::<0xA8>(b, c).to_array()[0]; + assert_eq!(or2_and, (av | bv) & cv, "OR2_AND"); + let xor3 = a.ternlog::<0x96>(b, c).to_array()[0]; + assert_eq!(xor3, av ^ bv ^ cv, "XOR3"); + let maj3 = a.ternlog::<0xE8>(b, c).to_array()[0]; + assert_eq!(maj3, (av & bv) | (av & cv) | (bv & cv), "MAJ3"); + let and2 = a.ternlog::<0xC0>(b, c).to_array()[0]; + assert_eq!(and2, av & bv, "AND2 (c ignored)"); + let or3 = a.ternlog::<0xFE>(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..d5af80c5 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -3500,3 +3500,135 @@ mod f16_precision_tests { } } } + +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (AVX2 backend) ─────────────── +// +// Written in the same idiom as this file's `avx2_int_type!` bitwise operators: +// an element-wise loop over the `#[repr(align(64))]` backing array, no +// intrinsics and no `unsafe`. Under this crate's x86-64-v3 baseline LLVM +// auto-vectorises these to `vpand`/`vpandn`/`vpor` on `ymm` — the alignment +// attribute is what lets it emit aligned moves. The same source lowers to +// `vandq_u64`/`vbicq_u64` on NEON and `v128_and`/`v128_andnot` on wasm, which +// is why one portable body serves all three of those profiles. + +/// Evaluate a ternlog truth table for one lane. `imm` is a compile-time +/// constant at every call site, so only the minterms the table selects survive +/// const-folding — `AND3` (0x80) reduces to two ANDs. +#[inline(always)] +const fn ternlog_lane_u64_avx2(a: u64, b: u64, c: u64, imm: i32) -> u64 { + let mut r = 0u64; + 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; + } + r +} + +#[inline(always)] +const fn ternlog_lane_u32_avx2(a: u32, b: u32, c: u32, imm: i32) -> u32 { + let mut r = 0u32; + 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; + } + r +} + +impl U64x8 { + /// Set difference: `self & !other`, lane-wise. + /// + /// **Argument order differs from the raw Intel intrinsic.** + /// `_mm256_andnot_si256(a, b)` computes `!a & b`; this method computes + /// `self & !other` — "self minus other". 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`. + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = self.0[i] & !other.0[i]; + } + Self(o) + } + + /// 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 and the AVX-512 backend rejects wider values at + /// compile time. Within that domain: total function, no lane interaction. + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + let mut o = [0u64; 8]; + for i in 0..8 { + o[i] = ternlog_lane_u64_avx2(self.0[i], b.0[i], c.0[i], IMM); + } + Self(o) + } +} + +impl U32x16 { + /// Set difference: `self & !other`, lane-wise. See [`U64x8::andnot`]. + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + let mut o = [0u32; 16]; + for i in 0..16 { + o[i] = self.0[i] & !other.0[i]; + } + Self(o) + } + + /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + let mut o = [0u32; 16]; + for i in 0..16 { + o[i] = ternlog_lane_u32_avx2(self.0[i], b.0[i], c.0[i], IMM); + } + Self(o) + } +} diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index c92c5a58..85618298 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -4873,3 +4873,69 @@ 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`. + #[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 the scalar backend's + /// `ternlog` module (`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. + #[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. + #[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. + #[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_scalar.rs b/src/simd_scalar.rs index 9004ebf9..e1adcbac 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -2028,3 +2028,161 @@ pub type u64x4 = U64x4; pub type i32x8 = I32x8; #[allow(non_camel_case_types)] pub type i64x4 = I64x4; + +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (scalar) ───────────────────── +// +// The masking primitives. `andnot` is set difference; `ternlog` folds any +// three-input boolean function into one call, mirroring AVX-512's VPTERNLOGQ / +// VPTERNLOGD. On this backend both are plain lane-wise integer arithmetic — +// the correctness anchor the AVX-512 and AVX2 paths are parity-tested against. + +/// 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`. This is Intel's VPTERNLOG convention, reproduced +/// exactly by every backend in this crate. +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; +} + +/// Scalar evaluation of one ternlog lane, shared by every non-AVX-512 backend. +/// `imm` is always a compile-time constant at the call site, so the whole +/// chain folds away under `#[inline(always)]`. +#[inline(always)] +const fn ternlog_lane_u64(a: u64, b: u64, c: u64, imm: i32) -> u64 { + let mut r = 0u64; + 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; + } + r +} + +#[inline(always)] +const fn ternlog_lane_u32(a: u32, b: u32, c: u32, imm: i32) -> u32 { + let mut r = 0u32; + 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; + } + r +} + +impl U64x8 { + /// Set difference: `self & !other`, lane-wise. + /// + /// **Argument order differs from the raw Intel intrinsic.** + /// `_mm512_andnot_si512(a, b)` computes `!a & b`; this method computes + /// `self & !other` — "self minus other" — because that is the direction a + /// mask narrowing wants. 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`. + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + let mut out = [0u64; 8]; + for i in 0..8 { + out[i] = self.0[i] & !other.0[i]; + } + Self(out) + } + + /// Any 3-input boolean function of `self`, `b` and `c`, selected by the + /// const truth-table immediate `IMM` (named values in [`ternlog`]). + /// + /// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit = + /// `(IMM >> index) & 1` — Intel's VPTERNLOG convention. On AVX-512 this is + /// a single VPTERNLOGQ regardless of how many bits are set. + /// + /// `IMM` is `i32` to match the intrinsic's signature; only `0..=255` is + /// legal and the AVX-512 backend rejects anything wider at compile time. + /// Within that domain this is a total function: no saturation, no + /// overflow, no UB, no lane interaction. + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + let mut out = [0u64; 8]; + for i in 0..8 { + out[i] = ternlog_lane_u64(self.0[i], b.0[i], c.0[i], IMM); + } + Self(out) + } +} + +impl U32x16 { + /// Set difference: `self & !other`, lane-wise. See [`U64x8::andnot`] for + /// the argument-order note — this method computes `self & !other`. + #[inline(always)] + pub fn andnot(self, other: Self) -> Self { + let mut out = [0u32; 16]; + for i in 0..16 { + out[i] = self.0[i] & !other.0[i]; + } + Self(out) + } + + /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. + #[inline(always)] + pub fn ternlog(self, b: Self, c: Self) -> Self { + let mut out = [0u32; 16]; + for i in 0..16 { + out[i] = ternlog_lane_u32(self.0[i], b.0[i], c.0[i], IMM); + } + Self(out) + } +} From ea793869610f6217e7352da2a08477c24d69f261 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:44:54 +0000 Subject: [PATCH 2/5] simd(W1a-#9): express masking as whole-register ops, not lane traversal Removes every lane index from the portable backends. andnot becomes `self & !other`; ternlog composes from the BitAnd/BitOr/Not these types already carry. No `for i in 0..N`, no per-lane helper functions -- masking is a projection over the whole register, and a loop expressed it as a traversal even though LLVM undid it. The two portable backends now carry byte-identical bodies, which is the polyfill's point: one source, one geometry, per-arch lowering selected by the compile-time dispatch in simd.rs. Scope note: ternlog is bitwise, so it has no lane semantics at all -- it applies its truth table independently at every bit position. Every reading of a 12-byte cell (6x2, 4x3, 3x4, 24xi4) is therefore masked by the IDENTICAL mask; the operation never sees, and never imposes, a carving. That is why this crate ships the node only: no fold, no composition structure, no state. Composition and interning belong to the consumer. Codegen re-measured after the rewrite, unchanged: v4: vpternlogq $0x80 / $0x40, vandnps -- one instruction each v3: 2 x [vmovaps ymm; vandps; vandps; vmovaps], aligned Net -49 lines. Suite 2207 passed / 0 failed, fmt clean, clippy baseline unchanged (3 pre-existing warnings in untouched files). --- src/simd_avx2.rs | 168 +++++++++++++++------------------ src/simd_scalar.rs | 225 ++++++++++++++++++++------------------------- 2 files changed, 172 insertions(+), 221 deletions(-) diff --git a/src/simd_avx2.rs b/src/simd_avx2.rs index d5af80c5..6f0c5ca7 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -3501,96 +3501,31 @@ mod f16_precision_tests { } } -// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (AVX2 backend) ─────────────── +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (portable backend) ─────────── // -// Written in the same idiom as this file's `avx2_int_type!` bitwise operators: -// an element-wise loop over the `#[repr(align(64))]` backing array, no -// intrinsics and no `unsafe`. Under this crate's x86-64-v3 baseline LLVM -// auto-vectorises these to `vpand`/`vpandn`/`vpor` on `ymm` — the alignment -// attribute is what lets it emit aligned moves. The same source lowers to -// `vandq_u64`/`vbicq_u64` on NEON and `v128_and`/`v128_andnot` on wasm, which -// is why one portable body serves all three of those profiles. - -/// Evaluate a ternlog truth table for one lane. `imm` is a compile-time -/// constant at every call site, so only the minterms the table selects survive -/// const-folding — `AND3` (0x80) reduces to two ANDs. -#[inline(always)] -const fn ternlog_lane_u64_avx2(a: u64, b: u64, c: u64, imm: i32) -> u64 { - let mut r = 0u64; - 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; - } - r -} - -#[inline(always)] -const fn ternlog_lane_u32_avx2(a: u32, b: u32, c: u32, imm: i32) -> u32 { - let mut r = 0u32; - 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; - } - r -} +// 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`, lane-wise. + /// Set difference: `self & !other`. /// /// **Argument order differs from the raw Intel intrinsic.** - /// `_mm256_andnot_si256(a, b)` computes `!a & b`; this method computes - /// `self & !other` — "self minus other". Every backend implements this - /// same direction. + /// `_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`. #[inline(always)] pub fn andnot(self, other: Self) -> Self { - let mut o = [0u64; 8]; - for i in 0..8 { - o[i] = self.0[i] & !other.0[i]; - } - Self(o) + self & !other } /// Any 3-input boolean function of `self`, `b` and `c`, selected by the @@ -3599,36 +3534,77 @@ impl U64x8 { /// 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 and the AVX-512 backend rejects wider values at - /// compile time. Within that domain: total function, no lane interaction. + /// `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. #[inline(always)] pub fn ternlog(self, b: Self, c: Self) -> Self { - let mut o = [0u64; 8]; - for i in 0..8 { - o[i] = ternlog_lane_u64_avx2(self.0[i], b.0[i], c.0[i], IMM); + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; } - Self(o) + 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`, lane-wise. See [`U64x8::andnot`]. + /// Set difference: `self & !other`. See [`U64x8::andnot`]. #[inline(always)] pub fn andnot(self, other: Self) -> Self { - let mut o = [0u32; 16]; - for i in 0..16 { - o[i] = self.0[i] & !other.0[i]; - } - Self(o) + self & !other } /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. #[inline(always)] pub fn ternlog(self, b: Self, c: Self) -> Self { - let mut o = [0u32; 16]; - for i in 0..16 { - o[i] = ternlog_lane_u32_avx2(self.0[i], b.0[i], c.0[i], IMM); + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; } - Self(o) + 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_scalar.rs b/src/simd_scalar.rs index e1adcbac..a426781e 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -2029,160 +2029,135 @@ pub type i32x8 = I32x8; #[allow(non_camel_case_types)] pub type i64x4 = I64x4; -// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (scalar) ───────────────────── +// ── W1a-#9: U64x8 / U32x16 :: andnot + ternlog (portable backend) ─────────── // -// The masking primitives. `andnot` is set difference; `ternlog` folds any -// three-input boolean function into one call, mirroring AVX-512's VPTERNLOGQ / -// VPTERNLOGD. On this backend both are plain lane-wise integer arithmetic — -// the correctness anchor the AVX-512 and AVX2 paths are parity-tested against. - -/// 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`. This is Intel's VPTERNLOG convention, reproduced -/// exactly by every backend in this crate. -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; -} - -/// Scalar evaluation of one ternlog lane, shared by every non-AVX-512 backend. -/// `imm` is always a compile-time constant at the call site, so the whole -/// chain folds away under `#[inline(always)]`. -#[inline(always)] -const fn ternlog_lane_u64(a: u64, b: u64, c: u64, imm: i32) -> u64 { - let mut r = 0u64; - 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; - } - r -} - -#[inline(always)] -const fn ternlog_lane_u32(a: u32, b: u32, c: u32, imm: i32) -> u32 { - let mut r = 0u32; - 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; - } - r -} +// 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`, lane-wise. + /// Set difference: `self & !other`. /// /// **Argument order differs from the raw Intel intrinsic.** - /// `_mm512_andnot_si512(a, b)` computes `!a & b`; this method computes - /// `self & !other` — "self minus other" — because that is the direction a - /// mask narrowing wants. Every backend implements this same direction. + /// `_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`. #[inline(always)] pub fn andnot(self, other: Self) -> Self { - let mut out = [0u64; 8]; - for i in 0..8 { - out[i] = self.0[i] & !other.0[i]; - } - Self(out) + self & !other } /// Any 3-input boolean function of `self`, `b` and `c`, selected by the - /// const truth-table immediate `IMM` (named values in [`ternlog`]). + /// const truth-table immediate `IMM`. /// /// Per bit position: `index = (self << 2) | (b << 1) | c`, result bit = - /// `(IMM >> index) & 1` — Intel's VPTERNLOG convention. On AVX-512 this is - /// a single VPTERNLOGQ regardless of how many bits are set. - /// - /// `IMM` is `i32` to match the intrinsic's signature; only `0..=255` is - /// legal and the AVX-512 backend rejects anything wider at compile time. - /// Within that domain this is a total function: no saturation, no - /// overflow, no UB, no lane interaction. + /// `(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. #[inline(always)] pub fn ternlog(self, b: Self, c: Self) -> Self { - let mut out = [0u64; 8]; - for i in 0..8 { - out[i] = ternlog_lane_u64(self.0[i], b.0[i], c.0[i], IMM); + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; } - Self(out) + 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`, lane-wise. See [`U64x8::andnot`] for - /// the argument-order note — this method computes `self & !other`. + /// Set difference: `self & !other`. See [`U64x8::andnot`]. #[inline(always)] pub fn andnot(self, other: Self) -> Self { - let mut out = [0u32; 16]; - for i in 0..16 { - out[i] = self.0[i] & !other.0[i]; - } - Self(out) + self & !other } /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. #[inline(always)] pub fn ternlog(self, b: Self, c: Self) -> Self { - let mut out = [0u32; 16]; - for i in 0..16 { - out[i] = ternlog_lane_u32(self.0[i], b.0[i], c.0[i], IMM); + let (a, z) = (self, Self::splat(0)); + let mut r = z; + if IMM & 0x01 != 0 { + r = r | !a & !b & !c; } - Self(out) + 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 } } + +/// 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. +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; +} From 4d172b75c4dde98d037cc443fe6fa1b48c889109 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 03:42:48 +0000 Subject: [PATCH 3/5] ci: gate w1a9_codegen_probe behind the std feature like its sibling examples The example imports ndarray::simd, which is #[cfg(feature = "std")]; without the [[example]] required-features declaration, --no-default-features CI jobs try to build it and fail with E0432. Same trampoline every other simd-using example already carries. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) 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"] From b865b7086f2eb534a171488b916dd1277cb2b8b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:18:21 +0000 Subject: [PATCH 4/5] review: masking primitives on every dispatch arm; named immediates on the facade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the three review findings on the W1a-#9 masking PR: - P1 (codex): andnot/ternlog now exist on every dispatched backend, not only scalar/avx2/avx512 — added to the aarch64 simd_neon U32x16 (elementwise over the fanned array, the file's oracle-blessed codegen shape), the wasm32_simd U32x16 (per-part v128_and/or/not/andnot; the wasm andnot intrinsic argument order already matches this crate's direction), and the nightly U64x8/U32x16 (core::simd whole-register ops). aarch64 compile-verified via cargo check --target; the wasm arm is blocked from a whole-crate check by the pre-existing getrandom wasm dependency gap, noted honestly. - P2 (cursor/codex/coderabbit): the named truth-table immediates moved from the scalar backend (compiled out on x86) to the always-compiled facade as crate::simd::ternlog, doc pointers in avx512/scalar updated, and the named-immediates test now routes through the public constants so they are exercised, not just documented. - coderabbit nitpick: portable/avx2/neon/wasm/nightly ternlog arms gain the same compile-time IMM domain guard the AVX-512 intrinsic enforces (inline const assert, 0..=255). All five w1a9 facade tests pass; full lib suite green (2255 passed). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp --- src/simd.rs | 47 +++++++++++++--- src/simd_avx2.rs | 2 + src/simd_avx512.rs | 4 +- src/simd_neon.rs | 61 +++++++++++++++++++++ src/simd_nightly/u_word_types.rs | 92 ++++++++++++++++++++++++++++++++ src/simd_scalar.rs | 26 +-------- src/simd_wasm.rs | 55 +++++++++++++++++++ 7 files changed, 252 insertions(+), 35 deletions(-) diff --git a/src/simd.rs b/src/simd.rs index e3b6a139..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}; @@ -1402,26 +1432,27 @@ mod tests { /// 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::<0x80>(b, c).to_array()[0]; + let and3 = a.ternlog::<{ ternlog::AND3 }>(b, c).to_array()[0]; assert_eq!(and3, av & bv & cv, "AND3"); - let and2_andnot = a.ternlog::<0x40>(b, c).to_array()[0]; + 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::<0x10>(b, c).to_array()[0]; + 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::<0xA8>(b, c).to_array()[0]; + 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::<0x96>(b, c).to_array()[0]; + let xor3 = a.ternlog::<{ ternlog::XOR3 }>(b, c).to_array()[0]; assert_eq!(xor3, av ^ bv ^ cv, "XOR3"); - let maj3 = a.ternlog::<0xE8>(b, c).to_array()[0]; + 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::<0xC0>(b, c).to_array()[0]; + let and2 = a.ternlog::<{ ternlog::AND2 }>(b, c).to_array()[0]; assert_eq!(and2, av & bv, "AND2 (c ignored)"); - let or3 = a.ternlog::<0xFE>(b, c).to_array()[0]; + 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 diff --git a/src/simd_avx2.rs b/src/simd_avx2.rs index 6f0c5ca7..e01474a8 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -3539,6 +3539,7 @@ impl U64x8 { /// no lane interaction. #[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 { @@ -3579,6 +3580,7 @@ impl U32x16 { /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. #[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 { diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index 85618298..f435f7b5 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -4905,8 +4905,8 @@ impl U64x8 { /// 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 the scalar backend's - /// `ternlog` module (`AND3`, `AND2_ANDNOT`, `OR2_AND`, `MAJ3`, …). + /// `(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 diff --git a/src/simd_neon.rs b/src/simd_neon.rs index 4301d23e..806125e3 100644 --- a/src/simd_neon.rs +++ b/src/simd_neon.rs @@ -1837,6 +1837,67 @@ 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. + #[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. + #[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..066cb9fc 100644 --- a/src/simd_nightly/u_word_types.rs +++ b/src/simd_nightly/u_word_types.rs @@ -882,3 +882,95 @@ 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). + #[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. + #[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). + #[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. + #[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 a426781e..7bc0839e 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -2107,6 +2107,7 @@ impl U32x16 { /// Any 3-input boolean function, 32-bit lanes. See [`U64x8::ternlog`]. #[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 { @@ -2136,28 +2137,3 @@ impl U32x16 { r } } - -/// 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. -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; -} diff --git a/src/simd_wasm.rs b/src/simd_wasm.rs index 75dd6ce9..a0605e1d 100644 --- a/src/simd_wasm.rs +++ b/src/simd_wasm.rs @@ -957,6 +957,61 @@ 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`. + #[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. + #[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)] From 5ac51cda018da7a85c9941e7f92bc2b7a00aee6e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 05:21:23 +0000 Subject: [PATCH 5/5] review: doc examples on every andnot/ternlog across all six backends CodeRabbit round 2, first finding: per the repo guideline every public API carries an example. Added compact # Examples blocks (andnot set difference; ternlog MAJ3 via the named facade immediate) to U64x8/U32x16 on scalar, avx2, avx512, nightly, and to U32x16 on neon and wasm. The avx512 examples execute in this environment's doc-test run (512 passed); the cfg-gated arms compile their examples only under their own targets. The second finding (do not extend the nightly-only backend) is declined with reasons on the PR thread: the nightly arm pre-exists behind the off-by-default nightly-simd feature, and codex's P1 in the same review round requires the methods on every dispatched arm precisely so no feature combination compiles into E0599. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp --- src/simd_avx2.rs | 36 ++++++++++++++++++++++++++++++++ src/simd_avx512.rs | 36 ++++++++++++++++++++++++++++++++ src/simd_neon.rs | 18 ++++++++++++++++ src/simd_nightly/u_word_types.rs | 36 ++++++++++++++++++++++++++++++++ src/simd_scalar.rs | 36 ++++++++++++++++++++++++++++++++ src/simd_wasm.rs | 18 ++++++++++++++++ 6 files changed, 180 insertions(+) diff --git a/src/simd_avx2.rs b/src/simd_avx2.rs index e01474a8..9f372b6b 100644 --- a/src/simd_avx2.rs +++ b/src/simd_avx2.rs @@ -3523,6 +3523,15 @@ impl U64x8 { /// /// 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 @@ -3537,6 +3546,15 @@ impl U64x8 { /// `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") } @@ -3572,12 +3590,30 @@ impl U64x8 { 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") } diff --git a/src/simd_avx512.rs b/src/simd_avx512.rs index f435f7b5..89dc104b 100644 --- a/src/simd_avx512.rs +++ b/src/simd_avx512.rs @@ -4892,6 +4892,15 @@ impl U64x8 { /// /// 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 = @@ -4912,6 +4921,15 @@ impl U64x8 { /// 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 @@ -4923,6 +4941,15 @@ impl U64x8 { 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 @@ -4932,6 +4959,15 @@ impl U32x16 { /// 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 diff --git a/src/simd_neon.rs b/src/simd_neon.rs index 806125e3..9c95f3e1 100644 --- a/src/simd_neon.rs +++ b/src/simd_neon.rs @@ -1845,6 +1845,15 @@ impl U32x16 { /// 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()); @@ -1860,6 +1869,15 @@ impl U32x16 { /// 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") } diff --git a/src/simd_nightly/u_word_types.rs b/src/simd_nightly/u_word_types.rs index 066cb9fc..00f03b6e 100644 --- a/src/simd_nightly/u_word_types.rs +++ b/src/simd_nightly/u_word_types.rs @@ -886,6 +886,15 @@ mod tests { 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) @@ -896,6 +905,15 @@ impl U64x8 { /// 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") } @@ -932,6 +950,15 @@ impl U64x8 { 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) @@ -942,6 +969,15 @@ impl U32x16 { /// 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") } diff --git a/src/simd_scalar.rs b/src/simd_scalar.rs index 7bc0839e..a63810b9 100644 --- a/src/simd_scalar.rs +++ b/src/simd_scalar.rs @@ -2051,6 +2051,15 @@ impl U64x8 { /// /// 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 @@ -2065,6 +2074,15 @@ impl U64x8 { /// `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)); @@ -2099,12 +2117,30 @@ impl U64x8 { 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") } diff --git a/src/simd_wasm.rs b/src/simd_wasm.rs index a0605e1d..5fb9f082 100644 --- a/src/simd_wasm.rs +++ b/src/simd_wasm.rs @@ -960,6 +960,15 @@ pub mod wasm32_simd { /// 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([ @@ -976,6 +985,15 @@ pub mod wasm32_simd { /// 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") }