From 102498b03af361260d598e0cd452f844ac442b23 Mon Sep 17 00:00:00 2001 From: Yuce Tekol Date: Wed, 23 Oct 2024 22:13:58 +0300 Subject: [PATCH] Updates --- docs/source/api/easy.rst | 5 +++ docs/source/api/modules.rst | 1 + src/pyswip/core.py | 3 ++ src/pyswip/easy.py | 71 ++++++++++++++++++++++++------------- src/pyswip/prolog.py | 69 +++++++++++++++++++++++++++++++++-- tests/test_foreign.py | 42 ++++++++++++++++++++++ 6 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 docs/source/api/easy.rst diff --git a/docs/source/api/easy.rst b/docs/source/api/easy.rst new file mode 100644 index 0000000..a9d1747 --- /dev/null +++ b/docs/source/api/easy.rst @@ -0,0 +1,5 @@ +Easy +---- + +.. automodule:: pyswip.easy + :members: diff --git a/docs/source/api/modules.rst b/docs/source/api/modules.rst index 295b605..a0eb18a 100644 --- a/docs/source/api/modules.rst +++ b/docs/source/api/modules.rst @@ -5,6 +5,7 @@ API Documentation examples prolog + easy diff --git a/src/pyswip/core.py b/src/pyswip/core.py index aed0550..4496694 100644 --- a/src/pyswip/core.py +++ b/src/pyswip/core.py @@ -1132,6 +1132,9 @@ def PL_STRINGS_MARK(): PL_register_foreign = _lib.PL_register_foreign PL_register_foreign = check_strings(0, None)(PL_register_foreign) +PL_register_foreign_in_module = _lib.PL_register_foreign_in_module +PL_register_foreign_in_module = check_strings([0, 1], None)(PL_register_foreign_in_module) + PL_new_atom = _lib.PL_new_atom PL_new_atom.argtypes = [c_char_p] PL_new_atom.restype = atom_t diff --git a/src/pyswip/easy.py b/src/pyswip/easy.py index 422da8e..f09e626 100644 --- a/src/pyswip/easy.py +++ b/src/pyswip/easy.py @@ -17,7 +17,9 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Union + +import inspect +from typing import Union, Callable, Optional from pyswip.core import ( PL_new_atom, PL_register_atom, PL_atom_wchars, PL_get_atom, @@ -28,7 +30,7 @@ PL_functor_arity, PL_get_functor, PL_new_term_refs, PL_get_arg, PL_cons_functor_v, PL_put_atom_chars, PL_put_integer, PL_put_functor, PL_put_nil, PL_cons_list, PL_get_long, PL_get_float, - PL_is_list, PL_get_list, PL_register_foreign, PL_call, + PL_is_list, PL_get_list, PL_register_foreign_in_module, PL_call, PL_new_module, PL_pred, PL_open_query, PL_next_solution, PL_cut_query, PL_close_query, PL_VARIABLE, PL_STRINGS_MARK, PL_TERM, PL_DICT, @@ -156,11 +158,7 @@ def __hash__(self): return self.handle -def isstr(s): - return isinstance(s, str) - - -class Variable(object): +class Variable: __slots__ = "handle", "chars" def __init__(self, handle=None, name=None): @@ -195,7 +193,7 @@ def _fun(self, value, t): if type(value) == Atom: fun = PL_unify_atom value = value.handle - elif isstr(value): + elif isinstance(value, str): fun = PL_unify_string_chars value = value.encode() elif type(value) == int: @@ -523,8 +521,6 @@ def getVariable(t): def _callbackWrapper(arity=1, nondeterministic=False): - global arities - res = arities.get((arity, nondeterministic)) if res is None: if nondeterministic: @@ -557,28 +553,40 @@ def wrapper(*args): cwraps = [] -def registerForeign(func, name=None, arity=None, flags=0): - """Register a Python predicate - ``func``: Function to be registered. The function should return a value in - ``foreign_t``, ``True`` or ``False``. - ``name`` : Name of the function. If this value is not used, ``func.func_name`` - should exist. - ``arity``: Arity (number of arguments) of the function. If this value is not - used, ``func.arity`` should exist. +def registerForeign(func: Callable, name: str="", arity: Optional[int]=None, flags: int=0): """ - if arity is None: - arity = func.arity + Registers a Python callable as a Prolog predicate - if name is None: - name = func.__name__ + :param func: Callable to be registered. The callable should return a value in ``foreign_t``, ``True`` or ``False``. + :param name: Name of the callable. If the name is not specified, it is derived from ``func.__name__``. + :param arity: Number of parameters of the callable. If not specified, it is derived from the callable signature. + :param flags: Only supported flag is ``PL_FA_NONDETERMINISTIC``. + + See: `PL_register_foreign `_. + .. Note:: + This function is deprecated. + Use :py:meth:`Prolog.register_foreign` instead. + """ + if not callable(func): + raise ValueError("func is not callable") nondeterministic = bool(flags & PL_FA_NONDETERMINISTIC) + if arity is None: + # backward compatibility + if hasattr(func, "arity"): + arity = func.arity + else: + arity = len(inspect.signature(func).parameters) + if nondeterministic: + arity -= 1 + if not name: + name = func.__name__ cwrap = _callbackWrapper(arity, nondeterministic) fwrap = _foreignWrapper(func, nondeterministic) fwrap2 = cwrap(fwrap) cwraps.append(fwrap2) - return PL_register_foreign(name, arity, fwrap2, flags) + return PL_register_foreign_in_module(None, name, arity, fwrap2, flags) newTermRef = PL_new_term_ref @@ -608,7 +616,22 @@ def call(*terms, **kwargs): def newModule(name: Union[str, Atom]) -> module_t: """ - Create a new module + Returns a module with the given name. + + The module is created if it does not exist. + + .. NOTE:: + This function is deprecated. Use ``module`` instead. + + :param name: Name of the module + """ + return module(name) + +def module(name: Union[str, Atom]) -> module_t: + """ + Returns a module with the given name. + + The module is created if it does not exist. :param name: Name of the module """ diff --git a/src/pyswip/prolog.py b/src/pyswip/prolog.py index 73a69a9..c424a08 100644 --- a/src/pyswip/prolog.py +++ b/src/pyswip/prolog.py @@ -21,8 +21,9 @@ """ Provides the basic Prolog interface. """ - -from typing import Union, Generator +import functools +import inspect +from typing import Union, Generator, Callable, Optional from pathlib import Path from pyswip.utils import resolve_path @@ -33,6 +34,8 @@ PL_Q_NODEBUG, PL_Q_CATCH_EXCEPTION, PL_Q_NORMAL, + PL_FA_NONDETERMINISTIC, + CFUNCTYPE, PL_initialise, PL_open_foreign_frame, PL_new_term_ref, @@ -49,6 +52,10 @@ PL_cut_query, PL_thread_self, PL_thread_attach_engine, + PL_register_foreign_in_module, + foreign_t, + term_t, + control_t, ) @@ -64,7 +71,6 @@ class NestedQueryError(PrologError): SWI-Prolog does not accept nested queries, that is, opening a query while the previous one was not closed. As this error may be somewhat difficult to debug in foreign code, it is automatically treated inside PySwip """ - pass @@ -111,6 +117,7 @@ class Prolog: # We keep track of open queries to avoid nested queries. _queryIsOpen = False + _cwraps = [] class _QueryWrapper(object): def __init__(self): @@ -359,6 +366,62 @@ def query( """ return cls._QueryWrapper()(query, maxresult, catcherrors, normalize) + @classmethod + @functools.cache + def _callback_wrapper(cls, arity, nondeterministic): + ps = [foreign_t] + [term_t] * arity + if nondeterministic: + return CFUNCTYPE(*(ps + [control_t])) + return CFUNCTYPE(*ps) + + @classmethod + @functools.cache + def _foreign_wrapper(cls, fun, nondeterministic=False): + def wrapper(*args): + if nondeterministic: + args = [getTerm(arg) for arg in args[:-1]] + [args[-1]] + else: + args = [getTerm(arg) for arg in args] + r = fun(*args) + return True if r is None else r + return wrapper + + @classmethod + def register_foreign(cls, func: Callable, /, name: str = "", arity: Optional[int] = None, *, module: str = "", + nondeterministic: bool=False): + """ + Registers a Python callable as a Prolog predicate + + :param func: + Callable to be registered. The callable should return a value in ``foreign_t``, ``True`` or ``False`` or ``None``. + Returning ``None`` is equivalent to returning ``True``. + :param name: + Name of the callable. If the name is not specified, it is derived from ``func.__name__``. + :param arity: + Number of parameters of the callable. If not specified, it is derived from the callable signature. + :param module: + Name of the module to register the predicate. By default, the current module. + :param nondeterministic: + Set the foreign callable as nondeterministic + """ + if not callable(func): + raise ValueError("func is not callable") + module = module or None + flags = PL_FA_NONDETERMINISTIC if nondeterministic else 0 + if arity is None: + arity = len(inspect.signature(func).parameters) + if nondeterministic: + arity -= 1 + if not name: + name = func.__name__ + + cwrap = cls._callback_wrapper(arity, nondeterministic) + # TODO: check func + fwrap = cls._foreign_wrapper(func, nondeterministic) + fwrap = cwrap(fwrap) + cls._cwraps.append(fwrap) + return PL_register_foreign_in_module(module, name, arity, fwrap, flags) + def normalize_values(values): from pyswip.easy import Atom, Functor diff --git a/tests/test_foreign.py b/tests/test_foreign.py index d9a7249..ced4320 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -32,6 +32,21 @@ def hello(t): {"X": name} in result, "Expected result X:{} not present".format(name) ) + def test_deterministic_foreign_automatic_arity(self): + def hello(t): + print("Hello,", t) + + Prolog.register_foreign(hello, module="autoarity") + + Prolog.assertz("autoarity:mother(emily,john)") + Prolog.assertz("autoarity:mother(emily,gina)") + result = list(Prolog.query("autoarity:mother(emily,X), autoarity:hello(X)")) + self.assertEqual(len(result), 2, "Query should return two results") + for name in ("john", "gina"): + self.assertTrue( + {"X": name} in result, "Expected result X:{} not present".format(name) + ) + def test_nondeterministic_foreign(self): def nondet(a, context): control = PL_foreign_control(context) @@ -60,6 +75,33 @@ def nondet(a, context): {"X": i} in result, "Expected result X:{} not present".format(i) ) + def test_nondeterministic_foreign_autoarity(self): + def nondet(a, context): + control = PL_foreign_control(context) + context = PL_foreign_context(context) + if control == PL_FIRST_CALL: + context = 0 + a.unify(int(context)) + context += 1 + return PL_retry(context) + elif control == PL_REDO: + a.unify(int(context)) + if context == 10: + return False + context += 1 + return PL_retry(context) + elif control == PL_PRUNED: + pass + + Prolog.register_foreign(nondet, module="autoarity", nondeterministic=True) + result = list(Prolog.query("autoarity:nondet(X)")) + + self.assertEqual(len(result), 10, "Query should return 10 results") + for i in range(10): + self.assertTrue( + {"X": i} in result, "Expected result X:{} not present".format(i) + ) + def test_atoms_and_strings_distinction(self): test_string = "string"