[pypy-commit] pypy default: (mattip, fijal reviewing) Merge numppy-flatitter branch (yes, with all the

fijal noreply at buildbot.pypy.org
Fri Jan 27 20:01:15 CET 2012


Author: Maciej Fijalkowski <fijall at gmail.com>
Branch: 
Changeset: r51858:caa2d47782ba
Date: 2012-01-27 21:00 +0200
http://bitbucket.org/pypy/pypy/changeset/caa2d47782ba/

Log:	(mattip, fijal reviewing) Merge numppy-flatitter branch (yes, with
	all the typos). This improves flat iterator and adds __getitem__ and
	__setitem__ to it.

diff --git a/pypy/module/micronumpy/compile.py b/pypy/module/micronumpy/compile.py
--- a/pypy/module/micronumpy/compile.py
+++ b/pypy/module/micronumpy/compile.py
@@ -32,7 +32,8 @@
 class BadToken(Exception):
     pass
 
-SINGLE_ARG_FUNCTIONS = ["sum", "prod", "max", "min", "all", "any", "unegative"]
+SINGLE_ARG_FUNCTIONS = ["sum", "prod", "max", "min", "all", "any",
+                        "unegative", "flat"]
 
 class FakeSpace(object):
     w_ValueError = None
@@ -396,6 +397,8 @@
             elif self.name == "unegative":
                 neg = interp_ufuncs.get(interp.space).negative
                 w_res = neg.call(interp.space, [arr])
+            elif self.name == "flat":
+                w_res = arr.descr_get_flatiter(interp.space)
             else:
                 assert False # unreachable code
             if isinstance(w_res, BaseArray):
diff --git a/pypy/module/micronumpy/interp_iter.py b/pypy/module/micronumpy/interp_iter.py
--- a/pypy/module/micronumpy/interp_iter.py
+++ b/pypy/module/micronumpy/interp_iter.py
@@ -4,6 +4,49 @@
 from pypy.module.micronumpy.strides import calculate_broadcast_strides,\
      calculate_slice_strides
 
+""" This is a mini-tutorial on iterators, strides, and
+memory layout. It assumes you are familiar with the terms, see
+http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html
+for a more gentle introduction.
+
+Given an array x: x.shape == [5,6],
+
+At which byte in x.data does the item x[3,4] begin?
+if x.strides==[1,5]:
+    pData = x.pData + (x.start + 3*1 + 4*5)*sizeof(x.pData[0])
+    pData = x.pData + (x.start + 24) * sizeof(x.pData[0])
+so the offset of the element is 24 elements after the first
+
+What is the next element in x after coordinates [3,4]?
+if x.order =='C':
+   next == [3,5] => offset is 28
+if x.order =='F':
+   next == [4,4] => offset is 24
+so for the strides [1,5] x is 'F' contiguous
+likewise, for the strides [6,1] x would be 'C' contiguous.
+
+Iterators have an internal representation of the current coordinates
+(indices), the array, strides, and backstrides. A short digression to
+explain backstrides: what is the coordinate and offset after [3,5] in
+the example above?
+if x.order == 'C':
+   next == [4,0] => offset is 4
+if x.order == 'F':
+   next == [4,5] => offset is 25
+Note that in 'C' order we stepped BACKWARDS 24 while 'overflowing' a
+shape dimension
+  which is back 25 and forward 1,
+  which is x.strides[1] * (x.shape[1] - 1) + x.strides[0]
+so if we precalculate the overflow backstride as 
+[x.strides[i] * (x.shape[i] - 1) for i in range(len(x.shape))]
+we can go faster.
+All the calculations happen in next()
+
+next_step_x() tries to do the iteration for a number of steps at once,
+but then we cannot gaurentee that we only overflow one single shape 
+dimension, perhaps we could overflow times in one big step.
+"""
+
 # structures to describe slicing
 
 class Chunk(object):
@@ -55,9 +98,9 @@
         self.size = size
 
     def next(self, shapelen):
-        return self._next(1)
+        return self.next_skip_x(1)
 
-    def _next(self, ofs):
+    def next_skip_x(self, ofs):
         arr = instantiate(ArrayIterator)
         arr.size = self.size
         arr.offset = self.offset + ofs
@@ -65,7 +108,7 @@
 
     def next_no_increase(self, shapelen):
         # a hack to make JIT believe this is always virtual
-        return self._next(0)
+        return self.next_skip_x(0)
 
     def done(self):
         return self.offset >= self.size
@@ -137,6 +180,36 @@
         res._done = done
         return res
 
+    @jit.unroll_safe
+    def next_skip_x(self, shapelen, step):
+        shapelen = jit.promote(len(self.res_shape))
+        offset = self.offset
+        indices = [0] * shapelen
+        for i in range(shapelen):
+            indices[i] = self.indices[i]
+        done = False
+        for i in range(shapelen - 1, -1, -1):
+            if indices[i] < self.res_shape[i] - step:
+                indices[i] += step
+                offset += self.strides[i] * step
+                break
+            else:
+                remaining_step = (indices[i] + step) // self.res_shape[i]
+                this_i_step = step - remaining_step * self.res_shape[i]
+                offset += self.strides[i] * this_i_step
+                indices[i] = indices[i] +  this_i_step
+                step = remaining_step
+        else:
+            done = True
+        res = instantiate(ViewIterator)
+        res.offset = offset
+        res.indices = indices
+        res.strides = self.strides
+        res.backstrides = self.backstrides
+        res.res_shape = self.res_shape
+        res._done = done
+        return res
+
     def apply_transformations(self, arr, transformations):
         v = BaseIterator.apply_transformations(self, arr, transformations)
         if len(arr.shape) == 1:
diff --git a/pypy/module/micronumpy/interp_numarray.py b/pypy/module/micronumpy/interp_numarray.py
--- a/pypy/module/micronumpy/interp_numarray.py
+++ b/pypy/module/micronumpy/interp_numarray.py
@@ -4,7 +4,7 @@
 from pypy.interpreter.typedef import TypeDef, GetSetProperty
 from pypy.module.micronumpy import interp_ufuncs, interp_dtype, signature,\
      interp_boxes
-from pypy.module.micronumpy.strides import calculate_slice_strides
+from pypy.module.micronumpy.strides import calculate_slice_strides, to_coords
 from pypy.rlib import jit
 from pypy.rpython.lltypesystem import lltype, rffi
 from pypy.tool.sourcetools import func_with_new_name
@@ -59,6 +59,18 @@
     name='numpy_filterset',
 )
 
+flat_get_driver = jit.JitDriver(
+    greens=['shapelen', 'base'],
+    reds=['step', 'ri', 'basei', 'res'],
+    name='numpy_flatget',
+)
+
+flat_set_driver = jit.JitDriver(
+    greens=['shapelen', 'base'],
+    reds=['step', 'ai', 'lngth', 'arr', 'basei'],
+    name='numpy_flatset',
+)
+
 def _find_shape_and_elems(space, w_iterable):
     shape = [space.len_w(w_iterable)]
     batch = space.listview(w_iterable)
@@ -1455,31 +1467,119 @@
     @jit.unroll_safe
     def __init__(self, arr):
         arr = arr.get_concrete()
-        size = 1
-        for sh in arr.shape:
-            size *= sh
-        self.strides = [arr.strides[-1]]
-        self.backstrides = [arr.backstrides[-1]]
-        ViewArray.__init__(self, size, [size], arr.dtype, arr.order,
-                               arr)
         self.shapelen = len(arr.shape)
-        self.iter = OneDimIterator(arr.start, self.strides[0],
-                                   self.shape[0])
+        sig = arr.find_sig()
+        self.iter = sig.create_frame(arr).get_final_iter()
+        self.base = arr
+        self.index = 0
+        ViewArray.__init__(self, arr.size, [arr.size], arr.dtype, arr.order, arr)
 
     def descr_next(self, space):
         if self.iter.done():
             raise OperationError(space.w_StopIteration, space.w_None)
-        result = self.getitem(self.iter.offset)
+        result = self.base.getitem(self.iter.offset)
         self.iter = self.iter.next(self.shapelen)
+        self.index += 1
         return result
 
     def descr_iter(self):
         return self
 
+    def descr_index(self, space):
+        return space.wrap(self.index)
+
+    def descr_coords(self, space):
+        coords, step, lngth = to_coords(space, self.base.shape, 
+                            self.base.size, self.base.order, 
+                            space.wrap(self.index))
+        return space.newtuple([space.wrap(c) for c in coords])
+
+    @jit.unroll_safe
+    def descr_getitem(self, space, w_idx):
+        if not (space.isinstance_w(w_idx, space.w_int) or
+            space.isinstance_w(w_idx, space.w_slice)):
+            raise OperationError(space.w_IndexError,
+                                 space.wrap('unsupported iterator index'))
+        base = self.base
+        start, stop, step, lngth = space.decode_index4(w_idx, base.size)
+        # setslice would have been better, but flat[u:v] for arbitrary
+        # shapes of array a cannot be represented as a[x1:x2, y1:y2]
+        basei = ViewIterator(base.start, base.strides,
+                               base.backstrides,base.shape)
+        shapelen = len(base.shape)
+        basei = basei.next_skip_x(shapelen, start)
+        if lngth <2:
+            return base.getitem(basei.offset)
+        ri = ArrayIterator(lngth)
+        res = W_NDimArray(lngth, [lngth], base.dtype,
+                                    base.order)
+        while not ri.done():
+            flat_get_driver.jit_merge_point(shapelen=shapelen,
+                                             base=base,
+                                             basei=basei,
+                                             step=step,
+                                             res=res,
+                                             ri=ri,
+                                            ) 
+            w_val = base.getitem(basei.offset)
+            res.setitem(ri.offset,w_val)
+            basei = basei.next_skip_x(shapelen, step)
+            ri = ri.next(shapelen)
+        return res
+
+    def descr_setitem(self, space, w_idx, w_value):
+        if not (space.isinstance_w(w_idx, space.w_int) or
+            space.isinstance_w(w_idx, space.w_slice)):
+            raise OperationError(space.w_IndexError,
+                                 space.wrap('unsupported iterator index'))
+        base = self.base
+        start, stop, step, lngth = space.decode_index4(w_idx, base.size)
+        arr = convert_to_array(space, w_value)
+        ai = 0
+        basei = ViewIterator(base.start, base.strides,
+                               base.backstrides,base.shape)
+        shapelen = len(base.shape)
+        basei = basei.next_skip_x(shapelen, start)
+        while lngth > 0:
+            flat_set_driver.jit_merge_point(shapelen=shapelen,
+                                             basei=basei,
+                                             base=base,
+                                             step=step,
+                                             arr=arr,
+                                             ai=ai,
+                                             lngth=lngth,
+                                            ) 
+            v = arr.getitem(ai).convert_to(base.dtype)
+            base.setitem(basei.offset, v)
+            # need to repeat input values until all assignments are done
+            ai = (ai + 1) % arr.size
+            basei = basei.next_skip_x(shapelen, step)
+            lngth -= 1
+
+    def create_sig(self):
+        return signature.FlatSignature(self.base.dtype)
+
+    def descr_base(self, space):
+        return space.wrap(self.base)
+
 W_FlatIterator.typedef = TypeDef(
     'flatiter',
+    #__array__  = #MISSING
+    __iter__ = interp2app(W_FlatIterator.descr_iter),
+    __getitem__ = interp2app(W_FlatIterator.descr_getitem),
+    __setitem__ = interp2app(W_FlatIterator.descr_setitem),
+    __eq__ = interp2app(BaseArray.descr_eq),
+    __ne__ = interp2app(BaseArray.descr_ne),
+    __lt__ = interp2app(BaseArray.descr_lt),
+    __le__ = interp2app(BaseArray.descr_le),
+    __gt__ = interp2app(BaseArray.descr_gt),
+    __ge__ = interp2app(BaseArray.descr_ge),
+    #__sizeof__ #MISSING
+    base = GetSetProperty(W_FlatIterator.descr_base),
+    index = GetSetProperty(W_FlatIterator.descr_index),
+    coords = GetSetProperty(W_FlatIterator.descr_coords),
     next = interp2app(W_FlatIterator.descr_next),
-    __iter__ = interp2app(W_FlatIterator.descr_iter),
+
 )
 W_FlatIterator.acceptable_as_base_class = False
 
diff --git a/pypy/module/micronumpy/signature.py b/pypy/module/micronumpy/signature.py
--- a/pypy/module/micronumpy/signature.py
+++ b/pypy/module/micronumpy/signature.py
@@ -224,6 +224,18 @@
         return ViewIterator(arr.start, arr.strides, arr.backstrides,
                             arr.shape).apply_transformations(arr, transforms)
 
+class FlatSignature(ViewSignature):
+    def debug_repr(self):
+        return 'Flat'
+
+    def allocate_iter(self, arr, transforms):
+        from pypy.module.micronumpy.interp_numarray import W_FlatIterator
+        assert isinstance(arr, W_FlatIterator)
+        return ViewIterator(arr.base.start, arr.base.strides, 
+                    arr.base.backstrides,
+                    arr.base.shape).apply_transformations(arr.base,
+                                                         transforms)
+
 class VirtualSliceSignature(Signature):
     def __init__(self, child):
         self.child = child
diff --git a/pypy/module/micronumpy/test/test_compile.py b/pypy/module/micronumpy/test/test_compile.py
--- a/pypy/module/micronumpy/test/test_compile.py
+++ b/pypy/module/micronumpy/test/test_compile.py
@@ -245,3 +245,11 @@
         a -> 3
         """)
         assert interp.results[0].value == 11
+
+    def test_flat_iter(self):
+        interp = self.run('''
+        a = |30|
+        b = flat(a)
+        b -> 3
+        ''')
+        assert interp.results[0].value == 3
diff --git a/pypy/module/micronumpy/test/test_iter.py b/pypy/module/micronumpy/test/test_iter.py
new file mode 100644
--- /dev/null
+++ b/pypy/module/micronumpy/test/test_iter.py
@@ -0,0 +1,88 @@
+from pypy.module.micronumpy.interp_iter import ViewIterator
+
+class TestIterDirect(object):
+    def test_C_viewiterator(self):
+        #Let's get started, simple iteration in C order with
+        #contiguous layout => strides[-1] is 1
+        start = 0
+        shape = [3, 5] 
+        strides = [5, 1]
+        backstrides = [x * (y - 1) for x,y in zip(strides, shape)]
+        assert backstrides == [10, 4]
+        i = ViewIterator(start, strides, backstrides, shape)
+        i = i.next(2)
+        i = i.next(2)
+        i = i.next(2)
+        assert i.offset == 3
+        assert not i.done()
+        assert i.indices == [0,3]
+        #cause a dimension overflow
+        i = i.next(2)
+        i = i.next(2)
+        assert i.offset == 5
+        assert i.indices == [1,0]
+
+        #Now what happens if the array is transposed? strides[-1] != 1
+        # therefore layout is non-contiguous
+        strides = [1, 3]
+        backstrides = [x * (y - 1) for x,y in zip(strides, shape)]
+        assert backstrides == [2, 12]
+        i = ViewIterator(start, strides, backstrides, shape)
+        i = i.next(2)
+        i = i.next(2)
+        i = i.next(2)
+        assert i.offset == 9
+        assert not i.done()
+        assert i.indices == [0,3]
+        #cause a dimension overflow
+        i = i.next(2)
+        i = i.next(2)
+        assert i.offset == 1
+        assert i.indices == [1,0]
+
+    def test_C_viewiterator_step(self):
+        #iteration in C order with #contiguous layout => strides[-1] is 1
+        #skip less than the shape
+        start = 0
+        shape = [3, 5] 
+        strides = [5, 1]
+        backstrides = [x * (y - 1) for x,y in zip(strides, shape)]
+        assert backstrides == [10, 4]
+        i = ViewIterator(start, strides, backstrides, shape)
+        i = i.next_skip_x(2,2)
+        i = i.next_skip_x(2,2)
+        i = i.next_skip_x(2,2)
+        assert i.offset == 6
+        assert not i.done()
+        assert i.indices == [1,1]
+        #And for some big skips
+        i = i.next_skip_x(2,5)
+        assert i.offset == 11
+        assert i.indices == [2,1]
+        i = i.next_skip_x(2,5)
+        # Note: the offset does not overflow but recycles,
+        # this is good for broadcast
+        assert i.offset == 1
+        assert i.indices == [0,1]
+        assert i.done()
+
+        #Now what happens if the array is transposed? strides[-1] != 1
+        # therefore layout is non-contiguous
+        strides = [1, 3]
+        backstrides = [x * (y - 1) for x,y in zip(strides, shape)]
+        assert backstrides == [2, 12]
+        i = ViewIterator(start, strides, backstrides, shape)
+        i = i.next_skip_x(2,2)
+        i = i.next_skip_x(2,2)
+        i = i.next_skip_x(2,2)
+        assert i.offset == 4
+        assert i.indices == [1,1]
+        assert not i.done()
+        i = i.next_skip_x(2,5)
+        assert i.offset == 5
+        assert i.indices == [2,1]
+        assert not i.done()
+        i = i.next_skip_x(2,5)
+        assert i.indices == [0,1]
+        assert i.offset == 3
+        assert i.done()
diff --git a/pypy/module/micronumpy/test/test_numarray.py b/pypy/module/micronumpy/test/test_numarray.py
--- a/pypy/module/micronumpy/test/test_numarray.py
+++ b/pypy/module/micronumpy/test/test_numarray.py
@@ -301,6 +301,12 @@
         for i in xrange(5):
             assert a[i] == b[i]
 
+    def test_getitem_nd(self):
+        from _numpypy import arange
+        a = arange(15).reshape(3, 5)
+        assert a[1, 3] == 8
+        assert a.T[1, 2] == 11 
+
     def test_setitem(self):
         from _numpypy import array
         a = array(range(5))
@@ -974,7 +980,7 @@
         assert debug_repr(a + a) == 'Call2(add, Array, Array)'
         assert debug_repr(a[::2]) == 'Slice'
         assert debug_repr(a + 2) == 'Call2(add, Array, Scalar)'
-        assert debug_repr(a + a.flat) == 'Call2(add, Array, Slice)'
+        assert debug_repr(a + a.flat) == 'Call2(add, Array, Flat)'
         assert debug_repr(sin(a)) == 'Call1(sin, Array)'
 
         b = a + a
@@ -1343,7 +1349,7 @@
         assert(b[:, 0] == a[0, :]).all()
 
     def test_flatiter(self):
-        from _numpypy import array, flatiter
+        from _numpypy import array, flatiter, arange
         a = array([[10, 30], [40, 60]])
         f_iter = a.flat
         assert f_iter.next() == 10
@@ -1356,6 +1362,9 @@
         for k in a.flat:
             s += k
         assert s == 140
+        a = arange(10).reshape(5, 2)
+        raises(IndexError, 'a.flat[(1, 2)]')
+        assert a.flat.base is a
 
     def test_flatiter_array_conv(self):
         from _numpypy import array, dot
@@ -1367,6 +1376,75 @@
         a = ones((2, 2))
         assert list(((a + a).flat)) == [2, 2, 2, 2]
 
+    def test_flatiter_getitem(self):
+        from _numpypy import arange
+        a = arange(10)
+        assert a.flat[3] == 3
+        assert a[2:].flat[3] == 5
+        assert (a + a).flat[3] == 6
+        assert a[::2].flat[3] == 6
+        assert a.reshape(2,5).flat[3] == 3
+        b = a.reshape(2,5).flat
+        b.next()
+        b.next()
+        b.next()
+        assert b[3] == 3
+        assert (b[::3] == [0, 3, 6, 9]).all()
+        assert (b[2::5] == [2, 7]).all()
+        assert b[-2] == 8
+        raises(IndexError, "b[11]")
+        raises(IndexError, "b[-11]")
+        raises(IndexError, 'b[0, 1]')
+        assert b.index == 3
+        assert b.coords == (0,3)
+
+    def test_flatiter_setitem(self):
+        from _numpypy import arange, array
+        a = arange(12).reshape(3,4)
+        b = a.T.flat
+        b[6::2] = [-1, -2]
+        assert (a == [[0, 1, -1, 3], [4, 5, 6, -1], [8, 9, -2, 11]]).all()
+        b[0:2] = [[[100]]]
+        assert(a[0,0] == 100)
+        assert(a[1,0] == 100)
+        raises(IndexError, 'b[array([10, 11])] == [-20, -40]')
+
+    def test_flatiter_ops(self):
+        from _numpypy import arange, array
+        a = arange(12).reshape(3,4)
+        b = a.T.flat
+        assert (b == [0,  4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]).all()
+        assert not (b != [0,  4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]).any()
+        assert ((b >= range(12)) == [True, True, True,False, True, True, 
+                             False, False, True, False, False, True]).all()
+        assert ((b < range(12)) != [True, True, True,False, True, True, 
+                             False, False, True, False, False, True]).all()
+        assert ((b <= range(12)) != [False, True, True,False, True, True, 
+                            False, False, True, False, False, False]).all()
+        assert ((b > range(12)) == [False, True, True,False, True, True, 
+                            False, False, True, False, False, False]).all()
+    def test_flatiter_view(self):
+        from _numpypy import arange
+        a = arange(10).reshape(5, 2)
+        #no == yet.
+        # a[::2].flat == [0, 1, 4, 5, 8, 9]
+        isequal = True
+        for y,z in zip(a[::2].flat, [0, 1, 4, 5, 8, 9]):
+            if y != z:
+                isequal = False
+        assert isequal == True
+
+    def test_flatiter_transpose(self):
+        from _numpypy import arange
+        a = arange(10).reshape(2,5).T
+        b = a.flat
+        assert (b[:5] == [0, 5, 1, 6, 2]).all()
+        b.next()
+        b.next()
+        b.next()
+        assert b.index == 3
+        assert b.coords == (1,1)
+
     def test_slice_copy(self):
         from _numpypy import zeros
         a = zeros((10, 10))
diff --git a/pypy/module/micronumpy/test/test_zjit.py b/pypy/module/micronumpy/test/test_zjit.py
--- a/pypy/module/micronumpy/test/test_zjit.py
+++ b/pypy/module/micronumpy/test/test_zjit.py
@@ -12,7 +12,7 @@
 from pypy.module.micronumpy.compile import (FakeSpace,
     IntObject, Parser, InterpreterState)
 from pypy.module.micronumpy.interp_numarray import (W_NDimArray,
-     BaseArray)
+     BaseArray, W_FlatIterator)
 from pypy.rlib.nonconst import NonConstant
 
 
@@ -369,7 +369,71 @@
                                 'setinteriorfield_raw': 1, 'int_add': 2,
                                 'int_ge': 1, 'guard_false': 1, 'jump': 1,
                                 'arraylen_gc': 1})
+    def define_flat_iter():
+        return '''
+        a = |30|
+        b = flat(a)
+        c = b + a
+        c -> 3
+        '''
 
+    def test_flat_iter(self):
+        result = self.run("flat_iter")
+        assert result == 6
+        self.check_trace_count(1)
+        self.check_simple_loop({'getinteriorfield_raw': 2, 'float_add': 1,
+                                'setinteriorfield_raw': 1, 'int_add': 3,
+                                'int_ge': 1, 'guard_false': 1,
+                                'arraylen_gc': 1, 'jump': 1})
+
+    def define_flat_getitem():
+        return '''
+        a = |30|
+        b = flat(a)
+        b -> 4: -> 6
+        '''
+
+    def test_flat_getitem(self):
+        result = self.run("flat_getitem")
+        assert result == 10.0
+        self.check_trace_count(1)
+        self.check_simple_loop({'getinteriorfield_raw': 1,
+                                'setinteriorfield_raw': 1,
+                                'int_lt': 1,
+                                'int_ge': 1,
+                                'int_add': 3,
+                                'guard_true': 1,
+                                'guard_false': 1,
+                                'arraylen_gc': 2,
+                                'jump': 1})
+
+    def define_flat_setitem():
+        return '''
+        a = |30|
+        b = flat(a)
+        b[4:] = a->:26
+        a -> 5
+        '''
+
+    def test_flat_setitem(self):
+        result = self.run("flat_setitem")
+        assert result == 1.0
+        self.check_trace_count(1)
+        # XXX not ideal, but hey, let's ignore it for now
+        self.check_simple_loop({'getinteriorfield_raw': 1,
+                                'setinteriorfield_raw': 1,
+                                'int_lt': 1,
+                                'int_gt': 1,
+                                'int_add': 4,
+                                'guard_true': 2,
+                                'arraylen_gc': 2,
+                                'jump': 1,
+                                'int_sub': 1,
+                                # XXX bad part
+                                'int_and': 1,
+                                'int_mod': 1,
+                                'int_rshift': 1,
+                                })
 
 class TestNumpyOld(LLJitMixin):
     def setup_class(cls):


More information about the pypy-commit mailing list