├── .github
└── workflows
│ └── tests.yaml
├── .gitignore
├── .readthedocs.yaml
├── CHANGELOG.md
├── CONTRIBUTORS.txt
├── INSTALL.md
├── LICENSE
├── Makefile
├── README.md
├── dev-requirements.txt
├── docs
├── Makefile
├── make.bat
└── source
│ ├── api
│ ├── easy.rst
│ ├── examples.rst
│ ├── modules.rst
│ └── prolog.rst
│ ├── conf.py
│ ├── get_started.rst
│ ├── index.rst
│ └── value_exchange.rst
├── examples
├── README.md
├── coins
│ ├── coins.pl
│ └── coins_new.py
├── create_term.py
├── draughts
│ ├── puzzle1.pl
│ └── puzzle1.py
├── father.py
├── knowledgebase.py
├── register_foreign.py
├── register_foreign_simple.py
└── sendmoremoney
│ ├── money.pl
│ ├── money.py
│ └── money_new.py
├── pyproject.toml
├── src
└── pyswip
│ ├── __init__.py
│ ├── core.py
│ ├── easy.py
│ ├── examples
│ ├── __init__.py
│ ├── coins.pl
│ ├── coins.py
│ ├── hanoi.pl
│ ├── hanoi.py
│ ├── sudoku.pl
│ └── sudoku.py
│ ├── prolog.py
│ ├── py.typed
│ └── utils.py
└── tests
├── __init__.py
├── examples
├── __init__.py
├── hanoi_fixture.txt
├── hanoi_simple_fixture.txt
├── sudoku.txt
├── test_coins.py
├── test_hanoi.py
├── test_sudoku.py
└── utils.py
├── test_examples.py
├── test_foreign.py
├── test_functor_return.pl
├── test_issues.py
├── test_prolog.py
├── test_read.pl
├── test_unicode.pl
└── test_utils.py
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: "Run tests"
2 |
3 | on:
4 | push:
5 | branches:
6 | - "master"
7 |
8 | pull_request_target:
9 | branches:
10 | - "master"
11 |
12 | jobs:
13 |
14 | run-tests:
15 | name: "Run tests with Python ${{ matrix.python-version }} on ${{ matrix.os }}"
16 | strategy:
17 | matrix:
18 | python-version: [ '3.9', '3.13' ]
19 | os: [ "ubuntu-22.04", "ubuntu-24.04" ]
20 | fail-fast: false
21 |
22 | runs-on: "${{ matrix.os }}"
23 |
24 | steps:
25 |
26 | - name: "Checkout PR"
27 | uses: "actions/checkout@v4"
28 | if: github.event_name == 'pull_request_target'
29 | with:
30 | ref: refs/pull/${{ github.event.pull_request.number }}/merge
31 |
32 | - name: "Checkout Branch"
33 | uses: "actions/checkout@v4"
34 | if: github.event_name == 'push'
35 |
36 | - name: "Set up Python ${{ matrix.python-version }}"
37 | uses: "actions/setup-python@v5"
38 | with:
39 | python-version: "${{ matrix.python-version }}"
40 |
41 | - name: "Install test dependencies"
42 | run: |
43 | pip install -r dev-requirements.txt coveralls
44 |
45 | - name: "Check style"
46 | run: |
47 | make check
48 |
49 | - name: "Install SWI-Prolog"
50 | run: |
51 | sudo apt-get install -y swi-prolog-nox
52 |
53 | - name: "Run tests"
54 | env:
55 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
56 | run: |
57 | make check
58 | make coverage
59 | make upload-coverage
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.swp
3 | *.pyc
4 | .coverage
5 | *.BAC
6 | .pytest_cache
7 | dist/
8 | pyswip.egg-info/
9 | build/
10 | /.idea/
11 | /.mypy_cache/
12 | .venv
13 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Set the OS, Python version and other tools you might need
9 | build:
10 | os: ubuntu-24.04
11 | tools:
12 | python: "3.12"
13 | apt_packages:
14 | - swi-prolog-nox
15 |
16 | # Build documentation in the "docs/" directory with Sphinx
17 | sphinx:
18 | configuration: docs/source/conf.py
19 |
20 | # Optionally build your docs in additional formats such as PDF and ePub
21 | # formats:
22 | # - pdf
23 | # - epub
24 |
25 | python:
26 | install:
27 | - requirements: dev-requirements.txt
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | This content has moved to [PySwip Change Log](https://pyswip.org/change-log.html)
--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
1 | Yüce Tekol
2 | Rodrigo Starr
3 | Markus Triska
4 | Sebastian Höhn
5 | Manuel Rotter
6 | dia.aljrab
7 | dylan-google@dylex.net
8 | jpthompson23
9 | Swen Wenzel
10 | Ilya Perevoznik
11 | Paul Brown
12 | Antonio de Luna
13 | Giri Gaurav Bhatnagar
14 | Ian Douglas Scott
15 | Michael Kasch
16 | Robert Simione
17 | dodgyville
18 | Till Hofmann
19 | Robert Simione
20 | rmanhaeve
21 | Galileo Sartor
22 | Stuart Reynolds
23 | Prologrules
24 | Dylan Lukes
25 | Guglielmo Gemignani
26 | Vince Jankovics
27 | Tobias Grubenmann
28 | Arvid Norlander
29 | David Cox
30 | Maximilian Peltzer
31 | Adi Harif
32 | Oisín Mac Fhearaí
33 | Lorenzo Matera
34 |
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
1 | # Installing PySwip
2 |
3 | This content has moved to [PySwip Get Started](https://pyswip.readthedocs.io/en/latest/get_started.html)
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2007-2024 Yüce Tekol and PySwip contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build clean coverage upload-coverage test upload
2 |
3 | build:
4 | pyproject-build
5 |
6 | clean:
7 | rm -rf dist build pyswip.egg-info src/pyswip.egg-info
8 |
9 | coverage:
10 | PYTHONPATH=src py.test tests --verbose --cov=pyswip
11 |
12 | upload-coverage: coverage
13 | coveralls
14 |
15 | test:
16 | PYTHONPATH=src py.test tests --verbose -m "not slow"
17 |
18 | upload:
19 | twine upload dist/*
20 |
21 | check:
22 | ruff format --check
23 | ruff check
24 |
25 | reformat:
26 | ruff format
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # PySwip
8 |
9 |
10 |

11 |
12 |
13 | ## What's New?
14 |
15 | See the [Change Log](https://pyswip.org/change-log.html).
16 |
17 | ## Install
18 |
19 | If you have SWI-Prolog installed, it's just:
20 | ```
21 | pip install -U pyswip
22 | ```
23 |
24 | See [Get Started](https://pyswip.readthedocs.io/en/latest/get_started.html) for detailed instructions.
25 |
26 | ## Introduction
27 |
28 | PySwip is a Python-Prolog interface that enables querying [SWI-Prolog](https://www.swi-prolog.org) in your Python programs.
29 | It features an SWI-Prolog foreign language interface, a utility class that makes it easy querying with Prolog and also a Pythonic interface.
30 |
31 | Since PySwip uses SWI-Prolog as a shared library and ctypes to access it, it doesn't require compilation to be installed.
32 |
33 | PySwip was brought to you by the PySwip community.
34 | Thanks to all [contributors](CONTRIBUTORS.txt).
35 |
36 | ## Documentation
37 |
38 | * [PySwip Home](https://pyswip.org)
39 | * [PySwip Documentation](https://pyswip.readthedocs.io/en/latest/)
40 |
41 | ## Examples
42 |
43 | ### Using Prolog
44 |
45 | ```python
46 | from pyswip import Prolog
47 | Prolog.assertz("father(michael,john)")
48 | Prolog.assertz("father(michael,gina)")
49 | list(Prolog.query("father(michael,X)")) == [{'X': 'john'}, {'X': 'gina'}]
50 | for soln in Prolog.query("father(X,Y)"):
51 | print(soln["X"], "is the father of", soln["Y"])
52 | # michael is the father of john
53 | # michael is the father of gina
54 | ```
55 |
56 | An existing knowledge base stored in a Prolog file can also be consulted, and queried.
57 | Assuming the filename "knowledge_base.pl" and the Python is being run in the same working directory, it is consulted like so:
58 |
59 | ```python
60 | from pyswip import Prolog
61 | Prolog.consult("knowledge_base.pl")
62 | ```
63 |
64 | ### Foreign Functions
65 |
66 | ```python
67 | from pyswip import Prolog, registerForeign
68 |
69 | def hello(t):
70 | print("Hello,", t)
71 | hello.arity = 1
72 |
73 | registerForeign(hello)
74 |
75 | Prolog.assertz("father(michael,john)")
76 | Prolog.assertz("father(michael,gina)")
77 | print(list(Prolog.query("father(michael,X), hello(X)")))
78 | ```
79 |
80 | ### Pythonic interface (Experimental)
81 |
82 | ```python
83 | from pyswip import Functor, Variable, Query, call
84 |
85 | assertz = Functor("assertz", 1)
86 | father = Functor("father", 2)
87 | call(assertz(father("michael","john")))
88 | call(assertz(father("michael","gina")))
89 | X = Variable()
90 |
91 | q = Query(father("michael",X))
92 | while q.nextSolution():
93 | print("Hello,", X.value)
94 | q.closeQuery()
95 |
96 | # Outputs:
97 | # Hello, john
98 | # Hello, gina
99 | ```
100 |
101 | The core functionality of `Prolog.query` is based on Nathan Denny's public domain prolog.py.
102 |
103 | ## Help!
104 |
105 | * [Support Forum](https://groups.google.com/forum/#!forum/pyswip)
106 | * [Stack Overflow](https://stackoverflow.com/search?q=pyswip)
107 |
108 | ## PySwip Community Home
109 |
110 | PySwip was used in scientific articles, dissertations, and student projects over the years.
111 | Head out to [PySwip Community](https://pyswip.org/community.html) for more information and community links.
112 |
113 | **Do you have a project, video or publication that uses/mentions PySwip?**
114 | **[file an issue](https://github.com/yuce/pyswip/issues/new?title=Powered%20by%20PySwip) or send a pull request.**
115 |
116 | If you would like to reference PySwip in a LaTeX document, you can use the provided [BibTeX file](https://pyswip.org/pyswip.bibtex).
117 | You can also use the following information to refer to PySwip:
118 | * Author: Yüce Tekol and PySwip contributors
119 | * Title: PySwip VERSION
120 | * URL: https://pyswip.org
121 |
122 | ## License
123 |
124 | PySwip is licensed under the [MIT license](LICENSE).
125 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | ruff==0.6.2
2 | build
3 | pytest-cov
4 | mypy>=1.0.0
5 | Sphinx
6 | sphinx-autodoc-typehints
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/source/api/easy.rst:
--------------------------------------------------------------------------------
1 | Easy
2 | ----
3 |
4 | .. automodule:: pyswip.easy
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/source/api/examples.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | --------
3 |
4 | .. automodule:: pyswip.examples
5 | :members:
6 |
7 | Sudoku
8 | ^^^^^^
9 |
10 | .. automodule:: pyswip.examples.sudoku
11 | :members:
12 |
13 | Hanoi
14 | ^^^^^
15 |
16 | .. automodule:: pyswip.examples.hanoi
17 | :members:
18 |
19 |
20 | Coins
21 | ^^^^^
22 |
23 | .. automodule:: pyswip.examples.coins
24 | :members:
25 |
26 |
--------------------------------------------------------------------------------
/docs/source/api/modules.rst:
--------------------------------------------------------------------------------
1 | API Documentation
2 | -----------------
3 |
4 | .. toctree::
5 |
6 | examples
7 | prolog
8 | easy
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/source/api/prolog.rst:
--------------------------------------------------------------------------------
1 | Prolog
2 | ------
3 |
4 | .. automodule:: pyswip.prolog
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | import sys
10 | from pathlib import Path
11 |
12 | sys.path.insert(0, str(Path("..", "..", "src").resolve()))
13 |
14 | from pyswip import __VERSION__
15 |
16 | project = "PySwip"
17 | copyright = "2024, Yüce Tekol and PySwip Contributors"
18 | author = "Yüce Tekol and PySwip Contributors"
19 | version = __VERSION__
20 | release = version
21 |
22 | # -- General configuration ---------------------------------------------------
23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
24 |
25 | extensions = [
26 | "sphinx.ext.duration",
27 | "sphinx.ext.doctest",
28 | "sphinx.ext.autodoc",
29 | "sphinx_autodoc_typehints",
30 | ]
31 |
32 | templates_path = ["_templates"]
33 | exclude_patterns = []
34 |
35 |
36 | # -- Options for HTML output -------------------------------------------------
37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
38 |
39 | html_theme = "alabaster"
40 | html_static_path = ["_static"]
41 |
42 | source_suffix = {
43 | ".rst": "restructuredtext",
44 | ".txt": "markdown",
45 | ".md": "markdown",
46 | }
47 |
48 | autodoc_member_order = "bysource"
49 | autoclass_content = "both"
50 |
51 | html_logo = "https://pyswip.org/images/pyswip_logo_sm_256colors.gif"
52 |
--------------------------------------------------------------------------------
/docs/source/get_started.rst:
--------------------------------------------------------------------------------
1 | Get Started
2 | ===========
3 |
4 | Requirements
5 | ------------
6 |
7 | * Python 3.9 or later
8 | * SWI-Prolog 8.4.2 or later
9 | * 64bit Intel or ARM processor
10 |
11 | .. IMPORTANT::
12 | Make sure the SWI-Prolog architecture is the same as the Python architecture.
13 | If you are using a 64bit build of Python, use a 64bit build of SWI-Prolog, etc.
14 |
15 |
16 | Installing PySwip
17 | -----------------
18 |
19 | .. _install_from_pypi:
20 |
21 | PyPI
22 | ^^^^
23 |
24 | PySwip is available to install from `Python Package Index `_.
25 |
26 | .. TIP::
27 | We recommend installing PySwip into a Python virtual environment.
28 | See: `Creation of virtual environments `_
29 |
30 | You can install PySwip using::
31 |
32 | pip install -U pyswip
33 |
34 | You will need to have SWI-Prolog installed on your system.
35 | See :ref:`install_swi_prolog`.
36 |
37 | PySwip requires the location of the ``libswpl`` shared library and also the SWI-Prolog home directory.
38 | In many cases, PySwip can find the shared library and the home directory automatically.
39 | Otherwise, you can use the following environment variables:
40 |
41 | * ``SWI_HOME_DIR``: The SWI-Prolog home directory. It must contain the ``swipl.home`` file.
42 | It's the ``$SWI_PROLOG_ROOT/lib/swipl`` directory if you have compiled SWI-Prolog form source.
43 | * ``LIBSWIPL_PATH``: The location of the ``libswipl`` shared library.
44 |
45 | You can get the locations mentioned above using the following commands::
46 |
47 | swipl --dump-runtime-variables
48 |
49 | That will output something like::
50 |
51 | PLBASE="/home/yuce/swipl-9.3.8/lib/swipl";
52 | ...
53 | PLLIBDIR="/home/yuce/swipl-9.3.8/lib/swipl/lib/x86_64-linux";
54 |
55 | Use the value in the ``PLBASE`` variable as the value for the ``SWI_HOME_DIR`` environment variable.
56 | Use the value in the ``PLLIBDIR`` variable as the value for the ``LIBSWIPL_PATH`` environment variable.
57 |
58 | Arch Linux / Manjaro Linux / Parabola GNU/Linux-libre
59 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 |
61 | These Linux distributions have PySwip in their package repositories.
62 | You can use the following to install PySwip globally::
63 |
64 | pacman -S python-pyswip
65 |
66 | .. NOTE::
67 | We recommend installing PySwip from :ref:`install_from_pypi`.
68 |
69 | Fedora Workstation
70 | ^^^^^^^^^^^^^^^^^^
71 |
72 | You can use the following to install PySwip globally::
73 |
74 | dnf install python3-pyswip
75 |
76 | .. NOTE::
77 | We recommend installing PySwip from :ref:`install_from_pypi`.
78 |
79 | .. _install_swi_prolog:
80 |
81 | Installing SWI-Prolog
82 | ---------------------
83 |
84 | Some operating systems have packages for SWI-Prolog.
85 | Otherwise, you can download it from `SWI-Prolog's website `_ or build from source.
86 |
87 | Arch Linux / Manjaro Linux / Parabola GNU/Linux-libre
88 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89 |
90 | SWI-Prolog is available in the standard package repository::
91 |
92 | pacman -S swi-prolog
93 |
94 | Fedora Workstation
95 | ^^^^^^^^^^^^^^^^^^
96 |
97 | Installing SWI-Prolog::
98 |
99 | dnf install pl
100 |
101 | Debian, Ubuntu, Raspbian
102 | ^^^^^^^^^^^^^^^^^^^^^^^^
103 |
104 | * Ubuntu 22.04 has SWI-Prolog 8.4.3 in its repository.
105 | * Debian Bookworm, Ubuntu 24.04 and Raspberry Pi OS Bookworm have SWI-Prolog 9.0.4 in their repositories.
106 |
107 | Use the following to install SWI-Prolog::
108 |
109 | apt install swi-prolog-nox
110 |
111 |
112 | Windows
113 | -------
114 |
115 | Download a recent version of SWI-Prolog from https://www.swi-prolog.org/Download.html and install it.
116 |
117 | MacOS
118 | -----
119 |
120 | The preferred way of installing SWI-Prolog on MacOS is using `Homebrew `_.
121 |
122 | Homebrew
123 | ^^^^^^^^
124 |
125 | Installing SWI-Prolog::
126 |
127 | brew install swi-prolog
128 |
129 |
130 | Official SWI-Prolog App
131 | ^^^^^^^^^^^^^^^^^^^^^^^
132 |
133 | Install SWI-Prolog from https://www.swi-prolog.org/Download.html.
134 |
135 | If you get an error like ``libgmp.X not found``, you have to set the ``DYLD_FALLBACK_LIBRARY_PATH`` environment variable before running Python::
136 |
137 | export DYLD_FALLBACK_LIBRARY_PATH=/Applications/SWI-Prolog.app/Contents/Frameworks
138 |
139 | OpenBSD
140 | -------
141 |
142 | Install SWI-Prolog using the following on OpenBSD 7.6 and later::
143 |
144 | pkg_add swi-prolog
145 |
146 | FreeBSD
147 | -------
148 |
149 | SWI-Prolog can be installed using ``pkg``::
150 |
151 | pkg install swi-pl
152 |
153 | Test Drive
154 | ----------
155 |
156 | Run a quick test by running following code at your Python console:
157 |
158 | .. code-block:: python
159 |
160 | from pyswip import Prolog
161 | Prolog.assertz("father(michael,john)")
162 | print(list(Prolog.query("father(X,Y)")))
163 |
164 |
165 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. PySwip documentation master file, created by
2 | sphinx-quickstart on Sun Oct 13 13:18:34 2024.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | PySwip Documentation
7 | ====================
8 |
9 | PySwip is a Python-Prolog interface that enables querying `SWI-Prolog `_ in your Python programs.
10 | It features an SWI-Prolog foreign language interface, a utility class that makes it easy querying with Prolog and also a Pythonic interface.
11 |
12 | .. toctree::
13 | :maxdepth: 2
14 | :caption: Contents:
15 |
16 | get_started
17 | value_exchange
18 | api/modules
19 |
20 | Indices and Tables
21 | ==================
22 |
23 | .. toctree::
24 |
25 | genindex
26 | modindex
27 |
28 |
--------------------------------------------------------------------------------
/docs/source/value_exchange.rst:
--------------------------------------------------------------------------------
1 | Value Exchange Between Python and Prolog
2 | ========================================
3 |
4 | String Interpolation from Python to Prolog
5 | ------------------------------------------
6 |
7 | Currently there's limited support for converting Python values automatically to Prolog via a string interpolation mechanism.
8 | This mechanism is available to be used with the following ``Prolog`` class methods:
9 |
10 | * ``assertz``
11 | * ``asserta``
12 | * ``retract``
13 | * ``query``
14 |
15 | These methods take one string format argument, and zero or more arguments to replace placeholders with in the format to produce the final string.
16 | Placeholder is ``%p`` for all types.
17 |
18 | The following types are recognized:
19 |
20 | * String
21 | * Integer
22 | * Float
23 | * Boolean
24 | * ``pyswip.Atom``
25 | * ``pyswip.Variable``
26 | * Lists of the types above
27 |
28 | Other types are converted to strings using the ``str`` function.
29 |
30 | .. list-table:: String Interpolation to Prolog
31 | :widths: 50 50
32 | :header-rows: 1
33 |
34 | * - Python Value
35 | - String
36 | * - str ``"Some value"``
37 | - ``"Some value"``
38 | * - int ``38``
39 | - ``38``
40 | * - float ``38.42``
41 | - ``38.42``
42 | * - bool ``True``
43 | - ``1``
44 | * - bool ``False``
45 | - ``0``
46 | * - ``pyswip.Atom("carrot")``
47 | - ``'carrot'``
48 | * - ``pyswip.Variable("Width")``
49 | - ``Width``
50 | * - list ``["string", 12, 12.34, Atom("jill")]``
51 | - ``["string", 12, 12.34, 'jill']``
52 | * - Other ``value``
53 | - ``str(value)``
54 |
55 |
56 | The placeholders are set using ``%p``.
57 |
58 | Example:
59 |
60 | .. code-block:: python
61 |
62 | ids = [1, 2, 3]
63 | joe = Atom("joe")
64 | Prolog.assertz("user(%p,%p)", joe, ids)
65 | list(Prolog.query("user(%p,IDs)", joe))
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # PySwip Examples
2 |
3 | This directory contains examples for PySwip.
4 |
5 | The ones marked with (clp) requires `clp` library of SWI-Prolog.
6 |
7 | * (clp) `coins/` : Moved to `pyswip.examples.coins` package
8 | * (clp) `draughts/`
9 | * `hanoi/` : Moved to `pyswip.examples.sudoku` package
10 | * (clp) `sendmoremoney/` : If, SEND * MORE = MONEY, what is S, E, N, D, M, O, R, Y?
11 | * (clp) `sudoku/` : Moved to `pyswip.examples.sudoku` package
12 | * `create_term.py` : Shows creating a Prolog term
13 | * `register_foreign.py` : Shows registering a foreign function
14 |
--------------------------------------------------------------------------------
/examples/coins/coins.pl:
--------------------------------------------------------------------------------
1 |
2 | % Coins -- 2007 by Yuce Tekol
3 |
4 | :- use_module(library('bounds')).
5 |
6 | coins(Count, Total, Solution) :-
7 | % A=1, B=5, C=10, D=50, E=100
8 | Solution = [A, B, C, D, E],
9 |
10 | Av is 1,
11 | Bv is 5,
12 | Cv is 10,
13 | Dv is 50,
14 | Ev is 100,
15 |
16 | Aup is Total // Av,
17 | Bup is Total // Bv,
18 | Cup is Total // Cv,
19 | Dup is Total // Dv,
20 | Eup is Total // Ev,
21 |
22 | A in 0..Aup,
23 | B in 0..Bup,
24 | C in 0..Cup,
25 | D in 0..Dup,
26 | E in 0..Eup,
27 |
28 | VA #= A*Av,
29 | VB #= B*Bv,
30 | VC #= C*Cv,
31 | VD #= D*Dv,
32 | VE #= E*Ev,
33 |
34 | sum(Solution, #=, Count),
35 | VA + VB + VC + VD + VE #= Total,
36 |
37 | label(Solution).
38 |
39 |
--------------------------------------------------------------------------------
/examples/coins/coins_new.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2018 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | # 100 coins must sum to $5.00
23 |
24 | from pyswip import Prolog, Functor, Variable, Query
25 |
26 |
27 | def main():
28 | Prolog.consult("coins.pl", relative_to=__file__)
29 | count = int(input("How many coins (default: 100)? ") or 100)
30 | total = int(input("What should be the total (default: 500)? ") or 500)
31 | coins = Functor("coins", 3)
32 | S = Variable()
33 | q = Query(coins(count, total, S))
34 | i = 0
35 | while q.nextSolution():
36 | ## [1,5,10,50,100]
37 | s = zip(S.value, [1, 5, 10, 50, 100])
38 | print(i, end=" ")
39 | for c, v in s:
40 | print(f"{c}x{v}", end=" ")
41 | print()
42 | i += 1
43 | q.closeQuery()
44 |
45 |
46 | if __name__ == "__main__":
47 | main()
48 |
--------------------------------------------------------------------------------
/examples/create_term.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | from pyswip.core import *
25 | from pyswip import Prolog
26 |
27 |
28 | def main():
29 | a1 = PL_new_term_refs(2)
30 | a2 = a1 + 1
31 | t = PL_new_term_ref()
32 | ta = PL_new_term_ref()
33 |
34 | animal2 = PL_new_functor(PL_new_atom("animal"), 2)
35 | assertz = PL_new_functor(PL_new_atom("assertz"), 1)
36 |
37 | PL_put_atom_chars(a1, "gnu")
38 | PL_put_integer(a2, 50)
39 | PL_cons_functor_v(t, animal2, a1)
40 | PL_cons_functor_v(ta, assertz, t)
41 | PL_call(ta, None)
42 |
43 | print(list(Prolog.query("animal(X,Y)", catcherrors=True)))
44 |
45 |
46 | if __name__ == "__main__":
47 | main()
48 |
--------------------------------------------------------------------------------
/examples/draughts/puzzle1.pl:
--------------------------------------------------------------------------------
1 | % This example is adapted from http://eclipse.crosscoreop.com/examples/puzzle1.pl.txt
2 |
3 | :- use_module(library('bounds')).
4 |
5 | solve(Board) :-
6 | Board = [NW,N,NE,W,E,SW,S,SE],
7 | Board in 0..12,
8 | sum(Board, #=, 12),
9 | NW + N + NE #= 5,
10 | NE + E + SE #= 5,
11 | NW + W + SW #= 5,
12 | SW + S + SE #= 5,
13 |
14 | label(Board).
15 |
--------------------------------------------------------------------------------
/examples/draughts/puzzle1.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2018 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | # This example is adapted from http://eclipse.crosscoreop.com/examples/puzzle1.pl.txt
23 |
24 | # "Twelve draught pieces are arranged in a square frame with four on
25 | # each side. Try placing them so there are 5 on each side. (Kordemsky)
26 | #
27 | # "Maybe this problem is not described very well but I wanted to stick
28 | # with the original text from Kordemsky. The problem may be stated in
29 | # terms of guards on the wall of a square fort. If a guard stands on a
30 | # side wall then he may only watch that particular wall whereas a guard
31 | # at a corner may watch two walls. If twelve guards are positioned such
32 | # that there are two on each side wall and one at each corner then there
33 | # are four guards watching each wall. How can they be rearranged such
34 | # that there are five watching each wall?"
35 |
36 | from pyswip import Prolog
37 |
38 |
39 | def main():
40 | Prolog.consult("puzzle1.pl", relative_to=__file__)
41 |
42 | for soln in Prolog.query("solve(B)."):
43 | B = soln["B"]
44 |
45 | # [NW,N,NE,W,E,SW,S,SE]
46 | print("%d %d %d" % tuple(B[:3]))
47 | print("%d %d" % tuple(B[3:5]))
48 | print("%d %d %d" % tuple(B[5:]))
49 |
50 | cont = input("Press 'n' to finish: ")
51 | if cont.lower() == "n":
52 | break
53 |
54 |
55 | if __name__ == "__main__":
56 | main()
57 |
--------------------------------------------------------------------------------
/examples/father.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | from pyswip import *
25 |
26 |
27 | def main():
28 | father = Functor("father", 2)
29 | mother = Functor("mother", 2)
30 |
31 | Prolog.assertz("father(john,mich)")
32 | Prolog.assertz("father(john,gina)")
33 | Prolog.assertz("mother(jane,mich)")
34 |
35 | Y = Variable()
36 | Z = Variable()
37 |
38 | listing = Functor("listing", 1)
39 | call(listing(father))
40 |
41 | q = Query(father("john", Y), mother(Z, Y))
42 | while q.nextSolution():
43 | print(Y.value, Z.value)
44 | q.closeQuery() # Newer versions of SWI-Prolog do not allow nested queries
45 |
46 | print("\nQuery with strings\n")
47 | for s in Prolog.query("father(john,Y),mother(Z,Y)"):
48 | print(s["Y"], s["Z"])
49 |
50 |
51 | if __name__ == "__main__":
52 | main()
53 |
--------------------------------------------------------------------------------
/examples/knowledgebase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | from pyswip import Prolog, Functor, Variable, Query, newModule, call
25 |
26 |
27 | def main():
28 | _ = Prolog() # not strictly required, but helps to silence the linter
29 |
30 | assertz = Functor("assertz")
31 | parent = Functor("parent", 2)
32 | test1 = newModule("test1")
33 | test2 = newModule("test2")
34 |
35 | call(assertz(parent("john", "bob")), module=test1)
36 | call(assertz(parent("jane", "bob")), module=test1)
37 |
38 | call(assertz(parent("mike", "bob")), module=test2)
39 | call(assertz(parent("gina", "bob")), module=test2)
40 |
41 | print("knowledgebase test1")
42 |
43 | X = Variable()
44 | q = Query(parent(X, "bob"), module=test1)
45 | while q.nextSolution():
46 | print(X.value)
47 | q.closeQuery()
48 |
49 | print("knowledgebase test2")
50 |
51 | q = Query(parent(X, "bob"), module=test2)
52 | while q.nextSolution():
53 | print(X.value)
54 | q.closeQuery()
55 |
56 |
57 | if __name__ == "__main__":
58 | main()
59 |
--------------------------------------------------------------------------------
/examples/register_foreign.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | from pyswip import Prolog, registerForeign, Atom
25 |
26 |
27 | def atom_checksum(*a):
28 | if isinstance(a[0], Atom):
29 | r = sum(ord(c) & 0xFF for c in str(a[0]))
30 | a[1].value = r & 0xFF
31 | return True
32 | else:
33 | return False
34 |
35 |
36 | registerForeign(atom_checksum, arity=2)
37 | print(list(Prolog.query("X='Python', atom_checksum(X, Y)", catcherrors=False)))
38 |
--------------------------------------------------------------------------------
/examples/register_foreign_simple.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | # Demonstrates registering a Python function as a Prolog predicate through SWI-Prolog's FFI.
25 |
26 | from pyswip.prolog import Prolog
27 | from pyswip.easy import registerForeign
28 |
29 |
30 | def hello(who):
31 | print("Hello,", who)
32 |
33 |
34 | def main():
35 | registerForeign(hello)
36 | Prolog.assertz("father(michael,john)")
37 | Prolog.assertz("father(michael,gina)")
38 | list(Prolog.query("father(michael,X), hello(X)"))
39 |
40 |
41 | if __name__ == "__main__":
42 | main()
43 |
--------------------------------------------------------------------------------
/examples/sendmoremoney/money.pl:
--------------------------------------------------------------------------------
1 |
2 | % SEND + MORE = MONEY
3 | % Adapted from: http://en.wikipedia.org/wiki/Constraint_programming
4 |
5 | :- use_module(library('bounds')).
6 |
7 | sendmore(Digits) :-
8 | Digits = [S,E,N,D,M,O,R,Y], % Create variables
9 | Digits in 0..9, % Associate domains to variables
10 | S #\= 0, % Constraint: S must be different from 0
11 | M #\= 0,
12 | all_different(Digits), % all the elements must take different values
13 | 1000*S + 100*E + 10*N + D % Other constraints
14 | + 1000*M + 100*O + 10*R + E
15 | #= 10000*M + 1000*O + 100*N + 10*E + Y,
16 | label(Digits). % Start the search
17 |
18 |
--------------------------------------------------------------------------------
/examples/sendmoremoney/money.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2024 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | # S E N D
23 | # M O R E
24 | # + -------
25 | # M O N E Y
26 | #
27 | # So, what should be the values of S, E, N, D, M, O, R, Y
28 | # if they are all distinct digits.
29 |
30 | from pyswip import Prolog
31 |
32 | letters = list("SENDMORY")
33 | Prolog.consult("money.pl", relative_to=__file__)
34 | for result in Prolog.query("sendmore(X)"):
35 | r = result["X"]
36 | for i, letter in enumerate(letters):
37 | print(letter, "=", r[i])
38 |
39 | print("That's all...")
40 |
--------------------------------------------------------------------------------
/examples/sendmoremoney/money_new.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2018 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | # S E N D
23 | # M O R E
24 | # + -------
25 | # M O N E Y
26 | #
27 | # So, what should be the values of S, E, N, D, M, O, R, Y
28 | # if they are all distinct digits.
29 |
30 | from pyswip import Prolog, Functor, Variable, call
31 |
32 |
33 | def main():
34 | letters = list("SENDMORY")
35 | sendmore = Functor("sendmore")
36 | Prolog.consult("money.pl", relative_to=__file__)
37 |
38 | X = Variable()
39 | call(sendmore(X))
40 | r = X.value
41 | for i, letter in enumerate(letters):
42 | print(letter, "=", r[i])
43 | print("That's all...")
44 |
45 |
46 | if __name__ == "__main__":
47 | main()
48 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "pyswip"
7 | version = "0.3.2"
8 | description = "PySwip enables querying SWI-Prolog in your Python programs."
9 | readme = "README.md"
10 | requires-python = ">=3.9"
11 | authors = [
12 | { name = "Yuce Tekol", email = "yucetekol@gmail.com" },
13 | ]
14 | keywords = [
15 | "ai",
16 | "artificial intelligence",
17 | "ctypes",
18 | "ffi",
19 | "prolog",
20 | ]
21 | classifiers = [
22 | "Development Status :: 4 - Beta",
23 | "Intended Audience :: Developers",
24 | "Intended Audience :: Science/Research",
25 | "License :: OSI Approved :: MIT License",
26 | "Operating System :: OS Independent",
27 | "Programming Language :: Python",
28 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
29 | "Topic :: Software Development :: Libraries :: Python Modules",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Programming Language :: Python :: 3.11",
33 | "Programming Language :: Python :: 3.12",
34 | "Programming Language :: Python :: 3.13",
35 | "Programming Language :: Python :: Implementation :: CPython",
36 | ]
37 |
38 | [project.urls]
39 | Download = "https://github.com/yuce/pyswip/releases"
40 | Homepage = "https://pyswip.org"
41 |
42 | [tool.ruff.lint]
43 | ignore = ["F403", "F405", "E721"]
44 |
45 | [tool.pytest.ini_options]
46 | markers = [
47 | "slow: marks tests as slow (deselect with '-m \"not slow\"')",
48 | ]
49 |
50 | [tool.setuptools.package-data]
51 | pyswip = ["py.typed"]
52 | "pyswip.examples" = ["*.pl"]
53 |
54 | [tool.setuptools.packages.find]
55 | where = ["src"]
56 |
--------------------------------------------------------------------------------
/src/pyswip/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip Contributors
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | __VERSION__ = "0.3.2"
23 |
24 | from pyswip.prolog import Prolog as Prolog
25 | from pyswip.easy import *
26 | from pyswip.core import *
27 |
--------------------------------------------------------------------------------
/src/pyswip/core.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip Contributors
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in all
11 | # copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | # SOFTWARE.
20 |
21 | import atexit
22 | import glob
23 | import os
24 | import sys
25 | from contextlib import contextmanager
26 | from ctypes import *
27 | from ctypes.util import find_library
28 | from subprocess import Popen, PIPE
29 | from typing import Tuple
30 |
31 | ENV_LIBSWIPL_PATH = "LIBSWIPL_PATH"
32 | ENV_SWI_HOME_DIR = "SWI_HOME_DIR"
33 |
34 |
35 | class PySwipError(Exception):
36 | def __init__(self, message):
37 | super().__init__(message)
38 |
39 |
40 | class SwiPrologNotFoundError(PySwipError):
41 | def __init__(self, message="SWI-Prolog not found"):
42 | super().__init__(message)
43 |
44 |
45 | # To initialize the SWI-Prolog environment, two things need to be done: the
46 | # first is to find where the SO/DLL is located and the second is to find the
47 | # SWI-Prolog home, to get the saved state.
48 | #
49 | # The goal of the (entangled) process below is to make the library installation
50 | # independent.
51 |
52 |
53 | def _findSwiplPathFromFindLib():
54 | """
55 | This function resorts to ctype's find_library to find the path to the
56 | DLL. The biggest problem is that find_library does not give the path to the
57 | resource file.
58 |
59 | :returns:
60 | A path to the swipl SO/DLL or None if it is not found.
61 |
62 | :returns type:
63 | {str, None}
64 | """
65 |
66 | path = (
67 | find_library("swipl") or find_library("pl") or find_library("libswipl")
68 | ) # This last one is for Windows
69 | return path
70 |
71 |
72 | def _findSwiplFromExec():
73 | """
74 | This function tries to use an executable on the path to find SWI-Prolog
75 | SO/DLL and the resource file.
76 |
77 | :returns:
78 | A tuple of (path to the swipl DLL, path to the resource file)
79 |
80 | :returns type:
81 | ({str, None}, {str, None})
82 | """
83 |
84 | platform = sys.platform[:3]
85 |
86 | fullName = None
87 | swiHome = None
88 |
89 | try: # try to get library path from swipl executable.
90 | # We may have pl or swipl as the executable
91 | cmd = Popen(["swipl", "--dump-runtime-variables"], stdout=PIPE)
92 | ret = cmd.communicate()
93 |
94 | # Parse the output into a dictionary
95 | ret = ret[0].decode().replace(";", "").splitlines()
96 | ret = [line.split("=", 1) for line in ret]
97 | rtvars = dict((name, value[1:-1]) for name, value in ret) # [1:-1] gets
98 | # rid of the
99 | # quotes
100 |
101 | if rtvars["PLSHARED"] == "no":
102 | raise ImportError("SWI-Prolog is not installed as a shared " "library.")
103 | else: # PLSHARED == 'yes'
104 | swiHome = rtvars["PLBASE"] # The environment is in PLBASE
105 | if not os.path.exists(swiHome):
106 | swiHome = None
107 |
108 | # determine platform specific path. First try runtime
109 | # variable `PLLIBSWIPL` introduced in 9.1.1/9.0.1
110 | if "PLLIBSWIPL" in rtvars:
111 | fullName = rtvars["PLLIBSWIPL"]
112 | # determine platform specific path
113 | elif platform == "win":
114 | dllName = rtvars["PLLIB"][:-4] + "." + rtvars["PLSOEXT"]
115 | path = os.path.join(rtvars["PLBASE"], "bin")
116 | fullName = os.path.join(path, dllName)
117 |
118 | if not os.path.exists(fullName):
119 | fullName = None
120 |
121 | elif platform == "cyg":
122 | # e.g. /usr/lib/pl-5.6.36/bin/i686-cygwin/cygpl.dll
123 |
124 | dllName = "cygpl.dll"
125 | path = os.path.join(rtvars["PLBASE"], "bin", rtvars["PLARCH"])
126 | fullName = os.path.join(path, dllName)
127 |
128 | if not os.path.exists(fullName):
129 | fullName = None
130 |
131 | elif platform == "dar":
132 | dllName = "lib" + rtvars["PLLIB"][2:] + "." + "dylib"
133 | path = os.path.join(rtvars["PLBASE"], "lib", rtvars["PLARCH"])
134 | baseName = os.path.join(path, dllName)
135 |
136 | if os.path.exists(baseName):
137 | fullName = baseName
138 | else: # We will search for versions
139 | fullName = None
140 |
141 | else: # assume UNIX-like
142 | # The SO name in some linuxes is of the form libswipl.so.5.10.2,
143 | # so we have to use glob to find the correct one
144 | dllName = "lib" + rtvars["PLLIB"][2:] + "." + rtvars["PLSOEXT"]
145 | path = os.path.join(rtvars["PLBASE"], "lib", rtvars["PLARCH"])
146 | baseName = os.path.join(path, dllName)
147 |
148 | if os.path.exists(baseName):
149 | fullName = baseName
150 | else: # We will search for versions
151 | pattern = baseName + ".*"
152 | files = glob.glob(pattern)
153 | if len(files) == 0:
154 | fullName = None
155 | else:
156 | fullName = files[0]
157 |
158 | except (OSError, KeyError): # KeyError from accessing rtvars
159 | pass
160 |
161 | return fullName, swiHome
162 |
163 |
164 | def _find_swipl_windows():
165 | """
166 | This function uses several heuristics to gues where SWI-Prolog is installed
167 | in Windows.
168 |
169 | :returns:
170 | A tuple of (path to the swipl DLL, path to the resource file)
171 |
172 | :returns type:
173 | ({str, None}, {str, None})
174 | """
175 |
176 | libswipl = "libswipl.dll"
177 | # tru to get the SWI dir from registry
178 | swi_dir = find_swipl_dir_from_registry()
179 | if swi_dir:
180 | # libswipl.dll must be in SWI_DIR/bin
181 | libswipl_path = os.path.join(swi_dir, "bin", libswipl)
182 | if not os.path.exists(libswipl_path):
183 | raise SwiPrologNotFoundError(
184 | f"could not locate {libswipl} at {libswipl_path}"
185 | )
186 | return libswipl_path, swi_dir
187 |
188 | raise SwiPrologNotFoundError
189 |
190 |
191 | def find_swipl_dir_from_registry():
192 | import winreg
193 |
194 | try:
195 | with winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, r"Software\SWI\Prolog") as key:
196 | path, _ = winreg.QueryValueEx(key, "home")
197 | return path
198 | except FileNotFoundError:
199 | return ""
200 |
201 |
202 | def _find_swipl_unix():
203 | """
204 | This function uses several heuristics to guess where SWI-Prolog is
205 | installed in Linuxes.
206 |
207 | :returns:
208 | A tuple of (path to the swipl so, path to the resource file)
209 |
210 | :returns type:
211 | ({str, None}, {str, None})
212 | """
213 |
214 | # Maybe the exec is on path?
215 | path, swi_home = _findSwiplFromExec()
216 | if path is not None:
217 | return path, swi_home
218 |
219 | # If it is not, use find_library
220 | path = _findSwiplPathFromFindLib()
221 | if path is not None:
222 | swi_home = find_swi_home(path)
223 | return path, swi_home
224 |
225 | # Our last try: some hardcoded paths.
226 | paths = [
227 | "/lib",
228 | "/usr/lib",
229 | "/usr/local/lib",
230 | ".",
231 | "./lib",
232 | "/usr/lib/swi-prolog/lib/x86_64-linux",
233 | ]
234 | names = ["libswipl.so"]
235 |
236 | path = None
237 | for name in names:
238 | for try_ in paths:
239 | try_ = os.path.join(try_, name)
240 | if os.path.exists(try_):
241 | path = try_
242 | break
243 |
244 | if path is not None:
245 | swi_home = find_swi_home(path)
246 | return path, swi_home
247 |
248 | raise SwiPrologNotFoundError
249 |
250 |
251 | def find_swipl_macos_home() -> Tuple[str, str]:
252 | """
253 | This function is guesing where SWI-Prolog is
254 | installed in MacOS via .app.
255 |
256 | :parameters:
257 | - `swi_ver` (str) - Version of SWI-Prolog in '[0-9].[0-9].[0-9]' format
258 |
259 | :returns:
260 | A tuple of (path to the swipl so, path to the resource file)
261 |
262 | :returns type:
263 | ({str, None}, {str, None})
264 | """
265 |
266 | swi_home = os.environ.get("SWI_HOME_DIR")
267 | if not swi_home:
268 | swi_home = "/Applications/SWI-Prolog.app/Contents/swipl"
269 | if os.path.exists(swi_home):
270 | swi_base = os.path.split(swi_home)[0]
271 | framework_path = os.path.join(swi_base, "Frameworks")
272 | lib = find_swipl_dylib(framework_path)
273 | if lib:
274 | lib_path = os.path.join(framework_path, lib)
275 | return lib_path, swi_home
276 |
277 | return "", ""
278 |
279 |
280 | def find_swipl_dylib(root) -> str:
281 | for item in os.listdir(root):
282 | if item.startswith("libswipl") and item.endswith(".dylib"):
283 | return item
284 | return ""
285 |
286 |
287 | def _find_swipl_darwin() -> (str, str):
288 | """
289 | This function uses several heuristics to guess where SWI-Prolog is
290 | installed in MacOS.
291 |
292 | :returns:
293 | A tuple of (path to the swipl so, path to the resource file)
294 |
295 | :returns type:
296 | ({str, None}, {str, None})
297 | """
298 |
299 | path, swi_home = find_swipl_macos_home()
300 | if path and swi_home:
301 | return path, swi_home
302 |
303 | # If the exec is in path
304 | path, swi_home = _findSwiplFromExec()
305 | if path:
306 | return path, swi_home
307 |
308 | # If it is not, use find_library
309 | path = _findSwiplPathFromFindLib()
310 | if path:
311 | swi_home = find_swi_home(os.path.dirname(path))
312 | return path, swi_home
313 |
314 | raise SwiPrologNotFoundError
315 |
316 |
317 | def find_swi_home(path) -> str:
318 | while True:
319 | swi_home = os.path.join(path, "swipl.home")
320 | if os.path.exists(swi_home):
321 | with open(swi_home) as f:
322 | sub_path = f.read().strip()
323 | return os.path.join(path, sub_path)
324 | path, leaf = os.path.split(path)
325 | if not leaf:
326 | break
327 | return ""
328 |
329 |
330 | def _find_swipl() -> (str, str):
331 | """
332 | This function makes a big effort to find the path to the SWI-Prolog shared
333 | library. Since this is both OS dependent and installation dependent, we may
334 | not aways succeed. If we do, we return a name/path that can be used by
335 | CDLL(). Otherwise we raise an exception.
336 |
337 | :return: Tuple. Fist element is the name or path to the library that can be
338 | used by CDLL. Second element is the path were SWI-Prolog resource
339 | file may be found (this is needed in some Linuxes)
340 | :rtype: Tuple of strings
341 | :raises ImportError: If we cannot guess the name of the library
342 | """
343 | # Check the environment first
344 | libswipl_path = os.environ.get(ENV_LIBSWIPL_PATH)
345 | swi_home_dir = os.environ.get(ENV_SWI_HOME_DIR)
346 | if libswipl_path and swi_home_dir:
347 | return libswipl_path, swi_home_dir
348 |
349 | # Now begins the guesswork
350 | platform = sys.platform
351 | if platform == "win32":
352 | libswipl_path, swi_home_dir = _find_swipl_windows()
353 | fix_windows_path(libswipl_path)
354 | return libswipl_path, swi_home_dir
355 | elif platform == "darwin":
356 | return _find_swipl_darwin()
357 | else:
358 | # This should work for other Linux and BSD
359 | return _find_swipl_unix()
360 |
361 |
362 | def fix_windows_path(dll):
363 | """
364 | When the path to the DLL is not in Windows search path, Windows will not be
365 | able to find other DLLs on the same directory, so we have to add it to the
366 | path. This function takes care of it.
367 |
368 | :parameters:
369 | - `dll` (str) - File name of the DLL
370 | """
371 |
372 | pathToDll = os.path.dirname(dll)
373 | currentWindowsPath = os.getenv("PATH")
374 |
375 | if pathToDll not in currentWindowsPath:
376 | # We will prepend the path, to avoid conflicts between DLLs
377 | newPath = pathToDll + ";" + currentWindowsPath
378 | os.putenv("PATH", newPath)
379 |
380 |
381 | _stringMap = {}
382 |
383 |
384 | def str_to_bytes(string):
385 | """
386 | Turns a string into a bytes if necessary (i.e. if it is not already a bytes
387 | object or None).
388 | If string is None, int or c_char_p it will be returned directly.
389 |
390 | :param string: The string that shall be transformed
391 | :type string: str, bytes or type(None)
392 | :return: Transformed string
393 | :rtype: c_char_p compatible object (bytes, c_char_p, int or None)
394 | """
395 | if string is None or isinstance(string, (int, c_char_p)):
396 | return string
397 |
398 | if not isinstance(string, bytes):
399 | if string not in _stringMap:
400 | _stringMap[string] = string.encode()
401 | string = _stringMap[string]
402 |
403 | return string
404 |
405 |
406 | def list_to_bytes_list(strList):
407 | """
408 | This function turns an array of strings into a pointer array
409 | with pointers pointing to the encodings of those strings
410 | Possibly contained bytes are kept as they are.
411 |
412 | :param strList: List of strings that shall be converted
413 | :type strList: List of strings
414 | :returns: Pointer array with pointers pointing to bytes
415 | :raises: TypeError if strList is not list, set or tuple
416 | """
417 | pList = c_char_p * len(strList)
418 |
419 | # if strList is already a pointerarray or None, there is nothing to do
420 | if isinstance(strList, (pList, type(None))):
421 | return strList
422 |
423 | if not isinstance(strList, (list, set, tuple)):
424 | raise TypeError("strList must be list, set or tuple, not " + str(type(strList)))
425 |
426 | pList = pList()
427 | for i, elem in enumerate(strList):
428 | pList[i] = str_to_bytes(elem)
429 | return pList
430 |
431 |
432 | # create a decorator that turns the incoming strings into c_char_p compatible
433 | # butes or pointer arrays
434 | def check_strings(strings, arrays):
435 | """
436 | Decorator function which can be used to automatically turn an incoming
437 | string into a bytes object and an incoming list to a pointer array if
438 | necessary.
439 |
440 | :param strings: Indices of the arguments must be pointers to bytes
441 | :type strings: List of integers
442 | :param arrays: Indices of the arguments must be arrays of pointers to bytes
443 | :type arrays: List of integers
444 | """
445 |
446 | # if given a single element, turn it into a list
447 | if isinstance(strings, int):
448 | strings = [strings]
449 | elif strings is None:
450 | strings = []
451 |
452 | # check if all entries are integers
453 | for i, k in enumerate(strings):
454 | if not isinstance(k, int):
455 | raise TypeError(
456 | (
457 | "Wrong type for index at {0} " + "in strings. Must be int, not {1}!"
458 | ).format(i, k)
459 | )
460 |
461 | # if given a single element, turn it into a list
462 | if isinstance(arrays, int):
463 | arrays = [arrays]
464 | elif arrays is None:
465 | arrays = []
466 |
467 | # check if all entries are integers
468 | for i, k in enumerate(arrays):
469 | if not isinstance(k, int):
470 | raise TypeError(
471 | (
472 | "Wrong type for index at {0} " + "in arrays. Must be int, not {1}!"
473 | ).format(i, k)
474 | )
475 |
476 | # check if some index occurs in both
477 | if set(strings).intersection(arrays):
478 | raise ValueError(
479 | "One or more elements occur in both arrays and "
480 | + " strings. One parameter cannot be both list and string!"
481 | )
482 |
483 | # create the checker that will check all arguments given by argsToCheck
484 | # and turn them into the right datatype.
485 | def checker(func):
486 | def check_and_call(*args):
487 | args = list(args)
488 | for i in strings:
489 | arg = args[i]
490 | args[i] = str_to_bytes(arg)
491 | for i in arrays:
492 | arg = args[i]
493 | args[i] = list_to_bytes_list(arg)
494 |
495 | return func(*args)
496 |
497 | return check_and_call
498 |
499 | return checker
500 |
501 |
502 | # Find the path and resource file. SWI_HOME_DIR shall be treated as a constant
503 | # by users of this module
504 | _path, SWI_HOME_DIR = _find_swipl()
505 |
506 | # Load the library
507 | _lib = CDLL(_path, mode=RTLD_GLOBAL)
508 |
509 | # /*******************************
510 | # * VERSIONS *
511 | # *******************************/
512 |
513 | PL_VERSION_SYSTEM = 1 # Prolog version
514 | PL_VERSION_FLI = 2 # PL_* compatibility
515 | PL_VERSION_REC = 3 # PL_record_external() compatibility
516 | PL_VERSION_QLF = 4 # Saved QLF format version
517 | PL_VERSION_QLF_LOAD = 5 # Min loadable QLF format version
518 | PL_VERSION_VM = 6 # VM signature
519 | PL_VERSION_BUILT_IN = 7 # Built-in predicate signature
520 |
521 | # After SWI-Prolog 8.5.2, PL_version was renamed to PL_version_info
522 | # to avoid a conflict with Perl. For more details, see the following:
523 | # https://github.com/SWI-Prolog/swipl-devel/issues/900
524 | # https://github.com/SWI-Prolog/swipl-devel/issues/910
525 | try:
526 | if hasattr(_lib, "PL_version_info"):
527 | PL_version = _lib.PL_version_info # swi-prolog > 8.5.2
528 | else:
529 | PL_version = _lib.PL_version # swi-prolog <= 8.5.2
530 | PL_version.argtypes = [c_int]
531 | PL_version.restype = c_uint
532 |
533 | PL_VERSION = PL_version(PL_VERSION_SYSTEM)
534 | if PL_VERSION < 80200:
535 | raise Exception("swi-prolog >= 8.2.0 is required")
536 | except AttributeError:
537 | raise Exception("swi-prolog version number could not be determined")
538 |
539 | # PySwip constants
540 | PYSWIP_MAXSTR = 1024
541 | c_int_p = c_void_p
542 | c_long_p = c_void_p
543 | c_double_p = c_void_p
544 | c_uint_p = c_void_p
545 |
546 | #
547 | # constants (from SWI-Prolog.h)
548 | # /* PL_unify_term( arguments */
549 |
550 |
551 | if PL_VERSION < 80200:
552 | # constants (from SWI-Prolog.h)
553 | # PL_unify_term() arguments
554 | PL_VARIABLE = 1 # nothing
555 | PL_ATOM = 2 # const char
556 | PL_INTEGER = 3 # int
557 | PL_FLOAT = 4 # double
558 | PL_STRING = 5 # const char *
559 | PL_TERM = 6 #
560 | # PL_unify_term()
561 | PL_FUNCTOR = 10 # functor_t, arg ...
562 | PL_LIST = 11 # length, arg ...
563 | PL_CHARS = 12 # const char *
564 | PL_POINTER = 13 # void *
565 | # /* PlArg::PlArg(text, type) */
566 | # define PL_CODE_LIST (14) /* [ascii...] */
567 | # define PL_CHAR_LIST (15) /* [h,e,l,l,o] */
568 | # define PL_BOOL (16) /* PL_set_feature() */
569 | # define PL_FUNCTOR_CHARS (17) /* PL_unify_term() */
570 | # define _PL_PREDICATE_INDICATOR (18) /* predicate_t (Procedure) */
571 | # define PL_SHORT (19) /* short */
572 | # define PL_INT (20) /* int */
573 | # define PL_LONG (21) /* long */
574 | # define PL_DOUBLE (22) /* double */
575 | # define PL_NCHARS (23) /* unsigned, const char * */
576 | # define PL_UTF8_CHARS (24) /* const char * */
577 | # define PL_UTF8_STRING (25) /* const char * */
578 | # define PL_INT64 (26) /* int64_t */
579 | # define PL_NUTF8_CHARS (27) /* unsigned, const char * */
580 | # define PL_NUTF8_CODES (29) /* unsigned, const char * */
581 | # define PL_NUTF8_STRING (30) /* unsigned, const char * */
582 | # define PL_NWCHARS (31) /* unsigned, const wchar_t * */
583 | # define PL_NWCODES (32) /* unsigned, const wchar_t * */
584 | # define PL_NWSTRING (33) /* unsigned, const wchar_t * */
585 | # define PL_MBCHARS (34) /* const char * */
586 | # define PL_MBCODES (35) /* const char * */
587 | # define PL_MBSTRING (36) /* const char * */
588 |
589 | REP_ISO_LATIN_1 = 0x0000 # output representation
590 | REP_UTF8 = 0x1000
591 | REP_MB = 0x2000
592 |
593 | else:
594 | PL_VARIABLE = 1 # nothing
595 | PL_ATOM = 2 # const char *
596 | PL_INTEGER = 3 # int
597 | PL_RATIONAL = 4 # rational number
598 | PL_FLOAT = 5 # double
599 | PL_STRING = 6 # const char *
600 | PL_TERM = 7
601 |
602 | PL_NIL = 8 # The constant []
603 | PL_BLOB = 9 # non-atom blob
604 | PL_LIST_PAIR = 10 # [_|_] term
605 |
606 | # # PL_unify_term(
607 | PL_FUNCTOR = 11 # functor_t, arg ...
608 | PL_LIST = 12 # length, arg ...
609 | PL_CHARS = 13 # const char *
610 | PL_POINTER = 14 # void *
611 | # PlArg::PlArg(text, type
612 | PL_CODE_LIST = 15 # [ascii...]
613 | PL_CHAR_LIST = 16 # [h,e,l,l,o]
614 | PL_BOOL = 17 # PL_set_prolog_flag(
615 | PL_FUNCTOR_CHARS = 18 # PL_unify_term(
616 | _PL_PREDICATE_INDICATOR = 19 # predicate_t= Procedure
617 | PL_SHORT = 20 # short
618 | PL_INT = 21 # int
619 | PL_LONG = 22 # long
620 | PL_DOUBLE = 23 # double
621 | PL_NCHARS = 24 # size_t, const char *
622 | PL_UTF8_CHARS = 25 # const char *
623 | PL_UTF8_STRING = 26 # const char *
624 | PL_INT64 = 27 # int64_t
625 | PL_NUTF8_CHARS = 28 # size_t, const char *
626 | PL_NUTF8_CODES = 29 # size_t, const char *
627 | PL_NUTF8_STRING = 30 # size_t, const char *
628 | PL_NWCHARS = 31 # size_t, const wchar_t *
629 | PL_NWCODES = 32 # size_t, const wchar_t *
630 | PL_NWSTRING = 33 # size_t, const wchar_t *
631 | PL_MBCHARS = 34 # const char *
632 | PL_MBCODES = 35 # const char *
633 | PL_MBSTRING = 36 # const char *
634 | PL_INTPTR = 37 # intptr_t
635 | PL_CHAR = 38 # int
636 | PL_CODE = 39 # int
637 | PL_BYTE = 40 # int
638 | # PL_skip_list(
639 | PL_PARTIAL_LIST = 41 # a partial list
640 | PL_CYCLIC_TERM = 42 # a cyclic list/term
641 | PL_NOT_A_LIST = 43 # Object is not a list
642 | # dicts
643 | PL_DICT = 44
644 |
645 | REP_ISO_LATIN_1 = 0x0000 # output representation
646 | REP_UTF8 = 0x00100000
647 | REP_MB = 0x00200000
648 |
649 | # /********************************
650 | # * NON-DETERMINISTIC CALL/RETURN *
651 | # *********************************/
652 | #
653 | # Note 1: Non-deterministic foreign functions may also use the deterministic
654 | # return methods PL_succeed and PL_fail.
655 | #
656 | # Note 2: The argument to PL_retry is a 30 bits signed integer (long).
657 |
658 | PL_FIRST_CALL = 0
659 | PL_CUTTED = 1
660 | PL_PRUNED = PL_CUTTED
661 | PL_REDO = 2
662 |
663 | PL_FA_NOTRACE = 0x01 # foreign cannot be traced
664 | PL_FA_TRANSPARENT = 0x02 # foreign is module transparent
665 | PL_FA_NONDETERMINISTIC = 0x04 # foreign is non-deterministic
666 | PL_FA_VARARGS = 0x08 # call using t0, ac, ctx
667 | PL_FA_CREF = 0x10 # Internal: has clause-reference */
668 |
669 | # /*******************************
670 | # * CALL-BACK *
671 | # *******************************/
672 |
673 | PL_Q_DEBUG = 0x01 # = TRUE for backward compatibility
674 | PL_Q_NORMAL = 0x02 # normal usage
675 | PL_Q_NODEBUG = 0x04 # use this one
676 | PL_Q_CATCH_EXCEPTION = 0x08 # handle exceptions in C
677 | PL_Q_PASS_EXCEPTION = 0x10 # pass to parent environment
678 | PL_Q_DETERMINISTIC = 0x20 # call was deterministic
679 |
680 | # /*******************************
681 | # * BLOBS *
682 | # *******************************/
683 |
684 | # define PL_BLOB_MAGIC_B 0x75293a00 /* Magic to validate a blob-type */
685 | # define PL_BLOB_VERSION 1 /* Current version */
686 | # define PL_BLOB_MAGIC (PL_BLOB_MAGIC_B|PL_BLOB_VERSION)
687 |
688 | # define PL_BLOB_UNIQUE 0x01 /* Blob content is unique */
689 | # define PL_BLOB_TEXT 0x02 /* blob contains text */
690 | # define PL_BLOB_NOCOPY 0x04 /* do not copy the data */
691 | # define PL_BLOB_WCHAR 0x08 /* wide character string */
692 |
693 | # /*******************************
694 | # * CHAR BUFFERS *
695 | # *******************************/
696 |
697 | # Changed in 8.1.22
698 | if PL_VERSION < 80122:
699 | CVT_ATOM = 0x0001
700 | CVT_STRING = 0x0002
701 | CVT_LIST = 0x0004
702 | CVT_INTEGER = 0x0008
703 | CVT_FLOAT = 0x0010
704 | CVT_VARIABLE = 0x0020
705 | CVT_NUMBER = CVT_INTEGER | CVT_FLOAT
706 | CVT_ATOMIC = CVT_NUMBER | CVT_ATOM | CVT_STRING
707 | CVT_WRITE = 0x0040 # as of version 3.2.10
708 | CVT_ALL = CVT_ATOMIC | CVT_LIST
709 | CVT_MASK = 0x00FF
710 |
711 | BUF_DISCARDABLE = 0x0000
712 | BUF_RING = 0x0100
713 | BUF_MALLOC = 0x0200
714 |
715 | CVT_EXCEPTION = 0x10000 # throw exception on error
716 | else:
717 | CVT_ATOM = 0x00000001
718 | CVT_STRING = 0x00000002
719 | CVT_LIST = 0x00000004
720 | CVT_INTEGER = 0x00000008
721 | CVT_RATIONAL = 0x00000010
722 | CVT_FLOAT = 0x00000020
723 | CVT_VARIABLE = 0x00000040
724 | CVT_NUMBER = CVT_RATIONAL | CVT_FLOAT
725 | CVT_ATOMIC = CVT_NUMBER | CVT_ATOM | CVT_STRING
726 | CVT_WRITE = 0x00000080
727 | CVT_WRITE_CANONICAL = 0x00000080
728 | CVT_WRITEQ = 0x000000C0
729 | CVT_ALL = CVT_ATOMIC | CVT_LIST
730 | CVT_MASK = 0x00000FFF
731 |
732 | BUF_DISCARDABLE = 0x00000000
733 | BUF_STACK = 0x00010000
734 | BUF_RING = BUF_STACK
735 | BUF_MALLOC = 0x00020000
736 | BUF_ALLOW_STACK = 0x00040000
737 |
738 | CVT_EXCEPTION = 0x00001000 # throw exception on error
739 |
740 | argv = list_to_bytes_list(sys.argv + [None])
741 | argc = len(sys.argv)
742 |
743 | # /*******************************
744 | # * TYPES *
745 | # *******************************/
746 | #
747 | # typedef uintptr_t atom_t; /* Prolog atom */
748 | # typedef uintptr_t functor_t; /* Name/arity pair */
749 | # typedef void * module_t; /* Prolog module */
750 | # typedef void * predicate_t; /* Prolog procedure */
751 | # typedef void * record_t; /* Prolog recorded term */
752 | # typedef uintptr_t term_t; /* opaque term handle */
753 | # typedef uintptr_t qid_t; /* opaque query handle */
754 | # typedef uintptr_t PL_fid_t; /* opaque foreign context handle */
755 | # typedef void * control_t; /* non-deterministic control arg */
756 | # typedef void * PL_engine_t; /* opaque engine handle */
757 | # typedef uintptr_t PL_atomic_t; /* same a word */
758 | # typedef uintptr_t foreign_t; /* return type of foreign functions */
759 | # typedef wchar_t pl_wchar_t; /* Prolog wide character */
760 | # typedef foreign_t (*pl_function_t)(); /* foreign language functions */
761 | # typedef uintptr_t buf_mark_t; /* buffer mark handle */
762 |
763 | atom_t = c_uint_p
764 | functor_t = c_uint_p
765 | module_t = c_void_p
766 | predicate_t = c_void_p
767 | record_t = c_void_p
768 | term_t = c_uint_p
769 | qid_t = c_uint_p
770 | PL_fid_t = c_uint_p
771 | fid_t = c_uint_p
772 | control_t = c_void_p
773 | PL_engine_t = c_void_p
774 | PL_atomic_t = c_uint_p
775 | foreign_t = c_uint_p
776 | pl_wchar_t = c_wchar
777 | intptr_t = c_long
778 | ssize_t = intptr_t
779 | wint_t = c_uint
780 | buf_mark_t = c_uint_p
781 |
782 | PL_initialise = _lib.PL_initialise
783 | PL_initialise = check_strings(None, 1)(PL_initialise)
784 |
785 | PL_mark_string_buffers = _lib.PL_mark_string_buffers
786 | PL_mark_string_buffers.argtypes = [buf_mark_t]
787 |
788 | PL_release_string_buffers_from_mark = _lib.PL_release_string_buffers_from_mark
789 | PL_release_string_buffers_from_mark.argtypes = [buf_mark_t]
790 |
791 |
792 | @contextmanager
793 | def PL_STRINGS_MARK():
794 | __PL_mark = buf_mark_t()
795 | PL_mark_string_buffers(byref(__PL_mark))
796 | try:
797 | yield
798 | finally:
799 | PL_release_string_buffers_from_mark(__PL_mark)
800 |
801 |
802 | PL_open_foreign_frame = _lib.PL_open_foreign_frame
803 | PL_open_foreign_frame.restype = fid_t
804 |
805 | PL_foreign_control = _lib.PL_foreign_control
806 | PL_foreign_control.argtypes = [control_t]
807 | PL_foreign_control.restype = c_int
808 |
809 | PL_foreign_context = _lib.PL_foreign_context
810 | PL_foreign_context.argtypes = [control_t]
811 | PL_foreign_context.restype = intptr_t
812 |
813 | PL_retry = _lib._PL_retry
814 | PL_retry.argtypes = [intptr_t]
815 | PL_retry.restype = foreign_t
816 |
817 | PL_new_term_ref = _lib.PL_new_term_ref
818 | PL_new_term_ref.restype = term_t
819 |
820 | PL_new_term_refs = _lib.PL_new_term_refs
821 | PL_new_term_refs.argtypes = [c_int]
822 | PL_new_term_refs.restype = term_t
823 |
824 | PL_chars_to_term = _lib.PL_chars_to_term
825 | PL_chars_to_term.argtypes = [c_char_p, term_t]
826 | PL_chars_to_term.restype = c_int
827 |
828 | PL_chars_to_term = check_strings(0, None)(PL_chars_to_term)
829 |
830 | PL_call = _lib.PL_call
831 | PL_call.argtypes = [term_t, module_t]
832 | PL_call.restype = c_int
833 |
834 | PL_call_predicate = _lib.PL_call_predicate
835 | PL_call_predicate.argtypes = [module_t, c_int, predicate_t, term_t]
836 | PL_call_predicate.restype = c_int
837 |
838 | PL_discard_foreign_frame = _lib.PL_discard_foreign_frame
839 | PL_discard_foreign_frame.argtypes = [fid_t]
840 | PL_discard_foreign_frame.restype = None
841 |
842 | PL_put_chars = _lib.PL_put_chars
843 | PL_put_chars.argtypes = [term_t, c_int, c_size_t, c_char_p]
844 | PL_put_chars.restype = c_int
845 |
846 | PL_put_list_chars = _lib.PL_put_list_chars
847 | PL_put_list_chars.argtypes = [term_t, c_char_p]
848 | PL_put_list_chars.restype = c_int
849 |
850 | PL_put_list_chars = check_strings(1, None)(PL_put_list_chars)
851 |
852 | # PL_EXPORT(void) PL_register_atom(atom_t a);
853 | PL_register_atom = _lib.PL_register_atom
854 | PL_register_atom.argtypes = [atom_t]
855 | PL_register_atom.restype = None
856 |
857 | # PL_EXPORT(void) PL_unregister_atom(atom_t a);
858 | PL_unregister_atom = _lib.PL_unregister_atom
859 | PL_unregister_atom.argtypes = [atom_t]
860 | PL_unregister_atom.restype = None
861 |
862 | # PL_EXPORT(atom_t) PL_functor_name(functor_t f);
863 | PL_functor_name = _lib.PL_functor_name
864 | PL_functor_name.argtypes = [functor_t]
865 | PL_functor_name.restype = atom_t
866 |
867 | # PL_EXPORT(int) PL_functor_arity(functor_t f);
868 | PL_functor_arity = _lib.PL_functor_arity
869 | PL_functor_arity.argtypes = [functor_t]
870 | PL_functor_arity.restype = c_int
871 |
872 | # /* Get C-values from Prolog terms */
873 | # PL_EXPORT(int) PL_get_atom(term_t t, atom_t *a);
874 | PL_get_atom = _lib.PL_get_atom
875 | PL_get_atom.argtypes = [term_t, POINTER(atom_t)]
876 | PL_get_atom.restype = c_int
877 |
878 | # PL_EXPORT(int) PL_get_bool(term_t t, int *value);
879 | PL_get_bool = _lib.PL_get_bool
880 | PL_get_bool.argtypes = [term_t, POINTER(c_int)]
881 | PL_get_bool.restype = c_int
882 |
883 | # PL_EXPORT(int) PL_get_atom_chars(term_t t, char **a);
884 | PL_get_atom_chars = _lib.PL_get_atom_chars # FIXME
885 | PL_get_atom_chars.argtypes = [term_t, POINTER(c_char_p)]
886 | PL_get_atom_chars.restype = c_int
887 |
888 | PL_get_atom_chars = check_strings(None, 1)(PL_get_atom_chars)
889 |
890 | PL_get_string_chars = _lib.PL_get_string
891 | PL_get_string_chars.argtypes = [term_t, POINTER(c_char_p), c_int_p]
892 |
893 | PL_get_chars = _lib.PL_get_chars # FIXME:
894 | PL_get_chars.argtypes = [term_t, POINTER(c_char_p), c_uint]
895 | PL_get_chars.restype = c_int
896 |
897 | PL_get_chars = check_strings(None, 1)(PL_get_chars)
898 |
899 | PL_get_integer = _lib.PL_get_integer
900 | PL_get_integer.argtypes = [term_t, POINTER(c_int)]
901 | PL_get_integer.restype = c_int
902 |
903 | PL_get_long = _lib.PL_get_long
904 | PL_get_long.argtypes = [term_t, POINTER(c_long)]
905 | PL_get_long.restype = c_int
906 |
907 | PL_get_float = _lib.PL_get_float
908 | PL_get_float.argtypes = [term_t, c_double_p]
909 | PL_get_float.restype = c_int
910 |
911 | PL_get_functor = _lib.PL_get_functor
912 | PL_get_functor.argtypes = [term_t, POINTER(functor_t)]
913 | PL_get_functor.restype = c_int
914 |
915 | PL_get_name_arity = _lib.PL_get_name_arity
916 | PL_get_name_arity.argtypes = [term_t, POINTER(atom_t), POINTER(c_int)]
917 | PL_get_name_arity.restype = c_int
918 |
919 | PL_get_arg = _lib.PL_get_arg
920 | PL_get_arg.argtypes = [c_int, term_t, term_t]
921 | PL_get_arg.restype = c_int
922 |
923 | PL_get_head = _lib.PL_get_head
924 | PL_get_head.argtypes = [term_t, term_t]
925 | PL_get_head.restype = c_int
926 |
927 | PL_get_tail = _lib.PL_get_tail
928 | PL_get_tail.argtypes = [term_t, term_t]
929 | PL_get_tail.restype = c_int
930 |
931 | PL_get_nil = _lib.PL_get_nil
932 | PL_get_nil.argtypes = [term_t]
933 | PL_get_nil.restype = c_int
934 |
935 | PL_put_atom_chars = _lib.PL_put_atom_chars
936 | PL_put_atom_chars.argtypes = [term_t, c_char_p]
937 | PL_put_atom_chars.restype = c_int
938 |
939 | PL_put_atom_chars = check_strings(1, None)(PL_put_atom_chars)
940 |
941 | PL_atom_chars = _lib.PL_atom_chars
942 | PL_atom_chars.argtypes = [atom_t]
943 | PL_atom_chars.restype = c_char_p
944 |
945 | PL_atom_wchars = _lib.PL_atom_wchars
946 | PL_atom_wchars.argtypes = [atom_t, POINTER(c_size_t)]
947 | PL_atom_wchars.restype = c_wchar_p
948 |
949 | PL_predicate = _lib.PL_predicate
950 | PL_predicate.argtypes = [c_char_p, c_int, c_char_p]
951 | PL_predicate.restype = predicate_t
952 |
953 | PL_predicate = check_strings([0, 2], None)(PL_predicate)
954 |
955 | PL_pred = _lib.PL_pred
956 | PL_pred.argtypes = [functor_t, module_t]
957 | PL_pred.restype = predicate_t
958 |
959 | PL_open_query = _lib.PL_open_query
960 | PL_open_query.argtypes = [module_t, c_int, predicate_t, term_t]
961 | PL_open_query.restype = qid_t
962 |
963 | PL_next_solution = _lib.PL_next_solution
964 | PL_next_solution.argtypes = [qid_t]
965 | PL_next_solution.restype = c_int
966 |
967 | PL_copy_term_ref = _lib.PL_copy_term_ref
968 | PL_copy_term_ref.argtypes = [term_t]
969 | PL_copy_term_ref.restype = term_t
970 |
971 | PL_get_list = _lib.PL_get_list
972 | PL_get_list.argtypes = [term_t, term_t, term_t]
973 | PL_get_list.restype = c_int
974 |
975 | PL_get_chars = _lib.PL_get_chars # FIXME
976 |
977 | PL_close_query = _lib.PL_close_query
978 | PL_close_query.argtypes = [qid_t]
979 | PL_close_query.restype = None
980 |
981 | PL_cut_query = _lib.PL_cut_query
982 | PL_cut_query.argtypes = [qid_t]
983 | PL_cut_query.restype = None
984 |
985 | PL_halt = _lib.PL_halt
986 | PL_halt.argtypes = [c_int]
987 | PL_halt.restype = None
988 |
989 | PL_cleanup = _lib.PL_cleanup
990 | PL_cleanup.restype = c_int
991 |
992 | PL_unify_integer = _lib.PL_unify_integer
993 | PL_unify_atom_chars = _lib.PL_unify_atom_chars
994 |
995 | PL_unify_float = _lib.PL_unify_float
996 | PL_unify_float.argtypes = [term_t, c_double]
997 | PL_unify_float.restype = c_int
998 |
999 | PL_unify_bool = _lib.PL_unify_bool
1000 | PL_unify_bool.argtypes = [term_t, c_int]
1001 | PL_unify_bool.restype = c_int
1002 |
1003 | PL_unify_list = _lib.PL_unify_list
1004 | PL_unify_list.argtypes = [term_t, term_t, term_t]
1005 | PL_unify_list.restype = c_int
1006 |
1007 | PL_unify_nil = _lib.PL_unify_nil
1008 | PL_unify_nil.argtypes = [term_t]
1009 | PL_unify_nil.restype = c_int
1010 |
1011 | PL_unify_atom = _lib.PL_unify_atom
1012 | PL_unify_atom.argtypes = [term_t, atom_t]
1013 | PL_unify_atom.restype = c_int
1014 |
1015 | PL_unify_atom_chars = _lib.PL_unify_atom_chars
1016 | PL_unify_atom_chars.argtypes = [term_t, c_char_p]
1017 | PL_unify_atom_chars.restype = c_int
1018 |
1019 | PL_unify_string_chars = _lib.PL_unify_string_chars
1020 | PL_unify_string_chars.argtypes = [term_t, c_char_p]
1021 | PL_unify_string_chars.restype = c_void_p
1022 |
1023 | PL_foreign_control = _lib.PL_foreign_control
1024 | PL_foreign_control.argtypes = [control_t]
1025 | PL_foreign_control.restypes = c_int
1026 |
1027 | PL_foreign_context_address = _lib.PL_foreign_context_address
1028 | PL_foreign_context_address.argtypes = [control_t]
1029 | PL_foreign_context_address.restypes = c_void_p
1030 |
1031 | PL_retry_address = _lib._PL_retry_address
1032 | PL_retry_address.argtypes = [c_void_p]
1033 | PL_retry_address.restypes = foreign_t
1034 |
1035 | PL_unify = _lib.PL_unify
1036 | PL_unify.restype = c_int
1037 |
1038 | PL_succeed = 1
1039 |
1040 | PL_unify_arg = _lib.PL_unify_arg
1041 | PL_unify_arg.argtypes = [c_int, term_t, term_t]
1042 | PL_unify_arg.restype = c_int
1043 |
1044 | # Verify types
1045 |
1046 | PL_term_type = _lib.PL_term_type
1047 | PL_term_type.argtypes = [term_t]
1048 | PL_term_type.restype = c_int
1049 |
1050 | PL_is_variable = _lib.PL_is_variable
1051 | PL_is_variable.argtypes = [term_t]
1052 | PL_is_variable.restype = c_int
1053 |
1054 | PL_is_ground = _lib.PL_is_ground
1055 | PL_is_ground.argtypes = [term_t]
1056 | PL_is_ground.restype = c_int
1057 |
1058 | PL_is_atom = _lib.PL_is_atom
1059 | PL_is_atom.argtypes = [term_t]
1060 | PL_is_atom.restype = c_int
1061 |
1062 | PL_is_integer = _lib.PL_is_integer
1063 | PL_is_integer.argtypes = [term_t]
1064 | PL_is_integer.restype = c_int
1065 |
1066 | PL_is_string = _lib.PL_is_string
1067 | PL_is_string.argtypes = [term_t]
1068 | PL_is_string.restype = c_int
1069 |
1070 | PL_is_float = _lib.PL_is_float
1071 | PL_is_float.argtypes = [term_t]
1072 | PL_is_float.restype = c_int
1073 |
1074 | PL_is_compound = _lib.PL_is_compound
1075 | PL_is_compound.argtypes = [term_t]
1076 | PL_is_compound.restype = c_int
1077 |
1078 | PL_is_functor = _lib.PL_is_functor
1079 | PL_is_functor.argtypes = [term_t, functor_t]
1080 | PL_is_functor.restype = c_int
1081 |
1082 | PL_is_list = _lib.PL_is_list
1083 | PL_is_list.argtypes = [term_t]
1084 | PL_is_list.restype = c_int
1085 |
1086 | PL_is_atomic = _lib.PL_is_atomic
1087 | PL_is_atomic.argtypes = [term_t]
1088 | PL_is_atomic.restype = c_int
1089 |
1090 | PL_is_number = _lib.PL_is_number
1091 | PL_is_number.argtypes = [term_t]
1092 | PL_is_number.restype = c_int
1093 |
1094 | PL_put_variable = _lib.PL_put_variable
1095 | PL_put_variable.argtypes = [term_t]
1096 | PL_put_variable.restype = None
1097 |
1098 | PL_put_integer = _lib.PL_put_integer
1099 | PL_put_integer.argtypes = [term_t, c_long]
1100 | PL_put_integer.restype = None
1101 |
1102 | PL_put_functor = _lib.PL_put_functor
1103 | PL_put_functor.argtypes = [term_t, functor_t]
1104 | PL_put_functor.restype = None
1105 |
1106 | PL_put_list = _lib.PL_put_list
1107 | PL_put_list.argtypes = [term_t]
1108 | PL_put_list.restype = None
1109 |
1110 | PL_put_nil = _lib.PL_put_nil
1111 | PL_put_nil.argtypes = [term_t]
1112 | PL_put_nil.restype = None
1113 |
1114 | PL_put_term = _lib.PL_put_term
1115 | PL_put_term.argtypes = [term_t, term_t]
1116 | PL_put_term.restype = None
1117 |
1118 | PL_cons_functor = _lib.PL_cons_functor # FIXME:
1119 |
1120 | PL_cons_functor_v = _lib.PL_cons_functor_v
1121 | PL_cons_functor_v.argtypes = [term_t, functor_t, term_t]
1122 | PL_cons_functor_v.restype = None
1123 |
1124 | PL_cons_list = _lib.PL_cons_list
1125 | PL_cons_list.argtypes = [term_t, term_t, term_t]
1126 | PL_cons_list.restype = None
1127 |
1128 | PL_exception = _lib.PL_exception
1129 | PL_exception.argtypes = [qid_t]
1130 | PL_exception.restype = term_t
1131 |
1132 | PL_register_foreign = _lib.PL_register_foreign
1133 | PL_register_foreign = check_strings(0, None)(PL_register_foreign)
1134 |
1135 | PL_register_foreign_in_module = _lib.PL_register_foreign_in_module
1136 | PL_register_foreign_in_module = check_strings([0, 1], None)(
1137 | PL_register_foreign_in_module
1138 | )
1139 |
1140 | PL_new_atom = _lib.PL_new_atom
1141 | PL_new_atom.argtypes = [c_char_p]
1142 | PL_new_atom.restype = atom_t
1143 |
1144 | PL_new_atom = check_strings(0, None)(PL_new_atom)
1145 |
1146 | PL_new_functor = _lib.PL_new_functor
1147 | PL_new_functor.argtypes = [atom_t, c_int]
1148 | PL_new_functor.restype = functor_t
1149 |
1150 | PL_compare = _lib.PL_compare
1151 | PL_compare.argtypes = [term_t, term_t]
1152 | PL_compare.restype = c_int
1153 |
1154 | PL_same_compound = _lib.PL_same_compound
1155 | PL_same_compound.argtypes = [term_t, term_t]
1156 | PL_same_compound.restype = c_int
1157 |
1158 | PL_record = _lib.PL_record
1159 | PL_record.argtypes = [term_t]
1160 | PL_record.restype = record_t
1161 |
1162 | PL_recorded = _lib.PL_recorded
1163 | PL_recorded.argtypes = [record_t, term_t]
1164 | PL_recorded.restype = None
1165 |
1166 | PL_erase = _lib.PL_erase
1167 | PL_erase.argtypes = [record_t]
1168 | PL_erase.restype = None
1169 |
1170 | PL_new_module = _lib.PL_new_module
1171 | PL_new_module.argtypes = [atom_t]
1172 | PL_new_module.restype = module_t
1173 |
1174 | PL_is_initialised = _lib.PL_is_initialised
1175 |
1176 | intptr_t = c_long
1177 | ssize_t = intptr_t
1178 | wint_t = c_uint
1179 |
1180 | PL_thread_self = _lib.PL_thread_self
1181 | PL_thread_self.restype = c_int
1182 |
1183 | PL_thread_attach_engine = _lib.PL_thread_attach_engine
1184 | PL_thread_attach_engine.argtypes = [c_void_p]
1185 | PL_thread_attach_engine.restype = c_int
1186 |
1187 |
1188 | class _mbstate_t_value(Union):
1189 | _fields_ = [("__wch", wint_t), ("__wchb", c_char * 4)]
1190 |
1191 |
1192 | class mbstate_t(Structure):
1193 | _fields_ = [("__count", c_int), ("__value", _mbstate_t_value)]
1194 |
1195 |
1196 | # stream related funcs
1197 | Sread_function = CFUNCTYPE(ssize_t, c_void_p, c_char_p, c_size_t)
1198 | Swrite_function = CFUNCTYPE(ssize_t, c_void_p, c_char_p, c_size_t)
1199 | Sseek_function = CFUNCTYPE(c_long, c_void_p, c_long, c_int)
1200 | Sseek64_function = CFUNCTYPE(c_int64, c_void_p, c_int64, c_int)
1201 | Sclose_function = CFUNCTYPE(c_int, c_void_p)
1202 | Scontrol_function = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p)
1203 |
1204 | # IOLOCK
1205 | IOLOCK = c_void_p
1206 |
1207 |
1208 | # IOFUNCTIONS
1209 | class IOFUNCTIONS(Structure):
1210 | _fields_ = [
1211 | ("read", Sread_function),
1212 | ("write", Swrite_function),
1213 | ("seek", Sseek_function),
1214 | ("close", Sclose_function),
1215 | ("seek64", Sseek64_function),
1216 | ("reserved", intptr_t * 2),
1217 | ]
1218 |
1219 |
1220 | # IOENC
1221 | (
1222 | ENC_UNKNOWN,
1223 | ENC_OCTET,
1224 | ENC_ASCII,
1225 | ENC_ISO_LATIN_1,
1226 | ENC_ANSI,
1227 | ENC_UTF8,
1228 | ENC_UNICODE_BE,
1229 | ENC_UNICODE_LE,
1230 | ENC_WCHAR,
1231 | ) = tuple(range(9))
1232 | IOENC = c_int
1233 |
1234 |
1235 | # IOPOS
1236 | class IOPOS(Structure):
1237 | _fields_ = [
1238 | ("byteno", c_int64),
1239 | ("charno", c_int64),
1240 | ("lineno", c_int),
1241 | ("linepos", c_int),
1242 | ("reserved", intptr_t * 2),
1243 | ]
1244 |
1245 |
1246 | # IOSTREAM
1247 | class IOSTREAM(Structure):
1248 | _fields_ = [
1249 | ("bufp", c_char_p),
1250 | ("limitp", c_char_p),
1251 | ("buffer", c_char_p),
1252 | ("unbuffer", c_char_p),
1253 | ("lastc", c_int),
1254 | ("magic", c_int),
1255 | ("bufsize", c_int),
1256 | ("flags", c_int),
1257 | ("posbuf", IOPOS),
1258 | ("position", POINTER(IOPOS)),
1259 | ("handle", c_void_p),
1260 | ("functions", IOFUNCTIONS),
1261 | ("locks", c_int),
1262 | ("mutex", IOLOCK),
1263 | ("closure_hook", CFUNCTYPE(None, c_void_p)),
1264 | ("closure", c_void_p),
1265 | ("timeout", c_int),
1266 | ("message", c_char_p),
1267 | ("encoding", IOENC),
1268 | ]
1269 |
1270 |
1271 | IOSTREAM._fields_.extend(
1272 | [("tee", IOSTREAM), ("mbstate", POINTER(mbstate_t)), ("reserved", intptr_t * 6)]
1273 | )
1274 |
1275 | Sopen_string = _lib.Sopen_string
1276 | Sopen_string.argtypes = [POINTER(IOSTREAM), c_char_p, c_size_t, c_char_p]
1277 | Sopen_string.restype = POINTER(IOSTREAM)
1278 |
1279 | Sclose = _lib.Sclose
1280 | Sclose.argtypes = [POINTER(IOSTREAM)]
1281 |
1282 | PL_unify_stream = _lib.PL_unify_stream
1283 | PL_unify_stream.argtypes = [term_t, POINTER(IOSTREAM)]
1284 |
1285 |
1286 | # create an exit hook which captures the exit code for our cleanup function
1287 | class ExitHook(object):
1288 | def __init__(self):
1289 | self.exit_code = None
1290 | self.exception = None
1291 |
1292 | def hook(self):
1293 | self._orig_exit = sys.exit
1294 | sys.exit = self.exit
1295 |
1296 | def exit(self, code=0):
1297 | self.exit_code = code
1298 | self._orig_exit(code)
1299 |
1300 |
1301 | _hook = ExitHook()
1302 | _hook.hook()
1303 |
1304 | _isCleaned = False
1305 | # create a property for Atom's delete method in order to avoid segmentation fault
1306 | cleaned = property(_isCleaned)
1307 |
1308 |
1309 | # register the cleanup function to be executed on system exit
1310 | @atexit.register
1311 | def cleanupProlog():
1312 | # only do something if prolog has been initialised
1313 | if PL_is_initialised(None, None):
1314 | # clean up the prolog system using the caught exit code
1315 | # if exit code is None, the program exits normally and we can use 0
1316 | # instead.
1317 | # TODO Prolog documentation says cleanup with code 0 may be interrupted
1318 | # If the program has come to an end the prolog system should not
1319 | # interfere with that. Therefore we may want to use 1 instead of 0.
1320 | PL_cleanup(int(_hook.exit_code or 0))
1321 | _isCleaned = True
1322 |
--------------------------------------------------------------------------------
/src/pyswip/easy.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip Contributors
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in all
11 | # copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | # SOFTWARE.
20 |
21 | import inspect
22 | from typing import Union, Callable, Optional
23 |
24 | from pyswip.core import (
25 | PL_new_atom,
26 | PL_register_atom,
27 | PL_atom_wchars,
28 | PL_get_atom,
29 | PL_unregister_atom,
30 | PL_new_term_ref,
31 | PL_compare,
32 | PL_get_chars,
33 | PL_copy_term_ref,
34 | PL_unify_atom,
35 | PL_unify_string_chars,
36 | PL_unify_integer,
37 | PL_unify_bool,
38 | PL_unify_float,
39 | PL_unify_list,
40 | PL_unify_nil,
41 | PL_term_type,
42 | PL_put_term,
43 | PL_new_functor,
44 | PL_functor_name,
45 | PL_functor_arity,
46 | PL_get_functor,
47 | PL_new_term_refs,
48 | PL_get_arg,
49 | PL_cons_functor_v,
50 | PL_put_atom_chars,
51 | PL_put_integer,
52 | PL_put_functor,
53 | PL_put_nil,
54 | PL_cons_list,
55 | PL_get_long,
56 | PL_get_float,
57 | PL_is_list,
58 | PL_get_list,
59 | PL_register_foreign_in_module,
60 | PL_call,
61 | PL_new_module,
62 | PL_pred,
63 | PL_open_query,
64 | PL_next_solution,
65 | PL_cut_query,
66 | PL_close_query,
67 | PL_VARIABLE,
68 | PL_STRINGS_MARK,
69 | PL_TERM,
70 | PL_DICT,
71 | PL_ATOM,
72 | PL_STRING,
73 | PL_INTEGER,
74 | PL_FLOAT,
75 | PL_Q_NODEBUG,
76 | PL_Q_CATCH_EXCEPTION,
77 | PL_FA_NONDETERMINISTIC,
78 | CVT_VARIABLE,
79 | BUF_RING,
80 | REP_UTF8,
81 | CVT_ATOM,
82 | CVT_STRING,
83 | CFUNCTYPE,
84 | cleaned,
85 | cast,
86 | c_size_t,
87 | byref,
88 | c_void_p,
89 | atom_t,
90 | create_string_buffer,
91 | c_char_p,
92 | functor_t,
93 | c_int,
94 | c_long,
95 | c_double,
96 | foreign_t,
97 | term_t,
98 | control_t,
99 | module_t,
100 | )
101 |
102 |
103 | integer_types = (int,)
104 |
105 |
106 | class InvalidTypeError(TypeError):
107 | def __init__(self, *args):
108 | type_ = args and args[0] or "Unknown"
109 | msg = f"Term is expected to be of type: '{type_}'"
110 | Exception.__init__(self, msg, *args)
111 |
112 |
113 | class ArgumentTypeError(Exception):
114 | """
115 | Thrown when an argument has the wrong type.
116 | """
117 |
118 | def __init__(self, expected, got):
119 | msg = f"Expected an argument of type '{expected}' but got '{got}'"
120 | Exception.__init__(self, msg)
121 |
122 |
123 | class Atom(object):
124 | __slots__ = "handle", "chars"
125 |
126 | def __init__(self, handleOrChars, chars=None):
127 | """Create an atom.
128 | ``handleOrChars``: handle or string of the atom.
129 | """
130 |
131 | if isinstance(handleOrChars, str):
132 | self.handle = PL_new_atom(handleOrChars)
133 | self.chars = handleOrChars
134 | else:
135 | self.handle = handleOrChars
136 | PL_register_atom(self.handle)
137 | if chars is None:
138 | slen = c_size_t()
139 | self.chars = PL_atom_wchars(self.handle, byref(slen))
140 | else: # WA: PL_atom_wchars can fail to return correct string
141 | self.chars = chars
142 |
143 | def fromTerm(cls, term):
144 | """Create an atom from a Term or term handle."""
145 |
146 | if isinstance(term, Term):
147 | term = term.handle
148 | elif not isinstance(term, (c_void_p, integer_types)):
149 | raise ArgumentTypeError((str(Term), str(c_void_p)), str(type(term)))
150 |
151 | a = atom_t()
152 | if PL_get_atom(term, byref(a)):
153 | return cls(a.value, getAtomChars(term))
154 |
155 | fromTerm = classmethod(fromTerm)
156 |
157 | def __del__(self):
158 | if not cleaned:
159 | PL_unregister_atom(self.handle)
160 |
161 | def get_value(self):
162 | ret = self.chars
163 | if not isinstance(ret, str):
164 | ret = ret.decode()
165 | return ret
166 |
167 | value = property(get_value)
168 |
169 | def __str__(self):
170 | if self.chars is not None:
171 | return self.value
172 | else:
173 | return self.__repr__()
174 |
175 | def __repr__(self):
176 | return str(self.handle).join(["Atom('", "')"])
177 |
178 | def __eq__(self, other):
179 | if type(self) != type(other):
180 | return False
181 | else:
182 | return self.handle == other.handle
183 |
184 | def __hash__(self):
185 | return self.handle
186 |
187 |
188 | class Term(object):
189 | __slots__ = "handle", "chars", "__value", "a0"
190 |
191 | def __init__(self, handle=None, a0=None):
192 | if handle:
193 | # self.handle = PL_copy_term_ref(handle)
194 | self.handle = handle
195 | else:
196 | self.handle = PL_new_term_ref()
197 | self.chars = None
198 | self.a0 = a0
199 |
200 | def __invert__(self):
201 | return _not(self)
202 |
203 | def get_value(self):
204 | pass
205 |
206 | def __eq__(self, other):
207 | if type(self) != type(other):
208 | return False
209 | else:
210 | return PL_compare(self.handle, other.handle) == 0
211 |
212 | def __hash__(self):
213 | return self.handle
214 |
215 |
216 | class Variable:
217 | __slots__ = "handle", "chars"
218 |
219 | def __init__(self, handle=None, name=None):
220 | self.chars = None
221 | if name:
222 | self.chars = name
223 | if handle:
224 | self.handle = handle
225 | s = create_string_buffer(b"\00" * 64) # FIXME:
226 | ptr = cast(s, c_char_p)
227 | if PL_get_chars(handle, byref(ptr), CVT_VARIABLE | BUF_RING | REP_UTF8):
228 | self.chars = ptr.value
229 | else:
230 | self.handle = PL_new_term_ref()
231 | # PL_put_variable(self.handle)
232 | if (self.chars is not None) and not isinstance(self.chars, str):
233 | self.chars = self.chars.decode()
234 |
235 | def unify(self, value):
236 | if self.handle is None:
237 | t = PL_new_term_ref(self.handle)
238 | else:
239 | t = PL_copy_term_ref(self.handle)
240 |
241 | self._fun(value, t)
242 | self.handle = t
243 |
244 | def _fun(self, value, t):
245 | if type(value) == Atom:
246 | fun = PL_unify_atom
247 | value = value.handle
248 | elif isinstance(value, str):
249 | fun = PL_unify_string_chars
250 | value = value.encode()
251 | elif type(value) == int:
252 | fun = PL_unify_integer
253 | elif type(value) == bool:
254 | fun = PL_unify_bool
255 | elif type(value) == float:
256 | fun = PL_unify_float
257 | elif type(value) == list:
258 | fun = PL_unify_list
259 | else:
260 | raise TypeError(
261 | "Cannot unify {} with value {} due to the value unknown type {}".format(
262 | self, value, type(value)
263 | )
264 | )
265 |
266 | if type(value) == list:
267 | a = PL_new_term_ref(self.handle)
268 | list_term = t
269 | for element in value:
270 | tail_term = PL_new_term_ref(self.handle)
271 | fun(list_term, a, tail_term)
272 | self._fun(element, a)
273 | list_term = tail_term
274 | PL_unify_nil(list_term)
275 | else:
276 | fun(t, value)
277 |
278 | def get_value(self):
279 | return getTerm(self.handle)
280 |
281 | value = property(get_value, unify)
282 |
283 | def unified(self):
284 | return PL_term_type(self.handle) == PL_VARIABLE
285 |
286 | def __str__(self):
287 | if self.chars is not None:
288 | return self.chars
289 | else:
290 | return self.__repr__()
291 |
292 | def __repr__(self):
293 | return f"Variable({self.handle})"
294 |
295 | def put(self, term):
296 | # PL_put_variable(term)
297 | PL_put_term(term, self.handle)
298 |
299 | def __eq__(self, other):
300 | if type(self) != type(other):
301 | return False
302 | else:
303 | return PL_compare(self.handle, other.handle) == 0
304 |
305 | def __hash__(self):
306 | return self.handle
307 |
308 |
309 | class Functor(object):
310 | __slots__ = "handle", "name", "arity", "args", "__value", "a0"
311 | func = {}
312 |
313 | def __init__(self, handleOrName, arity=1, args=None, a0=None):
314 | """Create a functor.
315 | ``handleOrName``: functor handle, a string or an atom.
316 | """
317 |
318 | self.args = args or []
319 | self.arity = arity
320 | self.a0 = a0
321 |
322 | if isinstance(handleOrName, str):
323 | self.name = Atom(handleOrName)
324 | self.handle = PL_new_functor(self.name.handle, arity)
325 | self.__value = "Functor%d" % self.handle
326 | elif isinstance(handleOrName, Atom):
327 | self.name = handleOrName
328 | self.handle = PL_new_functor(self.name.handle, arity)
329 | self.__value = "Functor%d" % self.handle
330 | else:
331 | self.handle = handleOrName
332 | self.name = Atom(PL_functor_name(self.handle))
333 | self.arity = PL_functor_arity(self.handle)
334 | try:
335 | self.__value = self.func[self.handle](self.arity, *self.args)
336 | except KeyError:
337 | self.__value = str(self)
338 |
339 | def fromTerm(cls, term):
340 | """Create a functor from a Term or term handle."""
341 |
342 | if isinstance(term, Term):
343 | term = term.handle
344 | elif not isinstance(term, (c_void_p, integer_types)):
345 | raise ArgumentTypeError((str(Term), str(int)), str(type(term)))
346 |
347 | f = functor_t()
348 | if PL_get_functor(term, byref(f)):
349 | # get args
350 | args = []
351 | arity = PL_functor_arity(f.value)
352 | # let's have all args be consecutive
353 | a0 = PL_new_term_refs(arity)
354 | for i, a in enumerate(range(1, arity + 1)):
355 | if PL_get_arg(a, term, a0 + i):
356 | args.append(getTerm(a0 + i))
357 |
358 | return cls(f.value, args=args, a0=a0)
359 |
360 | fromTerm = classmethod(fromTerm)
361 |
362 | @property
363 | def value(self):
364 | return self.__value
365 |
366 | def __call__(self, *args):
367 | assert self.arity == len(args) # FIXME: Put a decent error message
368 | a = PL_new_term_refs(len(args))
369 | for i, arg in enumerate(args):
370 | putTerm(a + i, arg)
371 |
372 | t = PL_new_term_ref()
373 | PL_cons_functor_v(t, self.handle, a)
374 | return Term(t)
375 |
376 | def __str__(self):
377 | if self.name is not None and self.arity is not None:
378 | return "%s(%s)" % (self.name, ", ".join([str(arg) for arg in self.args]))
379 | else:
380 | return self.__repr__()
381 |
382 | def __repr__(self):
383 | return "".join(
384 | [
385 | "Functor(",
386 | ",".join(str(x) for x in [self.handle, self.arity] + self.args),
387 | ")",
388 | ]
389 | )
390 |
391 | def __eq__(self, other):
392 | if type(self) != type(other):
393 | return False
394 | else:
395 | return PL_compare(self.handle, other.handle) == 0
396 |
397 | def __hash__(self):
398 | return self.handle
399 |
400 |
401 | def _unifier(arity, *args):
402 | assert arity == 2
403 | try:
404 | return {args[0].value: args[1].value}
405 | except AttributeError:
406 | return {args[0].value: args[1]}
407 |
408 |
409 | _unify = Functor("=", 2)
410 | Functor.func[_unify.handle] = _unifier
411 | _not = Functor("not", 1)
412 | _comma = Functor(",", 2)
413 |
414 |
415 | def putTerm(term, value):
416 | if isinstance(value, Term):
417 | PL_put_term(term, value.handle)
418 | elif isinstance(value, str):
419 | PL_put_atom_chars(term, value)
420 | elif isinstance(value, int):
421 | PL_put_integer(term, value)
422 | elif isinstance(value, Variable):
423 | value.put(term)
424 | elif isinstance(value, list):
425 | putList(term, value)
426 | elif isinstance(value, Functor):
427 | PL_put_functor(term, value.handle)
428 | else:
429 | raise Exception(f"Not implemented for type: {type(value)}")
430 |
431 |
432 | def putList(l, ls): # noqa: E741
433 | PL_put_nil(l)
434 | for item in reversed(ls):
435 | a = PL_new_term_ref() # PL_new_term_refs(len(ls))
436 | putTerm(a, item)
437 | PL_cons_list(l, a, l)
438 |
439 |
440 | def getAtomChars(t):
441 | """If t is an atom, return it as a string, otherwise raise InvalidTypeError."""
442 | s = c_char_p()
443 | if PL_get_chars(t, byref(s), CVT_ATOM | REP_UTF8):
444 | return s.value
445 | else:
446 | raise InvalidTypeError("atom")
447 |
448 |
449 | def getAtom(t):
450 | """If t is an atom, return it , otherwise raise InvalidTypeError."""
451 | return Atom.fromTerm(t)
452 |
453 |
454 | def getBool(t):
455 | """If t is of type bool, return it, otherwise raise InvalidTypeError."""
456 | b = c_int()
457 | if PL_get_long(t, byref(b)):
458 | return bool(b.value)
459 | else:
460 | raise InvalidTypeError("bool")
461 |
462 |
463 | def getLong(t):
464 | """If t is of type long, return it, otherwise raise InvalidTypeError."""
465 | i = c_long()
466 | if PL_get_long(t, byref(i)):
467 | return i.value
468 | else:
469 | raise InvalidTypeError("long")
470 |
471 |
472 | getInteger = getLong # just an alias for getLong
473 |
474 |
475 | def getFloat(t):
476 | """If t is of type float, return it, otherwise raise InvalidTypeError."""
477 | d = c_double()
478 | if PL_get_float(t, byref(d)):
479 | return d.value
480 | else:
481 | raise InvalidTypeError("float")
482 |
483 |
484 | def getString(t):
485 | """If t is of type string, return it, otherwise raise InvalidTypeError."""
486 | s = c_char_p()
487 | if PL_get_chars(t, byref(s), REP_UTF8 | CVT_STRING):
488 | return s.value
489 | else:
490 | raise InvalidTypeError("string")
491 |
492 |
493 | def getTerm(t):
494 | if t is None:
495 | return None
496 | with PL_STRINGS_MARK():
497 | p = PL_term_type(t)
498 | if p < PL_TERM:
499 | res = _getterm_router[p](t)
500 | elif PL_is_list(t):
501 | res = getList(t)
502 | elif p == PL_DICT:
503 | res = getDict(t)
504 | else:
505 | res = getFunctor(t)
506 | return res
507 |
508 |
509 | def getDict(term):
510 | """
511 | Return term as a dictionary.
512 | """
513 |
514 | if isinstance(term, Term):
515 | term = term.handle
516 | elif not isinstance(term, (c_void_p, int)):
517 | raise ArgumentTypeError((str(Term), str(int)), str(type(term)))
518 |
519 | f = functor_t()
520 | if PL_get_functor(term, byref(f)):
521 | args = []
522 | arity = PL_functor_arity(f.value)
523 | a0 = PL_new_term_refs(arity)
524 | for i, a in enumerate(range(1, arity + 1)):
525 | if PL_get_arg(a, term, a0 + i):
526 | args.append(getTerm(a0 + i))
527 | else:
528 | raise Exception("Missing arg")
529 |
530 | it = iter(args[1:])
531 | d = {k.value: v for v, k in zip(it, it)}
532 |
533 | return d
534 | else:
535 | res = getFunctor(term)
536 | return res
537 |
538 |
539 | def getList(x):
540 | """
541 | Return t as a list.
542 | """
543 |
544 | t = PL_copy_term_ref(x)
545 | head = PL_new_term_ref()
546 | result = []
547 | while PL_get_list(t, head, t):
548 | result.append(getTerm(head))
549 | head = PL_new_term_ref()
550 |
551 | return result
552 |
553 |
554 | def getFunctor(t):
555 | """Return t as a functor"""
556 | return Functor.fromTerm(t)
557 |
558 |
559 | def getVariable(t):
560 | return Variable(t)
561 |
562 |
563 | _getterm_router = {
564 | PL_VARIABLE: getVariable,
565 | PL_ATOM: getAtom,
566 | PL_STRING: getString,
567 | PL_INTEGER: getInteger,
568 | PL_FLOAT: getFloat,
569 | PL_TERM: getTerm,
570 | }
571 |
572 | arities = {}
573 |
574 |
575 | def _callbackWrapper(arity=1, nondeterministic=False):
576 | res = arities.get((arity, nondeterministic))
577 | if res is None:
578 | if nondeterministic:
579 | res = CFUNCTYPE(*([foreign_t] + [term_t] * arity + [control_t]))
580 | else:
581 | res = CFUNCTYPE(*([foreign_t] + [term_t] * arity))
582 | arities[(arity, nondeterministic)] = res
583 | return res
584 |
585 |
586 | funwraps = {}
587 |
588 |
589 | def _foreignWrapper(fun, nondeterministic=False):
590 | res = funwraps.get(fun)
591 | if res is None:
592 |
593 | def wrapper(*args):
594 | if nondeterministic:
595 | args = [getTerm(arg) for arg in args[:-1]] + [args[-1]]
596 | else:
597 | args = [getTerm(arg) for arg in args]
598 | r = fun(*args)
599 | return (r is None) and True or r
600 |
601 | res = wrapper
602 | funwraps[fun] = res
603 | return res
604 |
605 |
606 | cwraps = []
607 |
608 |
609 | def registerForeign(
610 | func: Callable, name: str = "", arity: Optional[int] = None, flags: int = 0
611 | ):
612 | """
613 | Registers a Python callable as a Prolog predicate
614 |
615 | :param func: Callable to be registered. The callable should return a value in ``foreign_t``, ``True`` or ``False``.
616 | :param name: Name of the callable. If the name is not specified, it is derived from ``func.__name__``.
617 | :param arity: Number of parameters of the callable. If not specified, it is derived from the callable signature.
618 | :param flags: Only supported flag is ``PL_FA_NONDETERMINISTIC``.
619 |
620 | See: `PL_register_foreign `_.
621 |
622 | .. Note::
623 | This function is deprecated.
624 | Use :py:meth:`Prolog.register_foreign` instead.
625 | """
626 | if not callable(func):
627 | raise ValueError("func is not callable")
628 | nondeterministic = bool(flags & PL_FA_NONDETERMINISTIC)
629 | if arity is None:
630 | # backward compatibility
631 | if hasattr(func, "arity"):
632 | arity = func.arity
633 | else:
634 | arity = len(inspect.signature(func).parameters)
635 | if nondeterministic:
636 | arity -= 1
637 | if not name:
638 | name = func.__name__
639 |
640 | cwrap = _callbackWrapper(arity, nondeterministic)
641 | fwrap = _foreignWrapper(func, nondeterministic)
642 | fwrap2 = cwrap(fwrap)
643 | cwraps.append(fwrap2)
644 | return PL_register_foreign_in_module(None, name, arity, fwrap2, flags)
645 |
646 |
647 | newTermRef = PL_new_term_ref
648 |
649 |
650 | def newTermRefs(count):
651 | a = PL_new_term_refs(count)
652 | return list(range(a, a + count))
653 |
654 |
655 | def call(*terms, **kwargs):
656 | """Call term in module.
657 | ``term``: a Term or term handle
658 | """
659 | for kwarg in kwargs:
660 | if kwarg not in ["module"]:
661 | raise KeyError
662 |
663 | module = kwargs.get("module", None)
664 |
665 | t = terms[0]
666 | for tx in terms[1:]:
667 | t = _comma(t, tx)
668 |
669 | return PL_call(t.handle, module)
670 |
671 |
672 | def newModule(name: Union[str, Atom]) -> module_t:
673 | """
674 | Returns a module with the given name.
675 |
676 | The module is created if it does not exist.
677 |
678 | .. NOTE::
679 | This function is deprecated. Use ``module`` instead.
680 |
681 | :param name: Name of the module
682 | """
683 | return module(name)
684 |
685 |
686 | def module(name: Union[str, Atom]) -> module_t:
687 | """
688 | Returns a module with the given name.
689 |
690 | The module is created if it does not exist.
691 |
692 | :param name: Name of the module
693 | """
694 | if isinstance(name, str):
695 | name = Atom(name)
696 | return PL_new_module(name.handle)
697 |
698 |
699 | class Query(object):
700 | qid = None
701 | fid = None
702 |
703 | def __init__(self, *terms, **kwargs):
704 | for key in kwargs:
705 | if key not in ["flags", "module"]:
706 | raise Exception(f"Invalid kwarg: {key}", key)
707 |
708 | flags = kwargs.get("flags", PL_Q_NODEBUG | PL_Q_CATCH_EXCEPTION)
709 | module = kwargs.get("module", None)
710 |
711 | t = terms[0]
712 | for tx in terms[1:]:
713 | t = _comma(t, tx)
714 |
715 | f = Functor.fromTerm(t)
716 | p = PL_pred(f.handle, module)
717 | Query.qid = PL_open_query(module, flags, p, f.a0)
718 |
719 | def nextSolution():
720 | return PL_next_solution(Query.qid)
721 |
722 | nextSolution = staticmethod(nextSolution)
723 |
724 | def cutQuery():
725 | PL_cut_query(Query.qid)
726 |
727 | cutQuery = staticmethod(cutQuery)
728 |
729 | def closeQuery():
730 | if Query.qid is not None:
731 | PL_close_query(Query.qid)
732 | Query.qid = None
733 |
734 | closeQuery = staticmethod(closeQuery)
735 |
--------------------------------------------------------------------------------
/src/pyswip/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuce/pyswip/0e9a952c140840e451d6f3bf05d4d6ca0311f59b/src/pyswip/examples/__init__.py
--------------------------------------------------------------------------------
/src/pyswip/examples/coins.pl:
--------------------------------------------------------------------------------
1 |
2 | % Coins -- 2007 by Yuce Tekol
3 |
4 | :- use_module(library('bounds')).
5 |
6 | coins(Count, Total, Solution) :-
7 | % A=1, B=5, C=10, D=50, E=100
8 | Solution = [A, B, C, D, E],
9 |
10 | Av is 1,
11 | Bv is 5,
12 | Cv is 10,
13 | Dv is 50,
14 | Ev is 100,
15 |
16 | Aup is Total // Av,
17 | Bup is Total // Bv,
18 | Cup is Total // Cv,
19 | Dup is Total // Dv,
20 | Eup is Total // Ev,
21 |
22 | A in 0..Aup,
23 | B in 0..Bup,
24 | C in 0..Cup,
25 | D in 0..Dup,
26 | E in 0..Eup,
27 |
28 | VA #= A*Av,
29 | VB #= B*Bv,
30 | VC #= C*Cv,
31 | VD #= D*Dv,
32 | VE #= E*Ev,
33 |
34 | sum(Solution, #=, Count),
35 | VA + VB + VC + VD + VE #= Total,
36 |
37 | label(Solution).
38 |
39 |
--------------------------------------------------------------------------------
/src/pyswip/examples/coins.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2018 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 | from typing import List, Dict
22 |
23 | # 100 coins must sum to $5.00
24 |
25 | from pyswip.prolog import Prolog
26 |
27 |
28 | __all__ = "solve", "prolog_source"
29 |
30 | _PROLOG_FILE = "coins.pl"
31 |
32 | Prolog.consult(_PROLOG_FILE, relative_to=__file__)
33 |
34 |
35 | def solve(
36 | *, coin_count: int = 100, total_cents: int = 500, max_solutions: int = 1
37 | ) -> List[Dict[int, int]]:
38 | """
39 | Solves the coins problem.
40 |
41 | Finds and returns combinations of ``coin_count`` coins that makes ``total`` cents.
42 |
43 | :param coin_count: Number of coins
44 | :param total_cents: Total cent value of coins
45 | """
46 | cents = [1, 5, 10, 50, 100]
47 | query = Prolog.query(
48 | "coins(%p, %p, Solution)", coin_count, total_cents, maxresult=max_solutions
49 | )
50 | return [
51 | {cent: count for cent, count in zip(cents, soln["Solution"])} for soln in query
52 | ]
53 |
54 |
55 | def prolog_source() -> str:
56 | """
57 | Returns the Prolog source file that solves the coins problem.
58 | """
59 | from pathlib import Path
60 |
61 | path = Path(__file__).parent / _PROLOG_FILE
62 | with open(path) as f:
63 | return f.read()
64 |
65 |
66 | def main():
67 | import argparse
68 |
69 | parser = argparse.ArgumentParser()
70 | parser.add_argument("-c", "--count", type=int, default=100)
71 | parser.add_argument("-t", "--total", type=int, default=500)
72 | parser.add_argument("-s", "--solutions", type=int, default=1)
73 | args = parser.parse_args()
74 | print(f"{args.count} coins must sum to ${args.total/100}:\n")
75 | solns = solve(
76 | coin_count=args.count, total_cents=args.total, max_solutions=args.solutions
77 | )
78 | for i, soln in enumerate(solns, start=1):
79 | text = " + ".join(
80 | f"{count}x{cent} cent(s)" for cent, count in soln.items() if count
81 | )
82 | print(f"{i}. {text}")
83 |
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/src/pyswip/examples/hanoi.pl:
--------------------------------------------------------------------------------
1 |
2 | % Towers of Hanoi
3 | % Based on: http://en.wikipedia.org/wiki/Prolog
4 |
5 | hanoi(N) :-
6 | move(N, left, right, center).
7 |
8 | move(0, _, _, _) :-
9 | !.
10 | move(N, A, B, C) :-
11 | M is N-1,
12 | move(M, A, C, B),
13 | notify([A,B]),
14 | move(M, C, B, A).
15 |
--------------------------------------------------------------------------------
/src/pyswip/examples/hanoi.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2018 Yüce Tekol
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | from collections import deque
23 | from typing import IO
24 |
25 | from pyswip.prolog import Prolog
26 |
27 |
28 | __all__ = "solve", "prolog_source"
29 |
30 | _PROLOG_FILE = "hanoi.pl"
31 |
32 | Prolog.consult(_PROLOG_FILE, relative_to=__file__)
33 |
34 |
35 | def make_notify_function(file):
36 | state = {"step": 1}
37 |
38 | def f(from_to):
39 | frm, to = from_to
40 | print(f"{state['step']}. Move disk from {frm} pole to {to} pole.", file=file)
41 | state["step"] += 1
42 |
43 | return f
44 |
45 |
46 | class Notifier:
47 | def __init__(self, fun):
48 | self.fun = fun
49 |
50 | def notify(self, t):
51 | return not self.fun(t)
52 |
53 |
54 | class Tower:
55 | def __init__(self, disk_count=3, file=None):
56 | if disk_count < 1 or disk_count > 9:
57 | raise ValueError("disk_count must be between 1 and 9")
58 | self.disk_count = disk_count
59 | self.file = file
60 | self.disks = dict(
61 | left=deque(range(disk_count, 0, -1)),
62 | center=deque(),
63 | right=deque(),
64 | )
65 | self.started = False
66 | self.step = 0
67 |
68 | def draw(self) -> None:
69 | print("\n Step", self.step, file=self.file)
70 | print(file=self.file)
71 | for i in range(self.disk_count):
72 | n = self.disk_count - i - 1
73 | print(" ", end=" ", file=self.file)
74 | for pole in ["left", "center", "right"]:
75 | if len(self.disks[pole]) - n > 0:
76 | print(self.disks[pole][n], end=" ", file=self.file)
77 | else:
78 | print(" ", end=" ", file=self.file)
79 | print(file=self.file)
80 | print("-" * 9, file=self.file)
81 | print(" ", "L", "C", "R", file=self.file)
82 |
83 | def move(self, r) -> None:
84 | if not self.started:
85 | self.draw()
86 | self.started = True
87 | self.disks[str(r[1])].append(self.disks[str(r[0])].pop())
88 | self.step += 1
89 | self.draw()
90 |
91 |
92 | def solve(disk_count: int = 3, simple: bool = False, file: IO = None) -> None:
93 | """
94 | Solves the Towers of Hanoi problem.
95 |
96 | :param disk_count:
97 | Number of disks to use
98 | :param simple:
99 | If set to ``True``, only the moves are printed.
100 | Otherwise all states are drawn.
101 | :param file:
102 | The file-like object to output the steps of the solution.
103 | By default stdout is used.
104 |
105 | >>> solve(3, simple=True)
106 | 1. Move disk from left pole to right pole.
107 | 2. Move disk from left pole to center pole.
108 | 3. Move disk from right pole to center pole.
109 | 4. Move disk from left pole to right pole.
110 | 5. Move disk from center pole to left pole.
111 | 6. Move disk from center pole to right pole.
112 | 7. Move disk from left pole to right pole.
113 | """
114 | if simple:
115 | Prolog.register_foreign(make_notify_function(file), name="notify")
116 | else:
117 | tower = Tower(disk_count, file=file)
118 | notifier = Notifier(tower.move)
119 | Prolog.register_foreign(notifier.notify)
120 | list(Prolog.query("hanoi(%p)", disk_count))
121 |
122 |
123 | def prolog_source() -> str:
124 | """
125 | Returns the Prolog source file that solves the Towers of Hanoi problem.
126 | """
127 | from pathlib import Path
128 |
129 | path = Path(__file__).parent / _PROLOG_FILE
130 | with open(path) as f:
131 | return f.read()
132 |
133 |
134 | def main():
135 | import argparse
136 |
137 | parser = argparse.ArgumentParser()
138 | parser.add_argument("disk_count", type=int, choices=list(range(1, 10)))
139 | parser.add_argument("-s", "--simple", action="store_true")
140 | args = parser.parse_args()
141 | solve(args.disk_count, simple=args.simple)
142 |
143 |
144 | if __name__ == "__main__":
145 | main()
146 |
--------------------------------------------------------------------------------
/src/pyswip/examples/sudoku.pl:
--------------------------------------------------------------------------------
1 |
2 | % Prolog Sudoku Solver (C) 2007 Markus Triska (triska@gmx.at)
3 | % Public domain code.
4 |
5 | :- use_module(library(bounds)).
6 |
7 | % Pss is a list of lists representing the game board.
8 |
9 | sudoku(Pss) :-
10 | flatten(Pss, Ps),
11 | Ps in 1..9,
12 | maplist(all_different, Pss),
13 | Pss = [R1,R2,R3,R4,R5,R6,R7,R8,R9],
14 | columns(R1, R2, R3, R4, R5, R6, R7, R8, R9),
15 | blocks(R1, R2, R3), blocks(R4, R5, R6), blocks(R7, R8, R9),
16 | label(Ps).
17 |
18 | columns([], [], [], [], [], [], [], [], []).
19 | columns([A|As],[B|Bs],[C|Cs],[D|Ds],[E|Es],[F|Fs],[G|Gs],[H|Hs],[I|Is]) :-
20 | all_different([A,B,C,D,E,F,G,H,I]),
21 | columns(As, Bs, Cs, Ds, Es, Fs, Gs, Hs, Is).
22 |
23 | blocks([], [], []).
24 | blocks([X1,X2,X3|R1], [X4,X5,X6|R2], [X7,X8,X9|R3]) :-
25 | all_different([X1,X2,X3,X4,X5,X6,X7,X8,X9]),
26 | blocks(R1, R2, R3).
27 |
--------------------------------------------------------------------------------
/src/pyswip/examples/sudoku.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2024 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | """
25 | Sudoku example
26 |
27 | You can run this module using::
28 |
29 | $ python3 -m pyswip.examples.sudoku
30 | """
31 |
32 | from typing import List, Union, Literal, Optional, IO
33 | from io import StringIO
34 |
35 | from pyswip.prolog import Prolog
36 |
37 | __all__ = "Matrix", "prolog_source", "sample_puzzle", "solve"
38 |
39 | _DIMENSION = 9
40 | _PROLOG_FILE = "sudoku.pl"
41 |
42 |
43 | Prolog.consult(_PROLOG_FILE, relative_to=__file__)
44 |
45 |
46 | class Matrix:
47 | """Represents a 9x9 Sudoku puzzle"""
48 |
49 | def __init__(self, matrix: List[List[int]]) -> None:
50 | if not matrix:
51 | raise ValueError("matrix must be given")
52 | if len(matrix) != _DIMENSION:
53 | raise ValueError("Matrix dimension must be 9")
54 | self._dimension = len(matrix)
55 | self._validate(self._dimension, matrix)
56 | self.matrix = matrix
57 |
58 | @classmethod
59 | def from_text(cls, text: str) -> "Matrix":
60 | """
61 | Create a Matrix from the given string
62 |
63 | The following are valid characters in the string:
64 |
65 | * `.`: Blank column
66 | * `1-9`: Numbers
67 |
68 | The text must contain exactly 9 rows and 9 columns.
69 | Each row ends with a newline character.
70 | You can use blank lines and spaces/tabs between columns.
71 |
72 | :param text: The text to use for creating the Matrix
73 |
74 | >>> puzzle = Matrix.from_text('''
75 | ... . . 5 . 7 . 2 6 8
76 | ... . . 4 . . 2 . . .
77 | ... . . 1 . 9 . . . .
78 | ... . 8 . . . . 1 . .
79 | ... . 2 . 9 . . . 7 .
80 | ... . . 6 . . . . 3 .
81 | ... . . 2 . 4 . 7 . .
82 | ... . . . 5 . . 9 . .
83 | ... 9 5 7 . 3 . . . .
84 | ... ''')
85 | """
86 | lines = [row for line in text.strip().split("\n") if (row := line.strip())]
87 | dimension = len(lines)
88 | rows = []
89 | for i, line in enumerate(lines):
90 | cols = line.split()
91 | if len(cols) != dimension:
92 | raise ValueError(
93 | f"All rows must have {dimension} columns, line {i+1} has {len(cols)}"
94 | )
95 | rows.append([0 if x == "." else int(x) for x in cols])
96 | return cls(rows)
97 |
98 | @classmethod
99 | def _validate(cls, dimension: int, matrix: List[List[int]]):
100 | if len(matrix) != dimension:
101 | raise ValueError(f"Matrix must have {dimension} rows, it has {len(matrix)}")
102 | for i, row in enumerate(matrix):
103 | if len(row) != dimension:
104 | raise ValueError(
105 | f"All rows must have {dimension} columns, row {i+1} has {len(row)}"
106 | )
107 |
108 | def __len__(self) -> int:
109 | return self._dimension
110 |
111 | def __str__(self) -> str:
112 | sio = StringIO()
113 | self.pretty_print(file=sio)
114 | return sio.getvalue()
115 |
116 | def __repr__(self) -> str:
117 | return str(self.matrix)
118 |
119 | def pretty_print(self, *, file: Optional[IO] = None) -> None:
120 | """
121 | Prints the matrix as a grid
122 |
123 | :param file: The file to use for printing
124 |
125 | >>> import sys
126 | >>> puzzle = sample_puzzle()
127 | >>> puzzle.pretty_print(file=sys.stdout)
128 | . . 5 . 7 . 2 6 8
129 | . . 4 . . 2 . . .
130 | . . 1 . 9 . . . .
131 | . 8 . . . . 1 . .
132 | . 2 . 9 . . . 7 .
133 | . . 6 . . . . 3 .
134 | . . 2 . 4 . 7 . .
135 | . . . 5 . . 9 . .
136 | 9 5 7 . 3 . . . .
137 | """
138 | for row in self.matrix:
139 | row = " ".join(str(x or ".") for x in row)
140 | print(row, file=file)
141 |
142 |
143 | def solve(matrix: Matrix) -> Union[Matrix, Literal[False]]:
144 | """
145 | Solves the given Sudoku puzzle
146 |
147 | :param matrix: The matrix that contains the Sudoku puzzle
148 |
149 | >>> puzzle = sample_puzzle()
150 | >>> print(puzzle)
151 | . . 5 . 7 . 2 6 8
152 | . . 4 . . 2 . . .
153 | . . 1 . 9 . . . .
154 | . 8 . . . . 1 . .
155 | . 2 . 9 . . . 7 .
156 | . . 6 . . . . 3 .
157 | . . 2 . 4 . 7 . .
158 | . . . 5 . . 9 . .
159 | 9 5 7 . 3 . . . .
160 |
161 | >>> print(solve(puzzle))
162 | 3 9 5 4 7 1 2 6 8
163 | 8 7 4 6 5 2 3 9 1
164 | 2 6 1 3 9 8 5 4 7
165 | 5 8 9 7 6 3 1 2 4
166 | 1 2 3 9 8 4 6 7 5
167 | 7 4 6 2 1 5 8 3 9
168 | 6 1 2 8 4 9 7 5 3
169 | 4 3 8 5 2 7 9 1 6
170 | 9 5 7 1 3 6 4 8 2
171 |
172 | """
173 | p = repr(matrix).replace("0", "_")
174 | result = list(Prolog.query(f"L={p},sudoku(L)", maxresult=1))
175 | if not result:
176 | return False
177 | result = result[0].get("L")
178 | if not result:
179 | return False
180 | return Matrix(result)
181 |
182 |
183 | def prolog_source() -> str:
184 | """Returns the Prolog source file that solves Sudoku puzzles."""
185 | from pathlib import Path
186 |
187 | path = Path(__file__).parent / _PROLOG_FILE
188 | with open(path) as f:
189 | return f.read()
190 |
191 |
192 | def sample_puzzle() -> Matrix:
193 | """Returns the sample Sudoku puzzle"""
194 | matrix = Matrix.from_text("""
195 | . . 5 . 7 . 2 6 8
196 | . . 4 . . 2 . . .
197 | . . 1 . 9 . . . .
198 | . 8 . . . . 1 . .
199 | . 2 . 9 . . . 7 .
200 | . . 6 . . . . 3 .
201 | . . 2 . 4 . 7 . .
202 | . . . 5 . . 9 . .
203 | 9 5 7 . 3 . . . .
204 | """)
205 | return matrix
206 |
207 |
208 | def main():
209 | puzzle = sample_puzzle()
210 | print("\n-- PUZZLE --")
211 | puzzle.pretty_print()
212 | print("\n-- SOLUTION --")
213 | solution = solve(puzzle)
214 | if solution:
215 | solution.pretty_print()
216 | else:
217 | print("This puzzle has no solutions. Is it valid?")
218 |
219 |
220 | if __name__ == "__main__":
221 | main()
222 |
--------------------------------------------------------------------------------
/src/pyswip/prolog.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip Contributors
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in all
11 | # copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | # SOFTWARE.
20 |
21 | """
22 | Provides the basic Prolog interface.
23 | """
24 |
25 | import functools
26 | import inspect
27 | import re
28 | from typing import Union, Generator, Callable, Optional, Tuple
29 | from pathlib import Path
30 |
31 | from pyswip.utils import resolve_path
32 | from pyswip.core import (
33 | SWI_HOME_DIR,
34 | PL_STRING,
35 | REP_UTF8,
36 | PL_Q_NODEBUG,
37 | PL_Q_CATCH_EXCEPTION,
38 | PL_Q_NORMAL,
39 | PL_FA_NONDETERMINISTIC,
40 | CFUNCTYPE,
41 | PL_initialise,
42 | PL_open_foreign_frame,
43 | PL_new_term_ref,
44 | PL_chars_to_term,
45 | PL_call,
46 | PL_discard_foreign_frame,
47 | PL_new_term_refs,
48 | PL_put_chars,
49 | PL_predicate,
50 | PL_open_query,
51 | PL_next_solution,
52 | PL_copy_term_ref,
53 | PL_exception,
54 | PL_cut_query,
55 | PL_thread_self,
56 | PL_thread_attach_engine,
57 | PL_register_foreign_in_module,
58 | foreign_t,
59 | term_t,
60 | control_t,
61 | )
62 |
63 |
64 | __all__ = "PrologError", "NestedQueryError", "Prolog"
65 |
66 |
67 | RE_PLACEHOLDER = re.compile(r"%p")
68 |
69 |
70 | class PrologError(Exception):
71 | pass
72 |
73 |
74 | class NestedQueryError(PrologError):
75 | """
76 | SWI-Prolog does not accept nested queries, that is, opening a query while the previous one was not closed.
77 | As this error may be somewhat difficult to debug in foreign code, it is automatically treated inside PySwip
78 | """
79 |
80 | pass
81 |
82 |
83 | def __initialize():
84 | args = []
85 | args.append("./")
86 | args.append("-q") # --quiet
87 | args.append("--nosignals") # "Inhibit any signal handling by Prolog"
88 | if SWI_HOME_DIR:
89 | args.append(f"--home={SWI_HOME_DIR}")
90 |
91 | result = PL_initialise(len(args), args)
92 | # result is a boolean variable (i.e. 0 or 1) indicating whether the
93 | # initialisation was successful or not.
94 | if not result:
95 | raise PrologError(
96 | "Could not initialize the Prolog environment."
97 | "PL_initialise returned %d" % result
98 | )
99 |
100 | swipl_fid = PL_open_foreign_frame()
101 | swipl_load = PL_new_term_ref()
102 | PL_chars_to_term(
103 | """
104 | asserta(pyrun(GoalString,BindingList) :-
105 | (read_term_from_atom(GoalString, Goal, [variable_names(BindingList)]),
106 | call(Goal))).
107 | """,
108 | swipl_load,
109 | )
110 | PL_call(swipl_load, None)
111 | PL_discard_foreign_frame(swipl_fid)
112 |
113 |
114 | __initialize()
115 |
116 |
117 | # NOTE: These imports MUST come after _initialize is called!!
118 | from pyswip.easy import getTerm, Atom, Variable # noqa: E402
119 |
120 |
121 | class Prolog:
122 | """Provides the entry point for the Prolog interface"""
123 |
124 | # We keep track of open queries to avoid nested queries.
125 | _queryIsOpen = False
126 | _cwraps = []
127 |
128 | class _QueryWrapper(object):
129 | def __init__(self):
130 | if Prolog._queryIsOpen:
131 | raise NestedQueryError("The last query was not closed")
132 |
133 | def __call__(self, query, maxresult, catcherrors, normalize):
134 | Prolog._init_prolog_thread()
135 | swipl_fid = PL_open_foreign_frame()
136 |
137 | swipl_args = PL_new_term_refs(2)
138 | swipl_goalCharList = swipl_args
139 | swipl_bindingList = swipl_args + 1
140 |
141 | PL_put_chars(
142 | swipl_goalCharList, PL_STRING | REP_UTF8, -1, query.encode("utf-8")
143 | )
144 |
145 | swipl_predicate = PL_predicate("pyrun", 2, None)
146 |
147 | plq = PL_Q_NODEBUG | PL_Q_CATCH_EXCEPTION if catcherrors else PL_Q_NORMAL
148 | swipl_qid = PL_open_query(None, plq, swipl_predicate, swipl_args)
149 |
150 | Prolog._queryIsOpen = True # From now on, the query will be considered open
151 | try:
152 | while maxresult and PL_next_solution(swipl_qid):
153 | maxresult -= 1
154 | swipl_list = PL_copy_term_ref(swipl_bindingList)
155 | t = getTerm(swipl_list)
156 | if normalize:
157 | try:
158 | v = t.value
159 | except AttributeError:
160 | v = {}
161 | for r in [x.value for x in t]:
162 | r = normalize_values(r)
163 | v.update(r)
164 | yield v
165 | else:
166 | yield t
167 |
168 | if PL_exception(swipl_qid):
169 | term = getTerm(PL_exception(swipl_qid))
170 |
171 | raise PrologError(
172 | "".join(
173 | [
174 | "Caused by: '",
175 | query,
176 | "'. ",
177 | "Returned: '",
178 | str(term),
179 | "'.",
180 | ]
181 | )
182 | )
183 |
184 | finally: # This ensures that, whatever happens, we close the query
185 | PL_cut_query(swipl_qid)
186 | PL_discard_foreign_frame(swipl_fid)
187 | Prolog._queryIsOpen = False
188 |
189 | @classmethod
190 | def _init_prolog_thread(cls):
191 | pengine_id = PL_thread_self()
192 | if pengine_id == -1:
193 | pengine_id = PL_thread_attach_engine(None)
194 | if pengine_id == -1:
195 | raise PrologError("Unable to attach new Prolog engine to the thread")
196 | elif pengine_id == -2:
197 | print("{WARN} Single-threaded swipl build, beware!")
198 |
199 | @classmethod
200 | def asserta(cls, format: str, *args, catcherrors: bool = False) -> None:
201 | """
202 | Assert a clause (fact or rule) into the database.
203 |
204 | ``asserta`` asserts the clause as the first clause of the predicate.
205 |
206 | See `asserta/1 `_ in SWI-Prolog documentation.
207 |
208 | :param format:
209 | The format to be used to generate the clause.
210 | The placeholders (``%p``) are replaced by the ``args`` if one ore more arguments are given.
211 | :param args:
212 | Arguments to replace the placeholders in the ``format`` string
213 | :param catcherrors:
214 | Catches the exception raised during goal execution
215 |
216 | .. Note::
217 | Currently, If no arguments given, the format string is used as the raw clause, even if it contains a placeholder.
218 | This behavior is kept for for compatibility reasons.
219 | It may be removed in future versions.
220 |
221 | >>> Prolog.asserta("big(airplane)")
222 | >>> Prolog.asserta("small(mouse)")
223 | >>> Prolog.asserta('''bigger(A, B) :-
224 | ... big(A),
225 | ... small(B)''')
226 | >>> nums = list(range(5))
227 | >>> Prolog.asserta("numbers(%p)", nums)
228 | """
229 | next(
230 | cls.query(format.join(["asserta((", "))."]), *args, catcherrors=catcherrors)
231 | )
232 |
233 | @classmethod
234 | def assertz(cls, format: str, *args, catcherrors: bool = False) -> None:
235 | """
236 | Assert a clause (fact or rule) into the database.
237 |
238 | ``assertz`` asserts the clause as the last clause of the predicate.
239 |
240 | See `assertz/1 `_ in SWI-Prolog documentation.
241 |
242 | :param format:
243 | The format to be used to generate the clause.
244 | The placeholders (``%p``) are replaced by the ``args`` if one ore more arguments are given.
245 | :param catcherrors:
246 | Catches the exception raised during goal execution
247 |
248 | .. Note::
249 | Currently, If no arguments given, the format string is used as the raw clause, even if it contains a placeholder.
250 | This behavior is kept for for compatibility reasons.
251 | It may be removed in future versions.
252 |
253 | >>> Prolog.assertz("big(airplane)")
254 | >>> Prolog.assertz("small(mouse)")
255 | >>> Prolog.assertz('''bigger(A, B) :-
256 | ... big(A),
257 | ... small(B)''')
258 | >>> nums = list(range(5))
259 | >>> Prolog.assertz("numbers(%p)", nums)
260 | """
261 | next(
262 | cls.query(format.join(["assertz((", "))."]), *args, catcherrors=catcherrors)
263 | )
264 |
265 | @classmethod
266 | def dynamic(cls, *terms: str, catcherrors: bool = False) -> None:
267 | """Informs the interpreter that the definition of the predicate(s) may change during execution
268 |
269 | See `dynamic/1 `_ in SWI-Prolog documentation.
270 |
271 | :param terms: One or more predicate indicators
272 | :param catcherrors: Catches the exception raised during goal execution
273 |
274 | :raises ValueError: if no terms was given.
275 |
276 | >>> Prolog.dynamic("person/1")
277 | >>> Prolog.asserta("person(jane)")
278 | >>> list(Prolog.query("person(X)"))
279 | [{'X': 'jane'}]
280 | >>> Prolog.retractall("person(_)")
281 | >>> list(Prolog.query("person(X)"))
282 | []
283 | """
284 | if len(terms) < 1:
285 | raise ValueError("One or more terms must be given")
286 | params = ",".join(terms)
287 | next(cls.query(f"dynamic(({params}))", catcherrors=catcherrors))
288 |
289 | @classmethod
290 | def retract(cls, format: str, *args, catcherrors: bool = False) -> None:
291 | """
292 | Removes the fact or clause from the database
293 |
294 | See `retract/1 `_ in SWI-Prolog documentation.
295 |
296 | :param format:
297 | The format to be used to generate the term.
298 | The placeholders (``%p``) are replaced by the ``args`` if one ore more arguments are given.
299 | :param catcherrors:
300 | Catches the exception raised during goal execution
301 |
302 | .. Note::
303 | Currently, If no arguments given, the format string is used as the raw term, even if it contains a placeholder.
304 | This behavior is kept for for compatibility reasons.
305 | It may be removed in future versions.
306 |
307 |
308 | >>> Prolog.dynamic("person/1")
309 | >>> Prolog.asserta("person(jane)")
310 | >>> list(Prolog.query("person(X)"))
311 | [{'X': 'jane'}]
312 | >>> Prolog.retract("person(jane)")
313 | >>> list(Prolog.query("person(X)"))
314 | []
315 | >>> Prolog.dynamic("numbers/1")
316 | >>> nums = list(range(5))
317 | >>> Prolog.asserta("numbers(10)")
318 | >>> Prolog.asserta("numbers(%p)", nums)
319 | >>> list(Prolog.query("numbers(X)"))
320 | [{'X': [0, 1, 2, 3, 4]}, {'X': 10}]
321 | >>> Prolog.retract("numbers(%p)", nums)
322 | >>> list(Prolog.query("numbers(X)"))
323 | [{'X': 10}]
324 | """
325 | next(
326 | cls.query(format.join(["retract((", "))."]), *args, catcherrors=catcherrors)
327 | )
328 |
329 | @classmethod
330 | def retractall(cls, format: str, *args, catcherrors: bool = False) -> None:
331 | """
332 | Removes all facts or clauses in the database where the ``head`` unifies.
333 |
334 | See `retractall/1 `_ in SWI-Prolog documentation.
335 |
336 | :param format: The term to unify with the facts or clauses in the database
337 | :param catcherrors: Catches the exception raised during goal execution
338 |
339 | >>> Prolog.dynamic("person/1")
340 | >>> Prolog.asserta("person(jane)")
341 | >>> Prolog.asserta("person(joe)")
342 | >>> list(Prolog.query("person(X)"))
343 | [{'X': 'joe'}, {'X': 'jane'}]
344 | >>> Prolog.retractall("person(_)")
345 | >>> list(Prolog.query("person(X)"))
346 | []
347 | """
348 | next(
349 | cls.query(
350 | format.join(["retractall((", "))."]), *args, catcherrors=catcherrors
351 | )
352 | )
353 |
354 | @classmethod
355 | def consult(
356 | cls,
357 | path: Union[str, Path],
358 | *,
359 | catcherrors: bool = False,
360 | relative_to: Union[str, Path] = "",
361 | ) -> None:
362 | """
363 | Reads the given Prolog source file
364 |
365 | The file is always reloaded when called.
366 |
367 | See `consult/1 `_ in SWI-Prolog documentation.
368 |
369 | Tilde character (``~``) in paths are expanded to the user home directory
370 |
371 | >>> Prolog.consult("~/my_files/hanoi.pl")
372 | >>> # consults file /home/me/my_files/hanoi.pl
373 |
374 | ``relative_to`` keyword argument makes it easier to construct the consult path.
375 | This keyword is no-op, if the consult path is absolute.
376 | If the given ``relative_to`` path is a file, then the consult path is updated to become a sibling of that path.
377 |
378 | Assume you have the ``/home/me/project/facts.pl`` that you want to consult from the ``run.py`` file which exists in the same directory ``/home/me/project``.
379 | Using the built-in ``__file__`` constant which contains the path of the current Python file , it becomes very easy to do that:
380 |
381 | >>> Prolog.consult("facts.pl", relative_to=__file__)
382 |
383 | If the given `relative_path` is a directory, then the consult path is updated to become a child of that path.
384 |
385 | >>> project_dir = "~/projects"
386 | >>> Prolog.consult("facts1.pl", relative_to=project_dir)
387 |
388 | :param path: The path to the Prolog source file
389 | :param catcherrors: Catches the exception raised during goal execution
390 | :param relative_to: The path where the consulted file is relative to
391 | """
392 | path = resolve_path(path, relative_to)
393 | next(
394 | cls.query(
395 | str(path.as_posix()).join(["consult('", "')"]), catcherrors=catcherrors
396 | )
397 | )
398 |
399 | @classmethod
400 | def query(
401 | cls,
402 | format: str,
403 | *args,
404 | maxresult: int = -1,
405 | catcherrors: bool = True,
406 | normalize: bool = True,
407 | ) -> Generator:
408 | """Run a prolog query and return a generator
409 |
410 | If the query is a yes/no question, returns {} for yes, and nothing for no.
411 | Otherwise returns a generator of dicts with variables as keys.
412 |
413 | :param format:
414 | The format to be used to generate the query.
415 | The placeholders (``%p``) are replaced by the ``args`` if one ore more arguments are given.
416 | :param args:
417 | Arguments to replace the placeholders in the ``format`` string
418 | :param maxresult:
419 | Maximum number of results to return
420 | :param catcherrors:
421 | Catches the exception raised during goal execution
422 | :param normalize:
423 | Return normalized values
424 |
425 | .. Note::
426 | Currently, If no arguments given, the format string is used as the raw query, even if it contains a placeholder.
427 | This behavior is kept for for compatibility reasons.
428 | It may be removed in future versions.
429 |
430 | >>> Prolog.assertz("father(michael,john)")
431 | >>> Prolog.assertz("father(michael,gina)")
432 | >>> bool(list(Prolog.query("father(michael,john)")))
433 | True
434 | >>> bool(list(Prolog.query("father(michael,olivia)")))
435 | False
436 | >>> print(sorted(Prolog.query("father(michael,X)")))
437 | [{'X': 'gina'}, {'X': 'john'}]
438 | """
439 | if args:
440 | query = format_prolog(format, args)
441 | else:
442 | query = format
443 | return cls._QueryWrapper()(query, maxresult, catcherrors, normalize)
444 |
445 | @classmethod
446 | @functools.cache
447 | def _callback_wrapper(cls, arity, nondeterministic):
448 | ps = [foreign_t] + [term_t] * arity
449 | if nondeterministic:
450 | return CFUNCTYPE(*(ps + [control_t]))
451 | return CFUNCTYPE(*ps)
452 |
453 | @classmethod
454 | @functools.cache
455 | def _foreign_wrapper(cls, fun, nondeterministic=False):
456 | def wrapper(*args):
457 | if nondeterministic:
458 | args = [getTerm(arg) for arg in args[:-1]] + [args[-1]]
459 | else:
460 | args = [getTerm(arg) for arg in args]
461 | r = fun(*args)
462 | return True if r is None else r
463 |
464 | return wrapper
465 |
466 | @classmethod
467 | def register_foreign(
468 | cls,
469 | func: Callable,
470 | /,
471 | name: str = "",
472 | arity: Optional[int] = None,
473 | *,
474 | module: str = "",
475 | nondeterministic: bool = False,
476 | ):
477 | """
478 | Registers a Python callable as a Prolog predicate
479 |
480 | :param func:
481 | Callable to be registered. The callable should return a value in ``foreign_t``, ``True`` or ``False`` or ``None``.
482 | Returning ``None`` is equivalent to returning ``True``.
483 | :param name:
484 | Name of the callable. If the name is not specified, it is derived from ``func.__name__``.
485 | :param arity:
486 | Number of parameters of the callable. If not specified, it is derived from the callable signature.
487 | :param module:
488 | Name of the module to register the predicate. By default, the current module.
489 | :param nondeterministic:
490 | Set the foreign callable as nondeterministic
491 | """
492 | if not callable(func):
493 | raise ValueError("func is not callable")
494 | module = module or None
495 | flags = PL_FA_NONDETERMINISTIC if nondeterministic else 0
496 | if arity is None:
497 | arity = len(inspect.signature(func).parameters)
498 | if nondeterministic:
499 | arity -= 1
500 | if not name:
501 | name = func.__name__
502 |
503 | cwrap = cls._callback_wrapper(arity, nondeterministic)
504 | # TODO: check func
505 | fwrap = cls._foreign_wrapper(func, nondeterministic)
506 | fwrap = cwrap(fwrap)
507 | cls._cwraps.append(fwrap)
508 | return PL_register_foreign_in_module(module, name, arity, fwrap, flags)
509 |
510 |
511 | def normalize_values(values):
512 | from pyswip.easy import Atom, Functor
513 |
514 | if isinstance(values, Atom):
515 | return values.value
516 | if isinstance(values, Functor):
517 | normalized = str(values.name.value)
518 | if values.arity:
519 | normalized_args = [str(normalize_values(arg)) for arg in values.args]
520 | normalized = normalized + "(" + ", ".join(normalized_args) + ")"
521 | return normalized
522 | elif isinstance(values, dict):
523 | return {key: normalize_values(v) for key, v in values.items()}
524 | elif isinstance(values, (list, tuple)):
525 | return [normalize_values(v) for v in values]
526 | return values
527 |
528 |
529 | def make_prolog_str(value) -> str:
530 | if isinstance(value, str):
531 | return f'"{value}"'
532 | elif isinstance(value, list):
533 | inner = ",".join(make_prolog_str(v) for v in value)
534 | return f"[{inner}]"
535 | elif isinstance(value, Atom):
536 | # TODO: escape atom nome
537 | return f"'{value.chars}'"
538 | elif isinstance(value, Variable):
539 | # TODO: escape variable name
540 | return value.chars
541 | elif value is True:
542 | return "1"
543 | elif value is False:
544 | return "0"
545 | return str(value)
546 |
547 |
548 | def format_prolog(fmt: str, args: Tuple) -> str:
549 | frags = RE_PLACEHOLDER.split(fmt)
550 | if len(args) != len(frags) - 1:
551 | raise ValueError("Number of arguments must match the number of placeholders")
552 | fs = []
553 | for i in range(len(args)):
554 | fs.append(frags[i])
555 | fs.append(make_prolog_str(args[i]))
556 | fs.append(frags[-1])
557 | return "".join(fs)
558 |
--------------------------------------------------------------------------------
/src/pyswip/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuce/pyswip/0e9a952c140840e451d6f3bf05d4d6ca0311f59b/src/pyswip/py.typed
--------------------------------------------------------------------------------
/src/pyswip/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip Contributors
2 | #
3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
4 | # of this software and associated documentation files (the "Software"), to deal
5 | # in the Software without restriction, including without limitation the rights
6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | # copies of the Software, and to permit persons to whom the Software is
8 | # furnished to do so, subject to the following conditions:
9 | #
10 | # The above copyright notice and this permission notice shall be included in all
11 | # copies or substantial portions of the Software.
12 | #
13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | # SOFTWARE.
20 |
21 | from typing import Union
22 | from pathlib import Path
23 |
24 |
25 | def resolve_path(path: Union[str, Path], relative_to: Union[str, Path] = "") -> Path:
26 | path = Path(path).expanduser()
27 | if path.is_absolute() or not relative_to:
28 | return path
29 | relative_to = Path(relative_to).expanduser()
30 | if relative_to.is_symlink():
31 | raise ValueError("Symbolic links are not supported")
32 | if relative_to.is_dir():
33 | return relative_to / path
34 | elif relative_to.is_file():
35 | return relative_to.parent / path
36 | raise ValueError("relative_to must be either a filename or a directory")
37 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuce/pyswip/0e9a952c140840e451d6f3bf05d4d6ca0311f59b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuce/pyswip/0e9a952c140840e451d6f3bf05d4d6ca0311f59b/tests/examples/__init__.py
--------------------------------------------------------------------------------
/tests/examples/hanoi_fixture.txt:
--------------------------------------------------------------------------------
1 |
2 | Step 0
3 |
4 | 1
5 | 2
6 | 3
7 | ---------
8 | L C R
9 |
10 | Step 1
11 |
12 |
13 | 2
14 | 3 1
15 | ---------
16 | L C R
17 |
18 | Step 2
19 |
20 |
21 |
22 | 3 2 1
23 | ---------
24 | L C R
25 |
26 | Step 3
27 |
28 |
29 | 1
30 | 3 2
31 | ---------
32 | L C R
33 |
34 | Step 4
35 |
36 |
37 | 1
38 | 2 3
39 | ---------
40 | L C R
41 |
42 | Step 5
43 |
44 |
45 |
46 | 1 2 3
47 | ---------
48 | L C R
49 |
50 | Step 6
51 |
52 |
53 | 2
54 | 1 3
55 | ---------
56 | L C R
57 |
58 | Step 7
59 |
60 | 1
61 | 2
62 | 3
63 | ---------
64 | L C R
65 |
--------------------------------------------------------------------------------
/tests/examples/hanoi_simple_fixture.txt:
--------------------------------------------------------------------------------
1 | 1. Move disk from left pole to right pole.
2 | 2. Move disk from left pole to center pole.
3 | 3. Move disk from right pole to center pole.
4 | 4. Move disk from left pole to right pole.
5 | 5. Move disk from center pole to left pole.
6 | 6. Move disk from center pole to right pole.
7 | 7. Move disk from left pole to right pole.
8 |
--------------------------------------------------------------------------------
/tests/examples/sudoku.txt:
--------------------------------------------------------------------------------
1 | . 6 . 1 . 4 . 5 .
2 | . . 8 3 . 5 6 . .
3 | 2 . . . . . . . 1
4 | 8 . . 4 . 7 . . 6
5 | . . 6 . . . 3 . .
6 | 7 . . 9 . 1 . . 4
7 | 5 . . . . . . . 2
8 | . . 7 2 . 6 9 . .
9 | . 4 . 5 . 8 . 7 .
--------------------------------------------------------------------------------
/tests/examples/test_coins.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pyswip.examples.coins import solve, prolog_source
4 |
5 |
6 | class CoinsTestCase(unittest.TestCase):
7 | def test_solve(self):
8 | fixture = [{1: 3, 5: 0, 10: 30, 50: 0, 100: 0}]
9 | soln = solve(coin_count=33, total_cents=303, max_solutions=1)
10 | self.assertEqual(fixture, soln)
11 |
12 | def test_prolog_source(self):
13 | source = prolog_source()
14 | self.assertIn("label(Solution)", source)
15 |
--------------------------------------------------------------------------------
/tests/examples/test_hanoi.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from io import StringIO
3 |
4 | from pyswip.examples.hanoi import solve, prolog_source
5 | from .utils import load_fixture
6 |
7 |
8 | class HanoiTestCase(unittest.TestCase):
9 | def test_solve(self):
10 | fixture = load_fixture("hanoi_fixture.txt")
11 | sio = StringIO()
12 | solve(3, file=sio)
13 | self.assertEqual(fixture, sio.getvalue())
14 |
15 | def test_solve_simple(self):
16 | fixture = load_fixture("hanoi_simple_fixture.txt")
17 | sio = StringIO()
18 | solve(3, simple=True, file=sio)
19 | self.assertEqual(fixture, sio.getvalue())
20 |
21 | def test_prolog_source(self):
22 | source = prolog_source()
23 | self.assertIn("move(N, A, B, C)", source)
24 |
--------------------------------------------------------------------------------
/tests/examples/test_sudoku.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pyswip.examples.sudoku import Matrix, solve, prolog_source
4 | from .utils import load_fixture
5 |
6 |
7 | class MatrixTestCase(unittest.TestCase):
8 | FIXTURE = load_fixture("sudoku.txt")
9 |
10 | def test_matrix_from_text(self):
11 | got = Matrix.from_text(self.FIXTURE)
12 | target = [
13 | [0, 6, 0, 1, 0, 4, 0, 5, 0],
14 | [0, 0, 8, 3, 0, 5, 6, 0, 0],
15 | [2, 0, 0, 0, 0, 0, 0, 0, 1],
16 | [8, 0, 0, 4, 0, 7, 0, 0, 6],
17 | [0, 0, 6, 0, 0, 0, 3, 0, 0],
18 | [7, 0, 0, 9, 0, 1, 0, 0, 4],
19 | [5, 0, 0, 0, 0, 0, 0, 0, 2],
20 | [0, 0, 7, 2, 0, 6, 9, 0, 0],
21 | [0, 4, 0, 5, 0, 8, 0, 7, 0],
22 | ]
23 | self.assertListEqual(target, got.matrix)
24 |
25 | def test_solve_success(self):
26 | puzzle = Matrix.from_text(self.FIXTURE)
27 | solution = solve(puzzle)
28 | target = [
29 | [9, 6, 3, 1, 7, 4, 2, 5, 8],
30 | [1, 7, 8, 3, 2, 5, 6, 4, 9],
31 | [2, 5, 4, 6, 8, 9, 7, 3, 1],
32 | [8, 2, 1, 4, 3, 7, 5, 9, 6],
33 | [4, 9, 6, 8, 5, 2, 3, 1, 7],
34 | [7, 3, 5, 9, 6, 1, 8, 2, 4],
35 | [5, 8, 9, 7, 1, 3, 4, 6, 2],
36 | [3, 1, 7, 2, 4, 6, 9, 8, 5],
37 | [6, 4, 2, 5, 9, 8, 1, 7, 3],
38 | ]
39 | self.assertEqual(target, solution.matrix)
40 |
41 | def test_solve_failure(self):
42 | fixture = "8 " + self.FIXTURE[2:]
43 | puzzle = Matrix.from_text(fixture)
44 | solution = solve(puzzle)
45 | self.assertFalse(solution)
46 |
47 | def test_prolog_source(self):
48 | text = prolog_source()
49 | self.assertIn("Prolog Sudoku Solver", text)
50 |
--------------------------------------------------------------------------------
/tests/examples/utils.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | def load_fixture(filename: str) -> str:
5 | path = Path(__file__).parent / filename
6 | with open(path) as f:
7 | return f.read()
8 |
--------------------------------------------------------------------------------
/tests/test_examples.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2018 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 | """
25 | Run several complex examples using pySwip. The main goal of these tests is
26 | ensure stability in several platforms.
27 | """
28 |
29 | import pytest
30 |
31 | examples = [
32 | "create_term.py",
33 | "father.py",
34 | "register_foreign.py",
35 | "register_foreign_simple.py",
36 | "knowledgebase.py",
37 | "sendmoremoney/money.py",
38 | "sendmoremoney/money_new.py",
39 | ]
40 |
41 |
42 | @pytest.mark.parametrize("example", examples)
43 | def test_example(example):
44 | path = example_path(example)
45 | execfile(path)
46 |
47 |
48 | def example_path(path):
49 | import os.path
50 |
51 | return os.path.normpath(
52 | os.path.join(
53 | os.path.split(os.path.abspath(__file__))[0], "..", "examples", path
54 | )
55 | ).replace("\\", "\\\\")
56 |
57 |
58 | def execfile(filepath, globals=None, locals=None):
59 | if globals is None:
60 | globals = {}
61 | globals.update(
62 | {
63 | "__file__": filepath,
64 | "__name__": "__main__",
65 | }
66 | )
67 | with open(filepath, "rb") as file:
68 | exec(compile(file.read(), filepath, "exec"), globals, locals)
69 |
--------------------------------------------------------------------------------
/tests/test_foreign.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pyswip import (
4 | Prolog,
5 | registerForeign,
6 | PL_foreign_context,
7 | PL_foreign_control,
8 | PL_FIRST_CALL,
9 | PL_REDO,
10 | PL_PRUNED,
11 | PL_retry,
12 | PL_FA_NONDETERMINISTIC,
13 | Variable,
14 | )
15 |
16 |
17 | class MyTestCase(unittest.TestCase):
18 | def test_deterministic_foreign(self):
19 | def hello(t):
20 | print("Hello,", t)
21 |
22 | hello.arity = 1
23 |
24 | registerForeign(hello)
25 |
26 | Prolog.assertz("mother(emily,john)")
27 | Prolog.assertz("mother(emily,gina)")
28 | result = list(Prolog.query("mother(emily,X), hello(X)"))
29 | self.assertEqual(len(result), 2, "Query should return two results")
30 | for name in ("john", "gina"):
31 | self.assertTrue(
32 | {"X": name} in result, "Expected result X:{} not present".format(name)
33 | )
34 |
35 | def test_deterministic_foreign_automatic_arity(self):
36 | def hello(t):
37 | print("Hello,", t)
38 |
39 | Prolog.register_foreign(hello, module="autoarity")
40 |
41 | Prolog.assertz("autoarity:mother(emily,john)")
42 | Prolog.assertz("autoarity:mother(emily,gina)")
43 | result = list(Prolog.query("autoarity:mother(emily,X), autoarity:hello(X)"))
44 | self.assertEqual(len(result), 2, "Query should return two results")
45 | for name in ("john", "gina"):
46 | self.assertTrue(
47 | {"X": name} in result, "Expected result X:{} not present".format(name)
48 | )
49 |
50 | def test_nondeterministic_foreign(self):
51 | def nondet(a, context):
52 | control = PL_foreign_control(context)
53 | context = PL_foreign_context(context)
54 | if control == PL_FIRST_CALL:
55 | context = 0
56 | a.unify(int(context))
57 | context += 1
58 | return PL_retry(context)
59 | elif control == PL_REDO:
60 | a.unify(int(context))
61 | if context == 10:
62 | return False
63 | context += 1
64 | return PL_retry(context)
65 | elif control == PL_PRUNED:
66 | pass
67 |
68 | nondet.arity = 1
69 | registerForeign(nondet, flags=PL_FA_NONDETERMINISTIC)
70 | result = list(Prolog.query("nondet(X)"))
71 |
72 | self.assertEqual(len(result), 10, "Query should return 10 results")
73 | for i in range(10):
74 | self.assertTrue(
75 | {"X": i} in result, "Expected result X:{} not present".format(i)
76 | )
77 |
78 | def test_nondeterministic_foreign_autoarity(self):
79 | def nondet(a, context):
80 | control = PL_foreign_control(context)
81 | context = PL_foreign_context(context)
82 | if control == PL_FIRST_CALL:
83 | context = 0
84 | a.unify(int(context))
85 | context += 1
86 | return PL_retry(context)
87 | elif control == PL_REDO:
88 | a.unify(int(context))
89 | if context == 10:
90 | return False
91 | context += 1
92 | return PL_retry(context)
93 | elif control == PL_PRUNED:
94 | pass
95 |
96 | Prolog.register_foreign(nondet, module="autoarity", nondeterministic=True)
97 | result = list(Prolog.query("autoarity:nondet(X)"))
98 |
99 | self.assertEqual(len(result), 10, "Query should return 10 results")
100 | for i in range(10):
101 | self.assertTrue(
102 | {"X": i} in result, "Expected result X:{} not present".format(i)
103 | )
104 |
105 | def test_atoms_and_strings_distinction(self):
106 | test_string = "string"
107 |
108 | def get_str(string):
109 | string.value = test_string
110 |
111 | def test_for_string(string, test_result):
112 | test_result.value = test_string == string.decode("utf-8")
113 |
114 | get_str.arity = 1
115 | test_for_string.arity = 2
116 |
117 | registerForeign(get_str)
118 | registerForeign(test_for_string)
119 |
120 | result = list(Prolog.query("get_str(String), test_for_string(String, Result)"))
121 | self.assertEqual(
122 | result[0]["Result"],
123 | "true",
124 | "A string return value should not be converted to an atom.",
125 | )
126 |
127 | def test_unifying_list_correctly(self):
128 | variable = Variable()
129 | variable.value = [1, 2]
130 | self.assertEqual(variable.value, [1, 2], "Lists should be unifyed correctly.")
131 |
132 | def test_nested_lists(self):
133 | def get_list_of_lists(result):
134 | result.value = [[1], [2]]
135 |
136 | get_list_of_lists.arity = 1
137 |
138 | registerForeign(get_list_of_lists)
139 |
140 | result = list(Prolog.query("get_list_of_lists(Result)"))
141 | self.assertTrue(
142 | {"Result": [[1], [2]]} in result,
143 | "Nested lists should be unified correctly as return value.",
144 | )
145 |
146 | def test_dictionary(self):
147 | result = list(Prolog.query("X = dict{key1:value1 , key2: value2}"))
148 | dict = result[0]
149 | self.assertTrue(
150 | {"key1": "value1", "key2": "value2"} == dict["X"],
151 | "Dictionary should be returned as a dictionary object",
152 | )
153 |
154 | def test_empty_dictionary(self):
155 | result = list(Prolog.query("X = dict{}"))
156 | dict = result[0]
157 | self.assertTrue(
158 | dict["X"] == {},
159 | "Empty dictionary should be returned as an empty dictionary object",
160 | )
161 |
162 | def test_nested_dictionary(self):
163 | result = list(Prolog.query("X = dict{key1:nested{key:value} , key2: value2}"))
164 | dict = result[0]
165 | self.assertTrue(
166 | {"key1": {"key": "value"}, "key2": "value2"} == dict["X"],
167 | "Nested Dictionary should be returned as a nested dictionary object",
168 | )
169 |
170 |
171 | if __name__ == "__main__":
172 | unittest.main()
173 |
--------------------------------------------------------------------------------
/tests/test_functor_return.pl:
--------------------------------------------------------------------------------
1 | sentence(s(NP,VP)) --> noun_phrase(NP), verb_phrase(VP).
2 | noun_phrase(np(D,N)) --> det(D), noun(N).
3 | verb_phrase(vp(V,NP)) --> verb(V), noun_phrase(NP).
4 | det(d(the)) --> [the].
5 | det(d(a)) --> [a].
6 | noun(n(bat)) --> [bat].
7 | noun(n(cat)) --> [cat].
8 | verb(v(eats)) --> [eats].
9 |
--------------------------------------------------------------------------------
/tests/test_issues.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | # pyswip -- Python SWI-Prolog bridge
5 | # Copyright (c) 2007-2012 Yüce Tekol
6 | #
7 | # Permission is hereby granted, free of charge, to any person obtaining a copy
8 | # of this software and associated documentation files (the "Software"), to deal
9 | # in the Software without restriction, including without limitation the rights
10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | # copies of the Software, and to permit persons to whom the Software is
12 | # furnished to do so, subject to the following conditions:
13 | #
14 | # The above copyright notice and this permission notice shall be included in all
15 | # copies or substantial portions of the Software.
16 | #
17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | # SOFTWARE.
24 |
25 |
26 | """Regression tests for issues."""
27 |
28 | import subprocess
29 | import sys
30 | import unittest
31 |
32 |
33 | class TestIssues(unittest.TestCase):
34 | """Each test method is named after the issue it is testing. The docstring
35 | contains the link for the issue and the issue's description.
36 | """
37 |
38 | def test_issue_13_17_and_6(self):
39 | """
40 | Improve library loading.
41 |
42 | This issue used to manifest as an inability to load SWI-Prolog's
43 | SO/DLL. If this test fails, it will usually kill Python, so the test is
44 | very simple.
45 |
46 | This test is here but it should be run on several platforms to ensure it
47 | works.
48 |
49 | http://code.google.com/p/pyswip/issues/detail?id=13
50 | http://code.google.com/p/pyswip/issues/detail?id=6
51 | https://code.google.com/p/pyswip/issues/detail?id=17
52 | """
53 |
54 | # won't be very useful if it is not tested in several
55 | # OSes
56 |
57 | def test_issue_1(self):
58 | """
59 | Segmentation fault when assertz-ing
60 |
61 | Notes: This issue manifests only in 64bit stacks (note that a full 64
62 | bit stack is needed. If running 32 in 64bit, it will not happen.)
63 |
64 | http://code.google.com/p/pyswip/issues/detail?id=1
65 | """
66 |
67 | # The simple code below should be enough to trigger the issue. As with
68 | # issue 13, if it does not work, it will segfault Python.
69 | from pyswip import Prolog
70 |
71 | Prolog.assertz("randomTerm(michael,john)")
72 |
73 | def test_issue_8(self):
74 | """
75 | Callbacks can cause segv's
76 |
77 | https://code.google.com/p/pyswip/issues/detail?id=8
78 | """
79 |
80 | from pyswip import Prolog, registerForeign
81 |
82 | callsToHello = []
83 |
84 | def hello(t):
85 | callsToHello.append(t)
86 |
87 | hello.arity = 1
88 |
89 | registerForeign(hello)
90 |
91 | Prolog.assertz("parent(michael,john)")
92 | Prolog.assertz("parent(michael,gina)")
93 | p = Prolog.query("parent(michael,X), hello(X)")
94 | result = list(p) # Will run over the iterator
95 |
96 | self.assertEqual(len(callsToHello), 2) # ['john', 'gina']
97 | self.assertEqual(len(result), 2) # [{'X': 'john'}, {'X': 'gina'}]
98 |
99 | def test_issue_15(self):
100 | """
101 | sys.exit does not work when importing pyswip
102 |
103 | https://code.google.com/p/pyswip/issues/detail?id=15
104 | """
105 |
106 | # We will use it to test several return codes
107 | pythonExec = sys.executable
108 |
109 | def runTestCode(code):
110 | parameters = [
111 | pythonExec,
112 | "-c",
113 | "import sys; import pyswip; sys.exit(%d)" % code,
114 | ]
115 | result = subprocess.call(parameters)
116 | self.assertEqual(result, code)
117 |
118 | runTestCode(0)
119 | runTestCode(1)
120 | runTestCode(2)
121 | runTestCode(127)
122 |
123 | def test_issue_5(self):
124 | """
125 | Patch: hash and eq methods for Atom class.
126 |
127 | Ensures that the patch is working.
128 |
129 | https://code.google.com/p/pyswip/issues/detail?id=5
130 | """
131 |
132 | from pyswip import Atom, Variable
133 |
134 | a = Atom("test")
135 | b = Atom("test2")
136 | c = Atom("test") # Should be equal to a
137 |
138 | self.assertNotEqual(a, b)
139 | self.assertNotEqual(c, b)
140 | self.assertEqual(a, c)
141 |
142 | atomSet = set()
143 | atomSet.add(a)
144 | atomSet.add(b)
145 | atomSet.add(c) # This is equal to a
146 | self.assertEqual(len(atomSet), 2)
147 | self.assertEqual(atomSet, set([a, b]))
148 |
149 | # The same semantics should be valid for other classes
150 | A = Variable()
151 | B = Variable()
152 | C = Variable(A.handle) # This is equal to A
153 |
154 | self.assertNotEqual(A, B)
155 | self.assertNotEqual(C, B)
156 | self.assertEqual(A, C)
157 | varSet = set()
158 | varSet.add(A)
159 | varSet.add(B)
160 | varSet.add(C) # This is equal to A
161 | self.assertEqual(len(varSet), 2)
162 | self.assertEqual(varSet, {A, B})
163 |
164 | def test_dynamic(self):
165 | """
166 | Patch for a dynamic method
167 |
168 | Ensures that the patch is working.
169 |
170 | https://code.google.com/p/pyswip/issues/detail?id=4
171 | """
172 |
173 | from pyswip import Prolog
174 |
175 | Prolog.dynamic("test_issue_4_d/1")
176 | Prolog.assertz("test_issue_4_d(test1)")
177 | Prolog.assertz("test_issue_4_d(test1)")
178 | Prolog.assertz("test_issue_4_d(test1)")
179 | Prolog.assertz("test_issue_4_d(test2)")
180 | results = list(Prolog.query("test_issue_4_d(X)"))
181 | self.assertEqual(len(results), 4)
182 |
183 | Prolog.retract("test_issue_4_d(test1)")
184 | results = list(Prolog.query("test_issue_4_d(X)"))
185 | self.assertEqual(len(results), 3)
186 |
187 | Prolog.retractall("test_issue_4_d(test1)")
188 | results = list(Prolog.query("test_issue_4_d(X)"))
189 | self.assertEqual(len(results), 1)
190 |
191 | def test_issue_3(self):
192 | """
193 | Problem with variables in lists
194 |
195 | https://code.google.com/p/pyswip/issues/detail?id=3
196 | """
197 |
198 | from pyswip import Prolog, Functor, Variable, Atom
199 |
200 | _ = Prolog()
201 |
202 | f = Functor("f", 1)
203 | A = Variable()
204 | B = Variable()
205 | C = Variable()
206 |
207 | x = f([A, B, C])
208 | x = Functor.fromTerm(x)
209 | args = x.args[0]
210 |
211 | self.assertFalse(args[0] == args[1], "Var A equals var B")
212 | self.assertFalse(args[0] == args[2], "Var A equals var C")
213 | self.assertFalse(args[1] == args[2], "Var B equals var C")
214 |
215 | self.assertFalse(A == B, "Var A equals var B")
216 | self.assertFalse(B == C, "Var A equals var C")
217 | self.assertFalse(A == C, "Var B equals var C")
218 |
219 | # A more complex test
220 | x = f([A, B, "c"])
221 | x = Functor.fromTerm(x)
222 | args = x.args[0]
223 | self.assertEqual(type(args[0]), Variable)
224 | self.assertEqual(type(args[1]), Variable)
225 | self.assertEqual(type(args[2]), Atom)
226 |
227 | # A test with repeated variables
228 | x = f([A, B, A])
229 | x = Functor.fromTerm(x)
230 | args = x.args[0]
231 | self.assertEqual(type(args[0]), Variable)
232 | self.assertEqual(type(args[1]), Variable)
233 | self.assertEqual(type(args[2]), Variable)
234 | self.assertTrue(
235 | args[0] == args[2],
236 | "The first and last var of " "f([A, B, A]) should be the same",
237 | )
238 |
239 | def test_issue_62(self):
240 | """
241 | Problem with non-ascii atoms and strings
242 |
243 | https://github.com/yuce/pyswip/issues/62
244 | """
245 | from pyswip import Prolog
246 |
247 | Prolog.consult("test_unicode.pl", catcherrors=True, relative_to=__file__)
248 | atoms = list(Prolog.query("unicode_atom(B)."))
249 |
250 | self.assertEqual(len(atoms), 3, "Query should return exactly three atoms")
251 |
252 | strings = list(Prolog.query("unicode_string(B)."))
253 |
254 | self.assertEqual(len(strings), 1, "Query should return exactly one string")
255 | self.assertEqual(strings[0]["B"], b"\xd1\x82\xd0\xb5\xd1\x81\xd1\x82")
256 |
257 | def test_functor_return(self):
258 | """
259 | pyswip should generate string representations of query results
260 | that are at least meaningful, preferably equal to what
261 | SWI-Prolog would generate. This test checks if this is true for
262 | `Functor` instance results.
263 |
264 | Not a formal issue, but see forum topic:
265 | https://groups.google.com/forum/#!topic/pyswip/Mpnfq4DH-mI
266 | """
267 |
268 | import pyswip.prolog as pl
269 |
270 | p = pl.Prolog()
271 | p.consult("test_functor_return.pl", catcherrors=True, relative_to=__file__)
272 | query = "sentence(Parse_tree, [the,bat,eats,a,cat], [])"
273 | expectedTree = "s(np(d(the), n(bat)), vp(v(eats), np(d(a), n(cat))))"
274 |
275 | # This should not throw an exception
276 | results = list(p.query(query))
277 | self.assertEqual(len(results), 1, "Query should return exactly one result")
278 |
279 | ptree = results[0]["Parse_tree"]
280 | self.assertEqual(ptree, expectedTree)
281 |
282 | # A second test, based on what was posted in the forum
283 | p.assertz("friend(john,son(miki))")
284 | p.assertz("friend(john,son(kiwi))")
285 | p.assertz("friend(john,son(wiki))")
286 | p.assertz("friend(john,son(tiwi))")
287 | p.assertz("father(son(miki),kur)")
288 | p.assertz("father(son(kiwi),kur)")
289 | p.assertz("father(son(wiki),kur)")
290 |
291 | soln = [s["Y"] for s in p.query("friend(john,Y), father(Y,kur)", maxresult=1)]
292 | self.assertEqual(soln[0], "son(miki)")
293 |
--------------------------------------------------------------------------------
/tests/test_prolog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # pyswip -- Python SWI-Prolog bridge
4 | # Copyright (c) 2007-2012 Yüce Tekol
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 |
24 |
25 | """
26 | Tests the Prolog class.
27 | """
28 |
29 | import os.path
30 | import unittest
31 |
32 | import pytest
33 |
34 | from pyswip import Atom, Variable
35 | from pyswip.prolog import Prolog, NestedQueryError, format_prolog
36 |
37 |
38 | class TestProlog(unittest.TestCase):
39 | """
40 | Unit tests for prolog module (contains only Prolog class).
41 | """
42 |
43 | def test_nested_queries(self):
44 | """
45 | SWI-Prolog cannot have nested queries called by the foreign function
46 | interface, that is, if we open a query and are getting results from it,
47 | we cannot open another query before closing that one.
48 |
49 | Since this is a user error, we just ensure that a appropriate error
50 | message is thrown.
51 | """
52 |
53 | # Add something to the base
54 | Prolog.assertz("father(john,mich)")
55 | Prolog.assertz("father(john,gina)")
56 | Prolog.assertz("mother(jane,mich)")
57 |
58 | somequery = "father(john, Y)"
59 | otherquery = "mother(jane, X)"
60 |
61 | # This should not throw an exception
62 | for _ in Prolog.query(somequery):
63 | pass
64 | for _ in Prolog.query(otherquery):
65 | pass
66 |
67 | with self.assertRaises(NestedQueryError):
68 | for q in Prolog.query(somequery):
69 | for j in Prolog.query(otherquery):
70 | # This should throw an error, because I opened the second
71 | # query
72 | pass
73 |
74 | def test_prolog_functor_in_list(self):
75 | Prolog.assertz("f([g(a,b),h(a,b,c)])")
76 | self.assertEqual([{"L": ["g(a, b)", "h(a, b, c)"]}], list(Prolog.query("f(L)")))
77 | Prolog.retract("f([g(a,b),h(a,b,c)])")
78 |
79 | def test_prolog_functor_in_functor(self):
80 | Prolog.assertz("f([g([h(a,1), h(b,1)])])")
81 | self.assertEqual(
82 | [{"G": ["g(['h(a, 1)', 'h(b, 1)'])"]}], list(Prolog.query("f(G)"))
83 | )
84 | Prolog.assertz("a([b(c(x), d([y, z, w]))])")
85 | self.assertEqual(
86 | [{"B": ["b(c(x), d(['y', 'z', 'w']))"]}], list(Prolog.query("a(B)"))
87 | )
88 | Prolog.retract("f([g([h(a,1), h(b,1)])])")
89 | Prolog.retract("a([b(c(x), d([y, z, w]))])")
90 |
91 | def test_prolog_strings(self):
92 | """
93 | See: https://github.com/yuce/pyswip/issues/9
94 | """
95 | Prolog.assertz('some_string_fact("abc")')
96 | self.assertEqual([{"S": b"abc"}], list(Prolog.query("some_string_fact(S)")))
97 |
98 | def test_quoted_strings(self):
99 | """
100 | See: https://github.com/yuce/pyswip/issues/90
101 | """
102 | self.assertEqual([{"X": b"a"}], list(Prolog.query('X = "a"')))
103 | Prolog.assertz('test_quoted_strings("hello","world")')
104 | self.assertEqual(
105 | [{"A": b"hello", "B": b"world"}],
106 | list(Prolog.query("test_quoted_strings(A,B)")),
107 | )
108 |
109 | def test_prolog_read_file(self):
110 | """
111 | See: https://github.com/yuce/pyswip/issues/10
112 | """
113 | current_dir = os.path.dirname(os.path.abspath(__file__))
114 | path = os.path.join(current_dir, "test_read.pl")
115 | Prolog.consult("test_read.pl", relative_to=__file__)
116 | list(Prolog.query(f'read_file("{path}", S)'))
117 |
118 | def test_retract(self):
119 | Prolog.dynamic("person/1")
120 | Prolog.asserta("person(jane)")
121 | result = list(Prolog.query("person(X)"))
122 | self.assertEqual([{"X": "jane"}], result)
123 | Prolog.retract("person(jane)")
124 | result = list(Prolog.query("person(X)"))
125 | self.assertEqual([], result)
126 |
127 | def test_placeholder_2(self):
128 | joe = Atom("joe")
129 | ids = [1, 2, 3]
130 | Prolog.assertz("user(%p,%p)", joe, ids)
131 | result = list(Prolog.query("user(%p,IDs)", joe))
132 | self.assertEqual([{"IDs": [1, 2, 3]}], result)
133 |
134 |
135 | format_prolog_fixture = [
136 | ("", (), ""),
137 | ("no-args", (), "no-args"),
138 | ("before%pafter", ("text",), 'before"text"after'),
139 | ("before%pafter", (123,), "before123after"),
140 | ("before%pafter", (123.45,), "before123.45after"),
141 | ("before%pafter", (Atom("foo"),), "before'foo'after"),
142 | ("before%pafter", (Variable(name="Foo"),), "beforeFooafter"),
143 | ("before%pafter", (False,), "before0after"),
144 | ("before%pafter", (True,), "before1after"),
145 | (
146 | "before%pafter",
147 | (["foo", 38, 45.897, [1, 2, 3]],),
148 | 'before["foo",38,45.897,[1,2,3]]after',
149 | ),
150 | ]
151 |
152 |
153 | @pytest.mark.parametrize("format, args, target", format_prolog_fixture)
154 | def test_convert_to_prolog(format, args, target):
155 | assert format_prolog(format, args) == target
156 |
--------------------------------------------------------------------------------
/tests/test_read.pl:
--------------------------------------------------------------------------------
1 | read_file(Filename, Strings) :-
2 | setup_call_cleanup(
3 | open(Filename, read, Stream),
4 | read_stream_string(Stream, Strings),
5 | close(Stream)
6 | ).
7 |
8 | read_stream_string(Stream, Strings) :-
9 | read_line_to_string(Stream, String),
10 | ( String == end_of_file -> Strings = []
11 | ;
12 | read_stream_string(Stream, RStrings),
13 | Strings = [String|RStrings]
14 | ).
15 |
--------------------------------------------------------------------------------
/tests/test_unicode.pl:
--------------------------------------------------------------------------------
1 | :- encoding(utf8).
2 |
3 | unicode_atom('peñarol').
4 | unicode_atom('franišek').
5 | unicode_atom('bonifác').
6 |
7 | unicode_string("тест").
8 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | # pyswip -- Python SWI-Prolog bridge
2 | # Copyright (c) 2007-2024 Yüce Tekol and PySwip
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to deal
6 | # in the Software without restriction, including without limitation the rights
7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | # copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all
12 | # copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | # SOFTWARE.
21 |
22 | import os
23 | import unittest
24 | import tempfile
25 | from pathlib import Path
26 |
27 | from pyswip.utils import resolve_path
28 |
29 |
30 | class UtilsTestCase(unittest.TestCase):
31 | def test_resolve_path_given_file(self):
32 | filename = "test_read.pl"
33 | path = resolve_path(filename)
34 | self.assertEqual(Path(filename), path)
35 |
36 | def test_resolve_path_given_dir(self):
37 | filename = "test_read.pl"
38 | path = resolve_path(filename, __file__)
39 | current_dir = Path(__file__).parent.absolute()
40 | self.assertEqual(current_dir / filename, path)
41 |
42 | def test_resolve_path_symbolic_link(self):
43 | current_dir = Path(__file__).parent.absolute()
44 | path = current_dir / "test_read.pl"
45 | temp_dir = tempfile.TemporaryDirectory("pyswip")
46 | try:
47 | symlink = Path(temp_dir.name) / "symlinked"
48 | os.symlink(path, symlink)
49 | self.assertRaises(ValueError, lambda: resolve_path("test_read.pl", symlink))
50 | finally:
51 | temp_dir.cleanup()
52 |
53 | def test_resolve_path_absolute(self):
54 | path = resolve_path("/home/pyswip/file")
55 | self.assertEqual(Path("/home/pyswip/file"), path)
56 |
57 | def test_resolve_path_without_resolve(self):
58 | path = resolve_path("files/file1.pl")
59 | self.assertEqual(Path("files/file1.pl"), path)
60 |
61 | def test_resolve_path_expanduser(self):
62 | path = resolve_path("~/foo/bar")
63 | self.assertEqual(Path.home() / "foo/bar", path)
64 |
--------------------------------------------------------------------------------