# This file defines workaround implementation for
# NumPy-compatibility functions that fall back to NumPy/CuPy functions
# for native/cuda devices respecitvely.
# The workaround does not support backprop, and also requires external
# libraries mentioned above.
# Functions defined in this file should be considered to have high priority for
# genuine implementations.
import numpy

import chainerx


try:
    import cupy
except Exception:
    cupy = None


class _DummyContext:
    def __enter__(self):
        pass

    def __exit__(self, type, value, traceback):
        pass


_dummy_context = _DummyContext()


def _to_numpy(array):
    assert isinstance(array, chainerx.ndarray)
    return chainerx.to_numpy(array, copy=False)


def _from_numpy(array):
    assert isinstance(array, numpy.ndarray)
    return chainerx.array(array, copy=False)


def _to_cupy(array):
    assert cupy is not None
    # Convert to cupy.ndarray on the same device as source array
    return chainerx._to_cupy(array)


def _from_cupy(array):
    assert cupy is not None
    assert isinstance(array, cupy.ndarray)
    device = chainerx.get_device('cuda', array.device.id)
    return chainerx._core._fromrawpointer(
        array.data.mem.ptr,
        array.shape,
        array.dtype,
        array.strides,
        device,
        array.data.ptr - array.data.mem.ptr,
        array)


def _from_chx(array, check_backprop=True):
    # Converts chainerx.ndarray to numpy/cupy.ndarray.
    # Objects with other types are kept intact.
    # Returns a pair: (xp, cupy device or dummy context, numpy/cupy.ndarray).
    if not isinstance(array, chainerx.ndarray):
        if (isinstance(array, numpy.ndarray)
                or (cupy and isinstance(array, cupy.ndarray))):
            raise TypeError(
                'ChainerX function fallback using NumPy/CuPy arrays '
                'is not supported.')
        # _from_chx is also called for slice and tuple objects
        # Used to index a chx array
        return None, _dummy_context, array
    if check_backprop and array.is_backprop_required():
        raise RuntimeError(
            'ChainerX function fallback using NumPy/CuPy is not '
            'supported for arrays that are connected to a graph.')
    backend_name = array.device.backend.name
    if backend_name == 'native':
        return numpy, _dummy_context, _to_numpy(array)
    if backend_name == 'cuda':
        if cupy is None:
            raise RuntimeError(
                'ChainerX fallback implementation for cuda backend requires '
                'cupy to be installed.')
        array_cupy = _to_cupy(array)
        return cupy, array_cupy.device, array_cupy
    raise RuntimeError(
        'ChainerX fallback implementation only supports native or cuda '
        'backends.')


def _to_chx(array):
    # Converts numpy/cupy.ndarray to chainerx.ndarray.
    # Objects with other types are kept intact.
    if isinstance(array, numpy.ndarray):
        return _from_numpy(array)
    elif cupy is not None and isinstance(array, cupy.ndarray):
        return _from_cupy(array)
    return array


def _populate_module_functions():

    def _fix(arr):
        xp, dev, arr = _from_chx(arr)
        with dev:
            ret = xp.fix(arr)
            ret = xp.asarray(ret)
        return _to_chx(ret)

    chainerx.fix = _fix


def _populate_ndarray():
    ndarray = chainerx.ndarray

    # __getitem__ with advanced indexing
    old_getitem = ndarray.__getitem__

    def __getitem__(arr, key):
        try:
            return old_getitem(arr, key)
        except (IndexError, chainerx.DimensionError):
            pass

        is_backprop_required = arr.is_backprop_required()

        xp, dev, arr = _from_chx(arr, check_backprop=False)
        # The elements used for indexing the array might be
        # also ChainerX arrays. _from_chx ignores
        # other types and return them as-is
        if isinstance(key, tuple):
            key = tuple([_from_chx(k, check_backprop=False)[2] for k in key])
        else:
            _, _, key = _from_chx(key, check_backprop=False)

        with dev:
            ret = arr[key]

        # Doing this check after the fallback __getitem__ because the error
        # which caused the fallback might not be due to advanced indexing.
        # In such case the fallback __getitem__ should also raise the error.

        if is_backprop_required:
            raise RuntimeError(
                'ChainerX getitem fallback for advanced indexing is not '
                'supported for arrays that are connected to a graph.')

        return _to_chx(ret)

    # __setitem__ with advanced indexing
    def __setitem__(self, key, value):
        if self.is_backprop_required():
            raise RuntimeError(
                'ChainerX setitem fallback for advanced indexing is not '
                'supported for arrays that are connected to a graph.')

        xp, dev, self = _from_chx(self)
        if isinstance(key, tuple):
            key = tuple([_from_chx(k)[2] for k in key])
        else:
            _, _, key = _from_chx(key)
        _, _, value = _from_chx(value)

        with dev:
            self[key] = value

    ndarray.__setitem__ = __setitem__
    ndarray.__getitem__ = __getitem__

    def tolist(arr):
        _, dev, arr = _from_chx(arr)
        with dev:
            ret = arr.tolist()
        return ret

    ndarray.tolist = tolist


def populate():
    _populate_module_functions()
    _populate_ndarray()

chainerx/_fallback_workarounds.py
# This file defines workaround implementation for
# NumPy-compatibility functions that fall back to NumPy/CuPy functions
# for native/cuda devices respecitvely.
# The workaround does not support backprop, and also requires external
# libraries mentioned above.
# Functions defined in this file should be considered to have high priority for
# genuine implementations.
import numpy

import chainerx


try:
    import cupy
except Exception:
    cupy = None


class _DummyContext:
    def __enter__(self):
        pass

    def __exit__(self, type, value, traceback):
        pass


_dummy_context = _DummyContext()


def _to_numpy(array):
    assert isinstance(array, chainerx.ndarray)
    return chainerx.to_numpy(array, copy=False)


def _from_numpy(array):
    assert isinstance(array, numpy.ndarray)
    return chainerx.array(array, copy=False)


def _to_cupy(array):
    assert cupy is not None
    # Convert to cupy.ndarray on the same device as source array
    return chainerx._to_cupy(array)


def _from_cupy(array):
    assert cupy is not None
    assert isinstance(array, cupy.ndarray)
    device = chainerx.get_device('cuda', array.device.id)
    return chainerx._core._fromrawpointer(
        array.data.mem.ptr,
        array.shape,
        array.dtype,
        array.strides,
        device,
        array.data.ptr - array.data.mem.ptr,
        array)


def _from_chx(array, check_backprop=True):
    # Converts chainerx.ndarray to numpy/cupy.ndarray.
    # Objects with other types are kept intact.
    # Returns a pair: (xp, cupy device or dummy context, numpy/cupy.ndarray).
    if not isinstance(array, chainerx.ndarray):
        if (isinstance(array, numpy.ndarray)
                or (cupy and isinstance(array, cupy.ndarray))):
            raise TypeError(
                'ChainerX function fallback using NumPy/CuPy arrays '
                'is not supported.')
        # _from_chx is also called for slice and tuple objects
        # Used to index a chx array
        return None, _dummy_context, array
    if check_backprop and array.is_backprop_required():
        raise RuntimeError(
            'ChainerX function fallback using NumPy/CuPy is not '
            'supported for arrays that are connected to a graph.')
    backend_name = array.device.backend.name
    if backend_name == 'native':
        return numpy, _dummy_context, _to_numpy(array)
    if backend_name == 'cuda':
        if cupy is None:
            raise RuntimeError(
                'ChainerX fallback implementation for cuda backend requires '
                'cupy to be installed.')
        array_cupy = _to_cupy(array)
        return cupy, array_cupy.device, array_cupy
    raise RuntimeError(
        'ChainerX fallback implementation only supports native or cuda '
        'backends.')


def _to_chx(array):
    # Converts numpy/cupy.ndarray to chainerx.ndarray.
    # Objects with other types are kept intact.
    if isinstance(array, numpy.ndarray):
        return _from_numpy(array)
    elif cupy is not None and isinstance(array, cupy.ndarray):
        return _from_cupy(array)
    return array


def _populate_module_functions():

    def _fix(arr):
        xp, dev, arr = _from_chx(arr)
        with dev:
            ret = xp.fix(arr)
            ret = xp.asarray(ret)
        return _to_chx(ret)

    chainerx.fix = _fix


def _populate_ndarray():
    ndarray = chainerx.ndarray

    # __getitem__ with advanced indexing
    old_getitem = ndarray.__getitem__

    def __getitem__(arr, key):
        if not isinstance(key, chainerx.ndarray):
            return old_getitem(arr, key)

        is_backprop_required = arr.is_backprop_required()

        xp, dev, arr = _from_chx(arr, check_backprop=False)
        # The elements used for indexing the array might be
        # also ChainerX arrays. _from_chx ignores
        # other types and return them as-is
        if isinstance(key, tuple):
            key = tuple([_from_chx(k, check_backprop=False)[2] for k in key])
        else:
            _, _, key = _from_chx(key, check_backprop=False)

        with dev:
            ret = arr[key]

        # Doing this check after the fallback __getitem__ because the error
        # which caused the fallback might not be due to advanced indexing.
        # In such case the fallback __getitem__ should also raise the error.

        if is_backprop_required:
            raise RuntimeError(
                'ChainerX getitem fallback for advanced indexing is not '
                'supported for arrays that are connected to a graph.')

        return _to_chx(ret)

    # __setitem__ with advanced indexing
    def __setitem__(self, key, value):
        if self.is_backprop_required():
            raise RuntimeError(
                'ChainerX setitem fallback for advanced indexing is not '
                'supported for arrays that are connected to a graph.')

        xp, dev, self = _from_chx(self)
        if isinstance(key, tuple):
            key = tuple([_from_chx(k)[2] for k in key])
        else:
            _, _, key = _from_chx(key)
        _, _, value = _from_chx(value)

        with dev:
            self[key] = value

    ndarray.__setitem__ = __setitem__
    ndarray.__getitem__ = __getitem__

    def tolist(arr):
        _, dev, arr = _from_chx(arr)
        with dev:
            ret = arr.tolist()
        return ret

    ndarray.tolist = tolist


def populate():
    _populate_module_functions()
    _populate_ndarray()

chainerx/_fallback_workarounds.py
diff --git a/chainerx/_fallback_workarounds.py b/chainerx/_fallback_workarounds.py
index 4ce2fd4b2998..bd9de3643515 100644
--- a/chainerx/_fallback_workarounds.py
+++ b/chainerx/_fallback_workarounds.py
@@ -118,10 +118,8 @@ def _populate_ndarray():
     old_getitem = ndarray.__getitem__
 
     def __getitem__(arr, key):
-        try:
+        if not isinstance(key, chainerx.ndarray):
             return old_getitem(arr, key)
-        except (IndexError, chainerx.DimensionError):
-            pass
 
         is_backprop_required = arr.is_backprop_required()
 
diff --git a/chainerx_cc/chainerx/CMakeLists.txt b/chainerx_cc/chainerx/CMakeLists.txt
index 056b91878335..ce0f781e6fb7 100644
--- a/chainerx_cc/chainerx/CMakeLists.txt
+++ b/chainerx_cc/chainerx/CMakeLists.txt
@@ -69,6 +69,7 @@ set(chainerx_srcs
     array.cc
     array_body.cc
     array_body_leak_detection.cc
+    array_index.cc
     array_repr.cc
     axes.cc
     backend.cc
diff --git a/chainerx_cc/chainerx/array_index.cc b/chainerx_cc/chainerx/array_index.cc
new file mode 100644
index 000000000000..243adbf74129
--- /dev/null
+++ b/chainerx_cc/chainerx/array_index.cc
@@ -0,0 +1,35 @@
+#include "chainerx/array_index.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "chainerx/error.h"
+
+namespace chainerx {
+namespace internal {
+
+std::vector<ArrayIndex> GetNormalizedArrayIndices(const std::vector<ArrayIndex>& indices, int8_t ndim) {
+    auto it = std::find_if(indices.begin(), indices.end(), [](const ArrayIndex& index) { return index.tag() == ArrayIndexTag::kEllipsis; });
+    if (it == indices.end()) {
+        return indices;
+    }
+    if (std::find_if(std::next(it), indices.end(), [](const ArrayIndex& index) { return index.tag() == ArrayIndexTag::kEllipsis; }) !=
+        indices.end()) {
+        throw IndexError{"Indices can only have a single ellipsis."};
+    }
+    std::vector<ArrayIndex> result;
+    int8_t ellipsized_ndim =
+            ndim - static_cast<int8_t>(indices.size()) + 1 +
+            std::count_if(indices.begin(), indices.end(), [](const ArrayIndex& index) { return index.tag() == ArrayIndexTag::kNewAxis; });
+    if (ellipsized_ndim < 0) {
+        throw IndexError{"Too many indices for array."};
+    }
+    result.reserve(ndim);
+    std::copy(indices.begin(), it, std::back_inserter(result));
+    std::fill_n(std::back_inserter(result), ellipsized_ndim, ArrayIndex{Slice{}});
+    std::copy(std::next(it), indices.end(), std::back_inserter(result));
+    return result;
+}
+
+}  // namespace internal
+}  // namespace chainerx
diff --git a/chainerx_cc/chainerx/array_index.h b/chainerx_cc/chainerx/array_index.h
index 474fae09804d..a0430b592434 100644
--- a/chainerx_cc/chainerx/array_index.h
+++ b/chainerx_cc/chainerx/array_index.h
@@ -2,6 +2,7 @@
 
 #include <cstdint>
 #include <utility>
+#include <vector>
 
 #include "chainerx/error.h"
 #include "chainerx/slice.h"
@@ -12,15 +13,19 @@ enum class ArrayIndexTag {
     kSingleElement = 1,
     kSlice,
     kNewAxis,
+    kEllipsis,
 };
 
 class NewAxis {};
 
+class Ellipsis {};
+
 class ArrayIndex {
 public:
     ArrayIndex(int64_t index) : tag_{ArrayIndexTag::kSingleElement}, index_{index} {}  // NOLINT
     ArrayIndex(Slice slice) : tag_{ArrayIndexTag::kSlice}, slice_{std::move(slice)} {}  // NOLINT
     ArrayIndex(NewAxis /*new_axis*/) : tag_{ArrayIndexTag::kNewAxis} {}  // NOLINT
+    ArrayIndex(Ellipsis /*ellipsis*/) : tag_{ArrayIndexTag::kEllipsis} {}  // NOLINT
 
     ArrayIndexTag tag() const { return tag_; }
 
@@ -44,4 +49,10 @@ class ArrayIndex {
     Slice slice_;
 };
 
+namespace internal {
+
+std::vector<ArrayIndex> GetNormalizedArrayIndices(const std::vector<ArrayIndex>& indices, int8_t ndim);
+
+}  // namespace internal
+
 }  // namespace chainerx
diff --git a/chainerx_cc/chainerx/python/array_index.cc b/chainerx_cc/chainerx/python/array_index.cc
index 288a9544ab9e..0fa4dbccb23e 100644
--- a/chainerx_cc/chainerx/python/array_index.cc
+++ b/chainerx_cc/chainerx/python/array_index.cc
@@ -28,6 +28,9 @@ ArrayIndex MakeArrayIndex(py::handle handle) {
     if (py::isinstance<py::slice>(handle)) {
         return ArrayIndex{MakeSlice(py::cast<py::slice>(handle))};
     }
+    if (py::isinstance<py::ellipsis>(handle)) {
+        return ArrayIndex{Ellipsis{}};
+    }
     // NumPy integer scalar
     auto numpy_integer_type = GetCachedNumpyInteger();
     static_assert(std::is_trivially_destructible<py::handle>::value, "");
diff --git a/chainerx_cc/chainerx/routines/indexing.cc b/chainerx_cc/chainerx/routines/indexing.cc
index 2cff41c13b36..ca7d958479c0 100644
--- a/chainerx_cc/chainerx/routines/indexing.cc
+++ b/chainerx_cc/chainerx/routines/indexing.cc
@@ -67,12 +67,13 @@ Array AddAt(const Array& a, const std::vector<ArrayIndex>& indices, const Array&
 }  // namespace
 
 Array At(const Array& a, const std::vector<ArrayIndex>& indices) {
+    std::vector<ArrayIndex> normalized_indices = internal::GetNormalizedArrayIndices(indices, a.ndim());
     Shape out_shape{};
     Strides out_strides{};
     int64_t out_offset = a.offset();
     bool is_a_empty = a.GetTotalSize() == 0;
     int64_t i_in = 0;
-    for (const ArrayIndex& index : indices) {
+    for (const ArrayIndex& index : normalized_indices) {
         switch (index.tag()) {
             case ArrayIndexTag::kSingleElement: {
                 int64_t dim = a.shape()[i_in];
@@ -104,7 +105,7 @@ Array At(const Array& a, const std::vector<ArrayIndex>& indices) {
                 out_strides.emplace_back(0);
                 break;
             default:
-                CHAINERX_NEVER_REACH();
+                ChainerxError{"Invalid ArrayIndexTag."};
         }
     }
     for (int64_t i = i_in; i < a.ndim(); ++i) {
@@ -116,7 +117,7 @@ Array At(const Array& a, const std::vector<ArrayIndex>& indices) {
 
     BackwardBuilder bb{"get_item", a, out};
     if (BackwardBuilder::Target bt = bb.CreateTarget(0)) {
-        bt.Define([indices, a_shape = a.shape(), a_dtype = a.dtype()](BackwardContext& bctx) {
+        bt.Define([indices = std::move(normalized_indices), a_shape = a.shape(), a_dtype = a.dtype()](BackwardContext& bctx) {
             const Array& gout = *bctx.output_grad();
             Array gin = Zeros(a_shape, a_dtype, gout.device());
             bctx.input_grad() = AddAt(gin, indices, gout);
diff --git a/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py b/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
index c6061ca742ca..1e63061341d2 100644
--- a/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
+++ b/tests/chainerx_tests/unit_tests/routines_tests/test_indexing.py
@@ -107,6 +107,12 @@
     ((), (chainerx.newaxis,)),
     ((3,), (chainerx.newaxis,)),
     ((2, 3), (chainerx.newaxis, chainerx.newaxis)),
+    # ellipsis indexing - non-tuple indexing
+    ((), Ellipsis),
+    ((3,), Ellipsis),
+    # ellipsis indexing - tuple indexing
+    ((), (Ellipsis,)),
+    ((2, 3), (Ellipsis,)),
     # mixed indexing - tuple indexing
     ((2, 3), (0, slice(1, 3))),
     ((4, 3), (slice(1, 3), 1)),
@@ -118,6 +124,14 @@
     ((2, 3, 4), (chainerx.newaxis, slice(0, 1), slice(1, 2), slice(1, 3))),
     ((2, 3, 4),
      (1, slice(2,), chainerx.newaxis, slice(1, 3), chainerx.newaxis)),
+    ((2, 3, 4), (0, Ellipsis)),
+    ((2, 3, 4), (Ellipsis, 2)),
+    ((2, 3, 4), (1, Ellipsis, 2)),
+    ((2, 3, 4), (1, Ellipsis, 2, 3)),
+    ((2, 3, 4), (chainerx.newaxis, Ellipsis, chainerx.newaxis)),
+    ((2, 3, 4), (1, Ellipsis, chainerx.newaxis, 3)),
+    ((2, 3, 4), (1, Ellipsis, 2, chainerx.newaxis, 3)),
+    ((2, 3, 4), (slice(0, 1), Ellipsis, slice(1, 3))),
 ])
 class TestGetitem(op_utils.NumpyOpTest):
     # TODO(niboshi): Remove this
@@ -141,6 +155,9 @@ def forward_xp(self, inputs, xp):
     ((3,), 3),
     ((3,), (0, 1)),
     ((2, 3,), (2, 0)),
+    ((2,), (2, chainerx.newaxis, 3)),
+    ((2,), (2, Ellipsis, chainerx.newaxis, 3)),
+    ((2,), (Ellipsis, Ellipsis)),
 ])
 def test_getitem_index_error(device, shape, indices):
     a = array_utils.create_dummy_ndarray(chainerx, shape, 'float32')

Support ellipsis in `Array::At` and `__getitem__`
Depends on #7559 because `py::ellipsis` is supported from v2.3.0.
