Skip to content

Commit c29f08f

Browse files
authored
Merge pull request #37 from Shimuuar/integer-tofrom
Add From/ToPy instances for Integer
2 parents 7a55003 + 2ea1502 commit c29f08f

8 files changed

Lines changed: 282 additions & 18 deletions

File tree

ChangeLog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
0.2.1.0 [XXX]
22
----------------
3+
* `From/ToPy` instance for `Integer`&`Natural` added.
4+
* `vector-0.13.2` is required
35
* Only Python>=3.10 is supported now. Earlier versions are not supported anymore.
46
Now they're tested on CI.
5-
67
* Documentation fixes
78

89
0.2 [2025.05.04]

cbits/python.c

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include <inline-python.h>
22
#include <stdlib.h>
33

4+
#include "MachDeps.h"
5+
6+
47
// ================================================================
58
// Callbacks
69
//
@@ -140,3 +143,70 @@ int inline_py_unpack_iterable(PyObject *iterable, int n, PyObject **out) {
140143
return -1;
141144
}
142145

146+
147+
PyObject* inline_py_Integer_ToPy(
148+
void* buf,
149+
size_t size,
150+
int sign
151+
)
152+
{
153+
PyObject* num =
154+
#if PY_MINOR_VERSION < 13
155+
_PyLong_FromByteArray(buf, size,
156+
1, // Little endian
157+
0 // Unsigned
158+
);
159+
#else
160+
PyLong_FromNativeBytes(buf, size,
161+
Py_ASNATIVEBYTES_LITTLE_ENDIAN |
162+
Py_ASNATIVEBYTES_UNSIGNED_BUFFER
163+
);
164+
#endif
165+
if( sign ) {
166+
PyObject* neg = PyNumber_Negative(num);
167+
Py_DECREF(num);
168+
return neg;
169+
} else {
170+
return num;
171+
}
172+
}
173+
174+
175+
ssize_t inline_py_Long_ByteSize(PyObject* p) {
176+
// See NOTE: [Integer encoding/decoding]
177+
//
178+
// PyLong_AsNativeBytes allows to compute buffer size but it does
179+
// so according to python's memory layout
180+
#if WORD_SIZE_IN_BITS == 32
181+
const int shiftW = 2;
182+
#elif WORD_SIZE_IN_BITS == 64
183+
const int shiftW = 3;
184+
#else
185+
#error "Something wrong with MachDeps.h"
186+
#endif
187+
const int shift = shiftW + 3;
188+
const ssize_t mask = (1<<shift) - 1;
189+
const ssize_t bits = _PyLong_NumBits(p);
190+
if( bits & mask ) {
191+
return ((bits >> shift) + 1) << shiftW;
192+
} else {
193+
return (bits >> shift) << shiftW;
194+
}
195+
}
196+
197+
void inline_py_Integer_FromPy(
198+
PyObject* p,
199+
void* buf,
200+
size_t size
201+
)
202+
{
203+
// N.B. _PyLong_AsByteArray changed signature in 3.13
204+
#if PY_MINOR_VERSION < 13
205+
_PyLong_AsByteArray((PyLongObject*)p, buf, size,
206+
1, // little_endian
207+
0 // is_signed
208+
);
209+
#else
210+
PyLong_AsNativeBytes(p, buf, size, -1);
211+
#endif
212+
}

include/inline-python.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,33 @@ int inline_py_unpack_iterable(
5151
int n,
5252
PyObject **out
5353
);
54+
55+
// Python's C API only gained public function to create integers of
56+
// arbitrary size in 3.13. We have to use internals for earlier
57+
// versions.
58+
PyObject* inline_py_Integer_ToPy(
59+
void* buf, // Buffer holding number
60+
size_t size, // Buffer size in bytes
61+
int sign // Sign of number (0 is +, 1 is -)
62+
);
63+
64+
// Compute size of buffer which can hold decoded number
65+
// and satistfy Integer's requirements
66+
//
67+
// See: NOTE: [Integer encoding/decoding]
68+
//
69+
// PRECONDITION: parameter must instance of PyLong. This is not
70+
// checked.
71+
// PRECONDITION: passed number must be positive
72+
ssize_t inline_py_Long_ByteSize(PyObject* p);
73+
74+
// Parse python integral number into buffer. This is compatibility
75+
// shim.
76+
//
77+
// PRECONDITION: parameter must instance of PyLong. This is not
78+
// checked.
79+
void inline_py_Integer_FromPy(
80+
PyObject* p,
81+
void* buf,
82+
size_t size
83+
);

inline-python.cabal

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Library
6767
, text >=2
6868
, bytestring >=0.11.2
6969
, exceptions >=0.10
70-
, vector >=0.13
70+
, vector >=0.13.2
7171
hs-source-dirs: src
7272
include-dirs: include
7373
c-sources: cbits/python.c
@@ -98,7 +98,7 @@ library test
9898
, tasty >=1.2
9999
, tasty-hunit >=0.10
100100
, tasty-quickcheck >=0.10
101-
, quickcheck-instances >=0.3.32
101+
, quickcheck-instances >=0.3.33
102102
, exceptions
103103
, containers
104104
, vector

src/Python/Inline/Literal.hs

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
{-# LANGUAGE CPP #-}
21
{-# LANGUAGE ForeignFunctionInterface #-}
32
{-# LANGUAGE MagicHash #-}
43
{-# LANGUAGE QuasiQuotes #-}
54
{-# LANGUAGE TemplateHaskell #-}
5+
{-# LANGUAGE UnliftedFFITypes #-}
66
-- |
77
-- Conversion between haskell data types and python values
88
module Python.Inline.Literal
@@ -34,18 +34,22 @@ import Data.Text.Lazy qualified as TL
3434
import Data.Vector.Generic qualified as VG
3535
import Data.Vector.Generic.Mutable qualified as MVG
3636
import Data.Vector qualified as V
37-
#if MIN_VERSION_vector(0,13,2)
3837
import Data.Vector.Strict qualified as VV
39-
#endif
4038
import Data.Vector.Storable qualified as VS
4139
import Data.Vector.Primitive qualified as VP
4240
import Data.Vector.Unboxed qualified as VU
41+
import Data.Primitive.ByteArray qualified as BA
42+
import Data.Primitive.Types (Prim(..))
43+
import Numeric.Natural (Natural)
4344
import Foreign.Ptr
4445
import Foreign.C.Types
4546
import Foreign.Storable
4647
import Foreign.Marshal.Alloc (alloca,mallocBytes)
4748
import Foreign.Marshal.Utils (copyBytes)
4849
import GHC.Float (float2Double, double2Float)
50+
import GHC.Exts (Int(..),Word(..),sizeofByteArray#,ByteArray#)
51+
import GHC.Num.Natural qualified
52+
import GHC.Num.Integer qualified
4953
import Data.Complex (Complex((:+)))
5054

5155
import Language.C.Inline qualified as C
@@ -290,6 +294,121 @@ instance FromPy Word32 where
290294
| otherwise -> throwM OutOfRange
291295

292296

297+
298+
-- NOTE: [Integer encoding/decoding]
299+
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
300+
--
301+
-- Interfacing between arbitrary precision integers in haskell and
302+
-- python is pain: they have different representations. And python got
303+
-- API for working with large numbers only in 3.13. We have to use
304+
-- internal API for earlier versions.
305+
--
306+
-- Only large number are discussed below. Small are straightforward
307+
-- enough.
308+
--
309+
-- + GHC's Integer use sign + little endian sequence of Word#. Since
310+
-- all supported platforms are LE it's same as little endian
311+
-- sequence of bytes.
312+
--
313+
-- + Important invariant: highest word must be nonzero!
314+
--
315+
-- + Python uses two-complement.
316+
--
317+
-- One problem is computation of required buffer size. (8byte word is
318+
-- assumed). For example 2^63 requires 9 bytes in two-complement
319+
-- encoding since we need one bit for sign. But 8 bytes enough for
320+
-- Integer's encoding. Sign is stored separately.
321+
322+
323+
-- | @since 0.2.1.0
324+
instance ToPy Integer where
325+
basicToPy (GHC.Num.Integer.IS i) = basicToPy (I# i)
326+
basicToPy (GHC.Num.Integer.IP p) = Py $ do
327+
let n = fromIntegral (I# (sizeofByteArray# p)) :: CSize
328+
inline_py_Integer_ToPy p n 0
329+
basicToPy (GHC.Num.Integer.IN p) = Py $ do
330+
let n = fromIntegral (I# (sizeofByteArray# p)) :: CSize
331+
inline_py_Integer_ToPy p n 1
332+
333+
-- | @since 0.2.1.0
334+
instance ToPy Natural where
335+
basicToPy (GHC.Num.Natural.NS i) = basicToPy (W# i)
336+
basicToPy (GHC.Num.Natural.NB p) = Py $ do
337+
let n = fromIntegral (I# (sizeofByteArray# p)) :: CSize
338+
inline_py_Integer_ToPy p n 0
339+
340+
-- | @since 0.2.1.0
341+
instance FromPy Integer where
342+
basicFromPy p = runProgram $ do
343+
progIO [CU.exp| int { PyLong_Check($(PyObject *p)) } |] >>= \case
344+
0 -> progIO $ throwM BadPyType
345+
_ -> pure ()
346+
-- At this point we know that p is number
347+
p_overflow <- withPyAlloca
348+
n <- progIO [CU.exp| long long { PyLong_AsLongLongAndOverflow($(PyObject* p), $(int* p_overflow)) } |]
349+
progIO (peek p_overflow) >>= \case
350+
-- Number fits into long long
351+
0 -> return $! fromIntegral n
352+
-- Number is positive
353+
1 -> do
354+
BA.ByteArray ba <- progIO $ decodePositiveInteger p
355+
pure $ GHC.Num.Integer.IP ba
356+
-- Number is negative
357+
-1 -> do
358+
neg <- takeOwnership
359+
<=< progPy
360+
$ throwOnNULL =<< Py [CU.exp| PyObject* { PyNumber_Negative( $(PyObject *p) ) } |]
361+
BA.ByteArray ba <- progIO $ decodePositiveInteger neg
362+
pure $ GHC.Num.Integer.IN ba
363+
-- Unreachable
364+
_ -> error "inline-py: FromPy Integer: INTERNAL ERROR"
365+
where
366+
367+
-- | @since 0.2.1.0
368+
instance FromPy Natural where
369+
basicFromPy p = runProgram $ do
370+
progIO [CU.exp| int { PyLong_Check($(PyObject *p)) } |] >>= \case
371+
0 -> progIO $ throwM BadPyType
372+
_ -> pure ()
373+
p_overflow <- withPyAlloca
374+
n <- progIO [CU.exp| long long { PyLong_AsLongLongAndOverflow($(PyObject* p), $(int* p_overflow)) } |]
375+
progIO (peek p_overflow) >>= \case
376+
-- Number fits into long long
377+
0 | n < 0 -> progIO $ throwM OutOfRange
378+
| otherwise -> return $! fromIntegral n
379+
-- Number is negative
380+
-1 -> progIO $ throwM OutOfRange
381+
-- Number is positive.
382+
--
383+
-- NOTE that if size of bytearray is equal to size of word we
384+
-- need to return small constructor
385+
1 -> progIO $ decodePositiveInteger p >>= \case
386+
BA.ByteArray ba
387+
| I# (sizeofByteArray# ba) == (finiteBitSize (0::Word) `div` 8)
388+
-> pure $! case indexByteArray# ba 0# of
389+
W# w -> GHC.Num.Natural.NS w
390+
| otherwise
391+
-> pure $! GHC.Num.Natural.NB ba
392+
-- Unreachable
393+
_ -> error "inline-py: FromPy Natural: INTERNAL ERROR"
394+
395+
-- Decode large positive number:
396+
-- + Must be instance of PyLong
397+
-- + Must be positive
398+
decodePositiveInteger :: Ptr PyObject -> IO BA.ByteArray
399+
decodePositiveInteger p_num = do
400+
sz <- [CU.exp| int { inline_py_Long_ByteSize( $(PyObject *p_num) ) } |]
401+
buf@(BA.MutableByteArray ptr_buf) <- BA.newByteArray (fromIntegral sz)
402+
_ <- inline_py_Integer_FromPy p_num ptr_buf (fromIntegral sz)
403+
BA.unsafeFreezeByteArray buf
404+
405+
406+
407+
foreign import ccall unsafe "inline_py_Integer_ToPy"
408+
inline_py_Integer_ToPy :: ByteArray# -> CSize -> CInt -> IO (Ptr PyObject)
409+
foreign import ccall unsafe "inline_py_Integer_FromPy"
410+
inline_py_Integer_FromPy :: Ptr PyObject -> BA.MutableByteArray# MVG.RealWorld -> CSize -> IO CInt
411+
293412
-- | Encoded as 1-character string
294413
instance ToPy Char where
295414
basicToPy c = do
@@ -308,8 +427,7 @@ instance FromPy Char where
308427
r <- Py [CU.block| int {
309428
PyObject* p = $(PyObject *p);
310429
if( !PyUnicode_Check(p) )
311-
return -1;
312-
if( 1 != PyUnicode_GET_LENGTH(p) )
430+
return -1; if( 1 != PyUnicode_GET_LENGTH(p) )
313431
return -1;
314432
switch( PyUnicode_KIND(p) ) {
315433
case PyUnicode_1BYTE_KIND:
@@ -515,11 +633,9 @@ instance (ToPy a, VP.Prim a) => ToPy (VP.Vector a) where
515633
-- | Converts to python's list
516634
instance (ToPy a, VU.Unbox a) => ToPy (VU.Vector a) where
517635
basicToPy = vectorToPy
518-
#if MIN_VERSION_vector(0,13,2)
519636
-- | Converts to python's list
520637
instance (ToPy a) => ToPy (VV.Vector a) where
521638
basicToPy = vectorToPy
522-
#endif
523639

524640
-- | Accepts python's sequence (@len@ and indexing)
525641
instance FromPy a => FromPy (V.Vector a) where
@@ -533,11 +649,9 @@ instance (FromPy a, VP.Prim a) => FromPy (VP.Vector a) where
533649
-- | Accepts python's sequence (@len@ and indexing)
534650
instance (FromPy a, VU.Unbox a) => FromPy (VU.Vector a) where
535651
basicFromPy = vectorFromPy
536-
#if MIN_VERSION_vector(0,13,2)
537652
-- | Accepts python's sequence (@len@ and indexing)
538653
instance FromPy a => FromPy (VV.Vector a) where
539654
basicFromPy = vectorFromPy
540-
#endif
541655

542656

543657
-- | Fold over python's iterator. Function takes ownership over iterator.

test/TST/FromPy.hs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Test.Tasty.HUnit
1010
import Python.Inline
1111
import Python.Inline.QQ
1212
import Data.Complex (Complex((:+)))
13+
import Numeric.Natural (Natural)
1314

1415
import TST.Util
1516

@@ -92,6 +93,27 @@ tests = testGroup "FromPy"
9293
, testCase "[3]" $ eq @[Int] (Just [1,2,3]) [pye| [1,2,3] |]
9394
, testCase "Int" $ eq @[Int] Nothing [pye| None |]
9495
]
96+
, testGroup "Integer" $
97+
let eqI = eq @Integer . Just
98+
in concat
99+
[ [ testCase (" 2^"++show k++"-1") $ eqI (2^k - 1) [pye| 2**k_hs - 1 |]
100+
, testCase (" 2^"++show k) $ eqI (2^k ) [pye| 2**k_hs |]
101+
, testCase (" 2^"++show k++"+1") $ eqI (2^k + 1) [pye| 2**k_hs + 1 |]
102+
, testCase ("-2^"++show k++"-1") $ eqI (negate $ 2^k - 1) [pye| -(2**k_hs - 1) |]
103+
, testCase ("-2^"++show k) $ eqI (negate $ 2^k ) [pye| -(2**k_hs) |]
104+
, testCase ("-2^"++show k++"+1") $ eqI (negate $ 2^k + 1) [pye| -(2**k_hs + 1) |]
105+
]
106+
| k <- [63,64,65,92,17,128,129,32100] :: [Int]
107+
]
108+
, testGroup "Natural" $
109+
let eqI = eq @Natural . Just
110+
in concat
111+
[ [ testCase (" 2^"++show k++"-1") $ eqI (2^k - 1) [pye| 2**k_hs - 1 |]
112+
, testCase (" 2^"++show k) $ eqI (2^k ) [pye| 2**k_hs |]
113+
, testCase (" 2^"++show k++"+1") $ eqI (2^k + 1) [pye| 2**k_hs + 1 |]
114+
]
115+
| k <- [63,64,65,92,17,128,129,32100] :: [Int]
116+
]
95117
]
96118

97119
failE :: forall a. (Eq a, Show a, FromPy a) => PyObject -> Py ()

0 commit comments

Comments
 (0)