Skip to content

Commit d1f6a6d

Browse files
authored
Merge pull request #301 from AdaWorldAPI/claude/pr-294-ragged-path-validation-170zcy
simd: mask_ternlog family + contiguous fast path for stride-4 eq masks
2 parents fe509db + 1493a10 commit d1f6a6d

3 files changed

Lines changed: 331 additions & 7 deletions

File tree

‎.claude/blackboard.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,3 +1094,48 @@ per target — every arm confirmed to carry `Not`). Deviation was
10941094
council-surfaced (5+3, S2-7) and operator-visible, not smuggled.
10951095

10961096
Loose ends: none.
1097+
1098+
## 2026-09-04 — mask_ternlog / mask_ternlog_assign + contiguous fast path in eq_u32_strided_to_mask (lance-graph-java lgj_hop)
1099+
1100+
Added `mask_ternlog::<IMM>(a, b, c, dst)` and `mask_ternlog_assign::<IMM>(a, b, c)`
1101+
to `src/simd_int_ops.rs`, re-exported through `ndarray::simd::{mask_ternlog,
1102+
mask_ternlog_assign}`. Consumer: lance-graph-java `lgj_hop`, whose selection
1103+
`selected = class_f ∧ src ∧ struct_f` was two `mask_and_assign` passes through a
1104+
scratch write and is now ONE `AND3` (0x80) pass — the rank-1 spelling of a
1105+
3-input mask op replaced by the op itself.
1106+
1107+
Shape: the general member of the mask-op family (`mask_and` is `AND2` with `c`
1108+
ignored, `mask_andnot` is `AND2_ANDNOT`). `U64x8::ternlog::<IMM>` polyfill
1109+
dispatch (one VPTERNLOGQ per 512 bits on AVX-512; AVX2/NEON/wasm/scalar arms
1110+
already carried `ternlog` from W1a-#9) with a bit-serial scalar tail
1111+
(`ternlog_word`) that doubles as the parity reference. Tail-bit contract stated
1112+
precisely: conforming inputs give a conforming `dst` **iff `IMM` is even** (the
1113+
all-zero row of the truth table); every named immediate in `simd::ternlog` is
1114+
even; the subset-shaped tables inherit `mask_andnot`'s stronger "subset of `a`"
1115+
guarantee. Tests: all 256 tables × 13 lengths against an independent bit-serial
1116+
reference, `AND3 == and∘and` (non-vacuous: both narrowers must contribute),
1117+
tail-conforms-iff-even with the odd (NOR3) can-it-fire arm, length-mismatch
1118+
panics for both forms.
1119+
1120+
**W1a deviation record — same as 2026-08-18 `mask_andnot`:** free-fn family
1121+
shape, not struct-method; a lone method beside six free-fn siblings would
1122+
fragment the `simd.rs` re-export surface. All other W1a criteria in full.
1123+
1124+
**Second change, same PR:** `eq_u32_strided_to_mask` at `stride_bytes == 4` was
1125+
gathering 16 bounds-checked scalar `u32` reads into a temporary array before the
1126+
`U32x16` compare — a copy where a cast belonged. That is the exact shape of
1127+
every facet lane in lance-graph-java's facet-major columnar store (ABI minor
1128+
10), so the store was contiguous and the kernel still read it as strided. Added
1129+
a contiguous path: one fixed `[u8; 64]` window per 16 elements (safe `try_into`,
1130+
bounds proven up front), which the compiler lowers to a single vector load. The
1131+
existing `eq_u32_strided_stride4_matches_contiguous_primitive` parity test
1132+
covers it. Measured through lance-graph-java's own `columnar_hop_bench`
1133+
(65 536 rows, 32 facets, both changes together): `lgj_hop` 6 342/6 408/7 547 µs
1134+
→ **1 203/1 101/1 851 µs** (classid / hop2 / all-rows frontiers), 4.1–5.8×,
1135+
equivalence asserted before timing. 16 MB of lane read in ~1.2 ms ≈ 13 GB/s,
1136+
up from 2.1 GB/s.
1137+
1138+
Loose ends: the general strided path still gathers scalar (correct — at row
1139+
strides ≥ a cache line a hardware gather buys nothing, per the doc); a
1140+
`stride_bytes == 8` twin for `u64` lanes does not exist yet because no caller
1141+
compares `u64` lanes.

‎src/simd.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ pub use crate::simd_amx::{amx_report, cpu_model, CpuModel};
730730
#[cfg(feature = "std")]
731731
pub use crate::simd_int_ops::{
732732
eq_u32_strided_to_mask, eq_u32_to_mask, gt_i32_to_mask, mask_and, mask_and_assign, mask_andnot, mask_andnot_assign,
733-
mask_or, mask_or_assign, masked_strided_group_sum, masked_sum_i32,
733+
mask_or, mask_or_assign, mask_ternlog, mask_ternlog_assign, masked_strided_group_sum, masked_sum_i32,
734734
};
735735
// The popcount that closes the loop on the masks above: `mask_count` in ABI
736736
// terms. Already public at `ndarray::bitwise::popcount_batch_u64`; re-exported

‎src/simd_int_ops.rs‎

Lines changed: 285 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)