Skip to content

Commit 476ebae

Browse files
authored
Fix indexing for arrays with non-zero base (#1824)
* Fix indexing N-based system arrays * Support negative base arrays * Fix for Mono
1 parent c1345b4 commit 476ebae

File tree

2 files changed

+202
-26
lines changed

2 files changed

+202
-26
lines changed

Src/IronPython/Runtime/Operations/ArrayOps.cs

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,14 @@ public static Array Multiply(Array data, int count) {
171171
if (data == null) throw PythonOps.TypeError("expected Array, got None");
172172
if (data.Rank != 1) throw PythonOps.TypeError("bad dimensions for array, got {0} expected {1}", 1, data.Rank);
173173

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

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

181-
return GetSlice(data, data.Length, slice);
181+
return GetSlice(data, slice);
182182
}
183183

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

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

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

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

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

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

234-
for (int i = 0; i < indices.Length; i++) indices[i] += a.GetLowerBound(i);
235-
236233
Type elm = t.GetElementType()!;
237234
a.SetValue(Converter.Convert(indexAndValue[indexAndValue.Length - 1], elm), indices);
238235
}
@@ -243,9 +240,15 @@ public static void SetItem(Array a, Slice index, object? value) {
243240

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

243+
int lb = a.GetLowerBound(0);
244+
if (lb != 0) {
245+
FixSlice(index, a, out int start, out int stop, out int step);
246+
index = new Slice(start - lb, stop - lb, step);
247+
}
248+
246249
index.DoSliceAssign(
247250
delegate (int idx, object? val) {
248-
a.SetValue(Converter.Convert(val, elm), idx + a.GetLowerBound(0));
251+
a.SetValue(Converter.Convert(val, elm), idx + lb);
249252
},
250253
a.Length,
251254
value);
@@ -375,10 +378,10 @@ public static string __repr__(CodeContext/*!*/ context, [NotNone] Array/*!*/ sel
375378
return GetSlice(data, start, stop, step);
376379
}
377380

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

381-
slice.Indices(size, out int start, out int stop, out int step);
384+
FixSlice(slice, data, out int start, out int stop, out int step);
382385

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

390-
if (step == 1) {
393+
if (step == 1 && (!ClrModule.IsMono || data.GetLowerBound(0) == 0)) {
391394
int n = stop - start;
392395
Array ret = Array.CreateInstance(data.GetType().GetElementType()!, n);
393-
Array.Copy(data, start + data.GetLowerBound(0), ret, 0, n);
396+
Array.Copy(data, start, ret, 0, n); // doesn't work OK on Mono with non-0-based arrays
394397
return ret;
395398
} else {
396399
int n = PythonOps.GetSliceCount(start, stop, step);
397400
Array ret = Array.CreateInstance(data.GetType().GetElementType()!, n);
398401
int ri = 0;
399402
for (int i = 0, index = start; i < n; i++, index += step) {
400-
ret.SetValue(data.GetValue(index + data.GetLowerBound(0)), ri++);
403+
ret.SetValue(data.GetValue(index), ri++);
401404
}
402405
return ret;
403406
}
@@ -427,11 +430,72 @@ private static int[] TupleToIndices(Array a, IList<object?> tuple) {
427430
int[] indices = new int[tuple.Count];
428431
for (int i = 0; i < indices.Length; i++) {
429432
int iindex = Converter.ConvertToInt32(tuple[i]);
430-
indices[i] = i < a.Rank ? PythonOps.FixIndex(iindex, a.GetLength(i)) : int.MinValue;
433+
indices[i] = i < a.Rank ? FixIndex(a, iindex, i) : int.MinValue;
431434
}
432435
return indices;
433436
}
434437

438+
private static int FixIndex(Array a, int v, int axis) {
439+
int idx = v;
440+
int lb = a.GetLowerBound(axis);
441+
int ub = a.GetUpperBound(axis);
442+
if (idx < 0 && lb >= 0) {
443+
idx += ub + 1;
444+
}
445+
if (idx < lb || idx > ub) {
446+
throw PythonOps.IndexError("index out of range: {0}", v);
447+
}
448+
return idx;
449+
}
450+
451+
private static void FixSlice(Slice slice, Array a, out int ostart, out int ostop, out int ostep) {
452+
Debug.Assert(a.Rank == 1);
453+
454+
if (slice.step == null) {
455+
ostep = 1;
456+
} else {
457+
ostep = Converter.ConvertToIndex(slice.step);
458+
if (ostep == 0) {
459+
throw PythonOps.ValueError("step cannot be zero");
460+
}
461+
}
462+
463+
int lb = a.GetLowerBound(0);
464+
int ub = a.GetUpperBound(0);
465+
466+
if (slice.start == null) {
467+
ostart = ostep > 0 ? lb : ub;
468+
} else {
469+
ostart = Converter.ConvertToIndex(slice.start);
470+
if (ostart < lb) {
471+
if (ostart < 0 && lb >= 0) {
472+
ostart += ub + 1;
473+
}
474+
if (ostart < lb) {
475+
ostart = ostep > 0 ? lb : lb - 1;
476+
}
477+
} else if (ostart > ub) {
478+
ostart = ostep > 0 ? ub + 1 : ub;
479+
}
480+
}
481+
482+
if (slice.stop == null) {
483+
ostop = ostep > 0 ? ub + 1 : lb - 1;
484+
} else {
485+
ostop = Converter.ConvertToIndex(slice.stop);
486+
if (ostop < lb) {
487+
if (ostop < 0 && lb >= 0) {
488+
ostop += ub + 1;
489+
}
490+
if (ostop < 0) {
491+
ostop = ostep > 0 ? lb : lb - 1;
492+
}
493+
} else if (ostop > ub) {
494+
ostop = ostep > 0 ? ub + 1 : ub;
495+
}
496+
}
497+
}
498+
435499
#endregion
436500
}
437501
}

Tests/test_array.py

Lines changed: 125 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
## Test array support by IronPython (System.Array)
77
##
88

9+
"""
10+
Indexing of CLI arrays in IronPython:
11+
12+
13+
| Base | Index >= 0 | Index < 0 |
14+
|------|--------------------------------------|-------------------|
15+
| > 0 | absolue | relative from end |
16+
| 0 | absolute == relative from beginning | relative from end |
17+
| < 0 | absolute | absolute |
18+
19+
Comparison to indexing in C# and CPython:
20+
21+
* Index >= 0, any base is C# compliant.
22+
* Base 0, any index is CPython compliant.
23+
* 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.
24+
* Base > 0, index < 0 is not supported by C#; IronPython follows CPython convention as more practical.
25+
* Base < 0, index < 0 is C# compliant.
26+
* Base != 0 is not supported by CPython for any builtin structures.
27+
"""
28+
929
from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython
1030

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

169+
# slices on non-1-dim arrays are not supported
170+
array2 = System.Array.CreateInstance(int, 20, 20)
171+
self.assertRaises(NotImplementedError, lambda: array2[:]) # TODO: TypeError?
172+
self.assertRaises(TypeError, lambda: array2[:, :]) # TODO: NotImplementedError? This would work in Numpy and Sympy
173+
self.assertRaises(TypeError, lambda: array2[:, :, :]) # OK
174+
175+
149176
def test_creation(self):
150177
t = System.Array
151178
ti = type(System.Array.CreateInstance(int, 1))
@@ -173,35 +200,55 @@ def test_constructor(self):
173200

174201
def test_nonzero_lowerbound(self):
175202
a = System.Array.CreateInstance(int, (5,), (5,))
176-
for i in range(5): a[i] = i
203+
for i in range(5, 5 + a.Length): a[i] = i
177204

178-
self.assertEqual(a[:2], System.Array[int]((0,1)))
179-
self.assertEqual(a[2:], System.Array[int]((2,3,4)))
180-
self.assertEqual(a[2:4], System.Array[int]((2,3)))
181-
self.assertEqual(a[-1], 4)
205+
self.assertEqual(a[:7], System.Array[int]((5,6)))
206+
self.assertEqual(a[7:], System.Array[int]((7,8,9)))
207+
self.assertEqual(a[7:9], System.Array[int]((7,8)))
208+
self.assertEqual(a[-1:-3:-1], System.Array[int]((9,8)))
209+
self.assertEqual(a[-1], 9)
182210

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

185213
a = System.Array.CreateInstance(int, (5,), (15,))
186214
b = System.Array.CreateInstance(int, (5,), (20,))
187215
self.assertEqual(a.Length, b.Length)
188216
for i in range(a.Length):
189-
self.assertEqual(a[i], b[i])
217+
self.assertEqual(a[i + 15], b[i + 20])
218+
219+
a0 = System.Array.CreateInstance(int, 5) # regular, 0-based
220+
for i in range(5): a0[i] = i
190221

191-
## 5-dimension
222+
a[17:19] = a0[2:4]
223+
self.assertEqual(a[17:19], System.Array[int]((2, 3)))
224+
225+
self.assertEqual(a0[3:1:-1], System.Array[int]((3, 2)))
226+
self.assertEqual(a[18:16:-1], System.Array[int]((3, 2)))
227+
228+
self.assertEqual(a0[-3:-1], System.Array[int]((2, 3)))
229+
self.assertEqual(a[-3:-1], System.Array[int]((2, 3)))
230+
231+
a[18:16:-1] = a0[2:4]
232+
self.assertEqual(a[17:19], System.Array[int]((3, 2)))
233+
234+
## 5-dimension, 2-length/dim, progressive lowerbound
192235
a = System.Array.CreateInstance(int, (2,2,2,2,2), (1,2,3,4,5))
193-
self.assertEqual(a[0,0,0,0,0], 0)
236+
self.assertEqual(a[1,2,3,4,5], 0)
237+
238+
## 5-dimension, 2-length/dim, progressive lowerbound
239+
a = System.Array.CreateInstance(int, (2,2,2,2,2), (1,2,3,4,5))
240+
self.assertEqual(a[1,2,3,4,5], 0)
194241

195242
for i in range(5):
196-
index = [0,0,0,0,0]
197-
index[i] = 1
243+
index = [1,2,3,4,5]
244+
index[i] += 1
198245

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

202249
for i in range(5):
203-
index = [0,0,0,0,0]
204-
index[i] = 0
250+
index = [2,3,4,5,6]
251+
index[i] -= 1
205252

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

268+
def test_base1(self):
269+
# For positive base arrays, indices are indexing elements directly (in absolute terms)
270+
# rather than relative form the base.
271+
# Negative indices are indexing relative form the end.
272+
273+
# 1-based 2x2 matrix
274+
arr = System.Array.CreateInstance(str, (2,2), (1,1))
275+
276+
self.assertEqual(arr.GetLowerBound(0), 1)
277+
self.assertEqual(arr.GetLowerBound(1), 1)
278+
self.assertEqual(arr.GetUpperBound(0), 2)
279+
self.assertEqual(arr.GetUpperBound(1), 2)
280+
281+
arr.SetValue("a_1,1", System.Array[System.Int32]((1,1)))
282+
arr.SetValue("a_1,2", System.Array[System.Int32]((1,2)))
283+
arr.SetValue("a_2,1", System.Array[System.Int32]((2,1)))
284+
arr.SetValue("a_2,2", System.Array[System.Int32]((2,2)))
285+
286+
self.assertEqual(arr[1, 1], "a_1,1")
287+
self.assertEqual(arr[1, 2], "a_1,2")
288+
self.assertEqual(arr[2, 1], "a_2,1")
289+
self.assertEqual(arr[2, 2], "a_2,2")
290+
291+
arr[1, 1] = "b_1,1"
292+
arr[1, 2] = "b_1,2"
293+
arr[2, 1] = "b_2,1"
294+
arr[2, 2] = "b_2,2"
295+
296+
self.assertEqual(arr.GetValue(System.Array[System.Int32]((1,1))), "b_1,1")
297+
self.assertEqual(arr.GetValue(System.Array[System.Int32]((1,2))), "b_1,2")
298+
self.assertEqual(arr.GetValue(System.Array[System.Int32]((2,1))), "b_2,1")
299+
self.assertEqual(arr.GetValue(System.Array[System.Int32]((2,2))), "b_2,2")
300+
301+
def test_base_negative(self):
302+
# For negative base arrays, negative indices are indexing elements directly (like non negative indices)
303+
# rather than indexing relative from the end.
304+
305+
# 2-dim array [-1, 0, 1] x [-1, 0, 1]
306+
arr = System.Array.CreateInstance(str, (3,3), (-1,-1))
307+
for i in range(-1, 2):
308+
for j in range(-1, 2):
309+
arr[i, j] = "a_%d,%d" % (i, j)
310+
311+
for i in range(-1, 2):
312+
for j in range(-1, 2):
313+
self.assertEqual(arr[i, j], "a_%d,%d" % (i, j))
314+
315+
# test that VauleError is raised when the index is out of range
316+
self.assertRaises(IndexError, lambda: arr[-2, 0])
317+
self.assertRaises(IndexError, lambda: arr[2, 0])
318+
self.assertRaises(IndexError, lambda: arr[0, -2])
319+
self.assertRaises(IndexError, lambda: arr[0, 2])
320+
321+
# test slice indexing
322+
# 1-dim array [-1, 0, 1]
323+
arr1 = System.Array.CreateInstance(int, (3,), (-1,))
324+
for i in range(-1, 2):
325+
arr1[i] = i
326+
self.assertEqual(arr1[-1:1], System.Array[int]((-1, 0)))
327+
self.assertEqual(arr1[-2:1], System.Array[int]((-1, 0)))
328+
self.assertEqual(arr1[0:], System.Array[int]((0, 1)))
329+
self.assertEqual(arr1[:1], System.Array[int]((-1, 0)))
330+
self.assertEqual(arr1[:], System.Array[int]((-1, 0, 1)))
331+
self.assertEqual(arr1[:-2], System.Array[int](0))
332+
221333
def test_array_type(self):
222334

223335
def type_helper(array_type, instance):

0 commit comments

Comments
 (0)