From 4441ad76bb8e393acb52352fca9688ba4b76b885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Wed, 23 Oct 2024 22:43:33 +0300 Subject: [PATCH] Refactor easy (1) (#189) Refactored easy --- docs/source/api/easy.rst | 5 ++ docs/source/api/modules.rst | 1 + src/pyswip/__init__.py | 1 + src/pyswip/core.py | 5 ++ src/pyswip/easy.py | 168 ++++++++++++++++++++++++++++-------- src/pyswip/prolog.py | 76 +++++++++++++++- tests/test_examples.py | 2 - tests/test_foreign.py | 42 +++++++++ 8 files changed, 262 insertions(+), 38 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/__init__.py b/src/pyswip/__init__.py index 2f5c6e7..6153042 100644 --- a/src/pyswip/__init__.py +++ b/src/pyswip/__init__.py @@ -23,3 +23,4 @@ from pyswip.prolog import Prolog as Prolog from pyswip.easy import * +from pyswip.core import * diff --git a/src/pyswip/core.py b/src/pyswip/core.py index aed0550..ab70825 100644 --- a/src/pyswip/core.py +++ b/src/pyswip/core.py @@ -1132,6 +1132,11 @@ 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 04a23bd..6acbfe8 100644 --- a/src/pyswip/easy.py +++ b/src/pyswip/easy.py @@ -18,7 +18,86 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyswip.core import * +import inspect +from typing import Union, Callable, Optional + +from pyswip.core import ( + PL_new_atom, + PL_register_atom, + PL_atom_wchars, + PL_get_atom, + PL_unregister_atom, + PL_new_term_ref, + PL_compare, + PL_get_chars, + PL_copy_term_ref, + PL_unify_atom, + PL_unify_string_chars, + PL_unify_integer, + PL_unify_bool, + PL_unify_float, + PL_unify_list, + PL_unify_nil, + PL_term_type, + PL_put_term, + PL_new_functor, + PL_functor_name, + 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_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, + PL_ATOM, + PL_STRING, + PL_INTEGER, + PL_FLOAT, + PL_Q_NODEBUG, + PL_Q_CATCH_EXCEPTION, + PL_FA_NONDETERMINISTIC, + CVT_VARIABLE, + BUF_RING, + REP_UTF8, + CVT_ATOM, + CVT_STRING, + CFUNCTYPE, + cleaned, + cast, + c_size_t, + byref, + c_void_p, + atom_t, + create_string_buffer, + c_char_p, + functor_t, + c_int, + c_long, + c_double, + foreign_t, + term_t, + control_t, + module_t, +) integer_types = (int,) @@ -134,11 +213,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): @@ -170,7 +245,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: @@ -284,7 +359,9 @@ def fromTerm(cls, term): fromTerm = classmethod(fromTerm) - value = property(lambda s: s.__value) + @property + def value(self): + return self.__value def __call__(self, *args): assert self.arity == len(args) # FIXME: Put a decent error message @@ -346,12 +423,10 @@ def putTerm(term, value): value.put(term) elif isinstance(value, list): putList(term, value) - elif isinstance(value, Atom): - print("ATOM") elif isinstance(value, Functor): PL_put_functor(term, value.handle) else: - raise Exception("Not implemented") + raise Exception(f"Not implemented for type: {type(value)}") def putList(l, ls): # noqa: E741 @@ -498,8 +573,6 @@ def getVariable(t): def _callbackWrapper(arity=1, nondeterministic=False): - global arities - res = arities.get((arity, nondeterministic)) if res is None: if nondeterministic: @@ -514,8 +587,6 @@ def _callbackWrapper(arity=1, nondeterministic=False): def _foreignWrapper(fun, nondeterministic=False): - global funwraps - res = funwraps.get(fun) if res is None: @@ -535,32 +606,42 @@ 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 +): """ - global cwraps + Registers a Python callable as a Prolog predicate - if arity is None: - arity = func.arity + :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``. - if name is None: - name = func.__name__ + 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(name, arity, - # _callbackWrapper(arity)(_foreignWrapper(func)), flags) + return PL_register_foreign_in_module(None, name, arity, fwrap2, flags) newTermRef = PL_new_term_ref @@ -588,13 +669,30 @@ def call(*terms, **kwargs): return PL_call(t.handle, module) -def newModule(name): - """Create a new module. - ``name``: An Atom or a string +def newModule(name: Union[str, Atom]) -> module_t: + """ + 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 """ if isinstance(name, str): name = Atom(name) - return PL_new_module(name.handle) diff --git a/src/pyswip/prolog.py b/src/pyswip/prolog.py index 73a69a9..ee302a8 100644 --- a/src/pyswip/prolog.py +++ b/src/pyswip/prolog.py @@ -22,7 +22,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 +35,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 +53,10 @@ PL_cut_query, PL_thread_self, PL_thread_attach_engine, + PL_register_foreign_in_module, + foreign_t, + term_t, + control_t, ) @@ -111,6 +119,7 @@ class Prolog: # We keep track of open queries to avoid nested queries. _queryIsOpen = False + _cwraps = [] class _QueryWrapper(object): def __init__(self): @@ -359,6 +368,71 @@ 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_examples.py b/tests/test_examples.py index 0dd8059..28d1903 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -28,8 +28,6 @@ import pytest -from pyswip import * - examples = [ "create_term.py", "father.py", 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"