├── .dockerignore ├── .github ├── CODE_OF_CONDUCT.md └── workflows │ ├── linux-wheels.yml │ ├── macos-wheels.yml │ └── windows-wheels.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── Makefile.win ├── README.rst ├── clips ├── __init__.py ├── agenda.py ├── classes.py ├── clips_build.py ├── common.py ├── environment.py ├── facts.py ├── functions.py ├── modules.py ├── routers.py └── values.py ├── doc ├── Makefile ├── clips.rst ├── conf.py ├── index.rst ├── make.bat └── requirements.txt ├── lib └── clips.cdef ├── setup.cfg ├── setup.py └── test ├── agenda_test.py ├── classes_test.py ├── environment_test.py ├── facts_test.py ├── functions_test.py └── modules_test.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | clips_source/ 3 | *.py[cod] 4 | clips/*.c 5 | *.so 6 | *.o 7 | GPATH 8 | GRTAGS 9 | GTAGS 10 | .coverage 11 | .cache 12 | *build* 13 | *eggs* 14 | *.egg-info/ 15 | *_clips.c 16 | dist/ 17 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Treat each other well 2 | 3 | Everyone participating in the _clipspy_ project, and in particular in the issue tracker, 4 | pull requests, and social media activity, is expected to treat other people with respect 5 | and more generally to follow the guidelines articulated in the 6 | [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). 7 | -------------------------------------------------------------------------------- /.github/workflows/linux-wheels.yml: -------------------------------------------------------------------------------- 1 | name: Linux Wheel 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | manylinux: 7 | runs-on: ${{ matrix.MANYLINUX.RUNNER }} 8 | container: 9 | image: quay.io/pypa/${{ matrix.MANYLINUX.NAME }} 10 | strategy: 11 | matrix: 12 | PYTHON: 13 | - { VERSION: "cp39-cp39", BINARY: "/opt/python/cp39-cp39/bin" } 14 | - { VERSION: "cp310-cp310", BINARY: "/opt/python/cp310-cp310/bin" } 15 | - { VERSION: "cp311-cp311", BINARY: "/opt/python/cp311-cp311/bin" } 16 | - { VERSION: "cp312-cp312", BINARY: "/opt/python/cp312-cp312/bin" } 17 | - { VERSION: "cp313-cp313", BINARY: "/opt/python/cp313-cp313/bin" } 18 | MANYLINUX: 19 | # x86_64 20 | - { NAME: "manylinux_2_28_x86_64", RUNNER: "ubuntu-latest" } 21 | - { NAME: "musllinux_1_2_x86_64", RUNNER: "ubuntu-latest" } 22 | # arm64 23 | - { NAME: "manylinux_2_28_aarch64", RUNNER: "ubuntu-24.04-arm" } 24 | # GitHub actions are not yet supported on Alpine arm64 25 | # - { NAME: "musllinux_1_2_aarch64", RUNNER: "ubuntu-24.04-arm" } 26 | name: "${{ matrix.PYTHON.VERSION }} - ${{ matrix.MANYLINUX.NAME }}" 27 | env: 28 | WHEEL_NAME: "clipspy-*-${{ matrix.PYTHON.VERSION }}*${{ matrix.MANYLINUX.NAME }}*.whl" 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | - name: Install Python dependencies 33 | run: | 34 | ${{ matrix.PYTHON.BINARY }}/pip install --upgrade build pip cffi pytest setuptools auditwheel 35 | - name: Build CLIPSPy 36 | run: | 37 | make clipspy PYTHON=${{ matrix.PYTHON.BINARY }}/python WHEEL_PLATFORM=${{ matrix.MANYLINUX.NAME }} 38 | - name: Copy the wheel 39 | run: | 40 | mkdir -p wheels 41 | cp dist/*.tar.gz dist/${{ env.WHEEL_NAME }} wheels 42 | - name: Install the wheel 43 | run: | 44 | ${{ matrix.PYTHON.BINARY }}/pip install wheels/${{ env.WHEEL_NAME }} 45 | - name: Run tests 46 | run: | 47 | # Run test from outside module to test installed package 48 | cd ../ 49 | ${{ matrix.PYTHON.BINARY }}/python -m pytest -v clipspy/test 50 | - name: Store build artifacts 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: linux-build-${{ matrix.PYTHON.VERSION }}-${{ matrix.MANYLINUX.NAME }} 54 | path: wheels 55 | 56 | linux-build: 57 | needs: [manylinux] 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/download-artifact@v4 61 | with: 62 | pattern: linux-build-* 63 | path: artifacts 64 | merge-multiple: true 65 | - name: Store build artifacts 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: linux-build 69 | path: artifacts 70 | -------------------------------------------------------------------------------- /.github/workflows/macos-wheels.yml: -------------------------------------------------------------------------------- 1 | name: MACos Wheel 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | macos: 7 | # On MACOS 13+, user 'runner' is not part of wheel group. 8 | # Hence, we need to use sudo everywhere. 9 | # Moreover, installed Python is 'universal2', this leads to packages 10 | # being mistakenly built as 'universal2' instead of 'x86_64'/'arm64'. 11 | # This confuses the heck out of 'delocate-wheel' which we need to patch 12 | # to make it work. 13 | runs-on: ${{ matrix.PLATFORM.RUNNER }} 14 | strategy: 15 | matrix: 16 | PYTHON: ['3.9', '3.10', '3.11', '3.12', '3.13'] 17 | PLATFORM: 18 | - { NAME: "Intel", ARCHITECTURE: "x86_64", RUNNER: "macos-13" } 19 | - { NAME: "Silicon", ARCHITECTURE: "arm64", RUNNER: "macos-14" } 20 | env: 21 | ARCHFLAGS: "-arch ${{ matrix.PLATFORM.ARCHITECTURE }}" 22 | MACOSX_DEPLOYMENT_TARGET: "11.0" 23 | name: "${{ matrix.PYTHON }} - ${{ matrix.PLATFORM.NAME }}" 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.PYTHON }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.PYTHON }} 30 | - name: Install Python dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install --upgrade cffi wheel delocate pytest setuptools build 34 | - name: Build CLIPSPy 35 | run: | 36 | export PY_PLATFORM=$(python -c "import sysconfig; print('%s' % sysconfig.get_platform());") 37 | export _PYTHON_HOST_PLATFORM="${PY_PLATFORM/universal2/${{ matrix.PLATFORM.ARCHITECTURE }}}" 38 | sudo --preserve-env make clipspy 39 | - name: Install CLIPSPy 40 | run: | 41 | pip install dist/*.whl 42 | - name: Run tests 43 | run: | 44 | # Run test from outside module to test installed package 45 | cd ../ 46 | python -m pytest -v clipspy/test 47 | - name: Store build artifacts 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: macos-build-${{ matrix.PYTHON }}-${{ matrix.PLATFORM.ARCHITECTURE }} 51 | path: dist 52 | 53 | macos-universal: 54 | # Merge MACOS 'x86_64' and 'arm64' into 'universal2' for most recent versions of Python. 55 | needs: [macos] 56 | runs-on: macos-14 57 | strategy: 58 | matrix: 59 | PYTHON: ['cp39-cp39', 'cp310-cp310', 'cp311-cp311', 'cp312-cp312', 'cp313-cp313'] 60 | steps: 61 | - uses: actions/download-artifact@v4 62 | with: 63 | pattern: macos-build-* 64 | path: artifacts/ 65 | merge-multiple: true 66 | - name: Set up Python 3.13 67 | uses: actions/setup-python@v2 68 | with: 69 | python-version: 3.13 70 | - name: Install Python dependencies 71 | run: | 72 | python -m pip install --upgrade pip 73 | pip install --upgrade wheel delocate setuptools 74 | - name: Run delocate fuse onto the wheels 75 | run: | 76 | mkdir -p dist 77 | # Can't understand why globbing does not work in here 78 | delocate-merge artifacts/clipspy-1.0.5-${{ matrix.PYTHON }}-macosx_11_0_arm64.whl artifacts/clipspy-1.0.5-${{ matrix.PYTHON }}-macosx_11_0_x86_64.whl -w dist/ 79 | - name: Store build artifacts 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: macos-universal-${{ matrix.PYTHON }} 83 | path: dist/ 84 | 85 | macos-build: 86 | needs: [macos-universal] 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/download-artifact@v4 90 | with: 91 | pattern: macos-universal-* 92 | path: artifacts 93 | merge-multiple: true 94 | - name: Store build artifacts 95 | uses: actions/upload-artifact@v4 96 | with: 97 | name: macos-build 98 | path: artifacts/*universal2*.whl 99 | -------------------------------------------------------------------------------- /.github/workflows/windows-wheels.yml: -------------------------------------------------------------------------------- 1 | name: Windows Wheel 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | windows: 7 | runs-on: windows-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 11 | steps: 12 | - uses: actions/checkout@v2 13 | # Install nmake 14 | - uses: ilammy/msvc-dev-cmd@v1 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Build CLIPSPy 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install --upgrade build cffi wheel pytest setuptools 23 | nmake /F Makefile.win 24 | - name: Install CLIPSPy 25 | run: | 26 | pip install clipspy --no-index --find-links dist 27 | - name: Run tests 28 | run: | 29 | # Run test from outside module to test installed package 30 | cd ../ 31 | python -m pytest -v clipspy/test 32 | - name: Store build artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: windows-build-${{ matrix.python-version }} 36 | path: dist 37 | 38 | windows-build: 39 | needs: [windows] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/download-artifact@v4 43 | with: 44 | pattern: windows-build-* 45 | path: artifacts 46 | merge-multiple: true 47 | - name: Store build artifacts 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: windows-build 51 | path: artifacts 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | clips/*.c 4 | clips.zip 5 | clips_source/ 6 | *.so 7 | *.o 8 | GPATH 9 | GRTAGS 10 | GTAGS 11 | .coverage 12 | .cache 13 | *build/ 14 | *eggs* 15 | *.egg-info/ 16 | *_clips.c 17 | dist/ 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: doc/conf.py 5 | 6 | build: 7 | os: "ubuntu-22.04" 8 | tools: 9 | python: "3.11" 10 | 11 | python: 12 | install: 13 | - requirements: doc/requirements.txt 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2023, Matteo Cafasso 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include version.py LICENSE.txt -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # vim: tabstop=8 2 | PYTHON ?= python 3 | CLIPS_VERSION ?= 6.42 4 | CLIPS_SOURCE_URL ?= "https://sourceforge.net/projects/clipsrules/files/CLIPS/6.4.2/clips_core_source_642.zip" 5 | LIBS_DIR ?= $(PWD)/libs 6 | DIST_DIR ?= $(PWD)/dist 7 | MAKEFILE_NAME ?= makefile 8 | WHEEL_PLATFORM ?= manylinux2014_x86_64 9 | SHARED_INCLUDE_DIR ?= /usr/local/include 10 | SHARED_LIBRARY_DIR ?= /usr/local/lib 11 | TARGET_ARCH ?= $(shell uname -m) 12 | LINUX_LDLIBS ?= -lm -lrt 13 | OSX_LDLIBS ?= -lm -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib 14 | 15 | # platform detection 16 | PLATFORM = $(shell uname -s) 17 | 18 | .PHONY: clips clipspy test install clean 19 | 20 | all: clips_source clips clipspy 21 | 22 | clips_source: 23 | curl --output clips.zip --location --url $(CLIPS_SOURCE_URL) 24 | unzip -jo clips.zip -d clips_source 25 | 26 | ifeq ($(PLATFORM),Darwin) # macOS 27 | clips: clips_source 28 | $(MAKE) -f $(MAKEFILE_NAME) -C clips_source \ 29 | CFLAGS="-std=c99 -O3 -fno-strict-aliasing -fPIC" \ 30 | LDLIBS="$(OSX_LDLIBS)" 31 | ld clips_source/*.o -dylib $(OSX_LDLIBS) -arch $(TARGET_ARCH) \ 32 | -o clips_source/libclips.so 33 | else 34 | clips: clips_source 35 | mkdir -p $(LIBS_DIR) 36 | $(MAKE) -f $(MAKEFILE_NAME) -C clips_source \ 37 | CFLAGS="-std=c99 -O3 -fno-strict-aliasing -fPIC" \ 38 | LDLIBS="$(LINUX_LDLIBS)" 39 | ld -G clips_source/*.o -o $(LIBS_DIR)/libclips.so 40 | endif 41 | 42 | build: clips 43 | mkdir -p $(DIST_DIR) 44 | $(PYTHON) -m build --sdist --wheel --outdir $(DIST_DIR) 45 | 46 | repair: export LD_LIBRARY_PATH := $LD_LIBRARY_PATH:$(LIBS_DIR) 47 | ifeq ($(PLATFORM),Darwin) # macOS 48 | repair: build 49 | delocate-wheel -v dist/*.whl 50 | else 51 | repair: build 52 | if ! auditwheel show $(DIST_DIR)/*.whl; then \ 53 | echo "Skipping non-platform wheel $$wheel"; \ 54 | else \ 55 | auditwheel repair $(DIST_DIR)/*.whl \ 56 | --plat $(WHEEL_PLATFORM) \ 57 | --wheel-dir $(DIST_DIR); \ 58 | fi 59 | endif 60 | 61 | clipspy: build repair 62 | 63 | test: export LD_LIBRARY_PATH := $LD_LIBRARY_PATH:$(LIBS_DIR) 64 | test: clipspy 65 | cp build/lib.*/clips/_clips*.so clips 66 | $(PYTHON) -m pytest -v 67 | 68 | install-clips: clips 69 | install -d $(SHARED_INCLUDE_DIR)/ 70 | install -m 644 clips_source/clips.h $(SHARED_INCLUDE_DIR)/ 71 | install -d $(SHARED_INCLUDE_DIR)/clips 72 | install -m 644 clips_source/*.h $(SHARED_INCLUDE_DIR)/clips/ 73 | install -d $(SHARED_LIBRARY_DIR)/ 74 | install -m 644 clips_source/libclips.so \ 75 | $(SHARED_LIBRARY_DIR)/libclips.so.$(CLIPS_VERSION) 76 | ln -sf $(SHARED_LIBRARY_DIR)/libclips.so.$(CLIPS_VERSION) \ 77 | $(SHARED_LIBRARY_DIR)/libclips.so.6 78 | ln -sf $(SHARED_LIBRARY_DIR)/libclips.so.$(CLIPS_VERSION) \ 79 | $(SHARED_LIBRARY_DIR)/libclips.so 80 | -ldconfig -n -v $(SHARED_LIBRARY_DIR) 81 | 82 | install: clipspy 83 | $(PYTHON) setup.py install 84 | 85 | clean: 86 | -rm clips.zip 87 | -rm -fr clips_source build dist clipspy.egg-info 88 | -------------------------------------------------------------------------------- /Makefile.win: -------------------------------------------------------------------------------- 1 | # vim: tabstop=8 2 | PYTHON = python 3 | CLIPS_VERSION = 6.42 4 | CLIPS_SOURCE_URL = "https://sourceforge.net/projects/clipsrules/files/CLIPS/6.4.2/clips_core_source_642.zip" 5 | 6 | .PHONY: clips_source clips clipspy 7 | 8 | all: clips_source clips clipspy 9 | 10 | clips_source: 11 | curl --output clips.zip --location --insecure --url $(CLIPS_SOURCE_URL) 12 | mkdir clips_source 13 | tar -xf clips.zip -C clips_source --strip-components=1 14 | 15 | clips: clips_source 16 | (cd clips_source/core/ && nmake /F makefile.win) 17 | 18 | clipspy: clips 19 | python setup.py build_ext --include-dirs=clips_source/core/ --library-dirs=clips_source/core/ 20 | python setup.py sdist bdist_wheel 21 | 22 | clean: 23 | -del clips.zip 24 | -rd /s /q clips_source build dist clipspy.egg-info 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | CLIPS Python bindings 2 | ===================== 3 | 4 | Python CFFI_ bindings for the 'C' Language Integrated Production System CLIPS_ 6.42. 5 | 6 | :Source: https://github.com/noxdafox/clipspy 7 | :Documentation: https://clipspy.readthedocs.io 8 | :Download: https://pypi.python.org/pypi/clipspy 9 | 10 | |build badge| |docs badge| 11 | 12 | .. |build badge| image:: https://github.com/noxdafox/clipspy/actions/workflows/linux-wheels.yml/badge.svg 13 | :target: https://github.com/noxdafox/clipspy/actions/workflows/linux-wheels.yml 14 | :alt: Build Status 15 | .. |docs badge| image:: https://readthedocs.org/projects/clipspy/badge/?version=latest 16 | :target: http://clipspy.readthedocs.io/en/latest/?badge=latest 17 | :alt: Documentation Status 18 | 19 | 20 | Initially developed at NASA's Johnson Space Center, CLIPS is a rule-based programming language useful for creating expert and production systems where a heuristic solution is easier to implement and maintain than an imperative one. CLIPS is designed to facilitate the development of software to model human knowledge or expertise. 21 | 22 | CLIPSPy brings CLIPS capabilities within the Python ecosystem. 23 | 24 | Installation 25 | ------------ 26 | 27 | Linux 28 | +++++ 29 | 30 | On Linux CLIPSPy is packaged for `x86_64` and `aarch64` architectures as a wheel according to PEP-513_ guidelines. PEP-656_ is supported solely for `x86_64` at the moment. Minimum Python version is 3.9. 31 | 32 | .. code:: bash 33 | 34 | $ pip install clipspy 35 | 36 | macOS 37 | +++++ 38 | 39 | Apple Intel and Silicon are supported for Python versions starting from 3.9. 40 | 41 | .. code:: bash 42 | 43 | $ pip install clipspy 44 | 45 | Windows 46 | +++++++ 47 | 48 | CLIPSPy comes as a wheel for Python versions starting from 3.9. 49 | 50 | .. code:: batch 51 | 52 | > pip install clipspy 53 | 54 | Building from sources 55 | +++++++++++++++++++++ 56 | 57 | The provided Makefiles take care of retrieving the CLIPS source code and compiling the Python bindings together with it. 58 | 59 | .. code:: bash 60 | 61 | $ make 62 | # make install 63 | 64 | Please check the documentation_ for more information regarding building CLIPSPy from sources. 65 | 66 | Example 67 | ------- 68 | 69 | .. code:: python 70 | 71 | import clips 72 | 73 | DEFTEMPLATE_STRING = """ 74 | (deftemplate person 75 | (slot name (type STRING)) 76 | (slot surname (type STRING)) 77 | (slot birthdate (type SYMBOL))) 78 | """ 79 | 80 | DEFRULE_STRING = """ 81 | (defrule hello-world 82 | "Greet a new person." 83 | (person (name ?name) (surname ?surname)) 84 | => 85 | (println "Hello " ?name " " ?surname)) 86 | """ 87 | 88 | environment = clips.Environment() 89 | 90 | # define constructs 91 | environment.build(DEFTEMPLATE_STRING) 92 | environment.build(DEFRULE_STRING) 93 | 94 | # retrieve the fact template 95 | template = environment.find_template('person') 96 | 97 | # assert a new fact through its template 98 | fact = template.assert_fact(name='John', 99 | surname='Doe', 100 | birthdate=clips.Symbol('01/01/1970')) 101 | 102 | # fact slots can be accessed as dictionary elements 103 | assert fact['name'] == 'John' 104 | 105 | # execute the activations in the agenda 106 | environment.run() 107 | 108 | .. _CLIPS: http://www.clipsrules.net/ 109 | .. _CFFI: https://cffi.readthedocs.io/en/latest/index.html 110 | .. _PEP-513: https://www.python.org/dev/peps/pep-0513/ 111 | .. _PEP-656: https://peps.python.org/pep-0656/ 112 | .. _documentation: https://clipspy.readthedocs.io 113 | -------------------------------------------------------------------------------- /clips/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | __author__ = 'Matteo Cafasso' 32 | __version__ = '1.0.5' 33 | __license__ = 'BSD-3' 34 | 35 | 36 | __all__ = ('CLIPSError', 37 | 'Environment', 38 | 'Router', 39 | 'LoggingRouter', 40 | 'ImpliedFact', 41 | 'TemplateFact', 42 | 'Template', 43 | 'Instance', 44 | 'InstanceName', 45 | 'Class', 46 | 'Strategy', 47 | 'SalienceEvaluation', 48 | 'Verbosity', 49 | 'ClassDefaultMode', 50 | 'TemplateSlotDefaultType', 51 | 'Symbol', 52 | 'InstanceName', 53 | 'SaveMode') 54 | 55 | 56 | from clips.environment import Environment 57 | from clips.classes import Instance, Class 58 | from clips.values import Symbol, InstanceName 59 | from clips.routers import Router, LoggingRouter 60 | from clips.facts import ImpliedFact, TemplateFact, Template 61 | from clips.common import SaveMode, Strategy, SalienceEvaluation, Verbosity 62 | from clips.common import CLIPSError, ClassDefaultMode, TemplateSlotDefaultType 63 | -------------------------------------------------------------------------------- /clips/agenda.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module contains the definition of: 31 | 32 | * Agenda class 33 | * Rule class 34 | * Activation class 35 | 36 | """ 37 | 38 | import clips 39 | 40 | from clips.modules import Module 41 | from clips.common import environment_builder 42 | from clips.common import CLIPSError, Strategy, SalienceEvaluation, Verbosity 43 | 44 | from clips._clips import lib, ffi 45 | 46 | 47 | class Rule: 48 | """A CLIPS rule. 49 | 50 | In CLIPS, Rules are defined via the (defrule) statement. 51 | 52 | """ 53 | 54 | __slots__ = '_env', '_name' 55 | 56 | def __init__(self, env: ffi.CData, name: str): 57 | self._env = env 58 | self._name = name.encode() 59 | 60 | def __hash__(self): 61 | return hash(self._ptr()) 62 | 63 | def __eq__(self, rule): 64 | return self._ptr() == rule._ptr() 65 | 66 | def __str__(self): 67 | string = lib.DefrulePPForm(self._ptr()) 68 | string = ffi.string(string).decode() if string != ffi.NULL else '' 69 | 70 | return ' '.join(string.split()) 71 | 72 | def __repr__(self): 73 | string = lib.DefrulePPForm(self._ptr()) 74 | string = ffi.string(string).decode() if string != ffi.NULL else '' 75 | 76 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 77 | 78 | def _ptr(self) -> ffi.CData: 79 | rule = lib.FindDefrule(self._env, self._name) 80 | if rule == ffi.NULL: 81 | raise CLIPSError(self._env, 'Rule <%s> not defined' % self.name) 82 | 83 | return rule 84 | 85 | @property 86 | def name(self) -> str: 87 | """Rule name.""" 88 | return self._name.decode() 89 | 90 | @property 91 | def module(self) -> Module: 92 | """The module in which the Rule is defined. 93 | 94 | Equivalent to the CLIPS (defrule-module) function. 95 | 96 | """ 97 | name = ffi.string(lib.DefruleModule(self._ptr())).decode() 98 | 99 | return Module(self._env, name) 100 | 101 | @property 102 | def deletable(self) -> bool: 103 | """True if the Rule can be deleted.""" 104 | return lib.DefruleIsDeletable(self._ptr()) 105 | 106 | @property 107 | def watch_firings(self) -> bool: 108 | """Whether or not the Rule firings are being watched.""" 109 | return lib.DefruleGetWatchFirings(self._ptr()) 110 | 111 | @watch_firings.setter 112 | def watch_firings(self, flag: bool): 113 | """Whether or not the Rule firings are being watched.""" 114 | lib.DefruleSetWatchFirings(self._ptr(), flag) 115 | 116 | @property 117 | def watch_activations(self) -> bool: 118 | """Whether or not the Rule Activations are being watched.""" 119 | return lib.DefruleGetWatchActivations(self._ptr()) 120 | 121 | @watch_activations.setter 122 | def watch_activations(self, flag: bool): 123 | """Whether or not the Rule Activations are being watched.""" 124 | lib.DefruleSetWatchActivations(self._ptr(), flag) 125 | 126 | def matches(self, verbosity: Verbosity = Verbosity.TERSE): 127 | """Shows partial matches and activations. 128 | 129 | Returns a tuple containing the combined sum of the matches 130 | for each pattern, the combined sum of partial matches 131 | and the number of activations. 132 | 133 | The verbosity parameter controls how much to output: 134 | 135 | * Verbosity.VERBOSE: detailed matches are printed to stdout 136 | * Verbosity.SUCCINT: a brief description is printed to stdout 137 | * Verbosity.TERSE: (default) nothing is printed to stdout 138 | 139 | """ 140 | value = clips.values.clips_value(self._env) 141 | 142 | lib.Matches(self._ptr(), verbosity, value) 143 | 144 | return clips.values.python_value(self._env, value) 145 | 146 | def refresh(self): 147 | """Refresh the Rule. 148 | 149 | Equivalent to the CLIPS (refresh) function. 150 | 151 | """ 152 | lib.Refresh(self._ptr()) 153 | 154 | def add_breakpoint(self): 155 | """Add a breakpoint for the Rule. 156 | 157 | Equivalent to the CLIPS (add-break) function. 158 | 159 | """ 160 | lib.SetBreak(self._ptr()) 161 | 162 | def remove_breakpoint(self): 163 | """Remove a breakpoint for the Rule. 164 | 165 | Equivalent to the CLIPS (remove-break) function. 166 | 167 | """ 168 | if not lib.RemoveBreak(self._env, self._ptr()): 169 | raise CLIPSError("No breakpoint set") 170 | 171 | def undefine(self): 172 | """Undefine the Rule. 173 | 174 | Equivalent to the CLIPS (undefrule) function. 175 | 176 | The object becomes unusable after this method has been called. 177 | 178 | """ 179 | if not lib.Undefrule(self._ptr(), self._env): 180 | raise CLIPSError(self._env) 181 | 182 | 183 | class Activation: 184 | """When all the constraints of a Rule are satisfied, 185 | the Rule becomes active. 186 | 187 | Activations are organized within the CLIPS Agenda. 188 | 189 | """ 190 | 191 | def __init__(self, env: ffi.CData, act: ffi.CData): 192 | self._env = env 193 | self._act = act 194 | self._pp = activation_pp_string(self._env, self._act) 195 | self._rule_name = ffi.string(lib.ActivationRuleName(self._act)) 196 | 197 | def __hash__(self): 198 | return hash(self._act) 199 | 200 | def __eq__(self, act): 201 | return self._act == act._act 202 | 203 | def __str__(self): 204 | return ' '.join(self._pp.split()) 205 | 206 | def __repr__(self): 207 | return "%s: %s" % (self.__class__.__name__, ' '.join(self._pp.split())) 208 | 209 | def _assert_is_active(self): 210 | """As the engine does not provide means to find activations, 211 | the existence of the pointer in the activations list is tested instead. 212 | 213 | """ 214 | activations = [] 215 | activation = lib.GetNextActivation(self._env, ffi.NULL) 216 | 217 | while activation != ffi.NULL: 218 | activations.append(activation) 219 | activation = lib.GetNextActivation(self._env, activation) 220 | 221 | if self._act not in activations: 222 | raise CLIPSError( 223 | self._env, "Activation %s not in the agenda" % self.name) 224 | 225 | @property 226 | def name(self) -> str: 227 | """Activation Rule name.""" 228 | return self._rule_name.decode() 229 | 230 | @property 231 | def salience(self) -> int: 232 | """Activation salience value.""" 233 | self._assert_is_active() 234 | return lib.ActivationGetSalience(self._act) 235 | 236 | @salience.setter 237 | def salience(self, salience: int): 238 | """Activation salience value.""" 239 | self._assert_is_active() 240 | lib.ActivationSetSalience(self._act, salience) 241 | 242 | def delete(self): 243 | """Remove the activation from the agenda.""" 244 | self._assert_is_active() 245 | lib.DeleteActivation(self._act) 246 | 247 | 248 | class Agenda: 249 | """In CLIPS, when all the conditions to activate a rule are met, 250 | The Rule action is placed within the Agenda. 251 | 252 | The CLIPS Agenda is responsible of sorting the Rule Activations 253 | according to their salience and the conflict resolution strategy. 254 | 255 | .. note:: 256 | 257 | All the Agenda methods are accessible through the Environment class. 258 | 259 | """ 260 | 261 | def __init__(self, env: ffi.CData): 262 | self._env = env 263 | 264 | @property 265 | def agenda_changed(self) -> bool: 266 | """True if any rule activation changes have occurred.""" 267 | value = lib.GetAgendaChanged(self._env) 268 | lib.SetAgendaChanged(self._env, False) 269 | 270 | return value 271 | 272 | @property 273 | def focus(self) -> Module: 274 | """The module associated with the current focus. 275 | 276 | Equivalent to the CLIPS (get-focus) function. 277 | 278 | """ 279 | current_focus = lib.GetFocus(self._env) 280 | if current_focus != ffi.NULL: 281 | name = ffi.string(lib.DefmoduleName(current_focus)).decode() 282 | 283 | return Module(self._env, name) 284 | 285 | return None 286 | 287 | @focus.setter 288 | def focus(self, module: Module): 289 | """The module associated with the current focus. 290 | 291 | Equivalent to the CLIPS (get-focus) function. 292 | 293 | """ 294 | return lib.Focus(module._ptr()) 295 | 296 | @property 297 | def strategy(self) -> Strategy: 298 | """The current conflict resolution strategy. 299 | 300 | Equivalent to the CLIPS (get-strategy) function. 301 | 302 | """ 303 | return Strategy(lib.GetStrategy(self._env)) 304 | 305 | @strategy.setter 306 | def strategy(self, value: Strategy): 307 | """The current conflict resolution strategy. 308 | 309 | Equivalent to the CLIPS (get-strategy) function. 310 | 311 | """ 312 | lib.SetStrategy(self._env, Strategy(value)) 313 | 314 | @property 315 | def salience_evaluation(self) -> SalienceEvaluation: 316 | """The salience evaluation behavior. 317 | 318 | Equivalent to the CLIPS (get-salience-evaluation) command. 319 | 320 | """ 321 | return SalienceEvaluation(lib.GetSalienceEvaluation(self._env)) 322 | 323 | @salience_evaluation.setter 324 | def salience_evaluation(self, value: SalienceEvaluation): 325 | """The salience evaluation behavior. 326 | 327 | Equivalent to the CLIPS (get-salience-evaluation) command. 328 | 329 | """ 330 | lib.SetSalienceEvaluation(self._env, SalienceEvaluation(value)) 331 | 332 | def rules(self) -> iter: 333 | """Iterate over the defined Rules.""" 334 | rule = lib.GetNextDefrule(self._env, ffi.NULL) 335 | 336 | while rule != ffi.NULL: 337 | name = ffi.string(lib.DefruleName(rule)).decode() 338 | yield Rule(self._env, name) 339 | 340 | rule = lib.GetNextDefrule(self._env, rule) 341 | 342 | def find_rule(self, name: str) -> Rule: 343 | """Find a Rule by name.""" 344 | defrule = lib.FindDefrule(self._env, name.encode()) 345 | if defrule == ffi.NULL: 346 | raise LookupError("Rule '%s' not found" % name) 347 | 348 | return Rule(self._env, name) 349 | 350 | def reorder(self, module: Module = None): 351 | """Reorder the Activations in the Agenda. 352 | 353 | If no Module is specified, the agendas of all modules are reordered. 354 | 355 | To be called after changing the conflict resolution strategy. 356 | 357 | """ 358 | if module is not None: 359 | lib.ReorderAgenda(module._ptr()) 360 | else: 361 | lib.ReorderAllAgendas(self._env) 362 | 363 | def refresh(self, module: Module = None): 364 | """Recompute the salience values of the Activations on the Agenda 365 | and then reorder the agenda. 366 | 367 | Equivalent to the CLIPS (refresh-agenda) function. 368 | 369 | If no Module is specified, the agendas of all modules are refreshed. 370 | 371 | """ 372 | if module is not None: 373 | lib.RefreshAgenda(module._ptr()) 374 | else: 375 | lib.RefreshAllAgendas(self._env) 376 | 377 | def activations(self) -> iter: 378 | """Iterate over the Activations in the Agenda.""" 379 | activation = lib.GetNextActivation(self._env, ffi.NULL) 380 | 381 | while activation != ffi.NULL: 382 | yield Activation(self._env, activation) 383 | 384 | activation = lib.GetNextActivation(self._env, activation) 385 | 386 | def delete_activations(self): 387 | """Delete all activations in the agenda.""" 388 | if not lib.DeleteActivation(self._env, ffi.NULL): 389 | raise CLIPSError(self._env) 390 | 391 | def clear_focus(self): 392 | """Remove all modules from the focus stack. 393 | 394 | Equivalent to the CLIPS (clear-focus-stack) function. 395 | 396 | """ 397 | lib.ClearFocusStack(self._env) 398 | 399 | def run(self, limit: int = None) -> int: 400 | """Runs the activations in the agenda. 401 | 402 | If limit is not None, the first activations up to limit will be run. 403 | 404 | Returns the number of activation which were run. 405 | 406 | """ 407 | return lib.Run(self._env, limit if limit is not None else -1) 408 | 409 | 410 | def activation_pp_string(env: ffi.CData, ist: ffi.CData) -> str: 411 | builder = environment_builder(env, 'string') 412 | lib.SBReset(builder) 413 | lib.ActivationPPForm(ist, builder) 414 | 415 | return ffi.string(builder.contents).decode() 416 | -------------------------------------------------------------------------------- /clips/clips_build.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | from cffi import FFI 32 | 33 | 34 | ffibuilder = FFI() 35 | CLIPS_SOURCE = """ 36 | #include 37 | 38 | /* Return true if the template is implied. */ 39 | bool ImpliedDeftemplate(Deftemplate *template) 40 | { 41 | return template->implied; 42 | } 43 | 44 | /* User Defined Functions support. */ 45 | static void python_function(Environment *env, UDFContext *udfc, UDFValue *out); 46 | 47 | int DefinePythonFunction(Environment *environment) 48 | { 49 | return AddUDF( 50 | environment, "python-function", 51 | NULL, UNBOUNDED, UNBOUNDED, NULL, 52 | python_function, "python_function", NULL); 53 | } 54 | """ 55 | 56 | 57 | with open("lib/clips.cdef") as cdef_file: 58 | CLIPS_CDEF = cdef_file.read() 59 | 60 | 61 | ffibuilder.set_source("_clips", 62 | CLIPS_SOURCE, 63 | libraries=["clips"]) 64 | 65 | 66 | ffibuilder.cdef(CLIPS_CDEF) 67 | 68 | 69 | if __name__ == "__main__": 70 | ffibuilder.compile(verbose=True) 71 | -------------------------------------------------------------------------------- /clips/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | from enum import IntEnum 31 | from collections import namedtuple 32 | 33 | from clips._clips import lib, ffi 34 | 35 | 36 | class CLIPSError(RuntimeError): 37 | """An error occurred within the CLIPS Environment.""" 38 | 39 | def __init__(self, env: ffi.CData, message: str = None, code: int = None): 40 | if message is None: 41 | routers = environment_data(env, 'routers') 42 | message = routers['python-error-router'].last_message 43 | message = message.lstrip('\n').rstrip('\n').replace('\n', ' ') 44 | 45 | super(CLIPSError, self).__init__(message) 46 | 47 | self.code = code 48 | 49 | 50 | class CLIPSType(IntEnum): 51 | FLOAT = 0 52 | INTEGER = 1 53 | SYMBOL = 2 54 | STRING = 3 55 | MULTIFIELD = 4 56 | EXTERNAL_ADDRESS = 5 57 | FACT_ADDRESS = 6 58 | INSTANCE_ADDRESS = 7 59 | INSTANCE_NAME = 8 60 | VOID = 9 61 | 62 | 63 | class SaveMode(IntEnum): 64 | LOCAL_SAVE = lib.LOCAL_SAVE 65 | VISIBLE_SAVE = lib.VISIBLE_SAVE 66 | 67 | 68 | class ClassDefaultMode(IntEnum): 69 | CONVENIENCE_MODE = 0 70 | CONSERVATION_MODE = 1 71 | 72 | 73 | class Strategy(IntEnum): 74 | DEPTH = 0 75 | BREADTH = 1 76 | LEX = 2 77 | MEA = 3 78 | COMPLEXITY = 4 79 | SIMPLICITY = 5 80 | RANDOM = 6 81 | 82 | 83 | class SalienceEvaluation(IntEnum): 84 | WHEN_DEFINED = lib.WHEN_DEFINED 85 | WHEN_ACTIVATED = lib.WHEN_ACTIVATED 86 | EVERY_CYCLE = lib.EVERY_CYCLE 87 | 88 | 89 | class Verbosity(IntEnum): 90 | VERBOSE = 0 91 | SUCCINT = 1 92 | TERSE = 2 93 | 94 | 95 | class TemplateSlotDefaultType(IntEnum): 96 | NO_DEFAULT = lib.NO_DEFAULT 97 | STATIC_DEFAULT = lib.STATIC_DEFAULT 98 | DYNAMIC_DEFAULT = lib.DYNAMIC_DEFAULT 99 | 100 | 101 | class PutSlotError(IntEnum): 102 | PSE_NO_ERROR = lib.PSE_NO_ERROR 103 | PSE_NULL_POINTER_ERROR = lib.PSE_NULL_POINTER_ERROR 104 | PSE_INVALID_TARGET_ERROR = lib.PSE_INVALID_TARGET_ERROR 105 | PSE_SLOT_NOT_FOUND_ERROR = lib.PSE_SLOT_NOT_FOUND_ERROR 106 | PSE_TYPE_ERROR = lib.PSE_TYPE_ERROR 107 | PSE_RANGE_ERROR = lib.PSE_RANGE_ERROR 108 | PSE_ALLOWED_VALUES_ERROR = lib.PSE_ALLOWED_VALUES_ERROR 109 | PSE_CARDINALITY_ERROR = lib.PSE_CARDINALITY_ERROR 110 | PSE_ALLOWED_CLASSES_ERROR = lib.PSE_ALLOWED_CLASSES_ERROR 111 | 112 | 113 | PUT_SLOT_ERROR = {PutSlotError.PSE_NULL_POINTER_ERROR: 114 | lambda s: RuntimeError("Internal error '%s'" % s), 115 | PutSlotError.PSE_INVALID_TARGET_ERROR: 116 | lambda s: ValueError("invalid target for slot '%s'" % s), 117 | PutSlotError.PSE_SLOT_NOT_FOUND_ERROR: 118 | lambda s: KeyError("slot '%s' does not exist" % s), 119 | PutSlotError.PSE_TYPE_ERROR: 120 | lambda s: TypeError("invalid type for slot '%s'" % s), 121 | PutSlotError.PSE_RANGE_ERROR: 122 | lambda s: ValueError("invalid range for slot '%s'" % s), 123 | PutSlotError.PSE_ALLOWED_VALUES_ERROR: 124 | lambda s: ValueError("value not allowed for slot '%s'" % s), 125 | PutSlotError.PSE_CARDINALITY_ERROR: 126 | lambda s: IndexError("invalid cardinality for slot '%s'" % s), 127 | PutSlotError.PSE_ALLOWED_CLASSES_ERROR: 128 | lambda s: ValueError("class not allowed for slot '%s'" % s)} 129 | 130 | 131 | def initialize_environment_data(env: ffi.CData) -> 'EnvData': 132 | fact = lib.CreateFactBuilder(env, ffi.NULL) 133 | if fact is ffi.NULL: 134 | raise CLIPSError(env, code=lib.FBError(env)) 135 | instance = lib.CreateInstanceBuilder(env, ffi.NULL) 136 | if fact is ffi.NULL: 137 | raise CLIPSError(env, code=lib.FBError(env)) 138 | function = lib.CreateFunctionCallBuilder(env, 0) 139 | if fact is ffi.NULL: 140 | raise CLIPSError(env, code=lib.FBError(env)) 141 | multifield = lib.CreateMultifieldBuilder(env, 0) 142 | if multifield is ffi.NULL: 143 | raise CLIPSError(env) 144 | string = lib.CreateStringBuilder(env, 0) 145 | if string is ffi.NULL: 146 | raise CLIPSError(env) 147 | builders = EnvBuilders(fact, instance, function, string, multifield) 148 | 149 | fact = lib.CreateFactModifier(env, ffi.NULL) 150 | if fact is ffi.NULL: 151 | raise CLIPSError(env, code=lib.FMError(env)) 152 | instance = lib.CreateInstanceModifier(env, ffi.NULL) 153 | if instance is ffi.NULL: 154 | raise CLIPSError(env, code=lib.FMError(env)) 155 | modifiers = EnvModifiers(fact, instance) 156 | 157 | functions = UserFunctions({}, {}) 158 | 159 | ENVIRONMENT_DATA[env] = EnvData(builders, modifiers, {}, functions) 160 | 161 | lib.DefinePythonFunction(env) 162 | 163 | return ENVIRONMENT_DATA[env] 164 | 165 | 166 | def delete_environment_data(env: ffi.CData): 167 | data = ENVIRONMENT_DATA.pop(env, None) 168 | 169 | if data is not None: 170 | fact, instance, function, string, multifield = data.builders 171 | 172 | lib.FBDispose(fact) 173 | lib.IBDispose(instance) 174 | lib.FCBDispose(function) 175 | lib.SBDispose(string) 176 | lib.MBDispose(multifield) 177 | 178 | fact, instance = data.modifiers 179 | lib.FMDispose(fact) 180 | lib.IMDispose(instance) 181 | 182 | 183 | def environment_data(env: ffi.CData, name: str) -> type: 184 | """Retrieve Environment specific data.""" 185 | return getattr(ENVIRONMENT_DATA[env], name) 186 | 187 | 188 | def environment_builder(env: ffi.CData, name: str) -> ffi.CData: 189 | """Retrieve Environment specific builder.""" 190 | return getattr(ENVIRONMENT_DATA[env].builders, name) 191 | 192 | 193 | def environment_modifier(env: ffi.CData, name: str) -> ffi.CData: 194 | """Retrieve Environment specific modifier.""" 195 | return getattr(ENVIRONMENT_DATA[env].modifiers, name) 196 | 197 | 198 | ENVIRONMENT_DATA = {} 199 | EnvData = namedtuple('EnvData', ('builders', 200 | 'modifiers', 201 | 'routers', 202 | 'user_functions')) 203 | EnvBuilders = namedtuple('EnvBuilders', ('fact', 204 | 'instance', 205 | 'function', 206 | 'string', 207 | 'multifield')) 208 | EnvModifiers = namedtuple('EnvModifiers', ('fact', 209 | 'instance')) 210 | UserFunctions = namedtuple('UserFunctions', ('functions', 211 | 'external_addresses')) 212 | -------------------------------------------------------------------------------- /clips/environment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import clips 31 | 32 | from clips.facts import Facts 33 | from clips.agenda import Agenda 34 | from clips.classes import Classes 35 | from clips.modules import Modules 36 | from clips.functions import Functions 37 | from clips.routers import Routers, ErrorRouter 38 | from clips.common import CLIPSError 39 | from clips.common import initialize_environment_data, delete_environment_data 40 | 41 | from clips._clips import lib 42 | 43 | 44 | class Environment: 45 | """The environment class encapsulates an independent CLIPS engine 46 | with its own data structures. 47 | 48 | """ 49 | 50 | __slots__ = ('_env', '_facts', '_agenda', '_classes', 51 | '_modules', '_functions', '_routers', '_namespaces') 52 | 53 | def __init__(self): 54 | self._env = lib.CreateEnvironment() 55 | 56 | initialize_environment_data(self._env) 57 | 58 | self._facts = Facts(self._env) 59 | self._agenda = Agenda(self._env) 60 | self._classes = Classes(self._env) 61 | self._modules = Modules(self._env) 62 | self._functions = Functions(self._env) 63 | self._routers = Routers(self._env) 64 | 65 | self._routers.add_router(ErrorRouter()) 66 | 67 | # mapping between the namespace and the methods it exposes 68 | self._namespaces = {m: n for n in (self._facts, 69 | self._agenda, 70 | self._classes, 71 | self._modules, 72 | self._functions, 73 | self._routers) 74 | for m in dir(n) if not m.startswith('_')} 75 | 76 | def __del__(self): 77 | try: 78 | delete_environment_data(self._env) 79 | lib.DestroyEnvironment(self._env) 80 | except (AttributeError, KeyError, TypeError): 81 | pass # mostly happening during interpreter shutdown 82 | 83 | def __getattr__(self, attr): 84 | try: 85 | return getattr(self._namespaces[attr], attr) 86 | except (KeyError, AttributeError): 87 | raise AttributeError("'%s' object has no attribute '%s'" % 88 | (self.__class__.__name__, attr)) 89 | 90 | def __setattr__(self, attr, value): 91 | if attr in self.__slots__: 92 | super(Environment, self).__setattr__(attr, value) 93 | return 94 | 95 | try: 96 | setattr(self._namespaces[attr], attr, value) 97 | except (KeyError, AttributeError): 98 | raise AttributeError("'%s' object has no attribute '%s'" % 99 | (self.__class__.__name__, attr)) 100 | 101 | def __dir__(self): 102 | return dir(self.__class__) + list(self._namespaces.keys()) 103 | 104 | def load(self, path: str, binary: bool = False): 105 | """Load a set of constructs into the CLIPS data base. 106 | 107 | If constructs were saved in binary format, 108 | the binary parameter should be set to True. 109 | 110 | Equivalent to the CLIPS (load) function. 111 | 112 | """ 113 | if binary: 114 | if not lib.Bload(self._env, path.encode()): 115 | raise CLIPSError(self._env) 116 | else: 117 | ret = lib.Load(self._env, path.encode()) 118 | if ret != lib.LE_NO_ERROR: 119 | raise CLIPSError(self._env, code=ret) 120 | 121 | def save(self, path: str, binary=False): 122 | """Save a set of constructs into the CLIPS data base. 123 | 124 | If binary is True, the constructs will be saved in binary format. 125 | 126 | Equivalent to the CLIPS (load) function. 127 | 128 | """ 129 | if binary: 130 | ret = lib.Bsave(self._env, path.encode()) 131 | else: 132 | ret = lib.Save(self._env, path.encode()) 133 | if ret == 0: 134 | raise CLIPSError(self._env) 135 | 136 | def batch_star(self, path: str): 137 | """Evaluate the commands contained in the specific path. 138 | 139 | Equivalent to the CLIPS (batch*) function. 140 | 141 | """ 142 | if lib.BatchStar(self._env, path.encode()) != 1: 143 | raise CLIPSError(self._env) 144 | 145 | def build(self, construct: str): 146 | """Build a single construct in CLIPS. 147 | 148 | Equivalent to the CLIPS (build) function. 149 | 150 | """ 151 | ret = lib.Build(self._env, construct.encode()) 152 | if ret != lib.BE_NO_ERROR: 153 | raise CLIPSError(self._env, code=ret) 154 | 155 | def eval(self, expression: str) -> type: 156 | """Evaluate an expression returning its value. 157 | 158 | Equivalent to the CLIPS (eval) function. 159 | 160 | """ 161 | value = clips.values.clips_value(self._env) 162 | 163 | ret = lib.Eval(self._env, expression.encode(), value) 164 | if ret != lib.EE_NO_ERROR: 165 | raise CLIPSError(self._env, code=ret) 166 | 167 | return clips.values.python_value(self._env, value) 168 | 169 | def reset(self): 170 | """Reset the CLIPS environment. 171 | 172 | Equivalent to the CLIPS (reset) function. 173 | 174 | """ 175 | if lib.Reset(self._env): 176 | raise CLIPSError(self._env) 177 | 178 | def clear(self): 179 | """Clear the CLIPS environment. 180 | 181 | Equivalent to the CLIPS (clear) function. 182 | 183 | """ 184 | if not lib.Clear(self._env): 185 | raise CLIPSError(self._env) 186 | -------------------------------------------------------------------------------- /clips/facts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module contains the definition of: 31 | 32 | * ImpliedFact class 33 | * TemplateFact class 34 | * Template class 35 | * TemplateSlot class 36 | * DefinedFacts class 37 | * Facts namespace class 38 | 39 | """ 40 | 41 | import os 42 | 43 | from itertools import chain 44 | 45 | import clips 46 | 47 | from clips.modules import Module 48 | from clips.common import PutSlotError, PUT_SLOT_ERROR 49 | from clips.common import environment_builder, environment_modifier 50 | from clips.common import CLIPSError, SaveMode, TemplateSlotDefaultType 51 | 52 | from clips._clips import lib, ffi 53 | 54 | 55 | class Fact: 56 | """CLIPS Fact base class.""" 57 | 58 | __slots__ = '_env', '_fact' 59 | 60 | def __init__(self, env: ffi.CData, fact: ffi.CData): 61 | self._env = env 62 | self._fact = fact 63 | lib.RetainFact(self._fact) 64 | 65 | def __del__(self): 66 | try: 67 | lib.ReleaseFact(self._env, self._fact) 68 | except (AttributeError, TypeError): 69 | pass # mostly happening during interpreter shutdown 70 | 71 | def __hash__(self): 72 | return hash(self._fact) 73 | 74 | def __eq__(self, fact): 75 | return self._fact == fact._fact 76 | 77 | def __str__(self): 78 | return ' '.join(fact_pp_string(self._env, self._fact).split()) 79 | 80 | def __repr__(self): 81 | string = ' '.join(fact_pp_string(self._env, self._fact).split()) 82 | 83 | return "%s: %s" % (self.__class__.__name__, string) 84 | 85 | @property 86 | def index(self) -> int: 87 | """The fact index.""" 88 | return lib.FactIndex(self._fact) 89 | 90 | @property 91 | def exists(self) -> bool: 92 | """True if the fact has been asserted within CLIPS. 93 | 94 | Equivalent to the CLIPS (fact-existp) function. 95 | 96 | """ 97 | return lib.FactExistp(self._fact) 98 | 99 | @property 100 | def template(self) -> 'Template': 101 | """The associated Template.""" 102 | template = lib.FactDeftemplate(self._fact) 103 | name = ffi.string(lib.DeftemplateName(template)).decode() 104 | return Template(self._env, name) 105 | 106 | def retract(self): 107 | """Retract the fact from the CLIPS environment.""" 108 | ret = lib.Retract(self._fact) 109 | if ret != lib.RE_NO_ERROR: 110 | raise CLIPSError(self._env, code=ret) 111 | 112 | 113 | class ImpliedFact(Fact): 114 | """An Implied Fact or Ordered Fact represents its data as a list 115 | of elements similarly as for a Multifield. 116 | 117 | Implied Fact cannot be build or modified. 118 | They can be asserted via the Environment.assert_string() method. 119 | 120 | """ 121 | 122 | def __iter__(self): 123 | return chain(slot_value(self._env, self._fact)) 124 | 125 | def __len__(self): 126 | return len(slot_value(self._env, self._fact)) 127 | 128 | def __getitem__(self, index): 129 | return slot_value(self._env, self._fact)[index] 130 | 131 | 132 | class TemplateFact(Fact): 133 | """A Template or Unordered Fact represents its data as a dictionary 134 | where each slot name is a key. 135 | 136 | TemplateFact slot values can be modified. 137 | The Fact will be re-evaluated against the rule network once modified. 138 | 139 | """ 140 | 141 | __slots__ = '_env', '_fact' 142 | 143 | def __init__(self, env: ffi.CData, fact: ffi.CData): 144 | super().__init__(env, fact) 145 | 146 | def __iter__(self): 147 | return chain(slot_values(self._env, self._fact)) 148 | 149 | def __len__(self): 150 | slots = slot_values(self._env, self._fact) 151 | 152 | return len(tuple(slots)) 153 | 154 | def __getitem__(self, key): 155 | try: 156 | return slot_value(self._env, self._fact, slot=str(key)) 157 | except CLIPSError as error: 158 | if error.code == lib.GSE_SLOT_NOT_FOUND_ERROR: 159 | raise KeyError("'%s'" % key) 160 | else: 161 | raise error 162 | 163 | def modify_slots(self, **slots): 164 | """Modify one or more slot values of the Fact. 165 | 166 | Fact must be asserted within the CLIPS engine. 167 | 168 | Equivalent to the CLIPS (modify) function. 169 | 170 | """ 171 | modifier = environment_modifier(self._env, 'fact') 172 | ret = lib.FMSetFact(modifier, self._fact) 173 | if ret != lib.FME_NO_ERROR: 174 | raise CLIPSError(self._env, code=ret) 175 | 176 | for slot, slot_val in slots.items(): 177 | value = clips.values.clips_value(self._env, value=slot_val) 178 | 179 | ret = lib.FMPutSlot(modifier, str(slot).encode(), value) 180 | if ret != PutSlotError.PSE_NO_ERROR: 181 | raise PUT_SLOT_ERROR[ret](slot) 182 | 183 | if lib.FMModify(modifier) is ffi.NULL: 184 | raise CLIPSError(self._env, code=lib.FBError(self._env)) 185 | 186 | 187 | class Template: 188 | """A Fact Template is a formal representation of the fact data structure. 189 | 190 | In CLIPS, Templates are defined via the (deftemplate) function. 191 | 192 | Templates allow to assert new facts within the CLIPS environment. 193 | 194 | Implied facts are associated to implied templates. Implied templates 195 | have a limited set of features. 196 | 197 | """ 198 | 199 | __slots__ = '_env', '_name' 200 | 201 | def __init__(self, env: ffi.CData, name: str): 202 | self._env = env 203 | self._name = name.encode() 204 | 205 | def __hash__(self): 206 | return hash(self._ptr()) 207 | 208 | def __eq__(self, tpl): 209 | return self._ptr() == tpl._ptr() 210 | 211 | def __str__(self): 212 | string = lib.DeftemplatePPForm(self._ptr()) 213 | string = ffi.string(string).decode() if string != ffi.NULL else '' 214 | 215 | return ' '.join(string.split()) 216 | 217 | def __repr__(self): 218 | string = lib.DeftemplatePPForm(self._ptr()) 219 | string = ffi.string(string).decode() if string != ffi.NULL else '' 220 | 221 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 222 | 223 | def _ptr(self) -> ffi.CData: 224 | tpl = lib.FindDeftemplate(self._env, self._name) 225 | if tpl == ffi.NULL: 226 | raise CLIPSError(self._env, 'Template <%s> not defined' % self.name) 227 | 228 | return tpl 229 | 230 | @property 231 | def implied(self) -> bool: 232 | """True if the Template is implied.""" 233 | return lib.ImpliedDeftemplate(self._ptr()) 234 | 235 | @property 236 | def name(self) -> str: 237 | """Template name.""" 238 | return self._name.decode() 239 | 240 | @property 241 | def module(self) -> Module: 242 | """The module in which the Template is defined. 243 | 244 | Python equivalent of the CLIPS deftemplate-module command. 245 | 246 | """ 247 | name = ffi.string(lib.DeftemplateModule(self._ptr())).decode() 248 | 249 | return Module(self._env, name) 250 | 251 | @property 252 | def deletable(self) -> bool: 253 | """True if the Template can be undefined.""" 254 | return lib.DeftemplateIsDeletable(self._ptr()) 255 | 256 | @property 257 | def slots(self) -> tuple: 258 | """The slots of the template.""" 259 | if self.implied: 260 | return () 261 | 262 | value = clips.values.clips_value(self._env) 263 | 264 | lib.DeftemplateSlotNames(self._ptr(), value) 265 | 266 | return tuple(TemplateSlot(self._env, self.name, n) 267 | for n in clips.values.python_value(self._env, value)) 268 | 269 | @property 270 | def watch(self) -> bool: 271 | """Whether or not the Template is being watched.""" 272 | return lib.DeftemplateGetWatch(self._ptr()) 273 | 274 | @watch.setter 275 | def watch(self, flag: bool): 276 | """Whether or not the Template is being watched.""" 277 | lib.DeftemplateSetWatch(self._ptr(), flag) 278 | 279 | def facts(self) -> iter: 280 | """Iterate over the asserted Facts belonging to this Template.""" 281 | fact = lib.GetNextFactInTemplate(self._ptr(), ffi.NULL) 282 | while fact != ffi.NULL: 283 | yield new_fact(self._env, fact) 284 | 285 | fact = lib.GetNextFactInTemplate(self._ptr(), fact) 286 | 287 | def assert_fact(self, **slots) -> TemplateFact: 288 | """Assert a new fact with the given slot values. 289 | 290 | Only deftemplates that have been explicitly defined can be asserted 291 | with this function. 292 | 293 | Equivalent to the CLIPS (assert) function. 294 | 295 | """ 296 | builder = environment_builder(self._env, 'fact') 297 | ret = lib.FBSetDeftemplate(builder, self._name) 298 | if ret != lib.FBE_NO_ERROR: 299 | raise CLIPSError(self._env, code=ret) 300 | 301 | for slot, slot_val in slots.items(): 302 | value = clips.values.clips_value(self._env, value=slot_val) 303 | 304 | ret = lib.FBPutSlot(builder, str(slot).encode(), value) 305 | if ret != PutSlotError.PSE_NO_ERROR: 306 | raise PUT_SLOT_ERROR[ret](slot) 307 | 308 | fact = lib.FBAssert(builder) 309 | if fact != ffi.NULL: 310 | return TemplateFact(self._env, fact) 311 | else: 312 | raise CLIPSError(self._env, code=lib.FBError(self._env)) 313 | 314 | def undefine(self): 315 | """Undefine the Template. 316 | 317 | Equivalent to the CLIPS (undeftemplate) function. 318 | 319 | The object becomes unusable after this method has been called. 320 | 321 | """ 322 | if not lib.Undeftemplate(self._ptr(), self._env): 323 | raise CLIPSError(self._env) 324 | 325 | 326 | class TemplateSlot: 327 | """Template Facts organize the information within Slots. 328 | 329 | Slots might restrict the type or amount of data they store. 330 | 331 | """ 332 | 333 | __slots__ = '_env', '_tpl', '_name' 334 | 335 | def __init__(self, env: ffi.CData, tpl: str, name: str): 336 | self._env = env 337 | self._tpl = tpl.encode() 338 | self._name = name.encode() 339 | 340 | def __hash__(self): 341 | return hash(self._ptr()) + hash(self._name) 342 | 343 | def __eq__(self, slot): 344 | return self._ptr() == slot._ptr() and self._name == slot._name 345 | 346 | def __str__(self): 347 | return self.name 348 | 349 | def __repr__(self): 350 | return "%s: %s" % (self.__class__.__name__, self.name) 351 | 352 | def _ptr(self) -> ffi.CData: 353 | tpl = lib.FindDeftemplate(self._env, self._tpl) 354 | if tpl == ffi.NULL: 355 | raise CLIPSError( 356 | self._env, 'Template <%s> not defined' % self._tpl.decode()) 357 | 358 | return tpl 359 | 360 | @property 361 | def name(self) -> str: 362 | """The slot name.""" 363 | return self._name.decode() 364 | 365 | @property 366 | def multifield(self) -> bool: 367 | """True if the slot is a multifield slot.""" 368 | return bool(lib.DeftemplateSlotMultiP(self._ptr(), self._name)) 369 | 370 | @property 371 | def types(self) -> tuple: 372 | """A tuple containing the value types for this Slot. 373 | 374 | Equivalent to the CLIPS (deftemplate-slot-types) function. 375 | 376 | """ 377 | value = clips.values.clips_value(self._env) 378 | 379 | if lib.DeftemplateSlotTypes(self._ptr(), self._name, value): 380 | return clips.values.python_value(self._env, value) 381 | 382 | raise CLIPSError(self._env) 383 | 384 | @property 385 | def range(self) -> tuple: 386 | """A tuple containing the numeric range for this Slot. 387 | 388 | Equivalent to the CLIPS (deftemplate-slot-range) function. 389 | 390 | """ 391 | value = clips.values.clips_value(self._env) 392 | 393 | if lib.DeftemplateSlotRange(self._ptr(), self._name, value): 394 | return clips.values.python_value(self._env, value) 395 | 396 | raise CLIPSError(self._env) 397 | 398 | @property 399 | def cardinality(self) -> tuple: 400 | """A tuple containing the cardinality for this Slot. 401 | 402 | Equivalent to the CLIPS (deftemplate-slot-cardinality) function. 403 | 404 | """ 405 | value = clips.values.clips_value(self._env) 406 | 407 | if lib.DeftemplateSlotCardinality(self._ptr(), self._name, value): 408 | return clips.values.python_value(self._env, value) 409 | 410 | raise CLIPSError(self._env) 411 | 412 | @property 413 | def default_type(self) -> TemplateSlotDefaultType: 414 | """The default value type for this Slot. 415 | 416 | Equivalent to the CLIPS (deftemplate-slot-defaultp) function. 417 | 418 | """ 419 | return TemplateSlotDefaultType( 420 | lib.DeftemplateSlotDefaultP(self._ptr(), self._name)) 421 | 422 | @property 423 | def default_value(self) -> type: 424 | """The default value for this Slot. 425 | 426 | Equivalent to the CLIPS (deftemplate-slot-default-value) function. 427 | 428 | """ 429 | value = clips.values.clips_value(self._env) 430 | 431 | if lib.DeftemplateSlotDefaultValue(self._ptr(), self._name, value): 432 | return clips.values.python_value(self._env, value) 433 | 434 | raise CLIPSError(self._env) 435 | 436 | @property 437 | def allowed_values(self) -> tuple: 438 | """A tuple containing the allowed values for this Slot. 439 | 440 | Equivalent to the CLIPS (slot-allowed-values) function. 441 | 442 | """ 443 | value = clips.values.clips_value(self._env) 444 | 445 | if lib.DeftemplateSlotAllowedValues(self._ptr(), self._name, value): 446 | return clips.values.python_value(self._env, value) 447 | 448 | raise CLIPSError(self._env) 449 | 450 | 451 | class DefinedFacts: 452 | """The DefinedFacts constitute a set of a priori 453 | or initial knowledge specified as a collection of facts of user 454 | defined classes. 455 | 456 | When the CLIPS environment is reset, every fact specified 457 | within a deffacts construct in the CLIPS knowledge base 458 | is added to the DefinedFacts list. 459 | 460 | """ 461 | 462 | __slots__ = '_env', '_name' 463 | 464 | def __init__(self, env: ffi.CData, name: str): 465 | self._env = env 466 | self._name = name.encode() 467 | 468 | def __hash__(self): 469 | return hash(self._ptr()) 470 | 471 | def __eq__(self, dfc): 472 | return self._ptr() == dfc._ptr() 473 | 474 | def __str__(self): 475 | string = lib.DeffactsPPForm(self._ptr()) 476 | string = ffi.string(string).decode() if string != ffi.NULL else '' 477 | 478 | return ' '.join(string.split()) 479 | 480 | def __repr__(self): 481 | string = lib.DeffactsPPForm(self._ptr()) 482 | string = ffi.string(string).decode() if string != ffi.NULL else '' 483 | 484 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 485 | 486 | def _ptr(self) -> ffi.CData: 487 | dfc = lib.FindDeffacts(self._env, self._name) 488 | if dfc == ffi.NULL: 489 | raise CLIPSError( 490 | self._env, 'DefinedFacts <%s> not defined' % self.name) 491 | 492 | return dfc 493 | 494 | @property 495 | def name(self) -> str: 496 | """DefinedFacts name.""" 497 | return self._name.decode() 498 | 499 | @property 500 | def module(self) -> Module: 501 | """The module in which the DefinedFacts is defined. 502 | 503 | Python equivalent of the CLIPS (deffacts-module) command. 504 | 505 | """ 506 | name = ffi.string(lib.DeffactsModule(self._ptr())).decode() 507 | 508 | return Module(self._env, name) 509 | 510 | @property 511 | def deletable(self) -> bool: 512 | """True if the DefinedFacts can be undefined.""" 513 | return lib.DeffactsIsDeletable(self._ptr()) 514 | 515 | def undefine(self): 516 | """Undefine the DefinedFacts. 517 | 518 | Equivalent to the CLIPS (undeffacts) function. 519 | 520 | The object becomes unusable after this method has been called. 521 | 522 | """ 523 | if not lib.Undeffacts(self._ptr(), self._env): 524 | raise CLIPSError(self._env) 525 | 526 | 527 | class Facts: 528 | """Facts and Templates namespace class. 529 | 530 | .. note:: 531 | 532 | All the Facts methods are accessible through the Environment class. 533 | 534 | """ 535 | 536 | __slots__ = ['_env'] 537 | 538 | def __init__(self, env): 539 | self._env = env 540 | 541 | @property 542 | def fact_duplication(self) -> bool: 543 | """Whether or not duplicate facts are allowed.""" 544 | return lib.GetFactDuplication(self._env) 545 | 546 | @fact_duplication.setter 547 | def fact_duplication(self, duplication: bool) -> bool: 548 | return lib.SetFactDuplication(self._env, duplication) 549 | 550 | def facts(self) -> iter: 551 | """Iterate over the asserted Facts.""" 552 | fact = lib.GetNextFact(self._env, ffi.NULL) 553 | while fact != ffi.NULL: 554 | yield new_fact(self._env, fact) 555 | 556 | fact = lib.GetNextFact(self._env, fact) 557 | 558 | def templates(self) -> iter: 559 | """Iterate over the defined Templates.""" 560 | template = lib.GetNextDeftemplate(self._env, ffi.NULL) 561 | while template != ffi.NULL: 562 | name = ffi.string(lib.DeftemplateName(template)).decode() 563 | yield Template(self._env, name) 564 | 565 | template = lib.GetNextDeftemplate(self._env, template) 566 | 567 | def find_template(self, name: str) -> Template: 568 | """Find the Template by its name.""" 569 | tpl = lib.FindDeftemplate(self._env, name.encode()) 570 | if tpl == ffi.NULL: 571 | raise LookupError("Template '%s' not found" % name) 572 | 573 | return Template(self._env, name) 574 | 575 | def defined_facts(self) -> iter: 576 | """Iterate over the DefinedFacts.""" 577 | deffacts = lib.GetNextDeffacts(self._env, ffi.NULL) 578 | while deffacts != ffi.NULL: 579 | name = ffi.string(lib.DeffactsName(deffacts)).decode() 580 | yield DefinedFacts(self._env, name) 581 | 582 | deffacts = lib.GetNextDeffacts(self._env, deffacts) 583 | 584 | def find_defined_facts(self, name: str) -> DefinedFacts: 585 | """Find the DefinedFacts by its name.""" 586 | dfs = lib.FindDeffacts(self._env, name.encode()) 587 | if dfs == ffi.NULL: 588 | raise LookupError("DefinedFacts '%s' not found" % name) 589 | 590 | return DefinedFacts(self._env, name) 591 | 592 | def assert_string(self, string: str) -> (ImpliedFact, TemplateFact): 593 | """Assert a fact as string.""" 594 | fact = lib.AssertString(self._env, string.encode()) 595 | 596 | if fact == ffi.NULL: 597 | raise CLIPSError( 598 | self._env, code=lib.GetAssertStringError(self._env)) 599 | 600 | return new_fact(self._env, fact) 601 | 602 | def load_facts(self, facts: str): 603 | """Load a set of facts into the CLIPS data base. 604 | 605 | Equivalent to the CLIPS (load-facts) function. 606 | 607 | Facts can be loaded from a string or from a text file. 608 | 609 | """ 610 | facts = facts.encode() 611 | 612 | if os.path.exists(facts): 613 | if not lib.LoadFacts(self._env, facts): 614 | raise CLIPSError(self._env) 615 | else: 616 | if not lib.LoadFactsFromString(self._env, facts, len(facts)): 617 | raise CLIPSError(self._env) 618 | 619 | def save_facts(self, path, mode=SaveMode.LOCAL_SAVE): 620 | """Save the facts in the system to the specified file. 621 | 622 | Equivalent to the CLIPS (save-facts) function. 623 | 624 | """ 625 | if not lib.SaveFacts(self._env, path.encode(), mode): 626 | raise CLIPSError(self._env) 627 | 628 | 629 | def new_fact(env: ffi.CData, fact: ffi.CData) -> (ImpliedFact, TemplateFact): 630 | if lib.ImpliedDeftemplate(lib.FactDeftemplate(fact)): 631 | return ImpliedFact(env, fact) 632 | else: 633 | return TemplateFact(env, fact) 634 | 635 | 636 | def slot_value(env: ffi.CData, fact: ffi.CData, slot: str = None) -> type: 637 | value = clips.values.clips_value(env) 638 | slot = slot.encode() if slot is not None else ffi.NULL 639 | implied = lib.ImpliedDeftemplate(lib.FactDeftemplate(fact)) 640 | 641 | if not implied and slot == ffi.NULL: 642 | raise ValueError() 643 | 644 | ret = lib.GetFactSlot(fact, slot, value) 645 | if ret != lib.GSE_NO_ERROR: 646 | raise CLIPSError(env, code=ret) 647 | 648 | return clips.values.python_value(env, value) 649 | 650 | 651 | def slot_values(env: ffi.CData, fact: ffi.CData) -> iter: 652 | value = clips.values.clips_value(env) 653 | lib.FactSlotNames(fact, value) 654 | 655 | return ((s, slot_value(env, fact, slot=s)) 656 | for s in clips.values.python_value(env, value)) 657 | 658 | 659 | def fact_pp_string(env: ffi.CData, fact: ffi.CData) -> str: 660 | builder = environment_builder(env, 'string') 661 | lib.SBReset(builder) 662 | lib.FactPPForm(fact, builder, False) 663 | 664 | return ffi.string(builder.contents).decode() 665 | -------------------------------------------------------------------------------- /clips/functions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module contains the definition of: 31 | 32 | * Function class 33 | * Generic class 34 | * Method class 35 | * Functions namespace class 36 | 37 | """ 38 | 39 | import traceback 40 | from typing import Union 41 | 42 | import clips 43 | 44 | from clips.modules import Module 45 | from clips.common import CLIPSError, environment_builder, environment_data 46 | 47 | from clips._clips import lib, ffi 48 | 49 | 50 | class Function: 51 | """A CLIPS user defined Function. 52 | 53 | In CLIPS, Functions are defined via the (deffunction) statement. 54 | 55 | """ 56 | 57 | __slots__ = '_env', '_name' 58 | 59 | def __init__(self, env: ffi.CData, name: str): 60 | self._env = env 61 | self._name = name.encode() 62 | 63 | def __hash__(self): 64 | return hash(self._ptr()) 65 | 66 | def __eq__(self, fnc): 67 | return self._ptr() == fnc._ptr() 68 | 69 | def __str__(self): 70 | string = lib.DeffunctionPPForm(self._ptr()) 71 | string = ffi.string(string).decode() if string != ffi.NULL else '' 72 | 73 | return ' '.join(string.split()) 74 | 75 | def __repr__(self): 76 | string = lib.DeffunctionPPForm(self._ptr()) 77 | string = ffi.string(string).decode() if string != ffi.NULL else '' 78 | 79 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 80 | 81 | def __call__(self, *arguments): 82 | """Call the CLIPS function with the given arguments.""" 83 | value = clips.values.clips_value(self._env) 84 | builder = environment_builder(self._env, 'function') 85 | 86 | lib.FCBReset(builder) 87 | for argument in arguments: 88 | lib.FCBAppend( 89 | builder, clips.values.clips_value(self._env, value=argument)) 90 | 91 | ret = lib.FCBCall(builder, lib.DeffunctionName(self._ptr()), value) 92 | if ret != lib.FCBE_NO_ERROR: 93 | raise CLIPSError(self._env, code=ret) 94 | 95 | return clips.values.python_value(self._env, value) 96 | 97 | def _ptr(self) -> ffi.CData: 98 | dfc = lib.FindDeffunction(self._env, self._name) 99 | if dfc == ffi.NULL: 100 | raise CLIPSError( 101 | self._env, 'Function <%s> not defined' % self.name) 102 | 103 | return dfc 104 | 105 | @property 106 | def name(self) -> str: 107 | """Function name.""" 108 | return self._name.decode() 109 | 110 | @property 111 | def module(self) -> Module: 112 | """The module in which the Function is defined. 113 | 114 | Equivalent to the CLIPS (deffunction-module) functions. 115 | 116 | """ 117 | name = ffi.string(lib.DeffunctionModule(self._ptr())).decode() 118 | 119 | return Module(self._env, name) 120 | 121 | @property 122 | def deletable(self) -> bool: 123 | """True if the Function can be deleted.""" 124 | return lib.DeffunctionIsDeletable(self._ptr()) 125 | 126 | @property 127 | def watch(self) -> bool: 128 | """Whether or not the Function is being watched.""" 129 | return lib.DeffunctionGetWatch(self._ptr()) 130 | 131 | @watch.setter 132 | def watch(self, flag: bool): 133 | """Whether or not the Function is being watched.""" 134 | lib.DeffunctionSetWatch(self._ptr(), flag) 135 | 136 | def undefine(self): 137 | """Undefine the Function. 138 | 139 | Equivalent to the CLIPS (undeffunction) command. 140 | 141 | The object becomes unusable after this method has been called. 142 | 143 | """ 144 | if not lib.Undeffunction(self._ptr(), self._env): 145 | raise CLIPSError(self._env) 146 | 147 | 148 | class Generic: 149 | """A CLIPS Generic Function. 150 | 151 | In CLIPS, Generic Functions are defined via the (defgeneric) statement. 152 | 153 | """ 154 | 155 | __slots__ = '_env', '_name' 156 | 157 | def __init__(self, env: ffi.CData, name: str): 158 | self._env = env 159 | self._name = name.encode() 160 | 161 | def __hash__(self): 162 | return hash(self._ptr()) 163 | 164 | def __eq__(self, gnc): 165 | return self._ptr() == gnc._ptr() 166 | 167 | def __str__(self): 168 | string = lib.DefgenericPPForm(self._ptr()) 169 | string = ffi.string(string).decode() if string != ffi.NULL else '' 170 | 171 | return ' '.join(string.split()) 172 | 173 | def __repr__(self): 174 | string = lib.DefgenericPPForm(self._ptr()) 175 | string = ffi.string(string).decode() if string != ffi.NULL else '' 176 | 177 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 178 | 179 | def __call__(self, *arguments): 180 | """Call the CLIPS Generic function with the given arguments.""" 181 | value = clips.values.clips_value(self._env) 182 | builder = environment_builder(self._env, 'function') 183 | 184 | lib.FCBReset(builder) 185 | for argument in arguments: 186 | lib.FCBAppend( 187 | builder, clips.values.clips_value(self._env, value=argument)) 188 | 189 | ret = lib.FCBCall(builder, lib.DefgenericName(self._ptr()), value) 190 | if ret != lib.FCBE_NO_ERROR: 191 | raise CLIPSError(self._env, code=ret) 192 | 193 | return clips.values.python_value(self._env, value) 194 | 195 | def _ptr(self) -> ffi.CData: 196 | gnc = lib.FindDefgeneric(self._env, self._name) 197 | if gnc == ffi.NULL: 198 | raise CLIPSError( 199 | self._env, 'Generic <%s> not defined' % self.name) 200 | 201 | return gnc 202 | 203 | @property 204 | def name(self) -> str: 205 | """Generic name.""" 206 | return self._name.decode() 207 | 208 | @property 209 | def module(self) -> Module: 210 | """The module in which the Generic is defined. 211 | 212 | Equivalent to the CLIPS (defgeneric-module) generics. 213 | 214 | """ 215 | name = ffi.string(lib.DefgenericModule(self._ptr())).decode() 216 | 217 | return Module(self._env, name) 218 | 219 | @property 220 | def deletable(self) -> bool: 221 | """True if the Generic can be deleted.""" 222 | return lib.DefgenericIsDeletable(self._ptr()) 223 | 224 | @property 225 | def watch(self) -> bool: 226 | """Whether or not the Generic is being watched.""" 227 | return lib.DefgenericGetWatch(self._ptr()) 228 | 229 | @watch.setter 230 | def watch(self, flag: bool): 231 | """Whether or not the Generic is being watched.""" 232 | lib.DefgenericSetWatch(self._ptr(), flag) 233 | 234 | def methods(self) -> iter: 235 | """Iterates over the defined Methods.""" 236 | index = lib.GetNextDefmethod(self._ptr(), 0) 237 | 238 | while index != 0: 239 | yield Method(self._env, self.name, index) 240 | 241 | index = lib.GetNextDefmethod(self._ptr(), index) 242 | 243 | def undefine(self): 244 | """Undefine the Generic. 245 | 246 | Equivalent to the CLIPS (undefgeneric) command. 247 | 248 | The object becomes unusable after this method has been called. 249 | 250 | """ 251 | if not lib.Undefgeneric(self._ptr(), self._env): 252 | raise CLIPSError(self._env) 253 | 254 | 255 | class Method(object): 256 | """Methods implement the generic logic 257 | according to the input parameter types. 258 | 259 | """ 260 | 261 | __slots__ = '_env', '_gnc', '_idx' 262 | 263 | def __init__(self, env: ffi.CData, gnc: str, idx: int): 264 | self._env = env 265 | self._gnc = gnc.encode() 266 | self._idx = idx 267 | 268 | def __hash__(self): 269 | return hash(self._ptr()) + self._idx 270 | 271 | def __eq__(self, gnc): 272 | return self._ptr() == gnc._ptr() and self._idx == gnc._idx 273 | 274 | def __str__(self): 275 | string = lib.DefmethodPPForm(self._ptr(), self._idx) 276 | string = ffi.string(string).decode() if string != ffi.NULL else '' 277 | 278 | return ' '.join(string.split()) 279 | 280 | def __repr__(self): 281 | string = lib.DefmethodPPForm(self._ptr(), self._idx) 282 | string = ffi.string(string).decode() if string != ffi.NULL else '' 283 | 284 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 285 | 286 | def _ptr(self) -> ffi.CData: 287 | gnc = lib.FindDefgeneric(self._env, self._gnc) 288 | if gnc == ffi.NULL: 289 | raise CLIPSError( 290 | self._env, 'Generic <%s> not defined' % self._gnc) 291 | 292 | return gnc 293 | 294 | @property 295 | def watch(self) -> bool: 296 | """Whether or not the Method is being watched.""" 297 | return lib.DefmethodGetWatch(self._ptr(), self._idx) 298 | 299 | @watch.setter 300 | def watch(self, flag: bool): 301 | """Whether or not the Method is being watched.""" 302 | lib.DefmethodSetWatch(self._ptr(), self._idx, flag) 303 | 304 | @property 305 | def deletable(self): 306 | """True if the Template can be undefined.""" 307 | return lib.DefmethodIsDeletable(self._ptr(), self._idx) 308 | 309 | @property 310 | def restrictions(self) -> tuple: 311 | value = clips.values.clips_value(self._env) 312 | 313 | lib.GetMethodRestrictions(self._ptr(), self._idx, value) 314 | 315 | return clips.values.python_value(self._env, value) 316 | 317 | @property 318 | def description(self) -> str: 319 | builder = environment_builder(self._env, 'string') 320 | lib.SBReset(builder) 321 | lib.DefmethodDescription(self._ptr(), self._idx, builder) 322 | 323 | return ffi.string(builder.contents).decode() 324 | 325 | def undefine(self): 326 | """Undefine the Method. 327 | 328 | Equivalent to the CLIPS (undefmethod) command. 329 | 330 | The object becomes unusable after this method has been called. 331 | 332 | """ 333 | if not lib.Undefmethod(self._ptr(), self._idx, self._env): 334 | raise CLIPSError(self._env) 335 | 336 | 337 | class Functions: 338 | """Functions, Generics and Methods namespace class. 339 | 340 | .. note:: 341 | 342 | All the Functions methods are accessible through the Environment class. 343 | 344 | """ 345 | 346 | __slots__ = ['_env'] 347 | 348 | def __init__(self, env: ffi.CData): 349 | self._env = env 350 | 351 | @property 352 | def error_state(self) -> Union[None, CLIPSError]: 353 | """Get the CLIPS environment error state. 354 | 355 | Equivalent to the CLIPS (get-error) function. 356 | 357 | """ 358 | value = clips.values.clips_udf_value(self._env) 359 | 360 | lib.GetErrorFunction(self._env, ffi.NULL, value) 361 | state = clips.values.python_value(self._env, value) 362 | 363 | if isinstance(state, clips.Symbol): 364 | return None 365 | else: 366 | return CLIPSError(self._env, message=state) 367 | 368 | def clear_error_state(self): 369 | """Clear the CLIPS environment error state. 370 | 371 | Equivalent to the CLIPS (clear-error) function. 372 | 373 | """ 374 | lib.ClearErrorValue(self._env) 375 | 376 | def call(self, function: str, *arguments) -> type: 377 | """Call the CLIPS function with the given arguments.""" 378 | value = clips.values.clips_value(self._env) 379 | builder = environment_builder(self._env, 'function') 380 | 381 | lib.FCBReset(builder) 382 | for argument in arguments: 383 | lib.FCBAppend( 384 | builder, clips.values.clips_value(self._env, value=argument)) 385 | 386 | ret = lib.FCBCall(builder, function.encode(), value) 387 | if ret != lib.FCBE_NO_ERROR: 388 | raise CLIPSError(self._env, code=ret) 389 | 390 | return clips.values.python_value(self._env, value) 391 | 392 | def functions(self): 393 | """Iterates over the defined Globals.""" 394 | deffunction = lib.GetNextDeffunction(self._env, ffi.NULL) 395 | 396 | while deffunction != ffi.NULL: 397 | name = ffi.string(lib.DeffunctionName(deffunction)).decode() 398 | yield Function(self._env, name) 399 | 400 | deffunction = lib.GetNextDeffunction(self._env, deffunction) 401 | 402 | def find_function(self, name: str) -> Function: 403 | """Find the Function by its name.""" 404 | deffunction = lib.FindDeffunction(self._env, name.encode()) 405 | if deffunction == ffi.NULL: 406 | raise LookupError("Function '%s' not found" % name) 407 | 408 | return Function(self._env, name) 409 | 410 | def generics(self) -> iter: 411 | """Iterates over the defined Generics.""" 412 | defgeneric = lib.GetNextDefgeneric(self._env, ffi.NULL) 413 | 414 | while defgeneric != ffi.NULL: 415 | name = ffi.string(lib.DefgenericName(defgeneric)).decode() 416 | yield Generic(self._env, name) 417 | 418 | defgeneric = lib.GetNextDefgeneric(self._env, name) 419 | 420 | def find_generic(self, name: str) -> Generic: 421 | """Find the Generic by its name.""" 422 | defgeneric = lib.FindDefgeneric(self._env, name.encode()) 423 | if defgeneric == ffi.NULL: 424 | raise LookupError("Generic '%s' not found" % name) 425 | 426 | return Generic(self._env, name) 427 | 428 | def define_function(self, function: callable, name: str = None): 429 | """Define the Python function within the CLIPS environment. 430 | 431 | If a name is given, it will be the function name within CLIPS. 432 | Otherwise, the name of the Python function will be used. 433 | 434 | The Python function will be accessible within CLIPS via its name 435 | as if it was defined via the `deffunction` construct. 436 | 437 | """ 438 | name = name if name is not None else function.__name__ 439 | 440 | user_functions = environment_data(self._env, 'user_functions') 441 | user_functions.functions[name] = function 442 | 443 | ret = lib.Build(self._env, DEFFUNCTION.format(name).encode()) 444 | if ret != lib.BE_NO_ERROR: 445 | raise CLIPSError(self._env, code=ret) 446 | 447 | 448 | @ffi.def_extern() 449 | def python_function(env: ffi.CData, context: ffi.CData, output: ffi.CData): 450 | arguments = [] 451 | value = clips.values.clips_udf_value(env) 452 | 453 | if lib.UDFFirstArgument(context, lib.SYMBOL_BIT, value): 454 | funcname = clips.values.python_value(env, value) 455 | else: 456 | lib.UDFThrowError(context) 457 | return 458 | 459 | while lib.UDFHasNextArgument(context): 460 | if lib.UDFNextArgument(context, clips.values.ANY_TYPE_BITS, value): 461 | arguments.append(clips.values.python_value(env, value)) 462 | else: 463 | lib.UDFThrowError(context) 464 | return 465 | 466 | try: 467 | user_functions = environment_data(env, 'user_functions') 468 | ret = user_functions.functions[funcname](*arguments) 469 | except Exception as error: 470 | message = "[PYCODEFUN1] %r" % error 471 | string = "\n".join((message, traceback.format_exc())) 472 | 473 | lib.WriteString(env, 'stderr'.encode(), string.encode()) 474 | clips.values.clips_udf_value(env, message, value) 475 | lib.SetErrorValue(env, value.header) 476 | lib.UDFThrowError(context) 477 | else: 478 | clips.values.clips_udf_value(env, ret, output) 479 | 480 | 481 | DEFFUNCTION = """ 482 | (deffunction {0} ($?args) 483 | (python-function {0} (expand$ ?args))) 484 | """ 485 | -------------------------------------------------------------------------------- /clips/modules.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module contains the definition of: 31 | 32 | * Modules namespace class 33 | * Module class 34 | * Global class 35 | 36 | """ 37 | 38 | import clips 39 | 40 | from clips.common import CLIPSError 41 | 42 | from clips._clips import lib, ffi 43 | 44 | 45 | class Module: 46 | """Modules are namespaces restricting the CLIPS constructs scope.""" 47 | 48 | __slots__ = '_env', '_name' 49 | 50 | def __init__(self, env: ffi.CData, name: str): 51 | self._env = env 52 | self._name = name.encode() 53 | 54 | def __hash__(self): 55 | return hash(self._ptr()) 56 | 57 | def __eq__(self, mdl): 58 | return self._ptr() == mdl._ptr() 59 | 60 | def __str__(self): 61 | string = lib.DefmodulePPForm(self._ptr()) 62 | string = ffi.string(string).decode() if string != ffi.NULL else '' 63 | 64 | return ' '.join(string.split()) 65 | 66 | def __repr__(self): 67 | string = lib.DefmodulePPForm(self._ptr()) 68 | string = ffi.string(string).decode() if string != ffi.NULL else '' 69 | 70 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 71 | 72 | def _ptr(self) -> ffi.CData: 73 | module = lib.FindDefmodule(self._env, self._name) 74 | if module == ffi.NULL: 75 | raise CLIPSError(self._env, 'Module <%s> not defined' % self.name) 76 | 77 | return module 78 | 79 | @property 80 | def name(self) -> str: 81 | """Module name.""" 82 | return self._name.decode() 83 | 84 | 85 | class Global: 86 | """A CLIPS global variable. 87 | 88 | In CLIPS, Globals are defined via the (defglobal) statement. 89 | 90 | """ 91 | 92 | __slots__ = '_env', '_name' 93 | 94 | def __init__(self, env: ffi.CData, name: str): 95 | self._env = env 96 | self._name = name.encode() 97 | 98 | def __hash__(self): 99 | return hash(self._ptr()) 100 | 101 | def __eq__(self, glb): 102 | return self._ptr() == glb._ptr() 103 | 104 | def __str__(self): 105 | string = lib.DefglobalPPForm(self._ptr()) 106 | string = ffi.string(string).decode() if string != ffi.NULL else '' 107 | 108 | return ' '.join(string.split()) 109 | 110 | def __repr__(self): 111 | string = lib.DefglobalPPForm(self._ptr()) 112 | string = ffi.string(string).decode() if string != ffi.NULL else '' 113 | 114 | return "%s: %s" % (self.__class__.__name__, ' '.join(string.split())) 115 | 116 | def _ptr(self) -> ffi.CData: 117 | glb = lib.FindDefglobal(self._env, self._name) 118 | if glb == ffi.NULL: 119 | raise CLIPSError( 120 | self._env, 'Global <%s> not defined' % self.name) 121 | 122 | return glb 123 | 124 | @property 125 | def value(self) -> type: 126 | """Global value.""" 127 | value = clips.values.clips_value(self._env) 128 | 129 | lib.DefglobalGetValue(self._ptr(), value) 130 | 131 | return clips.values.python_value(self._env, value) 132 | 133 | @value.setter 134 | def value(self, value: type): 135 | """Global value.""" 136 | value = clips.values.clips_value(self._env, value=value) 137 | 138 | lib.DefglobalSetValue(self._ptr(), value) 139 | 140 | @property 141 | def name(self) -> str: 142 | """Global name.""" 143 | return self._name.decode() 144 | 145 | @property 146 | def module(self) -> Module: 147 | """The module in which the Global is defined. 148 | 149 | Equivalent to the CLIPS (defglobal-module) function. 150 | 151 | """ 152 | name = ffi.string(lib.DefglobalModule(self._ptr())).decode() 153 | 154 | return Module(self._env, name) 155 | 156 | @property 157 | def deletable(self) -> bool: 158 | """True if the Global can be deleted.""" 159 | return lib.DefglobalIsDeletable(self._ptr()) 160 | 161 | @property 162 | def watch(self) -> bool: 163 | """Whether or not the Global is being watched.""" 164 | return lib.DefglobalGetWatch(self._ptr()) 165 | 166 | @watch.setter 167 | def watch(self, flag: bool): 168 | """Whether or not the Global is being watched.""" 169 | lib.DefglobalSetWatch(self._ptr(), flag) 170 | 171 | def undefine(self): 172 | """Undefine the Global. 173 | 174 | Equivalent to the CLIPS (undefglobal) function. 175 | 176 | The object becomes unusable after this method has been called. 177 | 178 | """ 179 | if not lib.Undefglobal(self._ptr(), self._env): 180 | raise CLIPSError(self._env) 181 | 182 | 183 | class Modules: 184 | """Globals and Modules namespace class. 185 | 186 | .. note:: 187 | 188 | All the Modules methods are accessible through the Environment class. 189 | 190 | """ 191 | 192 | __slots__ = ['_env'] 193 | 194 | def __init__(self, env: ffi.CData): 195 | self._env = env 196 | 197 | @property 198 | def current_module(self) -> Module: 199 | """The current module. 200 | 201 | Equivalent to the CLIPS (get-current-module) function. 202 | 203 | """ 204 | module = lib.GetCurrentModule(self._env) 205 | name = ffi.string(lib.DefmoduleName(module)).decode() 206 | 207 | return Module(self._env, name) 208 | 209 | @current_module.setter 210 | def current_module(self, module: Module): 211 | """The current module. 212 | 213 | Equivalent to the CLIPS (get-current-module) function. 214 | 215 | """ 216 | lib.SetCurrentModule(self._env, module._ptr()) 217 | 218 | @property 219 | def reset_globals(self) -> bool: 220 | """True if Globals reset behaviour is enabled.""" 221 | return lib.GetResetGlobals(self._env) 222 | 223 | @reset_globals.setter 224 | def reset_globals(self, value: bool): 225 | """True if Globals reset behaviour is enabled.""" 226 | lib.SetResetGlobals(self._env, value) 227 | 228 | @property 229 | def globals_changed(self) -> bool: 230 | """True if any Global has changed since last check.""" 231 | value = lib.GetGlobalsChanged(self._env) 232 | lib.SetGlobalsChanged(self._env, False) 233 | 234 | return value 235 | 236 | def globals(self) -> iter: 237 | """Iterates over the defined Globals.""" 238 | defglobal = lib.GetNextDefglobal(self._env, ffi.NULL) 239 | 240 | while defglobal != ffi.NULL: 241 | name = ffi.string(lib.DefglobalName(defglobal)).decode() 242 | yield Global(self._env, name) 243 | 244 | defglobal = lib.GetNextDefglobal(self._env, defglobal) 245 | 246 | def find_global(self, name: str) -> Module: 247 | """Find the Global by its name.""" 248 | defglobal = lib.FindDefglobal(self._env, name.encode()) 249 | if defglobal == ffi.NULL: 250 | raise LookupError("Global '%s' not found" % name) 251 | 252 | return Global(self._env, name) 253 | 254 | def modules(self) -> iter: 255 | """Iterates over the defined Modules.""" 256 | defmodule = lib.GetNextDefmodule(self._env, ffi.NULL) 257 | 258 | while defmodule != ffi.NULL: 259 | name = ffi.string(lib.DefmoduleName(defmodule)).decode() 260 | yield Module(self._env, name) 261 | 262 | defmodule = lib.GetNextDefmodule(self._env, defmodule) 263 | 264 | def find_module(self, name: str) -> Module: 265 | """Find the Module by its name.""" 266 | defmodule = lib.FindDefmodule(self._env, name.encode()) 267 | if defmodule == ffi.NULL: 268 | raise LookupError("Module '%s' not found" % name) 269 | 270 | return Module(self._env, name) 271 | -------------------------------------------------------------------------------- /clips/routers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module contains the definition of: 31 | 32 | * Router class 33 | * LoggingRouter class 34 | * Routers namespace class 35 | 36 | """ 37 | 38 | import logging 39 | import traceback 40 | 41 | import clips 42 | from clips import common 43 | 44 | from clips._clips import lib, ffi 45 | 46 | 47 | class Router: 48 | 49 | __slots__ = '_env', '_name', '_userdata', '_priority' 50 | 51 | def __init__(self, name: str, priority: int): 52 | self._env = None 53 | self._name = name 54 | self._priority = priority 55 | self._userdata = ffi.new_handle(self) 56 | 57 | @property 58 | def name(self) -> str: 59 | """The Router name.""" 60 | return self._name 61 | 62 | @property 63 | def priority(self) -> int: 64 | """The Router priority.""" 65 | return self._priority 66 | 67 | def query(self, _name: str) -> bool: 68 | """This method should return True if the provided logical name 69 | is handled by the Router. 70 | 71 | """ 72 | return False 73 | 74 | def write(self, _name: str, _message: str): 75 | """If the query method returns True for the given logical name, 76 | this method will be called with the forwarded message. 77 | 78 | """ 79 | return None 80 | 81 | def read(self, _name: str) -> int: 82 | """Callback implementation for the `Environment.read_router` 83 | function. 84 | 85 | """ 86 | return 0 87 | 88 | def unread(self, _name: str, _char: int) -> int: 89 | """Callback implementation for the `Environment.unread_router` 90 | function. 91 | 92 | """ 93 | return 0 94 | 95 | def exit(self, _exitcode: int): 96 | return None 97 | 98 | def activate(self): 99 | """Activate the Router.""" 100 | if not lib.ActivateRouter(self._env, self._name.encode()): 101 | raise RuntimeError("Unable to activate router %s" % self._name) 102 | 103 | def deactivate(self): 104 | """Deactivate the Router.""" 105 | if not lib.DeactivateRouter(self._env, self._name.encode()): 106 | raise RuntimeError("Unable to deactivate router %s" % self._name) 107 | 108 | def delete(self): 109 | """Delete the Router.""" 110 | clips.common.environment_data(self._env, 'routers').pop(self.name, None) 111 | 112 | if not lib.DeleteRouter(self._env, self._name.encode()): 113 | raise RuntimeError("Unable to delete router %s" % self._name) 114 | 115 | def share_message(self, name: str, message: str): 116 | """Share the captured message with other Routers.""" 117 | self.deactivate() 118 | lib.WriteString(self._env, name.encode(), message.encode()) 119 | self.activate() 120 | 121 | 122 | class ErrorRouter(Router): 123 | """Router capturing error messages for CLIPSError exceptions.""" 124 | 125 | __slots__ = '_env', '_name', '_userdata', '_priority', '_last_message' 126 | 127 | def __init__(self): 128 | super().__init__('python-error-router', 40) 129 | self._last_message = '' 130 | 131 | @property 132 | def last_message(self) -> str: 133 | ret = self._last_message 134 | 135 | self._last_message = '' 136 | 137 | return ret 138 | 139 | def query(self, name: str): 140 | return True if name == 'stderr' else False 141 | 142 | def write(self, name: str, message: str): 143 | self._last_message += message 144 | self.share_message(name, message) 145 | 146 | 147 | class LoggingRouter(Router): 148 | """Python logging Router. 149 | 150 | A helper Router to get Python standard logging facilities 151 | integrated with CLIPS. 152 | 153 | It captures CLIPS output and re-directs it to Python logging library. 154 | 155 | """ 156 | 157 | __slots__ = '_env', '_name', '_userdata', '_priority', '_message' 158 | 159 | LOGGERS = {'stdout': logging.info, 160 | 'stderr': logging.error, 161 | 'stdwrn': logging.warning} 162 | 163 | def __init__(self): 164 | super().__init__('python-logging-router', 30) 165 | self._message = '' 166 | 167 | def query(self, name: str) -> bool: 168 | """Capture log from CLIPS output routers.""" 169 | return name in self.LOGGERS 170 | 171 | def write(self, name: str, message: str): 172 | """If the message is a new-line terminate sentence, 173 | log it at according to the mapped level. 174 | 175 | Otherwise, append it to the message string. 176 | 177 | """ 178 | if message == '\n': 179 | self.log_message(name) 180 | else: 181 | self._message += message 182 | if self._message.rstrip(' ').endswith('\n'): 183 | self.log_message(name) 184 | 185 | def log_message(self, name: str): 186 | if self._message: 187 | self.LOGGERS[name](self._message.lstrip('\n').rstrip('\n')) 188 | self._message = '' 189 | 190 | 191 | class Routers: 192 | """Routers namespace class. 193 | 194 | .. note:: 195 | 196 | All the Routers methods are accessible through the Environment class. 197 | 198 | """ 199 | 200 | __slots__ = ['_env'] 201 | 202 | def __init__(self, env): 203 | self._env = env 204 | 205 | def routers(self) -> iter: 206 | """The User defined routers installed within the Environment.""" 207 | return common.environment_data(self._env, 'routers').values() 208 | 209 | def read_router(self, router_name: str) -> int: 210 | """Query the Router by the given name calling its `read` callback.""" 211 | return lib.ReadRouter(self._env, router_name.encode()) 212 | 213 | def unread_router(self, router_name: str, characters: int) -> int: 214 | """Query the Router by the given name calling its `unread` callback.""" 215 | return lib.UnReadRouter(self._env, router_name.encode(), characters) 216 | 217 | def write_router(self, router_name: str, *args): 218 | """Send the given arguments to the given Router for writing.""" 219 | for arg in args: 220 | if type(arg) == str: 221 | lib.WriteString(self._env, router_name.encode(), arg.encode()) 222 | else: 223 | value = clips.values.clips_value(self._env, arg) 224 | lib.WriteCLIPSValue(self._env, router_name.encode(), value) 225 | 226 | def add_router(self, router: Router): 227 | """Add the given Router to the Environment.""" 228 | name = router.name 229 | router._env = self._env 230 | 231 | common.environment_data(self._env, 'routers')[name] = router 232 | 233 | lib.AddRouter(self._env, 234 | name.encode(), 235 | router.priority, 236 | lib.query_function, 237 | lib.write_function, 238 | lib.read_function, 239 | lib.unread_function, 240 | lib.exit_function, 241 | router._userdata) 242 | 243 | 244 | @ffi.def_extern() 245 | def query_function(env: ffi.CData, name: ffi.CData, context: ffi.CData): 246 | router = ffi.from_handle(context) 247 | 248 | return bool(router.query(ffi.string(name).decode())) 249 | 250 | 251 | @ffi.def_extern() 252 | def write_function(env: ffi.CData, name: ffi.CData, 253 | message: ffi.CData, context: ffi.CData): 254 | router = ffi.from_handle(context) 255 | 256 | try: 257 | router.write(ffi.string(name).decode(), ffi.string(message).decode()) 258 | except BaseException as error: 259 | message = "[ROUTER2] Router callback error: %r" % error 260 | string = "\n".join((message, traceback.format_exc())) 261 | 262 | lib.WriteString(env, 'stderr'.encode(), string.encode()) 263 | 264 | 265 | @ffi.def_extern() 266 | def read_function(env: ffi.CData, name: ffi.CData, context: ffi.CData): 267 | router = ffi.from_handle(context) 268 | 269 | try: 270 | return int(router.read(ffi.string(name).decode())) 271 | except BaseException as error: 272 | message = "[ROUTER2] Router callback error: %r" % error 273 | string = "\n".join((message, traceback.format_exc())) 274 | 275 | lib.WriteString(env, 'stderr'.encode(), string.encode()) 276 | 277 | return 0 278 | 279 | 280 | @ffi.def_extern() 281 | def unread_function(env: ffi.CData, char: ffi.CData, 282 | name: ffi.CData, context: ffi.CData): 283 | router = ffi.from_handle(context) 284 | 285 | try: 286 | return int(router.unread(ffi.string(name).decode(), char)) 287 | except BaseException as error: 288 | message = "[ROUTER2] Router callback error: %r" % error 289 | string = "\n".join((message, traceback.format_exc())) 290 | 291 | lib.WriteString(env, 'stderr'.encode(), string.encode()) 292 | 293 | return 0 294 | 295 | 296 | @ffi.def_extern() 297 | def exit_function(env: ffi.CData, exitcode: int, context: ffi.CData): 298 | router = ffi.from_handle(context) 299 | 300 | try: 301 | router.exit(exitcode) 302 | except BaseException as error: 303 | message = "[ROUTER2] Router callback error: %r" % error 304 | string = "\n".join((message, traceback.format_exc())) 305 | 306 | lib.WriteString(env, 'stderr'.encode(), string.encode()) 307 | -------------------------------------------------------------------------------- /clips/values.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import sys 31 | 32 | from clips import common 33 | from clips.classes import Instance 34 | from clips.facts import new_fact, ImpliedFact, TemplateFact 35 | 36 | from clips._clips import lib, ffi # pylint: disable=E0611 37 | 38 | 39 | class Symbol(str): 40 | """Python equivalent of a CLIPS SYMBOL.""" 41 | def __new__(cls, symbol): 42 | return str.__new__(cls, sys.intern(symbol)) 43 | 44 | 45 | class InstanceName(Symbol): 46 | """Python equivalent of a CLIPS INSTANCE_NAME.""" 47 | 48 | 49 | def python_value(env, value: ffi.CData) -> type: 50 | """Convert a CLIPSValue or UDFValue into Python.""" 51 | return PYTHON_VALUES[value.header.type](env, value) 52 | 53 | 54 | def clips_value(env: ffi.CData, value: type = ffi.NULL) -> ffi.CData: 55 | """Convert a Python value into CLIPS. 56 | 57 | If no value is provided, an empty value is returned. 58 | 59 | """ 60 | val = ffi.new("CLIPSValue *") 61 | 62 | if value is not ffi.NULL: 63 | constructor = CLIPS_VALUES.get(type(value), clips_external_address) 64 | val.value = constructor(env, value) 65 | 66 | return val 67 | 68 | 69 | def clips_udf_value(env: ffi.CData, value: type = ffi.NULL, 70 | udf_value: ffi.CData = ffi.NULL) -> ffi.CData: 71 | """Convert a Python value into a CLIPS UDFValue. 72 | 73 | If no value is provided, an empty value is returned. 74 | 75 | """ 76 | if udf_value is ffi.NULL: 77 | return ffi.new("UDFValue *") 78 | 79 | constructor = CLIPS_VALUES.get(type(value), clips_external_address) 80 | udf_value.value = constructor(env, value) 81 | 82 | return udf_value 83 | 84 | 85 | def multifield_value(env: ffi.CData, values: (list, tuple)) -> ffi.CData: 86 | """Convert a Python list or tuple into a CLIPS multifield.""" 87 | if not values: 88 | return lib.EmptyMultifield(env) 89 | 90 | builder = common.environment_builder(env, 'multifield') 91 | 92 | lib.MBReset(builder) 93 | for value in values: 94 | lib.MBAppend(builder, clips_value(env, value)) 95 | 96 | return lib.MBCreate(builder) 97 | 98 | 99 | def clips_external_address(env: ffi.CData, value: type) -> ffi.CData: 100 | """Convert a Python object into a CLIPSExternalAddress.""" 101 | handle = ffi.new_handle(value) 102 | 103 | # Hold reference to CData handle 104 | user_functions = common.environment_data(env, 'user_functions') 105 | user_functions.external_addresses[value] = handle 106 | 107 | return lib.CreateCExternalAddress(env, handle) 108 | 109 | 110 | def python_external_address(env: ffi.CData, value: ffi.CData) -> type: 111 | """Convert a CLIPSExternalAddress into a Python object.""" 112 | obj = ffi.from_handle(value.externalAddressValue.contents) 113 | 114 | # Remove reference to CData handle 115 | user_functions = common.environment_data(env, 'user_functions') 116 | del user_functions.external_addresses[obj] 117 | 118 | return obj 119 | 120 | 121 | PYTHON_VALUES = {common.CLIPSType.FLOAT: 122 | lambda e, v: float(v.floatValue.contents), 123 | common.CLIPSType.INTEGER: 124 | lambda e, v: int(v.integerValue.contents), 125 | common.CLIPSType.SYMBOL: 126 | lambda e, v: Symbol( 127 | ffi.string(v.lexemeValue.contents).decode()), 128 | common.CLIPSType.STRING: 129 | lambda e, v: ffi.string(v.lexemeValue.contents).decode(), 130 | common.CLIPSType.MULTIFIELD: 131 | lambda e, v: tuple( 132 | python_value(e, v.multifieldValue.contents + i) 133 | for i in range(v.multifieldValue.length)), 134 | common.CLIPSType.FACT_ADDRESS: 135 | lambda e, v: new_fact(e, v.factValue), 136 | common.CLIPSType.INSTANCE_ADDRESS: 137 | lambda e, v: Instance(e, v.instanceValue), 138 | common.CLIPSType.INSTANCE_NAME: 139 | lambda e, v: InstanceName( 140 | ffi.string(v.lexemeValue.contents).decode()), 141 | common.CLIPSType.EXTERNAL_ADDRESS: python_external_address, 142 | common.CLIPSType.VOID: lambda e, v: None} 143 | 144 | 145 | CLIPS_VALUES = {int: lib.CreateInteger, 146 | float: lib.CreateFloat, 147 | list: multifield_value, 148 | tuple: multifield_value, 149 | bool: lib.CreateBoolean, 150 | type(None): lambda e, v: lib.CreateSymbol(e, b'nil'), 151 | str: lambda e, v: lib.CreateString(e, v.encode()), 152 | Instance: lambda e, v: v._ist, 153 | ImpliedFact: lambda e, v: v._fact, 154 | TemplateFact: lambda e, v: v._fact, 155 | Symbol: lambda e, v: lib.CreateSymbol(e, v.encode()), 156 | InstanceName: 157 | lambda e, v: lib.CreateInstanceName(e, v.encode())} 158 | 159 | 160 | ANY_TYPE_BITS = (lib.FLOAT_BIT | lib.INTEGER_BIT | lib.SYMBOL_BIT | 161 | lib.STRING_BIT | lib.MULTIFIELD_BIT | 162 | lib.EXTERNAL_ADDRESS_BIT | lib.FACT_ADDRESS_BIT | 163 | lib.INSTANCE_ADDRESS_BIT | lib.INSTANCE_NAME_BIT 164 | | lib.VOID_BIT | lib.BOOLEAN_BIT) 165 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/clipspy.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/clipspy.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/clipspy" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/clipspy" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /doc/clips.rst: -------------------------------------------------------------------------------- 1 | CLIPS 2 | ===== 3 | 4 | .. only:: html 5 | 6 | :Release: |release| 7 | :Date: |today| 8 | 9 | Namespaces 10 | ---------- 11 | 12 | To keep the design simple and modular, the CLIPS functions are organised into namespaces. 13 | 14 | A namespace is a way to group a set of APIs which belong to the same domain into a category. 15 | 16 | The User shall not worry about the namespaces themselves as all the APIs are accessible through the Environment class. 17 | 18 | Submodules 19 | ---------- 20 | 21 | clips.environment module 22 | ------------------------ 23 | 24 | .. automodule:: clips.environment 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | clips.facts module 30 | ------------------ 31 | 32 | .. automodule:: clips.facts 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | :exclude-members: new_fact, slot_value, slot_values, fact_pp_string 37 | 38 | clips.agenda module 39 | ------------------- 40 | 41 | .. automodule:: clips.agenda 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | :exclude-members: fact_pp_string 46 | 47 | clips.classes module 48 | -------------------- 49 | 50 | .. automodule:: clips.classes 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | :exclude-members: instance_pp_string, slot_value 55 | 56 | clips.functions module 57 | ---------------------- 58 | 59 | .. automodule:: clips.functions 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | clips.modules module 65 | -------------------- 66 | 67 | .. automodule:: clips.modules 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | clips.routers module 73 | -------------------- 74 | 75 | .. automodule:: clips.routers 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | clips.common module 81 | ------------------- 82 | 83 | .. automodule:: clips.common 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | 88 | Module contents 89 | --------------- 90 | 91 | .. automodule:: clips 92 | :members: 93 | :undoc-members: 94 | :show-inheritance: 95 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # clipspy documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Oct 7 00:14:23 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | import fileinput 23 | from unittest.mock import MagicMock 24 | 25 | module_dir = os.path.abspath( 26 | os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) 27 | sys.path.insert(0, module_dir) 28 | sys.modules['clips._clips'] = MagicMock() 29 | 30 | # -- General configuration ------------------------------------------------ 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # 34 | # needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | # You can specify multiple suffix as a list of string: 48 | # 49 | # source_suffix = ['.rst', '.md'] 50 | source_suffix = '.rst' 51 | 52 | # The encoding of source files. 53 | # 54 | # source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'clipspy' 61 | copyright = '2016-2025, Matteo Cafasso' 62 | author = 'Matteo Cafasso' 63 | 64 | 65 | CWD = os.path.dirname(__file__) 66 | 67 | 68 | def package_version(): 69 | module_path = os.path.join(CWD, '..', 'clips', '__init__.py') 70 | for line in fileinput.input(module_path): 71 | if line.startswith('__version__'): 72 | return line.split('=')[-1].strip().replace('\'', '') 73 | 74 | 75 | # The version info for the project you're documenting, acts as replacement for 76 | # |version| and |release|, also used in various other places throughout the 77 | # built documents. 78 | # 79 | # The short X.Y version. 80 | version = package_version() 81 | # The full version, including alpha/beta/rc tags. 82 | release = version 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = None 90 | 91 | # There are two options for replacing |today|: either, you set today to some 92 | # non-false value, then it is used: 93 | # 94 | # today = '' 95 | # 96 | # Else, today_fmt is used as the format for a strftime call. 97 | # 98 | # today_fmt = '%B %d, %Y' 99 | 100 | # List of patterns, relative to source directory, that match files and 101 | # directories to ignore when looking for source files. 102 | # This patterns also effect to html_static_path and html_extra_path 103 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 104 | 105 | # The reST default role (used for this markup: `text`) to use for all 106 | # documents. 107 | # 108 | # default_role = None 109 | 110 | # If true, '()' will be appended to :func: etc. cross-reference text. 111 | # 112 | # add_function_parentheses = True 113 | 114 | # If true, the current module name will be prepended to all description 115 | # unit titles (such as .. function::). 116 | # 117 | # add_module_names = True 118 | 119 | # If true, sectionauthor and moduleauthor directives will be shown in the 120 | # output. They are ignored by default. 121 | # 122 | # show_authors = False 123 | 124 | # The name of the Pygments (syntax highlighting) style to use. 125 | pygments_style = 'sphinx' 126 | 127 | # A list of ignored prefixes for module index sorting. 128 | # modindex_common_prefix = [] 129 | 130 | # If true, keep warnings as "system message" paragraphs in the built documents. 131 | # keep_warnings = False 132 | 133 | # If true, `todo` and `todoList` produce output, else they produce nothing. 134 | todo_include_todos = False 135 | 136 | 137 | # -- Options for HTML output ---------------------------------------------- 138 | 139 | # The theme to use for HTML and HTML Help pages. See the documentation for 140 | # a list of builtin themes. 141 | # 142 | html_theme = 'alabaster' 143 | html_theme_options = { 144 | 'page_width': '80%', 145 | 'github_user': 'noxdafox', 146 | 'github_repo': 'clipspy', 147 | 'show_related': True 148 | } 149 | 150 | # Theme options are theme-specific and customize the look and feel of a theme 151 | # further. For a list of options available for each theme, see the 152 | # documentation. 153 | # 154 | # html_theme_options = {} 155 | 156 | # Add any paths that contain custom themes here, relative to this directory. 157 | # html_theme_path = [] 158 | 159 | # The name for this set of Sphinx documents. 160 | # " v documentation" by default. 161 | # 162 | # html_title = 'clipspy v0.0.8' 163 | 164 | # A shorter title for the navigation bar. Default is the same as html_title. 165 | # 166 | # html_short_title = None 167 | 168 | # The name of an image file (relative to this directory) to place at the top 169 | # of the sidebar. 170 | # 171 | # html_logo = None 172 | 173 | # The name of an image file (relative to this directory) to use as a favicon of 174 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 175 | # pixels large. 176 | # 177 | # html_favicon = None 178 | 179 | # Add any paths that contain custom static files (such as style sheets) here, 180 | # relative to this directory. They are copied after the builtin static files, 181 | # so a file named "default.css" will overwrite the builtin "default.css". 182 | html_static_path = ['_static'] 183 | 184 | # Add any extra paths that contain custom files (such as robots.txt or 185 | # .htaccess) here, relative to this directory. These files are copied 186 | # directly to the root of the documentation. 187 | # 188 | # html_extra_path = [] 189 | 190 | # If not None, a 'Last updated on:' timestamp is inserted at every page 191 | # bottom, using the given strftime format. 192 | # The empty string is equivalent to '%b %d, %Y'. 193 | # 194 | # html_last_updated_fmt = None 195 | 196 | # If true, SmartyPants will be used to convert quotes and dashes to 197 | # typographically correct entities. 198 | # 199 | # html_use_smartypants = True 200 | 201 | # Custom sidebar templates, maps document names to template names. 202 | # 203 | # html_sidebars = {} 204 | 205 | # Additional templates that should be rendered to pages, maps page names to 206 | # template names. 207 | # 208 | # html_additional_pages = {} 209 | 210 | # If false, no module index is generated. 211 | # 212 | # html_domain_indices = True 213 | 214 | # If false, no index is generated. 215 | # 216 | # html_use_index = True 217 | 218 | # If true, the index is split into individual pages for each letter. 219 | # 220 | # html_split_index = False 221 | 222 | # If true, links to the reST sources are added to the pages. 223 | # 224 | # html_show_sourcelink = True 225 | 226 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 227 | # 228 | # html_show_sphinx = True 229 | 230 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 231 | # 232 | # html_show_copyright = True 233 | 234 | # If true, an OpenSearch description file will be output, and all pages will 235 | # contain a tag referring to it. The value of this option must be the 236 | # base URL from which the finished HTML is served. 237 | # 238 | # html_use_opensearch = '' 239 | 240 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 241 | # html_file_suffix = None 242 | 243 | # Language to be used for generating the HTML full-text search index. 244 | # Sphinx supports the following languages: 245 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 246 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 247 | # 248 | # html_search_language = 'en' 249 | 250 | # A dictionary with options for the search language support, empty by default. 251 | # 'ja' uses this config value. 252 | # 'zh' user can custom change `jieba` dictionary path. 253 | # 254 | # html_search_options = {'type': 'default'} 255 | 256 | # The name of a javascript file (relative to the configuration directory) that 257 | # implements a search results scorer. If empty, the default will be used. 258 | # 259 | # html_search_scorer = 'scorer.js' 260 | 261 | # Output file base name for HTML help builder. 262 | htmlhelp_basename = 'clipspydoc' 263 | 264 | # -- Options for LaTeX output --------------------------------------------- 265 | 266 | latex_elements = { 267 | # The paper size ('letterpaper' or 'a4paper'). 268 | # 269 | # 'papersize': 'letterpaper', 270 | 271 | # The font size ('10pt', '11pt' or '12pt'). 272 | # 273 | # 'pointsize': '10pt', 274 | 275 | # Additional stuff for the LaTeX preamble. 276 | # 277 | # 'preamble': '', 278 | 279 | # Latex figure (float) alignment 280 | # 281 | # 'figure_align': 'htbp', 282 | } 283 | 284 | # Grouping the document tree into LaTeX files. List of tuples 285 | # (source start file, target name, title, 286 | # author, documentclass [howto, manual, or own class]). 287 | latex_documents = [ 288 | (master_doc, 'clipspy.tex', 'clipspy Documentation', 289 | 'Matteo Cafasso', 'manual'), 290 | ] 291 | 292 | # The name of an image file (relative to this directory) to place at the top of 293 | # the title page. 294 | # 295 | # latex_logo = None 296 | 297 | # For "manual" documents, if this is true, then toplevel headings are parts, 298 | # not chapters. 299 | # 300 | # latex_use_parts = False 301 | 302 | # If true, show page references after internal links. 303 | # 304 | # latex_show_pagerefs = False 305 | 306 | # If true, show URL addresses after external links. 307 | # 308 | # latex_show_urls = False 309 | 310 | # Documents to append as an appendix to all manuals. 311 | # 312 | # latex_appendices = [] 313 | 314 | # It false, will not define \strong, \code, itleref, \crossref ... but only 315 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 316 | # packages. 317 | # 318 | # latex_keep_old_macro_names = True 319 | 320 | # If false, no module index is generated. 321 | # 322 | # latex_domain_indices = True 323 | 324 | 325 | # -- Options for manual page output --------------------------------------- 326 | 327 | # One entry per manual page. List of tuples 328 | # (source start file, name, description, authors, manual section). 329 | man_pages = [ 330 | (master_doc, 'clipspy', 'clipspy Documentation', 331 | [author], 1) 332 | ] 333 | 334 | # If true, show URL addresses after external links. 335 | # 336 | # man_show_urls = False 337 | 338 | 339 | # -- Options for Texinfo output ------------------------------------------- 340 | 341 | # Grouping the document tree into Texinfo files. List of tuples 342 | # (source start file, target name, title, author, 343 | # dir menu entry, description, category) 344 | texinfo_documents = [ 345 | (master_doc, 'clipspy', 'clipspy Documentation', 346 | author, 'clipspy', 'One line description of project.', 347 | 'Miscellaneous'), 348 | ] 349 | 350 | # Documents to append as an appendix to all manuals. 351 | # 352 | # texinfo_appendices = [] 353 | 354 | # If false, no module index is generated. 355 | # 356 | # texinfo_domain_indices = True 357 | 358 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 359 | # 360 | # texinfo_show_urls = 'footnote' 361 | 362 | # If true, do not generate a @detailmenu in the "Top" node's menu. 363 | # 364 | # texinfo_no_detailmenu = False 365 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. clipspy documentation master file, created by 2 | sphinx-quickstart on Sat Oct 7 00:14:23 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | CLIPS Python bindings 7 | ===================== 8 | 9 | .. only:: html 10 | 11 | :Release: |release| 12 | :Date: |today| 13 | 14 | Python CFFI_ bindings for the 'C' Language Integrated Production System (CLIPS_) 6.41 15 | 16 | Design principles 17 | ----------------- 18 | 19 | The clipspy bindings aim to be a "pythonic" thin layer built on top of the CLIPS native C APIs. Most of the functions and the methods directly resolve to the CLIPS functions documented in the `Advanced Programming Guide`_. 20 | 21 | Python standard paradigms are preferred such as property getters and setters, generators and magic methods. 22 | 23 | Data types 24 | ---------- 25 | 26 | The mapping between CLIPS and Python types is as follows. 27 | 28 | +------------------+----------------+ 29 | | CLIPS | Python | 30 | +==================+================+ 31 | | INTEGER | int | 32 | +------------------+----------------+ 33 | | FLOAT | float | 34 | +------------------+----------------+ 35 | | STRING | str | 36 | +------------------+----------------+ 37 | | SYMBOL | Symbol * | 38 | +------------------+----------------+ 39 | | MULTIFIELD | list | 40 | +------------------+----------------+ 41 | | FACT_ADDRESS | Fact ** | 42 | +------------------+----------------+ 43 | | INSTANCE_NAME | InstanceName * | 44 | +------------------+----------------+ 45 | | INSTANCE_ADDRESS | Instance | 46 | +------------------+----------------+ 47 | | EXTERNAL_ADDRESS | ffi.CData | 48 | +------------------+----------------+ 49 | 50 | Python native types returned by functions defined within an Environment are mapped to the following CLIPS symbols. 51 | 52 | +------------------+------------+ 53 | | CLIPS | Python | 54 | +==================+============+ 55 | | nil | None | 56 | +------------------+------------+ 57 | | TRUE | True | 58 | +------------------+------------+ 59 | | FALSE | False | 60 | +------------------+------------+ 61 | 62 | \* The Python Symbol and InstanceName objects are `interned string`_. 63 | 64 | ** `ImpliedFact` and `TemplateFact` are `Fact` subclasses. 65 | 66 | Basic Data Abstractions 67 | ----------------------- 68 | 69 | Facts 70 | +++++ 71 | 72 | A `fact` is a list of atomic values that are either referenced positionally (ordered or implied facts) or by name (unordered or template facts). 73 | 74 | Ordered Facts 75 | ************* 76 | 77 | Ordered or implied facts represent information as a list of elements. As the order of the data is what matters, implied facts do not have explicit templates. Ordered facts are pretty limited in terms of supported features. 78 | 79 | .. code:: python 80 | 81 | import clips 82 | 83 | env = clips.Environment() 84 | 85 | # Ordered facts can only be asserted as strings 86 | fact = env.assert_string('(ordered-fact 1 2 3)') 87 | 88 | # Ordered facts data can be accessed as list elements 89 | assert fact[0] == 1 90 | assert list(fact) == [1, 2, 3] 91 | 92 | Template Facts 93 | ************** 94 | 95 | Template or unordered facts represent data similarly to Python dictionaries. Unordered facts require a template to be defined. Templates are formal descriptions of the data represented by the fact. 96 | 97 | Template facts are more flexible as they support features such as constraints for the data types, default values and more. Template facts can also be modified once asserted. 98 | 99 | .. code:: python 100 | 101 | import clips 102 | 103 | template_string = """ 104 | (deftemplate person 105 | (slot name (type STRING)) 106 | (slot surname (type STRING)) 107 | (slot birthdate (type SYMBOL))) 108 | """ 109 | 110 | env = clips.Environment() 111 | 112 | env.build(template_string) 113 | 114 | template = env.find_template('person') 115 | 116 | fact = template.assert_fact(name='John', 117 | surname='Doe', 118 | birthdate=clips.Symbol('01/01/1970')) 119 | 120 | assert dict(fact) == {'name': 'John', 121 | 'surname': 'Doe', 122 | 'birthdate': clips.Symbol('01/01/1970')} 123 | 124 | fact.modify_slots(name='Leeroy', 125 | surname='Jenkins', 126 | birthdate=clips.Symbol('11/05/2005')) 127 | 128 | for fact in env.facts(): 129 | print(fact) 130 | 131 | Instances 132 | +++++++++ 133 | 134 | Objects are instantiations of specific classes. They support more features such as class inheritance and message sending. 135 | 136 | .. code:: python 137 | 138 | import clips 139 | 140 | env = clips.Environment() 141 | 142 | class_string = """ 143 | (defclass MyClass (is-a USER) 144 | (slot One) 145 | (slot Two)) 146 | """ 147 | handler_string = """ 148 | (defmessage-handler MyClass handler () 149 | (+ ?self:One ?self:Two)) 150 | """ 151 | env.build(class_string) 152 | env.build(handler_string) 153 | 154 | defclass = env.find_class('MyClass') 155 | instance = defclass.make_instance('instance-name', One=1, Two=2) 156 | retval = instance.send('handler') 157 | 158 | assert retval == 3 159 | 160 | for instance in env.instances(): 161 | print(instance) 162 | 163 | Evaluating CLIPS code 164 | --------------------- 165 | 166 | It is possible to quickly evaluate CLIPS statements retrieving their results in Python. 167 | 168 | Create a `multifield` value. 169 | 170 | .. code:: python 171 | 172 | import clips 173 | 174 | env = clips.Environment() 175 | 176 | env.eval("(create$ hammer drill saw screw pliers wrench)") 177 | 178 | CLIPS functions can also be called directly without the need of building language specific strings. 179 | 180 | .. code:: python 181 | 182 | import clips 183 | 184 | env = clips.Environment() 185 | 186 | env.call('create$', clips.Symbol('hammer'), 'drill', 1, 2.0) 187 | 188 | .. note:: None of the above can be used to define CLIPS constructs. Use the `build` or `load` functions instead. 189 | 190 | Defining CLIPS constructs 191 | ------------------------- 192 | 193 | CLIPS constructs must be defined in CLIPS language. Use the `load` or the `build` functions to define the constructs within the engine. 194 | 195 | Rule definition example. 196 | 197 | .. code:: python 198 | 199 | import clips 200 | 201 | env = clips.Environment() 202 | 203 | rule = """ 204 | (defrule my-rule 205 | (my-fact first-slot) 206 | => 207 | (printout t "My Rule fired!" crlf)) 208 | """ 209 | env.build(rule) 210 | 211 | for rule in env.rules(): 212 | print(rule) 213 | 214 | Embedding Python 215 | ---------------- 216 | 217 | Through the `define_function` method, it is possible to embed Python code within the CLIPS environment. 218 | 219 | The Python function will be accessible within CLIPS via its name as if it was defined via the `deffunction` construct. 220 | 221 | In this example, Python regular expression support is added within the CLIPS engine. 222 | 223 | .. code:: python 224 | 225 | import re 226 | import clips 227 | 228 | def regex_match(pattern, string): 229 | """Match pattern against string returning a multifield 230 | with the first element containing the full match 231 | followed by all captured groups. 232 | 233 | """ 234 | match = re.match(pattern, string) 235 | if match is not None: 236 | return (match.group(),) + match.groups() 237 | else: 238 | return [] 239 | 240 | env = clips.Environment() 241 | env.define_function(regex_match) 242 | 243 | env.eval('(regex_match "(www.)(.*)(.com)" "www.example.com")') 244 | 245 | I/O Routers 246 | ----------- 247 | 248 | CLIPS provides a system to manage I/O via a Router interface documented in the Section 9 of the `Advanced Programming Guide`_. CLIPS routers mechanics are used, for example, to capture error messages and expose them through the `CLIPSError` exception. 249 | 250 | The following example shows how CLIPS routers can be used to integrate CLIPS output with Python logging facilities. 251 | 252 | .. code:: python 253 | 254 | import logging 255 | import clips 256 | 257 | log_format = '%(asctime)s - %(levelname)s - %(message)s' 258 | logging.basicConfig(level=logging.INFO, format=log_format) 259 | 260 | env = clips.Environment() 261 | 262 | router = clips.LoggingRouter() 263 | env.add_router(router) 264 | 265 | fact = env.assert_string('(foo bar baz)') 266 | multifield = env.call('create$', 1, 2.0, clips.Symbol('three'), 'four') 267 | 268 | env.write_router('stdout', 'New fact asserted: ', fact, '. ', 'A multifield: ', multifield, '\n') 269 | 270 | 271 | Example output. 272 | :: 273 | 274 | 2019-02-10 20:36:26,669 - INFO - New fact asserted: . A multifield: (1 2.0 three "four") 275 | 276 | Memory management and objects lifecycle 277 | --------------------------------------- 278 | 279 | All clipspy objects are wrappers of CLIPS data structures. 280 | 281 | Facts and Instances implement a simple reference counting mechanism. Therefore, they can be accessed even when retracted from the engine working memory. Nevertheless, retaining these objects beyond their normal lifecycle will add pressure to the engine internal memory. 282 | 283 | All other constructs become unusable when deleted or undefined. 284 | 285 | Example: 286 | 287 | .. code:: python 288 | 289 | template = env.find_template('some-fact') 290 | 291 | # remove the Template from the CLIPS Environment 292 | template.undefine() # from here on, the template object is unusable 293 | 294 | # this will cause an error 295 | print(template) 296 | 297 | If the previous example is pretty straightforward, there are more subtle scenarios. 298 | 299 | .. code:: python 300 | 301 | templates = tuple(env.templates()) 302 | 303 | # remove all CLIPS constructs from the environment 304 | env.clear() # from here on, all the previously created objects are unusable 305 | 306 | # this will cause an error 307 | for template in templates: 308 | print(template) 309 | 310 | Building from sources 311 | --------------------- 312 | 313 | The provided Makefile takes care of retrieving the CLIPS source code and compiling the Python bindings together with it. 314 | 315 | .. code:: bash 316 | 317 | $ make 318 | $ sudo make install 319 | 320 | The following tools are required to build the sources. 321 | 322 | - gcc 323 | - make 324 | - curl 325 | - unzip 326 | - python 327 | - python-cffi 328 | 329 | The following conditional variables are accepted by the Makefile. 330 | 331 | - PYTHON: Python interpreter to use, default `python` 332 | - CLIPS_SOURCE_URL: Location from where to retrieve CLIPS source code archive. 333 | - SHARED_LIBRARY_DIR: Path where to install CLIPS shared library, default `/usr/lib` 334 | 335 | Manylinux Wheels 336 | ++++++++++++++++ 337 | 338 | It is possible to build `x86_64` wheels for Linux based on PEP-599_ standards. Only requirement is Docker_. 339 | 340 | To build the container, issue the following command from the project root folder. 341 | 342 | .. code:: bash 343 | 344 | $ docker build -t clipspy-build-wheels:latest -f manylinux/Dockerfile . 345 | 346 | The wheels can then be built within the container placing the resulting packages in the `manylinux/wheelhouse` folder as follows. 347 | 348 | .. code:: bash 349 | 350 | $ docker run --rm -v `pwd`/manylinux/wheelhouse:/io/wheelhouse clipspy-build-wheels:latest 351 | 352 | The container takes care of building the wheel packages and running the tests. 353 | 354 | API documentation 355 | ----------------- 356 | 357 | .. toctree:: 358 | :maxdepth: 2 359 | 360 | clips 361 | 362 | Indices and tables 363 | ================== 364 | 365 | * :ref:`genindex` 366 | * :ref:`modindex` 367 | * :ref:`search` 368 | 369 | .. _CLIPS: http://www.clipsrules.net/ 370 | .. _CFFI: https://cffi.readthedocs.io/en/latest/index.html 371 | .. _`Advanced Programming Guide`: http://clipsrules.sourceforge.net/documentation/v640/apg.pdf 372 | .. _`interned string`: https://docs.python.org/3/library/sys.html?highlight=sys%20intern#sys.intern 373 | .. _PEP-599: https://www.python.org/dev/peps/pep-0599/ 374 | .. _Docker: https://www.docker.com 375 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\clipspy.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\clipspy.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.0.0 2 | -------------------------------------------------------------------------------- /lib/clips.cdef: -------------------------------------------------------------------------------- 1 | typedef enum { 2 | LE_NO_ERROR, 3 | LE_OPEN_FILE_ERROR, 4 | LE_PARSING_ERROR 5 | } LoadError; 6 | 7 | typedef enum { 8 | EE_NO_ERROR, 9 | EE_PARSING_ERROR, 10 | EE_PROCESSING_ERROR 11 | } EvalError; 12 | 13 | typedef enum { 14 | BE_NO_ERROR, 15 | BE_COULD_NOT_BUILD_ERROR, 16 | BE_CONSTRUCT_NOT_FOUND_ERROR, 17 | BE_PARSING_ERROR, 18 | } BuildError; 19 | 20 | typedef enum { 21 | ASE_NO_ERROR, 22 | ASE_NULL_POINTER_ERROR, 23 | ASE_PARSING_ERROR, 24 | ASE_COULD_NOT_ASSERT_ERROR, 25 | ASE_RULE_NETWORK_ERROR 26 | } AssertStringError; 27 | 28 | typedef enum { 29 | RE_NO_ERROR, 30 | RE_NULL_POINTER_ERROR, 31 | RE_COULD_NOT_RETRACT_ERROR, 32 | RE_RULE_NETWORK_ERROR 33 | } RetractError; 34 | 35 | typedef enum { 36 | MIE_NO_ERROR, 37 | MIE_NULL_POINTER_ERROR, 38 | MIE_PARSING_ERROR, 39 | MIE_COULD_NOT_CREATE_ERROR, 40 | MIE_RULE_NETWORK_ERROR 41 | } MakeInstanceError; 42 | 43 | typedef enum { 44 | UIE_NO_ERROR, 45 | UIE_NULL_POINTER_ERROR, 46 | UIE_COULD_NOT_DELETE_ERROR, 47 | UIE_DELETED_ERROR, 48 | UIE_RULE_NETWORK_ERROR 49 | } UnmakeInstanceError; 50 | 51 | typedef enum { 52 | FBE_NO_ERROR, 53 | FBE_NULL_POINTER_ERROR, 54 | FBE_DEFTEMPLATE_NOT_FOUND_ERROR, 55 | FBE_IMPLIED_DEFTEMPLATE_ERROR, 56 | FBE_COULD_NOT_ASSERT_ERROR, 57 | FBE_RULE_NETWORK_ERROR 58 | } FactBuilderError; 59 | 60 | typedef enum { 61 | FME_NO_ERROR, 62 | FME_NULL_POINTER_ERROR, 63 | FME_RETRACTED_ERROR, 64 | FME_IMPLIED_DEFTEMPLATE_ERROR, 65 | FME_COULD_NOT_MODIFY_ERROR, 66 | FME_RULE_NETWORK_ERROR 67 | } FactModifierError; 68 | 69 | typedef enum { 70 | PSE_NO_ERROR, 71 | PSE_NULL_POINTER_ERROR, 72 | PSE_INVALID_TARGET_ERROR, 73 | PSE_SLOT_NOT_FOUND_ERROR, 74 | PSE_TYPE_ERROR, 75 | PSE_RANGE_ERROR, 76 | PSE_ALLOWED_VALUES_ERROR, 77 | PSE_CARDINALITY_ERROR, 78 | PSE_ALLOWED_CLASSES_ERROR 79 | } PutSlotError; 80 | 81 | typedef enum { 82 | GSE_NO_ERROR, 83 | GSE_NULL_POINTER_ERROR, 84 | GSE_INVALID_TARGET_ERROR, 85 | GSE_SLOT_NOT_FOUND_ERROR 86 | } GetSlotError; 87 | 88 | typedef enum { 89 | LOCAL_SAVE, 90 | VISIBLE_SAVE 91 | } SaveScope; 92 | 93 | typedef enum { 94 | NO_DEFAULT, 95 | STATIC_DEFAULT, 96 | DYNAMIC_DEFAULT 97 | } DefaultType; 98 | 99 | typedef enum { 100 | IBE_NO_ERROR, 101 | IBE_NULL_POINTER_ERROR, 102 | IBE_DEFCLASS_NOT_FOUND_ERROR, 103 | IBE_COULD_NOT_CREATE_ERROR, 104 | IBE_RULE_NETWORK_ERROR 105 | } InstanceBuilderError; 106 | 107 | typedef enum { 108 | IME_NO_ERROR, 109 | IME_NULL_POINTER_ERROR, 110 | IME_DELETED_ERROR, 111 | IME_COULD_NOT_MODIFY_ERROR, 112 | IME_RULE_NETWORK_ERROR 113 | } InstanceModifierError; 114 | 115 | typedef enum { 116 | CONVENIENCE_MODE, 117 | CONSERVATION_MODE 118 | } ClassDefaultsMode; 119 | 120 | typedef enum { 121 | WHEN_DEFINED, 122 | WHEN_ACTIVATED, 123 | EVERY_CYCLE 124 | } SalienceEvaluationType; 125 | 126 | typedef enum { 127 | DEPTH_STRATEGY, 128 | BREADTH_STRATEGY, 129 | LEX_STRATEGY, 130 | MEA_STRATEGY, 131 | COMPLEXITY_STRATEGY, 132 | SIMPLICITY_STRATEGY, 133 | RANDOM_STRATEGY 134 | } StrategyType; 135 | 136 | typedef enum { 137 | VERBOSE, 138 | SUCCINCT, 139 | TERSE 140 | } Verbosity; 141 | 142 | typedef enum { 143 | FCBE_NO_ERROR, 144 | FCBE_NULL_POINTER_ERROR, 145 | FCBE_FUNCTION_NOT_FOUND_ERROR, 146 | FCBE_INVALID_FUNCTION_ERROR, 147 | FCBE_ARGUMENT_COUNT_ERROR, 148 | FCBE_ARGUMENT_TYPE_ERROR, 149 | FCBE_PROCESSING_ERROR 150 | } FunctionCallBuilderError; 151 | 152 | typedef enum { 153 | AUE_NO_ERROR, 154 | AUE_MIN_EXCEEDS_MAX_ERROR, 155 | AUE_FUNCTION_NAME_IN_USE_ERROR, 156 | AUE_INVALID_ARGUMENT_TYPE_ERROR, 157 | AUE_INVALID_RETURN_TYPE_ERROR 158 | } AddUDFError; 159 | 160 | typedef enum { 161 | FLOAT_BIT = 1, 162 | INTEGER_BIT = 2, 163 | SYMBOL_BIT = 4, 164 | STRING_BIT = 8, 165 | MULTIFIELD_BIT = 16, 166 | EXTERNAL_ADDRESS_BIT = 32, 167 | FACT_ADDRESS_BIT = 64, 168 | INSTANCE_ADDRESS_BIT = 128, 169 | INSTANCE_NAME_BIT = 256, 170 | VOID_BIT = 512, 171 | BOOLEAN_BIT = 1024 172 | } CLIPSType; 173 | 174 | typedef struct environment Environment; 175 | typedef struct defrule Defrule; 176 | typedef struct fact Fact; 177 | typedef struct deftemplate Deftemplate; 178 | typedef struct deffacts Deffacts; 179 | typedef struct instance Instance; 180 | typedef struct defclass Defclass; 181 | typedef struct definstances Definstances; 182 | typedef struct deffunction Deffunction; 183 | typedef struct defgeneric Defgeneric; 184 | typedef struct defmodule Defmodule; 185 | typedef struct defglobal Defglobal; 186 | typedef struct activation Activation; 187 | typedef struct clipsValue CLIPSValue; 188 | typedef struct factBuilder FactBuilder; 189 | typedef struct factModifier FactModifier; 190 | typedef struct instanceModifier InstanceModifier; 191 | typedef struct instanceBuilder InstanceBuilder; 192 | typedef struct multifieldBuilder MultifieldBuilder; 193 | typedef struct functionCallBuilder FunctionCallBuilder; 194 | 195 | typedef struct stringBuilder { 196 | char *contents; 197 | size_t length; 198 | ...; 199 | } StringBuilder; 200 | 201 | typedef struct typeHeader { 202 | unsigned short type; 203 | } TypeHeader; 204 | 205 | typedef struct clipsLexeme { 206 | TypeHeader header; 207 | const char *contents; 208 | ...; 209 | } CLIPSLexeme; 210 | 211 | typedef struct clipsInteger { 212 | TypeHeader header; 213 | long long contents; 214 | ...; 215 | } CLIPSInteger; 216 | 217 | typedef struct clipsFloat { 218 | TypeHeader header; 219 | double contents; 220 | ...; 221 | } CLIPSFloat; 222 | 223 | typedef struct multifield { 224 | TypeHeader header; 225 | size_t length; 226 | CLIPSValue contents[1]; 227 | ...; 228 | } Multifield; 229 | 230 | typedef struct clipsExternalAddress { 231 | TypeHeader header; 232 | void *contents; 233 | ...; 234 | } CLIPSExternalAddress; 235 | 236 | typedef struct clipsVoid { 237 | TypeHeader header; 238 | } CLIPSVoid; 239 | 240 | typedef struct clipsValue { 241 | union { 242 | void *value; 243 | TypeHeader *header; 244 | CLIPSLexeme *lexemeValue; 245 | CLIPSFloat *floatValue; 246 | CLIPSInteger *integerValue; 247 | CLIPSVoid *voidValue; 248 | Fact *factValue; 249 | Instance *instanceValue; 250 | Multifield *multifieldValue; 251 | CLIPSExternalAddress *externalAddressValue; 252 | }; 253 | } CLIPSValue; 254 | 255 | typedef struct udfContext { 256 | Environment *environment; 257 | void *context; 258 | ...; 259 | } UDFContext; 260 | 261 | typedef struct udfValue { 262 | union { 263 | void *value; 264 | TypeHeader *header; 265 | CLIPSLexeme *lexemeValue; 266 | CLIPSFloat *floatValue; 267 | CLIPSInteger *integerValue; 268 | CLIPSVoid *voidValue; 269 | Multifield *multifieldValue; 270 | Fact *factValue; 271 | Instance *instanceValue; 272 | CLIPSExternalAddress *externalAddressValue; 273 | }; 274 | size_t begin; 275 | size_t range; 276 | } UDFValue; 277 | 278 | /***************/ 279 | /* Environment */ 280 | /***************/ 281 | 282 | Environment *CreateEnvironment(); 283 | bool DestroyEnvironment(Environment *); 284 | BuildError Build(Environment *, const char *); 285 | EvalError Eval(Environment *, const char *, CLIPSValue *); 286 | bool Clear(Environment *); 287 | void Reset(Environment *); 288 | bool BatchStar(Environment *, const char *); 289 | bool Save(Environment *, const char *); 290 | bool Bsave(Environment *, const char *); 291 | LoadError Load(Environment *, const char *); 292 | bool Bload(Environment *, const char *); 293 | 294 | /****************************/ 295 | /* Primitive Types Creation */ 296 | /****************************/ 297 | 298 | CLIPSLexeme *CreateSymbol(Environment *, const char *); 299 | CLIPSLexeme *CreateString(Environment *, const char *); 300 | CLIPSLexeme *CreateInstanceName(Environment *, const char *); 301 | CLIPSLexeme *CreateBoolean(Environment *, bool); 302 | CLIPSLexeme *FalseSymbol(Environment *); 303 | CLIPSLexeme *TrueSymbol(Environment *); 304 | CLIPSInteger *CreateInteger(Environment *, long long); 305 | CLIPSFloat *CreateFloat(Environment *, double); 306 | Multifield *EmptyMultifield(Environment *); 307 | MultifieldBuilder *CreateMultifieldBuilder(Environment *, size_t); 308 | Multifield *MBCreate(MultifieldBuilder *); 309 | void MBReset(MultifieldBuilder *); 310 | void MBDispose(MultifieldBuilder *); 311 | void MBAppend(MultifieldBuilder *, CLIPSValue *); 312 | CLIPSVoid *VoidConstant(Environment *); 313 | CLIPSExternalAddress *CreateCExternalAddress(Environment *, void *); 314 | StringBuilder *CreateStringBuilder(Environment *, size_t); 315 | void SBReset(StringBuilder *); 316 | void SBDispose(StringBuilder *); 317 | 318 | /*********/ 319 | /* Facts */ 320 | /*********/ 321 | 322 | void RetainFact(Fact *); 323 | void ReleaseFact(Fact *); 324 | Fact *AssertString(Environment *, const char *); 325 | AssertStringError GetAssertStringError(Environment *); 326 | RetractError Retract(Fact *); 327 | FactBuilder *CreateFactBuilder(Environment *, const char *); 328 | Fact *FBAssert(FactBuilder *); 329 | void FBDispose(FactBuilder *); 330 | FactBuilderError FBSetDeftemplate(FactBuilder *, const char *); 331 | void FBAbort(FactBuilder *); 332 | FactBuilderError FBError(Environment *); 333 | FactModifier *CreateFactModifier(Environment *, Fact *); 334 | Fact *FMModify(FactModifier *); 335 | void FMDispose(FactModifier *); 336 | FactModifierError FMSetFact(FactModifier *, Fact *); 337 | void FMAbort(FactModifier *); 338 | FactModifierError FMError(Environment *); 339 | RetractError RetractAllFacts(Environment *); 340 | bool FactExistp(Fact *); 341 | Deftemplate *FactDeftemplate(Fact *); 342 | long long FactIndex(Fact *); 343 | const char *DeftemplateName(Deftemplate *); 344 | bool ImpliedDeftemplate(Deftemplate *); 345 | bool DeftemplateIsDeletable(Deftemplate *); 346 | bool Undeftemplate(Deftemplate *, Environment *); 347 | void FactSlotNames(Fact *, CLIPSValue *); 348 | void DeftemplateSlotNames(Deftemplate *, CLIPSValue *); 349 | Deftemplate *FindDeftemplate(Environment *, const char *); 350 | Fact *GetNextFact(Environment *, Fact *); 351 | Fact *GetNextFactInTemplate(Deftemplate *, Fact *); 352 | bool LoadFacts(Environment *, const char *); 353 | bool LoadFactsFromString(Environment *, const char *, size_t); 354 | bool SaveFacts(Environment *, const char *, SaveScope); 355 | const char *DeftemplatePPForm(Deftemplate *); 356 | bool DeftemplateSlotAllowedValues(Deftemplate *, const char *, CLIPSValue *); 357 | bool DeftemplateSlotCardinality(Deftemplate *, const char *, CLIPSValue *); 358 | bool DeftemplateSlotRange(Deftemplate *, const char *, CLIPSValue *); 359 | bool DeftemplateSlotDefaultValue(Deftemplate *, const char *, CLIPSValue *); 360 | bool DeftemplateSlotTypes(Deftemplate *, const char *, CLIPSValue *); 361 | bool DeftemplateSlotExistP(Deftemplate *, const char *); 362 | bool DeftemplateSlotMultiP(Deftemplate *, const char *); 363 | bool DeftemplateSlotSingleP(Deftemplate *, const char *); 364 | DefaultType DeftemplateSlotDefaultP(Deftemplate *, const char *); 365 | bool GetFactDuplication(Environment *); 366 | bool SetFactDuplication(Environment *, bool); 367 | const char *DeftemplateModule(Deftemplate *); 368 | Deftemplate *GetNextDeftemplate(Environment *, Deftemplate *); 369 | bool DeftemplateGetWatch(Deftemplate *); 370 | void DeftemplateSetWatch(Deftemplate *, bool); 371 | void FactPPForm(Fact *, StringBuilder *, bool); 372 | Deffacts *FindDeffacts(Environment *, const char *); 373 | Deffacts *GetNextDeffacts(Environment *, Deffacts *); 374 | const char *DeffactsModule(Deffacts *); 375 | const char *DeffactsName(Deffacts *); 376 | const char *DeffactsPPForm(Deffacts *); 377 | bool DeffactsIsDeletable(Deffacts *); 378 | bool Undeffacts(Deffacts *, Environment *); 379 | 380 | /*************/ 381 | /* Instances */ 382 | /*************/ 383 | 384 | void RetainInstance(Instance *); 385 | void ReleaseInstance(Instance *); 386 | Instance *MakeInstance(Environment *, const char *); 387 | MakeInstanceError GetMakeInstanceError(Environment *); 388 | UnmakeInstanceError UnmakeInstance(Instance *); 389 | UnmakeInstanceError DeleteInstance(Instance *); 390 | InstanceBuilder *CreateInstanceBuilder(Environment *, const char *); 391 | Instance *IBMake(InstanceBuilder *, const char *); 392 | void IBDispose(InstanceBuilder *); 393 | InstanceBuilderError IBSetDefclass(InstanceBuilder *, const char *); 394 | InstanceBuilderError IBError(Environment *); 395 | InstanceModifier *CreateInstanceModifier(Environment *, Instance *); 396 | Instance *IMModify(InstanceModifier *); 397 | void IMDispose(InstanceModifier *); 398 | InstanceModifierError IMSetInstance(InstanceModifier *, Instance *); 399 | InstanceModifierError IMError(Environment *); 400 | Defclass *FindDefclass(Environment *, const char *); 401 | Defclass *GetNextDefclass(Environment *, Defclass *); 402 | const char *DefclassModule(Defclass *); 403 | const char *DefclassName(Defclass *); 404 | const char *DefclassPPForm(Defclass *); 405 | void ClassSlots(Defclass *, CLIPSValue *, bool); 406 | void ClassSubclasses(Defclass *, CLIPSValue *, bool); 407 | void ClassSuperclasses(Defclass *, CLIPSValue *, bool); 408 | bool DefclassIsDeletable(Defclass *); 409 | bool Undefclass(Defclass *, Environment *); 410 | bool DefclassGetWatchInstances(Defclass *); 411 | bool DefclassGetWatchSlots(Defclass *); 412 | void DefclassSetWatchInstances(Defclass *, bool); 413 | void DefclassSetWatchSlots(Defclass *, bool); 414 | bool ClassAbstractP(Defclass *); 415 | bool ClassReactiveP(Defclass *); 416 | bool SubclassP(Defclass *, Defclass *); 417 | bool SuperclassP(Defclass *, Defclass *); 418 | bool SlotAllowedClasses(Defclass *, const char *, CLIPSValue *); 419 | bool SlotAllowedValues(Defclass *, const char *, CLIPSValue *); 420 | bool SlotCardinality(Defclass *, const char *, CLIPSValue *); 421 | bool SlotDefaultValue(Defclass *, const char *, CLIPSValue *); 422 | bool SlotFacets(Defclass *, const char *, CLIPSValue *); 423 | bool SlotRange(Defclass *, const char *, CLIPSValue *); 424 | bool SlotSources(Defclass *, const char *, CLIPSValue *); 425 | bool SlotTypes(Defclass *, const char *, CLIPSValue *); 426 | bool SlotDirectAccessP(Defclass *, const char *); 427 | bool SlotExistP(Defclass *, const char *, bool); 428 | bool SlotInitableP(Defclass *, const char *); 429 | bool SlotPublicP(Defclass *, const char *); 430 | bool SlotWritableP(Defclass *, const char *); 431 | ClassDefaultsMode GetClassDefaultsMode(Environment *); 432 | ClassDefaultsMode SetClassDefaultsMode(Environment *, ClassDefaultsMode); 433 | Instance *FindInstance(Environment *, Defmodule *, const char *, bool); 434 | Instance *GetNextInstance(Environment *, Instance *); 435 | Instance *GetNextInstanceInClass(Defclass *, Instance *); 436 | Defclass *InstanceClass(Instance *); 437 | const char *InstanceName(Instance *); 438 | void InstancePPForm(Instance *, StringBuilder *); 439 | void Send(Environment *, CLIPSValue *, const char *, 440 | const char *, CLIPSValue *); 441 | unsigned FindDefmessageHandler(Defclass *, const char *, const char *); 442 | unsigned GetNextDefmessageHandler(Defclass *, unsigned); 443 | const char *DefmessageHandlerName(Defclass *, unsigned); 444 | const char *DefmessageHandlerPPForm(Defclass *, unsigned); 445 | const char *DefmessageHandlerType(Defclass *, unsigned); 446 | bool DefmessageHandlerIsDeletable(Defclass *, unsigned); 447 | bool UndefmessageHandler(Defclass *, unsigned, Environment *); 448 | bool DefmessageHandlerGetWatch(Defclass *, unsigned); 449 | void DefmessageHandlerSetWatch(Defclass *, unsigned, bool); 450 | Definstances *FindDefinstances(Environment *, const char *); 451 | Definstances *GetNextDefinstances(Environment *, Definstances *); 452 | const char *DefinstancesModule(Definstances *); 453 | const char *DefinstancesName(Definstances *); 454 | const char *DefinstancesPPForm(Definstances *); 455 | bool DefinstancesIsDeletable(Definstances *); 456 | bool Undefinstances(Definstances *, Environment *); 457 | bool GetInstancesChanged(Environment *); 458 | void SetInstancesChanged(Environment *, bool); 459 | long BinaryLoadInstances(Environment *, const char *); 460 | long LoadInstances(Environment *, const char *); 461 | long LoadInstancesFromString(Environment *, const char *, size_t); 462 | long RestoreInstances(Environment *, const char *); 463 | long RestoreInstancesFromString(Environment *, const char *, size_t); 464 | long BinarySaveInstances(Environment *, const char *, SaveScope); 465 | long SaveInstances(Environment *, const char *, SaveScope); 466 | 467 | /*********/ 468 | /* Slots */ 469 | /*********/ 470 | 471 | PutSlotError IBPutSlot(InstanceBuilder *, const char *, CLIPSValue *); 472 | PutSlotError IMPutSlot(InstanceModifier *, const char *, CLIPSValue *); 473 | PutSlotError FBPutSlot(FactBuilder *, const char *, CLIPSValue *); 474 | PutSlotError FMPutSlot(FactModifier *, const char *, CLIPSValue *); 475 | GetSlotError GetFactSlot(Fact *, const char *, CLIPSValue *); 476 | GetSlotError DirectGetSlot(Instance *, const char *, CLIPSValue *); 477 | 478 | /**********/ 479 | /* Agenda */ 480 | /**********/ 481 | 482 | Activation *GetNextActivation(Environment *, Activation *); 483 | const char *ActivationRuleName(Activation *); 484 | void ActivationPPForm(Activation *, StringBuilder *); 485 | int ActivationGetSalience(Activation *); 486 | int ActivationSetSalience(Activation *, int); 487 | void RefreshAgenda(Defmodule *); 488 | void RefreshAllAgendas(Environment *); 489 | void ReorderAgenda(Defmodule *); 490 | void ReorderAllAgendas(Environment *); 491 | void DeleteActivation(Activation *); 492 | bool GetAgendaChanged(Environment *); 493 | void SetAgendaChanged(Environment *, bool); 494 | SalienceEvaluationType GetSalienceEvaluation(Environment *); 495 | SalienceEvaluationType SetSalienceEvaluation(Environment *, 496 | SalienceEvaluationType); 497 | StrategyType GetStrategy(Environment *); 498 | StrategyType SetStrategy(Environment *, StrategyType); 499 | Defrule *FindDefrule(Environment *, const char *); 500 | Defrule *GetNextDefrule(Environment *, Defrule *); 501 | const char *DefruleModule(Defrule *); 502 | const char *DefruleName(Defrule *); 503 | const char *DefrulePPForm(Defrule *); 504 | bool DefruleIsDeletable(Defrule *); 505 | bool Undefrule(Defrule *, Environment *); 506 | bool DefruleGetWatchActivations(Defrule *); 507 | bool DefruleGetWatchFirings(Defrule *); 508 | void DefruleSetWatchActivations(Defrule *, bool); 509 | void DefruleSetWatchFirings(Defrule *, bool); 510 | bool DefruleHasBreakpoint(Defrule *); 511 | bool RemoveBreak(Defrule *); 512 | void SetBreak(Defrule *); 513 | void Matches(Defrule *, Verbosity, CLIPSValue *); 514 | void Refresh(Defrule *); 515 | void ClearFocusStack(Environment *); 516 | void Focus(Defmodule *); 517 | Defmodule *PopFocus(Environment *); 518 | Defmodule *GetFocus(Environment *); 519 | void DeleteAllActivations(Defmodule *); 520 | long long Run(Environment *, long long); 521 | 522 | /*********************/ 523 | /* Modules & Globals */ 524 | /*********************/ 525 | 526 | Defmodule *FindDefmodule(Environment *, const char *); 527 | Defmodule *GetNextDefmodule(Environment *, Defmodule *); 528 | const char *DefmoduleName(Defmodule *); 529 | const char *DefmodulePPForm(Defmodule *); 530 | Defmodule *GetCurrentModule(Environment *); 531 | Defmodule *SetCurrentModule(Environment *, Defmodule *); 532 | Defglobal *FindDefglobal(Environment *, const char *); 533 | Defglobal *GetNextDefglobal(Environment *, Defglobal *); 534 | const char *DefglobalModule(Defglobal *); 535 | const char *DefglobalName(Defglobal *); 536 | const char *DefglobalPPForm(Defglobal *); 537 | void DefglobalValueForm(Defglobal *, StringBuilder *); 538 | void DefglobalGetValue(Defglobal *, CLIPSValue *); 539 | void DefglobalSetValue(Defglobal *, CLIPSValue *); 540 | bool DefglobalIsDeletable(Defglobal *); 541 | bool Undefglobal(Defglobal *, Environment *); 542 | bool DefglobalGetWatch(Defglobal *); 543 | void DefglobalSetWatch(Defglobal *, bool); 544 | bool GetGlobalsChanged(Environment *); 545 | void SetGlobalsChanged(Environment *, bool); 546 | bool GetResetGlobals(Environment *); 547 | bool SetResetGlobals(Environment *, bool); 548 | 549 | /*********************************/ 550 | /* Functions, Generics & Methods */ 551 | /*********************************/ 552 | 553 | Deffunction *FindDeffunction(Environment *, const char *); 554 | Deffunction *GetNextDeffunction(Environment *, Deffunction *); 555 | const char *DeffunctionModule(Deffunction *); 556 | const char *DeffunctionName(Deffunction *); 557 | const char *DeffunctionPPForm(Deffunction *); 558 | bool DeffunctionIsDeletable(Deffunction *); 559 | bool Undeffunction(Deffunction *, Environment *); 560 | bool DeffunctionGetWatch(Deffunction *); 561 | void DeffunctionSetWatch(Deffunction *, bool); 562 | Defgeneric *FindDefgeneric(Environment *, const char *); 563 | Defgeneric *GetNextDefgeneric(Environment *, Defgeneric *); 564 | const char *DefgenericModule(Defgeneric *); 565 | const char *DefgenericName(Defgeneric *); 566 | const char *DefgenericPPForm(Defgeneric *); 567 | bool DefgenericIsDeletable(Defgeneric *); 568 | bool Undefgeneric(Defgeneric *, Environment *); 569 | bool DefgenericGetWatch(Defgeneric *); 570 | void DefgenericSetWatch(Defgeneric *, bool); 571 | unsigned GetNextDefmethod(Defgeneric *, unsigned); 572 | void DefmethodDescription(Defgeneric *, unsigned, StringBuilder *); 573 | const char *DefmethodPPForm(Defgeneric *, unsigned); 574 | void GetMethodRestrictions(Defgeneric *, unsigned, CLIPSValue *); 575 | bool DefmethodIsDeletable(Defgeneric *, unsigned); 576 | bool Undefmethod(Defgeneric *, unsigned, Environment *); 577 | bool DefmethodGetWatch(Defgeneric *, unsigned); 578 | void DefmethodSetWatch(Defgeneric *, unsigned, bool); 579 | FunctionCallBuilder *CreateFunctionCallBuilder(Environment *, size_t); 580 | FunctionCallBuilderError FCBCall(FunctionCallBuilder *, const char *, 581 | CLIPSValue *); 582 | void FCBReset(FunctionCallBuilder *); 583 | void FCBDispose(FunctionCallBuilder *); 584 | void FCBAppend(FunctionCallBuilder *, CLIPSValue *); 585 | 586 | /***********/ 587 | /* Routers */ 588 | /***********/ 589 | 590 | typedef bool RouterQueryFunction(Environment *, const char *, void *); 591 | typedef void RouterWriteFunction(Environment *, const char *, const char *, 592 | void *); 593 | typedef int RouterReadFunction(Environment *, const char *, void *); 594 | typedef int RouterUnreadFunction(Environment *, const char *, int, void *); 595 | typedef void RouterExitFunction(Environment *, int, void *); 596 | extern "Python" bool query_function(Environment *, const char *, void *); 597 | extern "Python" void write_function(Environment *, const char *, const char *, 598 | void *); 599 | extern "Python" int read_function(Environment *, const char *, void *); 600 | extern "Python" int unread_function(Environment *, const char *, int, void *); 601 | extern "Python" void exit_function(Environment *, int, void *); 602 | bool AddRouter(Environment *, const char *, int, RouterQueryFunction *, 603 | RouterWriteFunction *, RouterReadFunction *, 604 | RouterUnreadFunction *, RouterExitFunction *, void *); 605 | bool DeleteRouter(Environment *, const char *); 606 | void WriteString(Environment *, const char *, const char *); 607 | void WriteCLIPSValue(Environment *, const char *, CLIPSValue *); 608 | int ReadRouter(Environment *, const char *); 609 | int UnreadRouter(Environment *, const char *, int); 610 | void ExitRouter(Environment *, int); 611 | bool ActivateRouter(Environment *, const char *); 612 | bool DeactivateRouter(Environment *, const char *); 613 | 614 | /**************************/ 615 | /* User Defined Functions */ 616 | /**************************/ 617 | 618 | typedef void UserDefinedFunction(Environment *, UDFContext *, UDFValue *); 619 | AddUDFError AddUDF(Environment *, const char *, const char *, 620 | unsigned short, unsigned short, const char *, 621 | UserDefinedFunction *, const char *, void *); 622 | unsigned UDFArgumentCount(UDFContext *); 623 | bool UDFFirstArgument(UDFContext *, unsigned, UDFValue *); 624 | bool UDFNextArgument(UDFContext *, unsigned, UDFValue *); 625 | bool UDFNthArgument(UDFContext *, unsigned, unsigned, UDFValue *); 626 | bool UDFHasNextArgument(UDFContext *); 627 | void UDFThrowError(UDFContext *); 628 | void SetErrorValue(Environment *, TypeHeader *); 629 | void GetErrorFunction(Environment *, UDFContext *, UDFValue *); 630 | void ClearErrorValue(Environment *); 631 | int DefinePythonFunction(Environment *); 632 | extern "Python" static void python_function(Environment *, UDFContext *, 633 | UDFValue *); 634 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_ext] 2 | include_dirs=clips_source 3 | library_dirs=clips_source 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2025, Matteo Cafasso 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 20 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 21 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 22 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import os 31 | import fileinput 32 | from setuptools import find_packages, setup 33 | 34 | 35 | CWD = os.path.dirname(__file__) 36 | 37 | 38 | def package_version(): 39 | module_path = os.path.join(CWD, 'clips', '__init__.py') 40 | for line in fileinput.input(module_path): 41 | if line.startswith('__version__'): 42 | return line.split('=')[-1].strip().replace('\'', '') 43 | 44 | 45 | setup( 46 | name="clipspy", 47 | version=package_version(), 48 | author="Matteo Cafasso", 49 | author_email="noxdafox@gmail.com", 50 | description=("CLIPS Python bindings"), 51 | license="BSD", 52 | long_description=open(os.path.join(CWD, 'README.rst')).read(), 53 | packages=find_packages(), 54 | ext_package="clips", 55 | setup_requires=["cffi>=1.0.0"], 56 | install_requires=["cffi>=1.0.0"], 57 | cffi_modules=["clips/clips_build.py:ffibuilder"], 58 | include_dirs=["/usr/include/clips", "/usr/local/include/clips"], 59 | data_files=[('lib', ['lib/clips.cdef'])], 60 | keywords="clips python cffi expert-system", 61 | url="https://github.com/noxdafox/clipspy", 62 | classifiers=[ 63 | "Programming Language :: Python :: 3", 64 | "Programming Language :: Python :: Implementation :: PyPy", 65 | "Development Status :: 5 - Production/Stable", 66 | "Intended Audience :: Developers", 67 | "License :: OSI Approved :: BSD License" 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /test/agenda_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from clips import Environment, CLIPSError, Strategy, SalienceEvaluation 4 | 5 | 6 | DEFTEMPLATE = """(deftemplate template-fact 7 | (slot template-slot)) 8 | """ 9 | 10 | DEFRULE = """(defrule MAIN::rule-name 11 | (declare (salience 10)) 12 | (implied-fact implied-value) 13 | => 14 | (assert (rule-fired))) 15 | """ 16 | 17 | DEFTEMPLATERULE = """(defrule MAIN::rule-name 18 | (implied-fact implied-value) 19 | (template-fact (template-slot template-value)) 20 | => 21 | (assert (rule-fired))) 22 | """ 23 | 24 | DEFOTHERRULE = """(defrule MAIN::other-rule-name 25 | (declare (salience 20)) 26 | (implied-fact implied-value) 27 | => 28 | (assert (rule-fired))) 29 | """ 30 | 31 | 32 | class TestAgenda(unittest.TestCase): 33 | def setUp(self): 34 | self.env = Environment() 35 | self.env.build(DEFTEMPLATE) 36 | self.env.build(DEFRULE) 37 | 38 | def test_agenda_strategy(self): 39 | """Agenda strategy getting/setting.""" 40 | for strategy in Strategy: 41 | self.env.strategy = strategy 42 | self.assertEqual(self.env.strategy, strategy) 43 | 44 | def test_agenda_salience_evaluation(self): 45 | """Agenda salience_evaluation getting/setting.""" 46 | for salience_evaluation in SalienceEvaluation: 47 | self.env.salience_evaluation = salience_evaluation 48 | self.assertEqual( 49 | self.env.salience_evaluation, salience_evaluation) 50 | 51 | def test_agenda_activation(self): 52 | """Agenda activation test.""" 53 | self.env.assert_string('(implied-fact implied-value)') 54 | 55 | self.assertTrue(self.env.agenda_changed) 56 | 57 | activation = tuple(self.env.activations())[0] 58 | 59 | self.assertEqual(activation.name, 'rule-name') 60 | self.assertEqual(activation.salience, 10) 61 | self.assertEqual(str(activation), '10 rule-name: f-1') 62 | self.assertEqual(repr(activation), 'Activation: 10 rule-name: f-1') 63 | 64 | activation.delete() 65 | 66 | self.assertFalse(activation in self.env.activations()) 67 | with self.assertRaises(CLIPSError): 68 | activation.salience = 10 69 | 70 | def test_agenda_run(self): 71 | """Agenda rules are fired on run.""" 72 | self.env.assert_string('(implied-fact implied-value)') 73 | 74 | self.assertEqual(self.env.focus, self.env.current_module) 75 | self.env.run() 76 | self.assertEqual(self.env.focus, None) 77 | 78 | fact_names = (f.template.name for f in self.env.facts()) 79 | self.assertTrue('rule-fired' in fact_names) 80 | 81 | def test_agenda_activation_order(self): 82 | """Agenda activations order change if salience or strategy change.""" 83 | self.env.build(DEFOTHERRULE) 84 | self.env.assert_string('(implied-fact implied-value)') 85 | 86 | self.assertTrue(self.env.agenda_changed) 87 | 88 | activations = tuple(self.env.activations()) 89 | 90 | self.assertEqual(tuple(a.name for a in activations), 91 | (u'other-rule-name', u'rule-name')) 92 | 93 | activations[1].salience = 30 94 | 95 | self.assertEqual(activations[1].salience, 30) 96 | 97 | self.assertFalse(self.env.agenda_changed) 98 | 99 | self.env.reorder() 100 | 101 | self.assertTrue(self.env.agenda_changed) 102 | 103 | activations = tuple(self.env.activations()) 104 | 105 | self.assertEqual(tuple(a.name for a in activations), 106 | (u'rule-name', u'other-rule-name')) 107 | 108 | self.env.refresh() 109 | 110 | self.assertTrue(self.env.agenda_changed) 111 | 112 | self.env.clear() 113 | 114 | activations = tuple(self.env.activations()) 115 | 116 | self.assertEqual(len(activations), 0) 117 | 118 | 119 | class TestRules(unittest.TestCase): 120 | def setUp(self): 121 | self.env = Environment() 122 | self.env.build(DEFTEMPLATE) 123 | self.env.build(DEFTEMPLATERULE) 124 | 125 | def test_rule_build(self): 126 | """Simple Rule build.""" 127 | rule = self.env.find_rule('rule-name') 128 | 129 | self.assertTrue(rule in self.env.rules()) 130 | self.assertEqual(rule.module.name, 'MAIN') 131 | self.assertTrue(rule.deletable) 132 | self.assertEqual(str(rule), ' '.join(DEFTEMPLATERULE.split())) 133 | self.assertEqual(repr(rule), 134 | "Rule: %s" % ' '.join(DEFTEMPLATERULE.split())) 135 | self.assertFalse(rule.watch_firings) 136 | rule.watch_firings = True 137 | self.assertTrue(rule.watch_firings) 138 | self.assertFalse(rule.watch_activations) 139 | rule.watch_activations = True 140 | self.assertTrue(rule.watch_activations) 141 | 142 | rule.undefine() 143 | 144 | with self.assertRaises(LookupError): 145 | self.env.find_rule('rule-name') 146 | with self.assertRaises(CLIPSError): 147 | print(rule) 148 | 149 | def test_rule_matches(self): 150 | """Partial rule matches.""" 151 | rule = self.env.find_rule('rule-name') 152 | self.env.assert_string('(implied-fact implied-value)') 153 | 154 | self.assertEqual(rule.matches(), (1, 0, 0)) 155 | 156 | rule.undefine() 157 | 158 | def test_rule_activation(self): 159 | """Rule activation.""" 160 | rule = self.env.find_rule('rule-name') 161 | self.env.assert_string('(implied-fact implied-value)') 162 | self.env.assert_string( 163 | '(template-fact (template-slot template-value))') 164 | 165 | self.assertEqual(rule.matches(), (2, 1, 1)) 166 | self.env.run() 167 | rule.refresh() 168 | 169 | fact_names = (f.template.name for f in self.env.facts()) 170 | self.assertTrue('rule-fired' in fact_names) 171 | -------------------------------------------------------------------------------- /test/classes_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from tempfile import mkstemp 4 | 5 | from clips import Environment, Symbol, InstanceName 6 | from clips import CLIPSError, ClassDefaultMode, LoggingRouter 7 | 8 | 9 | DEFCLASSES = [ 10 | """ 11 | (defclass AbstractClass (is-a USER) 12 | (role abstract)) 13 | """, 14 | """(defclass InheritClass (is-a AbstractClass))""", 15 | """ 16 | (defclass ConcreteClass (is-a USER) 17 | (slot Slot (type SYMBOL) (allowed-values value another-value))) 18 | """, 19 | """ 20 | (defclass MessageHandlerClass (is-a USER) 21 | (slot One) 22 | (slot Two)) 23 | """, 24 | """ 25 | (defmessage-handler MessageHandlerClass test-handler () 26 | (+ ?self:One ?self:Two)) 27 | """ 28 | ] 29 | DEFINSTANCES = """(definstances MAIN::defined-instances 30 | (c1 of ConcreteClass (Slot a-slot))) 31 | """ 32 | 33 | 34 | class TempFile: 35 | """Cross-platform temporary file.""" 36 | name = None 37 | 38 | def __enter__(self): 39 | fobj, self.name = mkstemp() 40 | os.close(fobj) 41 | 42 | return self 43 | 44 | def __exit__(self, *_): 45 | os.remove(self.name) 46 | 47 | 48 | class TestClasses(unittest.TestCase): 49 | def setUp(self): 50 | self.env = Environment() 51 | self.env.add_router(LoggingRouter()) 52 | 53 | for defclass in DEFCLASSES: 54 | self.env.build(defclass) 55 | 56 | def test_classes(self): 57 | """Classes wrapper test.""" 58 | self.assertEqual( 59 | self.env.default_mode, ClassDefaultMode.CONVENIENCE_MODE) 60 | self.env.default_mode = ClassDefaultMode.CONSERVATION_MODE 61 | self.assertEqual( 62 | self.env.default_mode, ClassDefaultMode.CONSERVATION_MODE) 63 | 64 | defclass = self.env.find_class('USER') 65 | self.assertTrue(defclass in self.env.classes()) 66 | 67 | with self.assertRaises(LookupError): 68 | self.env.find_class('NonExisting') 69 | 70 | defclass = self.env.find_class('ConcreteClass') 71 | 72 | defclass.make_instance('some-instance') 73 | defclass.make_instance('test-instance') 74 | 75 | instance = self.env.find_instance('test-instance') 76 | self.assertTrue(instance in self.env.instances()) 77 | 78 | with self.assertRaises(LookupError): 79 | self.env.find_instance('non-existing-instance') 80 | 81 | self.assertTrue(self.env.instances_changed) 82 | self.assertFalse(self.env.instances_changed) 83 | 84 | with TempFile() as tmp: 85 | saved = self.env.save_instances(tmp.name) 86 | self.env.reset() 87 | loaded = self.env.load_instances(tmp.name) 88 | self.assertEqual(saved, loaded) 89 | 90 | with TempFile() as tmp: 91 | saved = self.env.save_instances(tmp.name) 92 | self.env.reset() 93 | loaded = self.env.restore_instances(tmp.name) 94 | self.assertEqual(saved, loaded) 95 | 96 | with TempFile() as tmp: 97 | saved = self.env.save_instances(tmp.name, binary=True) 98 | self.env.reset() 99 | loaded = self.env.load_instances(tmp.name) 100 | self.assertEqual(saved, loaded) 101 | 102 | def test_abstract_class(self): 103 | """Abstract class test.""" 104 | superclass = self.env.find_class('USER') 105 | subclass = self.env.find_class('InheritClass') 106 | defclass = self.env.find_class('AbstractClass') 107 | 108 | self.assertTrue(defclass.abstract) 109 | self.assertFalse(defclass.reactive) 110 | self.assertEqual(defclass.name, 'AbstractClass') 111 | self.assertEqual(defclass.module.name, 'MAIN') 112 | self.assertTrue(defclass.deletable) 113 | self.assertTrue(defclass.subclass(superclass)) 114 | self.assertTrue(defclass.superclass(subclass)) 115 | self.assertEqual(tuple(defclass.subclasses()), (subclass, )) 116 | self.assertEqual(tuple(defclass.superclasses()), (superclass, )) 117 | 118 | with self.assertRaises(CLIPSError): 119 | defclass.make_instance('foobar') 120 | 121 | defclass.undefine() 122 | 123 | def test_concrete_class(self): 124 | """Concrete class test.""" 125 | defclass = self.env.find_class('ConcreteClass') 126 | 127 | self.assertFalse(defclass.abstract) 128 | self.assertTrue(defclass.reactive) 129 | self.assertEqual(defclass.name, 'ConcreteClass') 130 | self.assertEqual(defclass.module.name, 'MAIN') 131 | self.assertTrue(defclass.deletable) 132 | 133 | self.assertFalse(defclass.watch_instances) 134 | defclass.watch_instances = True 135 | self.assertTrue(defclass.watch_instances) 136 | 137 | self.assertFalse(defclass.watch_slots) 138 | defclass.watch_slots = True 139 | self.assertTrue(defclass.watch_slots) 140 | 141 | defclass.undefine() 142 | 143 | def test_slot(self): 144 | """Slot test.""" 145 | defclass = self.env.find_class('ConcreteClass') 146 | 147 | slot = tuple(defclass.slots())[0] 148 | 149 | self.assertFalse(slot.public) 150 | self.assertTrue(slot.writable) 151 | self.assertTrue(slot.accessible) 152 | self.assertTrue(slot.initializable) 153 | self.assertEqual(slot.name, 'Slot') 154 | self.assertEqual(slot.types, ('SYMBOL', )) 155 | self.assertEqual(slot.sources, (defclass.name, )) 156 | self.assertEqual(slot.range, Symbol('FALSE')) 157 | self.assertEqual(slot.facets, ('SGL', 'STC', 'INH', 'RW', 'LCL', 'RCT', 158 | 'EXC', 'PRV', 'RW', 'put-Slot')) 159 | self.assertEqual(slot.cardinality, ()) 160 | self.assertEqual(slot.default_value, Symbol('value')) 161 | self.assertEqual(slot.allowed_values, ('value', 'another-value')) 162 | self.assertEqual(tuple(slot.allowed_classes()), ()) 163 | 164 | def test_make_instance(self): 165 | """Instance test.""" 166 | defclass = self.env.find_class('ConcreteClass') 167 | 168 | instance_name = self.env.eval( 169 | '(make-instance test-name-instance of ConcreteClass)') 170 | self.assertEqual(instance_name, 'test-name-instance') 171 | self.assertTrue(isinstance(instance_name, InstanceName)) 172 | 173 | instance = defclass.make_instance() 174 | self.assertEqual(instance.name, 'gen1') 175 | 176 | instance = defclass.make_instance('test-instance', Slot=Symbol('value')) 177 | self.assertTrue(instance in defclass.instances()) 178 | self.assertEqual(instance.name, 'test-instance') 179 | self.assertEqual(instance.instance_class, defclass) 180 | self.assertEqual(instance['Slot'], Symbol('value')) 181 | self.assertEqual( 182 | str(instance), '[test-instance] of ConcreteClass (Slot value)') 183 | self.assertEqual( 184 | repr(instance), 185 | 'Instance: [test-instance] of ConcreteClass (Slot value)') 186 | self.assertEqual(dict(instance), {'Slot': Symbol('value')}) 187 | 188 | instance.delete() 189 | 190 | with self.assertRaises(LookupError): 191 | self.env.find_instance('test-instance') 192 | 193 | instance = defclass.make_instance('test-instance') 194 | 195 | instance.unmake() 196 | 197 | with self.assertRaises(LookupError): 198 | self.env.find_instance('test-instance') 199 | 200 | def test_make_instance_errors(self): 201 | """Instance errors.""" 202 | defclass = self.env.find_class('ConcreteClass') 203 | 204 | with self.assertRaises(KeyError): 205 | defclass.make_instance('some-instance', NonExistingSlot=1) 206 | with self.assertRaises(TypeError): 207 | defclass.make_instance('some-instance', Slot="wrong type") 208 | with self.assertRaises(ValueError): 209 | defclass.make_instance('some-instance', Slot=Symbol('wrong-value')) 210 | 211 | def test_modify_instance(self): 212 | """Instance slot modification test.""" 213 | defclass = self.env.find_class('ConcreteClass') 214 | 215 | defclass.make_instance('some-instance') 216 | instance = defclass.make_instance('test-instance', Slot=Symbol('value')) 217 | instance.modify_slots(Slot=Symbol('another-value')) 218 | 219 | self.assertEqual(instance['Slot'], Symbol('another-value')) 220 | 221 | instance.delete() 222 | 223 | def test_message_handler(self): 224 | """MessageHandler test.""" 225 | defclass = self.env.find_class('MessageHandlerClass') 226 | 227 | handler = defclass.find_message_handler('test-handler') 228 | 229 | expected_str = "(defmessage-handler MAIN::MessageHandlerClass " + \ 230 | "test-handler () (+ ?self:One ?self:Two))" 231 | 232 | self.assertTrue(handler.deletable) 233 | self.assertEqual(handler.type, 'primary') 234 | self.assertEqual(handler.name, 'test-handler') 235 | self.assertTrue(handler in defclass.message_handlers()) 236 | 237 | self.assertEqual(str(handler), expected_str) 238 | self.assertEqual(repr(handler), 'MessageHandler: ' + expected_str) 239 | 240 | self.assertFalse(handler.watch) 241 | handler.watch = True 242 | self.assertTrue(handler.watch) 243 | 244 | handler.undefine() 245 | 246 | def test_message_handler_instance(self): 247 | """MessageHandler instance test.""" 248 | defclass = self.env.find_class('MessageHandlerClass') 249 | 250 | instance = defclass.make_instance('test-instance', One=1, Two=2) 251 | 252 | self.assertEqual(instance.send('test-handler'), 3) 253 | 254 | def test_defined_instances(self): 255 | """DefinedInstances tests.""" 256 | self.env.build(DEFINSTANCES) 257 | definstances = self.env.find_defined_instances('defined-instances') 258 | listed = list(self.env.defined_instances()) 259 | 260 | self.assertEqual(definstances, listed[0]) 261 | self.assertEqual(definstances.name, 'defined-instances') 262 | self.assertEqual( 263 | str(definstances), 264 | '(definstances MAIN::defined-instances (c1 of ConcreteClass (Slot a-slot)))') 265 | self.assertEqual(definstances.module.name, 'MAIN') 266 | self.assertTrue(definstances.deletable) 267 | 268 | definstances.undefine() 269 | 270 | with self.assertRaises(LookupError): 271 | self.env.find_defined_instances('defined-instances') 272 | with self.assertRaises(CLIPSError): 273 | print(definstances) 274 | -------------------------------------------------------------------------------- /test/environment_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from tempfile import mkstemp 4 | 5 | from clips import CLIPSError 6 | from clips import Environment, Symbol, LoggingRouter, ImpliedFact, InstanceName 7 | 8 | DEFRULE_FACT = """ 9 | (defrule fact-rule 10 | ?fact <- (test-fact) 11 | => 12 | (python_method ?fact)) 13 | """ 14 | 15 | DEFRULE_INSTANCE = """ 16 | (defrule instance-rule 17 | ?instance <- (object (is-a TEST) 18 | (name ?instance-name)) 19 | => 20 | (python_method ?instance) 21 | (python_method ?instance-name)) 22 | """ 23 | 24 | DEFFUNCTION = """ 25 | (deffunction test-fact-function () 26 | (bind ?facts (python_fact_method)) 27 | (python_method ?facts)) 28 | """ 29 | 30 | DEFCLASS = """(defclass TEST (is-a USER))""" 31 | 32 | 33 | def python_function(*value): 34 | return value 35 | 36 | 37 | def python_types(): 38 | return None, True, False 39 | 40 | 41 | def python_objects(obj): 42 | return obj 43 | 44 | 45 | def python_error(): 46 | raise Exception("BOOM!") 47 | 48 | 49 | class TempFile: 50 | """Cross-platform temporary file.""" 51 | name = None 52 | 53 | def __enter__(self): 54 | fobj, self.name = mkstemp() 55 | os.close(fobj) 56 | 57 | return self 58 | 59 | def __exit__(self, *_): 60 | os.remove(self.name) 61 | 62 | 63 | class ObjectTest: 64 | def __init__(self, value): 65 | self.value = value 66 | 67 | 68 | class TestEnvironment(unittest.TestCase): 69 | def setUp(self): 70 | self.values = [] 71 | self.env = Environment() 72 | self.env.add_router(LoggingRouter()) 73 | self.env.define_function(python_function) 74 | self.env.define_function(python_function, 75 | name='python-function-renamed') 76 | self.env.define_function(python_error) 77 | self.env.define_function(python_types) 78 | self.env.define_function(python_objects) 79 | self.env.define_function(self.python_method) 80 | self.env.define_function(self.python_fact_method) 81 | self.env.build(DEFCLASS) 82 | self.env.build(DEFFUNCTION) 83 | self.env.build(DEFRULE_FACT) 84 | self.env.build(DEFRULE_INSTANCE) 85 | 86 | def tearDown(self): 87 | for router in tuple(self.env.routers()): 88 | router.delete() 89 | 90 | def python_method(self, *values): 91 | self.values += values 92 | 93 | def python_fact_method(self): 94 | """Returns a list with one fact.""" 95 | return [self.env.assert_string('(test-fact 5)')] 96 | 97 | def test_eval_python_function(self): 98 | """Python function is evaluated correctly.""" 99 | expected = (0, 1.1, "2", Symbol('three'), InstanceName('four')) 100 | ret = self.env.eval('(python_function 0 1.1 "2" three [four])') 101 | self.assertEqual(ret, expected) 102 | 103 | expected = (0, 1.1, "2", Symbol('three')) 104 | ret = self.env.eval('(python-function-renamed 0 1.1 "2" three)') 105 | self.assertEqual(ret, expected) 106 | 107 | expected = (Symbol('nil'), Symbol('TRUE'), Symbol('FALSE')) 108 | ret = self.env.eval('(python_types)') 109 | self.assertEqual(ret, expected) 110 | 111 | def test_eval_python_error(self): 112 | """Errors in Python functions are correctly set.""" 113 | self.assertIsNone(self.env.error_state) 114 | 115 | with self.assertRaises(CLIPSError): 116 | self.env.eval('(python_error)') 117 | self.assertTrue("[PYCODEFUN1]" in str(self.env.error_state)) 118 | 119 | self.env.clear_error_state() 120 | self.assertIsNone(self.env.error_state) 121 | 122 | def test_eval_python_method(self): 123 | """Python method is evaluated correctly.""" 124 | expected = [0, 1.1, "2", Symbol('three')] 125 | 126 | ret = self.env.eval('(python_method 0 1.1 "2" three)') 127 | 128 | self.assertEqual(ret, Symbol('nil')) 129 | self.assertEqual(self.values, expected) 130 | 131 | def test_call_python_object(self): 132 | """Python objects are correctly marshalled.""" 133 | test_object = ObjectTest(42) 134 | 135 | ret = self.env.call('python_objects', test_object) 136 | 137 | self.assertEqual(ret, test_object) 138 | 139 | def test_rule_python_fact(self): 140 | """Facts are forwarded to Python """ 141 | fact = self.env.assert_string('(test-fact)') 142 | self.env.run() 143 | 144 | self.assertEqual(self.values[0], fact) 145 | 146 | def test_rule_python_instance(self): 147 | """Instances are forwarded to Python """ 148 | defclass = self.env.find_class('TEST') 149 | inst = defclass.make_instance('test') 150 | self.env.run() 151 | 152 | self.assertEqual(self.values[0], inst) 153 | self.assertEqual(self.values[1], inst.name) 154 | 155 | def test_facts_function(self): 156 | """Python functions can return list of facts""" 157 | function = self.env.find_function('test-fact-function') 158 | function() 159 | 160 | self.assertTrue(isinstance(self.values[0], ImpliedFact)) 161 | 162 | def test_batch_star(self): 163 | """Commands are evaluated from file.""" 164 | with TempFile() as tmp: 165 | with open(tmp.name, 'wb') as tmpfile: 166 | tmpfile.write(b"(assert (test-fact))\n") 167 | 168 | self.env.batch_star(tmp.name) 169 | 170 | self.assertTrue( 171 | 'test-fact' in (f.template.name for f in self.env.facts())) 172 | 173 | def test_save_load(self): 174 | """Constructs are saved and loaded.""" 175 | with TempFile() as tmp: 176 | self.env.save(tmp.name) 177 | self.env.clear() 178 | self.env.load(tmp.name) 179 | 180 | self.assertTrue('fact-rule' in 181 | (r.name for r in self.env.rules())) 182 | 183 | with TempFile() as tmp: 184 | self.env.save(tmp.name, binary=True) 185 | self.env.clear() 186 | self.env.load(tmp.name, binary=True) 187 | 188 | self.assertTrue('fact-rule' in 189 | (r.name for r in self.env.rules())) 190 | -------------------------------------------------------------------------------- /test/facts_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from tempfile import mkstemp 4 | 5 | from clips import Environment, Symbol, CLIPSError, TemplateSlotDefaultType 6 | 7 | 8 | DEFTEMPLATE = """(deftemplate MAIN::template-fact 9 | (slot int (type INTEGER) (allowed-values 0 1 2 3 4 5 6 7 8 9)) 10 | (slot float (type FLOAT)) 11 | (slot str (type STRING)) 12 | (slot symbol (type SYMBOL)) 13 | (multislot multifield)) 14 | """ 15 | DEFFACTS = """(deffacts MAIN::defined-facts 16 | (template-fact (int 1) (str "a-string"))) 17 | """ 18 | 19 | IMPL_STR = '(implied-fact 1 2.3 "4" five)' 20 | IMPL_RPR = 'ImpliedFact: (implied-fact 1 2.3 "4" five)' 21 | TMPL_STR = '(template-fact (int 1) (float 2.2) (str "4") (symbol five) ' + \ 22 | '(multifield 1 2))' 23 | TMPL_RPR = 'TemplateFact: (template-fact (int 1) (float 2.2) ' + \ 24 | '(str "4") (symbol five) (multifield 1 2))' 25 | 26 | 27 | class TempFile: 28 | """Cross-platform temporary file.""" 29 | name = None 30 | 31 | def __enter__(self): 32 | fobj, self.name = mkstemp() 33 | os.close(fobj) 34 | 35 | return self 36 | 37 | def __exit__(self, *_): 38 | os.remove(self.name) 39 | 40 | 41 | class TestFacts(unittest.TestCase): 42 | def setUp(self): 43 | self.env = Environment() 44 | self.env.build(DEFTEMPLATE) 45 | 46 | def test_facts(self): 47 | """Facts wrapper test.""" 48 | template = self.env.find_template('template-fact') 49 | self.assertTrue(template in self.env.templates()) 50 | fact = self.env.assert_string('(implied-fact)') 51 | self.assertTrue(fact in self.env.facts()) 52 | 53 | self.env.load_facts('(one-fact) (two-facts)') 54 | self.assertTrue('(two-facts)' in (str(f) 55 | for f in self.env.facts())) 56 | 57 | with TempFile() as tmp: 58 | saved = self.env.save_facts(tmp.name) 59 | self.env.reset() 60 | loaded = self.env.load_facts(tmp.name) 61 | self.assertEqual(saved, loaded) 62 | 63 | def test_implied_fact(self): 64 | """ImpliedFacts are asserted.""" 65 | expected = (1, 2.3, '4', Symbol('five')) 66 | fact = self.env.assert_string('(implied-fact 1 2.3 "4" five)') 67 | 68 | self.assertEqual(fact[0], 1) 69 | self.assertEqual(len(fact), 4) 70 | self.assertEqual(fact.index, 1) 71 | self.assertEqual(tuple(fact), expected) 72 | self.assertEqual(str(fact), IMPL_STR) 73 | self.assertEqual(repr(fact), IMPL_RPR) 74 | self.assertTrue(fact in tuple(self.env.facts())) 75 | 76 | def test_template_fact(self): 77 | """TemplateFacts are asserted.""" 78 | expected = {'int': 1, 79 | 'float': 2.2, 80 | 'str': '4', 81 | 'symbol': Symbol('five'), 82 | 'multifield': (1, 2)} 83 | template = self.env.find_template('template-fact') 84 | fact = template.assert_fact(**expected) 85 | 86 | self.assertEqual(len(fact), 5) 87 | self.assertEqual(fact.index, 1) 88 | self.assertEqual(fact['int'], 1) 89 | self.assertEqual(dict(fact), expected) 90 | self.assertEqual(str(fact), TMPL_STR) 91 | self.assertEqual(repr(fact), TMPL_RPR) 92 | self.assertTrue(fact in tuple(template.facts())) 93 | self.assertTrue(fact in tuple(self.env.facts())) 94 | self.assertEqual(str(tuple(template.facts())[0]), TMPL_STR) 95 | self.assertEqual(str(tuple(self.env.facts())[0]), TMPL_STR) 96 | 97 | def test_template_fact_errors(self): 98 | """TemplateFacts errors.""" 99 | with self.assertRaises(LookupError): 100 | self.env.find_template('non-existing-template') 101 | 102 | template = self.env.find_template('template-fact') 103 | 104 | with self.assertRaises(KeyError): 105 | template.assert_fact(non_existing_slot=1) 106 | with self.assertRaises(TypeError): 107 | template.assert_fact(int=1.0) 108 | with self.assertRaises(ValueError): 109 | template.assert_fact(int=10) 110 | 111 | def test_fact_duplication(self): 112 | """Test fact duplication.""" 113 | fact = self.env.assert_string('(implied-fact)') 114 | new_fact = self.env.assert_string('(implied-fact)') 115 | 116 | self.assertEqual(fact, new_fact) 117 | self.assertEqual(len(tuple(self.env.facts())), 1) 118 | 119 | self.env.fact_duplication = True 120 | 121 | new_fact = self.env.assert_string('(implied-fact)') 122 | 123 | self.assertNotEqual(fact, new_fact) 124 | self.assertEqual(len(tuple(self.env.facts())), 2) 125 | 126 | def test_modify_fact(self): 127 | """Asserted TemplateFacts can be modified.""" 128 | template = self.env.find_template('template-fact') 129 | fact = template.assert_fact(**{'int': 1, 130 | 'float': 2.2, 131 | 'str': '4', 132 | 'symbol': Symbol('five'), 133 | 'multifield': (1, 2)}) 134 | 135 | fact.modify_slots(symbol=Symbol('six')) 136 | self.assertEqual(fact['symbol'], Symbol('six')) 137 | 138 | def test_retract_fact(self): 139 | """Retracted fact is not anymore in the fact list.""" 140 | fact = self.env.assert_string('(implied-fact)') 141 | 142 | self.assertTrue(fact in list(self.env.facts())) 143 | 144 | fact.retract() 145 | 146 | self.assertFalse(fact in list(self.env.facts())) 147 | 148 | def test_implied_fact_template(self): 149 | """ImpliedFact template properties.""" 150 | fact = self.env.assert_string('(implied-fact 1 2.3 "4" five)') 151 | template = fact.template 152 | 153 | self.assertEqual(template.watch, False) 154 | template.watch = True 155 | self.assertEqual(template.watch, True) 156 | self.assertTrue(template.implied) 157 | self.assertEqual(template.name, 'implied-fact') 158 | self.assertEqual(template.module.name, 'MAIN') 159 | self.assertEqual(template.slots, ()) 160 | self.assertEqual(str(template), '') 161 | self.assertEqual(repr(template), 'Template: ') 162 | self.assertFalse(template.deletable) 163 | with self.assertRaises(CLIPSError): 164 | template.undefine() 165 | 166 | def test_template_fact_template(self): 167 | """TemplateFact template properties.""" 168 | template = self.env.find_template('template-fact') 169 | 170 | self.assertEqual(template.watch, False) 171 | template.watch = True 172 | self.assertEqual(template.watch, True) 173 | self.assertEqual(template.name, 'template-fact') 174 | self.assertEqual(template.module.name, 'MAIN') 175 | self.assertEqual(len(tuple(template.slots)), 5) 176 | self.assertEqual(str(template), ' '.join(DEFTEMPLATE.split())) 177 | self.assertEqual(repr(template), 178 | 'Template: ' + ' '.join(DEFTEMPLATE.split())) 179 | self.assertTrue(template.deletable) 180 | 181 | template.undefine() 182 | 183 | with self.assertRaises(LookupError): 184 | self.env.find_template('template-fact') 185 | with self.assertRaises(CLIPSError): 186 | print(template) 187 | 188 | def test_template_fact_slot(self): 189 | """TemplateFact template Slot.""" 190 | template = self.env.find_template('template-fact') 191 | 192 | slots = {s.name: s for s in template.slots} 193 | 194 | self.assertEqual(slots['int'].name, 'int') 195 | self.assertFalse(slots['int'].multifield) 196 | self.assertTrue(slots['multifield'].multifield) 197 | 198 | self.assertEqual(slots['int'].types, ('INTEGER', )) 199 | self.assertEqual(slots['float'].types, ('FLOAT', )) 200 | self.assertEqual(slots['str'].types, ('STRING', )) 201 | self.assertEqual(slots['symbol'].types, ('SYMBOL', )) 202 | 203 | self.assertEqual(slots['int'].range, ('-oo', '+oo')) 204 | self.assertEqual(slots['float'].cardinality, ()) 205 | self.assertEqual(slots['str'].default_type, 206 | TemplateSlotDefaultType.STATIC_DEFAULT) 207 | self.assertEqual(slots['str'].default_value, '') 208 | self.assertEqual(slots['int'].allowed_values, 209 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) 210 | 211 | def test_defined_facts(self): 212 | """DefinedFacts tests.""" 213 | self.env.build(DEFFACTS) 214 | deffacts = self.env.find_defined_facts('defined-facts') 215 | listed = list(self.env.defined_facts()) 216 | 217 | self.assertEqual(deffacts, listed[0]) 218 | self.assertEqual(deffacts.name, 'defined-facts') 219 | self.assertEqual( 220 | str(deffacts), 221 | '(deffacts MAIN::defined-facts (template-fact (int 1) (str "a-string")))') 222 | self.assertEqual(deffacts.module.name, 'MAIN') 223 | self.assertTrue(deffacts.deletable) 224 | 225 | deffacts.undefine() 226 | 227 | with self.assertRaises(LookupError): 228 | self.env.find_defined_facts('defined-facts') 229 | with self.assertRaises(CLIPSError): 230 | print(deffacts) 231 | -------------------------------------------------------------------------------- /test/functions_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from clips import Environment, Symbol, CLIPSError 4 | 5 | 6 | DEFFUNCTION1 = """(deffunction function-sum (?a ?b) (+ ?a ?b))""" 7 | DEFFUNCTION2 = """(deffunction function-sub (?a ?b) (- ?a ?b))""" 8 | DEFGENERIC1 = """(defgeneric generic-sum)""" 9 | DEFGENERIC2 = """(defgeneric generic-sub)""" 10 | DEFMETHOD = """ 11 | (defmethod generic-sum ((?a INTEGER) (?b INTEGER)) (+ ?a ?b)) 12 | """ 13 | 14 | 15 | class TestFunctions(unittest.TestCase): 16 | def setUp(self): 17 | self.env = Environment() 18 | self.env.build(DEFMETHOD) 19 | self.env.build(DEFGENERIC1) 20 | self.env.build(DEFGENERIC2) 21 | self.env.build(DEFFUNCTION1) 22 | self.env.build(DEFFUNCTION2) 23 | 24 | def test_function_call(self): 25 | """Test function call.""" 26 | function = self.env.find_function('function-sum') 27 | self.assertEqual(function(1, 2), 3) 28 | 29 | function = self.env.find_generic('generic-sum') 30 | self.assertEqual(function(1, 2), 3) 31 | 32 | self.assertEqual(self.env.call('function-sum', 1, 2), 3) 33 | self.assertEqual(self.env.call('generic-sum', 1, 2), 3) 34 | 35 | self.assertEqual( 36 | self.env.call('create$', 1, 2.0, "three", Symbol('four')), 37 | (1, 2.0, 'three', 'four')) 38 | 39 | def test_function(self): 40 | """Deffunction object test.""" 41 | func = self.env.find_function("function-sub") 42 | 43 | self.assertTrue(func in self.env.functions()) 44 | 45 | self.assertEqual(func.name, "function-sub") 46 | self.assertEqual(func.module.name, "MAIN") 47 | self.assertTrue('deffunction' in str(func)) 48 | self.assertTrue('deffunction' in repr(func)) 49 | self.assertTrue(func.deletable) 50 | self.assertFalse(func.watch) 51 | 52 | func.watch = True 53 | 54 | self.assertTrue(func.watch) 55 | 56 | func.undefine() 57 | 58 | with self.assertRaises(LookupError): 59 | self.env.find_function("function-sub") 60 | with self.assertRaises(CLIPSError): 61 | print(func) 62 | 63 | def test_generic(self): 64 | """Defgeneric object test.""" 65 | func = self.env.find_generic("generic-sum") 66 | 67 | self.assertTrue(func in self.env.generics()) 68 | 69 | self.assertEqual(func.name, "generic-sum") 70 | self.assertEqual(func.module.name, "MAIN") 71 | self.assertTrue('defgeneric' in str(func)) 72 | self.assertTrue('defgeneric' in repr(func)) 73 | self.assertTrue(func.deletable) 74 | self.assertFalse(func.watch) 75 | 76 | func.watch = True 77 | 78 | self.assertTrue(func.watch) 79 | 80 | func.undefine() 81 | 82 | with self.assertRaises(LookupError): 83 | self.env.find_function("generic-sum") 84 | with self.assertRaises(CLIPSError): 85 | print(func) 86 | 87 | def test_method(self): 88 | """Defgeneric object test.""" 89 | restr = (2, 2, 2, 6, 9, Symbol('FALSE'), 1, Symbol('INTEGER'), 90 | Symbol('FALSE'), 1, Symbol('INTEGER')) 91 | func = self.env.find_generic("generic-sum") 92 | 93 | method = tuple(func.methods())[0] 94 | self.assertTrue('defmethod' in str(method)) 95 | self.assertTrue('defmethod' in repr(method)) 96 | self.assertTrue(method.deletable) 97 | self.assertFalse(method.watch) 98 | self.assertEqual(method.description, "1 (INTEGER) (INTEGER)") 99 | self.assertEqual(method.restrictions, restr) 100 | 101 | method.watch = True 102 | 103 | self.assertTrue(method.watch) 104 | 105 | method.undefine() 106 | 107 | self.assertTrue(method not in func.methods()) 108 | -------------------------------------------------------------------------------- /test/modules_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from clips import Environment, Symbol, CLIPSError 4 | 5 | 6 | DEFGLOBAL = """ 7 | (defglobal 8 | ?*a* = 1 9 | ?*b* = 2) 10 | """ 11 | 12 | DEFMODULE = """(defmodule TEST)""" 13 | 14 | 15 | class TestModules(unittest.TestCase): 16 | def setUp(self): 17 | self.env = Environment() 18 | self.env.build(DEFGLOBAL) 19 | 20 | def tearDown(self): 21 | self.env = None 22 | 23 | def test_modules(self): 24 | """Modules wrapper class test.""" 25 | self.env.build(DEFMODULE) 26 | 27 | # reset MAIN module 28 | module = self.env.find_module('MAIN') 29 | self.env.current_module = module 30 | 31 | module = self.env.find_module('TEST') 32 | self.env.current_module = module 33 | 34 | self.assertEqual(self.env.current_module, 35 | self.env.find_module('TEST')) 36 | self.assertTrue(module in self.env.modules()) 37 | self.assertEqual(self.env.current_module, module) 38 | 39 | with self.assertRaises(LookupError): 40 | self.env.find_module("NONEXISTING") 41 | 42 | def test_global(self): 43 | """Defglobal object test.""" 44 | glbl = self.env.find_global("b") 45 | 46 | self.assertTrue(glbl in self.env.globals()) 47 | self.assertEqual(glbl.value, 2) 48 | 49 | glbl.value = 3 50 | 51 | self.assertEqual(glbl.value, 3) 52 | self.assertTrue(self.env.globals_changed) 53 | 54 | self.assertEqual(glbl.name, "b") 55 | self.assertEqual(glbl.module.name, "MAIN") 56 | self.assertTrue('defglobal' in str(glbl)) 57 | self.assertTrue('defglobal' in repr(glbl)) 58 | self.assertTrue(glbl.deletable) 59 | self.assertFalse(glbl.watch) 60 | 61 | glbl.watch = True 62 | 63 | self.assertTrue(glbl.watch) 64 | 65 | glbl.undefine() 66 | 67 | with self.assertRaises(LookupError): 68 | self.env.find_global("b") 69 | with self.assertRaises(CLIPSError): 70 | print(glbl) 71 | 72 | def test_module(self): 73 | """Module object test.""" 74 | module = self.env.current_module 75 | 76 | self.assertEqual(module.name, 'MAIN') 77 | self.assertEqual(str(module), '') 78 | self.assertEqual(repr(module), 'Module: ') 79 | --------------------------------------------------------------------------------