Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
yuce committed Oct 23, 2024
1 parent 94389d2 commit 102498b
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 27 deletions.
5 changes: 5 additions & 0 deletions docs/source/api/easy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Easy
----

.. automodule:: pyswip.easy
:members:
1 change: 1 addition & 0 deletions docs/source/api/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ API Documentation

examples
prolog
easy



3 changes: 3 additions & 0 deletions src/pyswip/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 47 additions & 24 deletions src/pyswip/easy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 <https://www.swi-prolog.org/pldoc/man?CAPI=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
Expand Down Expand Up @@ -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
"""
Expand Down
69 changes: 66 additions & 3 deletions src/pyswip/prolog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
)


Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions tests/test_foreign.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"

Expand Down

0 comments on commit 102498b

Please sign in to comment.