diff --git a/arrayfire.cabal b/arrayfire.cabal
--- a/arrayfire.cabal
+++ b/arrayfire.cabal
@@ -1,6 +1,6 @@
 cabal-version:       3.0
 name:                arrayfire
-version:             0.8.0.0
+version:             0.9.0.0
 synopsis:            Haskell bindings to the ArrayFire general-purpose GPU library
 homepage:            https://github.com/arrayfire/arrayfire-haskell
 license:             BSD-3-Clause
@@ -185,6 +185,7 @@
     ArrayFire.SignalSpec
     ArrayFire.SparseSpec
     ArrayFire.StatisticsSpec
+    ArrayFire.TestHelper
     ArrayFire.UtilSpec
     ArrayFire.VisionSpec
 
diff --git a/cbits/eigsh.c b/cbits/eigsh.c
--- a/cbits/eigsh.c
+++ b/cbits/eigsh.c
@@ -47,8 +47,24 @@
     memset(v, 0, (size_t)n * n * sizeof(double));
     for (int i = 0; i < n; i++) ELEM(v, i, i, n) = 1.0;
 
-    /* Up to 50 full sweeps; typical convergence is << 10 for moderate n. */
-    for (int sweep = 0; sweep < 50 * n; sweep++) {
+    /* Scale-invariant convergence threshold. */
+    double amax = 0.0;
+    for (int c = 0; c < n; c++)
+        for (int r = 0; r < n; r++) {
+            double val = fabs(ELEM(a, r, c, n));
+            if (val > amax) amax = val;
+        }
+    double tol = 1e-14 * (amax > 0.0 ? amax : 1.0);
+
+    /* Classical Jacobi performs one rotation per iteration; a sweep is
+     * ~n^2/2 rotations and convergence typically needs O(log) sweeps, so
+     * 10*n*n rotations is a generous budget. Hitting it means we failed to
+     * converge and must report an error rather than silently return
+     * inaccurate results (the old cap of 50*n was routinely exhausted for
+     * n in the low hundreds). */
+    long max_rot   = 10L * n * n + 100;
+    int  converged = (n <= 1);
+    for (long rot = 0; rot < max_rot; rot++) {
         /* Locate largest off-diagonal element */
         int p = 0, q = 1;
         double max_off = 0.0;
@@ -58,7 +74,7 @@
                 if (val > max_off) { max_off = val; p = r; q = c; }
             }
         }
-        if (max_off < 1e-14) break;
+        if (max_off < tol) { converged = 1; break; }
 
         double apq  = ELEM(a, p, q, n);
         double tau  = (ELEM(a, q, q, n) - ELEM(a, p, p, n)) / (2.0 * apq);
@@ -88,7 +104,7 @@
     for (int i = 0; i < n; i++) evals[i] = ELEM(a, i, i, n);
     memcpy(a, v, (size_t)n * n * sizeof(double));
     free(v);
-    return 0;
+    return converged ? 0 : 2;
 }
 
 static int jacobi_f(int n, float *a, float *evals)
@@ -99,7 +115,18 @@
     memset(v, 0, (size_t)n * n * sizeof(float));
     for (int i = 0; i < n; i++) ELEM(v, i, i, n) = 1.0f;
 
-    for (int sweep = 0; sweep < 50 * n; sweep++) {
+    /* Scale-invariant convergence threshold. */
+    float amax = 0.0f;
+    for (int c = 0; c < n; c++)
+        for (int r = 0; r < n; r++) {
+            float val = fabsf(ELEM(a, r, c, n));
+            if (val > amax) amax = val;
+        }
+    float tol = 1e-6f * (amax > 0.0f ? amax : 1.0f);
+
+    long max_rot   = 10L * n * n + 100;
+    int  converged = (n <= 1);
+    for (long rot = 0; rot < max_rot; rot++) {
         int p = 0, q = 1;
         float max_off = 0.0f;
         for (int c = 1; c < n; c++) {
@@ -108,7 +135,7 @@
                 if (val > max_off) { max_off = val; p = r; q = c; }
             }
         }
-        if (max_off < 1e-6f) break;
+        if (max_off < tol) { converged = 1; break; }
 
         float apq  = ELEM(a, p, q, n);
         float tau  = (ELEM(a, q, q, n) - ELEM(a, p, p, n)) / (2.0f * apq);
@@ -136,7 +163,7 @@
     for (int i = 0; i < n; i++) evals[i] = ELEM(a, i, i, n);
     memcpy(a, v, (size_t)n * n * sizeof(float));
     free(v);
-    return 0;
+    return converged ? 0 : 2;
 }
 
 /* Selection sort on eigenvalues, mirroring the column swaps in evecs. */
@@ -199,7 +226,10 @@
 
     int ret = (dtype == f64) ? jacobi_d(n, (double *)A, (double *)W)
                              : jacobi_f(n, (float  *)A, (float  *)W);
-    if (ret != 0) { free(A); free(W); return AF_ERR_NO_MEM; }
+    if (ret != 0) {
+        free(A); free(W);
+        return (ret == 1) ? AF_ERR_NO_MEM : AF_ERR_RUNTIME;
+    }
 
     if (dtype == f64) sort_eigs_d(n, (double *)W, (double *)A);
     else              sort_eigs_f(n, (float  *)W, (float  *)A);
@@ -368,6 +398,11 @@
     if ((err = af_get_type(&dtype, input)) != AF_SUCCESS) return err;
     if (dtype != f64 && dtype != f32) return AF_ERR_TYPE;
 
+    dim_t d0, d1, d2, d3;
+    if ((err = af_get_dims(&d0, &d1, &d2, &d3, input)) != AF_SUCCESS) return err;
+    if (d0 < 1 || d0 != d1 || d2 != 1 || d3 != 1 || d0 > 0x7fffffff)
+        return AF_ERR_SIZE;
+
     af_backend backend;
     if ((err = af_get_active_backend(&backend)) != AF_SUCCESS) return err;
 
@@ -377,8 +412,6 @@
     if (ensure_init() != AF_SUCCESS)
         return eigsh_cpu(evals_out, evecs_out, input);
 
-    dim_t d0, d1, d2, d3;
-    if ((err = af_get_dims(&d0, &d1, &d2, &d3, input)) != AF_SUCCESS) return err;
     int n = (int)d0;
 
     af_array evecs;
diff --git a/src/ArrayFire/Algorithm.hs b/src/ArrayFire/Algorithm.hs
--- a/src/ArrayFire/Algorithm.hs
+++ b/src/ArrayFire/Algorithm.hs
@@ -29,6 +29,7 @@
 import Data.Word (Word32)
 import Foreign.C.Types (CBool)
 
+import ArrayFire.Arith (cast)
 import ArrayFire.FFI
 import ArrayFire.Internal.Algorithm
 import ArrayFire.Internal.Types
@@ -196,7 +197,11 @@
   -- ^ Dimension along which to count
   -> Array Int
   -- ^ Count of all elements along dimension
-count x (fromIntegral -> n) = x `op1` (\p a -> af_count p a n)
+count x (fromIntegral -> n) =
+  -- af_count produces a u32 array; cast to s64 so the data matches the
+  -- declared element type (otherwise host reads via toVector/toList would
+  -- read 8 bytes per element from a 4-byte-per-element buffer).
+  cast (x `op1` (\p a -> af_count p a n) :: Array Word32)
 
 -- | Sum all elements in an 'Array' along all dimensions
 --
diff --git a/src/ArrayFire/Arith.hs b/src/ArrayFire/Arith.hs
--- a/src/ArrayFire/Arith.hs
+++ b/src/ArrayFire/Arith.hs
@@ -28,7 +28,7 @@
 --------------------------------------------------------------------------------
 module ArrayFire.Arith where
 
-import Prelude                  (Bool(..), ($), (.), flip, fromEnum, fromIntegral, Real, RealFloat)
+import Prelude                  (Bool(..), Fractional, IO, ($), (.), flip, fromEnum, fromIntegral, Real, RealFloat)
 
 import Data.Coerce
 import Data.Proxy
@@ -36,10 +36,24 @@
 
 import ArrayFire.FFI
 import ArrayFire.Internal.Arith
+import ArrayFire.Internal.Defines (AFArray, AFErr)
 import ArrayFire.Internal.Types
 
 import Foreign.C.Types
+import Foreign.Ptr              (Ptr)
 
+-- | Applies a unary ArrayFire function and casts the result back to the
+-- element type of the input. Several ArrayFire unary functions (@af_abs@,
+-- @af_sign@, @af_round@, @af_trunc@, @af_floor@, @af_ceil@, @af_arg@)
+-- internally promote integral inputs to @f32@\/@f64@ (and produce real
+-- outputs for complex inputs); without casting back, the returned handle's
+-- dtype would no longer match the phantom type @a@ and later host reads
+-- ('ArrayFire.Array.toVector', 'ArrayFire.Array.toList',
+-- 'ArrayFire.Array.getScalar') would reinterpret raw bytes at the wrong
+-- type. When the dtype already matches, the cast is a cheap retain.
+op1ReType :: forall a. AFType a => Array a -> (Ptr AFArray -> AFArray -> IO AFErr) -> Array a
+op1ReType a f = cast (op1 a f :: Array a)
+
 -- | Adds two 'Array' objects
 --
 -- >>> A.scalar @Int 1 `A.add` A.scalar @Int 1
@@ -953,10 +967,16 @@
 
 -- | Take the absolute value of an array
 --
+-- For complex arrays the result is the magnitude @|z|@ with a zero imaginary
+-- part (matching @Prelude.abs@ for 'Data.Complex.Complex'). For integral
+-- arrays with magnitudes at or above @2^53@ the value may lose precision,
+-- because ArrayFire computes the absolute value in double precision
+-- internally.
+--
 -- >>> A.abs (A.scalar @Int (-1))
 -- ArrayFire Array
 -- [1 1 1 1]
---    1.0000
+--          1
 --
 abs
   :: AFType a
@@ -964,7 +984,7 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'abs'
-abs = flip op1 af_abs
+abs = flip op1ReType af_abs
 
 -- | Find the arg of an array
 --
@@ -987,30 +1007,30 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'arg'
-arg = flip op1 af_arg
+arg = flip op1ReType af_arg
 
 -- | Find the sign of two 'Array's
 --
 -- >>> A.sign (vector @Int 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
---     0.0000
+--          0
+--          0
+--          0
+--          0
+--          0
+--          0
+--          0
+--          0
+--          0
+--          0
 sign
   :: AFType a
   => Array a
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'sign'
-sign = flip op1 af_sign
+sign = flip op1ReType af_sign
 
 -- | Round the values in an 'Array'
 --
@@ -1033,7 +1053,7 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'round'
-round = flip op1 af_round
+round = flip op1ReType af_round
 
 -- | Truncate the values of an 'Array'
 --
@@ -1056,7 +1076,7 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'trunc'
-trunc = flip op1 af_trunc
+trunc = flip op1ReType af_trunc
 
 -- | Take the floor of all values in an 'Array'
 --
@@ -1079,7 +1099,7 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'floor'
-floor = flip op1 af_floor
+floor = flip op1ReType af_floor
 
 -- | Take the ceil of all values in an 'Array'
 --
@@ -1102,11 +1122,11 @@
   -- ^ Input array
   -> Array a
   -- ^ Result of calling 'ceil'
-ceil = flip op1 af_ceil
+ceil = flip op1ReType af_ceil
 
 -- | Take the sin of all values in an 'Array'
 --
--- >>> A.sin (A.vector @Int 10 [1..])
+-- >>> A.sin (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.8415
@@ -1120,7 +1140,7 @@
 --     0.4121
 --    -0.5440
 sin
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1129,7 +1149,7 @@
 
 -- | Take the cos of all values in an 'Array'
 --
--- >>> A.cos (A.vector @Int 10 [1..])
+-- >>> A.cos (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.5403
@@ -1143,7 +1163,7 @@
 --    -0.9111
 --    -0.8391
 cos
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1152,7 +1172,7 @@
 
 -- | Take the tan of all values in an 'Array'
 --
--- >>> A.tan (A.vector @Int 10 [1..])
+-- >>> A.tan (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.5574
@@ -1166,7 +1186,7 @@
 --    -0.4523
 --     0.6484
 tan
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1175,7 +1195,7 @@
 
 -- | Take the asin of all values in an 'Array'
 --
--- >>> A.asin (A.vector @Int 10 [1..])
+-- >>> A.asin (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.5708
@@ -1190,7 +1210,7 @@
 --        nan
 --
 asin
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1199,7 +1219,7 @@
 
 -- | Take the acos of all values in an 'Array'
 --
--- >>> A.acos (A.vector @Int 10 [1..])
+-- >>> A.acos (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.0000
@@ -1213,7 +1233,7 @@
 --        nan
 --        nan
 acos
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1222,7 +1242,7 @@
 
 -- | Take the atan of all values in an 'Array'
 --
--- >>> A.atan (A.vector @Int 10 [1..])
+-- >>> A.atan (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.7854
@@ -1236,7 +1256,7 @@
 --     1.4601
 --     1.4711
 atan
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1259,7 +1279,7 @@
 --     0.7328
 --     0.7378
 atan2
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ First input
   -> Array a
@@ -1286,7 +1306,7 @@
 --     0.7328
 --     0.7378
 atan2Batched
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ First input
   -> Array a
@@ -1433,7 +1453,7 @@
 
 -- | Execute sinh
 --
--- >>> A.sinh (A.vector @Int 10 [1..])
+-- >>> A.sinh (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.1752
@@ -1447,7 +1467,7 @@
 --  4051.5420
 -- 11013.2324
 sinh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1470,7 +1490,7 @@
 --  4051.5420
 -- 11013.2329
 cosh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1479,7 +1499,7 @@
 
 -- | Execute tanh
 --
--- >>> A.tanh (A.vector @Int 10 [1..])
+-- >>> A.tanh (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.7616
@@ -1493,7 +1513,7 @@
 --     1.0000
 --     1.0000
 tanh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1502,7 +1522,7 @@
 
 -- | Execute asinh
 --
--- >>> A.asinh (A.vector @Int 10 [1..])
+-- >>> A.asinh (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.8814
@@ -1516,7 +1536,7 @@
 --     2.8934
 --     2.9982
 asinh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1539,7 +1559,7 @@
 --     2.8873
 --     2.9932
 acosh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1562,7 +1582,7 @@
 --        nan
 --        nan
 atanh
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1572,12 +1592,12 @@
 -- | Execute root: compute the nth root of each element.
 -- @root base n@ computes @base^(1\/n)@.
 --
--- >>> A.root (A.scalar @Double 1 8) (A.scalar @Double 1 3)
+-- >>> A.root (A.scalar @Double 8) (A.scalar @Double 3)
 -- ArrayFire Array
 -- [1 1 1 1]
 --     2.0000
 root
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ The input data (base)
   -> Array a
@@ -1604,7 +1624,7 @@
 --     1.2765
 --     1.2589
 rootBatched
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ First input
   -> Array a
@@ -1673,7 +1693,7 @@
   x `op2` y $ \arr arr1 arr2 ->
     af_pow arr arr1 arr2 batch
 
--- | Raise an 'Array' to the second power
+-- | Raise 2 to the power of each element of an 'Array' (@2 ** x@)
 --
 -- >>> A.pow2 (A.vector @Int 10 [1..])
 -- ArrayFire Array
@@ -1712,7 +1732,7 @@
 --  8103.0839
 -- 22026.4658
 exp
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1721,7 +1741,7 @@
 
 -- | Execute sigmoid on 'Array'
 --
--- >>> A.sigmoid (A.vector @Int 10 [1..])
+-- >>> A.sigmoid (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.7311
@@ -1735,7 +1755,7 @@
 --     0.9999
 --     1.0000
 sigmoid
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1744,7 +1764,7 @@
 
 -- | Execute expm1
 --
--- >>> A.expm1 (A.vector @Int 10 [1..])
+-- >>> A.expm1 (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.7183
@@ -1758,7 +1778,7 @@
 --  8102.0840
 -- 22025.4648
 expm1
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1767,7 +1787,7 @@
 
 -- | Execute erf
 --
--- >>> A.erf (A.vector @Int 10 [1..])
+-- >>> A.erf (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.8427
@@ -1781,7 +1801,7 @@
 --     1.0000
 --     1.0000
 erf
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1790,7 +1810,7 @@
 
 -- | Execute erfc
 --
--- >>> A.erfc (A.vector @Int 10 [1..])
+-- >>> A.erfc (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.1573
@@ -1804,7 +1824,7 @@
 --     0.0000
 --     0.0000
 erfc
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1813,7 +1833,7 @@
 
 -- | Execute log
 --
--- >>> A.log (A.vector @Int 10 [1..])
+-- >>> A.log (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.0000
@@ -1827,7 +1847,7 @@
 --     2.1972
 --     2.3026
 log
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1836,7 +1856,7 @@
 
 -- | Execute log1p
 --
--- >>> A.log1p (A.vector @Int 10 [1..])
+-- >>> A.log1p (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.6931
@@ -1850,7 +1870,7 @@
 --     2.3026
 --     2.3979
 log1p
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1859,7 +1879,7 @@
 
 -- | Execute log10
 --
--- >>> A.log10 (A.vector @Int 10 [1..])
+-- >>> A.log10 (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.0000
@@ -1873,7 +1893,7 @@
 --     0.9542
 --     1.0000
 log10
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1882,7 +1902,7 @@
 
 -- | Execute log2
 --
--- >>> A.log2 (A.vector @Int 10 [1..])
+-- >>> A.log2 (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.0000
@@ -1896,7 +1916,7 @@
 --     3.1699
 --     3.3219
 log2
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1905,7 +1925,7 @@
 
 -- | Execute sqrt
 --
--- >>> A.sqrt (A.vector @Int 10 [ x * x | x <- [ 1 .. 10 ]])
+-- >>> A.sqrt (A.vector @Double 10 [ x * x | x <- [ 1 .. 10 ]])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.0000
@@ -1919,7 +1939,7 @@
 --     9.0000
 --    10.0000
 sqrt
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1928,7 +1948,7 @@
 
 -- | Execute cbrt
 --
--- >>> A.cbrt (A.vector @Int 10 [ x * x * x | x <- [ 1 .. 10 ]])
+-- >>> A.cbrt (A.vector @Double 10 [ x * x * x | x <- [ 1 .. 10 ]])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.0000
@@ -1942,7 +1962,7 @@
 --     9.0000
 --    10.0000
 cbrt
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1951,7 +1971,7 @@
 
 -- | Execute factorial
 --
--- >>> A.factorial (A.vector @Int 10 [1..])
+-- >>> A.factorial (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.0000
@@ -1965,7 +1985,7 @@
 -- 362880.0000
 -- 3628801.7500
 factorial
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1974,7 +1994,7 @@
 
 -- | Execute tgamma
 --
--- >>> tgamma (vector @Int 10 [1..])
+-- >>> tgamma (vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     1.0000
@@ -1988,7 +2008,7 @@
 -- 40319.9961
 -- 362880.0000
 tgamma
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -1997,7 +2017,7 @@
 
 -- | Execute lgamma
 --
--- >>> A.lgamma (A.vector @Int 10 [1..])
+-- >>> A.lgamma (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --     0.0000
@@ -2011,7 +2031,7 @@
 --    10.6046
 --    12.8018
 lgamma
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input array
   -> Array a
@@ -2066,7 +2086,7 @@
 
 -- | Execute isNaN
 --
--- >>> A.isNaN $ A.acos (A.vector @Int 10 [1..])
+-- >>> A.isNaN $ A.acos (A.vector @Double 10 [1..])
 -- ArrayFire Array
 -- [10 1 1 1]
 --          0
diff --git a/src/ArrayFire/Data.hs b/src/ArrayFire/Data.hs
--- a/src/ArrayFire/Data.hs
+++ b/src/ArrayFire/Data.hs
@@ -100,36 +100,41 @@
       | x == u64 ->
         cast $ constantULong dims (unsafeCoerce val :: Word64)
       | x == s32 ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: Int32) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: Int32) :: Double)
       | x == s16 ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: Int16) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: Int16) :: Double)
       | x == u32 ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: Word32) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: Word32) :: Double)
       | x == u8 ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: Word8) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: Word8) :: Double)
       | x == u16 ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: Word16) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: Word16) :: Double)
       | x == f64 ->
-        cast $ constant' dims (unsafeCoerce val :: Double)
+        constant' dims (unsafeCoerce val :: Double)
       | x == b8  ->
-        cast $ constant' dims (fromIntegral (unsafeCoerce val :: CBool) :: Double)
+        constant' dims (fromIntegral (unsafeCoerce val :: CBool) :: Double)
       | x == f32 ->
-        cast $ constant' dims (realToFrac (unsafeCoerce val :: Float))
+        constant' dims (realToFrac (unsafeCoerce val :: Float))
       | otherwise -> error "constant: Invalid array fire type"
   where
     dtyp = afType (Proxy @a)
 
+    -- Creates the array directly with the target dtype: @af_constant@ takes
+    -- the value as a C double for every non-complex, non-64-bit-integral
+    -- dtype. Routing through an f64 array and casting (as this used to do)
+    -- fails with AF_ERR_NO_DBL on OpenCL devices without fp64 support and
+    -- changes b8 semantics (the cast normalises non-zero values to 1).
     constant'
       :: [Int]
       -- ^ Dimensions
       -> Double
       -- ^ Scalar value
-      -> Array Double
+      -> Array a
     constant' dims' val' =
       unsafePerformIO . mask_ $ do
         ptr <- calloca $ \ptrPtr -> do
           withArray (fromIntegral <$> dims') $ \dimArray -> do
-            throwAFError =<< af_constant ptrPtr val' n dimArray typ
+            throwAFError =<< af_constant ptrPtr val' n dimArray dtyp
             peek ptrPtr
         Array <$>
           newForeignPtr
@@ -137,7 +142,6 @@
               ptr
           where
             n = fromIntegral (length dims')
-            typ = afType (Proxy @Double)
 
     -- | Creates an 'Array (Complex Double)' from a scalar val'ue
     --
diff --git a/src/ArrayFire/Device.hs b/src/ArrayFire/Device.hs
--- a/src/ArrayFire/Device.hs
+++ b/src/ArrayFire/Device.hs
@@ -20,6 +20,7 @@
 
 import Control.Exception (finally)
 import Foreign.C.String
+import Foreign.Ptr (castPtr)
 import ArrayFire.Internal.Device
 import ArrayFire.FFI
 
@@ -61,7 +62,12 @@
 -- >>> getInfoString
 -- "ArrayFire v3.6.4 (OpenCL, 64-bit Mac OSX, build 1b8030c5)\n[0] APPLE: AMD Radeon Pro 555X Compute Engine, 4096 MB\n-1- APPLE: Intel(R) UHD Graphics 630, 1536 MB\n"
 getInfoString :: IO String
-getInfoString = peekCString =<< afCall1 (flip af_info_string 1)
+getInfoString = do
+  strPtr <- afCall1 (flip af_info_string 1)
+  str <- peekCString strPtr
+  -- allocated by ArrayFire with af_alloc_host; free to avoid leaking
+  _ <- af_free_host (castPtr strPtr)
+  pure str
 
 -- | Retrieves count of devices
 --
diff --git a/src/ArrayFire/FFI.hs b/src/ArrayFire/FFI.hs
--- a/src/ArrayFire/FFI.hs
+++ b/src/ArrayFire/FFI.hs
@@ -195,12 +195,14 @@
     pure (Array fptrA, Array fptrB, Array fptrC, g)
 
 -- | Applies a C function that takes two input 'Array's and produces a pair of
--- output 'Array's.
+-- output 'Array's. The element types of the outputs are free so callers can
+-- pin them to whatever the C function actually produces (e.g. @u32@ index
+-- arrays from the matcher functions).
 op2p2
   :: Array a
-  -> Array a
+  -> Array b
   -> (Ptr AFArray -> Ptr AFArray -> AFArray -> AFArray -> IO AFErr)
-  -> (Array a, Array a)
+  -> (Array c, Array d)
 {-# NOINLINE op2p2 #-}
 op2p2 (Array fptr1) (Array fptr2) op =
   unsafePerformIO . mask_ $ do
@@ -461,8 +463,11 @@
       throwAFError =<< op ptrInput
       peek ptrInput
 
--- | Note: We don't add a finalizer to 'Array' since the 'Features' finalizer frees 'Array'
--- under the hood.
+-- | Extracts one of the component 'Array's of a 'Features' handle. The C
+-- getters return the raw handle stored inside the features struct without
+-- retaining it, so we retain it here before attaching the release finalizer;
+-- otherwise the 'Features' finalizer and the 'Array' finalizer would double
+-- free.
 featuresToArray
   :: Features
   -> (Ptr AFArray -> AFFeatures -> IO AFErr)
diff --git a/src/ArrayFire/Features.hs b/src/ArrayFire/Features.hs
--- a/src/ArrayFire/Features.hs
+++ b/src/ArrayFire/Features.hs
@@ -83,7 +83,7 @@
 --     2.4375
 getFeaturesXPos
   :: Features
-  -> Array a
+  -> Array Float
 getFeaturesXPos = (`featuresToArray` af_get_features_xpos)
 
 -- | Get Feature Y-position
@@ -103,7 +103,7 @@
 --        nan
 getFeaturesYPos
   :: Features
-  -> Array a
+  -> Array Float
 getFeaturesYPos = (`featuresToArray` af_get_features_ypos)
 
 -- | Get Feature Score
@@ -123,7 +123,7 @@
 --        nan
 getFeaturesScore
   :: Features
-  -> Array a
+  -> Array Float
 getFeaturesScore = (`featuresToArray` af_get_features_score)
 
 -- | Get Feature orientation
@@ -143,7 +143,7 @@
 --        nan
 getFeaturesOrientation
   :: Features
-  -> Array a
+  -> Array Float
 getFeaturesOrientation = (`featuresToArray` af_get_features_orientation)
 
 -- | Get Feature size
@@ -163,5 +163,5 @@
 --        nan
 getFeaturesSize
   :: Features
-  -> Array a
+  -> Array Float
 getFeaturesSize = (`featuresToArray` af_get_features_size)
diff --git a/src/ArrayFire/Graphics.hs b/src/ArrayFire/Graphics.hs
--- a/src/ArrayFire/Graphics.hs
+++ b/src/ArrayFire/Graphics.hs
@@ -18,8 +18,6 @@
 module ArrayFire.Graphics where
 
 import Control.Exception
-import Foreign.Marshal
-import Foreign.Storable
 import Foreign.ForeignPtr
 import Foreign.C.String
 
@@ -116,8 +114,7 @@
 drawImage (Window wfptr) (Array fptr) cell =
   mask_ $ withForeignPtr fptr $ \aptr ->
     withForeignPtr wfptr $ \wptr ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_image wptr aptr cellPtr
 
 -- | Draw a plot onto a 'Window'
@@ -142,8 +139,7 @@
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
     withForeignPtr fptr2 $ \ptr2 ->
       withForeignPtr w $ \wptr ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_plot wptr ptr1 ptr2 cellPtr
 
 -- | Draw a plot onto a 'Window'
@@ -163,8 +159,7 @@
 drawPlot3 (Window w) (Array fptr) cell =
   mask_ $ withForeignPtr fptr $ \aptr ->
     withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_plot3 wptr aptr cellPtr
 
 -- | Draw a plot onto a 'Window'
@@ -184,8 +179,7 @@
 drawPlotNd (Window w) (Array fptr) cell =
   mask_ $ withForeignPtr fptr $ \aptr ->
     withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_plot_nd wptr aptr cellPtr
 
 -- | Draw a plot onto a 'Window'
@@ -208,8 +202,7 @@
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
     withForeignPtr fptr2 $ \ptr2 ->
      withForeignPtr w $ \wptr ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_plot_2d wptr ptr1 ptr2 cellPtr
 
 -- | Draw a 3D plot onto a 'Window'
@@ -235,8 +228,7 @@
     withForeignPtr fptr2 $ \ptr2 ->
       withForeignPtr fptr3 $ \ptr3 ->
         withForeignPtr w $ \wptr ->
-          alloca $ \cellPtr -> do
-            poke cellPtr =<< cellToAFCell cell
+          withAFCell cell $ \cellPtr -> do
             throwAFError =<< af_draw_plot_3d wptr ptr1 ptr2 ptr3 cellPtr
 
 -- | Draw a scatter plot onto a 'Window'
@@ -261,8 +253,7 @@
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
     withForeignPtr fptr2 $ \ptr2 ->
      withForeignPtr w $ \wptr ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_scatter wptr ptr1 ptr2 m cellPtr
 
 -- | Draw a scatter plot onto a 'Window'
@@ -284,8 +275,7 @@
 drawScatter3 (Window w) (Array fptr1) (fromMarkerType -> m) cell =
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_scatter3 wptr ptr1 m cellPtr
 
 -- | Draw a scatter plot onto a 'Window'
@@ -307,8 +297,7 @@
 drawScatterNd (Window w) (Array fptr1) (fromMarkerType -> m) cell =
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_scatter_nd wptr ptr1 m cellPtr
 
 -- | Draw a scatter plot onto a 'Window'
@@ -333,8 +322,7 @@
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
    withForeignPtr w $ \wptr ->
    withForeignPtr fptr2 $ \ptr2 ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_scatter_2d wptr ptr1 ptr2 m cellPtr
 
 -- | Draw a scatter plot onto a 'Window'
@@ -362,8 +350,7 @@
    withForeignPtr w $ \wptr ->
    withForeignPtr fptr2 $ \ptr2 ->
     withForeignPtr fptr3 $ \ptr3 ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_scatter_3d wptr ptr1 ptr2 ptr3 m cellPtr
 
 -- | Draw a Histogram onto a 'Window'
@@ -387,8 +374,7 @@
 drawHistogram (Window w) (Array fptr1) minval maxval cell =
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_draw_hist wptr ptr1 minval maxval cellPtr
 
 -- | Draw a Surface onto a 'Window'
@@ -414,8 +400,7 @@
    withForeignPtr w $ \wptr ->
    withForeignPtr fptr2 $ \ptr2 ->
     withForeignPtr fptr3 $ \ptr3 ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_surface wptr ptr1 ptr2 ptr3 cellPtr
 
 -- | Draw a Vector Field onto a 'Window'
@@ -438,8 +423,7 @@
   mask_ $ withForeignPtr fptr1 $ \ptr1 ->
    withForeignPtr fptr2 $ \ptr2 ->
      withForeignPtr w $ \wptr ->
-      alloca $ \cellPtr -> do
-        poke cellPtr =<< cellToAFCell cell
+      withAFCell cell $ \cellPtr -> do
         throwAFError =<< af_draw_vector_field_nd wptr ptr1 ptr2 cellPtr
 
 -- | Draw a Vector Field onto a 'Window'
@@ -476,8 +460,7 @@
           withForeignPtr fptr4 $ \ptr4 ->
             withForeignPtr fptr5 $ \ptr5 ->
               withForeignPtr fptr6 $ \ptr6 -> do
-                alloca $ \cellPtr -> do
-                  poke cellPtr =<< cellToAFCell cell
+                withAFCell cell $ \cellPtr -> do
                   throwAFError =<< af_draw_vector_field_3d wptr ptr1 ptr2 ptr3 ptr4 ptr5 ptr6 cellPtr
 
 -- | Draw a Vector Field onto a 'Window'
@@ -507,8 +490,7 @@
       withForeignPtr fptr2 $ \ptr2 ->
         withForeignPtr fptr3 $ \ptr3 ->
           withForeignPtr fptr4 $ \ptr4 ->
-            alloca $ \cellPtr -> do
-              poke cellPtr =<< cellToAFCell cell
+            withAFCell cell $ \cellPtr -> do
               throwAFError =<< af_draw_vector_field_2d wptr ptr1 ptr2 ptr3 ptr4 cellPtr
 
 -- | Draw a grid onto a 'Window'
@@ -555,8 +537,7 @@
     withForeignPtr fptr1 $ \ptr1 ->
       withForeignPtr fptr2 $ \ptr2 ->
         withForeignPtr fptr3 $ \ptr3 ->
-          alloca $ \cellPtr -> do
-            poke cellPtr =<< cellToAFCell cell
+          withAFCell cell $ \cellPtr -> do
             throwAFError =<< af_set_axes_limits_compute wptr ptr1 ptr2 ptr3 exact cellPtr
 
 -- | Setting axes limits for a 2D histogram/plot/surface/vector field.
@@ -582,8 +563,7 @@
 setAxesLimits2d (Window w) xmin xmax ymin ymax (fromIntegral . fromEnum -> exact) cell =
   mask_ $ do
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_set_axes_limits_2d wptr xmin xmax ymin ymax exact cellPtr
 
 -- | Setting axes limits for a 3D histogram/plot/surface/vector field.
@@ -613,8 +593,7 @@
 setAxesLimits3d (Window w) xmin xmax ymin ymax zmin zmax (fromIntegral . fromEnum -> exact) cell =
   mask_ $ do
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
-      poke cellPtr =<< cellToAFCell cell
+    withAFCell cell $ \cellPtr -> do
       throwAFError =<< af_set_axes_limits_3d wptr xmin xmax ymin ymax zmin zmax exact cellPtr
 
 
@@ -637,11 +616,10 @@
 setAxesTitles (Window w) x y z cell =
   mask_ $ do
    withForeignPtr w $ \wptr ->
-    alloca $ \cellPtr -> do
+    withAFCell cell $ \cellPtr ->
       withCString x $ \xstr ->
         withCString y $ \ystr ->
-          withCString z $ \zstr -> do
-            poke cellPtr =<< cellToAFCell cell
+          withCString z $ \zstr ->
             throwAFError =<< af_set_axes_titles wptr xstr ystr zstr cellPtr
 
 -- | Displays 'Window'
diff --git a/src/ArrayFire/Image.hs b/src/ArrayFire/Image.hs
--- a/src/ArrayFire/Image.hs
+++ b/src/ArrayFire/Image.hs
@@ -19,9 +19,17 @@
 --------------------------------------------------------------------------------
 module ArrayFire.Image where
 
+import Control.Exception        (mask_)
+import Data.Bits                (popCount)
 import Data.Proxy
 import Data.Word
+import Foreign.C.Types          (CBool)
+import Foreign.ForeignPtr       (withForeignPtr)
+import Foreign.Marshal.Array    (allocaArray, peekArray)
+import System.IO.Unsafe         (unsafePerformIO)
 
+import ArrayFire.Exception      (throwAFError)
+import ArrayFire.Internal.Defines (AFMomentType(..))
 import ArrayFire.Internal.Types
 import ArrayFire.Internal.Image
 import ArrayFire.FFI
@@ -232,9 +240,9 @@
   -> Int
   -- ^ is the second output dimension
   -> InterpType
-  -- ^ if true applies inverse transform, if false applies forward transoform
-  -> Bool
   -- ^ is the interpolation type (Nearest by default)
+  -> Bool
+  -- ^ if true applies inverse transform, if false applies forward transform
   -> Array a
   -- ^ will contain the skewed image
 skew a trans0 trans1 (fromIntegral -> odim0) (fromIntegral -> odim1) (fromInterpType -> interp) (fromIntegral . fromEnum -> b) =
@@ -688,10 +696,21 @@
   -- ^ is the input image
   -> MomentType
   -- ^ is moment(s) to calculate
-  -> Double
-  -- ^ is a pointer to a pre-allocated array where the calculated moment(s) will be placed. User is responsible for ensuring enough space to hold all requested moments
-momentsAll in' m =
-  in' `infoFromArray` (\p a -> af_moments_all p a (fromMomentType m))
+  -> [Double]
+  -- ^ the calculated moment(s); one element per requested moment
+  -- (so 'FirstOrder' yields four values, the single moments one)
+{-# NOINLINE momentsAll #-}
+momentsAll (Array fptr) m =
+  -- af_moments_all writes one double per moment selected in the bitmask, so
+  -- the output buffer must be sized accordingly: passing a single-double
+  -- buffer for FirstOrder (four moments) would smash the stack.
+  unsafePerformIO . mask_ . withForeignPtr fptr $ \aptr ->
+    allocaArray n $ \outPtr -> do
+      throwAFError =<< af_moments_all outPtr aptr afm
+      peekArray n outPtr
+  where
+    afm@(AFMomentType raw) = fromMomentType m
+    n = popCount raw
 
 -- | Canny Edge Detector
 --
@@ -712,8 +731,8 @@
   -- ^ is the window size of sobel kernel for computing gradient direction and magnitude
   -> Bool
   -- ^ indicates if L1 norm(faster but less accurate) is used to compute image gradient magnitude instead of L2 norm.
-  -> Array a
-  -- ^ is an binary array containing edges
+  -> Array CBool
+  -- ^ is a binary (@b8@) array containing edges
 canny in' (fromCannyThreshold -> canny') low high (fromIntegral -> window) (fromIntegral . fromEnum -> fast) =
   in' `op1` (\p a -> af_canny p a canny' low high window fast)
 
diff --git a/src/ArrayFire/Internal/Types.hsc b/src/ArrayFire/Internal/Types.hsc
--- a/src/ArrayFire/Internal/Types.hsc
+++ b/src/ArrayFire/Internal/Types.hsc
@@ -20,6 +20,8 @@
 import Foreign.C.Types
 import Foreign.ForeignPtr
 import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr)
 import Foreign.Storable
 import GHC.Int
 
@@ -613,14 +615,21 @@
   -- ^ Color map used for rendering
   } deriving (Show, Eq)
 
-cellToAFCell :: Cell -> IO AFCell
-cellToAFCell Cell {..} =
+-- | Marshals a 'Cell' into a temporary 'AFCell' and hands a pointer to it to
+-- the continuation. The title 'CString' is only valid for the duration of the
+-- continuation, so the C call consuming the cell must happen inside it —
+-- returning the 'AFCell' from under 'withCString' would leave a dangling
+-- title pointer.
+withAFCell :: Cell -> (Ptr AFCell -> IO a) -> IO a
+withAFCell Cell {..} f =
   withCString cellTitle $ \cstr ->
-    pure AFCell { afCellRow = cellRow
-                , afCellCol = cellCol
-                , afCellTitle = cstr
-                , afCellColorMap = fromColorMap cellColorMap
-                }
+    alloca $ \cellPtr -> do
+      poke cellPtr AFCell { afCellRow = cellRow
+                          , afCellCol = cellCol
+                          , afCellTitle = cstr
+                          , afCellColorMap = fromColorMap cellColorMap
+                          }
+      f cellPtr
 
 -- | Color map for rendering
 data ColorMap
@@ -817,11 +826,24 @@
   -- ^ The default. Same as AF_NORM_VECTOR_2
   deriving (Show, Eq, Enum)
 
+-- | Note: this cannot be derived via 'fromEnum' because in @af\/defines.h@
+-- @AF_NORM_EUCLID@ is an alias for @AF_NORM_VECTOR_2@ (value 2), not a
+-- distinct enum value following @AF_NORM_MATRIX_L_PQ@.
 fromNormType :: NormType -> AFNormType
-fromNormType = AFNormType . fromIntegral . fromEnum
+fromNormType NormVectorOne  = AFNormType 0
+fromNormType NormVectorInf  = AFNormType 1
+fromNormType NormVector2    = AFNormType 2
+fromNormType NormVectorP    = AFNormType 3
+fromNormType NormMatrix1    = AFNormType 4
+fromNormType NormMatrixInf  = AFNormType 5
+fromNormType NormMatrix2    = AFNormType 6
+fromNormType NormMatrixLPQ  = AFNormType 7
+fromNormType NormEuclid     = AFNormType 2
 
 toNormType :: AFNormType -> NormType
-toNormType (AFNormType (fromIntegral -> x)) = toEnum x
+toNormType (AFNormType (fromIntegral -> x))
+  | x >= 0 && x <= 7 = toEnum x
+  | otherwise = error ("Invalid AFNormType value: " <> show x)
 
 -- | Convolution Domain
 data ConvDomain
diff --git a/src/ArrayFire/Orphans.hs b/src/ArrayFire/Orphans.hs
--- a/src/ArrayFire/Orphans.hs
+++ b/src/ArrayFire/Orphans.hs
@@ -19,11 +19,13 @@
 import qualified Prelude
 
 import           Control.DeepSeq (NFData(..))
+import           Data.Proxy      (Proxy (..))
 
 import qualified ArrayFire.Arith     as A
 import qualified ArrayFire.Array     as A
 import qualified ArrayFire.Algorithm as A
 import qualified ArrayFire.Data      as A
+import           ArrayFire.Internal.Defines (s16, s32, s64, u8, u16, u32, u64, b8)
 import           ArrayFire.Types
 import           ArrayFire.Util
 
@@ -32,10 +34,21 @@
 
 -- | Structural equality on 'Array': equal shapes and elementwise-equal values.
 --
--- Both inputs are 'A.eval'-ed before the comparison to flush each array's JIT
--- queue; skipping either eval can produce stale results. 'A.allTrueAll' reads
--- back a @(real, imaginary)@ pair; the imaginary component is reliably @0@ for
--- boolean reductions, so comparing only the real part against @1.0@ is safe.
+-- Both inputs are 'A.eval'-ed before comparison.  On asynchronous backends
+-- (OpenCL) a freshly-created array's fill kernel is enqueued but may not have
+-- retired before the JIT for 'eqBatched' runs, so the comparison can read
+-- stale buffer contents.  'A.eval' flushes the command queue for each array,
+-- ensuring the buffer is populated.  The CPU backend is synchronous and does
+-- not require this, but the call is cheap and correct on all backends.
+--
+-- 'A.allTrueAll' returns a @(real, imaginary)@ pair; imaginary is reliably
+-- @0@ for boolean reductions, so comparing only the real part against @1.0@
+-- is safe.
+--
+-- /Caveat/: comparisons follow IEEE semantics elementwise, so an array
+-- containing @NaN@ is not equal to itself (@x == x@ is 'False'), violating
+-- 'Eq' reflexivity exactly as 'Double' itself does. @(\/=)@ remains the exact
+-- negation of @(==)@ in all cases, including @NaN@.
 instance (AFType a, Eq a) => Eq (Array a) where
   x == y = A.getDims x == A.getDims y
         && A.allTrueAll (A.eqBatched (A.eval x) (A.eval y) False) == 1.0
@@ -61,7 +74,15 @@
 instance (Num a, AFType a) => Num (Array a) where
   x + y       = A.add x y
   x * y       = A.mul x y
-  abs         = A.abs
+  -- af_abs promotes all integer inputs to f32 internally (see complex.cpp),
+  -- losing precision for |x| > 2^24.  For integer types we implement abs
+  -- entirely in integer arithmetic: signed types negate negative elements via
+  -- select; unsigned types are already non-negative so abs is the identity.
+  abs x
+    | dt `elem` [s16, s32, s64] = A.select (A.lt x 0) (0 - x) x
+    | dt `elem` [u8, u16, u32, u64, b8] = x
+    | otherwise = A.abs x   -- float / complex: delegate to AF
+    where dt = afType (Proxy @a)
   signum x    = A.select (A.gt x 0) 1 (A.select (A.lt x 0) (-1) 0)
   negate arr  = A.scalar @a (fromInteger (-1)) `A.mul` arr
   x - y       = A.sub x y
diff --git a/src/ArrayFire/Random.hs b/src/ArrayFire/Random.hs
--- a/src/ArrayFire/Random.hs
+++ b/src/ArrayFire/Random.hs
@@ -149,7 +149,7 @@
     r `infoFromRandomEngine` af_random_engine_get_type
 
 foreign import ccall unsafe "af_random_engine_set_seed_"
-  af_random_engine_set_seed_ :: AFRandomEngine -> IntL -> IO AFErr
+  af_random_engine_set_seed_ :: AFRandomEngine -> UIntL -> IO AFErr
 
 -- | Sets seed on 'RandomEngine'
 --
@@ -159,7 +159,7 @@
 randomEngineSetSeed
   :: RandomEngine
   -- ^ 'RandomEngine' argument
-  -> Int
+  -> Word
   -- ^ Seed
   -> IO ()
 randomEngineSetSeed r t =
diff --git a/src/ArrayFire/Sparse.hs b/src/ArrayFire/Sparse.hs
--- a/src/ArrayFire/Sparse.hs
+++ b/src/ArrayFire/Sparse.hs
@@ -14,14 +14,18 @@
 -- *Note*
 -- Sparse functionality support was added to ArrayFire in v3.4.0.
 --
--- >>> createSparseArray 10 10 (matrix @Double (10,10) [[1,2],[3,4]]) (vector @Int32 10 [1..]) (vector @Int32 10 [1..]) CSR
+-- >>> createSparseArray 3 3 (vector @Double 3 [1,2,3]) (vector @Int32 3 [0,1,2]) (vector @Int32 3 [0,1,2]) COO
 --
 --
 --------------------------------------------------------------------------------
 module ArrayFire.Sparse where
 
+import Control.Exception (throw)
+
+import ArrayFire.Exception
 import ArrayFire.Types
 import ArrayFire.FFI
+import ArrayFire.Internal.Algorithm (af_any_true_all)
 import ArrayFire.Internal.Sparse
 import ArrayFire.Internal.Types
 import Data.Int
@@ -33,7 +37,7 @@
 -- *Note*
 -- This function only create references of these arrays into the sparse data structure and does not do deep copies.
 --
--- >>> createSparseArray 10 10 (matrix @Double (10,10) [[1,2],[3,4]]) (vector @Int32 10 [1..]) (vector @Int32 10 [1..]) CSR
+-- >>> createSparseArray 3 3 (vector @Double 3 [1,2,3]) (vector @Int32 3 [0,1,2]) (vector @Int32 3 [0,1,2]) COO
 --
 createSparseArray
   :: (AFType a, Fractional a)
@@ -85,8 +89,17 @@
   -- ^ is the storage format of the sparse array
   -> Array a
   -- ^ 'Array' for the sparse array with the given storage type
-createSparseArrayFromDense a s =
-  a `op1` (\p x -> af_create_sparse_array_from_dense p x (toStorage s))
+createSparseArrayFromDense a s
+  -- Guard: converting an all-zero dense matrix (NNZ = 0) segfaults inside
+  -- ArrayFire (observed on AF 3.8.2). Throw a proper AFException instead of
+  -- crashing the process.
+  | nonZero == 0.0 =
+      throw $ AFException SizeError 203
+        "createSparseArrayFromDense: input has no non-zero elements; zero-NNZ sparse arrays crash the underlying ArrayFire library"
+  | otherwise =
+      a `op1` (\p x -> af_create_sparse_array_from_dense p x (toStorage s))
+  where
+    (nonZero, _) = a `infoFromArray2` af_any_true_all :: (Double, Double)
 
 -- | Convert an existing sparse array into a different storage format.
 --
@@ -207,7 +220,7 @@
 --
 -- Returns reference to values, row indices, column indices and storage format of an input sparse array
 --
--- >>> (values, cols, rows, storage) = sparseGetInfo $ createSparseArrayFromDense (matrix @Double (2,2) [[1,2],[3,4]]) CSR
+-- >>> (values, rows, cols, storage) = sparseGetInfo $ createSparseArrayFromDense (matrix @Double (2,2) [[1,2],[3,4]]) CSR
 -- >>> values
 -- ArrayFire Array
 -- [4 1 1 1]
@@ -268,7 +281,9 @@
 -- [ArrayFire Docs](http://arrayfire.org/docs/group__sparse__func__row__idx.htm)
 --
 -- Returns reference to the row indices component of the sparse array.
--- Row indices is the 'Array' containing the column indices of the sparse array.
+-- Row indices is the 'Array' containing the row indices of the sparse array
+-- (for 'CSR' storage these are the compressed row offsets, of length
+-- rows + 1).
 --
 -- >>> sparseGetRowIdx (createSparseArrayFromDense (matrix @Double (2,2) [[1,2],[3,4]]) CSR)
 -- ArrayFire Array
diff --git a/src/ArrayFire/Statistics.hs b/src/ArrayFire/Statistics.hs
--- a/src/ArrayFire/Statistics.hs
+++ b/src/ArrayFire/Statistics.hs
@@ -45,12 +45,12 @@
 
 -- | Calculates 'mean' of 'Array' along user-specified dimension.
 --
--- >>> mean (vector @Int 10 [1..]) 0
+-- >>> mean (vector @Double 10 [1..]) 0
 -- ArrayFire Array
 --   [1 1 1 1]
 --      5.5000
 mean
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Int
@@ -68,7 +68,7 @@
 --   [1 1 1 1]
 --      7.0000
 meanWeighted
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Array a
@@ -88,7 +88,7 @@
 --   [1 1 1 1]
 --      5.2500
 var
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> VarianceType
@@ -112,7 +112,7 @@
 --   [1 1 1 1]
 --      1.9091
 varWeighted
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Array a
@@ -132,7 +132,7 @@
 --   [1 1 1 1]
 --      1.0000
 stdev
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Int
@@ -150,7 +150,7 @@
 --   [1 1 1 1]
 --      0.0000
 cov
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ First input 'Array'
   -> Array a
@@ -170,7 +170,7 @@
 --   [1 1 1 1]
 --      5.5000
 median
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Int
@@ -332,7 +332,7 @@
 -- [1 1 1 1]
 --    1.2500
 meanVar
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> VarBias
@@ -353,7 +353,7 @@
 -- [1 1 1 1]
 --    2.5000
 meanVarWeighted
-  :: AFType a
+  :: (AFType a, Fractional a)
   => Array a
   -- ^ Input 'Array'
   -> Array a
diff --git a/src/ArrayFire/Util.hs b/src/ArrayFire/Util.hs
--- a/src/ArrayFire/Util.hs
+++ b/src/ArrayFire/Util.hs
@@ -39,9 +39,11 @@
 import Foreign.C.String
 import Foreign.ForeignPtr
 import Foreign.Marshal         hiding (void)
+import Foreign.Ptr             (castPtr)
 import Foreign.Storable
 import System.IO.Unsafe
 
+import ArrayFire.Internal.Device (af_free_host)
 import ArrayFire.Internal.Types
 import ArrayFire.Internal.Util
 
@@ -264,7 +266,12 @@
     withCString expr $ \expCstr ->
       alloca $ \ocstr -> do
         throwAFError =<< af_array_to_string ocstr expCstr aptr prec trans
-        peekCString =<< peek ocstr
+        strPtr <- peek ocstr
+        str <- peekCString strPtr
+        -- the string is allocated by ArrayFire with af_alloc_host; free it
+        -- to avoid leaking on every Show
+        _ <- af_free_host (castPtr strPtr)
+        pure str
 
 -- | Retrieve size of ArrayFire data type
 --
diff --git a/src/ArrayFire/Vision.hs b/src/ArrayFire/Vision.hs
--- a/src/ArrayFire/Vision.hs
+++ b/src/ArrayFire/Vision.hs
@@ -22,6 +22,8 @@
 import Foreign.Storable
 import System.IO.Unsafe
 
+import Data.Word (Word32)
+
 import ArrayFire.Exception
 import ArrayFire.FFI
 import ArrayFire.Internal.Features
@@ -77,8 +79,9 @@
   -> Int
   -- ^ square window size, the covariation matrix will be calculated to a square neighborhood of this size (must be >= 3 and <= 31)
   -> Float
-  -- ^ struct containing arrays for x and y coordinates and score (Harris response), while arrays orientation and size are set to 0 and 1, respectively, because Harris does not compute that information
+  -- ^ Harris constant k, the sensitivity factor used in the corner response formula (usually 0.04)
   -> Features
+  -- ^ struct containing arrays for x and y coordinates and score (Harris response), while arrays orientation and size are set to 0 and 1, respectively, because Harris does not compute that information
 {-# NOINLINE harris #-}
 harris (Array fptr) (fromIntegral -> maxc) minresp sigma (fromIntegral -> bs) thr
   = unsafePerformIO . mask_ . withForeignPtr fptr $ \aptr ->
@@ -212,9 +215,9 @@
  -- ^ indicates the dimension to analyze for distance (the dimension indicated here must be of equal length for both query and train arrays)
  -> Int
  -- ^ is the number of smallest distances to return (currently, only 1 is supported)
- -> (Array a, Array a)
- -- ^ is an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the index of the Jth smallest distance to the Ith query value in the train data array. the index of the Ith smallest distance of the Mth query.
- -- is an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the Hamming distance of the Jth smallest distance to the Ith query value in the train data array.
+ -> (Array Word32, Array a)
+ -- ^ first component: an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the index (@u32@) of the Jth smallest distance to the Ith query value in the train data array.
+ -- second component: an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the Hamming distance of the Jth smallest distance to the Ith query value in the train data array.
 hammingMatcher a b (fromIntegral -> x) (fromIntegral -> y)
   = op2p2 a b (\p c d e -> af_hamming_matcher p c d e x y)
 
@@ -235,9 +238,9 @@
  -- ^ is the number of smallest distances to return (currently, only values <= 256 are supported)
  -> MatchType
  -- ^ is the distance computation type. Currently AF_SAD (sum of absolute differences), AF_SSD (sum of squared differences), and AF_SHD (hamming distances) are supported.
- -> (Array a, Array a)
- -- ^ is an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the index of the Jth smallest distance to the Ith query value in the train data array. the index of the Ith smallest distance of the Mth query.
- -- is an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the distance of the Jth smallest distance to the Ith query value in the train data array based on the dist_type chosen.
+ -> (Array Word32, Array a)
+ -- ^ first component: an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the index (@u32@) of the Jth smallest distance to the Ith query value in the train data array.
+ -- second component: an array of MxN size, where M is equal to the number of query features and N is equal to n_dist. The value at position IxJ indicates the distance of the Jth smallest distance to the Ith query value in the train data array based on the dist_type chosen.
 nearestNeighbor a b (fromIntegral -> x) (fromIntegral -> y) (fromMatchType -> match)
   = op2p2 a b (\p c d e -> af_nearest_neighbour p c d e x y match)
 
diff --git a/test/ArrayFire/AlgorithmSpec.hs b/test/ArrayFire/AlgorithmSpec.hs
--- a/test/ArrayFire/AlgorithmSpec.hs
+++ b/test/ArrayFire/AlgorithmSpec.hs
@@ -381,7 +381,7 @@
         length xs >= 2 ==>
           closeList
             (A.toList (A.diff1 (A.accum (A.vector (length xs) xs) 0) 0))
-            (tail xs)
+            (drop 1 xs)
 
     describe "set operation properties" $ do
       -- setUnion result contains all elements of each input.
diff --git a/test/ArrayFire/ImageSpec.hs b/test/ArrayFire/ImageSpec.hs
--- a/test/ArrayFire/ImageSpec.hs
+++ b/test/ArrayFire/ImageSpec.hs
@@ -94,9 +94,13 @@
       -- column-major: last element is the integral over the whole image
       last (A.toList (A.sat gray)) `shouldBeApprox` (16.0 :: Float)
 
-  describe "moments" $
+  describe "moments" $ do
     it "M00 of a constant image equals its total intensity (area)" $
-      A.momentsAll gray A.M00 `shouldBeApprox` (16.0 :: Double)
+      case A.momentsAll gray A.M00 of
+        [m00] -> m00 `shouldBeApprox` (16.0 :: Double)
+        ms    -> expectationFailure ("expected one moment, got " <> show ms)
+    it "FirstOrder returns all four moments without corrupting memory" $
+      length (A.momentsAll gray A.FirstOrder) `shouldBe` 4
 
   describe "Image I/O" $ do
     it "saveImage/loadImage round-trips a grayscale image" $ do
diff --git a/test/ArrayFire/NumericalSpec.hs b/test/ArrayFire/NumericalSpec.hs
--- a/test/ArrayFire/NumericalSpec.hs
+++ b/test/ArrayFire/NumericalSpec.hs
@@ -6,6 +6,7 @@
 module ArrayFire.NumericalSpec where
 
 import qualified ArrayFire as A
+import           ArrayFire.TestHelper  (skipOnBrokenOpenCL)
 import           Data.Function ((&))
 import           Test.Hspec
 import           Test.Hspec.QuickCheck (prop)
@@ -35,7 +36,8 @@
   -- Exact dominant eigenvalue = 3, eigenvector = [1,1]/√2
   -- Exercises: matrix, matmul, sumAll, *, /, scalar, sqrt, Haskell iterate
   describe "Power iteration" $ do
-    it "converges to dominant eigenvalue 3 of [[2,1],[1,2]]" $ do
+    it "converges to dominant eigenvalue 3 of [[2,1],[1,2]]" $
+      skipOnBrokenOpenCL "af_matmul output not synced on AF 3.8.2 OpenCL" $ do
       let a       = A.matrix @Double (2,2) [[2,1],[1,2]]
           v0      = A.matrix @Double (2,1) [[1,1]]
           norm2 v = sqrt @Double (A.sumAll (v * v))
@@ -106,7 +108,8 @@
   -- Uses a complex Dirac delta: |x|² = 1, FFT is a flat spectrum |X[k]|² = 1 each.
   -- Exercises: mkArray, fft, conjg, real, sumAll, *
   describe "Parseval's theorem" $ do
-    it "time-domain and frequency-domain energies agree" $ do
+    it "time-domain and frequency-domain energies agree" $
+      skipOnBrokenOpenCL "af_fft unreliable on AF 3.8.2 OpenCL" $ do
       let n       = 64 :: Int
           -- Dirac delta: all energy in first sample
           xs      = A.mkArray @(A.Complex Double) [n] (1 : repeat 0)
diff --git a/test/ArrayFire/SparseSpec.hs b/test/ArrayFire/SparseSpec.hs
--- a/test/ArrayFire/SparseSpec.hs
+++ b/test/ArrayFire/SparseSpec.hs
@@ -2,6 +2,7 @@
 module ArrayFire.SparseSpec where
 
 import qualified ArrayFire as A
+import           Control.Exception (evaluate)
 import           Data.Int
 import           Test.Hspec
 
@@ -11,10 +12,6 @@
 diag3 = A.mkArray @Double [3,3] [1,0,0, 0,2,0, 0,0,3]
 
 spec :: Spec
-spec = pure ()
-
-{--
-
 spec =
   describe "Sparse" $ do
 
@@ -25,6 +22,10 @@
         A.sparseGetNNZ (A.createSparseArrayFromDense (A.mkArray @Double [2,2] [1,2,3,4]) A.CSR) `shouldBe` 4
       it "storage format is preserved" $
         A.sparseGetStorage (A.createSparseArrayFromDense diag3 A.CSR) `shouldBe` A.CSR
+      it "all-zero matrix throws instead of segfaulting" $ do
+        let z = A.mkArray @Double [3,3] (replicate 9 0)
+        evaluate (A.sparseGetNNZ (A.createSparseArrayFromDense z A.CSR))
+          `shouldThrow` anyException
 
     describe "sparseToDense" $
       it "CSR round-trip preserves all values" $ do
@@ -47,5 +48,3 @@
         A.sparseGetNNZ sp `shouldBe` 3
         A.sparseGetStorage sp `shouldBe` A.COO
         A.sparseToDense (A.sparseConvertTo sp A.CSR) `shouldBe` diag3
-
---}
diff --git a/test/ArrayFire/StatisticsSpec.hs b/test/ArrayFire/StatisticsSpec.hs
--- a/test/ArrayFire/StatisticsSpec.hs
+++ b/test/ArrayFire/StatisticsSpec.hs
@@ -28,14 +28,17 @@
         `shouldBe`
            5.25
     it "Should find the weighted variance (equal weights)" $ do
-      head (toList (varWeighted (vector @Double 8 [1..]) (vector @Double 8 (repeat 1)) 0))
-        `shouldBeApprox` 5.25
+      case listToMaybe (toList (varWeighted (vector @Double 8 [1..]) (vector @Double 8 (repeat 1)) 0)) of
+        Nothing -> expectationFailure "expected a value, got empty array"
+        Just v  -> v `shouldBeApprox` 5.25
     it "Should find the weighted variance (increasing weights)" $ do
-      head (toList (varWeighted (vector @Double 10 [1..]) (vector @Double 10 [1..]) 0))
-        `shouldBeApprox` (21/11 :: Double)
+      case listToMaybe (toList (varWeighted (vector @Double 10 [1..]) (vector @Double 10 [1..]) 0)) of
+        Nothing -> expectationFailure "expected a value, got empty array"
+        Just v  -> v `shouldBeApprox` (21/11 :: Double)
     it "Should find the standard deviation" $ do
-      head (toList (stdev (vector @Double 10 (cycle [1,-1])) 0))
-        `shouldBeApprox` 1.0
+      case listToMaybe (toList (stdev (vector @Double 10 (cycle [1,-1])) 0)) of
+        Nothing -> expectationFailure "expected a value, got empty array"
+        Just v  -> v `shouldBeApprox` 1.0
     it "Should find the covariance" $ do
       cov (vector @Double 10 (repeat 1)) (vector @Double 10 (repeat 1)) False
         `shouldBe`
diff --git a/test/ArrayFire/TestHelper.hs b/test/ArrayFire/TestHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/ArrayFire/TestHelper.hs
@@ -0,0 +1,26 @@
+module ArrayFire.TestHelper where
+
+import qualified ArrayFire       as A
+import           System.IO.Unsafe (unsafePerformIO)
+import           Test.Hspec
+
+-- | True when running on the AF 3.8.2 OpenCL backend.
+--
+-- AF 3.8.2 OpenCL has two distinct classes of breakage:
+--
+--  * Vision kernels (FAST, Harris, ORB, SUSAN): thresholds ignored, garbage
+--    feature coordinates, af_orb can abort the process.
+--
+--  * Asynchronous BLAS (af_matmul, af_gemm): clBLAS enqueues kernels without
+--    a synchronisation barrier on the output buffer.  Subsequent JIT
+--    operations read the unfilled buffer, producing wrong results.  The CPU
+--    backend uses synchronous BLAS so it is unaffected.
+brokenOpenCL :: Bool
+brokenOpenCL = unsafePerformIO ((== A.OpenCL) <$> A.getActiveBackend)
+{-# NOINLINE brokenOpenCL #-}
+
+-- | Skip an expectation on the broken AF 3.8.2 OpenCL backend.
+skipOnBrokenOpenCL :: String -> Expectation -> Expectation
+skipOnBrokenOpenCL reason action
+  | brokenOpenCL = pendingWith reason
+  | otherwise    = action
diff --git a/test/ArrayFire/VisionSpec.hs b/test/ArrayFire/VisionSpec.hs
--- a/test/ArrayFire/VisionSpec.hs
+++ b/test/ArrayFire/VisionSpec.hs
@@ -3,11 +3,15 @@
 module ArrayFire.VisionSpec where
 
 import qualified ArrayFire as A
+import           ArrayFire.TestHelper  (skipOnBrokenOpenCL)
 import           Control.Exception (SomeException, evaluate, try)
 import           Control.Monad     (when)
 import           Test.Hspec
 
--- | 100×100 constant-intensity Float image. No edges or corners.
+skipOnBrokenBackend :: Expectation -> Expectation
+skipOnBrokenBackend = skipOnBrokenOpenCL "Vision detectors broken on AF 3.8.2 OpenCL"
+
+-- | 32×32 constant-intensity Float image. No edges or corners.
 -- FAST / Harris / SUSAN must produce 0 features on this image.
 flatImg :: A.Array Float
 flatImg = A.constant @Float [32, 32] 0.5
@@ -22,6 +26,20 @@
       br = A.constant @Float [16, 16] 0.0
   in A.join 0 (A.join 1 tl tr) (A.join 1 bl br)
 
+-- | 128×128 quadrant image for ORB tests.
+-- ORB requires min(h,w) / scl_fctr >= REF_PAT_SIZE (31), i.e. the image must
+-- be at least 47px on each side for scl_fctr=1.5.  32×32 triggers an
+-- unchecked underflow in the pyramid-sizing loop (max_levels stays 0, then
+-- lvl_best[UINT_MAX] is written → process abort).  128×128 is well above
+-- the threshold and gives ORB enough room to find features at multiple levels.
+orbImg :: A.Array Float
+orbImg =
+  let tl = A.constant @Float [64, 64] 0.0
+      tr = A.constant @Float [64, 64] 1.0
+      bl = A.constant @Float [64, 64] 1.0
+      br = A.constant @Float [64, 64] 0.0
+  in A.join 0 (A.join 1 tl tr) (A.join 1 bl br)
+
 xpos, ypos, score, orient, size_ :: A.Features -> A.Array Float
 xpos   = A.getFeaturesXPos
 ypos   = A.getFeaturesYPos
@@ -30,11 +48,6 @@
 size_  = A.getFeaturesSize
 
 spec :: Spec
-spec = pure ()
-
-{--
-
-spec :: Spec
 spec = describe "Vision spec" $ do
 
   -- ------------------------------------------------------------------ --
@@ -58,15 +71,15 @@
       A.getElements (orient feats) `shouldBe` n
       A.getElements (size_  feats) `shouldBe` n
 
-    it "detected x-coordinates lie in [0, 32)" $ do
+    it "detected x-coordinates lie in [0, 32)" $ skipOnBrokenBackend $ do
       let feats = A.fast quadrantImg 0.1 9 False 1.0 3
       A.toList (xpos feats) `shouldSatisfy` all (\x -> x >= (0 :: Float) && x < 32)
 
-    it "detected y-coordinates lie in [0, 32)" $ do
+    it "detected y-coordinates lie in [0, 32)" $ skipOnBrokenBackend $ do
       let feats = A.fast quadrantImg 0.1 9 False 1.0 3
       A.toList (ypos feats) `shouldSatisfy` all (\y -> y >= (0 :: Float) && y < 32)
 
-    it "all feature scores are non-negative" $ do
+    it "all feature scores are non-negative" $ skipOnBrokenBackend $ do
       let feats = A.fast quadrantImg 0.1 9 False 1.0 3
       A.toList (score feats) `shouldSatisfy` all (>= (0 :: Float))
 
@@ -74,21 +87,21 @@
   --  Harris
   -- ------------------------------------------------------------------ --
   describe "harris" $ do
-    it "detects 0 corners on a flat image" $ do
+    it "detects 0 corners on a flat image" $ skipOnBrokenBackend $ do
       A.getFeaturesNum (A.harris flatImg 500 1e-3 1.0 0 0.04) `shouldBe` 0
 
-    it "all accessor arrays are consistent with getFeaturesNum" $ do
+    it "all accessor arrays are consistent with getFeaturesNum" $ skipOnBrokenBackend $ do
       let feats = A.harris quadrantImg 500 1e-3 1.0 0 0.04
           n     = A.getFeaturesNum feats
       A.getElements (xpos  feats) `shouldBe` n
       A.getElements (ypos  feats) `shouldBe` n
       A.getElements (score feats) `shouldBe` n
 
-    it "detected x-coordinates lie in [0, 32)" $ do
+    it "detected x-coordinates lie in [0, 32)" $ skipOnBrokenBackend $ do
       A.toList (xpos (A.harris quadrantImg 500 1e-3 1.0 0 0.04))
         `shouldSatisfy` all (\x -> x >= 0 && x < 32)
 
-    it "detected y-coordinates lie in [0, 32)" $ do
+    it "detected y-coordinates lie in [0, 32)" $ skipOnBrokenBackend $ do
       A.toList (ypos (A.harris quadrantImg 500 1e-3 1.0 0 0.04))
         `shouldSatisfy` all (\y -> y >= 0 && y < 32)
 
@@ -96,14 +109,14 @@
   --  ORB
   -- ------------------------------------------------------------------ --
   describe "orb" $ do
-    it "descriptor row count equals getFeaturesNum" $ do
-      let (feats, descs) = A.orb quadrantImg 0.1 500 1.5 4 False
+    it "descriptor column count equals getFeaturesNum" $ skipOnBrokenBackend $ do
+      let (feats, descs) = A.orb orbImg 0.1 500 1.5 4 False
           n              = A.getFeaturesNum feats
-          (d0, _, _, _)  = A.getDims (descs :: A.Array Float)
-      d0 `shouldBe` n
+          (_, d1, _, _)  = A.getDims (descs :: A.Array Float)
+      d1 `shouldBe` n
 
-    it "all coordinate arrays are consistent with getFeaturesNum" $ do
-      let (feats, _) = A.orb quadrantImg 0.1 500 1.5 4 False
+    it "all coordinate arrays are consistent with getFeaturesNum" $ skipOnBrokenBackend $ do
+      let (feats, _) = A.orb orbImg 0.1 500 1.5 4 False
           n          = A.getFeaturesNum feats
       A.getElements (xpos   feats) `shouldBe` n
       A.getElements (ypos   feats) `shouldBe` n
@@ -123,14 +136,14 @@
         then pendingWith "susan threshold ignored on this platform (AF 3.8.2 OpenCL)"
         else n `shouldBe` 0
 
-    it "all accessor arrays are consistent with getFeaturesNum" $ do
+    it "all accessor arrays are consistent with getFeaturesNum" $ skipOnBrokenBackend $ do
       let feats = A.susan quadrantImg 3 0.1 0.5 0.05 3
           n     = A.getFeaturesNum feats
       A.getElements (xpos  feats) `shouldBe` n
       A.getElements (ypos  feats) `shouldBe` n
       A.getElements (score feats) `shouldBe` n
 
-    it "detected x-coordinates lie in [0, 32)" $ do
+    it "detected x-coordinates lie in [0, 32)" $ skipOnBrokenBackend $ do
       A.toList (xpos (A.susan quadrantImg 3 0.1 0.5 0.05 3))
         `shouldSatisfy` all (\x -> x >= (0 :: Float) && x < 32)
 
@@ -215,14 +228,14 @@
       let query         = A.mkArray @Float [4, 3] (replicate 12 0.0)
           train         = A.mkArray @Float [4, 5] (replicate 20 1.0)
           (idxs, dists) = A.nearestNeighbor query train 0 1 A.MatchTypeSAD
-      A.getElements @Float idxs  `shouldBe` 3
+      A.getElements @A.Word32 idxs  `shouldBe` 3
       A.getElements @Float dists `shouldBe` 3
 
     it "returned indices are within training-set bounds" $ do
       let query     = A.mkArray @Float [4, 3] (replicate 12 0.0)
           train     = A.mkArray @Float [4, 5] (replicate 20 1.0)
           (idxs, _) = A.nearestNeighbor query train 0 1 A.MatchTypeSAD
-      A.toList @Float idxs `shouldSatisfy` all (< 5)
+      A.toList @A.Word32 idxs `shouldSatisfy` all (< 5)
 
   -- ------------------------------------------------------------------ --
   --  homography
@@ -280,4 +293,3 @@
               (d0, d1, _, _) = A.getDims descs
           d0 `shouldBe` n
           when (n > 0) $ d1 `shouldBe` 272
---}
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -13,7 +13,9 @@
 import           Test.Hspec              (hspec, after_)
 import           Test.QuickCheck
 import           Test.QuickCheck.Classes
+import           Data.Typeable
 
+
 import qualified ArrayFire               as A
 import           ArrayFire               (Array)
 
@@ -34,8 +36,11 @@
     ]
    where
     (d0, d1, d2, d3) = A.getDims arr
-    ndim             = A.getNumDims arr
-    currentDims      = take ndim [d0, d1, d2, d3]
+    -- af_get_numdims collapses trailing unit dims ([2,1,1] → 1), losing the
+    -- constructed dimensionality.  Compute ndim directly from getDims instead.
+    allDims     = [d0, d1, d2, d3]
+    ndim        = length (dropWhile (== 1) (reverse allDims)) `max` 1
+    currentDims = take ndim allDims
     shrunkDims =
       [ [if i == j then d - 1 else d | (j, d) <- zip [0..] currentDims]
       | i <- [0 .. ndim - 1]
@@ -66,7 +71,7 @@
   negate x = 0 - x
 
 instance Arbitrary CBool where
-  arbitrary = CBool <$> arbitrary
+  arbitrary = elements [0, 1]
 
 instance (A.AFType a, Arbitrary a) => Arbitrary (Scalar a) where
   arbitrary = Scalar . A.scalar <$> arbitrary
@@ -87,6 +92,7 @@
 
 main :: IO ()
 main = A.withArrayFire $ do
+  A.info
   ref <- newIORef True
   let check = checkLaws ref
   -- IEEE 754 is not an exact ring; only Eq laws for floating-point arrays.
@@ -110,9 +116,10 @@
   ok <- readIORef ref
   unless ok exitFailure
 
-intChecks :: forall a. (A.AFType a, Arbitrary a, Num a, Eq a) => IORef Bool -> Proxy a -> IO ()
+intChecks :: forall a. (Typeable a, A.AFType a, Arbitrary a, Num a, Eq a) => IORef Bool -> Proxy a -> IO ()
 intChecks ref _ = do
-  checkLaws ref (numLaws      (Proxy :: Proxy (Scalar a)))
+  print $ typeOf (undefined :: a)
+  checkLaws ref (numLaws       (Proxy :: Proxy (Scalar a)))
   checkLaws ref (semiringLaws (Proxy :: Proxy (Scalar a)))
   checkLaws ref (ringLaws     (Proxy :: Proxy (Scalar a)))
   checkLaws ref (eqLaws       (Proxy :: Proxy (Array  a)))
