Repository: chainer/chainer

Issue:
F.matmul allows scalar operand
PEP465 does not allow multiplying constant using the matmul operator `@`.
```
>>> import chainer; import numpy as np
>>> chainer.functions.matmul(np.zeros((), 'f'), np.zeros((), 'f'))
variable(0.)
>>> np.matmul(np.zeros((), 'f'), np.zeros((), 'f'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Scalar operands are not allowed, use '*' instead
>>> np.zeros((), 'f') @ np.zeros((), 'f')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Scalar operands are not allowed, use '*' instead
>>> chainer.print_runtime_info()
Chainer: 4.0.0
NumPy: 1.14.2
CuPy: Not Available
>>>
```


Discussion:

Resolving patch:
diff --git a/chainer/functions/math/matmul.py b/chainer/functions/math/matmul.py
--- a/chainer/functions/math/matmul.py
+++ b/chainer/functions/math/matmul.py
@@ -83,13 +83,13 @@ def check_type_forward(self, in_types):
         type_check.expect(
             a_type.dtype.kind == 'f',
             b_type.dtype.kind == 'f',
+            a_type.ndim >= 1,
+            b_type.ndim >= 1,
         )
 
         a_ndim = type_check.eval(a_type.ndim)
         b_ndim = type_check.eval(b_type.ndim)
-        if a_ndim == 0 or b_ndim == 0:
-            pass
-        elif b_ndim == 1:
+        if b_ndim == 1:
             a_idx = -2 if self.transa and a_ndim > 1 else -1
             type_check.expect(a_type.shape[a_idx] == b_type.shape[0])
         elif a_ndim == 1:
@@ -107,11 +107,8 @@ def check_type_forward(self, in_types):
     def forward(self, x):
         self.retain_inputs((0, 1))
         a, b = x
-        if a.ndim == 0 or b.ndim == 0:
-            y = a * b
-        else:
-            # may broadcast
-            y = _matmul(a, b, self.transa, self.transb, self.transc)
+        # may broadcast
+        y = _matmul(a, b, self.transa, self.transb, self.transc)
 
         if self.dtype is not None:
             dtype = self.dtype


Test patch:
diff --git a/tests/chainer_tests/functions_tests/math_tests/test_basic_math.py b/tests/chainer_tests/functions_tests/math_tests/test_basic_math.py
--- a/tests/chainer_tests/functions_tests/math_tests/test_basic_math.py
+++ b/tests/chainer_tests/functions_tests/math_tests/test_basic_math.py
@@ -3,6 +3,7 @@
 import unittest
 
 import numpy
+import pytest
 
 import chainer
 from chainer.backends import cuda
@@ -399,16 +400,16 @@ def _test_constant_array(self, func):
         self._test_constant_array_one(
             func, x_data, numpy.array([3.0, 4.0], self.dtype))
 
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             self._test_constant_array_one(func, x_data, [3.0, 4.0])
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             self._test_constant_array_one(func, x_data, (3.0, 4.0))
 
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             self._test_constant_array_one(func, x_data, [3.0, 4.0, 5.0])
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             self._test_constant_array_one(func, x_data, (3.0, 4.0, 5.0))
-        with self.assertRaises(type_check.InvalidType):
+        with pytest.raises(type_check.InvalidType):
             self._test_constant_array_one(
                 func, x_data, numpy.array([3.0, 4.0, 5.0], self.dtype))
 
@@ -426,7 +427,7 @@ def _test_constant_array_gpu(self, func, exception=TypeError):
         self._test_constant_array_gpu_one(
             func, x_data, cuda.to_gpu(numpy.array([3.0, 4.0], self.dtype)))
 
-        with self.assertRaises(exception):
+        with pytest.raises(exception):
             self._test_constant_array_one(
                 func, x_data, cuda.to_gpu(
                     numpy.array([3.0, 4.0, 5.0], self.dtype)))
@@ -1311,6 +1312,30 @@ def test_double_backward_gpu(self):
             cuda.to_gpu(self.ggx), cuda.to_gpu(self.ggy))
 
 
+@testing.parameterize(
+    {'x_shape': (), 'y_shape': ()},
+    {'x_shape': (3, 2), 'y_shape': ()},
+    {'x_shape': (), 'y_shape': (2, 4)},
+    {'x_shape': (2, 3), 'y_shape': (2, 3)},
+    {'x_shape': (2,), 'y_shape': (1,)},
+)
+@unittest.skipUnless(sys.version_info >= (3, 5),
+                     'Only for Python3.5 or higher')
+class TestMatMulInvalidShape(unittest.TestCase):
+
+    dtype = numpy.float32
+
+    def setUp(self):
+        self.x = numpy.random.uniform(-1, 1, self.x_shape).astype(self.dtype)
+        self.y = numpy.random.uniform(-1, 1, self.y_shape).astype(self.dtype)
+
+    def test_invalid_type(self):
+        x = chainer.Variable(self.x)
+        y = chainer.Variable(self.y)
+        with pytest.raises(type_check.InvalidType):
+            operator.matmul(x, y)
+
+
 class TestNotSupportOperation(unittest.TestCase):
 
     def setUp(self):
@@ -1318,31 +1343,31 @@ def setUp(self):
         self.y = chainer.Variable(numpy.zeros(10))
 
     def test_lt(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x < self.y
 
     def test_le(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x <= self.y
 
     def test_eq(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x == self.y
 
     def test_ne(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x != self.y
 
     def test_gt(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x > self.y
 
     def test_ge(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             self.x >= self.y
 
     def test_nonzero(self):
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             if self.x:
                 pass