@@ -590,7 +590,8 @@ pub fn eq_u32_to_mask(values: &[u32], needle: u32, out_words: &mut [u64]) {
590590///
591591/// Element `i` is the little-endian `u32` at byte offset
592592/// `first_offset + i * stride_bytes`. `stride_bytes == 4` reads a contiguous
593- /// `u32` column (then [`eq_u32_to_mask`] is the better call); `stride_bytes
593+ /// `u32` column and takes a dedicated contiguous path (one 64-byte window per
594+ /// 16 elements — the same load shape as [`eq_u32_to_mask`]); `stride_bytes
594595/// == 0` re-reads the same field `count` times, which is legal and produces
595596/// an all-ones or all-zeros mask.
596597///
@@ -666,11 +667,30 @@ pub fn eq_u32_strided_to_mask(
666667
667668 let needle_v = crate :: simd:: U32x16 :: splat ( needle) ;
668669 let groups = count / 16 ;
669- for g in 0 ..groups {
670- let base = first_offset + g * 16 * stride_bytes;
671- let lanes: [ u32 ; 16 ] = core:: array:: from_fn ( |k| read_le_u32 ( bytes, base + k * stride_bytes) ) ;
672- let bits = crate :: simd:: U32x16 :: from_array ( lanes) . eq_bitmask ( needle_v) ;
673- out_words[ g / 4 ] |= ( bits as u64 ) << ( ( g % 4 ) * 16 ) ;
670+ if stride_bytes == 4 {
671+ // Contiguous lane (a facet-major column): 16 elements are ONE 64-byte
672+ // window. Alias it as a fixed-size array so the compiler emits a single
673+ // vector load instead of the general path's 16 bounds-checked scalar
674+ // reads gathered into a temporary — a cast, not a copy. Bounds were
675+ // proven above for the last element, so `try_into` cannot fail here.
676+ for g in 0 ..groups {
677+ let base = first_offset + g * 64 ;
678+ let window: & [ u8 ; 64 ] = bytes[ base..base + 64 ]
679+ . try_into ( )
680+ . expect ( "64-byte window proven in bounds" ) ;
681+ let lanes: [ u32 ; 16 ] = core:: array:: from_fn ( |k| {
682+ u32:: from_le_bytes ( [ window[ 4 * k] , window[ 4 * k + 1 ] , window[ 4 * k + 2 ] , window[ 4 * k + 3 ] ] )
683+ } ) ;
684+ let bits = crate :: simd:: U32x16 :: from_array ( lanes) . eq_bitmask ( needle_v) ;
685+ out_words[ g / 4 ] |= ( bits as u64 ) << ( ( g % 4 ) * 16 ) ;
686+ }
687+ } else {
688+ for g in 0 ..groups {
689+ let base = first_offset + g * 16 * stride_bytes;
690+ let lanes: [ u32 ; 16 ] = core:: array:: from_fn ( |k| read_le_u32 ( bytes, base + k * stride_bytes) ) ;
691+ let bits = crate :: simd:: U32x16 :: from_array ( lanes) . eq_bitmask ( needle_v) ;
692+ out_words[ g / 4 ] |= ( bits as u64 ) << ( ( g % 4 ) * 16 ) ;
693+ }
674694 }
675695 for i in ( groups * 16 ) ..count {
676696 if read_le_u32 ( bytes, first_offset + i * stride_bytes) == needle {
@@ -926,6 +946,126 @@ pub fn mask_andnot_assign(a: &mut [u64], b: &[u64]) {
926946 }
927947}
928948
949+ /// `dst = ternlog::<IMM>(a, b, c)`, elementwise over `u64` mask words — any
950+ /// 3-input Boolean function of three masks in one pass.
951+ ///
952+ /// `IMM` is the 8-bit truth table in Intel's VPTERNLOG convention (index
953+ /// `(a<<2)|(b<<1)|c`, result bit `(IMM >> index) & 1`); the named tables in
954+ /// [`crate::simd::ternlog`] (`AND3`, `OR3`, `MAJ3`, `AND2_ANDNOT`, …) are
955+ /// the sanctioned spellings. This is the mask-op family's general member:
956+ /// [`mask_and`] is `mask_ternlog::<{ ternlog::AND2 }>` with `c` ignored,
957+ /// [`mask_andnot`] is `AND2_ANDNOT` with `c` ignored, and the composed
958+ /// `a & b & c` that a consumer would otherwise spell as two `mask_and_assign`
959+ /// passes through a scratch buffer is ONE `AND3` pass here — one
960+ /// `VPTERNLOGQ` per 512 bits on AVX-512, the polyfill elsewhere.
961+ ///
962+ /// # Tail-bit semantics
963+ ///
964+ /// Whether `dst`'s tail conforms depends on the truth table, not on the
965+ /// inputs alone: the tail of every conforming input is zero, so `dst`'s tail
966+ /// is `IMM & 1` replicated — **zero iff `IMM` is even** (index 0 = all-zero
967+ /// inputs maps to 0). Every named table in [`crate::simd::ternlog`] is even.
968+ /// An odd `IMM` (one whose function is true of `(0,0,0)`) sets every tail bit
969+ /// and the caller must clear the tail against its own known row count, exactly
970+ /// as [`mask_or`] documents for a non-conforming operand. For the
971+ /// subset-shaped tables (`AND3`, `AND2_ANDNOT`, `AND_ANDNOT2`, `AND2`) the
972+ /// stronger [`mask_andnot`] guarantee also holds: the result is a bitwise
973+ /// subset of `a`, so `dst`'s tail is zero whenever `a`'s is, regardless of
974+ /// `b` and `c`.
975+ ///
976+ /// `dst` must not overlap `a`, `b` or `c`; use [`mask_ternlog_assign`] for
977+ /// the in-place case.
978+ ///
979+ /// # Panics
980+ ///
981+ /// Panics unless `a.len() == b.len() == c.len() == dst.len()`.
982+ #[ inline]
983+ pub fn mask_ternlog < const IMM : i32 > ( a : & [ u64 ] , b : & [ u64 ] , c : & [ u64 ] , dst : & mut [ u64 ] ) {
984+ assert_eq ! ( a. len( ) , b. len( ) , "mask_ternlog: a/b length mismatch" ) ;
985+ assert_eq ! ( a. len( ) , c. len( ) , "mask_ternlog: a/c length mismatch" ) ;
986+ assert_eq ! ( a. len( ) , dst. len( ) , "mask_ternlog: a/dst length mismatch" ) ;
987+ let n = a. len ( ) ;
988+
989+ const L : usize = crate :: simd:: U64x8 :: LANES ;
990+ let groups = n / L ;
991+ for g in 0 ..groups {
992+ let off = g * L ;
993+ let va = crate :: simd:: U64x8 :: from_slice ( & a[ off..] ) ;
994+ let vb = crate :: simd:: U64x8 :: from_slice ( & b[ off..] ) ;
995+ let vc = crate :: simd:: U64x8 :: from_slice ( & c[ off..] ) ;
996+ va. ternlog :: < IMM > ( vb, vc) . copy_to_slice ( & mut dst[ off..] ) ;
997+ }
998+ for i in ( groups * L ) ..n {
999+ dst[ i] = ternlog_word :: < IMM > ( a[ i] , b[ i] , c[ i] ) ;
1000+ }
1001+ }
1002+
1003+ /// `a = ternlog::<IMM>(a, b, c)`, elementwise over `u64` mask words.
1004+ ///
1005+ /// The in-place form of [`mask_ternlog`] — `a` is the first truth-table
1006+ /// operand AND the destination, which is the shape a fused predicate plan
1007+ /// wants when narrowing an accumulator against two more masks in one pass
1008+ /// (`selected = selected & src & gate` as `AND3`). Same tail contract as
1009+ /// [`mask_ternlog`].
1010+ ///
1011+ /// # Panics
1012+ ///
1013+ /// Panics unless `a.len() == b.len() == c.len()`.
1014+ #[ inline]
1015+ pub fn mask_ternlog_assign < const IMM : i32 > ( a : & mut [ u64 ] , b : & [ u64 ] , c : & [ u64 ] ) {
1016+ assert_eq ! ( a. len( ) , b. len( ) , "mask_ternlog_assign: a/b length mismatch" ) ;
1017+ assert_eq ! ( a. len( ) , c. len( ) , "mask_ternlog_assign: a/c length mismatch" ) ;
1018+ let n = a. len ( ) ;
1019+
1020+ const L : usize = crate :: simd:: U64x8 :: LANES ;
1021+ let groups = n / L ;
1022+ for g in 0 ..groups {
1023+ let off = g * L ;
1024+ let va = crate :: simd:: U64x8 :: from_slice ( & a[ off..] ) ;
1025+ let vb = crate :: simd:: U64x8 :: from_slice ( & b[ off..] ) ;
1026+ let vc = crate :: simd:: U64x8 :: from_slice ( & c[ off..] ) ;
1027+ va. ternlog :: < IMM > ( vb, vc) . copy_to_slice ( & mut a[ off..] ) ;
1028+ }
1029+ for i in ( groups * L ) ..n {
1030+ a[ i] = ternlog_word :: < IMM > ( a[ i] , b[ i] , c[ i] ) ;
1031+ }
1032+ }
1033+
1034+ /// One `u64` of the truth-table function — the scalar tail of the two
1035+ /// `mask_ternlog` forms, and the independent reference their parity test is
1036+ /// checked against. Bit-serial over the eight table rows, so it cannot share
1037+ /// a bug with any backend's lane implementation.
1038+ #[ inline( always) ]
1039+ fn ternlog_word < const IMM : i32 > ( a : u64 , b : u64 , c : u64 ) -> u64 {
1040+ const { assert ! ( IMM >= 0 && IMM <= 255 , "ternlog IMM is an 8-bit truth table" ) }
1041+ let mut r = 0u64 ;
1042+ if IMM & 0x01 != 0 {
1043+ r |= !a & !b & !c;
1044+ }
1045+ if IMM & 0x02 != 0 {
1046+ r |= !a & !b & c;
1047+ }
1048+ if IMM & 0x04 != 0 {
1049+ r |= !a & b & !c;
1050+ }
1051+ if IMM & 0x08 != 0 {
1052+ r |= !a & b & c;
1053+ }
1054+ if IMM & 0x10 != 0 {
1055+ r |= a & !b & !c;
1056+ }
1057+ if IMM & 0x20 != 0 {
1058+ r |= a & !b & c;
1059+ }
1060+ if IMM & 0x40 != 0 {
1061+ r |= a & b & !c;
1062+ }
1063+ if IMM & 0x80 != 0 {
1064+ r |= a & b & c;
1065+ }
1066+ r
1067+ }
1068+
9291069/// Sum of `values[i]` where mask bit `i` is set, widened to `i64`.
9301070///
9311071/// Bit order is the module convention: element `i` is bit `i % 64` of
@@ -2069,6 +2209,145 @@ mod tests {
20692209 mask_andnot_assign ( & mut a, & [ 0u64 ; 3 ] ) ;
20702210 }
20712211
2212+ // ── mask_ternlog (any 3-input Boolean, one pass) ─────────────────────────
2213+
2214+ /// Truth-table reference evaluated one BIT at a time — independent of
2215+ /// both `ternlog_word` (bit-parallel over rows) and every backend lane.
2216+ fn ref_ternlog_bitwise ( a : u64 , b : u64 , c : u64 , imm : i32 ) -> u64 {
2217+ let mut r = 0u64 ;
2218+ for bit in 0 ..64 {
2219+ let idx = ( ( ( a >> bit) & 1 ) << 2 ) | ( ( ( b >> bit) & 1 ) << 1 ) | ( ( c >> bit) & 1 ) ;
2220+ if ( imm >> idx) & 1 == 1 {
2221+ r |= 1u64 << bit;
2222+ }
2223+ }
2224+ r
2225+ }
2226+
2227+ /// Exercise one IMM over the family's standard length set, both forms,
2228+ /// against the bit-serial reference.
2229+ fn check_ternlog_imm < const IMM : i32 > ( ) {
2230+ for & len in & [ 0usize , 1 , 2 , 7 , 8 , 9 , 15 , 16 , 17 , 31 , 63 , 64 , 100 ] {
2231+ let mut seed = 0x7E12_10C0_0000_0001 ^ ( IMM as u64 ) ;
2232+ let a: Vec < u64 > = ( 0 ..len) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2233+ let b: Vec < u64 > = ( 0 ..len) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2234+ let c: Vec < u64 > = ( 0 ..len) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2235+ let expect: Vec < u64 > = ( 0 ..len)
2236+ . map ( |i| ref_ternlog_bitwise ( a[ i] , b[ i] , c[ i] , IMM ) )
2237+ . collect ( ) ;
2238+
2239+ let mut dst = vec ! [ 0xDEAD_BEEFu64 ; len] ;
2240+ mask_ternlog :: < IMM > ( & a, & b, & c, & mut dst) ;
2241+ assert_eq ! ( dst, expect, "mask_ternlog imm={IMM:#04x} len={len}" ) ;
2242+
2243+ let mut dst = a. clone ( ) ;
2244+ mask_ternlog_assign :: < IMM > ( & mut dst, & b, & c) ;
2245+ assert_eq ! ( dst, expect, "mask_ternlog_assign imm={IMM:#04x} len={len}" ) ;
2246+ }
2247+ }
2248+
2249+ #[ test]
2250+ fn mask_ternlog_matches_bitwise_reference_for_all_256_tables ( ) {
2251+ // Const generics need a literal per instantiation; a macro unrolls
2252+ // all 256 so no table is left to "obviously the same as the others".
2253+ macro_rules! all_imms {
2254+ ( $( $imm: literal) ,* $( , ) ?) => { $( check_ternlog_imm:: <$imm>( ) ; ) * } ;
2255+ }
2256+ all_imms ! (
2257+ 0x00 , 0x01 , 0x02 , 0x03 , 0x04 , 0x05 , 0x06 , 0x07 , 0x08 , 0x09 , 0x0A , 0x0B , 0x0C , 0x0D , 0x0E , 0x0F , 0x10 , 0x11 ,
2258+ 0x12 , 0x13 , 0x14 , 0x15 , 0x16 , 0x17 , 0x18 , 0x19 , 0x1A , 0x1B , 0x1C , 0x1D , 0x1E , 0x1F , 0x20 , 0x21 , 0x22 , 0x23 ,
2259+ 0x24 , 0x25 , 0x26 , 0x27 , 0x28 , 0x29 , 0x2A , 0x2B , 0x2C , 0x2D , 0x2E , 0x2F , 0x30 , 0x31 , 0x32 , 0x33 , 0x34 , 0x35 ,
2260+ 0x36 , 0x37 , 0x38 , 0x39 , 0x3A , 0x3B , 0x3C , 0x3D , 0x3E , 0x3F , 0x40 , 0x41 , 0x42 , 0x43 , 0x44 , 0x45 , 0x46 , 0x47 ,
2261+ 0x48 , 0x49 , 0x4A , 0x4B , 0x4C , 0x4D , 0x4E , 0x4F , 0x50 , 0x51 , 0x52 , 0x53 , 0x54 , 0x55 , 0x56 , 0x57 , 0x58 , 0x59 ,
2262+ 0x5A , 0x5B , 0x5C , 0x5D , 0x5E , 0x5F , 0x60 , 0x61 , 0x62 , 0x63 , 0x64 , 0x65 , 0x66 , 0x67 , 0x68 , 0x69 , 0x6A , 0x6B ,
2263+ 0x6C , 0x6D , 0x6E , 0x6F , 0x70 , 0x71 , 0x72 , 0x73 , 0x74 , 0x75 , 0x76 , 0x77 , 0x78 , 0x79 , 0x7A , 0x7B , 0x7C , 0x7D ,
2264+ 0x7E , 0x7F , 0x80 , 0x81 , 0x82 , 0x83 , 0x84 , 0x85 , 0x86 , 0x87 , 0x88 , 0x89 , 0x8A , 0x8B , 0x8C , 0x8D , 0x8E , 0x8F ,
2265+ 0x90 , 0x91 , 0x92 , 0x93 , 0x94 , 0x95 , 0x96 , 0x97 , 0x98 , 0x99 , 0x9A , 0x9B , 0x9C , 0x9D , 0x9E , 0x9F , 0xA0 , 0xA1 ,
2266+ 0xA2 , 0xA3 , 0xA4 , 0xA5 , 0xA6 , 0xA7 , 0xA8 , 0xA9 , 0xAA , 0xAB , 0xAC , 0xAD , 0xAE , 0xAF , 0xB0 , 0xB1 , 0xB2 , 0xB3 ,
2267+ 0xB4 , 0xB5 , 0xB6 , 0xB7 , 0xB8 , 0xB9 , 0xBA , 0xBB , 0xBC , 0xBD , 0xBE , 0xBF , 0xC0 , 0xC1 , 0xC2 , 0xC3 , 0xC4 , 0xC5 ,
2268+ 0xC6 , 0xC7 , 0xC8 , 0xC9 , 0xCA , 0xCB , 0xCC , 0xCD , 0xCE , 0xCF , 0xD0 , 0xD1 , 0xD2 , 0xD3 , 0xD4 , 0xD5 , 0xD6 , 0xD7 ,
2269+ 0xD8 , 0xD9 , 0xDA , 0xDB , 0xDC , 0xDD , 0xDE , 0xDF , 0xE0 , 0xE1 , 0xE2 , 0xE3 , 0xE4 , 0xE5 , 0xE6 , 0xE7 , 0xE8 , 0xE9 ,
2270+ 0xEA , 0xEB , 0xEC , 0xED , 0xEE , 0xEF , 0xF0 , 0xF1 , 0xF2 , 0xF3 , 0xF4 , 0xF5 , 0xF6 , 0xF7 , 0xF8 , 0xF9 , 0xFA , 0xFB ,
2271+ 0xFC , 0xFD , 0xFE , 0xFF ,
2272+ ) ;
2273+ }
2274+
2275+ #[ test]
2276+ fn mask_ternlog_and3_equals_two_and_passes ( ) {
2277+ // The consumer motivation: `selected & src & gate` as one AND3 pass
2278+ // must be bit-identical to the two-pass `mask_and_assign` spelling.
2279+ use crate :: simd:: ternlog:: AND3 ;
2280+ let mut seed = 0xA3D3_0000_0000_0001 ;
2281+ let sel: Vec < u64 > = ( 0 ..20 ) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2282+ let src: Vec < u64 > = ( 0 ..20 ) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2283+ let gate: Vec < u64 > = ( 0 ..20 ) . map ( |_| splitmix64 ( & mut seed) ) . collect ( ) ;
2284+
2285+ let mut two_pass = sel. clone ( ) ;
2286+ mask_and_assign ( & mut two_pass, & src) ;
2287+ mask_and_assign ( & mut two_pass, & gate) ;
2288+
2289+ let mut one_pass = sel. clone ( ) ;
2290+ mask_ternlog_assign :: < AND3 > ( & mut one_pass, & src, & gate) ;
2291+ assert_eq ! ( one_pass, two_pass, "AND3 == and∘and" ) ;
2292+
2293+ // Non-vacuous: the narrowing must have removed bits, and both narrower
2294+ // operands must have contributed (each alone leaves a different set).
2295+ assert_ne ! ( one_pass, sel, "AND3 must narrow on this corpus" ) ;
2296+ let mut src_only = sel. clone ( ) ;
2297+ mask_and_assign ( & mut src_only, & src) ;
2298+ assert_ne ! ( one_pass, src_only, "gate must contribute, not just src" ) ;
2299+ }
2300+
2301+ #[ test]
2302+ fn mask_ternlog_tail_conforms_iff_imm_is_even ( ) {
2303+ // 2 words = the `mask_words_for(70)` shape; tail = word 1 bits 7..63.
2304+ const TAIL_MASK : u64 = !0x7Fu64 ;
2305+ let a = [ 0x1234_5678_9ABC_DEF0u64 , 0x0000_0000_0000_005Bu64 ] ;
2306+ let b = [ 0x0F0F_0F0F_0F0F_0F0Fu64 , 0x0000_0000_0000_0071u64 ] ;
2307+ let c = [ 0xFFFF_0000_FFFF_0000u64 , 0x0000_0000_0000_002Eu64 ] ;
2308+ for m in [ & a, & b, & c] {
2309+ assert_eq ! ( m[ 1 ] & TAIL_MASK , 0 , "fixture precondition: conforming inputs" ) ;
2310+ }
2311+ use crate :: simd:: ternlog:: { AND3 , MAJ3 , OR3 , XOR3 } ;
2312+
2313+ // Every named table is even: tail stays zero.
2314+ let mut d = [ 0xDEAD_BEEFu64 ; 2 ] ;
2315+ mask_ternlog :: < AND3 > ( & a, & b, & c, & mut d) ;
2316+ assert_eq ! ( d[ 1 ] & TAIL_MASK , 0 , "AND3 tail" ) ;
2317+ mask_ternlog :: < OR3 > ( & a, & b, & c, & mut d) ;
2318+ assert_eq ! ( d[ 1 ] & TAIL_MASK , 0 , "OR3 tail" ) ;
2319+ mask_ternlog :: < MAJ3 > ( & a, & b, & c, & mut d) ;
2320+ assert_eq ! ( d[ 1 ] & TAIL_MASK , 0 , "MAJ3 tail" ) ;
2321+ mask_ternlog :: < XOR3 > ( & a, & b, & c, & mut d) ;
2322+ assert_eq ! ( d[ 1 ] & TAIL_MASK , 0 , "XOR3 tail" ) ;
2323+
2324+ // The can-it-fire half: an ODD table (NOR3 = 0x01, true of all-zero
2325+ // inputs) sets every tail bit, so the doc's "iff even" is a real
2326+ // boundary and not a restatement of the inputs.
2327+ mask_ternlog :: < 0x01 > ( & a, & b, & c, & mut d) ;
2328+ assert_eq ! ( d[ 1 ] & TAIL_MASK , TAIL_MASK , "odd IMM fills the tail" ) ;
2329+
2330+ // Subset-shaped table against dirty b/c: still a subset of a.
2331+ let dirty = [ u64:: MAX ; 2 ] ;
2332+ mask_ternlog :: < AND3 > ( & a, & dirty, & dirty, & mut d) ;
2333+ assert_eq ! ( d, a, "AND3 against all-ones is a" ) ;
2334+ assert_eq ! ( d[ 1 ] & TAIL_MASK , 0 , "AND3 tail follows a's tail" ) ;
2335+ }
2336+
2337+ #[ test]
2338+ #[ should_panic( expected = "length mismatch" ) ]
2339+ fn mask_ternlog_rejects_length_mismatch ( ) {
2340+ let mut dst = [ 0u64 ; 4 ] ;
2341+ mask_ternlog :: < 0x80 > ( & [ 0u64 ; 4 ] , & [ 0u64 ; 4 ] , & [ 0u64 ; 3 ] , & mut dst) ;
2342+ }
2343+
2344+ #[ test]
2345+ #[ should_panic( expected = "length mismatch" ) ]
2346+ fn mask_ternlog_assign_rejects_length_mismatch ( ) {
2347+ let mut a = [ 0u64 ; 4 ] ;
2348+ mask_ternlog_assign :: < 0x80 > ( & mut a, & [ 0u64 ; 3 ] , & [ 0u64 ; 4 ] ) ;
2349+ }
2350+
20722351 #[ test]
20732352 #[ should_panic( expected = "out_words.len()" ) ]
20742353 fn eq_u32_to_mask_rejects_short_destination ( ) {
0 commit comments