1"""
2Convert Python functions to PARI closures
3*****************************************
4
5AUTHORS:
6
7- Jeroen Demeyer (2015-04-10): initial version, :trac:`18052`.
8
9Examples:
10
11>>> def the_answer():
12...     return 42
13>>> import cypari2
14>>> pari = cypari2.Pari()
15>>> f = pari(the_answer)
16>>> f()
1742
18
19>>> cube = pari(lambda i: i**3)
20>>> cube.apply(range(10))
21[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
22"""
23
24#*****************************************************************************
25#       Copyright (C) 2015 Jeroen Demeyer <jdemeyer@cage.ugent.be>
26#
27# This program is free software: you can redistribute it and/or modify
28# it under the terms of the GNU General Public License as published by
29# the Free Software Foundation, either version 2 of the License, or
30# (at your option) any later version.
31#                  http://www.gnu.org/licenses/
32#*****************************************************************************
33
34from __future__ import absolute_import, division, print_function
35
36from cysignals.signals cimport sig_on, sig_off, sig_block, sig_unblock, sig_error
37
38from cpython.tuple cimport *
39from cpython.object cimport PyObject_Call
40from cpython.ref cimport Py_INCREF
41
42from .paridecl cimport *
43from .stack cimport new_gen, new_gen_noclear, clone_gen_noclear, DetachGen
44from .gen cimport objtogen
45
46try:
47    from inspect import getfullargspec as getargspec
48except ImportError:
49    from inspect import getargspec
50
51
52cdef inline GEN call_python_func_impl "call_python_func"(GEN* args, object py_func) except NULL:
53    """
54    Call ``py_func(*args)`` where ``py_func`` is a Python function
55    and ``args`` is an array of ``GEN``s terminated by ``NULL``.
56
57    The arguments are converted from ``GEN`` to a cypari ``gen`` before
58    calling ``py_func``. The result is converted back to a PARI ``GEN``.
59    """
60    # We need to ensure that nothing above avma is touched
61    avmaguard = new_gen_noclear(<GEN>avma)
62
63    # How many arguments are there?
64    cdef Py_ssize_t n = 0
65    while args[n] is not NULL:
66        n += 1
67
68    # Construct a Python tuple for args
69    cdef tuple t = PyTuple_New(n)
70    cdef Py_ssize_t i
71    for i in range(n):
72        a = clone_gen_noclear(args[i])
73        Py_INCREF(a)  # Need to increase refcount because the tuple steals it
74        PyTuple_SET_ITEM(t, i, a)
75
76    # Call the Python function
77    r = PyObject_Call(py_func, t, <dict>NULL)
78
79    # Convert the result to a GEN and copy it to the PARI stack
80    # (with a special case for None)
81    if r is None:
82        return gnil
83
84    # Safely delete r and avmaguard
85    d = DetachGen(objtogen(r))
86    del r
87    res = d.detach()
88    d = DetachGen(avmaguard)
89    del avmaguard
90    d.detach()
91
92    return res
93
94
95# We rename this function to be able to call it with a different
96# signature. In particular, we want manual exception handling and we
97# implicitly convert py_func from a PyObject* to an object.
98cdef extern from *:
99    GEN call_python_func(GEN* args, PyObject* py_func)
100
101
102cdef GEN call_python(GEN arg1, GEN arg2, GEN arg3, GEN arg4, GEN arg5, ulong nargs, ulong py_func):
103    """
104    This function, which will be installed in PARI, is a front-end for
105    ``call_python_func_impl``.
106
107    It has 5 optional ``GEN``s as argument, a ``nargs`` argument
108    specifying how many arguments are valid and one ``ulong``, which is
109    actually a Python callable object cast to ``ulong``.
110    """
111    if nargs > 5:
112        sig_error()
113
114    # Convert arguments to a NULL-terminated array.
115    cdef GEN args[6]
116    args[0] = arg1
117    args[1] = arg2
118    args[2] = arg3
119    args[3] = arg4
120    args[4] = arg5
121    args[nargs] = NULL
122
123    sig_block()
124    # Disallow interrupts during the Python code inside
125    # call_python_func_impl(). We need to do this because this function
126    # is very likely called within sig_on() and interrupting arbitrary
127    # Python code is bad.
128    cdef GEN r = call_python_func(args, <PyObject*>py_func)
129    sig_unblock()
130    if not r:  # An exception was raised
131        sig_error()
132    return r
133
134
135# Install the function "call_python" for use in the PARI library.
136cdef entree* ep_call_python
137
138cdef int _pari_init_closure() except -1:
139    sig_on()
140    global ep_call_python
141    ep_call_python = install(<void*>call_python, "call_python", 'DGDGDGDGDGD5,U,U')
142    sig_off()
143
144
145cpdef Gen objtoclosure(f):
146    """
147    Convert a Python function (more generally, any callable) to a PARI
148    ``t_CLOSURE``.
149
150    .. NOTE::
151
152        With the current implementation, the function can be called
153        with at most 5 arguments.
154
155    .. WARNING::
156
157        The function ``f`` which is called through the closure cannot
158        be interrupted. Therefore, it is advised to use this only for
159        simple functions which do not take a long time.
160
161    Examples:
162
163    >>> from cypari2.closure import objtoclosure
164    >>> def pymul(i,j): return i*j
165    >>> mul = objtoclosure(pymul)
166    >>> mul
167    (v1,v2)->call_python(v1,v2,0,0,0,2,...)
168    >>> mul(6,9)
169    54
170    >>> mul.type()
171    't_CLOSURE'
172    >>> mul.arity()
173    2
174    >>> def printme(x):
175    ...     print(x)
176    >>> objtoclosure(printme)('matid(2)')
177    [1, 0; 0, 1]
178
179    Construct the Riemann zeta function using a closure:
180
181    >>> from cypari2 import Pari; pari = Pari()
182    >>> def coeffs(n):
183    ...     return [1 for i in range(n)]
184    >>> Z = pari.lfuncreate([coeffs, 0, [0], 1, 1, 1, 1])
185    >>> Z.lfun(2)
186    1.64493406684823
187
188    A trivial closure:
189
190    >>> f = pari(lambda x: x)
191    >>> f(10)
192    10
193
194    Test various kinds of errors:
195
196    >>> mul(4)
197    Traceback (most recent call last):
198    ...
199    TypeError: pymul() ...
200    >>> mul(None, None)
201    Traceback (most recent call last):
202    ...
203    ValueError: Cannot convert None to pari
204    >>> mul(*range(100))
205    Traceback (most recent call last):
206    ...
207    PariError: call_python: too many parameters in user-defined function call
208    >>> mul([1], [2])
209    Traceback (most recent call last):
210    ...
211    PariError: call_python: forbidden multiplication t_VEC (1 elts) * t_VEC (1 elts)
212    """
213    if not callable(f):
214        raise TypeError("argument to objtoclosure() must be callable")
215
216    # Determine number of arguments of f
217    cdef Py_ssize_t i, nargs
218    try:
219        argspec = getargspec(f)
220    except Exception:
221        nargs = 5
222    else:
223        nargs = len(argspec.args)
224
225    # Only 5 arguments are supported for now...
226    if nargs > 5:
227        nargs = 5
228
229    # Fill in default arguments of PARI function
230    sig_on()
231    cdef GEN args = cgetg((5 - nargs) + 2 + 1, t_VEC)
232    for i in range(5 - nargs):
233        set_gel(args, i + 1, gnil)
234    set_gel(args, (5 - nargs) + 1, stoi(nargs))
235    # Convert f to a t_INT containing the address of f
236    set_gel(args, (5 - nargs) + 1 + 1, utoi(<ulong><PyObject*>f))
237
238    # Create a t_CLOSURE which calls call_python() with py_func equal to f
239    cdef Gen res = new_gen(snm_closure(ep_call_python, args))
240
241    # We need to keep a reference to f somewhere and there is no way to
242    # have PARI handle this reference for us. So the only way out is to
243    # force f to be never deallocated
244    Py_INCREF(f)
245
246    return res
247