Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix indexing for arrays with non-zero base #1824

Merged
merged 3 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions Src/IronPython/Runtime/Operations/ArrayOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,14 @@ public static Array Multiply(Array data, int count) {
if (data == null) throw PythonOps.TypeError("expected Array, got None");
if (data.Rank != 1) throw PythonOps.TypeError("bad dimensions for array, got {0} expected {1}", 1, data.Rank);

return data.GetValue(PythonOps.FixIndex(index, data.Length) + data.GetLowerBound(0));
return data.GetValue(FixIndex(data, index, 0));
}

[SpecialName]
public static object GetItem(Array data, Slice slice) {
if (data == null) throw PythonOps.TypeError("expected Array, got None");

return GetSlice(data, data.Length, slice);
return GetSlice(data, slice);
}

[SpecialName]
Expand All @@ -201,7 +201,6 @@ public static object GetItem(Array data, Slice slice) {
int[] iindices = TupleToIndices(data, indices);
if (data.Rank != indices.Length) throw PythonOps.TypeError("bad dimensions for array, got {0} expected {1}", indices.Length, data.Rank);

for (int i = 0; i < iindices.Length; i++) iindices[i] += data.GetLowerBound(i);
return data.GetValue(iindices);
}

Expand All @@ -210,7 +209,7 @@ public static void SetItem(Array data, int index, object value) {
if (data == null) throw PythonOps.TypeError("expected Array, got None");
if (data.Rank != 1) throw PythonOps.TypeError("bad dimensions for array, got {0} expected {1}", 1, data.Rank);

data.SetValue(Converter.Convert(value, data.GetType().GetElementType()), PythonOps.FixIndex(index, data.Length) + data.GetLowerBound(0));
data.SetValue(Converter.Convert(value, data.GetType().GetElementType()), FixIndex(data, index, 0));
}

[SpecialName]
Expand All @@ -231,8 +230,6 @@ public static void SetItem(Array a, params object[] indexAndValue) {

if (a.Rank != args.Length) throw PythonOps.TypeError("bad dimensions for array, got {0} expected {1}", args.Length, a.Rank);

for (int i = 0; i < indices.Length; i++) indices[i] += a.GetLowerBound(i);

Type elm = t.GetElementType()!;
a.SetValue(Converter.Convert(indexAndValue[indexAndValue.Length - 1], elm), indices);
}
Expand All @@ -243,9 +240,15 @@ public static void SetItem(Array a, Slice index, object? value) {

Type elm = a.GetType().GetElementType()!;

int lb = a.GetLowerBound(0);
if (lb != 0) {
FixSlice(index, a, out int start, out int stop, out int step);
index = new Slice(start - lb, stop - lb, step);
}

index.DoSliceAssign(
delegate (int idx, object? val) {
a.SetValue(Converter.Convert(val, elm), idx + a.GetLowerBound(0));
a.SetValue(Converter.Convert(val, elm), idx + lb);
},
a.Length,
value);
Expand Down Expand Up @@ -375,10 +378,10 @@ public static string __repr__(CodeContext/*!*/ context, [NotNone] Array/*!*/ sel
return GetSlice(data, start, stop, step);
}

internal static Array GetSlice(Array data, int size, Slice slice) {
private static Array GetSlice(Array data, Slice slice) {
if (data.Rank != 1) throw PythonOps.NotImplementedError("slice on multi-dimensional array");

slice.Indices(size, out int start, out int stop, out int step);
FixSlice(slice, data, out int start, out int stop, out int step);

if ((step > 0 && start >= stop) || (step < 0 && start <= stop)) {
if (data.GetType().GetElementType() == typeof(object))
Expand All @@ -387,17 +390,17 @@ internal static Array GetSlice(Array data, int size, Slice slice) {
return Array.CreateInstance(data.GetType().GetElementType()!, 0);
}

if (step == 1) {
if (step == 1 && (!ClrModule.IsMono || data.GetLowerBound(0) == 0)) {
int n = stop - start;
Array ret = Array.CreateInstance(data.GetType().GetElementType()!, n);
Array.Copy(data, start + data.GetLowerBound(0), ret, 0, n);
Array.Copy(data, start, ret, 0, n); // doesn't work OK on Mono with non-0-based arrays
return ret;
} else {
int n = PythonOps.GetSliceCount(start, stop, step);
Array ret = Array.CreateInstance(data.GetType().GetElementType()!, n);
int ri = 0;
for (int i = 0, index = start; i < n; i++, index += step) {
ret.SetValue(data.GetValue(index + data.GetLowerBound(0)), ri++);
ret.SetValue(data.GetValue(index), ri++);
}
return ret;
}
Expand Down Expand Up @@ -427,11 +430,72 @@ private static int[] TupleToIndices(Array a, IList<object?> tuple) {
int[] indices = new int[tuple.Count];
for (int i = 0; i < indices.Length; i++) {
int iindex = Converter.ConvertToInt32(tuple[i]);
indices[i] = i < a.Rank ? PythonOps.FixIndex(iindex, a.GetLength(i)) : int.MinValue;
indices[i] = i < a.Rank ? FixIndex(a, iindex, i) : int.MinValue;
}
return indices;
}

private static int FixIndex(Array a, int v, int axis) {
int idx = v;
int lb = a.GetLowerBound(axis);
int ub = a.GetUpperBound(axis);
if (idx < 0 && lb >= 0) {
idx += ub + 1;
}
if (idx < lb || idx > ub) {
throw PythonOps.IndexError("index out of range: {0}", v);
}
return idx;
}

private static void FixSlice(Slice slice, Array a, out int ostart, out int ostop, out int ostep) {
Debug.Assert(a.Rank == 1);

if (slice.step == null) {
ostep = 1;
} else {
ostep = Converter.ConvertToIndex(slice.step);
if (ostep == 0) {
throw PythonOps.ValueError("step cannot be zero");
}
}

int lb = a.GetLowerBound(0);
int ub = a.GetUpperBound(0);

if (slice.start == null) {
ostart = ostep > 0 ? lb : ub;
} else {
ostart = Converter.ConvertToIndex(slice.start);
if (ostart < lb) {
if (ostart < 0 && lb >= 0) {
ostart += ub + 1;
}
if (ostart < lb) {
ostart = ostep > 0 ? lb : lb - 1;
}
} else if (ostart > ub) {
ostart = ostep > 0 ? ub + 1 : ub;
}
}

if (slice.stop == null) {
ostop = ostep > 0 ? ub + 1 : lb - 1;
} else {
ostop = Converter.ConvertToIndex(slice.stop);
if (ostop < lb) {
if (ostop < 0 && lb >= 0) {
ostop += ub + 1;
}
if (ostop < 0) {
ostop = ostep > 0 ? lb : lb - 1;
}
} else if (ostop > ub) {
ostop = ostep > 0 ? ub + 1 : ub;
}
}
}

#endregion
}
}
138 changes: 125 additions & 13 deletions Tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
## Test array support by IronPython (System.Array)
##

"""
Indexing of CLI arrays in IronPython:


| Base | Index >= 0 | Index < 0 |
|------|--------------------------------------|-------------------|
| > 0 | absolue | relative from end |
| 0 | absolute == relative from beginning | relative from end |
| < 0 | absolute | absolute |

Comparison to indexing in C# and CPython:

* Index >= 0, any base is C# compliant.
* Base 0, any index is CPython compliant.
* Base 0, index < 0 is not supported by C# but can be achieved by `System.Index` with 1-dim arrays only; then IronPython indexing is C# compliant.
* Base > 0, index < 0 is not supported by C#; IronPython follows CPython convention as more practical.
* Base < 0, index < 0 is C# compliant.
* Base != 0 is not supported by CPython for any builtin structures.
"""

from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython

if is_cli:
Expand Down Expand Up @@ -146,6 +166,13 @@ def test_slice(self):
def f(): array1[::2] = [x * 2 for x in range(11)]
self.assertRaises(ValueError, f)

# slices on non-1-dim arrays are not supported
array2 = System.Array.CreateInstance(int, 20, 20)
self.assertRaises(NotImplementedError, lambda: array2[:]) # TODO: TypeError?
self.assertRaises(TypeError, lambda: array2[:, :]) # TODO: NotImplementedError? This would work in Numpy and Sympy
self.assertRaises(TypeError, lambda: array2[:, :, :]) # OK


def test_creation(self):
t = System.Array
ti = type(System.Array.CreateInstance(int, 1))
Expand Down Expand Up @@ -173,35 +200,55 @@ def test_constructor(self):

def test_nonzero_lowerbound(self):
a = System.Array.CreateInstance(int, (5,), (5,))
for i in range(5): a[i] = i
for i in range(5, 5 + a.Length): a[i] = i

self.assertEqual(a[:2], System.Array[int]((0,1)))
self.assertEqual(a[2:], System.Array[int]((2,3,4)))
self.assertEqual(a[2:4], System.Array[int]((2,3)))
self.assertEqual(a[-1], 4)
self.assertEqual(a[:7], System.Array[int]((5,6)))
self.assertEqual(a[7:], System.Array[int]((7,8,9)))
self.assertEqual(a[7:9], System.Array[int]((7,8)))
self.assertEqual(a[-1:-3:-1], System.Array[int]((9,8)))
self.assertEqual(a[-1], 9)

self.assertEqual(repr(a), 'Array[int]((0, 1, 2, 3, 4), base: 5)')
self.assertEqual(repr(a), 'Array[int]((5, 6, 7, 8, 9), base: 5)')

a = System.Array.CreateInstance(int, (5,), (15,))
b = System.Array.CreateInstance(int, (5,), (20,))
self.assertEqual(a.Length, b.Length)
for i in range(a.Length):
self.assertEqual(a[i], b[i])
self.assertEqual(a[i + 15], b[i + 20])

a0 = System.Array.CreateInstance(int, 5) # regular, 0-based
for i in range(5): a0[i] = i

## 5-dimension
a[17:19] = a0[2:4]
self.assertEqual(a[17:19], System.Array[int]((2, 3)))

self.assertEqual(a0[3:1:-1], System.Array[int]((3, 2)))
self.assertEqual(a[18:16:-1], System.Array[int]((3, 2)))

self.assertEqual(a0[-3:-1], System.Array[int]((2, 3)))
self.assertEqual(a[-3:-1], System.Array[int]((2, 3)))

a[18:16:-1] = a0[2:4]
self.assertEqual(a[17:19], System.Array[int]((3, 2)))

## 5-dimension, 2-length/dim, progressive lowerbound
a = System.Array.CreateInstance(int, (2,2,2,2,2), (1,2,3,4,5))
self.assertEqual(a[0,0,0,0,0], 0)
self.assertEqual(a[1,2,3,4,5], 0)

## 5-dimension, 2-length/dim, progressive lowerbound
a = System.Array.CreateInstance(int, (2,2,2,2,2), (1,2,3,4,5))
self.assertEqual(a[1,2,3,4,5], 0)

for i in range(5):
index = [0,0,0,0,0]
index[i] = 1
index = [1,2,3,4,5]
index[i] += 1

a[index[0], index[1], index[2], index[3], index[4]] = i
self.assertEqual(a[index[0], index[1], index[2], index[3], index[4]], i)

for i in range(5):
index = [0,0,0,0,0]
index[i] = 0
index = [2,3,4,5,6]
index[i] -= 1

a[index[0], index[1], index[2], index[3], index[4]] = i
self.assertEqual(a[index[0], index[1], index[2], index[3], index[4]], i)
Expand All @@ -218,6 +265,71 @@ def sliceArrayAssign(arr, index, val):
self.assertRaises(NotImplementedError, sliceArrayAssign, a, -200, 1)
self.assertRaises(NotImplementedError, sliceArrayAssign, a, 1, 1)

def test_base1(self):
# For positive base arrays, indices are indexing elements directly (in absolute terms)
# rather than relative form the base.
# Negative indices are indexing relative form the end.

# 1-based 2x2 matrix
arr = System.Array.CreateInstance(str, (2,2), (1,1))

self.assertEqual(arr.GetLowerBound(0), 1)
self.assertEqual(arr.GetLowerBound(1), 1)
self.assertEqual(arr.GetUpperBound(0), 2)
self.assertEqual(arr.GetUpperBound(1), 2)

arr.SetValue("a_1,1", System.Array[System.Int32]((1,1)))
arr.SetValue("a_1,2", System.Array[System.Int32]((1,2)))
arr.SetValue("a_2,1", System.Array[System.Int32]((2,1)))
arr.SetValue("a_2,2", System.Array[System.Int32]((2,2)))

self.assertEqual(arr[1, 1], "a_1,1")
self.assertEqual(arr[1, 2], "a_1,2")
self.assertEqual(arr[2, 1], "a_2,1")
self.assertEqual(arr[2, 2], "a_2,2")

arr[1, 1] = "b_1,1"
arr[1, 2] = "b_1,2"
arr[2, 1] = "b_2,1"
arr[2, 2] = "b_2,2"

self.assertEqual(arr.GetValue(System.Array[System.Int32]((1,1))), "b_1,1")
self.assertEqual(arr.GetValue(System.Array[System.Int32]((1,2))), "b_1,2")
self.assertEqual(arr.GetValue(System.Array[System.Int32]((2,1))), "b_2,1")
self.assertEqual(arr.GetValue(System.Array[System.Int32]((2,2))), "b_2,2")

def test_base_negative(self):
# For negative base arrays, negative indices are indexing elements directly (like non negative indices)
# rather than indexing relative from the end.

# 2-dim array [-1, 0, 1] x [-1, 0, 1]
arr = System.Array.CreateInstance(str, (3,3), (-1,-1))
for i in range(-1, 2):
for j in range(-1, 2):
arr[i, j] = "a_%d,%d" % (i, j)

for i in range(-1, 2):
for j in range(-1, 2):
self.assertEqual(arr[i, j], "a_%d,%d" % (i, j))

# test that VauleError is raised when the index is out of range
self.assertRaises(IndexError, lambda: arr[-2, 0])
self.assertRaises(IndexError, lambda: arr[2, 0])
self.assertRaises(IndexError, lambda: arr[0, -2])
self.assertRaises(IndexError, lambda: arr[0, 2])

# test slice indexing
# 1-dim array [-1, 0, 1]
arr1 = System.Array.CreateInstance(int, (3,), (-1,))
for i in range(-1, 2):
arr1[i] = i
self.assertEqual(arr1[-1:1], System.Array[int]((-1, 0)))
self.assertEqual(arr1[-2:1], System.Array[int]((-1, 0)))
self.assertEqual(arr1[0:], System.Array[int]((0, 1)))
self.assertEqual(arr1[:1], System.Array[int]((-1, 0)))
self.assertEqual(arr1[:], System.Array[int]((-1, 0, 1)))
self.assertEqual(arr1[:-2], System.Array[int](0))

def test_array_type(self):

def type_helper(array_type, instance):
Expand Down
Loading