├── .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 | PySwip logo 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 | --------------------------------------------------------------------------------