├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── RELEASE_PR.md └── workflows │ ├── docs.yml │ ├── lint.yml │ ├── python-publish.yml │ └── test.yml ├── .gitignore ├── AUTHORS.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── docs ├── grammars.md ├── index.md ├── introduction.md ├── parsing.md ├── references.md ├── supported_logics.md └── syntax.md ├── mkdocs.yml ├── poetry.lock ├── pylogics ├── __init__.py ├── deduction │ ├── __init__.py │ └── fol │ │ ├── __init__.py │ │ ├── base.py │ │ └── nd.py ├── exceptions.py ├── helpers │ ├── __init__.py │ ├── cache_hash.py │ └── misc.py ├── parsers │ ├── __init__.py │ ├── base.py │ ├── ldl.lark │ ├── ldl.py │ ├── ltl.lark │ ├── ltl.py │ ├── pl.lark │ ├── pl.py │ ├── pltl.lark │ └── pltl.py ├── semantics │ ├── __init__.py │ ├── base.py │ └── pl.py ├── syntax │ ├── __init__.py │ ├── base.py │ ├── fol.py │ ├── ldl.py │ ├── ltl.py │ ├── pl.py │ └── pltl.py └── utils │ ├── __init__.py │ └── to_string.py ├── pyproject.toml ├── pytest.ini ├── scripts ├── check_copyright.py ├── sync-tox-ini.py ├── update-dependencies.py └── whitelist.py ├── setup.cfg ├── tests ├── __init__.py ├── conftest.py ├── test_base.py ├── test_docs │ ├── __init__.py │ ├── base.py │ ├── test_grammars.py │ ├── test_introduction.py │ └── test_parsing.py ├── test_exceptions.py ├── test_helpers.py ├── test_ldl.py ├── test_ltl.py ├── test_pl.py └── test_pltl.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Subject of the issue 11 | Describe your issue here. 12 | 13 | ### Your environment 14 | - OS: [e.g. iOS] 15 | - Python version: [e.g. 3.7.2] 16 | - Package Version [e.g. 0.1.2] 17 | - Anything else you consider helpful. 18 | 19 | ### Steps to reproduce 20 | Tell us how to reproduce this issue. 21 | 22 | ### Expected behaviour 23 | Tell us what should happen 24 | 25 | ### Actual behaviour 26 | Tell us what happens instead 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. 4 | 5 | ## Fixes 6 | 7 | If it fixes a bug or resolves a feature request, be sure to link to that issue. 8 | 9 | ## Types of changes 10 | 11 | What types of changes does your code introduce? 12 | _Put an `x` in the boxes that apply_ 13 | 14 | - [ ] Bugfix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | 18 | ## Checklist 19 | 20 | _Put an `x` in the boxes that apply._ 21 | 22 | - [ ] I have read the [CONTRIBUTING](../blob/master/CONTRIBUTING.md) doc 23 | - [ ] I am making a pull request against the `develop` branch (left side). Also you should start your branch off our `develop`. 24 | - [ ] Lint and unit tests pass locally with my changes 25 | - [ ] I have added tests that prove my fix is effective or that my feature works 26 | 27 | ## Further comments 28 | 29 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 30 | -------------------------------------------------------------------------------- /.github/RELEASE_PR.md: -------------------------------------------------------------------------------- 1 | ## Release summary 2 | 3 | Version number: [e.g. 1.0.1] 4 | 5 | ## Release details 6 | 7 | Describe in short the main changes with the new release. 8 | 9 | ## Checklist 10 | 11 | _Put an `x` in the boxes that apply._ 12 | 13 | - [ ] I have read the [CONTRIBUTING](../master/CONTRIBUTING.md) doc 14 | - [ ] I am making a pull request against the `master` branch (left side), from `develop` 15 | - [ ] I've updated the dependencies versions to the latest, wherever is possible. 16 | - [ ] Lint and unit tests pass locally (please run tests also manually, not only with `tox`) 17 | - [ ] I built the documentation and updated it with the latest changes 18 | - [ ] I've added an item in `HISTORY.md` for this release 19 | - [ ] I bumped the version number in the `__init__.py` file. 20 | - [ ] I published the latest version on TestPyPI and checked that the following command work: 21 | ```pip install pylogics== --index-url https://test.pypi.org/simple --force --no-cache-dir --no-deps``` 22 | - [ ] After merging the PR, I'll publish the build also on PyPI. Then, I'll make sure the following 23 | command will work: 24 | ```pip install pylogics== --force --no-cache-dir --no-deps``` 25 | - [ ] After merging the PR, I'll tag the repo with `v${VERSION_NUMVER}` (e.g. `v0.1.2`) 26 | 27 | 28 | ## Further comments 29 | 30 | Write here any other comment about the release, if any. 31 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | run: 12 | continue-on-error: True 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | python-version: [3.8] 19 | 20 | timeout-minutes: 30 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions/setup-python@master 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: pip install tox 29 | - name: Generate Documentation 30 | run: tox -e docs 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | run: 12 | continue-on-error: True 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | python-version: [3.8] 19 | 20 | timeout-minutes: 30 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions/setup-python@master 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: pip install tox 29 | # TODO: introduce Pylint 30 | # - name: Pylint 31 | # run: tox -e pylint 32 | - name: Code style check 33 | run: | 34 | tox -e black-check 35 | tox -e isort-check 36 | tox -e flake8 37 | tox -e vulture 38 | - name: Static type check 39 | run: tox -e mypy 40 | - name: Check copyright 41 | run: tox -e check-copyright 42 | - name: Misc checks 43 | run: | 44 | tox -e bandit 45 | tox -e safety 46 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | run: 12 | continue-on-error: True 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] #, macos-latest, windows-latest] 18 | python-version: ["3.8", "3.9", "3.10", "3.11"] 19 | 20 | timeout-minutes: 30 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: actions/setup-python@master 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: pip install tox poetry 29 | # # set up environment depending on the platform in use 30 | # - if: matrix.os == 'ubuntu-latest' 31 | # name: Install dependencies (ubuntu-latest) 32 | # run: ... 33 | # - if: matrix.os == 'macos-latest' 34 | # name: Install dependencies (macos-latest) 35 | # run: ... 36 | # - if: matrix.os == 'windows-latest' 37 | # name: Install dependencies (windows-latest) 38 | # env: 39 | # ACTIONS_ALLOW_UNSECURE_COMMANDS: true 40 | # run: ... 41 | - name: Unit tests and coverage 42 | run: | 43 | tox -e py${{ matrix.python-version }} 44 | - name: Upload coverage to Codecov 45 | uses: codecov/codecov-action@v1 46 | with: 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | file: ./coverage.xml 49 | flags: unittests 50 | name: codecov-umbrella 51 | fail_ci_if_error: true 52 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Maintainers 4 | 5 | - [Marco Favorito](https://github.com/marcofavorito) 6 | 7 | ## Contributors 8 | 9 | - [Francesco Fuggitti](https://github.com/francescofuggitti) 10 | - [Roberto Cipollone](https://github.com/cipollone) 11 | 12 | 13 | [Do you want to be a contributor](./contributing.md)? 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and greatly appreciated! Every little bit helps, and credit will always be given. 4 | 5 | If you need support, want to report/fix a bug, ask for/implement features, you can check the 6 | [Issues page](https://github.com/marcofavorito/pylogics/issues) 7 | or [submit a Pull request](https://github.com/marcofavorito/pylogics/pulls). 8 | 9 | For other kinds of feedback, you can contact one of the [authors](./authors.md) by email. 10 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 0.2.1 (2023-06-06) 4 | 5 | The main change of this release is to relax the supported grammar, in particular regarding the symbols definition. 6 | 7 | * Wip/grammar by @francescofuggitti in https://github.com/whitemech/pylogics/pull/81 8 | * Earlier, symbols could not contain uppercase letters. Now, symbols cannot *start* with uppercase letters. 9 | * Now, hyphens can only occur in the middle characters of a word, i.e. neither at the start nor at the end of the symbol (unless quoted). 10 | * Double quoted words can contain all printable characters (except the double quote ". 11 | 12 | 13 | ## 0.1.1 (2021-09-25) 14 | 15 | - Fixed bug in LDLf parsing; the regular expression of type "test" 16 | was not parsed correctly ([#76](https://github.com/whitemech/pylogics/pull/76)). 17 | - Other minor bug fixing 18 | 19 | ## 0.1.0 (2021-06-07) 20 | 21 | - Improved to the behaviour of `Not`: 22 | - Make `Not` to simplify when argument is a boolean formula. If `Not` is applied to `TrueFormula`, then the output is `FalseFormula`; 23 | likewise, if it is applied to `FalseFormula`, the output is `TrueFormula`. 24 | - Fix: replace `__neg__` with `__invert__` 25 | - Improved simplification of monotone operators: check also 26 | the presence of `phi OP ~phi` and reduce according to the 27 | binary operator involved. 28 | - Added tests to check consistency between code and documentation. 29 | - Updated grammars so to be compliant with 30 | version `0.2.0` of [this standard](https://marcofavorito.me/tl-grammars/v/7d9a17267fbf525d9a6a1beb92a46f05cf652db6/). 31 | 32 | 33 | 34 | ## 0.1.0a0 (2021-04-23) 35 | 36 | - Added support for Propositional Logic parsing, 37 | syntax representation and parsing. 38 | - Added support for Linear Temporal Logic 39 | parsing and syntax representation. 40 | - Added support for Past Linear Temporal Logic 41 | parsing and syntax representation. 42 | - Added support for Linear Dynamic Logic 43 | parsing and syntax representation. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 The Pylogics contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test clean-docs ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-docs: ## remove MkDocs products. 48 | mkdocs build --clean 49 | rm -fr site/ 50 | 51 | 52 | clean-test: ## remove test and coverage artifacts 53 | rm -fr .tox/ 54 | rm -f .coverage 55 | rm -fr htmlcov/ 56 | rm -fr .pytest_cache 57 | rm -fr .mypy_cache 58 | rm -fr .hypothesis **/.hypothesis 59 | rm -fr coverage.xml 60 | 61 | lint-all: black isort lint static bandit safety vulture pylint ## run all linters 62 | 63 | lint: ## check style with flake8 64 | flake8 pylogics tests scripts 65 | 66 | static: ## static type checking with mypy 67 | mypy pylogics tests scripts 68 | 69 | isort: ## sort import statements with isort 70 | isort pylogics tests scripts 71 | 72 | isort-check: ## check import statements order with isort 73 | isort --check-only pylogics tests scripts 74 | 75 | black: ## apply black formatting 76 | black pylogics tests scripts 77 | 78 | black-check: ## check black formatting 79 | black --check --verbose pylogics tests scripts 80 | 81 | bandit: ## run bandit 82 | bandit pylogics tests scripts 83 | 84 | safety: ## run safety 85 | safety check 86 | 87 | pylint: ## run pylint 88 | pylint pylogics tests scripts 89 | 90 | vulture: ## run vulture 91 | vulture pylogics scripts/whitelist.py 92 | 93 | test: ## run tests quickly with the default Python 94 | pytest tests --doctest-modules \ 95 | pylogics tests/ \ 96 | --cov=pylogics \ 97 | --cov-report=xml \ 98 | --cov-report=html \ 99 | --cov-report=term 100 | 101 | test-all: ## run tests on every Python version with tox 102 | tox 103 | 104 | coverage: ## check code coverage quickly with the default Python 105 | coverage run --source pylogics -m pytest 106 | coverage report -m 107 | coverage html 108 | $(BROWSER) htmlcov/index.html 109 | 110 | docs: ## generate MkDocs HTML documentation, including API docs 111 | mkdocs build --clean 112 | $(BROWSER) site/index.html 113 | 114 | servedocs: docs ## compile the docs watching for changes 115 | mkdocs build --clean 116 | python -c 'print("###### Starting local server. Press Control+C to stop server ######")' 117 | mkdocs serve 118 | 119 | release: dist ## package and upload a release 120 | twine upload dist/* 121 | 122 | dist: clean ## builds source and wheel package 123 | poetry build 124 | ls -l dist 125 | 126 | install: clean ## install the package to the active Python's site-packages 127 | poetry install 128 | 129 | develop: clean ## install the package in development mode 130 | echo "Not supported by Poetry yet!" 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PyLogics 3 |

4 | 5 |

6 | 7 | PyPI 8 | 9 | 10 | PyPI - Python Version 11 | 12 | 13 | PyPI - Status 14 | 15 | 16 | PyPI - Implementation 17 | 18 | 19 | PyPI - Wheel 20 | 21 | 22 | GitHub 23 | 24 |

25 |

26 | 27 | test 28 | 29 | 30 | lint 31 | 32 | 33 | docs 34 | 35 | 36 | codecov 37 | 38 |

39 |

40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | black 48 | 49 | 50 | 51 | 52 |

53 | 54 | 55 | A Python library for logic formalisms representation and manipulation. 56 | 57 | ## Install 58 | 59 | To install the package from PyPI: 60 | ``` 61 | pip install pylogics 62 | ``` 63 | 64 | ## Tests 65 | 66 | To run tests: `tox` 67 | 68 | To run only the code tests: `tox -e py3.7` 69 | 70 | To run only the linters: 71 | - `tox -e flake8` 72 | - `tox -e mypy` 73 | - `tox -e black-check` 74 | - `tox -e isort-check` 75 | 76 | Please look at the `tox.ini` file for the full list of supported commands. 77 | 78 | ## Docs 79 | 80 | To build the docs: `mkdocs build` 81 | 82 | To view documentation in a browser: `mkdocs serve` 83 | and then go to [http://localhost:8000](http://localhost:8000) 84 | 85 | ## License 86 | 87 | pylogics is released under the MIT License. 88 | 89 | Copyright 2021-2024 The Pylogics contributors 90 | 91 | ## Authors 92 | 93 | - [Marco Favorito](https://github.com/marcofavorito) 94 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: "reach,diff,flags,tree" 19 | behavior: default 20 | require_changes: false 21 | -------------------------------------------------------------------------------- /docs/grammars.md: -------------------------------------------------------------------------------- 1 | 2 | In this section, all the grammars used 3 | by the library are reported here. 4 | 5 | ## Propositional Logic 6 | 7 | The file 8 | [`pl.lark`](https://github.com/whitemech/pylogics/blob/main/pylogics/parsers/pl.lark) 9 | contains the specification of the Lark grammar, and it is reported below: 10 | 11 | ```lark 12 | start: propositional_formula 13 | 14 | ?propositional_formula: prop_equivalence 15 | ?prop_equivalence: prop_implication (EQUIVALENCE prop_implication)* 16 | ?prop_implication: prop_or (IMPLY prop_or)* 17 | ?prop_or: prop_and (OR prop_and)* 18 | ?prop_and: prop_not (AND prop_not)* 19 | ?prop_not: NOT* prop_wrapped 20 | ?prop_wrapped: prop_atom 21 | | LEFT_PARENTHESIS propositional_formula RIGHT_PARENTHESIS 22 | ?prop_atom: atom 23 | | prop_true 24 | | prop_false 25 | 26 | atom: SYMBOL_NAME 27 | prop_true: TRUE 28 | prop_false: FALSE 29 | 30 | LEFT_PARENTHESIS : "(" 31 | RIGHT_PARENTHESIS : ")" 32 | EQUIVALENCE : "<->" 33 | IMPLY : ">>"|"->" 34 | OR: "||"|"|" 35 | AND: "&&"|"&" 36 | NOT: "!"|"~" 37 | TRUE.2: /true/ 38 | FALSE.2: /false/ 39 | 40 | // Symbols cannot start with uppercase letters, because these are reserved. Moreover, any word between quotes is a symbol. 41 | // More in detail: 42 | // 1) either start with [a-z_], followed by at least one [a-zA-Z0-9_-], and by one [a-zA-Z0-9_] (i.e. hyphens only in between) 43 | // 2) or, start with [a-z_] and follows with any sequence of [a-zA-Z0-9_] (no hyphens) 44 | // 3) or, any sequence of ASCII printable characters (i.e. going from ' ' to '~'), except '"'. 45 | SYMBOL_NAME: FIRST_SYMBOL_CHAR _SYMBOL_1_BODY _SYMBOL_1_TAIL 46 | | FIRST_SYMBOL_CHAR _SYMBOL_2_BODY 47 | | DOUBLE_QUOTES _SYMBOL_3_BODY DOUBLE_QUOTES 48 | 49 | _SYMBOL_QUOTED: DOUBLE_QUOTES _SYMBOL_3_BODY DOUBLE_QUOTES 50 | _SYMBOL_1_BODY: /[a-zA-Z0-9_\-]+/ 51 | _SYMBOL_1_TAIL: /[a-zA-Z0-9_]/ 52 | _SYMBOL_2_BODY: /[a-zA-Z0-9_]*/ 53 | _SYMBOL_3_BODY: /[ -!#-~]+?/ 54 | 55 | DOUBLE_QUOTES: "\"" 56 | FIRST_SYMBOL_CHAR: /[a-z_]/ 57 | 58 | 59 | %ignore /\s+/ 60 | ``` 61 | 62 | ## Linear Temporal Logic 63 | 64 | The Lark grammar for Linear Temporal Logic is defined 65 | in [`ltl.lark`](https://github.com/whitemech/pylogics/blob/main/pylogics/parsers/ltl.lark), 66 | and it is reported below: 67 | 68 | ```lark 69 | start: ltlf_formula 70 | 71 | ?ltlf_formula: ltlf_equivalence 72 | ?ltlf_equivalence: ltlf_implication (EQUIVALENCE ltlf_implication)* 73 | ?ltlf_implication: ltlf_or (IMPLY ltlf_or)* 74 | ?ltlf_or: ltlf_and (OR ltlf_and)* 75 | ?ltlf_and: ltlf_weak_until (AND ltlf_weak_until)* 76 | ?ltlf_weak_until: ltlf_until (WEAK_UNTIL ltlf_until)* 77 | ?ltlf_until: ltlf_release (UNTIL ltlf_release)* 78 | ?ltlf_release: ltlf_strong_release (RELEASE ltlf_strong_release)* 79 | ?ltlf_strong_release: ltlf_unaryop (STRONG_RELEASE ltlf_unaryop)* 80 | 81 | ?ltlf_unaryop: ltlf_always 82 | | ltlf_eventually 83 | | ltlf_next 84 | | ltlf_weak_next 85 | | ltlf_not 86 | | ltlf_wrapped 87 | 88 | ?ltlf_always: ALWAYS ltlf_unaryop 89 | ?ltlf_eventually: EVENTUALLY ltlf_unaryop 90 | ?ltlf_next: NEXT ltlf_unaryop 91 | ?ltlf_weak_next: WEAK_NEXT ltlf_unaryop 92 | ?ltlf_not: NOT ltlf_unaryop 93 | ?ltlf_wrapped: ltlf_atom 94 | | LEFT_PARENTHESIS ltlf_formula RIGHT_PARENTHESIS 95 | ?ltlf_atom: ltlf_symbol 96 | | ltlf_true 97 | | ltlf_false 98 | | ltlf_tt 99 | | ltlf_ff 100 | | ltlf_last 101 | 102 | ltlf_symbol: SYMBOL_NAME 103 | ltlf_true: prop_true 104 | ltlf_false: prop_false 105 | ltlf_tt: TT 106 | ltlf_ff: FF 107 | ltlf_last: LAST 108 | 109 | // Operators must not be part of a word 110 | UNTIL.2: /U(?=[ "\(])/ 111 | RELEASE.2: /R(?=[ "\(])/ 112 | ALWAYS.2: /G(?=[ "\(])/ 113 | EVENTUALLY.2: /F(?=[ "\(])/ 114 | NEXT.2: /X\[!\](?=[ "\(])/ 115 | WEAK_NEXT.2: /X(?=[ "\(])/ 116 | WEAK_UNTIL.2: /W(?=[ "\(])/ 117 | STRONG_RELEASE.2: /M(?=[ "\(])/ 118 | 119 | 120 | END.2: /end/ 121 | LAST.2: /last/ 122 | 123 | TT.2: /tt/ 124 | FF.2: /ff/ 125 | 126 | %ignore /\s+/ 127 | 128 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 129 | %import .pl.prop_true -> prop_true 130 | %import .pl.prop_false -> prop_false 131 | %import .pl.NOT -> NOT 132 | %import .pl.OR -> OR 133 | %import .pl.AND -> AND 134 | %import .pl.EQUIVALENCE -> EQUIVALENCE 135 | %import .pl.IMPLY -> IMPLY 136 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 137 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 138 | ``` 139 | 140 | ## Past Linear Temporal Logic 141 | 142 | The Lark grammar for Past Linear Temporal Logic is defined 143 | in [`pltl.lark`](https://github.com/whitemech/pylogics/blob/main/pylogics/parsers/pltl.lark), 144 | and it is reported below: 145 | 146 | ```lark 147 | start: pltlf_formula 148 | 149 | ?pltlf_formula: pltlf_equivalence 150 | ?pltlf_equivalence: pltlf_implication (EQUIVALENCE pltlf_implication)* 151 | ?pltlf_implication: pltlf_or (IMPLY pltlf_or)* 152 | ?pltlf_or: pltlf_and (OR pltlf_and)* 153 | ?pltlf_and: pltlf_since (AND pltlf_since)* 154 | ?pltlf_since: pltlf_unaryop (SINCE pltlf_unaryop)* 155 | 156 | ?pltlf_unaryop: pltlf_historically 157 | | pltlf_once 158 | | pltlf_before 159 | | pltlf_not 160 | | pltlf_wrapped 161 | 162 | ?pltlf_historically: HISTORICALLY pltlf_unaryop 163 | ?pltlf_once: ONCE pltlf_unaryop 164 | ?pltlf_before: BEFORE pltlf_unaryop 165 | ?pltlf_not: NOT pltlf_unaryop 166 | ?pltlf_wrapped: pltlf_atom 167 | | LEFT_PARENTHESIS pltlf_formula RIGHT_PARENTHESIS 168 | ?pltlf_atom: pltlf_symbol 169 | | pltlf_true 170 | | pltlf_false 171 | | pltlf_tt 172 | | pltlf_ff 173 | | pltlf_start 174 | 175 | pltlf_symbol: SYMBOL_NAME 176 | pltlf_true: prop_true 177 | pltlf_false: prop_false 178 | pltlf_tt: TT 179 | pltlf_ff: FF 180 | pltlf_start: START 181 | 182 | // Operators must not be part of a word 183 | SINCE.2: /S(?=[ "\(])/ 184 | HISTORICALLY.2: /H(?=[ "\(])/ 185 | ONCE.2: /O(?=[ "\(])/ 186 | BEFORE.2: /Y(?=[ "\(])/ 187 | FIRST.2: /first/ 188 | START.2: /start/ 189 | 190 | %ignore /\s+/ 191 | 192 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 193 | %import .pl.prop_true -> prop_true 194 | %import .pl.prop_false -> prop_false 195 | %import .pl.NOT -> NOT 196 | %import .pl.OR -> OR 197 | %import .pl.AND -> AND 198 | %import .pl.EQUIVALENCE -> EQUIVALENCE 199 | %import .pl.IMPLY -> IMPLY 200 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 201 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 202 | %import .ltl.TT -> TT 203 | %import .ltl.FF -> FF 204 | ``` 205 | 206 | ## Linear Dynamic Logic 207 | 208 | The Lark grammar for Linear Dynamic Logic is defined 209 | in [`ldl.lark`](https://github.com/whitemech/pylogics/blob/main/pylogics/parsers/ldl.lark), 210 | and it is reported below: 211 | 212 | ```lark 213 | start: ldlf_formula 214 | 215 | ?ldlf_formula: ldlf_equivalence 216 | ?ldlf_equivalence: ldlf_implication (EQUIVALENCE ldlf_implication)* 217 | ?ldlf_implication: ldlf_or (IMPLY ldlf_or)* 218 | ?ldlf_or: ldlf_and (OR ldlf_and)* 219 | ?ldlf_and: ldlf_unaryop (AND ldlf_unaryop)* 220 | 221 | ?ldlf_unaryop: ldlf_box 222 | | ldlf_diamond 223 | | ldlf_not 224 | | ldlf_wrapped 225 | ?ldlf_box: LEFT_SQUARE_BRACKET regular_expression RIGHT_SQUARE_BRACKET ldlf_unaryop 226 | ?ldlf_diamond: LEFT_ANGLE_BRACKET regular_expression RIGHT_ANGLE_BRACKET ldlf_unaryop 227 | ?ldlf_not: NOT ldlf_unaryop 228 | ?ldlf_wrapped: ldlf_atom 229 | | LEFT_PARENTHESIS ldlf_formula RIGHT_PARENTHESIS 230 | ?ldlf_atom: ldlf_tt 231 | | ldlf_ff 232 | | ldlf_last 233 | | ldlf_end 234 | 235 | 236 | ldlf_tt: TT 237 | ldlf_ff: FF 238 | ldlf_last: LAST 239 | ldlf_end: END 240 | 241 | regular_expression: re_union 242 | 243 | ?re_union: re_sequence (UNION re_sequence)* 244 | ?re_sequence: re_star (SEQ re_star)* 245 | ?re_star: re_test STAR? 246 | ?re_test: ldlf_formula TEST 247 | | re_wrapped 248 | ?re_wrapped: re_propositional 249 | | LEFT_PARENTHESIS regular_expression RIGHT_PARENTHESIS 250 | re_propositional: propositional_formula 251 | 252 | 253 | LEFT_SQUARE_BRACKET: "[" 254 | RIGHT_SQUARE_BRACKET: "]" 255 | LEFT_ANGLE_BRACKET: "<" 256 | RIGHT_ANGLE_BRACKET: ">" 257 | UNION: "+" 258 | SEQ: ";" 259 | TEST: "?" 260 | STAR: "*" 261 | 262 | %ignore /\s+/ 263 | 264 | %import .pl.propositional_formula 265 | %import .pl.TRUE -> TRUE 266 | %import .pl.FALSE -> FALSE 267 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 268 | %import .pl.EQUIVALENCE -> EQUIVALENCE 269 | %import .pl.IMPLY -> IMPLY 270 | %import .pl.OR -> OR 271 | %import .pl.AND -> AND 272 | %import .pl.NOT -> NOT 273 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 274 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 275 | %import .ltl.LAST -> LAST 276 | %import .ltl.END -> END 277 | %import .ltl.TT -> TT 278 | %import .ltl.FF -> FF 279 | ``` 280 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {!../README.md!} 2 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `pylogics` is a Python package 4 | to parse and manipulate several 5 | logic formalisms, with a focus 6 | on [temporal logics](https://plato.stanford.edu/entries/logic-temporal/). 7 | 8 | > :warning: This library is in early development, and the 9 | > API might be broken frequently or may contain bugs :bug:. 10 | 11 | > :warning: Docs are not thorough and might be inaccurate. 12 | > Apologies. 13 | 14 | ## Quickstart 15 | 16 | For example, consider [Propositional Logic](https://iep.utm.edu/prop-log/). 17 | The following code allows you to parse a propositional logic formula 18 | $\phi = (a \wedge b) \vee (c \wedge d)$: 19 | ```python 20 | from pylogics.parsers import parse_pl 21 | formula = parse_pl("(a & b) | (c & d)") 22 | ``` 23 | The object referenced by `formula` is an instance 24 | of `Formula`. Each instance of `Formula` is associated with 25 | a certain logic formalism. You can read it 26 | by accessing the `logic` attribute: 27 | ``` 28 | formlua.logic 29 | ``` 30 | which returns an instance of the Enum class 31 | `pylogics.syntax.base.Logic`. 32 | 33 | We can evaluate the formula on a propositional interpretation. 34 | First, import the function `evaluate_pl`: 35 | ```python 36 | from pylogics.semantics.pl import evaluate_pl 37 | ``` 38 | 39 | `evaluate_pl` takes in input an instance of 40 | `Formula`, with `formula.logic` equal to `Logic.PL`, 41 | and a propositional interpretation $I$ in the form 42 | of a set of strings or a dictionary from strings to booleans. 43 | 44 | - when a set is given, each symbol in the set 45 | is considered true; if not, it is considered 46 | false. 47 | - when a dictionary is given, the value 48 | of the symbol in the model is determined by 49 | the boolean value in the dictionary associated 50 | to that key. If the key is not present, it is 51 | assumed to be false. 52 | 53 | For example, say we want to evaluate 54 | the formula over the model $\mathcal{I}_1 = \{a\}$. 55 | We have $\mathcal{I}_1 \not\models \phi$: 56 | ```python 57 | evaluate_pl(formula, {'a'}) # returns False 58 | ``` 59 | 60 | Now consider $\mathcal{I}_2 = \{a, b\}$. 61 | We have $\mathcal{I}_2 \models \phi$: 62 | ```python 63 | evaluate_pl(formula, {'a', 'b'}) # returns True 64 | ``` 65 | 66 | Alternatively, we could have written: 67 | ```python 68 | evaluate_pl(formula, {'a': True, 'b': True, 'c': False}) # returns True 69 | ``` 70 | The value for `d` is assumed to be false, since it is not in the 71 | dictionary. 72 | 73 | ## Other logics 74 | 75 | Currently, the package provides support for: 76 | 77 | - Linear Temporal Logic (on finite traces) 78 | ([De Giacomo and Vardi, 2013](https://www.cs.rice.edu/~vardi/papers/ijcai13.pdf), 79 | [Brafman et al., 2018](http://www.diag.uniroma1.it//~patrizi/docs/papers/BDP@AAAI18.pdf)) 80 | - Past Linear Temporal Logic (on finite traces) 81 | [(De Giacomo et al., 2020)](http://www.dis.uniroma1.it/~degiacom/papers/2020draft/ijcai2020ddfr.pdf) 82 | - Linear Dynamic Logic (on finite traces) 83 | ([De Giacomo and Vardi, 2013](https://www.cs.rice.edu/~vardi/papers/ijcai13.pdf), 84 | [Brafman et al., 2018](http://www.diag.uniroma1.it//~patrizi/docs/papers/BDP@AAAI18.pdf)) 85 | 86 | We consider the variants of these formalisms that also works for empty 87 | traces; hence (Brafman et al., 2018) is a better reference for the 88 | supported logics. More details will be provided in the next sections. 89 | -------------------------------------------------------------------------------- /docs/parsing.md: -------------------------------------------------------------------------------- 1 | One of the core features of the library is 2 | the support of parsing strings 3 | compliant to a certain grammar 4 | to handily build formulae. 5 | 6 | The parsing functions, one for each logic formalism, 7 | can be imported from `pylogics.parsers`, and their 8 | name is `parse_`, where `` is the 9 | identifier of the logic formalism. 10 | For example, the parsing for propositional logic (`pl`) 11 | is `parse_pl`, whereas the parsing function 12 | for Linear Temporal Logic (`ltl`) is `parse_ltl`. 13 | For a list of the supported logics and their identifier 14 | please look at [this page](supported_logics.md). 15 | 16 | The library uses [Lark](https://lark-parser.readthedocs.io/en/latest/) 17 | to generate the parser automatically. 18 | The grammar files are reported at [this page](grammars.md). 19 | 20 | The syntax for `LTL`, `PLTL` and `LDL` 21 | aims to be compliant with 22 | [this specification](https://marcofavorito.me/tl-grammars/v/7d9a17267fbf525d9a6a1beb92a46f05cf652db6/). 23 | 24 | 25 | ## Symbols 26 | 27 | A symbol is determined by the following regular expression: 28 | 29 | ``` 30 | SYMBOL: [a-z][a-z0-9_-]*[a-z0-9_]*|"\w+" 31 | ``` 32 | 33 | That is: 34 | 35 | - if between quotes `""`, a symbol can be any 36 | non-empty sequence of word characters: `[a-zA-Z0-9_-]+` 37 | - if not, a symbol must: 38 | 39 | - have only have lower case characters 40 | - have at least one character and start with a non-digit character. 41 | 42 | 43 | ## Propositional Logic 44 | 45 | Informally, the supported grammar for propositional logic is: 46 | ``` 47 | pl_formula: pl_formula <-> pl_formula // equivalence 48 | | pl_formula -> pl_formula // implication 49 | | pl_formula || pl_formula // disjunction 50 | | pl_formula && pl_formula // conjunction 51 | | !pl_formula // negation 52 | | ( pl_formula ) // brackets 53 | | true // boolean propositional constant 54 | | false // boolean propositional constant 55 | | SYMBOL // prop. atom 56 | ``` 57 | 58 | Some examples: 59 | ```python 60 | from pylogics.parsers import parse_pl 61 | parse_pl("a") 62 | parse_pl("b") 63 | parse_pl("a & b") 64 | parse_pl("a | b") 65 | parse_pl("a >> b") 66 | parse_pl("a <-> b") 67 | parse_pl("a <-> a") # returns a 68 | parse_pl("!(a)") 69 | parse_pl("true | false") # returns true 70 | parse_pl("true & false") # returns false 71 | ``` 72 | 73 | ## Linear Temporal Logic 74 | 75 | Informally, the supported grammar for linear temporal logic is: 76 | ``` 77 | ltl_formula: ltl_formula <-> ltl_formula // equivalence 78 | | ltl_formula -> ltl_formula // implication 79 | | ltl_formula || ltl_formula // disjunction 80 | | ltl_formula && ltl_formula // conjunction 81 | | !ltl_formula // negation 82 | | ( ltl_formula ) // brackets 83 | | ltl_formula U ltl_formula // until 84 | | ltl_formula R ltl_formula // release 85 | | ltl_formula W ltl_formula // weak until 86 | | ltl_formula M ltl_formula // strong release 87 | | F ltl_formula // eventually 88 | | G ltl_formula // always 89 | | X[!] ltl_formula // next 90 | | X ltl_formula // weak next 91 | | true // boolean propositional constant 92 | | false // boolean propositional constant 93 | | tt // boolean logical constant 94 | | ff // boolean logical constant 95 | | SYMBOL // propositional atom 96 | ``` 97 | 98 | Some examples: 99 | ```python 100 | from pylogics.parsers import parse_ltl 101 | parse_ltl("tt") 102 | parse_ltl("ff") 103 | parse_ltl("true") 104 | parse_ltl("false") 105 | parse_ltl("a") 106 | parse_ltl("b") 107 | parse_ltl("X(a)") 108 | parse_ltl("X[!](b)") 109 | parse_ltl("F(a)") 110 | parse_ltl("G(b)") 111 | parse_ltl("G(a -> b)") 112 | parse_ltl("a U b") 113 | parse_ltl("a R b") 114 | parse_ltl("a W b") 115 | parse_ltl("a M b") 116 | ``` 117 | 118 | ## Past Linear Temporal Logic 119 | 120 | Informally, the supported grammar for past linear temporal logic is: 121 | ``` 122 | pltl_formula: pltl_formula <-> pltl_formula // equivalence 123 | | pltl_formula -> pltl_formula // implication 124 | | pltl_formula || pltl_formula // disjunction 125 | | pltl_formula && pltl_formula // conjunction 126 | | !pltl_formula // negation 127 | | ( pltl_formula ) // brackets 128 | | pltl_formula S pltl_formula // since 129 | | H pltl_formula // historically 130 | | O pltl_formula // once 131 | | Y pltl_formula // before 132 | | true // boolean propositional constant 133 | | false // boolean propositional constant 134 | | tt // boolean logical constant 135 | | ff // boolean logical constant 136 | | SYMBOL // propositional atom 137 | ``` 138 | 139 | Some examples: 140 | ```python 141 | from pylogics.parsers import parse_pltl 142 | parse_pltl("tt") 143 | parse_pltl("ff") 144 | parse_pltl("true") 145 | parse_pltl("false") 146 | parse_pltl("a") 147 | parse_pltl("b") 148 | parse_pltl("Y(a)") 149 | parse_pltl("O(b)") 150 | parse_pltl("H(a)") 151 | parse_pltl("a S b") 152 | ``` 153 | 154 | ## Linear Dynamic Logic 155 | 156 | Informally, the supported grammar for linear dynamic logic is 157 | (note; it is doubly-inductive): 158 | ``` 159 | ldl_formula: ldl_formula <-> ldl_formula // equivalence 160 | | ldl_formula -> ldl_formula // implication 161 | | ldl_formula || ldl_formula // disjunction 162 | | ldl_formula && ldl_formula // conjunction 163 | | !ldl_formula // negation 164 | | ( ldl_formula ) // brackets 165 | | ldl_formula // diamond formula 166 | | [regex]ldl_formula // box formula 167 | | tt // boolean constant 168 | | ff // boolean constant 169 | 170 | regex : regex + regex // union 171 | | regex ; regex // sequence 172 | | ?regex // test 173 | | regex* // star 174 | | pl_formula // prop. formula (see above) 175 | ``` 176 | 177 | Note: the question mark in the test regular expression 178 | is on the left, not on the right. This is done 179 | to avoid parse conflicts in the parser generation. 180 | 181 | Some examples: 182 | ```python 183 | from pylogics.parsers import parse_ldl 184 | parse_ldl("tt") 185 | parse_ldl("ff") 186 | parse_ldl("tt") 187 | parse_ldl("[a & b]ff") 188 | parse_ldl("tt") 189 | parse_ldl("tt") 190 | parse_ldl("<(a ; b)*>tt") 191 | parse_ldl("tt") # Next a 192 | parse_ldl("<(tt?;true)*>(tt)") # (a Until b) in LDLf 193 | ``` 194 | -------------------------------------------------------------------------------- /docs/references.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | 1. G. De Giacomo and M. Vardi. Linear temporal logic and linear dynamic logic on finite traces. In IJCAI, 2013. 4 | 2. R. Brafman, G. De Giacomo, and F. Patrizi. LTLf/LDLf non-markovian rewards. In AAAI, 2018. 5 | 3. De Giacomo, G., Di Stasio, A., Fuggitti, F., & Rubin, S. (2020). Pure-Past Linear Temporal and Dynamic Logic on Finite Traces. In IJCAI, 2020. 6 | -------------------------------------------------------------------------------- /docs/supported_logics.md: -------------------------------------------------------------------------------- 1 | Each logic formalism is associated 2 | to a string identifier, to make it 3 | easier for the user to navigate the APIs. 4 | 5 | Follows the table with the supported features 6 | for each formal language: 7 | 8 | | Logics | Identifier | Parsing | Syntax | Semantics | 9 | |------------------------------------------|:-----------:|:------------------:|:------------------:|:------------------:| 10 | | Propositional Logic | `pl` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 11 | | Linear Temporal Logic (fin. traces) | `ltl` | :heavy_check_mark: | :heavy_check_mark: | :x: | 12 | | Past Linear Temporal Logic (fin. traces) | `pltl` | :heavy_check_mark: | :heavy_check_mark: | :x: | 13 | | Linear Dynamic Logic (fin. traces) | `ldl` | :heavy_check_mark: | :heavy_check_mark: | :x: | 14 | | Past Linear Dynamic Logic (fin. traces) | `pldl` | :x: | :x: | :x: | 15 | | First-order Logic | `fol` | :x: | :x: | :x: | 16 | | Monadic Second-order Logic | `mso` | :x: | :x: | :x: | 17 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | The library can also be used through 2 | the syntax APIs. 3 | 4 | Each class needed to build the syntax tree 5 | of a formula of a certain logics 6 | is in `pylogics.syntax.`, 7 | where `` is the logic formalism 8 | identifier. 9 | See [this page](supported_logics.md) 10 | for information about the supported 11 | formalisms. 12 | 13 | The basic boolean connectives are defined 14 | in `pylogics.syntax.base`. These are: 15 | 16 | - `And` 17 | - `Or` 18 | - `Not` 19 | - `Implies` 20 | - `Equivalence` 21 | 22 | The binary operators take 23 | in input a sequence of arguments 24 | instances of a subclass of `Formula`. 25 | Note that as a precondition the operands. 26 | must belong to the same logic formalism 27 | 28 | E.g. to build $a & b$, one can do: 29 | ```python 30 | from pylogics.syntax.pl import Atomic 31 | a = Atomic("a") 32 | b = Atomic("b") 33 | formula = a & b 34 | ``` 35 | 36 | Now `formula` is an instance of `pylogics.syntax.base.And`. 37 | For the other operators (except equivalence): 38 | ``` 39 | a | b 40 | !a 41 | a >> b 42 | ``` 43 | 44 | For `true` and `false`, you can use 45 | `TrueFormula` and `FalseFormula`, 46 | respectively: 47 | 48 | ```python 49 | from pylogics.syntax.base import Logic, TrueFormula, FalseFormula 50 | true = TrueFormula(logic=Logic.PL) 51 | false = TrueFormula(logic=Logic.PL) 52 | ``` 53 | 54 | ## Linear Temporal Logic 55 | 56 | For LTL, you can use the following classes, 57 | defined in `pylogics.syntax.ltl`: 58 | 59 | - `Atom`, an LTL atom 60 | - `Next` (unary operator) 61 | - `WeakNext` (unary operator) 62 | - `Until` (binary operator) 63 | - `Release` (binary operator) 64 | - `WeakUntil` (binary operator) 65 | - `StrongRelease` (binary operator) 66 | - `Eventually` (unary operator) 67 | - `Always` (unary operator) 68 | 69 | To combine the above using boolean connectives, 70 | you can use the classes in `pylogics.syntax.base` 71 | described above. 72 | 73 | ## Past Linear Temporal Logic 74 | 75 | For PLTL, you can use the following classes, 76 | defined in `pylogics.syntax.pltl`: 77 | 78 | - `Atom`, a PLTL atom 79 | - `Before` (unary operator) 80 | - `Since` (binary operator) 81 | - `Once` (unary operator) 82 | - `Historically` (unary operator) 83 | 84 | To combine the above using boolean connectives, 85 | you can use the classes in `pylogics.syntax.base` 86 | described above. 87 | 88 | 89 | ## Linear Dynamic Temporal Logic 90 | 91 | For LDL, you can use the following classes, 92 | defined in `pylogics.syntax.ldl`: 93 | 94 | - `TrueFormula(logic=Logic.LDL)`, the boolean positive constant `tt` 95 | - `FalseFormula(logic=Logic.LDL)`, the boolean negative constant `ff` 96 | - `Diamond(regex, ldlf_formula)` 97 | - `Box(regex, ldlf_formula)` 98 | 99 | To combine the above using boolean connectives, 100 | you can use the classes in `pylogics.syntax.base` 101 | described above. 102 | 103 | To build regular expressions: 104 | 105 | - `Prop(propositional)`, where `propositional` is a 106 | propositional formula (i.e. `propositional.logic` is `Logic.PL`) 107 | - `Union` (binary operator) 108 | - `Seq` (binary operator) 109 | - `Test(ldlf_formula)` (unary operator) 110 | - `Star(regex)` (unary operator) 111 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyLogics Docs 2 | site_url: "https://whitemech.github.io/pylogics/" 3 | repo_name: 'whitemech/pylogics' 4 | repo_url: "https://github.com/whitemech/pylogics" 5 | 6 | nav: 7 | - Home: index.md 8 | - Introduction: introduction.md 9 | - Supported Logics: supported_logics.md 10 | - Parsing: parsing.md 11 | - Grammars: grammars.md 12 | - Syntax: syntax.md 13 | - References: references.md 14 | 15 | plugins: 16 | - search 17 | 18 | theme: 19 | name: material 20 | 21 | markdown_extensions: 22 | - codehilite 23 | - pymdownx.arithmatex 24 | - pymdownx.highlight 25 | - pymdownx.superfences 26 | - pymdownx.emoji: 27 | emoji_index: !!python/name:materialx.emoji.twemoji 28 | emoji_generator: !!python/name:materialx.emoji.to_svg 29 | - markdown_include.include: 30 | base_path: docs 31 | 32 | extra_javascript: 33 | - 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-MML-AM_CHTML' 34 | -------------------------------------------------------------------------------- /pylogics/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """A Python library for logic formalisms representation and manipulation.""" 25 | 26 | __version__ = "0.2.1" 27 | -------------------------------------------------------------------------------- /pylogics/deduction/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | -------------------------------------------------------------------------------- /pylogics/deduction/fol/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Deduction procedures related to first-order logic.""" 25 | from .nd import NaturalDeduction -------------------------------------------------------------------------------- /pylogics/deduction/fol/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Base classes for pylogics deduction systems.""" 25 | from abc import ABC, abstractmethod 26 | from typing import Container, Optional 27 | from pylogics.syntax.base import Logic 28 | 29 | class AbstractDeductionSystem(ABC): 30 | """Base class for all the deduction systems.""" 31 | 32 | ALLOWED_LOGICS: Optional[Container[Logic]] = None 33 | FORBIDDEN_LOGICS: Optional[Container[Logic]] = None 34 | 35 | @abstractmethod 36 | def Proof(proof) -> bool: 37 | """Build a proof according to the deduction system.""" 38 | 39 | @abstractmethod 40 | def check(proof) -> bool: 41 | """Check a given proof according to the deduction system rules.""" 42 | -------------------------------------------------------------------------------- /pylogics/deduction/fol/nd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for natural deduction systems.""" 25 | 26 | from enum import Enum 27 | 28 | from pylogics.syntax.base import Logic, Formula, FalseFormula, And, Or, Not, Implies 29 | from pylogics.exceptions import PylogicsError 30 | 31 | from pylogics.syntax.fol import Term, Variable, Constant, Function 32 | from pylogics.syntax.fol import Predicate, ForAll, Exists 33 | from pylogics.deduction.fol.base import AbstractDeductionSystem 34 | 35 | class NaturalDeductionRule(Enum): 36 | """Enumeration of natural deduction rules.""" 37 | 38 | and_e1 = "and_e1" 39 | and_e2 = "and_e2" 40 | and_i = "and_i" 41 | assumption = "assumption" 42 | bot_e = "bot_e" 43 | copy = "copy" 44 | dneg_e = "dneg_e" 45 | dneg_i = "dneg_i" 46 | exists_e = "exists_e" 47 | exists_i = "exists_i" 48 | forall_e = "forall_e" 49 | forall_i = "forall_i" 50 | impl_e = "impl_e" 51 | impl_i = "impl_i" 52 | MT = "MT" 53 | neg_e = "neg_e" 54 | neg_i = "neg_i" 55 | or_e = "or_e" 56 | or_i1 = "or_i1" 57 | or_i2 = "or_i2" 58 | premise = "premise" 59 | 60 | class NaturalDeductionProof(list): 61 | def __init__(self, proof: list): 62 | super().__init__() 63 | while proof: 64 | row, content, *proof = proof 65 | if isinstance(content, Formula) or isinstance(content, Term): 66 | justification, *proof = proof 67 | self.append((row, content, justification)) 68 | elif isinstance(content, list): 69 | if isinstance(content[0], Term) and isinstance(content[1], Formula): 70 | content = [content[0]] + [NaturalDeductionRule.assumption, row] + content[1:] 71 | self.append((row, NaturalDeductionProof([row] + content), NaturalDeductionRule.assumption)) 72 | 73 | class NaturalDeduction(AbstractDeductionSystem): 74 | """Natural Deduction System.""" 75 | 76 | ALLOWED_LOGICS = {Logic.PL, Logic.FOL} 77 | 78 | rule = NaturalDeductionRule 79 | 80 | def __init__(self): 81 | self.check_justification = { 82 | self.rule.and_e1:self._check_justification_and_e1, 83 | self.rule.and_e2:self._check_justification_and_e2, 84 | self.rule.and_i:self._check_justification_and_i, 85 | self.rule.assumption:self._check_justification_assumption, 86 | self.rule.bot_e:self._check_justification_bot_e, 87 | self.rule.copy:self._check_justification_copy, 88 | self.rule.dneg_e:self._check_justification_dneg_e, 89 | self.rule.dneg_i:self._check_justification_dneg_i, 90 | self.rule.exists_e:self._check_justification_exists_e, 91 | self.rule.exists_i:self._check_justification_exists_i, 92 | self.rule.forall_e:self._check_justification_forall_e, 93 | self.rule.forall_i:self._check_justification_forall_i, 94 | self.rule.impl_e:self._check_justification_impl_e, 95 | self.rule.impl_i:self._check_justification_impl_i, 96 | self.rule.MT:self._check_justification_MT, 97 | self.rule.neg_e:self._check_justification_neg_e, 98 | self.rule.neg_i:self._check_justification_neg_i, 99 | self.rule.or_e:self._check_justification_or_e, 100 | self.rule.or_i1:self._check_justification_or_i1, 101 | self.rule.or_i2:self._check_justification_or_i2, 102 | self.rule.premise:self._check_justification_premise, 103 | } 104 | 105 | def proof(self, proof: list): 106 | """Build a proof in the natural deduction expected format.""" 107 | return NaturalDeductionProof(proof) 108 | 109 | def check_proof(self, proof: NaturalDeductionProof, sound = None) -> bool: 110 | """Check a given proof according to natural deduction rules.""" 111 | # raise PylogicsError( 112 | # f"proof '{proof}' cannot be processed by {self.check.__name__}" # type: ignore 113 | # ) 114 | 115 | sound = sound if sound else {} 116 | 117 | for row, content, justification in proof: 118 | if isinstance(content, Formula): 119 | rule = justification[0] 120 | args = [sound[i] for i in justification[1:] if i in sound] 121 | if rule not in self.check_justification: 122 | return False 123 | if self.check_justification[rule](content, *args) == False: 124 | return False 125 | elif isinstance(content, list): 126 | if isinstance(content[0][1], Term) and self._find_term(content[0][1], *sound.values()): 127 | return False # Term occurs outside its assumption 128 | if self.check_proof(content, {i:sound[i] for i in sound}) == False: 129 | return False 130 | elif isinstance(content, Term): 131 | continue 132 | else: 133 | return False 134 | sound[row] = content 135 | return True 136 | 137 | 138 | def _check_justification_and_e1(self, formula, *args): 139 | """Check if the deduction is valid according to and-elimination (1) rule.""" 140 | return str(formula) == str(args[0].operands[0]) 141 | 142 | def _check_justification_and_e2(self, formula, *args): 143 | """Check if the deduction is valid according to and-elimination (2) rule.""" 144 | return str(formula) == str(args[0].operands[1]) 145 | 146 | def _check_justification_and_i(self, formula, *args): 147 | """Check if the deduction is valid according to and-introduction rule.""" 148 | return str(formula) == str(args[0] & args[1]) 149 | 150 | def _check_justification_assumption(self, formula, *args): 151 | """Check if the deduction is valid according to assumption rule""" 152 | return True 153 | 154 | def _check_justification_bot_e(self, formula, *args): 155 | """Check if the deduction is valid according to absurd-elimination rule""" 156 | return args[0] == FalseFormula() 157 | 158 | def _check_justification_copy(self, formula, *args): 159 | """Check if the deduction is valid according to copy rule""" 160 | return str(formula) == str(args[0]) 161 | 162 | def _check_justification_dneg_e(self, formula, *args): 163 | """Check if the deduction is valid according to double negation-elimination rule""" 164 | return str(~~formula) == str(args[0]) 165 | 166 | def _check_justification_dneg_i(self, formula, *args): 167 | """Check if the deduction is valid according to double negation-introduction rule""" 168 | return str(formula) == str(~~args[0]) 169 | 170 | def _check_justification_impl_e(self, formula, *args): 171 | """Check if the deduction is valid according to implies-elimination rule""" 172 | return str(args[0] >> formula) == str(args[1]) 173 | 174 | def _check_justification_impl_i(self, formula, *args): 175 | """Check if the deduction is valid according to implies-introduction rule""" 176 | phi = args[0][ 0][1] 177 | psi = args[0][-1][1] 178 | return str(formula) == str(phi >> psi) 179 | 180 | def _check_justification_MT(self, formula, *args): 181 | """Check if the deduction is valid according to modus tollens rule""" 182 | return str(formula.argument >> args[1].argument) == str(args[0]) 183 | 184 | def _check_justification_neg_e(self, formula, *args): 185 | """Check if the deduction is valid according to negation-elimination rule""" 186 | return str(~args[0]) == str(args[1]) and formula == FalseFormula() 187 | 188 | def _check_justification_neg_i(self, formula, *args): 189 | """Check if the deduction is valid according to negation-introduction rule""" 190 | phi = args[0][ 0][1] 191 | psi = args[0][-1][1] 192 | return (psi == FalseFormula()) and (str(formula) == str(~phi)) 193 | 194 | def _check_justification_or_e(self, formula, *args): 195 | """Check if the deduction is valid according to or-elimination rule""" 196 | phi_or_psi = args[0] 197 | phi, chi_1 = args[1][0][1], args[1][-1][1] 198 | psi, chi_2 = args[2][0][1], args[2][-1][1] 199 | return (str(phi_or_psi) == str(phi | psi)) and (str(formula) == str(chi_1)) and (str(formula) == str(chi_2)) 200 | 201 | def _check_justification_or_i1(self, formula, *args): 202 | """Check if the deduction is valid according to or-introduction 1 rule""" 203 | return str(formula.operands[0]) == str(args[0]) 204 | 205 | def _check_justification_or_i2(self, formula, *args): 206 | """Check if the deduction is valid according to or-introduction 2 rule""" 207 | return str(formula.operands[1]) == str(args[0]) 208 | 209 | def _check_justification_premise(self, formula, *args): 210 | """Check if the deduction is valid according to premise rule""" 211 | return True 212 | 213 | 214 | def _find_term(self, t:Term, *args): 215 | for x in args: 216 | if t == x: 217 | return True 218 | if 'argument' in dir(x) and self._find_term(t, x.argument): 219 | return True 220 | if 'operands' in dir(x) and self._find_term(t, *(x.operands)): 221 | return True 222 | return False 223 | 224 | def _find_diff(self, x:Formula, y:Formula): 225 | diffset = set() 226 | if type(x) != type(y): 227 | diffset = diffset | {(x,y)} 228 | elif 'argument' in dir(x): 229 | diffset = diffset | self._find_diff(x.argument, y.argument) 230 | elif 'operands' in dir(x): 231 | if len(x.operands) != len(y.operands): 232 | diffset = diffset | {(x,y)} 233 | else: 234 | for a,b in zip(x.operands, y.operands): 235 | diffset = diffset | self._find_diff(a,b) 236 | return diffset 237 | 238 | def _replace(self, formula:Formula, x:Variable, a:Term): 239 | if formula is x: 240 | return a 241 | if isinstance(formula,Not): 242 | return type(formula)(self._replace(formula.argument, x, a)) 243 | if isinstance(formula,(Or,And,Implies)): 244 | return type(formula)(*[self._replace(o, x, a) for o in formula.operands]) 245 | if isinstance(formula,(ForAll,Exists)): 246 | if formula.variable == x: 247 | return formula 248 | return type(formula)(formula.variable, [self._replace(o, x, a) for o in formula.operands]) 249 | if isinstance(formula,(Predicate,Function)): 250 | return type(formula)(formula.name, [self._replace(o, x, a) for o in formula.operands]) 251 | return formula 252 | 253 | def _check_justification_forall_i(self, formula, *args): 254 | """Check if the deduction is valid according to forall-introduction rule""" 255 | if not isinstance(formula, ForAll): 256 | return False 257 | a, phi_x_a = args[0][0][1], args[0][-1][1] 258 | if not isinstance(a, Term): 259 | return False 260 | x, phi = formula.variable, formula.formula 261 | return str(self._replace(phi,x,a)) == str(phi_x_a) 262 | 263 | def _check_justification_forall_e(self, formula, *args): 264 | """Check if the deduction is valid according to forall-elimination rule""" 265 | if not isinstance(args[0], ForAll): 266 | return False 267 | x, phi = args[0].variable, args[0].formula 268 | a = [n for m,n in self._find_diff(phi, formula) if m == x] 269 | if len(a) != 1: 270 | return False 271 | a = a[0] 272 | phi_x_a = self._replace(phi, x, a) 273 | return str(phi_x_a) == str(formula) 274 | 275 | def _check_justification_exists_i(self, formula, *args): 276 | """Check if the deduction is valid according to exists-introduction rule""" 277 | if not isinstance(formula, Exists): 278 | return False 279 | x, phi = formula.variable, formula.formula 280 | a = [n for m,n in self._find_diff(phi, args[0]) if m == x] 281 | if len(a) > 1: 282 | return False 283 | a = a[0] if a else x 284 | phi_x_a = self._replace(phi, x, a) 285 | return str(phi_x_a) == str(args[0]) 286 | 287 | def _check_justification_exists_e(self, formula, *args): 288 | """Check if the deduction is valid according to exists-elimination rule""" 289 | phi = args[0] 290 | if not isinstance(phi, Exists): 291 | return False 292 | a, phi_x_a = args[1][0][1], args[1][1][1] 293 | if not str(phi_x_a) == str(self._replace(phi.formula, phi.variable, a)): 294 | return False 295 | chi = args[1][-1][1] 296 | if self._find_term(a, chi): 297 | return False 298 | return str(formula) == str(chi) 299 | 300 | @staticmethod 301 | def Proof(proof: list): 302 | """Build a proof according to the deduction system.""" 303 | nd = NaturalDeduction() 304 | return nd.proof(proof) 305 | 306 | @staticmethod 307 | def check(proof: NaturalDeductionProof): 308 | """Check a given proof according to natural deduction rules.""" 309 | nd = NaturalDeduction() 310 | return nd.check_proof(proof) -------------------------------------------------------------------------------- /pylogics/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | """Custom library exceptions.""" 24 | 25 | 26 | class PylogicsError(Exception): 27 | """Generic library error.""" 28 | 29 | 30 | class ParsingError(PylogicsError): 31 | """Parsing error.""" 32 | 33 | def __init__(self, msg: str = "parsing error"): 34 | """Initialize the exception.""" 35 | super().__init__(msg) 36 | -------------------------------------------------------------------------------- /pylogics/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """This subpackage contains helper modules and functions.""" 25 | -------------------------------------------------------------------------------- /pylogics/helpers/cache_hash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Base classes for pylogics logic formulas.""" 25 | from abc import ABCMeta 26 | from functools import wraps 27 | from typing import Any, Callable, cast 28 | 29 | 30 | def _cache_hash(fn) -> Callable[[Any], int]: 31 | """ 32 | Compute the (possibly memoized) hash. 33 | 34 | If the hash for this object has already 35 | been computed, return it. Otherwise, 36 | compute it and store for later calls. 37 | 38 | :param fn: the hashing function. 39 | :return: the new hashing function. 40 | """ 41 | 42 | @wraps(fn) 43 | def __hash__(self): 44 | if not hasattr(self, "__hash"): 45 | self.__hash = fn(self) 46 | return cast(int, self.__hash) 47 | 48 | return __hash__ 49 | 50 | 51 | def _getstate(fn): 52 | """ 53 | Get the state. 54 | 55 | We need to ignore the hash value because in case the object 56 | is serialized with e.g. Pickle, if the state is restored 57 | with another instance of the interpreter, the stored hash might 58 | be inconsistent with the PYTHONHASHSEED initialization of the 59 | new interpreter. 60 | """ 61 | 62 | @wraps(fn) 63 | def __getstate__(self): 64 | d = fn(self) 65 | d.pop("__hash") 66 | return d 67 | 68 | return __getstate__ 69 | 70 | 71 | def default_getstate(self): 72 | """Implement the default getstate.""" 73 | return self.__dict__ 74 | 75 | 76 | def default_setstate(self, state): 77 | """Implement the default getstate.""" 78 | self.__dict__ = state 79 | 80 | 81 | def _setstate(fn): 82 | """ 83 | Set the state. 84 | 85 | The hash value needs to be set to None 86 | as the state might be restored in another 87 | interpreter in which the old hash value 88 | might not be consistent anymore. 89 | """ 90 | 91 | @wraps(fn) 92 | def __setstate__(self, state): 93 | fn(self, state) 94 | if hasattr(self, "__hash"): 95 | delattr(self, "__hash") 96 | 97 | return __setstate__ 98 | 99 | 100 | class Hashable(ABCMeta): 101 | """ 102 | A metaclass that makes class instances to cache their hash. 103 | 104 | It sets: 105 | __hash__ 106 | __getstate__ 107 | __setstate__ 108 | 109 | """ 110 | 111 | def __new__(mcs, *args, **kwargs): 112 | """Create a new instance.""" 113 | class_ = super().__new__(mcs, *args, **kwargs) 114 | 115 | class_.__hash__ = _cache_hash(class_.__hash__) 116 | 117 | getstate_fn = ( 118 | class_.__getstate__ if hasattr(class_, "__getstate__") else default_getstate 119 | ) 120 | class_.__getstate__ = _getstate(getstate_fn) 121 | 122 | setstate_fn = ( 123 | class_.__setstate__ if hasattr(class_, "__setstate__") else default_setstate 124 | ) 125 | class_.__setstate__ = _setstate(setstate_fn) 126 | return class_ 127 | -------------------------------------------------------------------------------- /pylogics/helpers/misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Helper functions.""" 25 | 26 | 27 | import re 28 | from pathlib import Path 29 | from typing import AbstractSet, Any, Callable, Collection, Optional, Sequence, Type 30 | 31 | 32 | def _get_current_path() -> Path: 33 | """Get the path to the file where the function is called.""" 34 | import inspect 35 | import os 36 | 37 | return Path(os.path.dirname(inspect.getfile(inspect.currentframe()))) # type: ignore 38 | 39 | 40 | def enforce( 41 | condition: bool, message: str = "", exception_cls: Type[Exception] = AssertionError 42 | ): 43 | """User-defined assert.""" 44 | if not condition: 45 | raise exception_cls(message) 46 | 47 | 48 | def ensure(arg: Optional[Any], default: Any): 49 | """Ensure an object is not None, or return a default.""" 50 | return arg if arg is not None else default 51 | 52 | 53 | def ensure_set(arg: Optional[Collection], immutable: bool = True) -> AbstractSet: 54 | """ 55 | Ensure the argument is a set. 56 | 57 | :param arg: the set, or None. 58 | :param immutable: whether the collection should be immutable. 59 | :return: the same set, or an empty set if the arg was None. 60 | """ 61 | op = frozenset if immutable else set 62 | return op(arg) if arg is not None else op() 63 | 64 | 65 | def ensure_sequence(arg: Optional[Sequence], immutable: bool = True) -> Sequence: 66 | """ 67 | Ensure the argument is a sequence. 68 | 69 | :param arg: the list, or None. 70 | :param immutable: whether the collection should be immutable. 71 | :return: the same list, or an empty list if the arg was None. 72 | """ 73 | op: Callable = tuple if immutable else list 74 | return op(arg) if arg is not None else op() 75 | 76 | 77 | def safe_index(seq: Sequence, *args, **kwargs): 78 | """Find element, safe.""" 79 | try: 80 | return seq.index(*args, **kwargs) 81 | except ValueError: 82 | return None 83 | 84 | 85 | def safe_get(seq: Sequence, index: int, default=None): 86 | """Get element at index, safe.""" 87 | return seq[index] if index < len(seq) else default 88 | 89 | 90 | def find(seq: Sequence, condition: Callable[[Any], bool]) -> int: 91 | """ 92 | Find the index of the first element that safisfies a condition. 93 | 94 | :param seq: the sequence. 95 | :param condition: the condition. 96 | :return: the index, or -1 if no element satisfies the condition. 97 | """ 98 | return next((i for i, x in enumerate(seq) if condition(x)), -1) 99 | 100 | 101 | class RegexConstrainedString(str): 102 | """ 103 | A string that is constrained by a regex. 104 | 105 | The default behaviour is to match anything. 106 | Subclass this class and change the 'REGEX' class 107 | attribute to implement a different behaviour. 108 | """ 109 | 110 | REGEX = re.compile(".*", flags=re.DOTALL) 111 | 112 | def __new__(cls, value, *args, **kwargs): 113 | """Instantiate a new object.""" 114 | if type(value) is cls: 115 | return value 116 | else: 117 | inst = super(RegexConstrainedString, cls).__new__(cls, value) 118 | return inst 119 | 120 | def __init__(self, *_, **__): 121 | """Initialize a regex constrained string.""" 122 | super().__init__() 123 | if not self.REGEX.fullmatch(self): 124 | self._handle_no_match() 125 | 126 | def _handle_no_match(self): 127 | raise ValueError( 128 | "Value '{data}' does not match the regular expression {regex}".format( 129 | data=self, regex=self.REGEX 130 | ) 131 | ) 132 | -------------------------------------------------------------------------------- /pylogics/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """This package contains the parsers for the supported logics.""" 25 | 26 | from .ldl import parse_ldl # noqa: ignore 27 | from .ltl import parse_ltl # noqa: ignore 28 | from .pl import parse_pl # noqa: ignore 29 | from .pltl import parse_pltl # noqa: ignore 30 | -------------------------------------------------------------------------------- /pylogics/parsers/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Base module for logic parsers.""" 25 | import inspect 26 | import os 27 | from abc import ABC 28 | from pathlib import Path 29 | from typing import Type 30 | 31 | from lark import Lark, Transformer 32 | 33 | from pylogics.exceptions import ParsingError 34 | from pylogics.syntax.base import Formula 35 | 36 | CURRENT_DIR = Path(os.path.dirname(inspect.getfile(inspect.currentframe()))).absolute() # type: ignore 37 | 38 | 39 | class AbstractParser(ABC): 40 | """ 41 | Abstract parser. 42 | 43 | Subclasses of this class need to specify: 44 | - transformer_cls: the class of the Lark transformer to use 45 | - lark_path: path to the Lark file that specifies the grammar. 46 | """ 47 | 48 | transformer_cls: Type[Transformer] 49 | lark_path: str 50 | 51 | def __init__(self): 52 | """Initialize the parser.""" 53 | self._transformer = self.transformer_cls() 54 | self._full_lark_path = CURRENT_DIR / self.lark_path 55 | self._parser = self._load_parser() 56 | 57 | def _load_parser(self) -> Lark: 58 | """Load the parser.""" 59 | return Lark( 60 | self._full_lark_path.read_text(), 61 | parser="lalr", 62 | import_paths=[str(CURRENT_DIR)], 63 | ) 64 | 65 | def __call__(self, text: str) -> Formula: 66 | """ 67 | Call the parser. 68 | 69 | :param text: the string to parse. 70 | :return: the parsed formula. 71 | """ 72 | tree = self._parser.parse(text) 73 | formula = self._transformer.transform(tree) 74 | return formula 75 | 76 | 77 | class AbstractTransformer(Transformer, ABC): 78 | """Abstract Lark transformer.""" 79 | 80 | @classmethod 81 | def _raise_parsing_error(cls, tag_name, args): 82 | """Raise a parsing error.""" 83 | raise ParsingError(f"error while parsing a '{tag_name}' with tokens: {args}") 84 | 85 | @classmethod 86 | def _starred_binaryop(cls, args, formula_type, func_name): 87 | """ 88 | Process a binary operator with repetitions. 89 | 90 | This parses rules of the form: rule -> a (OP b)* 91 | 92 | :param args: The parsing Tree. 93 | :param formula_type: Constructor of the OP class. It must accept 94 | a list of arguments. 95 | :return: a Formula. 96 | """ 97 | if len(args) == 1: 98 | return args[0] 99 | if (len(args) - 1) % 2 == 0: 100 | subformulas = args[::2] 101 | return formula_type(*subformulas) 102 | cls._raise_parsing_error(func_name, args) 103 | 104 | @classmethod 105 | def _process_unaryop(cls, args, formula_type): 106 | """ 107 | Process a unary operator. 108 | 109 | This parses rules of the form: rule -> a (OP b)* 110 | 111 | :param args: The parsing Tree. 112 | :param formula_type: Constructor of the OP class. It must accept 113 | a list of arguments. 114 | :return: a Formula. 115 | """ 116 | if len(args) == 1: 117 | return args[0] 118 | f = args[-1] 119 | for _ in args[:-1]: 120 | f = formula_type(f) 121 | return f 122 | -------------------------------------------------------------------------------- /pylogics/parsers/ldl.lark: -------------------------------------------------------------------------------- 1 | start: ldlf_formula 2 | 3 | ?ldlf_formula: ldlf_equivalence 4 | ?ldlf_equivalence: ldlf_implication (EQUIVALENCE ldlf_implication)* 5 | ?ldlf_implication: ldlf_or (IMPLY ldlf_or)* 6 | ?ldlf_or: ldlf_and (OR ldlf_and)* 7 | ?ldlf_and: ldlf_unaryop (AND ldlf_unaryop)* 8 | 9 | ?ldlf_unaryop: ldlf_box 10 | | ldlf_diamond 11 | | ldlf_not 12 | | ldlf_wrapped 13 | ?ldlf_box: LEFT_SQUARE_BRACKET regular_expression RIGHT_SQUARE_BRACKET ldlf_unaryop 14 | ?ldlf_diamond: LEFT_ANGLE_BRACKET regular_expression RIGHT_ANGLE_BRACKET ldlf_unaryop 15 | ?ldlf_not: NOT ldlf_unaryop 16 | ?ldlf_wrapped: ldlf_atom 17 | | LEFT_PARENTHESIS ldlf_formula RIGHT_PARENTHESIS 18 | ?ldlf_atom: ldlf_tt 19 | | ldlf_ff 20 | | ldlf_last 21 | | ldlf_end 22 | 23 | 24 | ldlf_tt: TT 25 | ldlf_ff: FF 26 | ldlf_last: LAST 27 | ldlf_end: END 28 | 29 | regular_expression: re_union 30 | 31 | ?re_union: re_sequence (UNION re_sequence)* 32 | ?re_sequence: re_star (SEQ re_star)* 33 | ?re_star: re_test STAR? 34 | ?re_test: ldlf_formula TEST 35 | | re_wrapped 36 | ?re_wrapped: re_propositional 37 | | LEFT_PARENTHESIS regular_expression RIGHT_PARENTHESIS 38 | re_propositional: propositional_formula 39 | 40 | 41 | LEFT_SQUARE_BRACKET: "[" 42 | RIGHT_SQUARE_BRACKET: "]" 43 | LEFT_ANGLE_BRACKET: "<" 44 | RIGHT_ANGLE_BRACKET: ">" 45 | UNION: "+" 46 | SEQ: ";" 47 | TEST: "?" 48 | STAR: "*" 49 | 50 | %ignore /\s+/ 51 | 52 | %import .pl.propositional_formula 53 | %import .pl.TRUE -> TRUE 54 | %import .pl.FALSE -> FALSE 55 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 56 | %import .pl.EQUIVALENCE -> EQUIVALENCE 57 | %import .pl.IMPLY -> IMPLY 58 | %import .pl.OR -> OR 59 | %import .pl.AND -> AND 60 | %import .pl.NOT -> NOT 61 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 62 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 63 | %import .ltl.LAST -> LAST 64 | %import .ltl.END -> END 65 | %import .ltl.TT -> TT 66 | %import .ltl.FF -> FF 67 | -------------------------------------------------------------------------------- /pylogics/parsers/ldl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Parser for linear temporal logic.""" 25 | 26 | from pylogics.exceptions import ParsingError 27 | from pylogics.parsers.base import AbstractParser, AbstractTransformer 28 | from pylogics.parsers.pl import _PLTransformer 29 | from pylogics.syntax.base import ( 30 | And, 31 | Equivalence, 32 | FalseFormula, 33 | Formula, 34 | Implies, 35 | Logic, 36 | Not, 37 | Or, 38 | TrueFormula, 39 | ) 40 | from pylogics.syntax.ldl import Box, Diamond, End, Last, Prop, Seq, Star, Test, Union 41 | from pylogics.syntax.pl import Atomic 42 | 43 | 44 | class _LDLTransformer(AbstractTransformer): 45 | """Transformer for LDL.""" 46 | 47 | _pl_transformer = _PLTransformer() 48 | _pl_imported = ("pl_atom", "propositional_formula") 49 | _prefix = "pl__" 50 | 51 | @classmethod 52 | def start(cls, args): 53 | assert len(args) == 1 54 | return args[0] 55 | 56 | @classmethod 57 | def ldlf_formula(cls, args): 58 | assert len(args) == 1 59 | return args[0] 60 | 61 | @classmethod 62 | def ldlf_equivalence(cls, args): 63 | return cls._starred_binaryop(args, Equivalence, cls.ldlf_equivalence.__name__) 64 | 65 | @classmethod 66 | def ldlf_implication(cls, args): 67 | return cls._starred_binaryop(args, Implies, cls.ldlf_implication.__name__) 68 | 69 | @classmethod 70 | def ldlf_or(cls, args): 71 | return cls._starred_binaryop(args, Or, cls.ldlf_or.__name__) 72 | 73 | @classmethod 74 | def ldlf_and(cls, args): 75 | return cls._starred_binaryop(args, And, cls.ldlf_and.__name__) 76 | 77 | @classmethod 78 | def ldlf_unaryop(cls, args): 79 | assert len(args) == 1 80 | return args[0] 81 | 82 | @classmethod 83 | def ldlf_box(cls, args): 84 | assert len(args) == 4 85 | _, regex, _, formula = args 86 | return Box(regex, formula) 87 | 88 | @classmethod 89 | def ldlf_diamond(cls, args): 90 | assert len(args) == 4 91 | _, regex, _, formula = args 92 | return Diamond(regex, formula) 93 | 94 | @classmethod 95 | def ldlf_not(cls, args): 96 | assert len(args) == 2 97 | return Not(args[1]) 98 | 99 | @classmethod 100 | def ldlf_wrapped(cls, args): 101 | if len(args) == 1: 102 | return args[0] 103 | if len(args) == 3: 104 | _, formula, _ = args 105 | return formula 106 | cls._raise_parsing_error(cls.ldlf_wrapped.__name__, args) 107 | 108 | @classmethod 109 | def ldlf_atom(cls, args): 110 | assert len(args) == 1 111 | formula = args[0] 112 | return formula 113 | 114 | @classmethod 115 | def ldlf_tt(cls, args): 116 | return TrueFormula(logic=Logic.LDL) 117 | 118 | @classmethod 119 | def ldlf_ff(cls, args): 120 | return FalseFormula(logic=Logic.LDL) 121 | 122 | @classmethod 123 | def ldlf_last(cls, args): 124 | return Last() 125 | 126 | @classmethod 127 | def ldlf_end(cls, args): 128 | return End() 129 | 130 | @classmethod 131 | def ldlf_prop_true(cls, args): 132 | return Diamond(Prop(TrueFormula()), TrueFormula(logic=Logic.LDL)) 133 | 134 | @classmethod 135 | def ldlf_prop_false(cls, args): 136 | return Diamond(Prop(FalseFormula()), TrueFormula(logic=Logic.LDL)) 137 | 138 | @classmethod 139 | def ldlf_prop_atom(cls, args): 140 | assert len(args) == 1 141 | token = args[0] 142 | symbol = str(token) 143 | return Diamond(Prop(Atomic(symbol)), TrueFormula(logic=Logic.LDL)) 144 | 145 | @classmethod 146 | def regular_expression(cls, args): 147 | assert len(args) == 1 148 | return args[0] 149 | 150 | @classmethod 151 | def re_union(cls, args): 152 | return cls._starred_binaryop(args, Union, cls.re_union.__name__) 153 | 154 | @classmethod 155 | def re_sequence(cls, args): 156 | return cls._starred_binaryop(args, Seq, cls.re_sequence.__name__) 157 | 158 | @classmethod 159 | def re_star(cls, args): 160 | if len(args) == 1: 161 | return args[0] 162 | if len(args) == 2: 163 | l, _ = args 164 | return Star(l) 165 | cls._raise_parsing_error(cls.re_star.__name__, args) 166 | 167 | @classmethod 168 | def re_test(cls, args): 169 | if len(args) == 1: 170 | return args[0] 171 | if len(args) == 2: 172 | formula, _ = args 173 | return Test(formula) 174 | cls._raise_parsing_error(cls.re_test.__name__, args) 175 | 176 | @classmethod 177 | def re_wrapped(cls, args): 178 | if len(args) == 1: 179 | return args[0] 180 | if len(args) == 3: 181 | _, formula, _ = args 182 | return formula 183 | cls._raise_parsing_error(cls.re_wrapped.__name__, args) 184 | 185 | @classmethod 186 | def re_propositional(cls, args): 187 | assert len(args) == 1 188 | return Prop(args[0]) 189 | 190 | def __getattr__(self, attr: str): 191 | """Also parse propositional logic.""" 192 | if attr.startswith(self._prefix): 193 | suffix = attr[len(self._prefix) :] # noqa 194 | return getattr(self._pl_transformer, suffix) 195 | if attr in self._pl_imported: 196 | return getattr(self._pl_transformer, attr) 197 | if attr.isupper(): 198 | raise AttributeError("Terminals should not be parsed") 199 | raise ParsingError(f"No transformation exists for rule: {attr}") 200 | 201 | 202 | class _LDLParser(AbstractParser): 203 | """Parser for linear temporal logic.""" 204 | 205 | transformer_cls = _LDLTransformer 206 | lark_path = "ldl.lark" 207 | 208 | 209 | __parser = _LDLParser() 210 | 211 | 212 | def parse_ldl(formula: str) -> Formula: 213 | """ 214 | Parse the linear dynamic logic formula. 215 | 216 | This is the main entrypoint for LDL parsing. 217 | 218 | :param formula: the string representation of the formula. 219 | :return: the parsed formula. 220 | """ 221 | return __parser(formula) 222 | -------------------------------------------------------------------------------- /pylogics/parsers/ltl.lark: -------------------------------------------------------------------------------- 1 | start: ltlf_formula 2 | 3 | ?ltlf_formula: ltlf_equivalence 4 | ?ltlf_equivalence: ltlf_implication (EQUIVALENCE ltlf_implication)* 5 | ?ltlf_implication: ltlf_or (IMPLY ltlf_or)* 6 | ?ltlf_or: ltlf_and (OR ltlf_and)* 7 | ?ltlf_and: ltlf_weak_until (AND ltlf_weak_until)* 8 | ?ltlf_weak_until: ltlf_until (WEAK_UNTIL ltlf_until)* 9 | ?ltlf_until: ltlf_release (UNTIL ltlf_release)* 10 | ?ltlf_release: ltlf_strong_release (RELEASE ltlf_strong_release)* 11 | ?ltlf_strong_release: ltlf_unaryop (STRONG_RELEASE ltlf_unaryop)* 12 | 13 | ?ltlf_unaryop: ltlf_always 14 | | ltlf_eventually 15 | | ltlf_next 16 | | ltlf_weak_next 17 | | ltlf_not 18 | | ltlf_wrapped 19 | 20 | ?ltlf_always: ALWAYS ltlf_unaryop 21 | ?ltlf_eventually: EVENTUALLY ltlf_unaryop 22 | ?ltlf_next: NEXT ltlf_unaryop 23 | ?ltlf_weak_next: WEAK_NEXT ltlf_unaryop 24 | ?ltlf_not: NOT ltlf_unaryop 25 | ?ltlf_wrapped: ltlf_atom 26 | | LEFT_PARENTHESIS ltlf_formula RIGHT_PARENTHESIS 27 | ?ltlf_atom: ltlf_symbol 28 | | ltlf_true 29 | | ltlf_false 30 | | ltlf_tt 31 | | ltlf_ff 32 | | ltlf_last 33 | 34 | ltlf_symbol: SYMBOL_NAME 35 | ltlf_true: prop_true 36 | ltlf_false: prop_false 37 | ltlf_tt: TT 38 | ltlf_ff: FF 39 | ltlf_last: LAST 40 | 41 | // Operators must not be part of a word 42 | UNTIL.2: /U(?=[ "\(])/ 43 | RELEASE.2: /R(?=[ "\(])/ 44 | ALWAYS.2: /G(?=[ "\(])/ 45 | EVENTUALLY.2: /F(?=[ "\(])/ 46 | NEXT.2: /X\[!\](?=[ "\(])/ 47 | WEAK_NEXT.2: /X(?=[ "\(])/ 48 | WEAK_UNTIL.2: /W(?=[ "\(])/ 49 | STRONG_RELEASE.2: /M(?=[ "\(])/ 50 | 51 | 52 | END.2: /end/ 53 | LAST.2: /last/ 54 | 55 | TT.2: /tt/ 56 | FF.2: /ff/ 57 | 58 | %ignore /\s+/ 59 | 60 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 61 | %import .pl.prop_true -> prop_true 62 | %import .pl.prop_false -> prop_false 63 | %import .pl.NOT -> NOT 64 | %import .pl.OR -> OR 65 | %import .pl.AND -> AND 66 | %import .pl.EQUIVALENCE -> EQUIVALENCE 67 | %import .pl.IMPLY -> IMPLY 68 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 69 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 70 | -------------------------------------------------------------------------------- /pylogics/parsers/ltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Parser for linear temporal logic.""" 25 | 26 | from pylogics.parsers.base import AbstractParser, AbstractTransformer 27 | from pylogics.syntax.base import ( 28 | And, 29 | Equivalence, 30 | Formula, 31 | Implies, 32 | Logic, 33 | Not, 34 | Or, 35 | make_boolean, 36 | ) 37 | from pylogics.syntax.ltl import ( 38 | Always, 39 | Atomic, 40 | Eventually, 41 | Last, 42 | Next, 43 | PropositionalFalse, 44 | PropositionalTrue, 45 | Release, 46 | StrongRelease, 47 | Until, 48 | WeakNext, 49 | WeakUntil, 50 | ) 51 | 52 | 53 | class _LTLTransformer(AbstractTransformer): 54 | @classmethod 55 | def start(cls, args): 56 | assert len(args) == 1 57 | return args[0] 58 | 59 | @classmethod 60 | def ltlf_formula(cls, args): 61 | assert len(args) == 1 62 | return args[0] 63 | 64 | @classmethod 65 | def ltlf_equivalence(cls, args): 66 | return cls._starred_binaryop(args, Equivalence, cls.ltlf_equivalence.__name__) 67 | 68 | @classmethod 69 | def ltlf_implication(cls, args): 70 | return cls._starred_binaryop(args, Implies, cls.ltlf_implication.__name__) 71 | 72 | @classmethod 73 | def ltlf_or(cls, args): 74 | return cls._starred_binaryop(args, Or, cls.ltlf_or.__name__) 75 | 76 | @classmethod 77 | def ltlf_and(cls, args): 78 | return cls._starred_binaryop(args, And, cls.ltlf_and.__name__) 79 | 80 | @classmethod 81 | def ltlf_until(cls, args): 82 | return cls._starred_binaryop(args, Until, cls.ltlf_until.__name__) 83 | 84 | @classmethod 85 | def ltlf_weak_until(cls, args): 86 | return cls._starred_binaryop(args, WeakUntil, cls.ltlf_weak_until.__name__) 87 | 88 | @classmethod 89 | def ltlf_release(cls, args): 90 | return cls._starred_binaryop(args, Release, cls.ltlf_release.__name__) 91 | 92 | @classmethod 93 | def ltlf_strong_release(cls, args): 94 | return cls._starred_binaryop( 95 | args, StrongRelease, cls.ltlf_strong_release.__name__ 96 | ) 97 | 98 | @classmethod 99 | def ltlf_always(cls, args): 100 | return cls._process_unaryop(args, Always) 101 | 102 | @classmethod 103 | def ltlf_eventually(cls, args): 104 | return cls._process_unaryop(args, Eventually) 105 | 106 | @classmethod 107 | def ltlf_next(cls, args): 108 | return cls._process_unaryop(args, Next) 109 | 110 | @classmethod 111 | def ltlf_weak_next(cls, args): 112 | return cls._process_unaryop(args, WeakNext) 113 | 114 | @classmethod 115 | def ltlf_not(cls, args): 116 | return cls._process_unaryop(args, Not) 117 | 118 | @classmethod 119 | def ltlf_wrapped(cls, args): 120 | if len(args) == 1: 121 | return args[0] 122 | if len(args) == 3: 123 | _, formula, _ = args 124 | return formula 125 | cls._raise_parsing_error(cls.ltlf_wrapped.__name__, args) 126 | 127 | @classmethod 128 | def ltlf_atom(cls, args): 129 | assert len(args) == 1 130 | return Atomic(args[0]) 131 | 132 | @classmethod 133 | def ltlf_true(cls, _args): 134 | return PropositionalTrue() 135 | 136 | @classmethod 137 | def ltlf_false(cls, _args): 138 | return PropositionalFalse() 139 | 140 | @classmethod 141 | def ltlf_tt(cls, _args): 142 | return make_boolean(True, logic=Logic.LTL) 143 | 144 | @classmethod 145 | def ltlf_ff(cls, _args): 146 | return make_boolean(False, logic=Logic.LTL) 147 | 148 | @classmethod 149 | def ltlf_last(cls, _args): 150 | return Last() 151 | 152 | @classmethod 153 | def ltlf_symbol(cls, args): 154 | assert len(args) == 1 155 | token = args[0] 156 | symbol = str(token) 157 | return Atomic(symbol) 158 | 159 | 160 | class _LTLParser(AbstractParser): 161 | """Parser for linear temporal logic.""" 162 | 163 | transformer_cls = _LTLTransformer 164 | lark_path = "ltl.lark" 165 | 166 | 167 | __parser = _LTLParser() 168 | 169 | 170 | def parse_ltl(formula: str) -> Formula: 171 | """ 172 | Parse the linear temporal logic formula. 173 | 174 | This is the main entrypoint for LTL parsing. 175 | 176 | :param formula: the string representation of the formula. 177 | :return: the parsed formula. 178 | """ 179 | return __parser(formula) 180 | -------------------------------------------------------------------------------- /pylogics/parsers/pl.lark: -------------------------------------------------------------------------------- 1 | start: propositional_formula 2 | 3 | ?propositional_formula: prop_equivalence 4 | ?prop_equivalence: prop_implication (EQUIVALENCE prop_implication)* 5 | ?prop_implication: prop_or (IMPLY prop_or)* 6 | ?prop_or: prop_and (OR prop_and)* 7 | ?prop_and: prop_not (AND prop_not)* 8 | ?prop_not: NOT* prop_wrapped 9 | ?prop_wrapped: prop_atom 10 | | LEFT_PARENTHESIS propositional_formula RIGHT_PARENTHESIS 11 | ?prop_atom: atom 12 | | prop_true 13 | | prop_false 14 | 15 | atom: SYMBOL_NAME 16 | prop_true: TRUE 17 | prop_false: FALSE 18 | 19 | LEFT_PARENTHESIS : "(" 20 | RIGHT_PARENTHESIS : ")" 21 | EQUIVALENCE : "<->" 22 | IMPLY : ">>"|"->" 23 | OR: "||"|"|" 24 | AND: "&&"|"&" 25 | NOT: "!"|"~" 26 | TRUE.2: /true/ 27 | FALSE.2: /false/ 28 | 29 | // Symbols cannot start with uppercase letters, because these are reserved. Moreover, any word between quotes is a symbol. 30 | // More in detail: 31 | // 1) either start with [a-z_], followed by at least one [a-zA-Z0-9_-], and by one [a-zA-Z0-9_] (i.e. hyphens only in between) 32 | // 2) or, start with [a-z_] and follows with any sequence of [a-zA-Z0-9_] (no hyphens) 33 | // 3) or, any sequence of ASCII printable characters (i.e. going from ' ' to '~'), except '"'. 34 | SYMBOL_NAME: FIRST_SYMBOL_CHAR _SYMBOL_1_BODY _SYMBOL_1_TAIL 35 | | FIRST_SYMBOL_CHAR _SYMBOL_2_BODY 36 | | DOUBLE_QUOTES _SYMBOL_3_BODY DOUBLE_QUOTES 37 | 38 | _SYMBOL_QUOTED: DOUBLE_QUOTES _SYMBOL_3_BODY DOUBLE_QUOTES 39 | _SYMBOL_1_BODY: /[a-zA-Z0-9_\-]+/ 40 | _SYMBOL_1_TAIL: /[a-zA-Z0-9_]/ 41 | _SYMBOL_2_BODY: /[a-zA-Z0-9_]*/ 42 | _SYMBOL_3_BODY: /[ -!#-~]+?/ 43 | 44 | DOUBLE_QUOTES: "\"" 45 | FIRST_SYMBOL_CHAR: /[a-z_]/ 46 | 47 | 48 | %ignore /\s+/ 49 | -------------------------------------------------------------------------------- /pylogics/parsers/pl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Parser for propositional logic.""" 25 | 26 | from pylogics.parsers.base import AbstractParser, AbstractTransformer 27 | from pylogics.syntax.base import ( 28 | And, 29 | Equivalence, 30 | Formula, 31 | Implies, 32 | Logic, 33 | Not, 34 | Or, 35 | make_boolean, 36 | ) 37 | from pylogics.syntax.pl import Atomic 38 | 39 | 40 | class _PLTransformer(AbstractTransformer): 41 | """Lark Transformer for propositional logic.""" 42 | 43 | @classmethod 44 | def start(cls, args): 45 | """Start.""" 46 | return args[0] 47 | 48 | @classmethod 49 | def propositional_formula(cls, args): 50 | """Parse the 'propositional_formula' tag.""" 51 | assert len(args) == 1 52 | return args[0] 53 | 54 | @classmethod 55 | def prop_equivalence(cls, args): 56 | """Parse the 'prop_equivalence' tag.""" 57 | return cls._starred_binaryop(args, Equivalence, cls.prop_equivalence.__name__) 58 | 59 | @classmethod 60 | def prop_implication(cls, args): 61 | return cls._starred_binaryop(args, Implies, cls.prop_implication.__name__) 62 | 63 | @classmethod 64 | def prop_or(cls, args): 65 | return cls._starred_binaryop(args, Or, cls.prop_or.__name__) 66 | 67 | @classmethod 68 | def prop_and(cls, args): 69 | return cls._starred_binaryop(args, And, cls.prop_and.__name__) 70 | 71 | @classmethod 72 | def prop_not(cls, args): 73 | return cls._process_unaryop(args, Not) 74 | 75 | @classmethod 76 | def prop_wrapped(cls, args): 77 | if len(args) == 1: 78 | return args[0] 79 | if len(args) == 3: 80 | _, f, _ = args 81 | return f 82 | cls._raise_parsing_error(cls.prop_wrapped.__name__, args) 83 | 84 | @classmethod 85 | def prop_atom(cls, args): 86 | assert len(args) == 1 87 | return Atomic(args[0]) 88 | 89 | @classmethod 90 | def prop_true(cls, args): 91 | assert len(args) == 1 92 | return make_boolean(True, logic=Logic.PL) 93 | 94 | @classmethod 95 | def prop_false(cls, args): 96 | assert len(args) == 1 97 | return make_boolean(False, logic=Logic.PL) 98 | 99 | @classmethod 100 | def atom(cls, args): 101 | assert len(args) == 1 102 | return Atomic(str(args[0])) 103 | 104 | 105 | class _PLParser(AbstractParser): 106 | """Parser for propositional logic.""" 107 | 108 | transformer_cls = _PLTransformer 109 | lark_path = "pl.lark" 110 | 111 | 112 | __parser = _PLParser() 113 | 114 | 115 | def parse_pl(formula: str) -> Formula: 116 | """ 117 | Parse the propositional formula. 118 | 119 | This is the main entrypoint for PL parsing. 120 | 121 | :param formula: the string representation of the formula. 122 | :return: the parsed formula. 123 | """ 124 | return __parser(formula) 125 | -------------------------------------------------------------------------------- /pylogics/parsers/pltl.lark: -------------------------------------------------------------------------------- 1 | start: pltlf_formula 2 | 3 | ?pltlf_formula: pltlf_equivalence 4 | ?pltlf_equivalence: pltlf_implication (EQUIVALENCE pltlf_implication)* 5 | ?pltlf_implication: pltlf_or (IMPLY pltlf_or)* 6 | ?pltlf_or: pltlf_and (OR pltlf_and)* 7 | ?pltlf_and: pltlf_since (AND pltlf_since)* 8 | ?pltlf_since: pltlf_unaryop (SINCE pltlf_unaryop)* 9 | 10 | ?pltlf_unaryop: pltlf_historically 11 | | pltlf_once 12 | | pltlf_before 13 | | pltlf_not 14 | | pltlf_wrapped 15 | 16 | ?pltlf_historically: HISTORICALLY pltlf_unaryop 17 | ?pltlf_once: ONCE pltlf_unaryop 18 | ?pltlf_before: BEFORE pltlf_unaryop 19 | ?pltlf_not: NOT pltlf_unaryop 20 | ?pltlf_wrapped: pltlf_atom 21 | | LEFT_PARENTHESIS pltlf_formula RIGHT_PARENTHESIS 22 | ?pltlf_atom: pltlf_symbol 23 | | pltlf_true 24 | | pltlf_false 25 | | pltlf_tt 26 | | pltlf_ff 27 | | pltlf_start 28 | 29 | pltlf_symbol: SYMBOL_NAME 30 | pltlf_true: prop_true 31 | pltlf_false: prop_false 32 | pltlf_tt: TT 33 | pltlf_ff: FF 34 | pltlf_start: START 35 | 36 | // Operators must not be part of a word 37 | SINCE.2: /S(?=[ "\(])/ 38 | HISTORICALLY.2: /H(?=[ "\(])/ 39 | ONCE.2: /O(?=[ "\(])/ 40 | BEFORE.2: /Y(?=[ "\(])/ 41 | FIRST.2: /first/ 42 | START.2: /start/ 43 | 44 | %ignore /\s+/ 45 | 46 | %import .pl.SYMBOL_NAME -> SYMBOL_NAME 47 | %import .pl.prop_true -> prop_true 48 | %import .pl.prop_false -> prop_false 49 | %import .pl.NOT -> NOT 50 | %import .pl.OR -> OR 51 | %import .pl.AND -> AND 52 | %import .pl.EQUIVALENCE -> EQUIVALENCE 53 | %import .pl.IMPLY -> IMPLY 54 | %import .pl.LEFT_PARENTHESIS -> LEFT_PARENTHESIS 55 | %import .pl.RIGHT_PARENTHESIS -> RIGHT_PARENTHESIS 56 | %import .ltl.TT -> TT 57 | %import .ltl.FF -> FF 58 | -------------------------------------------------------------------------------- /pylogics/parsers/pltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Parser for past linear temporal logic.""" 25 | 26 | from pylogics.parsers.base import AbstractParser, AbstractTransformer 27 | from pylogics.syntax.base import ( 28 | And, 29 | Equivalence, 30 | Formula, 31 | Implies, 32 | Logic, 33 | Not, 34 | Or, 35 | make_boolean, 36 | ) 37 | from pylogics.syntax.pltl import ( 38 | Atomic, 39 | Before, 40 | Historically, 41 | Once, 42 | PropositionalFalse, 43 | PropositionalTrue, 44 | Since, 45 | Start, 46 | ) 47 | 48 | 49 | class _PLTLTransformer(AbstractTransformer): 50 | @classmethod 51 | def start(cls, args): 52 | assert len(args) == 1 53 | return args[0] 54 | 55 | @classmethod 56 | def pltlf_formula(cls, args): 57 | assert len(args) == 1 58 | return args[0] 59 | 60 | @classmethod 61 | def pltlf_equivalence(cls, args): 62 | return cls._starred_binaryop(args, Equivalence, cls.pltlf_equivalence.__name__) 63 | 64 | @classmethod 65 | def pltlf_implication(cls, args): 66 | return cls._starred_binaryop(args, Implies, cls.pltlf_implication.__name__) 67 | 68 | @classmethod 69 | def pltlf_or(cls, args): 70 | return cls._starred_binaryop(args, Or, cls.pltlf_or.__name__) 71 | 72 | @classmethod 73 | def pltlf_and(cls, args): 74 | return cls._starred_binaryop(args, And, cls.pltlf_and.__name__) 75 | 76 | @classmethod 77 | def pltlf_since(cls, args): 78 | return cls._starred_binaryop(args, Since, cls.pltlf_since.__name__) 79 | 80 | @classmethod 81 | def pltlf_historically(cls, args): 82 | return cls._process_unaryop(args, Historically) 83 | 84 | @classmethod 85 | def pltlf_once(cls, args): 86 | return cls._process_unaryop(args, Once) 87 | 88 | @classmethod 89 | def pltlf_before(cls, args): 90 | return cls._process_unaryop(args, Before) 91 | 92 | @classmethod 93 | def pltlf_not(cls, args): 94 | return cls._process_unaryop(args, Not) 95 | 96 | @classmethod 97 | def pltlf_wrapped(cls, args): 98 | if len(args) == 1: 99 | return args[0] 100 | if len(args) == 3: 101 | _, formula, _ = args 102 | return formula 103 | cls._raise_parsing_error(cls.pltlf_wrapped.__name__, args) 104 | 105 | @classmethod 106 | def pltlf_atom(cls, args): 107 | assert len(args) == 1 108 | return Atomic(args[0]) 109 | 110 | @classmethod 111 | def pltlf_true(cls, _args): 112 | return PropositionalTrue() 113 | 114 | @classmethod 115 | def pltlf_false(cls, _args): 116 | return PropositionalFalse() 117 | 118 | @classmethod 119 | def pltlf_tt(cls, _args): 120 | return make_boolean(True, logic=Logic.PLTL) 121 | 122 | @classmethod 123 | def pltlf_ff(cls, _args): 124 | return make_boolean(False, logic=Logic.PLTL) 125 | 126 | @classmethod 127 | def pltlf_start(cls, _args): 128 | return Start() 129 | 130 | @classmethod 131 | def pltlf_symbol(cls, args): 132 | assert len(args) == 1 133 | token = args[0] 134 | symbol = str(token) 135 | return Atomic(symbol) 136 | 137 | 138 | class _PLTLParser(AbstractParser): 139 | """Parser for linear temporal logic.""" 140 | 141 | transformer_cls = _PLTLTransformer 142 | lark_path = "pltl.lark" 143 | 144 | 145 | __parser = _PLTLParser() 146 | 147 | 148 | def parse_pltl(formula: str) -> Formula: 149 | """ 150 | Parse the past linear temporal logic formula. 151 | 152 | This is the main entrypoint for PLTL parsing. 153 | 154 | :param formula: the string representation of the formula. 155 | :return: the parsed formula. 156 | """ 157 | return __parser(formula) 158 | -------------------------------------------------------------------------------- /pylogics/semantics/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes to represent the semantics of several logic formalisms.""" 25 | -------------------------------------------------------------------------------- /pylogics/semantics/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Semantics for propositional logic.""" 25 | import functools 26 | from typing import Callable 27 | 28 | from pylogics.exceptions import PylogicsError 29 | from pylogics.syntax.base import And, Equivalence, Formula, Implies, Not, Or 30 | 31 | 32 | @functools.singledispatch 33 | def base_semantics(formula: Formula, fn: Callable[..., bool], *args, **kwargs) -> bool: 34 | """Compute semantics of common boolean operators.""" 35 | raise PylogicsError(f"formula '{formula}' is not recognized.") 36 | 37 | 38 | @base_semantics.register(And) 39 | def and_semantics(formula: And, fn: Callable[..., bool], *args, **kwargs) -> bool: 40 | """Evaluate a conjunction formula.""" 41 | return all(fn(sub_formula, *args, **kwargs) for sub_formula in formula.operands) 42 | 43 | 44 | @base_semantics.register(Or) 45 | def or_semantics(formula: Or, fn: Callable[..., bool], *args, **kwargs) -> bool: 46 | """Evaluate a disjunction formula.""" 47 | return any(fn(sub_formula, *args, **kwargs) for sub_formula in formula.operands) 48 | 49 | 50 | @base_semantics.register(Not) 51 | def not_semantics(formula: Not, fn: Callable[..., bool], *args, **kwargs) -> bool: 52 | """Evaluate a negation.""" 53 | return not fn(formula.argument, *args, **kwargs) 54 | 55 | 56 | @base_semantics.register(Implies) 57 | def implies_semantics( 58 | formula: Implies, fn: Callable[..., bool], *args, **kwargs 59 | ) -> bool: 60 | """ 61 | Evaluate an implication formula. 62 | 63 | We use the fact that: 64 | 65 | a -> b -> c === a -> (b -> c) 66 | 67 | is equivalent to 68 | 69 | ~a | ~b | c 70 | 71 | """ 72 | 73 | def _visitor(): 74 | for index, sub_formula in enumerate(formula.operands): 75 | if index < len(formula.operands) - 1: 76 | yield not fn(sub_formula, *args, **kwargs) 77 | yield fn(sub_formula, *args, **kwargs) 78 | 79 | return any(_visitor()) 80 | 81 | 82 | @base_semantics.register(Equivalence) 83 | def equivalence_semantics( 84 | formula: Equivalence, fn: Callable[..., bool], *args, **kwargs 85 | ) -> bool: 86 | """Evaluate an equivalence formula.""" 87 | result = fn(formula.operands[0], *args, **kwargs) 88 | for sub_formula in formula.operands[1:]: 89 | result = fn(sub_formula, *args, **kwargs) == result 90 | return result 91 | -------------------------------------------------------------------------------- /pylogics/semantics/pl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Semantics for propositional logic.""" 25 | from dataclasses import dataclass 26 | from functools import singledispatch 27 | from typing import Dict, Set, Union, cast 28 | 29 | from pylogics.exceptions import PylogicsError 30 | from pylogics.helpers.misc import enforce 31 | from pylogics.semantics.base import base_semantics 32 | from pylogics.syntax.base import ( 33 | AtomName, 34 | FalseFormula, 35 | Formula, 36 | TrueFormula, 37 | _BinaryOp, 38 | _UnaryOp, 39 | ) 40 | from pylogics.syntax.pl import Atomic 41 | 42 | _PropInterpretation = Union[Dict, Set] 43 | 44 | 45 | @dataclass 46 | class _PropInterpretationWrapper: 47 | """Wrapper to a propositional interpretation (either a dict or a set).""" 48 | 49 | _interpretation: Dict[AtomName, bool] 50 | 51 | def __init__(self, interpretation: _PropInterpretation): 52 | """Initialize the object.""" 53 | enforce( 54 | isinstance(interpretation, (dict, set)), 55 | "interpretation must be either a dictionary or a set", 56 | ) 57 | if isinstance(interpretation, set): 58 | self._interpretation = dict.fromkeys(interpretation, True) 59 | else: 60 | self._interpretation = cast(Dict[AtomName, bool], interpretation) 61 | 62 | def __contains__(self, item: str): 63 | """Check an atom name is present in the interpretation.""" 64 | item = AtomName(item) 65 | return self._interpretation.get(item, False) 66 | 67 | 68 | @singledispatch 69 | def evaluate_pl(formula: Formula, _interpretation: _PropInterpretation) -> bool: 70 | """Evaluate a propositional formula against an interpretation.""" 71 | raise PylogicsError( 72 | f"formula '{formula}' cannot be processed by {evaluate_pl.__name__}" # type: ignore 73 | ) 74 | 75 | 76 | @evaluate_pl.register(_BinaryOp) 77 | def evaluate_binary_op(formula: _BinaryOp, interpretation: _PropInterpretation) -> bool: 78 | """Evaluate a propositional formula over a binary operator.""" 79 | return base_semantics(formula, evaluate_pl, interpretation) 80 | 81 | 82 | @evaluate_pl.register(_UnaryOp) 83 | def evaluate_unary_op(formula: _UnaryOp, interpretation: _PropInterpretation) -> bool: 84 | """Evaluate a propositional formula over a unary operator.""" 85 | return base_semantics(formula, evaluate_pl, interpretation) 86 | 87 | 88 | @evaluate_pl.register(Atomic) 89 | def evaluate_atomic(formula: Atomic, interpretation: _PropInterpretation) -> bool: 90 | """Evaluate a propositional formula over an atomic formula.""" 91 | return formula.name in _PropInterpretationWrapper(interpretation) 92 | 93 | 94 | @evaluate_pl.register(TrueFormula) 95 | def evaluate_true(_formula: TrueFormula, _interpretation: _PropInterpretation) -> bool: 96 | """Evaluate a "true" formula.""" 97 | return True 98 | 99 | 100 | @evaluate_pl.register(FalseFormula) 101 | def evaluate_false( 102 | _formula: FalseFormula, _interpretation: _PropInterpretation 103 | ) -> bool: 104 | """Evaluate a "false" formula.""" 105 | return False 106 | -------------------------------------------------------------------------------- /pylogics/syntax/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """This Python package contains the definition of the syntax.""" 25 | -------------------------------------------------------------------------------- /pylogics/syntax/fol.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for first-order logic.""" 25 | from abc import ABC 26 | from functools import partial 27 | from typing import Set 28 | 29 | from pylogics.helpers.misc import enforce 30 | from pylogics.syntax.base import ( 31 | FalseFormula, 32 | Formula, 33 | Logic, 34 | TrueFormula, 35 | _BinaryOp, 36 | _CommutativeBinaryOp, 37 | _UnaryOp, 38 | ) 39 | 40 | 41 | class _FOL(Formula, ABC): 42 | """Interface for FOL formulae.""" 43 | 44 | @property 45 | def logic(self) -> Logic: 46 | """Get the logic formalism.""" 47 | return Logic.FOL 48 | 49 | def __hash__(self): 50 | """Delegate the computation of the hash to the superclass.""" 51 | return super(Formula, self).__hash__() 52 | 53 | 54 | class Term(ABC): 55 | @property 56 | def logic(self) -> Logic: 57 | """Get the logic formalism.""" 58 | return Logic.FOL 59 | 60 | def __str__(self) -> str: 61 | """Get the string representation.""" 62 | return f"{self.name}({', '.join([str(o) for o in self.operands])})" if 'operands' in dir(self) else f"{self.name}" 63 | 64 | def __repr__(self) -> str: 65 | """Get an unambiguous string representation.""" 66 | return f"{type(self).__name__}({str(self)})" 67 | 68 | 69 | class Variable(Term): 70 | def __init__(self, name: str): 71 | self.name = name 72 | 73 | 74 | class Constant(Term): 75 | def __init__(self, name: str, value = None): 76 | self.name = name 77 | self.value = value 78 | 79 | 80 | class Function(Term): 81 | 82 | def __init__(self, name: str, operands: list): 83 | self.name = name 84 | self.operands = operands 85 | 86 | def __call__(self, *operands) -> Term: 87 | enforce(len(operands) == len(self.operands), f"expected {len(self.operands)} operands, got {len(operands)}.") 88 | enforce(all([isinstance(t, Term) for t in operands]), f"all operands must be terms") 89 | return Function(self.name, operands) 90 | 91 | 92 | class _Quantifier(_BinaryOp): 93 | """Interface for quantifiers.""" 94 | 95 | ALLOWED_LOGICS = {Logic.FOL} 96 | 97 | def __init__(self, variable: Variable, formula: Formula): 98 | """ 99 | Initialize the quantifier. 100 | 101 | :param variable: 102 | :param formula: 103 | """ 104 | enforce(isinstance(variable, Variable), f"{type(self).__name__}: expected {Variable}, got {type(variable)}") 105 | enforce(formula.logic == Logic.FOL, "tail formula not valid") 106 | self._variable = variable 107 | self._formula = formula 108 | self._operands = [variable, formula] 109 | 110 | @property 111 | def variable(self) -> Variable: 112 | """Get the variable.""" 113 | return self._variable 114 | 115 | @property 116 | def formula(self) -> Formula: 117 | """Get the quantified formula.""" 118 | return self._formula 119 | 120 | def __hash__(self) -> int: 121 | """Compute the hash.""" 122 | return hash((type(self), self._variable, self._formula)) 123 | 124 | 125 | class Predicate(_FOL): 126 | """Predicate formula.""" 127 | def __init__(self, name: str, operands: list = []): 128 | self._name = name 129 | self._operands = operands 130 | 131 | @property 132 | def name(self) -> str: 133 | """Get the name of the predicate.""" 134 | return self._name 135 | 136 | @property 137 | def operands(self) -> list: 138 | """Get the operands of the predicate.""" 139 | return self._operands 140 | 141 | def __call__(self, *operands) -> Term: 142 | enforce(len(operands) == len(self._operands), f"{self}: expected {len(self._operands)} operands, got {len(operands)}.") 143 | enforce(all([isinstance(t, Term) for t in operands]), f"all operands must be terms") 144 | return Predicate(self._name, operands) 145 | 146 | def __str__(self) -> str: 147 | """Get the string representation.""" 148 | return f"{self._name}({', '.join([str(o) for o in self._operands])})" if self.operands else f"{self._name}" 149 | 150 | def __repr__(self) -> str: 151 | """Get an unambiguous string representation.""" 152 | return f"{type(self).__name__}({str(self)})" 153 | 154 | 155 | class ForAll(_Quantifier, _FOL): 156 | """Universal formula.""" 157 | SYMBOL = "forall" 158 | 159 | 160 | class Exists(_Quantifier, _FOL): 161 | """Existential formula.""" 162 | SYMBOL = "exists" 163 | 164 | -------------------------------------------------------------------------------- /pylogics/syntax/ldl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for linear dynamic logic.""" 25 | from abc import ABC 26 | from functools import partial 27 | from typing import Set 28 | 29 | from pylogics.helpers.misc import enforce 30 | from pylogics.syntax.base import ( 31 | FalseFormula, 32 | Formula, 33 | Logic, 34 | TrueFormula, 35 | _BinaryOp, 36 | _CommutativeBinaryOp, 37 | _UnaryOp, 38 | ) 39 | 40 | 41 | class _LDL(Formula, ABC): 42 | """Interface for LDL formulae.""" 43 | 44 | @property 45 | def logic(self) -> Logic: 46 | """Get the logic formalism.""" 47 | return Logic.LDL 48 | 49 | def __hash__(self): 50 | """Delegate the computation of the hash to the superclass.""" 51 | return super(Formula, self).__hash__() 52 | 53 | 54 | class _RegularExpression(Formula): 55 | """Regular expression.""" 56 | 57 | @property 58 | def logic(self) -> Logic: 59 | """Get the logic formalism.""" 60 | return Logic.RE 61 | 62 | 63 | class Seq(_BinaryOp, _RegularExpression): 64 | """Sequence of regular expressions.""" 65 | 66 | SYMBOL = "seq" 67 | ALLOWED_LOGICS: Set[Logic] = {Logic.RE} 68 | 69 | def __post_init__(self): 70 | """Check consistency after initialization.""" 71 | enforce( 72 | all(op.logic == Logic.RE for op in self.operands), 73 | "not all the operands are regular expressions.", 74 | ) 75 | 76 | 77 | class Union(_CommutativeBinaryOp, _RegularExpression): 78 | """Union of regular expressions.""" 79 | 80 | SYMBOL = "union" 81 | ALLOWED_LOGICS: Set[Logic] = {Logic.RE} 82 | 83 | def __post_init__(self): 84 | """Check consistency after initialization.""" 85 | enforce( 86 | all(op.logic == Logic.RE for op in self.operands), 87 | "not all the operands are regular expressions.", 88 | ) 89 | 90 | 91 | class Test(_UnaryOp, _RegularExpression): 92 | """Test of an LDL formula.""" 93 | 94 | SYMBOL = "test" 95 | ALLOWED_LOGICS: Set[Logic] = {Logic.RE} 96 | 97 | 98 | class Star(_UnaryOp, _RegularExpression): 99 | """Kleene star of regular expressions.""" 100 | 101 | SYMBOL = "star" 102 | 103 | 104 | class Prop(_UnaryOp, _RegularExpression): 105 | """The propositional regular expression.""" 106 | 107 | SYMBOL = "prop" 108 | ALLOWED_LOGICS: Set[Logic] = {Logic.PL} 109 | 110 | 111 | class _TemporalFormula(_LDL, ABC): 112 | """LDL Temporal formula.""" 113 | 114 | def __init__(self, regular_expression: Formula, tail_formula: Formula): 115 | """ 116 | Initialize the formula. 117 | 118 | :param regular_expression: 119 | :param tail_formula: 120 | """ 121 | enforce(regular_expression.logic == Logic.RE, "regular expression not valid") 122 | enforce(tail_formula.logic == Logic.LDL, "tail formula not valid") 123 | self._regular_expression = regular_expression 124 | self._tail_formula = tail_formula 125 | 126 | @property 127 | def regular_expression(self) -> Formula: 128 | """Get the regular expression.""" 129 | return self._regular_expression 130 | 131 | @property 132 | def tail_formula(self) -> Formula: 133 | """Get the tail expression.""" 134 | return self._tail_formula 135 | 136 | def __eq__(self, other): 137 | """Test equality.""" 138 | return ( 139 | type(self) is type(other) 140 | and self.regular_expression == other.regular_expression 141 | and self.tail_formula == other.tail_formula 142 | ) 143 | 144 | def __hash__(self) -> int: 145 | """Compute the hash.""" 146 | return hash((type(self), self.regular_expression, self.tail_formula)) 147 | 148 | 149 | class Diamond(_TemporalFormula): 150 | """Diamond formula.""" 151 | 152 | 153 | class Box(_TemporalFormula): 154 | """Box formula.""" 155 | 156 | 157 | End = partial(Box, Prop(TrueFormula()), FalseFormula(logic=Logic.LDL)) 158 | Last = partial(Diamond, Prop(TrueFormula()), End()) 159 | -------------------------------------------------------------------------------- /pylogics/syntax/ltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for linear temporal logic.""" 25 | from abc import ABC 26 | from functools import partial 27 | 28 | from pylogics.syntax.base import ( 29 | AbstractAtomic, 30 | FalseFormula, 31 | Formula, 32 | Logic, 33 | TrueFormula, 34 | _BinaryOp, 35 | _UnaryOp, 36 | ) 37 | 38 | 39 | class _LTL(Formula, ABC): 40 | """Interface for LTL formulae.""" 41 | 42 | @property 43 | def logic(self) -> Logic: 44 | """Get the logic formalism.""" 45 | return Logic.LTL 46 | 47 | def __hash__(self): 48 | """Delegate the computation of the hash to the superclass.""" 49 | return super(Formula, self).__hash__() 50 | 51 | 52 | class _LTLUnaryOp(_UnaryOp, _LTL): 53 | """LTL Unary operation.""" 54 | 55 | ALLOWED_LOGICS = {Logic.LTL} 56 | 57 | 58 | class _LTLBinaryOp(_BinaryOp, _LTL): 59 | """LTL Unary operation.""" 60 | 61 | ALLOWED_LOGICS = {Logic.LTL} 62 | 63 | 64 | class Atomic(AbstractAtomic, _LTL): 65 | """An atomic proposition of LTL.""" 66 | 67 | 68 | class PropositionalTrue(TrueFormula, _LTL): 69 | """ 70 | A propositional true. 71 | 72 | This is equivalent to 'a | ~a'. for some atom 'a'. 73 | It requires reading any symbol, at least once. 74 | """ 75 | 76 | def __init__(self): 77 | """Initialize.""" 78 | super().__init__(logic=Logic.LTL) 79 | 80 | def __invert__(self) -> "Formula": 81 | """Negate.""" 82 | return PropositionalFalse() 83 | 84 | def __repr__(self) -> str: 85 | """Get an unambiguous string representation.""" 86 | return f"PropositionalTrue({self.logic})" 87 | 88 | 89 | class PropositionalFalse(FalseFormula, _LTL): 90 | """ 91 | A propositional false. 92 | 93 | This is equivalent to 'a & ~a'. for some atom 'a'. 94 | It requires reading no symbol, at least once. 95 | The meaning of this formula is a bit blurred with 'ff' 96 | with respect to 'tt' and 'true'. 97 | """ 98 | 99 | def __init__(self): 100 | """Initialize.""" 101 | super().__init__(logic=Logic.LTL) 102 | 103 | def __invert__(self) -> "Formula": 104 | """Negate.""" 105 | return PropositionalTrue() 106 | 107 | def __repr__(self) -> str: 108 | """Get an unambiguous string representation.""" 109 | return f"PropositionalFalse({self.logic})" 110 | 111 | 112 | class Next(_LTLUnaryOp): 113 | """The "next" formula in LTL.""" 114 | 115 | SYMBOL = "next" 116 | 117 | 118 | class WeakNext(_LTLUnaryOp): 119 | """The "weak next" formula in LTL.""" 120 | 121 | SYMBOL = "weak_next" 122 | 123 | 124 | class Until(_LTLBinaryOp): 125 | """The "next" formula in LTL.""" 126 | 127 | SYMBOL = "until" 128 | 129 | 130 | class Release(_LTLBinaryOp): 131 | """The "release" formula in LTL.""" 132 | 133 | SYMBOL = "release" 134 | 135 | 136 | class Eventually(_LTLUnaryOp): 137 | """The "eventually" formula in LTL.""" 138 | 139 | SYMBOL = "eventually" 140 | 141 | 142 | class Always(_LTLUnaryOp): 143 | """The "always" formula in LTL.""" 144 | 145 | SYMBOL = "always" 146 | 147 | 148 | class WeakUntil(_LTLBinaryOp): 149 | """The "weak until" formula in LTL.""" 150 | 151 | SYMBOL = "weak_until" 152 | 153 | 154 | class StrongRelease(_LTLBinaryOp): 155 | """The "strong release" formula in LTL.""" 156 | 157 | SYMBOL = "strong_release" 158 | 159 | 160 | Last = partial(Always, FalseFormula(logic=Logic.LTL)) 161 | -------------------------------------------------------------------------------- /pylogics/syntax/pl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for propositional logic.""" 25 | 26 | from pylogics.syntax.base import AbstractAtomic, Logic 27 | 28 | 29 | class Atomic(AbstractAtomic): 30 | """An atomic proposition.""" 31 | 32 | @property 33 | def logic(self) -> Logic: 34 | """Get the logic formalism.""" 35 | return Logic.PL 36 | -------------------------------------------------------------------------------- /pylogics/syntax/pltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Classes for past linear temporal logic.""" 25 | from abc import ABC 26 | from functools import partial 27 | 28 | from pylogics.syntax.base import ( 29 | AbstractAtomic, 30 | FalseFormula, 31 | Formula, 32 | Logic, 33 | Not, 34 | TrueFormula, 35 | _BinaryOp, 36 | _UnaryOp, 37 | ) 38 | 39 | 40 | class _PLTL(Formula, ABC): 41 | """Interface for PLTL formulae.""" 42 | 43 | @property 44 | def logic(self) -> Logic: 45 | """Get the logic formalism.""" 46 | return Logic.PLTL 47 | 48 | def __hash__(self): 49 | """Delegate the computation of the hash to the superclass.""" 50 | return super(Formula, self).__hash__() 51 | 52 | 53 | class _PLTLUnaryOp(_UnaryOp, _PLTL): 54 | """PLTL Unary operation.""" 55 | 56 | ALLOWED_LOGICS = {Logic.PLTL} 57 | 58 | 59 | class _PLTLBinaryOp(_BinaryOp, _PLTL): 60 | """PLTL Unary operation.""" 61 | 62 | ALLOWED_LOGICS = {Logic.PLTL} 63 | 64 | 65 | class Atomic(AbstractAtomic, _PLTL): 66 | """An atomic proposition of PLTL.""" 67 | 68 | 69 | class PropositionalTrue(TrueFormula, _PLTL): 70 | """ 71 | A propositional true. 72 | 73 | This is equivalent to 'a | ~a'. for some atom 'a'. 74 | It requires reading any symbol, at least once. 75 | """ 76 | 77 | def __init__(self): 78 | """Initialize.""" 79 | super().__init__(logic=Logic.PLTL) 80 | 81 | def __invert__(self) -> "Formula": 82 | """Negate.""" 83 | return PropositionalFalse() 84 | 85 | def __repr__(self) -> str: 86 | """Get an unambiguous string representation.""" 87 | return f"PropositionalTrue({self.logic})" 88 | 89 | 90 | class PropositionalFalse(FalseFormula, _PLTL): 91 | """ 92 | A propositional false. 93 | 94 | This is equivalent to 'a & ~a'. for some atom 'a'. 95 | It requires reading no symbol, at least once. 96 | The meaning of this formula is a bit blurred with 'ff' 97 | with respect to 'tt' and 'true'. 98 | """ 99 | 100 | def __init__(self): 101 | """Initialize.""" 102 | super().__init__(logic=Logic.PLTL) 103 | 104 | def __invert__(self) -> "Formula": 105 | """Negate.""" 106 | return PropositionalTrue() 107 | 108 | def __repr__(self) -> str: 109 | """Get an unambiguous string representation.""" 110 | return f"PropositionalFalse({self.logic})" 111 | 112 | 113 | class Before(_PLTLUnaryOp): 114 | """The "before" formula in PLTL.""" 115 | 116 | SYMBOL = "before" 117 | 118 | 119 | class Since(_PLTLBinaryOp): 120 | """The "since" formula in PLTL.""" 121 | 122 | SYMBOL = "since" 123 | 124 | 125 | class Once(_PLTLUnaryOp): 126 | """The "once" formula in PLTL.""" 127 | 128 | SYMBOL = "once" 129 | 130 | 131 | class Historically(_PLTLUnaryOp): 132 | """The "historically" formula in PLTL.""" 133 | 134 | SYMBOL = "historically" 135 | 136 | 137 | Start = partial(Not, Before(TrueFormula(logic=Logic.PLTL))) 138 | -------------------------------------------------------------------------------- /pylogics/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Utility functions.""" 25 | -------------------------------------------------------------------------------- /pylogics/utils/to_string.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Transform formulae to string.""" 25 | import functools 26 | from typing import Sequence 27 | 28 | from pylogics.exceptions import PylogicsError 29 | from pylogics.syntax.base import ( 30 | AbstractAtomic, 31 | And, 32 | Equivalence, 33 | FalseFormula, 34 | Formula, 35 | Implies, 36 | Logic, 37 | Not, 38 | Or, 39 | TrueFormula, 40 | ) 41 | from pylogics.syntax.ldl import Box, Diamond, Prop, Seq, Star, Test, Union 42 | from pylogics.syntax.ltl import Always, Eventually, Next 43 | from pylogics.syntax.ltl import PropositionalFalse as LTLPropositionalFalse 44 | from pylogics.syntax.ltl import PropositionalTrue as LTLPropositionalTrue 45 | from pylogics.syntax.ltl import Release, StrongRelease, Until, WeakNext, WeakUntil 46 | from pylogics.syntax.pltl import Before, Historically, Once 47 | from pylogics.syntax.pltl import PropositionalFalse as PLTLPropositionalFalse 48 | from pylogics.syntax.pltl import PropositionalTrue as PLTLPropositionalTrue 49 | from pylogics.syntax.pltl import Since 50 | 51 | 52 | @functools.singledispatch 53 | def to_string(formula: Formula) -> str: 54 | """Transform a formula to a parsable string.""" 55 | raise PylogicsError(f"formula '{formula}' is not supported") 56 | 57 | 58 | def _map_operands_to_string(operands: Sequence[Formula]): 59 | """Map a list of operands to a list of strings (with brackets).""" 60 | return map(lambda sub_formula: f"({to_string(sub_formula)})", operands) 61 | 62 | 63 | @to_string.register(And) 64 | def to_string_and(formula: And) -> str: 65 | """Transform an And into string.""" 66 | return " & ".join(_map_operands_to_string(formula.operands)) 67 | 68 | 69 | @to_string.register(Or) 70 | def to_string_or(formula: Or) -> str: 71 | """Transform an Or into string.""" 72 | return " | ".join(_map_operands_to_string(formula.operands)) 73 | 74 | 75 | @to_string.register(Not) 76 | def to_string_not(formula: Not) -> str: 77 | """Transform a Not into string.""" 78 | return f"~({to_string(formula.argument)})" 79 | 80 | 81 | @to_string.register(Implies) 82 | def to_string_implies(formula: Implies) -> str: 83 | """Transform an Implies into string.""" 84 | return " -> ".join(_map_operands_to_string(formula.operands)) 85 | 86 | 87 | @to_string.register(Equivalence) 88 | def to_string_equivalence(formula: Equivalence) -> str: 89 | """Transform an Equivalence into string.""" 90 | return " <-> ".join(_map_operands_to_string(formula.operands)) 91 | 92 | 93 | @to_string.register(AbstractAtomic) 94 | def to_string_atomic(formula: AbstractAtomic) -> str: 95 | """Transform an atomic formula into string.""" 96 | return formula.name 97 | 98 | 99 | @to_string.register(TrueFormula) 100 | def to_string_logical_true(formula: TrueFormula) -> str: 101 | """Transform the "tt" formula into string.""" 102 | return "tt" if formula.logic != Logic.PL else "true" 103 | 104 | 105 | @to_string.register(FalseFormula) 106 | def to_string_logical_false(formula: FalseFormula) -> str: 107 | """Transform the "ff" formula into string.""" 108 | return "ff" if formula.logic != Logic.PL else "false" 109 | 110 | 111 | @to_string.register 112 | def to_string_ltl_propositional_true(_formula: LTLPropositionalTrue) -> str: 113 | """Transform the "tt" formula into string.""" 114 | return "true" 115 | 116 | 117 | @to_string.register 118 | def to_string_ltl_propositional_false(_formula: LTLPropositionalFalse) -> str: 119 | """Transform the "ff" formula into string.""" 120 | return "false" 121 | 122 | 123 | @to_string.register(Next) 124 | def to_string_next(formula: Next) -> str: 125 | """Transform a next formula into string.""" 126 | return f"X[!]({to_string(formula.argument)})" 127 | 128 | 129 | @to_string.register(WeakNext) 130 | def to_string_weak_next(formula: WeakNext) -> str: 131 | """Transform a weak next formula into string.""" 132 | return f"X({to_string(formula.argument)})" 133 | 134 | 135 | @to_string.register(Until) 136 | def to_string_until(formula: Until) -> str: 137 | """Transform a until formula into string.""" 138 | return " U ".join(_map_operands_to_string(formula.operands)) 139 | 140 | 141 | @to_string.register(WeakUntil) 142 | def to_string_weak_until(formula: WeakUntil) -> str: 143 | """Transform a weak until formula into string.""" 144 | return " W ".join(_map_operands_to_string(formula.operands)) 145 | 146 | 147 | @to_string.register(Release) 148 | def to_string_release(formula: Release) -> str: 149 | """Transform a release formula into string.""" 150 | return " R ".join(_map_operands_to_string(formula.operands)) 151 | 152 | 153 | @to_string.register(StrongRelease) 154 | def to_string_strong_release(formula: StrongRelease) -> str: 155 | """Transform a strong release formula into string.""" 156 | return " M ".join(_map_operands_to_string(formula.operands)) 157 | 158 | 159 | @to_string.register(Eventually) 160 | def to_string_eventually(formula: Eventually) -> str: 161 | """Transform a eventually formula into string.""" 162 | return f"F({to_string(formula.argument)})" 163 | 164 | 165 | @to_string.register(Always) 166 | def to_string_always(formula: Always) -> str: 167 | """Transform a always formula into string.""" 168 | return f"G({to_string(formula.argument)})" 169 | 170 | 171 | @to_string.register 172 | def to_string_pltl_propositional_true(_formula: PLTLPropositionalTrue) -> str: 173 | """Transform the "true" formula into string.""" 174 | return "true" 175 | 176 | 177 | @to_string.register 178 | def to_string_pltl_propositional_false(_formula: PLTLPropositionalFalse) -> str: 179 | """Transform the "false" formula into string.""" 180 | return "false" 181 | 182 | 183 | @to_string.register(Before) 184 | def to_string_pltl_before(formula: Before) -> str: 185 | """Transform a 'before' formula into string.""" 186 | return f"Y({to_string(formula.argument)})" 187 | 188 | 189 | @to_string.register(Since) 190 | def to_string_pltl_since(formula: Since) -> str: 191 | """Transform a 'since' formula into string.""" 192 | return " S ".join(_map_operands_to_string(formula.operands)) 193 | 194 | 195 | @to_string.register(Once) 196 | def to_string_pltl_once(formula: Once) -> str: 197 | """Transform a 'once' formula into string.""" 198 | return f"O({to_string(formula.argument)})" 199 | 200 | 201 | @to_string.register(Historically) 202 | def to_string_pltl_historically(formula: Historically) -> str: 203 | """Transform a 'historically' formula into string.""" 204 | return f"H({to_string(formula.argument)})" 205 | 206 | 207 | @to_string.register(Diamond) 208 | def to_string_ldl_diamond(formula: Diamond) -> str: 209 | """Transform an LDL diamond formula into string.""" 210 | return f"<({to_string(formula.regular_expression)})>({to_string(formula.tail_formula)})" 211 | 212 | 213 | @to_string.register(Box) 214 | def to_string_ldl_box(formula: Box) -> str: 215 | """Transform an LDL box formula into string.""" 216 | return f"[({to_string(formula.regular_expression)})]({to_string(formula.tail_formula)})" 217 | 218 | 219 | @to_string.register(Seq) 220 | def to_string_re_seq(formula: Seq) -> str: 221 | """Transform a sequence regular expression into string.""" 222 | return ";".join(_map_operands_to_string(formula.operands)) 223 | 224 | 225 | @to_string.register(Union) 226 | def to_string_re_union(formula: Union) -> str: 227 | """Transform a union regular expression into string.""" 228 | return "+".join(_map_operands_to_string(formula.operands)) 229 | 230 | 231 | @to_string.register(Star) 232 | def to_string_re_star(formula: Star) -> str: 233 | """Transform a star regular expression into string.""" 234 | return f"({to_string(formula.argument)})*" 235 | 236 | 237 | @to_string.register(Test) 238 | def to_string_re_test(formula: Test) -> str: 239 | """Transform a test regular expression into string.""" 240 | return f"?({to_string(formula.argument)})" 241 | 242 | 243 | @to_string.register(Prop) 244 | def to_string_re_prop(formula: Prop) -> str: 245 | """Transform a propositional regular expression into string.""" 246 | return f"({to_string(formula.argument)})" 247 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pylogics" 3 | version = "0.2.1" 4 | description = "A Python library for logic formalisms representation and manipulation." 5 | authors = ["MarcoFavorito "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://whitemech.github.io/pylogics" 9 | repository = "https://github.com/whitemech/pylogics.git" 10 | documentation = "https://whitemech.github.io/pylogics" 11 | keywords = [ 12 | "logic", 13 | "propositional logic", 14 | "predicate logic", 15 | "temporal logic" 16 | ] 17 | classifiers = [ 18 | 'Development Status :: 2 - Pre-Alpha', 19 | 'Intended Audience :: Developers', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Natural Language :: English', 22 | 'Programming Language :: Python :: 3', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'Programming Language :: Python :: 3.10', 26 | 'Programming Language :: Python :: 3.11', 27 | ] 28 | #packages = [] 29 | include = [] 30 | 31 | 32 | #[tool.poetry.scripts] 33 | #script_name = 'path/to/script' 34 | 35 | [tool.poetry.urls] 36 | "Bug Tracker" = "https://github.com/whitemech/pylogics/issues" 37 | "Pull Requests" = "https://github.com/whitemech/pylogics/pulls" 38 | 39 | 40 | [tool.poetry.dependencies] 41 | python = ">=3.8,<4.0" 42 | lark = "^1.1.5" 43 | 44 | 45 | [tool.poetry.group.dev.dependencies] 46 | bandit = "^1.7.5" 47 | black = "^23.3.0" 48 | codecov = "^2.1.13" 49 | flake8-bugbear = "^23.3" 50 | flake8-docstrings = "^1.7.0" 51 | flake8-eradicate = "^1.5.0" 52 | flake8-isort = "^6.0.0" 53 | hypothesis = "^6.76.0" 54 | hypothesis-pytest = "^0.19.0" 55 | ipython = "^8.12.2" 56 | isort = "^5.12.0" 57 | jupyter = "^1.0.0" 58 | markdown = "^3.3.4" 59 | markdown-include = "^0.8.1" 60 | mistune = "^2.0.5" 61 | mkdocs = "^1.4.3" 62 | mkdocs-material = "^9.1.15" 63 | mknotebooks = "^0.7.1" 64 | mypy = "^1.3.0" 65 | packaging = "^23.0" 66 | pylint = "^2.17.4" 67 | pymdown-extensions = "^10.0.1" 68 | pytest = "^7.3.1" 69 | pytest-cov = "^4.1.0" 70 | pytest-randomly = "^3.12.0" 71 | safety = "^2.4.0b1" 72 | toml = "^0.10.2" 73 | tox = "^4.4.12" 74 | twine = "^4.0.2" 75 | types-requests = "^2.31.0.1" 76 | types-setuptools = "^67.8.0.0" 77 | types-toml = "^0.10.8.6" 78 | vulture = "^2.7" 79 | 80 | [build-system] 81 | requires = ["poetry>=0.12"] 82 | build-backend = "poetry.masonry.api" 83 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = 1 3 | log_cli_level = DEBUG 4 | log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) 5 | log_cli_date_format=%Y-%m-%d %H:%M:%S 6 | -------------------------------------------------------------------------------- /scripts/check_copyright.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2021-2024 The Pylogics contributors 4 | # 5 | # ------------------------------ 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | This script checks that all the Python files of the repository have the copyright notice. 27 | 28 | In particular: 29 | - (optional) the Python shebang 30 | - the encoding header; 31 | - the copyright and license notices; 32 | 33 | It is assumed the script is run from the repository root. 34 | """ 35 | 36 | import argparse 37 | import itertools 38 | import re 39 | import sys 40 | from pathlib import Path 41 | 42 | HEADER_REGEX = re.compile( 43 | r"""(#!/usr/bin/env python3 44 | )?# -\*- coding: utf-8 -\*- 45 | # Copyright 2021-2024 The Pylogics contributors 46 | # 47 | # ------------------------------ 48 | # 49 | # Permission is hereby granted, free of charge, to any person obtaining a copy 50 | # of this software and associated documentation files \(the "Software"\), to deal 51 | # in the Software without restriction, including without limitation the rights 52 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 53 | # copies of the Software, and to permit persons to whom the Software is 54 | # furnished to do so, subject to the following conditions: 55 | # 56 | # The above copyright notice and this permission notice shall be included in all 57 | # copies or substantial portions of the Software\. 58 | # 59 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\. IN NO EVENT SHALL THE 62 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 65 | # SOFTWARE\. 66 | """, 67 | re.MULTILINE, 68 | ) 69 | 70 | 71 | def check_copyright(file: Path) -> bool: 72 | """ 73 | Given a file, check if the header stuff is in place. 74 | 75 | Return True if the files has the encoding header and the copyright notice, 76 | optionally prefixed by the shebang. Return False otherwise. 77 | 78 | :param file: the file to check. 79 | :return True if the file is compliant with the checks, False otherwise. 80 | """ 81 | content = file.read_text() 82 | return re.match(HEADER_REGEX, content) is not None 83 | 84 | 85 | def parse_args(): 86 | """Parse arguments.""" 87 | parser = argparse.ArgumentParser("check_copyright_notice") 88 | parser.add_argument( 89 | "--directory", type=str, default=".", help="The path to the repository root." 90 | ) 91 | 92 | 93 | if __name__ == "__main__": 94 | exclude_files = {Path("scripts", "whitelist.py")} 95 | python_files = filter( 96 | lambda x: x not in exclude_files, 97 | itertools.chain( 98 | Path("pylogics").glob("**/*.py"), 99 | Path("tests").glob("**/*.py"), 100 | Path("scripts").glob("**/*.py"), 101 | ), 102 | ) 103 | 104 | bad_files = [filepath for filepath in python_files if not check_copyright(filepath)] 105 | 106 | if len(bad_files) > 0: 107 | print("The following files are not well formatted:") 108 | print("\n".join(map(str, bad_files))) 109 | sys.exit(1) 110 | else: 111 | print("OK") 112 | sys.exit(0) 113 | -------------------------------------------------------------------------------- /scripts/sync-tox-ini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2021-2024 The Pylogics contributors 4 | # 5 | # ------------------------------ 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | """ 25 | This Python script updates the dependencies in a tox.ini file based on the deps specified in a pyproject.toml file. 26 | 27 | The script reads the dev-dependencies from the pyproject.toml file and then updates the 28 | version specifiers in the tox.ini file. The version specifiers in the pyproject.toml file 29 | use the caret (^) version specifier, which is not compatible with pip. Therefore, this script 30 | converts the caret version specifiers into pip-friendly lower and upper bounds. 31 | 32 | Usage: 33 | 34 | python sync-tox-ini.py path/to/pyproject.toml path/to/tox.ini 35 | 36 | """ 37 | import argparse 38 | import configparser 39 | import logging 40 | from pathlib import Path 41 | from typing import Dict 42 | 43 | import toml 44 | from packaging.requirements import Requirement 45 | 46 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s") 47 | 48 | 49 | def get_dependencies(pyproject_toml_path: Path) -> Dict[str, str]: 50 | """ 51 | Get the dev and main dependencies from pyproject.toml. 52 | 53 | Args: 54 | pyproject_toml_path: Path to pyproject.toml. 55 | 56 | Returns: 57 | A dictionary mapping dependency names to their versions. 58 | """ 59 | pyproject_toml = toml.load(str(pyproject_toml_path)) 60 | dependencies = ( 61 | pyproject_toml.get("tool", {}).get("poetry", {}).get("dependencies", {}) 62 | ) 63 | dev_dependencies = ( 64 | pyproject_toml.get("tool", {}) 65 | .get("poetry", {}) 66 | .get("group", {}) 67 | .get("dev", {}) 68 | .get("dependencies", {}) 69 | ) 70 | 71 | # Merge the two dictionaries 72 | all_dependencies = {**dependencies, **dev_dependencies} 73 | 74 | return all_dependencies 75 | 76 | 77 | def parse_version(version: str) -> str: 78 | """ 79 | Parse a version specifier and convert it into pip-friendly format. 80 | 81 | Args: 82 | version: Version specifier. Could be in the form '^x.y', '^x.y.z' or 'x.y.z'. 83 | 84 | Returns: 85 | A string representing the version specifier in pip-friendly format. 86 | """ 87 | if version[0] != "^": # if version doesn't start with caret, return as is 88 | return version 89 | 90 | parts = list(map(int, version[1:].split("."))) 91 | if len(parts) == 2: # ^x.y 92 | major, minor = parts 93 | return f">={major}.{minor},<{major + 1}" 94 | elif len(parts) == 3: # ^x.y.z 95 | major, minor, patch = parts 96 | return f">={major}.{minor}.{patch},<{major}.{minor + 1}.0" 97 | elif len(parts) == 4: # ^x.y.z.w 98 | major, minor, patch, patch_level = parts 99 | return f">={major}.{minor}.{patch}.{patch_level},<{major}.{minor}.{patch + 1}.0" 100 | else: 101 | raise ValueError(f"Invalid version specifier: {version}") 102 | 103 | 104 | def update_tox_ini(tox_ini_path: Path, dependencies: Dict[str, str]) -> None: 105 | """ 106 | Update dependencies in tox.ini based on the provided dictionary. 107 | 108 | Args: 109 | tox_ini_path: Path to tox.ini. 110 | dependencies: A dictionary mapping dependency names to their versions. 111 | """ 112 | config = configparser.ConfigParser() 113 | config.read(tox_ini_path) 114 | 115 | for section in config.sections(): 116 | if section.startswith("testenv"): 117 | deps = config[section].get("deps", "").split("\n") 118 | new_deps = [] 119 | for dep in deps: 120 | try: 121 | req = Requirement(dep) 122 | dep_name = req.name 123 | if dep_name in dependencies: 124 | new_version = parse_version(dependencies[dep_name]) 125 | new_dep = dep_name + new_version 126 | new_deps.append(new_dep) 127 | else: 128 | new_deps.append(dep) 129 | except Exception: 130 | new_deps.append(dep) 131 | config[section]["deps"] = "\n".join(new_deps) 132 | 133 | with tox_ini_path.open("w") as file: 134 | config.write(file) 135 | 136 | 137 | def main() -> None: 138 | """Parse command line arguments and update dependencies in tox.ini based on pyproject.toml.""" 139 | parser = argparse.ArgumentParser( 140 | description="Update tox.ini dependencies based on pyproject.toml" 141 | ) 142 | parser.add_argument("pyproject", type=Path, help="Path to pyproject.toml") 143 | parser.add_argument("tox", type=Path, help="Path to tox.ini") 144 | args = parser.parse_args() 145 | 146 | logging.info("Reading dev-dependencies from pyproject.toml") 147 | dependencies = get_dependencies(args.pyproject) 148 | logging.info("Updating tox.ini") 149 | update_tox_ini(args.tox, dependencies) 150 | logging.info("Done") 151 | 152 | 153 | if __name__ == "__main__": 154 | main() 155 | -------------------------------------------------------------------------------- /scripts/update-dependencies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2021-2024 The Pylogics contributors 4 | # 5 | # ------------------------------ 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | """ 25 | This script updates the dev-dependencies specified in a Poetry managed pyproject.toml file to their latest versions. 26 | 27 | The script reads the dev-dependencies from the provided pyproject.toml file and fetches the latest version of each 28 | dependency from PyPI. It then updates the dependency in the pyproject.toml file to this latest version by running 29 | the 'poetry add' command with the '^' compatibility character. If an error occurs while updating a dependency, 30 | that dependency is skipped and the script continues with the next one. 31 | 32 | Usage: 33 | python update_dependencies.py /path/to/pyproject.toml 34 | 35 | Requirements: 36 | - Python 3.x 37 | - requests library 38 | - toml library 39 | - A valid pyproject.toml file managed by Poetry. 40 | """ 41 | 42 | import argparse 43 | import logging 44 | import subprocess 45 | from typing import Optional 46 | 47 | import requests 48 | import toml 49 | from packaging.specifiers import SpecifierSet 50 | from packaging.version import InvalidVersion, Version, parse 51 | from pkg_resources import parse_version 52 | 53 | URL_PATTERN = "https://pypi.python.org/pypi/{package}/json" 54 | 55 | 56 | def clean_version_specifier(version_specifier: str) -> str: 57 | """Clean version specifier by removing '.*' if present.""" 58 | cleaned = version_specifier.replace(".*", "") 59 | return cleaned 60 | 61 | 62 | def get_lowest_python_version(specifier_set: SpecifierSet) -> str: 63 | """Get the lowest Python version that satisfies the specifier set.""" 64 | possible_versions = [ 65 | f"{major}.{minor}.{patch}" 66 | for major in range(3, 4) 67 | for minor in range(10) 68 | for patch in range(10) 69 | ] 70 | satisfying_versions = [ 71 | version for version in possible_versions if parse(version) in specifier_set 72 | ] 73 | return min(satisfying_versions, default="3") 74 | 75 | 76 | def get_pyproject_python_version(pyproject_file: str) -> str: 77 | """Extract the Python version from a pyproject.toml file.""" 78 | pyproject_data = toml.load(pyproject_file) 79 | python_version_specifier = pyproject_data["tool"]["poetry"]["dependencies"][ 80 | "python" 81 | ] 82 | specifier_set = SpecifierSet(python_version_specifier) 83 | return get_lowest_python_version(specifier_set) 84 | 85 | 86 | def get_version(package: str, python_version: str) -> Optional[str]: 87 | """Get the latest version of a package that is compatible with the provided Python version.""" 88 | req = requests.get(URL_PATTERN.format(package=package)) 89 | version = parse_version("0") 90 | if req.status_code == requests.codes.ok: 91 | j = req.json() 92 | releases = j.get("releases", []) 93 | for release in releases: 94 | try: 95 | ver = parse_version(release) 96 | except InvalidVersion: 97 | logging.warning(f"Skipping invalid version: {release}") 98 | continue 99 | 100 | release_info = j["releases"].get(str(ver), [{}]) 101 | if release_info: 102 | info = release_info[-1] 103 | if not ver.is_prerelease and info: 104 | requires_python = info["requires_python"] 105 | requires_python = requires_python if requires_python else ">=3" 106 | cleaned_requires_python = clean_version_specifier(requires_python) 107 | if Version(python_version) in SpecifierSet(cleaned_requires_python): 108 | version = max(version, ver) 109 | return str(version) 110 | 111 | 112 | def update_dependencies(pyproject_file: str, poetry_path: str = "poetry") -> None: 113 | """Update the dev-dependencies in a pyproject.toml file to their latest versions.""" 114 | pyproject_data = toml.load(pyproject_file) 115 | dev_dependencies = pyproject_data["tool"]["poetry"]["group"]["dev"]["dependencies"] 116 | python_version = get_pyproject_python_version(pyproject_file) 117 | 118 | for package in dev_dependencies: 119 | if package == "python": 120 | continue 121 | try: 122 | logging.info(f"Updating {package}") 123 | latest_version = get_version(package, python_version) 124 | if latest_version == "0": 125 | logging.warning( 126 | f"Could not find a valid version for {package}, skipping." 127 | ) 128 | continue 129 | logging.info(f"Latest version of {package} is {latest_version}") 130 | subprocess.run( 131 | [poetry_path, "add", "--group", "dev", f"{package}^{latest_version}"], 132 | check=True, 133 | ) 134 | except Exception as e: 135 | logging.exception(f"Failed to update {package} due to error: {str(e)}") 136 | continue 137 | 138 | 139 | def main(): 140 | """Run main function to parse arguments and initiate the update process.""" 141 | parser = argparse.ArgumentParser( 142 | description="Update the dev-dependencies in a pyproject.toml file to their latest versions." 143 | ) 144 | parser.add_argument("file_path", help="The path to the pyproject.toml file.") 145 | parser.add_argument( 146 | "--poetry_path", default="poetry", help="The path to the Poetry executable." 147 | ) 148 | args = parser.parse_args() 149 | 150 | logging.basicConfig(level=logging.INFO) 151 | update_dependencies(args.file_path, args.poetry_path) 152 | 153 | 154 | if __name__ == "__main__": 155 | main() 156 | -------------------------------------------------------------------------------- /scripts/whitelist.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # type: ignore 3 | # pylint: skip-file 4 | _get_current_path # unused function (pylogics/helpers/misc.py:31) 5 | ensure # unused function (pylogics/helpers/misc.py:47) 6 | ensure_set # unused function (pylogics/helpers/misc.py:52) 7 | ensure_sequence # unused function (pylogics/helpers/misc.py:64) 8 | safe_index # unused function (pylogics/helpers/misc.py:76) 9 | safe_get # unused function (pylogics/helpers/misc.py:84) 10 | find # unused function (pylogics/helpers/misc.py:89) 11 | _.start # unused method (pylogics/parsers/ldl.py:50) 12 | _.ldlf_formula # unused method (pylogics/parsers/ldl.py:55) 13 | _.ldlf_unaryop # unused method (pylogics/parsers/ldl.py:76) 14 | _.ldlf_box # unused method (pylogics/parsers/ldl.py:81) 15 | _.ldlf_diamond # unused method (pylogics/parsers/ldl.py:87) 16 | _.ldlf_not # unused method (pylogics/parsers/ldl.py:93) 17 | _.ldlf_atom # unused method (pylogics/parsers/ldl.py:107) 18 | _.ldlf_tt # unused method (pylogics/parsers/ldl.py:113) 19 | _.ldlf_ff # unused method (pylogics/parsers/ldl.py:117) 20 | _.ldlf_last # unused method (pylogics/parsers/ldl.py:121) 21 | _.ldlf_end # unused method (pylogics/parsers/ldl.py:125) 22 | _.ldlf_prop_true # unused method (pylogics/parsers/ldl.py:129) 23 | _.ldlf_prop_false # unused method (pylogics/parsers/ldl.py:133) 24 | _.ldlf_prop_atom # unused method (pylogics/parsers/ldl.py:137) 25 | _.re_propositional # unused method (pylogics/parsers/ldl.py:184) 26 | parse_ldl # unused function (pylogics/parsers/ldl.py:211) 27 | _.start # unused method (pylogics/parsers/ltl.py:53) 28 | _.ltlf_formula # unused method (pylogics/parsers/ltl.py:58) 29 | _.ltlf_always # unused method (pylogics/parsers/ltl.py:97) 30 | _.ltlf_eventually # unused method (pylogics/parsers/ltl.py:101) 31 | _.ltlf_next # unused method (pylogics/parsers/ltl.py:105) 32 | _.ltlf_weak_next # unused method (pylogics/parsers/ltl.py:109) 33 | _.ltlf_not # unused method (pylogics/parsers/ltl.py:113) 34 | _.ltlf_atom # unused method (pylogics/parsers/ltl.py:126) 35 | _.ltlf_true # unused method (pylogics/parsers/ltl.py:131) 36 | _.ltlf_false # unused method (pylogics/parsers/ltl.py:135) 37 | _.ltlf_tt # unused method (pylogics/parsers/ltl.py:139) 38 | _.ltlf_ff # unused method (pylogics/parsers/ltl.py:143) 39 | _.ltlf_last # unused method (pylogics/parsers/ltl.py:147) 40 | _.ltlf_symbol # unused method (pylogics/parsers/ltl.py:151) 41 | parse_ltl # unused function (pylogics/parsers/ltl.py:169) 42 | _.start # unused method (pylogics/parsers/pl.py:42) 43 | _.propositional_formula # unused method (pylogics/parsers/pl.py:47) 44 | _.prop_not # unused method (pylogics/parsers/pl.py:70) 45 | _.prop_atom # unused method (pylogics/parsers/pl.py:83) 46 | _.prop_true # unused method (pylogics/parsers/pl.py:88) 47 | _.prop_false # unused method (pylogics/parsers/pl.py:93) 48 | _.atom # unused method (pylogics/parsers/pl.py:98) 49 | parse_pl # unused function (pylogics/parsers/pl.py:114) 50 | _.start # unused method (pylogics/parsers/pltl.py:50) 51 | _.pltlf_formula # unused method (pylogics/parsers/pltl.py:55) 52 | _.pltlf_historically # unused method (pylogics/parsers/pltl.py:80) 53 | _.pltlf_once # unused method (pylogics/parsers/pltl.py:84) 54 | _.pltlf_before # unused method (pylogics/parsers/pltl.py:88) 55 | _.pltlf_not # unused method (pylogics/parsers/pltl.py:92) 56 | _.pltlf_atom # unused method (pylogics/parsers/pltl.py:105) 57 | _.pltlf_true # unused method (pylogics/parsers/pltl.py:110) 58 | _.pltlf_false # unused method (pylogics/parsers/pltl.py:114) 59 | _.pltlf_tt # unused method (pylogics/parsers/pltl.py:118) 60 | _.pltlf_ff # unused method (pylogics/parsers/pltl.py:122) 61 | _.pltlf_start # unused method (pylogics/parsers/pltl.py:126) 62 | _.pltlf_first # unused method (pylogics/parsers/pltl.py:130) 63 | _.pltlf_symbol # unused method (pylogics/parsers/pltl.py:134) 64 | parse_pltl # unused function (pylogics/parsers/pltl.py:152) 65 | and_semantics # unused function (pylogics/semantics/base.py:37) 66 | or_semantics # unused function (pylogics/semantics/base.py:43) 67 | not_semantics # unused function (pylogics/semantics/base.py:49) 68 | implies_semantics # unused function (pylogics/semantics/base.py:55) 69 | equivalence_semantics # unused function (pylogics/semantics/base.py:81) 70 | evaluate_binary_op # unused function (pylogics/semantics/pl.py:75) 71 | evaluate_unary_op # unused function (pylogics/semantics/pl.py:81) 72 | evaluate_atomic # unused function (pylogics/semantics/pl.py:87) 73 | evaluate_true # unused function (pylogics/semantics/pl.py:93) 74 | evaluate_false # unused function (pylogics/semantics/pl.py:99) 75 | PLDL # unused variable (pylogics/syntax/base.py:41) 76 | FOL # unused variable (pylogics/syntax/base.py:42) 77 | MSO # unused variable (pylogics/syntax/base.py:43) 78 | _.__context # unused attribute (pylogics/syntax/base.py:59) 79 | reset_cache # unused function (pylogics/syntax/base.py:80) 80 | get_cache_context # unused function (pylogics/syntax/base.py:85) 81 | ensure_formula # unused function (pylogics/syntax/base.py:583) 82 | to_string_and # unused function (pylogics/utils/to_string.py:62) 83 | to_string_or # unused function (pylogics/utils/to_string.py:68) 84 | to_string_not # unused function (pylogics/utils/to_string.py:74) 85 | to_string_implies # unused function (pylogics/utils/to_string.py:80) 86 | to_string_equivalence # unused function (pylogics/utils/to_string.py:86) 87 | to_string_atomic # unused function (pylogics/utils/to_string.py:92) 88 | to_string_logical_true # unused function (pylogics/utils/to_string.py:98) 89 | to_string_logical_false # unused function (pylogics/utils/to_string.py:104) 90 | to_string_ltl_propositional_true # unused function (pylogics/utils/to_string.py:110) 91 | to_string_ltl_propositional_false # unused function (pylogics/utils/to_string.py:116) 92 | to_string_next # unused function (pylogics/utils/to_string.py:122) 93 | to_string_weak_next # unused function (pylogics/utils/to_string.py:128) 94 | to_string_until # unused function (pylogics/utils/to_string.py:134) 95 | to_string_weak_until # unused function (pylogics/utils/to_string.py:140) 96 | to_string_release # unused function (pylogics/utils/to_string.py:146) 97 | to_string_strong_release # unused function (pylogics/utils/to_string.py:152) 98 | to_string_eventually # unused function (pylogics/utils/to_string.py:158) 99 | to_string_always # unused function (pylogics/utils/to_string.py:164) 100 | to_string_pltl_propositional_true # unused function (pylogics/utils/to_string.py:170) 101 | to_string_pltl_propositional_false # unused function (pylogics/utils/to_string.py:176) 102 | to_string_pltl_before # unused function (pylogics/utils/to_string.py:182) 103 | to_string_pltl_since # unused function (pylogics/utils/to_string.py:188) 104 | to_string_pltl_once # unused function (pylogics/utils/to_string.py:194) 105 | to_string_pltl_historically # unused function (pylogics/utils/to_string.py:200) 106 | to_string_ldl_diamond # unused function (pylogics/utils/to_string.py:206) 107 | to_string_ldl_box # unused function (pylogics/utils/to_string.py:212) 108 | to_string_re_seq # unused function (pylogics/utils/to_string.py:218) 109 | to_string_re_union # unused function (pylogics/utils/to_string.py:224) 110 | to_string_re_star # unused function (pylogics/utils/to_string.py:230) 111 | to_string_re_test # unused function (pylogics/utils/to_string.py:236) 112 | to_string_re_prop # unused function (pylogics/utils/to_string.py:242) 113 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [aliases] 5 | test = pytest 6 | 7 | [metadata] 8 | license_file = LICENSE 9 | 10 | [flake8] 11 | ignore = W503 12 | exclude = 13 | .tox, 14 | .git, 15 | __pycache__, 16 | build, 17 | dist, 18 | tests/fixtures/*, 19 | *.md, 20 | *.pyc, 21 | *.egg-info, 22 | .cache, 23 | .eggs, 24 | pylogics/__init__.py, 25 | scripts/whitelist.py 26 | max-complexity = 10 27 | max-line-length = 120 28 | 29 | [isort] 30 | multi_line_output=3 31 | include_trailing_comma=True 32 | force_grid_wrap=0 33 | use_parentheses=True 34 | line_length=88 35 | 36 | [black] 37 | exclude = "scripts/whitelist.py 38 | 39 | 40 | [mypy] 41 | python_version = 3.7 42 | strict_optional = True 43 | 44 | # Per-module options: 45 | 46 | [mypy-lark.*] 47 | ignore_missing_imports = True 48 | 49 | [mypy-mistune.*] 50 | ignore_missing_imports = True 51 | 52 | # Per-module options for tests dir: 53 | 54 | [mypy-pytest] 55 | ignore_missing_imports = True 56 | 57 | # Per-script options 58 | [mypy-scripts/whitelist] 59 | ignore_errors = True 60 | 61 | [darglint] 62 | docstring_style=sphinx 63 | strictness=short 64 | ignore_regex= 65 | ignore=DAR401 66 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Test for the pylogics project.""" 25 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """The conftest.py module for pytest.""" 25 | import inspect 26 | from pathlib import Path 27 | 28 | import pytest 29 | from hypothesis import HealthCheck, settings 30 | 31 | import pylogics 32 | from pylogics.syntax.base import reset_cache 33 | 34 | _current_filepah = inspect.getframeinfo(inspect.currentframe()).filename # type: ignore 35 | TEST_DIRECTORY = Path(_current_filepah).absolute().parent 36 | ROOT_DIRECTORY = TEST_DIRECTORY.parent 37 | LIBRARY_DIRECTORY = ROOT_DIRECTORY / pylogics.__name__ 38 | DOCS_DIRECTORY = ROOT_DIRECTORY / "docs" 39 | 40 | 41 | @pytest.fixture(scope="class", autouse=True) 42 | def reset_cache_fixture(): 43 | """Reset hash-consing global cache after each test function/class call.""" 44 | reset_cache() 45 | 46 | 47 | suppress_health_checks_for_lark = settings( 48 | suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much] 49 | ) 50 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.syntax.ltl module.""" 25 | import pytest 26 | 27 | from pylogics.syntax.base import FalseFormula, Logic, Not, TrueFormula 28 | 29 | 30 | @pytest.mark.parametrize("logic", list(Logic)) 31 | def test_not_true(logic): 32 | """Test that the negation of true gives false.""" 33 | assert FalseFormula(logic=logic) == Not(TrueFormula(logic=logic)) 34 | 35 | 36 | @pytest.mark.parametrize("logic", list(Logic)) 37 | def test_not_false(logic): 38 | """Test that the negation of false gives true.""" 39 | assert TrueFormula(logic=logic) == Not(FalseFormula(logic=logic)) 40 | -------------------------------------------------------------------------------- /tests/test_docs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Test documentation code.""" 25 | -------------------------------------------------------------------------------- /tests/test_docs/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Utilities for docs tests.""" 25 | import traceback 26 | from functools import partial 27 | from pathlib import Path 28 | from typing import Dict, List, Optional 29 | 30 | import mistune 31 | import pytest 32 | 33 | MISTUNE_BLOCK_CODE_ID = "block_code" 34 | 35 | 36 | def compile_and_exec(code: str, locals_dict: Optional[Dict] = None) -> Dict: 37 | """ 38 | Compile and exec the code. 39 | 40 | :param code: the code to execute. 41 | :param locals_dict: the dictionary of local variables. 42 | :return: the dictionary of locals. 43 | """ 44 | locals_dict = {} if locals_dict is None else locals_dict 45 | try: 46 | code_obj = compile(code, "fakemodule", "exec") 47 | exec(code_obj, locals_dict) # nosec 48 | except Exception: # type: ignore 49 | pytest.fail( 50 | "The execution of the following code:\n{}\nfailed with error:\n{}".format( 51 | code, traceback.format_exc() 52 | ) 53 | ) 54 | return locals_dict 55 | 56 | 57 | class BaseTestMarkdownDocs: 58 | """Base test class for testing Markdown documents.""" 59 | 60 | MD_FILE: Optional[Path] = None 61 | code_blocks: List[Dict] = [] 62 | 63 | @classmethod 64 | def setup_class(cls): 65 | """Set up class.""" 66 | if cls.MD_FILE is None: 67 | raise ValueError("cannot set up method as MD_FILE is None") 68 | content = cls.MD_FILE.read_text() 69 | markdown_parser = mistune.create_markdown(renderer=mistune.AstRenderer()) 70 | cls.blocks = markdown_parser(content) 71 | cls.code_blocks = list(filter(cls.block_code_filter, cls.blocks)) 72 | 73 | @staticmethod 74 | def block_code_filter(block: Dict) -> bool: 75 | """Check Mistune block is a code block.""" 76 | return block["type"] == MISTUNE_BLOCK_CODE_ID 77 | 78 | @staticmethod 79 | def type_filter(type_: Optional[str], b: Dict) -> bool: 80 | """ 81 | Check Mistune code block is of a certain type. 82 | 83 | If the field "info" is None, return False. 84 | If type_ is None, this function always return true. 85 | 86 | :param type_: the expected type of block (optional) 87 | :param b: the block dicionary. 88 | :return: True if the block should be accepted, false otherwise. 89 | """ 90 | if type_ is None: 91 | return True 92 | return b["info"].strip() == type_ if b["info"] is not None else False 93 | 94 | @classmethod 95 | def extract_code_blocks(cls, filter_: Optional[str] = None): 96 | """Extract code blocks from .md files.""" 97 | actual_type_filter = partial(cls.type_filter, filter_) 98 | bash_code_blocks = filter(actual_type_filter, cls.code_blocks) 99 | return list(b["text"] for b in bash_code_blocks) 100 | 101 | 102 | class BasePythonMarkdownDocs(BaseTestMarkdownDocs): 103 | """Test Markdown documentation by running Python snippets in sequence.""" 104 | 105 | @classmethod 106 | def setup_class(cls): 107 | """ 108 | Set up class. 109 | 110 | It sets the initial value of locals and globals. 111 | """ 112 | super().setup_class() 113 | cls.locals = {} 114 | cls.globals = {} 115 | 116 | @classmethod 117 | def _python_selector(cls, block: Dict) -> bool: 118 | return block["type"] == MISTUNE_BLOCK_CODE_ID and ( 119 | block["info"].strip() == "python" if block["info"] else False 120 | ) 121 | 122 | def _assert(self, locals_, *mocks): 123 | """Do assertions after Python code execution.""" 124 | 125 | def test_python_blocks(self, *mocks): 126 | """Run Python code block in sequence.""" 127 | python_blocks = list(filter(self._python_selector, self.blocks)) 128 | 129 | globals_, locals_ = self.globals, self.locals 130 | for python_block in python_blocks: 131 | python_code = python_block["text"] 132 | exec(python_code, globals_, locals_) # nosec 133 | self._assert(locals_, *mocks) 134 | -------------------------------------------------------------------------------- /tests/test_docs/test_grammars.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Test grammars documentation page.""" 25 | 26 | from tests.conftest import DOCS_DIRECTORY, LIBRARY_DIRECTORY 27 | from tests.test_docs.base import BaseTestMarkdownDocs 28 | 29 | 30 | class TestGrammars(BaseTestMarkdownDocs): 31 | """Test that the docs' and library's grammars are the same.""" 32 | 33 | MD_FILE = DOCS_DIRECTORY / "grammars.md" 34 | 35 | @classmethod 36 | def setup_class(cls): 37 | """Set up the test.""" 38 | super().setup_class() 39 | 40 | grammar_directory = LIBRARY_DIRECTORY / "parsers" 41 | 42 | for formalism in ["pl", "ltl", "pltl", "ldl"]: 43 | text = (grammar_directory / f"{formalism}.lark").read_text() 44 | setattr(cls, formalism + "_grammar", text) 45 | 46 | cls.actual_grammars = cls.extract_code_blocks(filter_="lark") 47 | 48 | def test_pl_grammar(self): 49 | """Test PL grammar is as expected.""" 50 | assert self.actual_grammars[0] == self.pl_grammar 51 | 52 | def test_ltl_grammar(self): 53 | """Test LTL grammar is as expected.""" 54 | assert self.actual_grammars[1] == self.ltl_grammar 55 | 56 | def test_pltl_grammar(self): 57 | """Test PLTL grammar is as expected.""" 58 | assert self.actual_grammars[2] == self.pltl_grammar 59 | 60 | def test_ldl_grammar(self): 61 | """Test LDL grammar is as expected.""" 62 | assert self.actual_grammars[3] == self.ldl_grammar 63 | -------------------------------------------------------------------------------- /tests/test_docs/test_introduction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Test introduction documentation page.""" 25 | from pylogics.semantics.pl import evaluate_pl 26 | from tests.conftest import DOCS_DIRECTORY 27 | from tests.test_docs.base import BasePythonMarkdownDocs 28 | 29 | 30 | class TestIntroduction(BasePythonMarkdownDocs): 31 | """Test that the code snippet in the introduction are correct.""" 32 | 33 | MD_FILE = DOCS_DIRECTORY / "introduction.md" 34 | 35 | def _assert(self, locals_, *mocks): 36 | """Do assertions after Python code execution.""" 37 | assert self.locals["evaluate_pl"] == evaluate_pl 38 | -------------------------------------------------------------------------------- /tests/test_docs/test_parsing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Test parsing documentation page.""" 25 | from tests.conftest import DOCS_DIRECTORY 26 | from tests.test_docs.base import BasePythonMarkdownDocs 27 | 28 | 29 | class TestParsing(BasePythonMarkdownDocs): 30 | """Test that the code snippet in the parsing page run without failure.""" 31 | 32 | MD_FILE = DOCS_DIRECTORY / "parsing.md" 33 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.exceptions module.""" 25 | from pylogics.exceptions import ParsingError 26 | 27 | 28 | def test_parsing_exception_init(): 29 | """Test initialization of parsing exception.""" 30 | e = ParsingError() 31 | assert e.args == ("parsing error",) 32 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.helpers module.""" 25 | import pickle 26 | 27 | from pylogics.helpers.cache_hash import Hashable 28 | 29 | 30 | class MyHashable(metaclass=Hashable): 31 | """A test class to test 'Hashable' metaclass.""" 32 | 33 | def __init__(self): 34 | """Initialize.""" 35 | super().__init__() 36 | self.a = "a" 37 | self.b = "b" 38 | 39 | def __hash__(self): 40 | """Compute the hash.""" 41 | return hash((self.a, self.b)) 42 | 43 | 44 | def test_hashable(): 45 | """Test the hashable class.""" 46 | obj = MyHashable() 47 | 48 | assert not hasattr(obj, "__hash") 49 | 50 | h1 = hash(obj) 51 | h2 = hash(obj) 52 | assert h1 == h2 53 | 54 | assert hasattr(obj, "__hash") 55 | assert obj.__hash == h1 == h2 56 | 57 | dumped_obj = pickle.dumps(obj) 58 | actual_obj = pickle.loads(dumped_obj) 59 | assert not hasattr(actual_obj, "__hash") 60 | -------------------------------------------------------------------------------- /tests/test_ldl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.syntax.ldl module.""" 25 | from hypothesis import given 26 | from hypothesis.extra.lark import from_lark 27 | 28 | from pylogics.parsers.ldl import __parser as ldl_parser 29 | from pylogics.parsers.ldl import parse_ldl 30 | from pylogics.syntax.base import Logic 31 | from pylogics.utils.to_string import to_string 32 | from tests.conftest import suppress_health_checks_for_lark 33 | 34 | 35 | @suppress_health_checks_for_lark 36 | @given(from_lark(ldl_parser._parser)) 37 | def test_parser(formula): 38 | """Test parsing is deterministic.""" 39 | formula_1 = parse_ldl(formula) 40 | formula_2 = parse_ldl(formula) 41 | assert formula_1 == formula_2 42 | assert Logic.LDL == formula_1.logic == formula_2.logic 43 | 44 | 45 | @suppress_health_checks_for_lark 46 | @given(from_lark(ldl_parser._parser)) 47 | def test_to_string(formula): 48 | """Test that the output of 'to_string' is parsable.""" 49 | expected_formula = parse_ldl(formula) 50 | formula_str = to_string(expected_formula) 51 | actual_formula = parse_ldl(formula_str) 52 | assert actual_formula == expected_formula 53 | -------------------------------------------------------------------------------- /tests/test_ltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.syntax.ltl module.""" 25 | from hypothesis import HealthCheck, given, settings 26 | from hypothesis.extra.lark import from_lark 27 | 28 | from pylogics.parsers.ltl import __parser as ltl_parser 29 | from pylogics.parsers.ltl import parse_ltl 30 | from pylogics.syntax.base import Logic 31 | from pylogics.utils.to_string import to_string 32 | 33 | 34 | @given(from_lark(ltl_parser._parser)) 35 | @settings( 36 | max_examples=1000, 37 | suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], 38 | ) 39 | def test_parser(formula): 40 | """Test parsing is deterministic.""" 41 | formula_1 = parse_ltl(formula) 42 | formula_2 = parse_ltl(formula) 43 | assert formula_1 == formula_2 44 | assert Logic.LTL == formula_1.logic == formula_2.logic 45 | 46 | 47 | @given(from_lark(ltl_parser._parser)) 48 | @settings( 49 | max_examples=1000, 50 | suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much], 51 | ) 52 | def test_to_string(formula): 53 | """Test that the output of 'to_string' is parsable.""" 54 | expected_formula = parse_ltl(formula) 55 | formula_str = to_string(expected_formula) 56 | actual_formula = parse_ltl(formula_str) 57 | assert actual_formula == expected_formula 58 | -------------------------------------------------------------------------------- /tests/test_pl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.syntax.propositional module.""" 25 | from hypothesis import given 26 | from hypothesis.extra.lark import from_lark 27 | from hypothesis.strategies import booleans, dictionaries, from_regex, one_of, sets 28 | 29 | from pylogics.parsers.pl import __parser as prop_parser 30 | from pylogics.parsers.pl import parse_pl 31 | from pylogics.semantics.pl import evaluate_pl 32 | from pylogics.syntax.base import AtomName, Logic, get_cache_context 33 | from pylogics.utils.to_string import to_string 34 | from tests.conftest import suppress_health_checks_for_lark 35 | 36 | 37 | @suppress_health_checks_for_lark 38 | @given(from_lark(prop_parser._parser)) 39 | def test_parser(formula): 40 | """Test parsing is deterministic.""" 41 | formula_1 = parse_pl(formula) 42 | formula_2 = parse_pl(formula) 43 | assert formula_1 == formula_2 44 | assert Logic.PL == formula_1.logic == formula_2.logic 45 | 46 | 47 | @suppress_health_checks_for_lark 48 | @given(from_lark(prop_parser._parser)) 49 | def test_to_string(formula): 50 | """Test that the output of 'to_string' is parsable.""" 51 | expected_formula = parse_pl(formula) 52 | formula_str = to_string(expected_formula) 53 | actual_formula = parse_pl(formula_str) 54 | assert actual_formula == expected_formula 55 | 56 | 57 | @suppress_health_checks_for_lark 58 | @given( 59 | from_lark(prop_parser._parser), 60 | one_of( 61 | sets(from_regex(AtomName.REGEX)), 62 | dictionaries(keys=from_regex(AtomName.REGEX), values=booleans()), 63 | ), 64 | ) 65 | def test_semantics_negation(formula, interpretation): 66 | """Test that for all models I, (I |= phi) != (I |= ~phi).""" 67 | positive_formula = parse_pl(formula) 68 | negative_formula = ~positive_formula 69 | assert evaluate_pl(positive_formula, interpretation) != evaluate_pl( 70 | negative_formula, interpretation 71 | ) 72 | 73 | 74 | def test_hash_consing(): 75 | """Test that hash consing works as expected.""" 76 | a1 = parse_pl("a") 77 | context = get_cache_context() 78 | assert len(context[a1.logic]) == 1 79 | assert context[a1.logic][a1] is None 80 | assert list(context[a1.logic])[0] == a1 81 | 82 | a2 = parse_pl("a") 83 | assert len(context[a1.logic]) == 1 84 | assert context[a1.logic][a1] is None 85 | assert list(context[a1.logic])[0] == a1 == a2 86 | -------------------------------------------------------------------------------- /tests/test_pltl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2021-2024 The Pylogics contributors 3 | # 4 | # ------------------------------ 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | """Tests on the pylogics.syntax.pltl module.""" 25 | from hypothesis import given 26 | from hypothesis.extra.lark import from_lark 27 | 28 | from pylogics.parsers.pltl import __parser as pltl_parser 29 | from pylogics.parsers.pltl import parse_pltl 30 | from pylogics.syntax.base import Logic 31 | from pylogics.utils.to_string import to_string 32 | from tests.conftest import suppress_health_checks_for_lark 33 | 34 | 35 | @suppress_health_checks_for_lark 36 | @given(from_lark(pltl_parser._parser)) 37 | def test_parser(formula): 38 | """Test parsing is deterministic.""" 39 | formula_1 = parse_pltl(formula) 40 | formula_2 = parse_pltl(formula) 41 | assert formula_1 == formula_2 42 | assert Logic.PLTL == formula_1.logic == formula_2.logic 43 | 44 | 45 | @suppress_health_checks_for_lark 46 | @given(from_lark(pltl_parser._parser)) 47 | def test_to_string(formula): 48 | """Test that the output of 'to_string' is parsable.""" 49 | expected_formula = parse_pltl(formula) 50 | formula_str = to_string(expected_formula) 51 | actual_formula = parse_pltl(formula_str) 52 | assert actual_formula == expected_formula 53 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = True 3 | envlist = bandit, safety, check-copyright, black-check, isort-check, vulture, flake8, mypy, docs, py3{8,9,10,11} 4 | 5 | [tox:.package] 6 | basepython = python3 7 | 8 | [testenv] 9 | setenv = 10 | PYTHONPATH = {toxinidir} 11 | deps = 12 | pytest>=7.3.1,<7.4.0 13 | pytest-cov>=4.1.0,<4.2.0 14 | pytest-randomly>=3.12.0,<3.13.0 15 | hypothesis-pytest>=0.19.0,<0.20.0 16 | hypothesis>=6.75.9,<6.76.0 17 | mistune>=2.0.5,<2.1.0 18 | allowlist_externals = pytest 19 | commands = 20 | pytest --basetemp={envtmpdir} --doctest-modules \ 21 | pylogics tests/ \ 22 | --cov=pylogics \ 23 | --cov-report=xml \ 24 | --cov-report=html \ 25 | --cov-report=term 26 | 27 | [testenv:py{37,38,39,310}] 28 | commands = 29 | {[testenv]commands} 30 | deps = 31 | 32 | [testenv:flake8] 33 | skip_install = True 34 | deps = 35 | flake8 36 | flake8-bugbear>=23.5.9,<23.6.0 37 | flake8-docstrings>=1.7.0,<1.8.0 38 | flake8-eradicate>=1.5.0,<1.6.0 39 | flake8-isort>=6.0.0,<6.1.0 40 | pydocstyle 41 | commands = 42 | flake8 pylogics tests scripts 43 | 44 | [testenv:mypy] 45 | skip_install = True 46 | deps = 47 | mypy>=1.3.0,<1.4.0 48 | hypothesis>=6.75.9,<6.76.0 49 | packaging>=23.0,<24 50 | importlib-metadata>=6.6.0,<6.7.0 51 | types-requests>=2.31.0.1,<2.31.1.0 52 | types-setuptools>=67.8.0.0,<67.8.1.0 53 | types-toml>=0.10.8.6,<0.10.9.0 54 | commands = 55 | mypy pylogics tests scripts 56 | 57 | [testenv:pylint] 58 | skipdist = True 59 | deps = pylint>=2.17.4,<2.18.0 60 | commands = pylint pylogics tests scripts 61 | 62 | [testenv:black] 63 | skip_install = True 64 | deps = black>=23.3.0,<23.4.0 65 | commands = black pylogics tests scripts 66 | 67 | [testenv:black-check] 68 | skip_install = True 69 | deps = black>=23.3.0,<23.4.0 70 | commands = black pylogics tests scripts --check --verbose 71 | 72 | [testenv:isort] 73 | skip_install = True 74 | deps = isort>=5.12.0,<5.13.0 75 | commands = isort pylogics tests scripts 76 | 77 | [testenv:isort-check] 78 | skip_install = True 79 | deps = isort>=5.12.0,<5.13.0 80 | commands = isort --check-only pylogics tests scripts 81 | 82 | [testenv:bandit] 83 | skipsdist = True 84 | skip_install = True 85 | deps = bandit>=1.7.5,<1.8.0 86 | commands = bandit pylogics tests scripts 87 | 88 | [testenv:safety] 89 | skipsdist = True 90 | skip_install = True 91 | deps = safety>=2.3.5,<2.4.0 92 | commands = safety 93 | 94 | [testenv:docs] 95 | skip_install = True 96 | deps = 97 | mkdocs>=1.4.3,<1.5.0 98 | mkdocs-material>=9.1.15,<9.2.0 99 | markdown-include>=0.8.1,<0.9.0 100 | pymdown-extensions>=10.0.1,<10.1.0 101 | markdown>=3.3.4,<3.4.0 102 | mknotebooks>=0.7.1,<0.8.0 103 | commands = 104 | mkdocs build --clean 105 | 106 | [testenv:docs-serve] 107 | skip_install = True 108 | deps = 109 | mkdocs>=1.4.3,<1.5.0 110 | mkdocs-material>=9.1.15,<9.2.0 111 | markdown-include>=0.8.1,<0.9.0 112 | pymdown-extensions>=10.0.1,<10.1.0 113 | markdown>=3.3.4,<3.4.0 114 | mknotebooks>=0.7.1,<0.8.0 115 | commands = 116 | mkdocs build --clean 117 | python -c 'print("###### Starting local server. Press Control+C to stop server ######")' 118 | mkdocs serve 119 | 120 | [testenv:check-copyright] 121 | skip_install = True 122 | deps = 123 | commands = python3 scripts/check_copyright.py 124 | 125 | [testenv:vulture] 126 | skipsdist = True 127 | skip_install = True 128 | deps = vulture>=2.7,<3 129 | commands = vulture pylogics scripts/whitelist.py 130 | 131 | --------------------------------------------------------------------------------