3131//! over every pair of 16 IEEE edge values: NaN, ±0, ±inf, subnormal); `0x73x` / `0x74x` / `0x750` the strided 16-byte register match (all sixteen
3232//! bytes; strides 16 / 20 / 32 / 512 from first offsets 0..15, care bits in
3333//! bytes 12..16); `0xFxx` the 16-lane byte vectors `I8x16` / `U8x16` (wrapping
34- //! `add`/`sub`, signed/unsigned `min`/`max`, the load/store round trips); `0x10xx` the saturating
35- //! `I32x16 → I8x16 / U8x16` narrows and the `F32x16 ⇄ I32x16` casts on in-range lanes. `main.rs` (native / qemu) and
34+ //! `add`/`sub`, signed/unsigned `min`/`max`, the load/store round trips); `0x10xx`
35+ //! the saturating narrows down the 512-bit ladder (`I32x16 → I16x32 → I8x64 /
36+ //! U8x64`) and the `F32x16` ↔ `I32x16` casts. `main.rs` (native / qemu) and
3637//! `selfcheck()` (the wasm cdylib export, driven by `run.mjs`) both call
3738//! [`run`].
3839
@@ -49,7 +50,7 @@ use ndarray::simd::{
4950 ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, ne_u64_to_mask, ne_u8_to_mask,
5051 ternary_match_strided16_to_mask, ternary_match_strided_to_mask, ternary_match_u32_to_mask,
5152 ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, ternlog, F32x16 ,
52- F64x8 , I32x16 , I8x16 , KeyRunCarry , MortonDir , U32x16 , U64x8 , U8x16 ,
53+ F64x8 , I16x32 , I32x16 , I8x16 , KeyRunCarry , MortonDir , U32x16 , U64x8 , U8x16 ,
5354} ;
5455
5556/// Number of check groups [`run`] executes (for the log line only).
@@ -67,7 +68,7 @@ pub fn run() -> u32 {
6768 check_ternlog_all_tables, check_u64x8_algebra, check_i32x16_compare, check_predicates_to_mask,
6869 check_mask_algebra, check_care_match, check_masked_reductions, check_blend, check_morton_shift,
6970 check_predicates_under, check_set_range, check_unsigned_compare_to_mask, check_gather_scatter_group,
70- check_f64x8_compare, check_i8x16_u8x16_lanes, check_care_match16, check_narrow_and_casts ,
71+ check_f64x8_compare, check_i8x16_u8x16_lanes, check_care_match16, check_narrow_ladder ,
7172 ] ;
7273 for g in groups {
7374 if let Err ( code) = g ( ) {
@@ -1845,59 +1846,93 @@ fn check_gather_scatter_group() -> Result<(), u32> {
18451846 Ok ( ( ) )
18461847}
18471848
1848- /// `I32x16::narrow_saturate_i8` / `narrow_saturate_u8` against `clamp`, and the
1849- /// `F32x16::cast_i32` / `I32x16::cast_f32` casts against `as` on in-range lanes.
1850- fn check_narrow_and_casts ( ) -> Result < ( ) , u32 > {
1851- let edges: [ i32 ; 16 ] =
1852- [ i32:: MIN , -1_000_000 , -256 , -129 , -128 , -127 , -1 , 0 , 1 , 126 , 127 , 128 , 254 , 255 , 256 , i32:: MAX ] ;
1853- let mut rng = SplitMix64 ( 0x0C0D_EC08_0000_0001 ) ;
1854- let mut vecs: Vec < [ i32 ; 16 ] > = vec ! [ edges] ;
1855- for k in 0 ..512 {
1856- vecs. push ( core:: array:: from_fn ( |_| {
1857- let r = rng. next ( ) ;
1858- // Half the vectors draw from the whole i32 range, half from the saturation band.
1859- if k % 2 == 0 {
1860- r as i32
1849+ /// The saturating narrows down the 512-bit ladder (16×32 → 32×16 → 64×8) and the in-range
1850+ /// f32 ↔ i32 casts, against `clamp` / `as` references.
1851+ fn check_narrow_ladder ( ) -> Result < ( ) , u32 > {
1852+ let mut rng = SplitMix64 ( 0x1000_0000_0017 ) ;
1853+ let edges32 =
1854+ [ i32:: MIN , i32:: MIN + 1 , -32769 , -32768 , -32767 , -256 , -129 , -128 , -127 , -1 , 0 , 1 , 127 , 128 , 255 , 256 ] ;
1855+ let edges16 = [ i16:: MIN , -32767 , -300 , -256 , -129 , -128 , -127 , -1 , 0 , 1 , 127 , 128 , 254 , 255 , 256 , i16:: MAX ] ;
1856+ let ( mut sat, mut kept) = ( 0usize , 0usize ) ;
1857+ for round in 0 ..64u32 {
1858+ // Round 0 is the edge set; later rounds mix random words with values near the seams.
1859+ let a: [ i32 ; 32 ] = core:: array:: from_fn ( |i| match ( round, rng. next ( ) ) {
1860+ ( 0 , _) => edges32[ i % 16 ] . wrapping_add ( if i >= 16 { 1 } else { 0 } ) ,
1861+ ( _, r) if r % 3 == 0 => r as i32 ,
1862+ ( _, r) => ( r as i32 ) >> ( 16 + ( r >> 40 ) % 16 ) ,
1863+ } ) ;
1864+ let lo = I32x16 :: from_array ( a[ ..16 ] . try_into ( ) . unwrap ( ) ) ;
1865+ let hi = I32x16 :: from_array ( a[ 16 ..] . try_into ( ) . unwrap ( ) ) ;
1866+ let n16 = lo. narrow_saturating_i16 ( hi) . to_array ( ) ;
1867+ for i in 0 ..32 {
1868+ let want = a[ i] . clamp ( i16:: MIN as i32 , i16:: MAX as i32 ) as i16 ;
1869+ if n16[ i] != want {
1870+ return Err ( 0x1000 + i as u32 ) ;
1871+ }
1872+ if want as i32 == a[ i] {
1873+ kept += 1
18611874 } else {
1862- ( r % 601 ) as i32 - 300
1875+ sat += 1
18631876 }
1864- } ) ) ;
1865- }
1866- let ( mut below, mut above) = ( 0usize , 0usize ) ;
1867- for v in & vecs {
1868- let x = I32x16 :: from_array ( * v) ;
1869- let want_i8: [ i8 ; 16 ] = core:: array:: from_fn ( |i| v[ i] . clamp ( -128 , 127 ) as i8 ) ;
1870- let want_u8: [ u8 ; 16 ] = core:: array:: from_fn ( |i| v[ i] . clamp ( 0 , 255 ) as u8 ) ;
1871- if x. narrow_saturate_i8 ( ) . to_array ( ) != want_i8 {
1872- return Err ( 0x1001 ) ;
18731877 }
1874- if x. narrow_saturate_u8 ( ) . to_array ( ) != want_u8 {
1875- return Err ( 0x1002 ) ;
1878+ // Chained to 8 bits: two saturating steps equal one clamp to the narrow range.
1879+ let n16v = I16x32 :: from_array ( n16) ;
1880+ let back: [ i16 ; 32 ] = core:: array:: from_fn ( |i| n16[ 31 - i] ) ;
1881+ let ( i8s, u8s) = (
1882+ n16v. narrow_saturating_i8 ( I16x32 :: from_array ( back) )
1883+ . to_array ( ) ,
1884+ n16v. narrow_saturating_u8 ( I16x32 :: from_array ( back) )
1885+ . to_array ( ) ,
1886+ ) ;
1887+ for i in 0 ..64 {
1888+ let src = if i < 32 {
1889+ a[ i]
1890+ } else {
1891+ a[ 63 - i] . clamp ( i16:: MIN as i32 , i16:: MAX as i32 )
1892+ } ;
1893+ if i8s[ i] != src. clamp ( -128 , 127 ) as i8 {
1894+ return Err ( 0x1040 + i as u32 ) ;
1895+ }
1896+ if u8s[ i] != src. clamp ( 0 , 255 ) as u8 {
1897+ return Err ( 0x1080 + i as u32 ) ;
1898+ }
18761899 }
1877- below += v. iter ( ) . filter ( |& & a| a < -128 ) . count ( ) ;
1878- above += v. iter ( ) . filter ( |& & a| a > 255 ) . count ( ) ;
1879- let want_f: [ u32 ; 16 ] = core:: array:: from_fn ( |i| ( v[ i] as f32 ) . to_bits ( ) ) ;
1880- if x. cast_f32 ( ) . to_array ( ) . map ( f32:: to_bits) != want_f {
1881- return Err ( 0x1003 ) ;
1900+ }
1901+ // The i16 edge set directly, both halves.
1902+ let e = I16x32 :: from_array ( core:: array:: from_fn ( |i| edges16[ i % 16 ] ) ) ;
1903+ let f = I16x32 :: from_array ( core:: array:: from_fn ( |i| edges16[ 15 - i % 16 ] ) ) ;
1904+ let ( i8s, u8s) = ( e. narrow_saturating_i8 ( f) . to_array ( ) , e. narrow_saturating_u8 ( f) . to_array ( ) ) ;
1905+ for i in 0 ..64 {
1906+ let v = if i < 32 {
1907+ edges16[ i % 16 ]
1908+ } else {
1909+ edges16[ 15 - ( i - 32 ) % 16 ]
1910+ } ;
1911+ if i8s[ i] != v. clamp ( -128 , 127 ) as i8 || u8s[ i] != v. clamp ( 0 , 255 ) as u8 {
1912+ return Err ( 0x10C0 + i as u32 ) ;
18821913 }
18831914 }
1884- // Anti-vacuity: the corpus must actually saturate in both directions .
1885- if below < 1000 || above < 1000 {
1886- return Err ( 0x10FF ) ;
1915+ // Anti-vacuity: the rounds must both saturate and pass lanes through .
1916+ if sat < 64 || kept < 64 {
1917+ return Err ( 0x10F0 ) ;
18871918 }
1888- // In-range f32 → i32 truncation, including negative fractions, ±0 and the ±2^31 edges.
1889- let fedges: [ f32 ; 16 ] = [
1890- -0.0 , 0.0 , 0.5 , -0.5 , 0.99999994 , -0.99999994 , 1.5 , -1.5 , 127.5 , -128.5 , 255.9 , -2147483520.0 , 2147483520.0 ,
1891- 8388607.5 , -8388607.5 , 1e9 ,
1892- ] ;
1893- let mut fvecs: Vec < [ f32 ; 16 ] > = vec ! [ fedges] ;
1894- for _ in 0 ..512 {
1895- fvecs. push ( core:: array:: from_fn ( |_| ( ( rng. next ( ) >> 11 ) as f64 / ( 1u64 << 53 ) as f64 * 4.0e9 - 2.0e9 ) as f32 ) ) ;
1896- }
1897- for v in & fvecs {
1898- let want: [ i32 ; 16 ] = core:: array:: from_fn ( |i| v[ i] as i32 ) ;
1899- if F32x16 :: from_array ( * v) . cast_i32 ( ) . to_array ( ) != want {
1900- return Err ( 0x1004 ) ;
1919+ // Casts, in range only (out-of-range and NaN differ by backend by design).
1920+ for _ in 0 ..64 {
1921+ let v: [ i32 ; 16 ] = core:: array:: from_fn ( |_| ( rng. next ( ) as i32 ) >> ( rng. next ( ) % 31 ) ) ;
1922+ let f = I32x16 :: from_array ( v) . cast_f32 ( ) . to_array ( ) ;
1923+ for i in 0 ..16 {
1924+ if f[ i] . to_bits ( ) != ( v[ i] as f32 ) . to_bits ( ) {
1925+ return Err ( 0x10F1 ) ;
1926+ }
1927+ }
1928+ let x: [ f32 ; 16 ] = core:: array:: from_fn ( |_| {
1929+ f32:: from_bits ( ( rng. next ( ) as u32 & 0x807F_FFFF ) | ( ( ( rng. next ( ) % 31 ) as u32 + 112 ) << 23 ) )
1930+ } ) ;
1931+ let t = F32x16 :: from_array ( x) . cast_i32 ( ) . to_array ( ) ;
1932+ for i in 0 ..16 {
1933+ if t[ i] != x[ i] as i32 {
1934+ return Err ( 0x10F2 ) ;
1935+ }
19011936 }
19021937 }
19031938 Ok ( ( ) )
0 commit comments