├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── Varphi.g4 ├── setup.py ├── tests ├── __init__.py ├── test_examples.py ├── test_varphi_parser_and_lexer.py └── test_varphi_types.py ├── varphi ├── __init__.py ├── compilation │ ├── __init__.py │ ├── core.py │ ├── varphi_compilation_errors.py │ └── varphi_translator.py ├── parsing │ └── __init__.py └── runtime │ ├── __init__.py │ ├── cli.py │ ├── cli_debugger.py │ ├── cli_with_complexity.py │ ├── cli_with_prompts.py │ ├── common.py │ ├── debug_adapter.py │ ├── exceptions.py │ └── types.py └── varphi_interpreter └── vpi.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release with Executables 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | name: Build executables 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, ubuntu-24.04, ubuntu-22.04, ubuntu-20.04, windows-latest, windows-2025, windows-2022, windows-2019, macos-13, macos-latest, macos-14, macos-15] 18 | steps: 19 | - name: Check out repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.13' 26 | 27 | - name: Set up Java 28 | uses: actions/setup-java@v2 29 | with: 30 | distribution: 'temurin' 31 | java-version: '23' 32 | 33 | - name: Build Executable 34 | run: | 35 | make executable EXE_NAME=vpi-${{ matrix.os }} 36 | 37 | - name: Upload executables 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: vpi-${{ matrix.os }} 41 | path: bin 42 | 43 | release: 44 | name: Create release 45 | runs-on: ubuntu-latest 46 | needs: build 47 | steps: 48 | - name: Download artifacts 49 | uses: actions/download-artifact@v4 50 | with: 51 | path: artifacts/ 52 | 53 | - name: List downloaded artifacts 54 | run: ls -R artifacts/ 55 | 56 | - name: Flatten artifact paths 57 | run: | 58 | mkdir -p flattened_artifacts 59 | find artifacts/ -type f -exec cp {} flattened_artifacts/ \; 60 | 61 | - name: List flattened artifacts 62 | run: ls -R flattened_artifacts/ 63 | 64 | - name: Create release 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | tag: ${{ github.ref_name }} 68 | run: | 69 | gh release create "$tag" \ 70 | --repo="$GITHUB_REPOSITORY" \ 71 | --title="${tag#v}" \ 72 | --generate-notes \ 73 | flattened_artifacts/* 74 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint_and_test_linux: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Step 1: Checkout the code 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | # Step 2: Set up Python 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.13' 24 | 25 | # Step 3: Set up Java 26 | - name: Set up Java 27 | uses: actions/setup-java@v2 28 | with: 29 | distribution: 'temurin' 30 | java-version: '23' 31 | 32 | # Step 4: Test 33 | - name: Test 34 | run: | 35 | make test 36 | 37 | # Step 5: Lint 38 | - name: Lint 39 | run: | 40 | make lint 41 | 42 | lint_and_test_windows: 43 | runs-on: windows-latest 44 | steps: 45 | # Step 1: Checkout the code 46 | - name: Checkout code 47 | uses: actions/checkout@v2 48 | 49 | # Step 2: Set up Python 50 | - name: Set up Python 51 | uses: actions/setup-python@v2 52 | with: 53 | python-version: '3.13' 54 | 55 | # Step 3: Set up Java 56 | - name: Set up Java 57 | uses: actions/setup-java@v2 58 | with: 59 | distribution: 'temurin' 60 | java-version: '23' 61 | 62 | # Step 4: Test 63 | - name: Test 64 | run: | 65 | make test 66 | 67 | # Step 5: Lint 68 | - name: Lint 69 | run: | 70 | make lint 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | ; # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | ; # 3 compatible code, which means that the block might have code that exists 5 | ; # only in one or another interpreter, leading to false positives when analysed. 6 | ; analyse-fallback-blocks=no 7 | 8 | ; # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | ; # in a server-like mode. 10 | ; clear-cache-post-run=no 11 | 12 | ; # Load and enable all available extensions. Use --list-extensions to see a list 13 | ; # all available extensions. 14 | ; #enable-all-extensions= 15 | 16 | ; # In error mode, messages with a category besides ERROR or FATAL are 17 | ; # suppressed, and no reports are done by default. Error mode is compatible with 18 | ; # disabling specific errors. 19 | ; #errors-only= 20 | 21 | ; # Always return a 0 (non-error) status code, even if lint errors are found. 22 | ; # This is primarily useful in continuous integration scripts. 23 | ; #exit-zero= 24 | 25 | ; # A comma-separated list of package or module names from where C extensions may 26 | ; # be loaded. Extensions are loading into the active Python interpreter and may 27 | ; # run arbitrary code. 28 | ; extension-pkg-allow-list= 29 | 30 | ; # A comma-separated list of package or module names from where C extensions may 31 | ; # be loaded. Extensions are loading into the active Python interpreter and may 32 | ; # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | ; # for backward compatibility.) 34 | ; extension-pkg-whitelist= 35 | 36 | ; # Return non-zero exit code if any of these messages/categories are detected, 37 | ; # even if score is above --fail-under value. Syntax same as enable. Messages 38 | ; # specified are enabled, while categories only check already-enabled messages. 39 | ; fail-on= 40 | 41 | ; # Specify a score threshold under which the program will exit with error. 42 | ; fail-under=10 43 | 44 | ; # Interpret the stdin as a python script, whose filename needs to be passed as 45 | ; # the module_or_package argument. 46 | ; #from-stdin= 47 | 48 | ; # Files or directories to be skipped. They should be base names, not paths. 49 | ; ignore=CVS 50 | 51 | ; # Add files or directories matching the regular expressions patterns to the 52 | ; # ignore-list. The regex matches against paths and can be in Posix or Windows 53 | ; # format. Because '\\' represents the directory delimiter on Windows systems, 54 | ; # it can't be used as an escape character. 55 | ignore-paths=varphi\parsing\VarphiLexer.py, varphi\parsing\VarphiParser.py, varphi\parsing\VarphiListener.py 56 | 57 | ; # Files or directories matching the regular expression patterns are skipped. 58 | ; # The regex matches against base names, not paths. The default value ignores 59 | ; # Emacs file locks 60 | ; ignore-patterns= 61 | 62 | ; # List of module names for which member attributes should not be checked and 63 | ; # will not be imported (useful for modules/projects where namespaces are 64 | ; # manipulated during runtime and thus existing member attributes cannot be 65 | ; # deduced by static analysis). It supports qualified module names, as well as 66 | ; # Unix pattern matching. 67 | ; ignored-modules= 68 | 69 | ; # Python code to execute, usually for sys.path manipulation such as 70 | ; # pygtk.require(). 71 | ; #init-hook= 72 | 73 | ; # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 74 | ; # number of processors available to use, and will cap the count on Windows to 75 | ; # avoid hangs. 76 | ; jobs=1 77 | 78 | ; # Control the amount of potential inferred values when inferring a single 79 | ; # object. This can help the performance when dealing with large functions or 80 | ; # complex, nested conditions. 81 | ; limit-inference-results=100 82 | 83 | ; # List of plugins (as comma separated values of python module names) to load, 84 | ; # usually to register additional checkers. 85 | ; load-plugins= 86 | 87 | ; # Pickle collected data for later comparisons. 88 | ; persistent=yes 89 | 90 | ; # Resolve imports to .pyi stubs if available. May reduce no-member messages and 91 | ; # increase not-an-iterable messages. 92 | ; prefer-stubs=no 93 | 94 | ; # Minimum Python version to use for version dependent checks. Will default to 95 | ; # the version used to run pylint. 96 | ; py-version=3.13 97 | 98 | ; # Discover python modules and packages in the file system subtree. 99 | ; recursive=no 100 | 101 | ; # Add paths to the list of the source roots. Supports globbing patterns. The 102 | ; # source root is an absolute path or a path relative to the current working 103 | ; # directory used to determine a package namespace for modules located under the 104 | ; # source root. 105 | ; source-roots= 106 | 107 | ; # When enabled, pylint would attempt to guess common misconfiguration and emit 108 | ; # user-friendly hints instead of false-positive error messages. 109 | ; suggestion-mode=yes 110 | 111 | ; # Allow loading of arbitrary C extensions. Extensions are imported into the 112 | ; # active Python interpreter and may run arbitrary code. 113 | ; unsafe-load-any-extension=no 114 | 115 | ; # In verbose mode, extra non-checker-related info will be displayed. 116 | ; #verbose= 117 | 118 | 119 | ; [BASIC] 120 | 121 | ; # Naming style matching correct argument names. 122 | ; argument-naming-style=snake_case 123 | 124 | ; # Regular expression matching correct argument names. Overrides argument- 125 | ; # naming-style. If left empty, argument names will be checked with the set 126 | ; # naming style. 127 | ; #argument-rgx= 128 | 129 | ; # Naming style matching correct attribute names. 130 | ; attr-naming-style=snake_case 131 | 132 | ; # Regular expression matching correct attribute names. Overrides attr-naming- 133 | ; # style. If left empty, attribute names will be checked with the set naming 134 | ; # style. 135 | ; #attr-rgx= 136 | 137 | ; # Bad variable names which should always be refused, separated by a comma. 138 | ; bad-names=foo, 139 | ; bar, 140 | ; baz, 141 | ; toto, 142 | ; tutu, 143 | ; tata 144 | 145 | ; # Bad variable names regexes, separated by a comma. If names match any regex, 146 | ; # they will always be refused 147 | ; bad-names-rgxs= 148 | 149 | ; # Naming style matching correct class attribute names. 150 | ; class-attribute-naming-style=any 151 | 152 | ; # Regular expression matching correct class attribute names. Overrides class- 153 | ; # attribute-naming-style. If left empty, class attribute names will be checked 154 | ; # with the set naming style. 155 | ; #class-attribute-rgx= 156 | 157 | ; # Naming style matching correct class constant names. 158 | ; class-const-naming-style=UPPER_CASE 159 | 160 | ; # Regular expression matching correct class constant names. Overrides class- 161 | ; # const-naming-style. If left empty, class constant names will be checked with 162 | ; # the set naming style. 163 | ; #class-const-rgx= 164 | 165 | ; # Naming style matching correct class names. 166 | ; class-naming-style=PascalCase 167 | 168 | ; # Regular expression matching correct class names. Overrides class-naming- 169 | ; # style. If left empty, class names will be checked with the set naming style. 170 | ; #class-rgx= 171 | 172 | ; # Naming style matching correct constant names. 173 | ; const-naming-style=UPPER_CASE 174 | 175 | ; # Regular expression matching correct constant names. Overrides const-naming- 176 | ; # style. If left empty, constant names will be checked with the set naming 177 | ; # style. 178 | ; #const-rgx= 179 | 180 | ; # Minimum line length for functions/classes that require docstrings, shorter 181 | ; # ones are exempt. 182 | ; docstring-min-length=-1 183 | 184 | ; # Naming style matching correct function names. 185 | ; function-naming-style=snake_case 186 | 187 | ; # Regular expression matching correct function names. Overrides function- 188 | ; # naming-style. If left empty, function names will be checked with the set 189 | ; # naming style. 190 | ; #function-rgx= 191 | 192 | ; # Good variable names which should always be accepted, separated by a comma. 193 | ; good-names=i, 194 | ; j, 195 | ; k, 196 | ; ex, 197 | ; Run, 198 | ; _ 199 | 200 | ; # Good variable names regexes, separated by a comma. If names match any regex, 201 | ; # they will always be accepted 202 | ; good-names-rgxs= 203 | 204 | ; # Include a hint for the correct naming format with invalid-name. 205 | ; include-naming-hint=no 206 | 207 | ; # Naming style matching correct inline iteration names. 208 | ; inlinevar-naming-style=any 209 | 210 | ; # Regular expression matching correct inline iteration names. Overrides 211 | ; # inlinevar-naming-style. If left empty, inline iteration names will be checked 212 | ; # with the set naming style. 213 | ; #inlinevar-rgx= 214 | 215 | ; # Naming style matching correct method names. 216 | ; method-naming-style=snake_case 217 | 218 | ; # Regular expression matching correct method names. Overrides method-naming- 219 | ; # style. If left empty, method names will be checked with the set naming style. 220 | ; #method-rgx= 221 | 222 | ; # Naming style matching correct module names. 223 | ; module-naming-style=snake_case 224 | 225 | ; # Regular expression matching correct module names. Overrides module-naming- 226 | ; # style. If left empty, module names will be checked with the set naming style. 227 | ; #module-rgx= 228 | 229 | ; # Colon-delimited sets of names that determine each other's naming style when 230 | ; # the name regexes allow several styles. 231 | ; name-group= 232 | 233 | ; # Regular expression which should only match function or class names that do 234 | ; # not require a docstring. 235 | ; no-docstring-rgx=^_ 236 | 237 | ; # List of decorators that produce properties, such as abc.abstractproperty. Add 238 | ; # to this list to register other decorators that produce valid properties. 239 | ; # These decorators are taken in consideration only for invalid-name. 240 | ; property-classes=abc.abstractproperty 241 | 242 | ; # Regular expression matching correct type alias names. If left empty, type 243 | ; # alias names will be checked with the set naming style. 244 | ; #typealias-rgx= 245 | 246 | ; # Regular expression matching correct type variable names. If left empty, type 247 | ; # variable names will be checked with the set naming style. 248 | ; #typevar-rgx= 249 | 250 | ; # Naming style matching correct variable names. 251 | ; variable-naming-style=snake_case 252 | 253 | ; # Regular expression matching correct variable names. Overrides variable- 254 | ; # naming-style. If left empty, variable names will be checked with the set 255 | ; # naming style. 256 | ; #variable-rgx= 257 | 258 | 259 | ; [CLASSES] 260 | 261 | ; # Warn about protected attribute access inside special methods 262 | ; check-protected-access-in-special-methods=no 263 | 264 | ; # List of method names used to declare (i.e. assign) instance attributes. 265 | ; defining-attr-methods=__init__, 266 | ; __new__, 267 | ; setUp, 268 | ; asyncSetUp, 269 | ; __post_init__ 270 | 271 | ; # List of member names, which should be excluded from the protected access 272 | ; # warning. 273 | ; exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 274 | 275 | ; # List of valid names for the first argument in a class method. 276 | ; valid-classmethod-first-arg=cls 277 | 278 | ; # List of valid names for the first argument in a metaclass class method. 279 | ; valid-metaclass-classmethod-first-arg=mcs 280 | 281 | 282 | ; [DESIGN] 283 | 284 | ; # List of regular expressions of class ancestor names to ignore when counting 285 | ; # public methods (see R0903) 286 | ; exclude-too-few-public-methods= 287 | 288 | ; # List of qualified class names to ignore when counting class parents (see 289 | ; # R0901) 290 | ; ignored-parents= 291 | 292 | ; # Maximum number of arguments for function / method. 293 | ; max-args=5 294 | 295 | ; # Maximum number of attributes for a class (see R0902). 296 | ; max-attributes=7 297 | 298 | ; # Maximum number of boolean expressions in an if statement (see R0916). 299 | ; max-bool-expr=5 300 | 301 | ; # Maximum number of branch for function / method body. 302 | ; max-branches=12 303 | 304 | ; # Maximum number of locals for function / method body. 305 | ; max-locals=15 306 | 307 | ; # Maximum number of parents for a class (see R0901). 308 | ; max-parents=7 309 | 310 | ; # Maximum number of positional arguments for function / method. 311 | ; max-positional-arguments=5 312 | 313 | ; # Maximum number of public methods for a class (see R0904). 314 | ; max-public-methods=20 315 | 316 | ; # Maximum number of return / yield for function / method body. 317 | ; max-returns=6 318 | 319 | ; # Maximum number of statements in function / method body. 320 | ; max-statements=50 321 | 322 | ; # Minimum number of public methods for a class (see R0903). 323 | ; min-public-methods=2 324 | 325 | 326 | ; [EXCEPTIONS] 327 | 328 | ; # Exceptions that will emit a warning when caught. 329 | ; overgeneral-exceptions=builtins.BaseException,builtins.Exception 330 | 331 | 332 | ; [FORMAT] 333 | 334 | ; # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 335 | ; expected-line-ending-format= 336 | 337 | ; # Regexp for a line that is allowed to be longer than the limit. 338 | ; ignore-long-lines=^\s*(# )??$ 339 | 340 | ; # Number of spaces of indent required inside a hanging or continued line. 341 | ; indent-after-paren=4 342 | 343 | ; # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 344 | ; # tab). 345 | ; indent-string=' ' 346 | 347 | ; # Maximum number of characters on a single line. 348 | ; max-line-length=100 349 | 350 | ; # Maximum number of lines in a module. 351 | ; max-module-lines=1000 352 | 353 | ; # Allow the body of a class to be on the same line as the declaration if body 354 | ; # contains single statement. 355 | ; single-line-class-stmt=no 356 | 357 | ; # Allow the body of an if to be on the same line as the test if there is no 358 | ; # else. 359 | ; single-line-if-stmt=no 360 | 361 | 362 | ; [IMPORTS] 363 | 364 | ; # List of modules that can be imported at any level, not just the top level 365 | ; # one. 366 | ; allow-any-import-level= 367 | 368 | ; # Allow explicit reexports by alias from a package __init__. 369 | ; allow-reexport-from-package=no 370 | 371 | ; # Allow wildcard imports from modules that define __all__. 372 | ; allow-wildcard-with-all=no 373 | 374 | ; # Deprecated modules which should not be used, separated by a comma. 375 | ; deprecated-modules= 376 | 377 | ; # Output a graph (.gv or any supported image format) of external dependencies 378 | ; # to the given file (report RP0402 must not be disabled). 379 | ; ext-import-graph= 380 | 381 | ; # Output a graph (.gv or any supported image format) of all (i.e. internal and 382 | ; # external) dependencies to the given file (report RP0402 must not be 383 | ; # disabled). 384 | ; import-graph= 385 | 386 | ; # Output a graph (.gv or any supported image format) of internal dependencies 387 | ; # to the given file (report RP0402 must not be disabled). 388 | ; int-import-graph= 389 | 390 | ; # Force import order to recognize a module as part of the standard 391 | ; # compatibility libraries. 392 | ; known-standard-library= 393 | 394 | ; # Force import order to recognize a module as part of a third party library. 395 | ; known-third-party=enchant 396 | 397 | ; # Couples of modules and preferred modules, separated by a comma. 398 | ; preferred-modules= 399 | 400 | 401 | ; [LOGGING] 402 | 403 | ; # The type of string formatting that logging methods do. `old` means using % 404 | ; # formatting, `new` is for `{}` formatting. 405 | ; logging-format-style=old 406 | 407 | ; # Logging modules to check that the string format arguments are in logging 408 | ; # function parameter format. 409 | ; logging-modules=logging 410 | 411 | 412 | ; [MESSAGES CONTROL] 413 | 414 | ; # Only show warnings with the listed confidence levels. Leave empty to show 415 | ; # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 416 | ; # UNDEFINED. 417 | ; confidence=HIGH, 418 | ; CONTROL_FLOW, 419 | ; INFERENCE, 420 | ; INFERENCE_FAILURE, 421 | ; UNDEFINED 422 | 423 | ; # Disable the message, report, category or checker with the given id(s). You 424 | ; # can either give multiple identifiers separated by comma (,) or put this 425 | ; # option multiple times (only on the command line, not in the configuration 426 | ; # file where it should appear only once). You can also use "--disable=all" to 427 | ; # disable everything first and then re-enable specific checks. For example, if 428 | ; # you want to run only the similarities checker, you can use "--disable=all 429 | ; # --enable=similarities". If you want to run only the classes checker, but have 430 | ; # no Warning level messages displayed, use "--disable=all --enable=classes 431 | ; # --disable=W". 432 | ; disable=raw-checker-failed, 433 | ; bad-inline-option, 434 | ; locally-disabled, 435 | ; file-ignored, 436 | ; suppressed-message, 437 | ; useless-suppression, 438 | ; deprecated-pragma, 439 | ; use-symbolic-message-instead, 440 | ; use-implicit-booleaness-not-comparison-to-string, 441 | ; use-implicit-booleaness-not-comparison-to-zero 442 | 443 | ; # Enable the message, report, category or checker with the given id(s). You can 444 | ; # either give multiple identifier separated by comma (,) or put this option 445 | ; # multiple time (only on the command line, not in the configuration file where 446 | ; # it should appear only once). See also the "--disable" option for examples. 447 | ; enable= 448 | 449 | 450 | ; [METHOD_ARGS] 451 | 452 | ; # List of qualified names (i.e., library.method) which require a timeout 453 | ; # parameter e.g. 'requests.api.get,requests.api.post' 454 | ; timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 455 | 456 | 457 | ; [MISCELLANEOUS] 458 | 459 | ; # List of note tags to take in consideration, separated by a comma. 460 | ; notes=FIXME, 461 | ; XXX, 462 | ; TODO 463 | 464 | ; # Regular expression of note tags to take in consideration. 465 | ; notes-rgx= 466 | 467 | 468 | ; [REFACTORING] 469 | 470 | ; # Maximum number of nested blocks for function / method body 471 | ; max-nested-blocks=5 472 | 473 | ; # Complete name of functions that never returns. When checking for 474 | ; # inconsistent-return-statements if a never returning function is called then 475 | ; # it will be considered as an explicit return statement and no message will be 476 | ; # printed. 477 | ; never-returning-functions=sys.exit,argparse.parse_error 478 | 479 | ; # Let 'consider-using-join' be raised when the separator to join on would be 480 | ; # non-empty (resulting in expected fixes of the type: ``"- " + " - 481 | ; # ".join(items)``) 482 | ; suggest-join-with-non-empty-separator=yes 483 | 484 | 485 | ; [REPORTS] 486 | 487 | ; # Python expression which should return a score less than or equal to 10. You 488 | ; # have access to the variables 'fatal', 'error', 'warning', 'refactor', 489 | ; # 'convention', and 'info' which contain the number of messages in each 490 | ; # category, as well as 'statement' which is the total number of statements 491 | ; # analyzed. This score is used by the global evaluation report (RP0004). 492 | ; evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 493 | 494 | ; # Template used to display messages. This is a python new-style format string 495 | ; # used to format the message information. See doc for all details. 496 | ; msg-template= 497 | 498 | ; # Set the output format. Available formats are: text, parseable, colorized, 499 | ; # json2 (improved json format), json (old json format) and msvs (visual 500 | ; # studio). You can also give a reporter class, e.g. 501 | ; # mypackage.mymodule.MyReporterClass. 502 | ; #output-format= 503 | 504 | ; # Tells whether to display a full report or only the messages. 505 | ; reports=no 506 | 507 | ; # Activate the evaluation score. 508 | ; score=yes 509 | 510 | 511 | ; [SIMILARITIES] 512 | 513 | ; # Comments are removed from the similarity computation 514 | ; ignore-comments=yes 515 | 516 | ; # Docstrings are removed from the similarity computation 517 | ; ignore-docstrings=yes 518 | 519 | ; # Imports are removed from the similarity computation 520 | ; ignore-imports=yes 521 | 522 | ; # Signatures are removed from the similarity computation 523 | ; ignore-signatures=yes 524 | 525 | ; # Minimum lines number of a similarity. 526 | ; min-similarity-lines=4 527 | 528 | 529 | ; [SPELLING] 530 | 531 | ; # Limits count of emitted suggestions for spelling mistakes. 532 | ; max-spelling-suggestions=4 533 | 534 | ; # Spelling dictionary name. No available dictionaries : You need to install 535 | ; # both the python package and the system dependency for enchant to work. 536 | ; spelling-dict= 537 | 538 | ; # List of comma separated words that should be considered directives if they 539 | ; # appear at the beginning of a comment and should not be checked. 540 | ; spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 541 | 542 | ; # List of comma separated words that should not be checked. 543 | ; spelling-ignore-words= 544 | 545 | ; # A path to a file that contains the private dictionary; one word per line. 546 | ; spelling-private-dict-file= 547 | 548 | ; # Tells whether to store unknown words to the private dictionary (see the 549 | ; # --spelling-private-dict-file option) instead of raising a message. 550 | ; spelling-store-unknown-words=no 551 | 552 | 553 | ; [STRING] 554 | 555 | ; # This flag controls whether inconsistent-quotes generates a warning when the 556 | ; # character used as a quote delimiter is used inconsistently within a module. 557 | ; check-quote-consistency=no 558 | 559 | ; # This flag controls whether the implicit-str-concat should generate a warning 560 | ; # on implicit string concatenation in sequences defined over several lines. 561 | ; check-str-concat-over-line-jumps=no 562 | 563 | 564 | ; [TYPECHECK] 565 | 566 | ; # List of decorators that produce context managers, such as 567 | ; # contextlib.contextmanager. Add to this list to register other decorators that 568 | ; # produce valid context managers. 569 | ; contextmanager-decorators=contextlib.contextmanager 570 | 571 | ; # List of members which are set dynamically and missed by pylint inference 572 | ; # system, and so shouldn't trigger E1101 when accessed. Python regular 573 | ; # expressions are accepted. 574 | ; generated-members= 575 | 576 | ; # Tells whether to warn about missing members when the owner of the attribute 577 | ; # is inferred to be None. 578 | ; ignore-none=yes 579 | 580 | ; # This flag controls whether pylint should warn about no-member and similar 581 | ; # checks whenever an opaque object is returned when inferring. The inference 582 | ; # can return multiple potential results while evaluating a Python object, but 583 | ; # some branches might not be evaluated, which results in partial inference. In 584 | ; # that case, it might be useful to still emit no-member and other checks for 585 | ; # the rest of the inferred objects. 586 | ; ignore-on-opaque-inference=yes 587 | 588 | ; # List of symbolic message names to ignore for Mixin members. 589 | ; ignored-checks-for-mixins=no-member, 590 | ; not-async-context-manager, 591 | ; not-context-manager, 592 | ; attribute-defined-outside-init 593 | 594 | ; # List of class names for which member attributes should not be checked (useful 595 | ; # for classes with dynamically set attributes). This supports the use of 596 | ; # qualified names. 597 | ; ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 598 | 599 | ; # Show a hint with possible names when a member name was not found. The aspect 600 | ; # of finding the hint is based on edit distance. 601 | ; missing-member-hint=yes 602 | 603 | ; # The minimum edit distance a name should have in order to be considered a 604 | ; # similar match for a missing member name. 605 | ; missing-member-hint-distance=1 606 | 607 | ; # The total number of similar names that should be taken in consideration when 608 | ; # showing a hint for a missing member. 609 | ; missing-member-max-choices=1 610 | 611 | ; # Regex pattern to define which classes are considered mixins. 612 | ; mixin-class-rgx=.*[Mm]ixin 613 | 614 | ; # List of decorators that change the signature of a decorated function. 615 | ; signature-mutators= 616 | 617 | 618 | ; [VARIABLES] 619 | 620 | ; # List of additional names supposed to be defined in builtins. Remember that 621 | ; # you should avoid defining new builtins when possible. 622 | ; additional-builtins= 623 | 624 | ; # Tells whether unused global variables should be treated as a violation. 625 | ; allow-global-unused-variables=yes 626 | 627 | ; # List of names allowed to shadow builtins 628 | ; allowed-redefined-builtins= 629 | 630 | ; # List of strings which can identify a callback function by name. A callback 631 | ; # name must start or end with one of those strings. 632 | ; callbacks=cb_, 633 | ; _cb 634 | 635 | ; # A regular expression matching the name of dummy variables (i.e. expected to 636 | ; # not be used). 637 | ; dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 638 | 639 | ; # Argument names that match this expression will be ignored. 640 | ; ignored-argument-names=_.*|^ignored_|^unused_ 641 | 642 | ; # Tells whether we should check for unused import in __init__ files. 643 | ; init-import=no 644 | 645 | ; # List of qualified module names which can have objects that can redefine 646 | ; # builtins. 647 | ; redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | support@varphi-lang.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to Varphi 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [I Have a Question](#i-have-a-question) 18 | - [I Want To Contribute](#i-want-to-contribute) 19 | - [Reporting Bugs](#reporting-bugs) 20 | - [Suggesting Enhancements](#suggesting-enhancements) 21 | - [Your First Code Contribution](#your-first-code-contribution) 22 | - [Improving The Documentation](#improving-the-documentation) 23 | - [Styleguides](#styleguides) 24 | - [Commit Messages](#commit-messages) 25 | - [Join The Project Team](#join-the-project-team) 26 | 27 | 28 | 29 | ## I Have a Question 30 | 31 | > If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/varphi-lang/varphi/blob/main/README.md). 32 | 33 | Before you ask a question, it is best to search for existing [Issues](https://github.com/varphi-lang/varphi/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 34 | 35 | If you then still feel the need to ask a question and need clarification, we recommend the following: 36 | 37 | - Open an [Issue](https://github.com/varphi-lang/varphi/issues/new). 38 | - Provide as much context as you can about what you're running into. 39 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 40 | 41 | We will then take care of the issue as soon as possible. 42 | 43 | 57 | 58 | ## I Want To Contribute 59 | 60 | > ### Legal Notice 61 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence. 62 | 63 | ### Reporting Bugs 64 | 65 | 66 | #### Before Submitting a Bug Report 67 | 68 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 69 | 70 | - Make sure that you are using the latest version. 71 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/varphi-lang/varphi/blob/main/README.md). If you are looking for support, you might want to check [this section](#i-have-a-question)). 72 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/varphi-lang/varphi/issues?q=label%3Abug). 73 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 74 | - Collect information about the bug: 75 | - Stack trace (Traceback) 76 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 77 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 78 | - Possibly your input and the output 79 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 80 | 81 | 82 | #### How Do I Submit a Good Bug Report? 83 | 84 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . 85 | 86 | 87 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 88 | 89 | - Open an [Issue](https://github.com/varphi-lang/varphi/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 90 | - Explain the behavior you would expect and the actual behavior. 91 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 92 | - Provide the information you collected in the previous section. 93 | 94 | Once it's filed: 95 | 96 | - The project team will label the issue accordingly. 97 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 98 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 99 | 100 | 101 | 102 | 103 | ### Suggesting Enhancements 104 | 105 | This section guides you through submitting an enhancement suggestion for Varphi, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 106 | 107 | 108 | #### Before Submitting an Enhancement 109 | 110 | - Make sure that you are using the latest version. 111 | - Read the [documentation](https://github.com/varphi-lang/varphi/blob/main/README.md) carefully and find out if the functionality is already covered, maybe by an individual configuration. 112 | - Perform a [search](https://github.com/varphi-lang/varphi/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 113 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 114 | 115 | 116 | #### How Do I Submit a Good Enhancement Suggestion? 117 | 118 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/varphi-lang/varphi/issues). 119 | 120 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 121 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 122 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 123 | - You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux. 124 | - **Explain why this enhancement would be useful** to most Varphi users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 125 | 126 | 127 | 128 | ### Structure of the Varphi Codebase 129 | 130 | An overview of the Varphi codebase is available [here](https://docs.varphi-lang.com/contributing/the-codebase) 131 | 132 | ## Attribution 133 | This guide is based on the [contributing.md](https://contributing.md/generator)! 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Hassan El-Sheikha 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) 2 | EXE_NAME ?= vpi 3 | VENV_ACTIVATE = Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process; venv\\Scripts\\activate 4 | ANTLR_GRAMMAR_FILE = Varphi.g4 5 | ANTLR_VERSION = 4.13.2 6 | ANTLR_JAR_FILE = antlr-$(ANTLR_VERSION)-complete.jar 7 | ANTLR_JAR_DOWNLOAD_LINK = https://www.antlr.org/download/$(ANTLR_JAR_FILE) 8 | ANTLR_OUTPUT_DIR = varphi\parsing 9 | 10 | $(ANTLR_JAR_FILE): 11 | powershell -Command "Invoke-WebRequest -Uri $(ANTLR_JAR_DOWNLOAD_LINK) -OutFile $(ANTLR_JAR_FILE)" 12 | 13 | parsing: $(ANTLR_JAR_FILE) 14 | powershell -Command "java -jar $(ANTLR_JAR_FILE) -Dlanguage=Python3 -o $(ANTLR_OUTPUT_DIR) $(ANTLR_GRAMMAR_FILE)" 15 | 16 | venv: 17 | python -m venv venv 18 | 19 | install: venv parsing 20 | powershell -Command "$(VENV_ACTIVATE); pip install ." 21 | 22 | test: install 23 | powershell -Command "$(VENV_ACTIVATE); pip install pytest pytest-cov; pytest --cov=varphi tests/" 24 | 25 | lint: install 26 | powershell -Command "$(VENV_ACTIVATE); pip install pylint; pylint varphi/" 27 | 28 | executable: install 29 | powershell -Command "$(VENV_ACTIVATE); pip install pyinstaller; pyinstaller --onefile --optimize 2 varphi_interpreter/vpi.py --distpath ./bin --noconfirm --hidden-import varphi --hidden-import argparse --name $(EXE_NAME)" 30 | 31 | clean: 32 | powershell -Command "Remove-Item -Recurse -Force *.pyc, __pycache__, varphi\\parsing\\Varphi*.py, varphi\\parsing\\*.tokens, varphi\\parsing\\*.interp, vpi.spec, venv, bin, varphi.egg-info, build, .pytest_cache, antlr-4.13.2-complete.jar, .coverage -ErrorAction SilentlyContinue; exit 0" 33 | 34 | 35 | else 36 | EXE_NAME ?= vpi 37 | VENV_ACTIVATE = . venv/bin/activate 38 | ANTLR_GRAMMAR_FILE = Varphi.g4 39 | ANTLR_VERSION = 4.13.2 40 | ANTLR_JAR_FILE = antlr-$(ANTLR_VERSION)-complete.jar 41 | ANTLR_JAR_DOWNLOAD_LINK = https://www.antlr.org/download/$(ANTLR_JAR_FILE) 42 | ANTLR_OUTPUT_DIR = varphi/parsing 43 | 44 | $(ANTLR_JAR_FILE): 45 | curl -L $(ANTLR_JAR_DOWNLOAD_LINK) -o $(ANTLR_JAR_FILE) 46 | 47 | parsing: $(ANTLR_JAR_FILE) 48 | java -jar $(ANTLR_JAR_FILE) -Dlanguage=Python3 -o $(ANTLR_OUTPUT_DIR) $(ANTLR_GRAMMAR_FILE) 49 | 50 | venv: 51 | python -m venv venv 52 | 53 | install: venv parsing 54 | $(VENV_ACTIVATE) && pip install . 55 | 56 | test: install 57 | $(VENV_ACTIVATE) && pip install pytest pytest-cov && pytest --cov=varphi tests/ 58 | 59 | lint: install 60 | $(VENV_ACTIVATE) && pip install pylint && pylint varphi/ 61 | 62 | executable: install 63 | $(VENV_ACTIVATE) && pip install pyinstaller && pyinstaller --onefile --optimize 2 varphi_interpreter/vpi.py --distpath ./bin --noconfirm --hidden-import varphi --hidden-import argparse --name $(EXE_NAME) 64 | 65 | clean: 66 | find . -type f -name "*.pyc" -exec rm -f {} +; \ 67 | find . -type d -name "__pycache__" -exec rm -rf {} +; 68 | rm -f varphi/parsing/Varphi*.py varphi/parsing/*.tokens varphi/parsing/*.interp vpi.spec || echo "Files not found, skipping..." 69 | rm -rf venv bin varphi.egg-info build .pytest_cache || echo "Directories not found, skipping..." 70 | rm -f antlr-4.13.2-complete.jar .coverage || echo "File not found, skipping..." 71 | endif 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the main source code repository for Varphi. It contains the compiler, interpreter, standard library, and documentation. 2 | 3 | ## Why Varphi? 4 | 5 | * **Productivity:** Designing Turing programs in longhand is tedious and can lead to errors. Having a dedicated, maintained programming langugage for Turing programs makes development times much faster, and Varphi debugging tools ensure programs are correct. 6 | * **Compactness:** Just one executable (the interpreter, `vpi`) is all you need to get started with Varphi on any operating system. 7 | * **Support:** Varphi is constantly being maintained and new features are being added. 8 | 9 | ## Documentation 10 | 11 | Complete documentation is available [here](https://docs.varphi-lang.com/). 12 | 13 | ## Contributing 14 | 15 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) 16 | 17 | ## License 18 | 19 | Varphi is primarily distributed under the terms of the BSD 3-Clause License. 20 | -------------------------------------------------------------------------------- /Varphi.g4: -------------------------------------------------------------------------------- 1 | grammar Varphi; 2 | 3 | // Parser rules 4 | program : line* EOF; 5 | line : STATE TAPE_CHARACTER STATE TAPE_CHARACTER HEAD_DIRECTION; 6 | 7 | // Lexer rules 8 | fragment LEFT : 'L'; 9 | fragment RIGHT : 'R'; 10 | fragment TALLY : '1'; 11 | fragment BLANK : '0'; 12 | 13 | STATE : 'q'[a-zA-Z0-9_]+; 14 | TAPE_CHARACTER : TALLY | BLANK; 15 | HEAD_DIRECTION : LEFT | RIGHT; 16 | 17 | // Single-line comment (starts with // and ends at the end of the line) 18 | COMMENT : '//' ~[\r\n]* -> skip; 19 | 20 | // Multi-line comment (starts with /* and ends with */, can span multiple lines) 21 | MULTI_COMMENT : '/*' .*? '*/' -> skip; 22 | 23 | // Skip unnecessary whitespaces 24 | WHITESPACE : [ \t\r\n]+ -> skip; -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="varphi", 5 | version="0.0.0", 6 | author="Hassan El-Sheikha", 7 | packages=find_packages(), 8 | classifiers=[ 9 | "Programming Language :: Python :: 3", 10 | "License :: OSI Approved :: BSD License", 11 | "Operating System :: OS Independent", 12 | ], 13 | include_package_data=True, 14 | install_requires=[ 15 | "antlr4-python3-runtime==4.13.2" 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/varphi-lang/varphi/2ad87ef43924af57aca2b087b2a4143b9246a75f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from io import StringIO 3 | 4 | from varphi.compilation.varphi_translator import VarphiTranslatorCLITarget 5 | from varphi.compilation.core import varphi_to_python 6 | 7 | def test_add1(): 8 | program = """ 9 | q0 1 q0 1 R 10 | q0 0 qf 1 L 11 | """ 12 | sys.stdin = StringIO("1\n") 13 | sys.stdout = captured_output = StringIO() 14 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 15 | assert captured_output.getvalue().strip().strip('0') == "11" 16 | 17 | sys.stdin = StringIO("11\n") 18 | sys.stdout = captured_output = StringIO() 19 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 20 | assert captured_output.getvalue().strip().strip('0') == "111" 21 | 22 | sys.stdin = StringIO("110\n") 23 | sys.stdout = captured_output = StringIO() 24 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 25 | assert captured_output.getvalue().strip().strip('0') == "111" 26 | 27 | 28 | def test_add(): 29 | program = """ 30 | q0 0 qh 0 R // Nothing from first number remaining; halt 31 | q0 1 q1 0 R // Change first tally (to the left) of the first number to a 0 (we'll "move" this to the end of the second number) 32 | q1 1 q1 1 R // Skip the rest of the tallies of the first number 33 | q1 0 q2 0 R // Found middle blank 34 | q2 1 q2 1 R // Skip through the tallies of the second number 35 | q2 0 q3 1 L // Found end of second number; add a tally here 36 | q3 1 q3 1 L // Skip through tallies of second number (moving left) 37 | q3 0 q4 0 L // Found middle blank 38 | q4 1 q4 1 L // Skip through tallies of first number 39 | q4 0 q0 0 R // Found end of first number (it will be 1 less than before now), switch to q0 and repeat the process 40 | """ 41 | sys.stdin = StringIO("101\n") 42 | sys.stdout = captured_output = StringIO() 43 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 44 | assert captured_output.getvalue().strip().strip('0') == "11" 45 | 46 | sys.stdin = StringIO("1101\n") 47 | sys.stdout = captured_output = StringIO() 48 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 49 | assert captured_output.getvalue().strip().strip('0') == "111" 50 | 51 | sys.stdin = StringIO("1011\n") 52 | sys.stdout = captured_output = StringIO() 53 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 54 | assert captured_output.getvalue().strip().strip('0') == "111" 55 | 56 | sys.stdin = StringIO("11011\n") 57 | sys.stdout = captured_output = StringIO() 58 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 59 | assert captured_output.getvalue().strip().strip('0') == "1111" 60 | 61 | 62 | def test_mult2(): 63 | program = """ 64 | q0 1 q1 0 R // "Mark" the current tally from the first number by changing it to 0 65 | q0 0 qmerge 0 R // No more tallies from the first number, "merge" the two halves of the doubled number 66 | q1 1 q1 1 R // Skip over the rest of the tallies from the first number 67 | q1 0 q2 0 R // We've reached the middle blank 68 | q2 1 q2 1 R // Skip over the tallies from the second number 69 | q2 0 q3 1 L // We've reached the end of the first number, place a tally here 70 | q3 1 q3 1 L // Skip over the tallies from the second number (moving left now) 71 | q3 0 q4 0 L // We've reached the middle blank 72 | q4 1 q4 1 L // Skip over the tallies from the first number (moving left now) 73 | q4 0 q0 1 R // We've reached the beginning of the first number. Return the tally that we replaced with a blank, and point the head to the next tally of the first number and restart 74 | 75 | qmerge 1 qmerge1 1 L // When we hit qmerge, the head will be pointed at the first tally from the second half. Shift to the left to go to the middle blank 76 | qmerge1 0 qmerge2 1 R // Change the middle blank to a tally 77 | qmerge2 1 qmerge2 1 R // Skip over the tallies of the second half 78 | qmerge2 0 qmerge3 0 L // We've reached the end of the second half, but there's one more tally at the far right of the second half. Remove it 79 | qmerge3 1 qh 0 R // Replace the last tally of the second half with a blank 80 | """ 81 | sys.stdin = StringIO("1\n") 82 | sys.stdout = captured_output = StringIO() 83 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 84 | assert captured_output.getvalue().strip().strip('0') == "11" 85 | 86 | sys.stdin = StringIO("11\n") 87 | sys.stdout = captured_output = StringIO() 88 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 89 | assert captured_output.getvalue().strip().strip('0') == "1111" 90 | 91 | def test_nonnegative_subtraction(): 92 | program = """ 93 | q0 1 q0 1 R // Keep scanning through tallies of the first number until you see the middle blank 94 | q0 0 q1 0 R // Middle blank found 95 | q1 1 q1 1 R // Go through all the tallies of the second number 96 | q1 0 q2 0 L // Found end of second number 97 | q2 0 qh 0 R // If the end of the last number is a blank, there's nothing left from the second number 98 | q2 1 q3 0 L // If the end of the last number is a tally, take out the last tally and replace with a blank 99 | q3 1 q3 1 L // Go through the remaining tallies of the second number until you see the middle blank 100 | q3 0 q4 0 L // Middle blank found 101 | q4 1 q4 1 L // Go through the tallies of the first number 102 | q4 0 q5 0 R // Found end of the first number (to the left) 103 | q5 1 q0 0 R // Take out first tally of the first number to the left (this subtracts 1); switch to q0 and repeat the process 104 | """ 105 | sys.stdin = StringIO("11101\n") 106 | sys.stdout = captured_output = StringIO() 107 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 108 | assert captured_output.getvalue().strip().strip('0') == "11" 109 | 110 | sys.stdin = StringIO("1101\n") 111 | sys.stdout = captured_output = StringIO() 112 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 113 | assert captured_output.getvalue().strip().strip('0') == "1" 114 | 115 | 116 | def test_rock_paper_scissors(): 117 | program = """ 118 | qUnknown 1 qRock 0 R 119 | qRock 1 qPaper 0 R 120 | qPaper 1 qScissors 0 R 121 | 122 | qRock 0 qRockUnknown 0 R 123 | qPaper 0 qPaperUnknown 0 R 124 | qScissors 0 qScissorsUnknown 0 R 125 | 126 | qRockUnknown 1 qRockRock 0 R 127 | qPaperUnknown 1 qPaperRock 0 R 128 | qScissorsUnknown 1 qScissorsRock 0 R 129 | 130 | qRockRock 1 qRockPaper 0 R 131 | qRockPaper 1 qRockScissors 0 R 132 | 133 | qPaperRock 1 qPaperPaper 0 R 134 | qPaperPaper 1 qPaperScissors 0 R 135 | 136 | qScissorsRock 1 qScissorsPaper 0 R 137 | qScissorsPaper 1 qScissorsScissors 0 R 138 | 139 | qRockRock 0 qTie 0 R 140 | qRockPaper 0 qPlayer2Won 0 R 141 | qRockScissors 0 qPlayer1Won 0 R 142 | qPaperRock 0 qPlayer1Won 0 R 143 | qPaperPaper 0 qTie 0 R 144 | qPaperScissors 0 qPlayer2Won 0 R 145 | qScissorsRock 0 qPlayer2Won 0 R 146 | qScissorsPaper 0 qPlayer1Won 0 R 147 | qScissorsScissors 0 qTie 0 R 148 | 149 | qTie 0 qWrite1 0 R 150 | qPlayer1Won 0 qWrite2 0 R 151 | qPlayer2Won 0 qWrite3 0 R 152 | 153 | qWrite1 0 qHalt 1 R 154 | qWrite2 0 qWrite1 1 R 155 | qWrite3 0 qWrite2 1 R 156 | """ 157 | sys.stdin = StringIO("101\n") # Rock v Rock 158 | sys.stdout = captured_output = StringIO() 159 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 160 | assert captured_output.getvalue().strip().strip('0') == "1" # Tie 161 | 162 | sys.stdin = StringIO("1011\n") # Rock v Paper 163 | sys.stdout = captured_output = StringIO() 164 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 165 | assert captured_output.getvalue().strip().strip('0') == "111" # Player 2 won 166 | 167 | sys.stdin = StringIO("10111\n") # Rock v Scissors 168 | sys.stdout = captured_output = StringIO() 169 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 170 | assert captured_output.getvalue().strip().strip('0') == "11" # Player 1 won 171 | 172 | sys.stdin = StringIO("1101\n") 173 | sys.stdout = captured_output = StringIO() 174 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 175 | assert captured_output.getvalue().strip().strip('0') == "11" # Player 1 won 176 | 177 | sys.stdin = StringIO("11011\n") 178 | sys.stdout = captured_output = StringIO() 179 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 180 | assert captured_output.getvalue().strip().strip('0') == "1" # Tie 181 | 182 | sys.stdin = StringIO("110111\n") 183 | sys.stdout = captured_output = StringIO() 184 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 185 | assert captured_output.getvalue().strip().strip('0') == "111" # Player 2 won 186 | 187 | sys.stdin = StringIO("11101\n") 188 | sys.stdout = captured_output = StringIO() 189 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 190 | assert captured_output.getvalue().strip().strip('0') == "111" # Player 2 won 191 | 192 | sys.stdin = StringIO("111011\n") 193 | sys.stdout = captured_output = StringIO() 194 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 195 | assert captured_output.getvalue().strip().strip('0') == "11" # Player 1 won 196 | 197 | sys.stdin = StringIO("1110111\n") 198 | sys.stdout = captured_output = StringIO() 199 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 200 | assert captured_output.getvalue().strip().strip('0') == "1" # Tie 201 | 202 | def test_coin_flip(): 203 | program = """ 204 | qStart 1 qHeads 0 R 205 | qStart 1 qTails 0 R 206 | 207 | qHeads 0 qWrite1 0 R 208 | qTails 0 qWrite2 0 R 209 | 210 | qWrite1 0 qHalt 1 R 211 | qWrite2 0 qWrite1 1 R 212 | """ 213 | sys.stdin = StringIO("1\n") # Heads 214 | sys.stdout = captured_output = StringIO() 215 | exec(varphi_to_python(program, "from varphi.runtime.cli import main\n", VarphiTranslatorCLITarget), {"__name__": "__main__"}) 216 | assert captured_output.getvalue().strip().strip('0') in {"1", "11"} -------------------------------------------------------------------------------- /tests/test_varphi_parser_and_lexer.py: -------------------------------------------------------------------------------- 1 | from antlr4 import InputStream, CommonTokenStream 2 | 3 | from varphi.parsing import * 4 | from varphi.compilation.varphi_compilation_errors import * 5 | 6 | def lexer_error_occurred(input_text: str) -> bool: 7 | input_stream = InputStream(input_text) 8 | lexer = VarphiLexer(input_stream) 9 | token_stream = CommonTokenStream(lexer) 10 | error_listener = VarphiSyntaxErrorListener(input_text) 11 | lexer.removeErrorListeners() 12 | lexer.addErrorListener(error_listener) 13 | 14 | try: 15 | token_stream.fill() 16 | return False 17 | except VarphiSyntaxError: 18 | return True 19 | 20 | def parser_error_occurred(input_text: str): 21 | input_stream = InputStream(input_text) 22 | lexer = VarphiLexer(input_stream) 23 | token_stream = CommonTokenStream(lexer) 24 | parser = VarphiParser(token_stream) 25 | error_listener = VarphiSyntaxErrorListener(input_text) 26 | lexer.removeErrorListeners() 27 | parser.removeErrorListeners() 28 | lexer.addErrorListener(error_listener) 29 | parser.addErrorListener(error_listener) 30 | 31 | try: 32 | # Parse the program 33 | parser.program() 34 | return False 35 | except VarphiSyntaxError: 36 | return True 37 | 38 | def test_empty_program(): 39 | program = "" 40 | expect_lexer_error = False 41 | expect_parser_error = False 42 | assert lexer_error_occurred(program) == expect_lexer_error 43 | assert parser_error_occurred(program) == expect_parser_error 44 | 45 | def test_invalid_token(): 46 | program = "a" 47 | expect_lexer_error = True 48 | expect_parser_error = True 49 | assert lexer_error_occurred(program) == expect_lexer_error 50 | assert parser_error_occurred(program) == expect_parser_error 51 | 52 | def test_invalid_line_ordering(): 53 | program = "L q0 q1 1 0" 54 | expect_lexer_error = False 55 | expect_parser_error = True 56 | assert lexer_error_occurred(program) == expect_lexer_error 57 | assert parser_error_occurred(program) == expect_parser_error 58 | 59 | def test_valid_line_ordering(): 60 | program = "q0 1 q1 0 R" 61 | expect_lexer_error = False 62 | expect_parser_error = False 63 | assert lexer_error_occurred(program) == expect_lexer_error 64 | assert parser_error_occurred(program) == expect_parser_error 65 | 66 | def test_multiple_valid_lines(): 67 | program = "q0 1 q1 0 R\nq1 0 q2 1 R" 68 | expect_lexer_error = False 69 | expect_parser_error = False 70 | assert lexer_error_occurred(program) == expect_lexer_error 71 | assert parser_error_occurred(program) == expect_parser_error 72 | 73 | def test_multiple_valid_lines_with_single_line_comment(): 74 | program = "q0 1 q1 0 R // Comment 1\nq1 0 q2 1 R // Comment 2" 75 | expect_lexer_error = False 76 | expect_parser_error = False 77 | assert lexer_error_occurred(program) == expect_lexer_error 78 | assert parser_error_occurred(program) == expect_parser_error 79 | 80 | def test_multiple_valid_lines_with_multi_line_comment(): 81 | program = "/*This is a multiline comment\n This is the second line of a multiline comment!*/q0 1 q1 0 R // Comment 1\nq1 0 q2 1 R // Comment 2" 82 | expect_lexer_error = False 83 | expect_parser_error = False 84 | assert lexer_error_occurred(program) == expect_lexer_error 85 | assert parser_error_occurred(program) == expect_parser_error 86 | -------------------------------------------------------------------------------- /tests/test_varphi_types.py: -------------------------------------------------------------------------------- 1 | """tests/test_varphi_types.py""" 2 | import pytest 3 | from varphi.runtime.types import State, Instruction, TapeCharacter, HeadDirection 4 | 5 | @pytest.fixture 6 | def state(): 7 | """Fixture to create a new state for each test.""" 8 | return State() 9 | 10 | 11 | def test_create_state(state): 12 | """Test the creation of a state and ensure its instruction lists are empty initially.""" 13 | assert state.on_tally_instructions == [] 14 | assert state.on_blank_instructions == [] 15 | 16 | 17 | def test_add_on_tally_instruction(state): 18 | """Test adding a valid instruction for the tally character.""" 19 | instruction = Instruction(next_state=state, 20 | character_to_place=TapeCharacter.TALLY, 21 | direction_to_move=HeadDirection.RIGHT) 22 | state.add_on_tally_instruction(instruction) 23 | assert instruction in state.on_tally_instructions 24 | assert len(state.on_tally_instructions) == 1 25 | 26 | 27 | def test_add_on_blank_instruction(state): 28 | """Test adding a valid instruction for the blank character.""" 29 | instruction = Instruction(next_state=state, 30 | character_to_place=TapeCharacter.BLANK, 31 | direction_to_move=HeadDirection.LEFT) 32 | state.add_on_blank_instruction(instruction) 33 | assert instruction in state.on_blank_instructions 34 | assert len(state.on_blank_instructions) == 1 35 | 36 | 37 | def test_duplicate_on_tally_instruction(state): 38 | """Test adding duplicate instructions to the tally list.""" 39 | instruction = Instruction(next_state=state, 40 | character_to_place=TapeCharacter.TALLY, 41 | direction_to_move=HeadDirection.RIGHT) 42 | state.add_on_tally_instruction(instruction) 43 | state.add_on_tally_instruction(instruction) # Adding the same instruction again 44 | assert len(state.on_tally_instructions) == 1 # It should not be duplicated 45 | 46 | 47 | def test_duplicate_on_blank_instruction(state): 48 | """Test adding duplicate instructions to the blank list.""" 49 | instruction = Instruction(next_state=state, 50 | character_to_place=TapeCharacter.BLANK, 51 | direction_to_move=HeadDirection.LEFT) 52 | state.add_on_blank_instruction(instruction) 53 | state.add_on_blank_instruction(instruction) # Adding the same instruction again 54 | assert len(state.on_blank_instructions) == 1 # It should not be duplicated 55 | 56 | 57 | def test_non_deterministic_state(state): 58 | """Test that a state can handle multiple instructions for the 59 | same character (non-determinism). 60 | """ 61 | state_1 = State() 62 | state_2 = State() 63 | 64 | instruction_1 = Instruction(next_state=state_1, 65 | character_to_place=TapeCharacter.TALLY, 66 | direction_to_move=HeadDirection.RIGHT) 67 | instruction_2 = Instruction(next_state=state_2,\ 68 | character_to_place=TapeCharacter.TALLY, 69 | direction_to_move=HeadDirection.LEFT) 70 | 71 | state.add_on_tally_instruction(instruction_1) 72 | state.add_on_tally_instruction(instruction_2) 73 | 74 | assert instruction_1 in state.on_tally_instructions 75 | assert instruction_2 in state.on_tally_instructions 76 | assert len(state.on_tally_instructions) == 2 77 | -------------------------------------------------------------------------------- /varphi/__init__.py: -------------------------------------------------------------------------------- 1 | """varphi/__init__.py""" 2 | -------------------------------------------------------------------------------- /varphi/compilation/__init__.py: -------------------------------------------------------------------------------- 1 | """varphi/parsing_tools/__init__.py""" 2 | 3 | from .core import varphi_file_to_python 4 | -------------------------------------------------------------------------------- /varphi/compilation/core.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0801 2 | """varphi/compilation/common.py""" 3 | 4 | import os 5 | import sys 6 | from typing import Type 7 | 8 | from antlr4 import InputStream, CommonTokenStream, ParseTreeWalker 9 | from .varphi_translator import VarphiTranslator 10 | from ..parsing import VarphiLexer, VarphiParser 11 | 12 | 13 | def get_translator_from_program( 14 | program: str, translator_type: Type[VarphiTranslator] 15 | ) -> VarphiTranslator: 16 | """Parses a Varphi program string and returns a VarphiTranslatorCLITarget object.""" 17 | # Create a lexer and parser for the input program 18 | input_stream = InputStream(program) 19 | lexer = VarphiLexer(input_stream) 20 | token_stream = CommonTokenStream(lexer) 21 | parser = VarphiParser(token_stream) 22 | 23 | # Parse the input and get the parse tree 24 | parse_tree = parser.program() 25 | 26 | # Instantiate the custom listener 27 | translator = translator_type() 28 | 29 | # Walk the parse tree with the listener 30 | walker = ParseTreeWalker() 31 | walker.walk(translator, parse_tree) 32 | 33 | return translator 34 | 35 | 36 | def varphi_to_python( 37 | program: str, main_import_statement: str, translator_type: Type[VarphiTranslator] 38 | ) -> str: 39 | """Compiles a Varphi program string to a Python program string.""" 40 | translated_program = main_import_statement 41 | translated_program += get_translator_from_program( 42 | program, translator_type 43 | ).translated_program 44 | translated_program += " main(initial_state)" 45 | return translated_program 46 | 47 | 48 | def varphi_file_to_python( 49 | file_path: str, main_import_statement: str, translator_type: Type[VarphiTranslator] 50 | ) -> str: 51 | """Compiles a Varphi program file to a Python program string.""" 52 | if not os.path.exists(file_path): 53 | print(f"Error: File {file_path} not found.") 54 | sys.exit(1) 55 | with open(file_path, "r", encoding="utf-8") as f: 56 | translated_code = varphi_to_python( 57 | f.read(), main_import_statement, translator_type 58 | ) 59 | return translated_code 60 | -------------------------------------------------------------------------------- /varphi/compilation/varphi_compilation_errors.py: -------------------------------------------------------------------------------- 1 | """varphi/parsing_tools/VarphiCompilationErrors.py""" 2 | 3 | from antlr4.error.ErrorListener import ErrorListener 4 | 5 | 6 | class VarphiSyntaxError(Exception): 7 | """Exception raised for general syntax errors.""" 8 | 9 | line: int 10 | column: int 11 | 12 | def __init__(self, message: str, line: int, column: int) -> None: 13 | """Initializes a VarphiSyntaxError with a message, line, and column. 14 | 15 | Args: 16 | message (str): The error message. 17 | line (int): The line where the error occurred. 18 | column (int): The column where the error occurred. 19 | """ 20 | super().__init__(message) 21 | self.line = line 22 | self.column = column 23 | 24 | 25 | class VarphiSyntaxErrorListener(ErrorListener): 26 | """Custom error listener for Varphi syntax errors. 27 | 28 | This listener processes syntax errors and raises the appropriate exceptions 29 | with detailed error messages, including the specific line and column where 30 | the error occurred. 31 | """ 32 | 33 | input_text: list[str] 34 | 35 | def __init__(self, input_text: str) -> None: 36 | """Initializes the VarphiSyntaxErrorListener with the input text. 37 | 38 | Args: 39 | input_text (str): The input text to be processed. 40 | """ 41 | super().__init__() 42 | self.input_text = ( 43 | input_text.splitlines() 44 | ) # Split text into lines for easy access 45 | 46 | def syntaxError( # pylint: disable=R0913, R0917 47 | self, recognizer, offendingSymbol, line, column, msg, e 48 | ) -> None: 49 | """Handles syntax errors encountered by the parser. 50 | 51 | If the program is empty, raises an EmptyProgramError. Otherwise, raises 52 | a VarphiSyntaxError with detailed information about the error. 53 | 54 | Args: 55 | recognizer: The recognizer that encountered the error. 56 | offending_symbol: The symbol that caused the error. 57 | line (int): The line number where the error occurred. 58 | column (int): The column number where the error occurred. 59 | msg (str): The error message. 60 | e: The exception that caused the error. 61 | """ 62 | # Get the specific line where the error occurred 63 | error_line = self.input_text[line - 1] 64 | # Create a line with ^ pointing to the offending symbol 65 | pointer_line = " " * column + "^" 66 | 67 | # Format the error message 68 | error = f"Syntax error at line {line}:{column} - {msg}\n" 69 | error += f" {error_line}\n" # Print the erroneous line 70 | error += f" {pointer_line}\n" # Print the pointer line 71 | 72 | # Raise a VarphiSyntaxError with the formatted error message 73 | raise VarphiSyntaxError(error, line, column) 74 | -------------------------------------------------------------------------------- /varphi/compilation/varphi_translator.py: -------------------------------------------------------------------------------- 1 | """varphi/parsing_tools/varphi_representor.py""" 2 | 3 | from ..parsing import VarphiParser, VarphiListener 4 | 5 | 6 | class VarphiTranslator(VarphiListener): 7 | """This class translates the tokens generated by VarphiParser into Python code.""" 8 | 9 | translated_program: str 10 | initial_state_found: bool 11 | seen_state_names: set[str] 12 | 13 | def enterLine(self, ctx: VarphiParser.LineContext) -> None: 14 | raise NotImplementedError 15 | 16 | 17 | class VarphiTranslatorCLITarget(VarphiTranslator): 18 | """This class translates the tokens generated by VarphiParser into Python code.""" 19 | 20 | def __init__(self): 21 | """Initialize a VarphiRepresentor.""" 22 | self.translated_program = "if __name__ == '__main__':\n" 23 | self.translated_program += ( 24 | " from varphi.runtime.types import " 25 | "TapeCharacter, HeadDirection, Instruction, State\n" 26 | ) 27 | self.translated_program += " initial_state = None\n" 28 | self.initial_state_found = False 29 | self.seen_state_names = set() 30 | super().__init__() 31 | 32 | def enterLine( 33 | self, ctx: VarphiParser.LineContext 34 | ) -> None: # pylint: disable=R0914, R0912, R0915 35 | """Enter a parse tree produced by VarphiParser#line. 36 | 37 | Processes the parse tree nodes to extract state transitions and 38 | instructions. 39 | 40 | Args: 41 | - ctx (VarphiParser.LineContext): The context for the parse tree 42 | node. 43 | """ 44 | if_state_string = str(ctx.STATE(0).getText()) 45 | if if_state_string not in self.seen_state_names: 46 | self.translated_program += f" {if_state_string} = State()\n" 47 | if not self.initial_state_found: 48 | self.translated_program += ( 49 | f" initial_state = {str(ctx.STATE(0).getText())}\n" 50 | ) 51 | self.initial_state_found = True 52 | self.seen_state_names.add(if_state_string) 53 | 54 | then_state_string = str(ctx.STATE(1).getText()) 55 | if then_state_string not in self.seen_state_names: 56 | self.translated_program += f" {str(ctx.STATE(1).getText())} = State()\n" 57 | self.seen_state_names.add(then_state_string) 58 | 59 | instruction = f"Instruction({then_state_string}, " 60 | if str(ctx.TAPE_CHARACTER(1).getText()) == "1": 61 | instruction += "TapeCharacter.TALLY, " 62 | else: 63 | instruction += "TapeCharacter.BLANK, " 64 | 65 | if str(ctx.HEAD_DIRECTION().getText()) == "R": 66 | instruction += "HeadDirection.RIGHT)" 67 | else: 68 | instruction += "HeadDirection.LEFT)" 69 | 70 | if str(ctx.TAPE_CHARACTER(0).getText()) == "1": 71 | self.translated_program += ( 72 | f" {str(ctx.STATE(0).getText())}.add_on_tally_instruction(" 73 | f"{instruction})\n" 74 | ) 75 | else: 76 | self.translated_program += ( 77 | f" {str(ctx.STATE(0).getText())}.add_on_blank_instruction(" 78 | f"{instruction})\n" 79 | ) 80 | 81 | 82 | class VarphiTranslatorCLIDebuggerTarget(VarphiTranslator): 83 | """This class translates the tokens generated by VarphiParser into Python code.""" 84 | 85 | def __init__(self): 86 | """Initialize a VarphiRepresentor.""" 87 | self.translated_program = "if __name__ == '__main__':\n" 88 | self.translated_program += ( 89 | " from varphi.runtime.types import " 90 | "TapeCharacter, HeadDirection, Instruction, NamedState\n" 91 | ) 92 | self.translated_program += " initial_state = None\n" 93 | self.initial_state_found = False 94 | self.seen_state_names = set() 95 | super().__init__() 96 | 97 | def enterLine( 98 | self, ctx: VarphiParser.LineContext 99 | ) -> None: # pylint: disable=R0914, R0912, R0915 100 | """Enter a parse tree produced by VarphiParser#line. 101 | 102 | Processes the parse tree nodes to extract state transitions and 103 | instructions. 104 | 105 | Args: 106 | - ctx (VarphiParser.LineContext): The context for the parse tree 107 | node. 108 | """ 109 | if_state_string = str(ctx.STATE(0).getText()) 110 | if if_state_string not in self.seen_state_names: 111 | self.translated_program += ( 112 | f' {if_state_string} = NamedState("{if_state_string}")\n' 113 | ) 114 | if not self.initial_state_found: 115 | self.translated_program += ( 116 | f" initial_state = {str(ctx.STATE(0).getText())}\n" 117 | ) 118 | self.initial_state_found = True 119 | self.seen_state_names.add(if_state_string) 120 | 121 | then_state_string = str(ctx.STATE(1).getText()) 122 | if then_state_string not in self.seen_state_names: 123 | self.translated_program += ( 124 | f" {str(ctx.STATE(1).getText())} = " 125 | f'NamedState("{then_state_string}")\n' 126 | ) 127 | self.seen_state_names.add(then_state_string) 128 | 129 | instruction = f"Instruction({then_state_string}, " 130 | if str(ctx.TAPE_CHARACTER(1).getText()) == "1": 131 | instruction += "TapeCharacter.TALLY, " 132 | else: 133 | instruction += "TapeCharacter.BLANK, " 134 | 135 | if str(ctx.HEAD_DIRECTION().getText()) == "R": 136 | instruction += "HeadDirection.RIGHT)" 137 | else: 138 | instruction += "HeadDirection.LEFT)" 139 | 140 | if str(ctx.TAPE_CHARACTER(0).getText()) == "1": 141 | self.translated_program += ( 142 | f" {str(ctx.STATE(0).getText())}.add_on_tally_instruction(" 143 | f"{instruction})\n" 144 | ) 145 | else: 146 | self.translated_program += ( 147 | f" {str(ctx.STATE(0).getText())}.add_on_blank_instruction(" 148 | f"{instruction})\n" 149 | ) 150 | 151 | 152 | class VarphiTranslatorCLIDebugAdapterTarget(VarphiTranslator): 153 | """This class translates the tokens generated by VarphiParser into Python code.""" 154 | 155 | def __init__(self): 156 | """Initialize a VarphiRepresentor.""" 157 | self.translated_program = "if __name__ == '__main__':\n" 158 | self.translated_program += ( 159 | " from varphi.runtime.types import " 160 | "TapeCharacter, HeadDirection, LineNumberedInstruction, NamedState\n" 161 | ) 162 | self.translated_program += " initial_state = None\n" 163 | self.initial_state_found = False 164 | self.seen_state_names = set() 165 | super().__init__() 166 | 167 | def enterLine( 168 | self, ctx: VarphiParser.LineContext 169 | ) -> None: # pylint: disable=R0914, R0912, R0915 170 | """Enter a parse tree produced by VarphiParser#line. 171 | 172 | Processes the parse tree nodes to extract state transitions and 173 | instructions. 174 | 175 | Args: 176 | - ctx (VarphiParser.LineContext): The context for the parse tree 177 | node. 178 | """ 179 | if_state_string = str(ctx.STATE(0).getText()) 180 | if if_state_string not in self.seen_state_names: 181 | self.translated_program += ( 182 | f' {if_state_string} = NamedState("{if_state_string}")\n' 183 | ) 184 | if not self.initial_state_found: 185 | self.translated_program += ( 186 | f" initial_state = {str(ctx.STATE(0).getText())}\n" 187 | ) 188 | self.initial_state_found = True 189 | self.seen_state_names.add(if_state_string) 190 | 191 | then_state_string = str(ctx.STATE(1).getText()) 192 | if then_state_string not in self.seen_state_names: 193 | self.translated_program += ( 194 | f" {str(ctx.STATE(1).getText())} = " 195 | f'NamedState("{then_state_string}")\n' 196 | ) 197 | self.seen_state_names.add(then_state_string) 198 | 199 | instruction = f"LineNumberedInstruction({then_state_string}, " 200 | if str(ctx.TAPE_CHARACTER(1).getText()) == "1": 201 | instruction += "TapeCharacter.TALLY, " 202 | else: 203 | instruction += "TapeCharacter.BLANK, " 204 | 205 | if str(ctx.HEAD_DIRECTION().getText()) == "R": 206 | instruction += f"HeadDirection.RIGHT, {ctx.start.line})" 207 | else: 208 | instruction += f"HeadDirection.LEFT, {ctx.start.line})" 209 | 210 | if str(ctx.TAPE_CHARACTER(0).getText()) == "1": 211 | self.translated_program += ( 212 | f" {str(ctx.STATE(0).getText())}.add_on_tally_instruction(" 213 | f"{instruction})\n" 214 | ) 215 | else: 216 | self.translated_program += ( 217 | f" {str(ctx.STATE(0).getText())}.add_on_blank_instruction(" 218 | f"{instruction})\n" 219 | ) 220 | -------------------------------------------------------------------------------- /varphi/parsing/__init__.py: -------------------------------------------------------------------------------- 1 | """varphi_lang/parsing/__init__.py""" 2 | from .VarphiLexer import * 3 | from .VarphiParser import * 4 | from .VarphiListener import * 5 | -------------------------------------------------------------------------------- /varphi/runtime/__init__.py: -------------------------------------------------------------------------------- 1 | """varphi/runtime/__init__.py""" 2 | -------------------------------------------------------------------------------- /varphi/runtime/cli.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | """varphi/command_line_executable.py""" 3 | 4 | from .common import get_tape_from_stdin 5 | from .types import State, TuringMachine 6 | from .exceptions import VarphiTuringMachineHaltedException, VarphiDomainError 7 | 8 | 9 | def main(initial_state: State | None): # pylint: disable=R0801 10 | """Construct the Turing machine given an initial state and run it. 11 | 12 | Reads the input tape from standard input and runs the Turing machine until it halts. 13 | """ 14 | tape = get_tape_from_stdin() 15 | if initial_state is None: 16 | raise VarphiDomainError("Error: Input provided to an empty Turing machine.") 17 | turing_machine = TuringMachine(tape, initial_state) 18 | while True: 19 | try: 20 | turing_machine.step() 21 | except VarphiTuringMachineHaltedException: 22 | break 23 | print(tape) 24 | -------------------------------------------------------------------------------- /varphi/runtime/cli_debugger.py: -------------------------------------------------------------------------------- 1 | """varphi/command_line_executable.py""" 2 | 3 | import sys 4 | 5 | from .common import get_tape_from_stdin 6 | from .types import NamedState, TuringMachine 7 | from .exceptions import VarphiTuringMachineHaltedException, VarphiDomainError 8 | from .common import debug_view_tape_head 9 | 10 | 11 | def main(initial_state: NamedState | None): # pylint: disable=R0801 12 | """Construct the Turing machine given an initial state and run it. 13 | 14 | Reads the input tape from standard input and runs the Turing machine until it halts. 15 | """ 16 | number_of_steps = 0 17 | print("Input Tape: ", end="") 18 | sys.stdout.flush() 19 | tape = get_tape_from_stdin() 20 | if initial_state is None: 21 | raise VarphiDomainError("Error: Input provided to an empty Turing machine.") 22 | turing_machine = TuringMachine(tape, initial_state) 23 | while True: 24 | try: 25 | print("State: ", turing_machine.state.name) 26 | print("Tape: ", debug_view_tape_head(tape, turing_machine.head)) 27 | print("Press ENTER to step...") 28 | sys.stdin.read(1) 29 | turing_machine.step() 30 | number_of_steps += 1 31 | except VarphiTuringMachineHaltedException: 32 | break 33 | print(f"Output Tape: {tape}") 34 | print(f"Number of Steps: {number_of_steps}") 35 | print(f"Number of Tape Cells Accessed: {len(str(tape))}") 36 | -------------------------------------------------------------------------------- /varphi/runtime/cli_with_complexity.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | """varphi/cli_with_complexity.py""" 3 | 4 | import sys 5 | from .common import get_tape_from_stdin 6 | from .types import State, TuringMachine 7 | from .exceptions import VarphiTuringMachineHaltedException, VarphiDomainError 8 | 9 | 10 | def main(initial_state: State | None): # pylint: disable=R0801 11 | """Construct the Turing machine given an initial state and run it. 12 | 13 | Reads the input tape from standard input and runs the Turing machine until it halts. 14 | """ 15 | number_of_steps = 0 16 | print("Input Tape: ", end="") 17 | sys.stdout.flush() 18 | tape = get_tape_from_stdin() 19 | if initial_state is None: 20 | raise VarphiDomainError("Error: Input provided to an empty Turing machine.") 21 | turing_machine = TuringMachine(tape, initial_state) 22 | while True: 23 | try: 24 | turing_machine.step() 25 | number_of_steps += 1 26 | except VarphiTuringMachineHaltedException: 27 | break 28 | print(f"Output Tape: {tape}") 29 | print(f"Number of Steps: {number_of_steps}") 30 | print(f"Number of Tape Cells Accessed: {len(str(tape))}") 31 | -------------------------------------------------------------------------------- /varphi/runtime/cli_with_prompts.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=duplicate-code 2 | """varphi/runtime/cli_with_prompts.py""" 3 | 4 | import sys 5 | from .common import get_tape_from_stdin 6 | from .types import State, TuringMachine 7 | from .exceptions import VarphiTuringMachineHaltedException, VarphiDomainError 8 | 9 | 10 | def main(initial_state: State | None): # pylint: disable=R0801 11 | """Construct the Turing machine given an initial state and run it. 12 | 13 | Reads the input tape from standard input and runs the Turing machine until it halts. 14 | """ 15 | print("Input Tape: ", end="") 16 | sys.stdout.flush() 17 | tape = get_tape_from_stdin() 18 | if initial_state is None: 19 | raise VarphiDomainError("Error: Input provided to an empty Turing machine.") 20 | turing_machine = TuringMachine(tape, initial_state) 21 | while True: 22 | try: 23 | turing_machine.step() 24 | except VarphiTuringMachineHaltedException: 25 | break 26 | print(f"Output Tape: {tape}") 27 | -------------------------------------------------------------------------------- /varphi/runtime/common.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0801 2 | """varphi/runtime/common.py""" 3 | 4 | import sys 5 | 6 | from .types import TapeCharacter, Tape, Head 7 | from .exceptions import VarphiNoTallyException, VarphiInvalidTapeCharacterException 8 | 9 | 10 | def debug_view_tape_head(tape: Tape, head: Head) -> str: 11 | """Return a string representation of the tape with the head position marked.""" 12 | tape_as_string = "" 13 | for i in range( 14 | tape._minimum_accessed_index, # pylint: disable=W0212 15 | max( 16 | tape._maximum_accessed_index, # pylint: disable=W0212 17 | head._current_tape_cell_index, # pylint: disable=W0212 18 | ) 19 | + 1, 20 | ): 21 | character = ( 22 | "1" 23 | if tape._tape[i] == TapeCharacter.TALLY # pylint: disable=W0212 24 | else "0" 25 | ) 26 | if i == 0: 27 | character = "{" + character + "}" 28 | if i == head._current_tape_cell_index: # pylint: disable=W0212 29 | character = "[" + character + "]" 30 | tape_as_string += character 31 | return tape_as_string 32 | 33 | 34 | def get_tape_from_stdin() -> Tape: 35 | """Reads the input tape from standard input and returns it as a Tape object.""" 36 | # Ignore all leading blanks (0s) 37 | found_tally = False 38 | while not found_tally: 39 | input_character = sys.stdin.read(1) 40 | if input_character == "1": 41 | found_tally = True 42 | elif input_character in {"\n", "\r"}: 43 | raise VarphiNoTallyException( 44 | "Error: Input tape must contain at least one tally (1)." 45 | ) 46 | elif input_character != "0": 47 | raise VarphiInvalidTapeCharacterException( 48 | f"Error: Invalid tape character (ASCII {ord(input_character)})." 49 | ) 50 | 51 | # Since the above loop exited, a tally must have been found. 52 | initial_characters = [TapeCharacter.TALLY] 53 | while (input_character := sys.stdin.read(1)) not in {"\n", "\r"}: 54 | if input_character == "0": 55 | initial_characters.append(TapeCharacter.BLANK) 56 | elif input_character == "1": 57 | initial_characters.append(TapeCharacter.TALLY) 58 | else: 59 | raise VarphiInvalidTapeCharacterException( 60 | f"Error: Invalid tape character (ASCII {ord(input_character)})." 61 | ) 62 | return Tape(initial_characters) 63 | -------------------------------------------------------------------------------- /varphi/runtime/debug_adapter.py: -------------------------------------------------------------------------------- 1 | """varphi/runtime/debug_adapter.py""" 2 | import sys 3 | import json 4 | 5 | from .types import Tape, TapeCharacter, State, DebugAdapterTuringMachine 6 | from .exceptions import ( 7 | VarphiInvalidTapeCharacterException, 8 | VarphiNoTallyException, 9 | VarphiTuringMachineHaltedException, 10 | ) 11 | from .common import debug_view_tape_head 12 | 13 | 14 | def get_tape_from_string(string: str) -> Tape: 15 | """Given a string with 1s and 0s, construct and return a tape.""" 16 | # First strip all '0's 17 | string = string.strip("0") 18 | if len(string) == 0 or string[0] != "1": 19 | raise VarphiNoTallyException( 20 | "Error: Input tape must contain at least one tally (1)." 21 | ) 22 | initial_characters = [] 23 | for input_character in string: 24 | if input_character == "0": 25 | initial_characters.append(TapeCharacter.BLANK) 26 | elif input_character == "1": 27 | initial_characters.append(TapeCharacter.TALLY) 28 | else: 29 | raise VarphiInvalidTapeCharacterException( 30 | f"Error: Invalid tape character (ASCII #{ord(input_character)})." 31 | ) 32 | return Tape(initial_characters) 33 | 34 | 35 | class VarphiDebugAdapter: 36 | """A class representing a debug adapter for the Varphi programming language.""" 37 | initial_state: State 38 | turing_machine: DebugAdapterTuringMachine | None 39 | no_debug: bool | None 40 | breakpoints: set[int] 41 | source_path: str | None 42 | current_line_number: int 43 | 44 | def __init__(self, initial_state: State): 45 | self.initial_state = initial_state 46 | self.breakpoints = set() 47 | self.current_line_number = -1 48 | 49 | def loop(self): 50 | """The debug adapter loop.""" 51 | try: 52 | while True: 53 | sys.stdin.buffer.read(16) # Skip "Content-Length: " 54 | content_length_string = "" 55 | character = sys.stdin.buffer.read(1) 56 | while character != b"\r": 57 | content_length_string += chr(character[0]) 58 | character = sys.stdin.buffer.read(1) 59 | # Skip the remaining \n\r\n 60 | sys.stdin.buffer.read(3) 61 | content_length = int(content_length_string) 62 | content_part = sys.stdin.buffer.read(content_length).decode() 63 | self.parse_input(content_part) 64 | except Exception: # pylint: disable=W0718 65 | import traceback # pylint: disable=C0415 66 | 67 | exited_event = {} 68 | exited_event["exitCode"] = 0 69 | self.send_event("exited", exited_event) 70 | output_event = {} 71 | output_event["category"] = "stderr" 72 | output_event["output"] = str(traceback.format_exc()) 73 | self.send_event("output", output_event) 74 | terminated_event = {} 75 | self.send_event("terminated", terminated_event) 76 | sys.exit(-1) 77 | 78 | def send_body_part(self, body_part: dict) -> None: 79 | """Send a message to the debug adapter client..""" 80 | body_part_string = json.dumps(body_part) 81 | content_length = str(len(body_part_string)) 82 | sys.stdout.buffer.write(b"Content-Length: ") 83 | sys.stdout.buffer.write(content_length.encode("utf-8")) 84 | sys.stdout.buffer.write(b"\r\n\r\n") 85 | sys.stdout.buffer.write(body_part_string.encode("utf-8")) 86 | sys.stdout.flush() 87 | 88 | def send_response( # pylint: disable=R0913 R0917 89 | self, 90 | request_seq: int, 91 | success: bool, 92 | command: str, 93 | message: str | None = None, 94 | body: dict | None = None, 95 | ) -> None: 96 | """Send a response to the debug adapter client.""" 97 | body_part = {} 98 | body_part["type"] = "response" 99 | body_part["request_seq"] = request_seq 100 | body_part["success"] = success 101 | body_part["command"] = command 102 | if message is not None: 103 | body_part["message"] = message 104 | if body is not None: 105 | body_part["body"] = body 106 | self.send_body_part(body_part) 107 | 108 | def send_event(self, event: str, body: dict | None = None) -> None: 109 | """Send an event to the debug adapter client.""" 110 | body_part = {} 111 | body_part["type"] = "event" 112 | body_part["event"] = event 113 | if body is not None: 114 | body_part["body"] = body 115 | self.send_body_part(body_part) 116 | 117 | def parse_input(self, input_json_string: str): 118 | """Parse and handle a message from the debug adapter client.""" 119 | json_input = json.loads(input_json_string) 120 | if json_input["type"] == "request": 121 | self.handle_request(json_input) 122 | 123 | def handle_request(self, json_input): # pylint: disable=R0912 124 | """Handle a general request from the debug adapter client.""" 125 | command = json_input["command"] 126 | if command == "initialize": 127 | self.handle_initialize_request(json_input) 128 | if command == "launch": 129 | self.handle_launch_request(json_input) 130 | if command == "setBreakpoints": 131 | self.handle_set_breakpoints_request(json_input) 132 | if command == "configurationDone": 133 | self.handle_configuration_done_request(json_input) 134 | if command == "threads": 135 | self.handle_threads_request(json_input) 136 | if command == "stackTrace": 137 | self.handle_stack_trace_request(json_input) 138 | if command == "scopes": 139 | self.handle_scopes_request(json_input) 140 | if command == "variables": 141 | self.handle_variables_request(json_input) 142 | if command == "continue": 143 | self.handle_continue_request(json_input) 144 | if command == "next": 145 | self.handle_next_request(json_input) 146 | if command == "stepIn": 147 | self.handle_step_in_request(json_input) 148 | if command == "stepOut": 149 | self.handle_step_out_request(json_input) 150 | if command == "disconnect": 151 | self.handle_disconnect_request(json_input) 152 | 153 | def handle_initialize_request(self, json_input): 154 | """Handle an initialize request from the debug adapter client.""" 155 | response_body = { 156 | "supportsConfigurationDoneRequest": True, 157 | "supportsSingleThreadExecutionRequests": True, 158 | } 159 | self.send_response(json_input["seq"], True, "initialize", None, response_body) 160 | self.send_event("initialized") 161 | 162 | def handle_set_breakpoints_request(self, json_input): 163 | """Handle a set breakpoints request from the debug adapter client.""" 164 | response_body = {} 165 | response_body["breakpoints"] = [] 166 | arguments = json_input["arguments"] 167 | if "breakpoints" in arguments: 168 | breakpoints = arguments["breakpoints"] 169 | for bp in breakpoints: 170 | breakpoint_line_number = bp["line"] 171 | self.breakpoints.add(breakpoint_line_number) 172 | breakpoint_response = {"verified": True} 173 | response_body["breakpoints"].append(breakpoint_response) 174 | 175 | if "sourceModified" in arguments and arguments["sourceModified"]: 176 | raise Exception( # pylint: disable=W0719 177 | "Error: Source code change detected. Please kindly restart the debugging process." 178 | ) 179 | self.send_response( 180 | json_input["seq"], True, "setBreakpoints", None, response_body 181 | ) 182 | if self.no_debug: 183 | self.breakpoints = set() 184 | 185 | def handle_launch_request(self, json_input): 186 | """Handle a launch request from the debug adapter client.""" 187 | arguments = json_input["arguments"] 188 | if "noDebug" not in arguments: 189 | raise Exception('Error: Missing argument "noDebug" for Debug Adapter.') # pylint: disable=W0719 190 | if "sourcePath" not in arguments: 191 | raise Exception('Error: Missing argument "sourcePath" for Debug Adapter.') # pylint: disable=W0719 192 | if "tape" not in arguments: 193 | raise Exception('Error: Missing argument "tape" for Debug Adapter.') # pylint: disable=W0719 194 | self.no_debug = arguments["noDebug"] 195 | self.source_path = arguments["sourcePath"] 196 | input_tape = arguments["tape"] 197 | self.turing_machine = DebugAdapterTuringMachine( 198 | get_tape_from_string(input_tape), self.initial_state 199 | ) 200 | self.send_response(json_input["seq"], True, "launch") 201 | 202 | def handle_configuration_done_request(self, json_input): # pylint: disable=R0915 203 | """Handle a configuration done request from the debug adapter client.""" 204 | self.send_response(json_input["seq"], True, "configurationDone") 205 | if self.no_debug: 206 | while True: 207 | try: 208 | self.turing_machine.step() 209 | except VarphiTuringMachineHaltedException: 210 | exited_event = {} 211 | exited_event["exitCode"] = 0 212 | self.send_event("exited", exited_event) 213 | output_event = {} 214 | output_event["category"] = "console" 215 | output_event["output"] = str(self.turing_machine.tape) 216 | self.send_event("output", output_event) 217 | terminated_event = {} 218 | self.send_event("terminated", terminated_event) 219 | break 220 | else: 221 | if len(self.breakpoints) == 0: 222 | try: 223 | self.current_line_number = ( 224 | self.turing_machine.determine_next_instruction().line_number 225 | ) 226 | stopped_event = {} 227 | stopped_event["reason"] = "step" 228 | stopped_event["threadId"] = 1 229 | stopped_event["allThreadsStopped"] = True 230 | self.send_event("stopped", stopped_event) 231 | except VarphiTuringMachineHaltedException: 232 | exited_event = {} 233 | exited_event["exitCode"] = 0 234 | self.send_event("exited", exited_event) 235 | output_event = {} 236 | output_event["category"] = "console" 237 | output_event["output"] = str(self.turing_machine.tape) 238 | self.send_event("output", output_event) 239 | terminated_event = {} 240 | self.send_event("terminated", terminated_event) 241 | return 242 | 243 | else: 244 | while True: 245 | try: 246 | self.current_line_number = ( 247 | self.turing_machine.determine_next_instruction().line_number 248 | ) 249 | if self.current_line_number in self.breakpoints: 250 | stopped_event = {} 251 | stopped_event["reason"] = "breakpoint" 252 | stopped_event["threadId"] = 1 253 | stopped_event["allThreadsStopped"] = True 254 | self.send_event("stopped", stopped_event) 255 | break 256 | self.turing_machine.execute_next_instruction() 257 | except VarphiTuringMachineHaltedException: 258 | exited_event = {} 259 | exited_event["exitCode"] = 0 260 | self.send_event("exited", exited_event) 261 | output_event = {} 262 | output_event["category"] = "console" 263 | output_event["output"] = str(self.turing_machine.tape) 264 | self.send_event("output", output_event) 265 | terminated_event = {} 266 | self.send_event("terminated", terminated_event) 267 | break 268 | 269 | def handle_threads_request(self, json_input): 270 | """Handle a threads request from the debug adapter client.""" 271 | response_body = {} 272 | response_body["threads"] = [{"id": 1, "name": "thread1"}] 273 | self.send_response(json_input["seq"], True, "threads", None, response_body) 274 | 275 | def handle_stack_trace_request(self, json_input): 276 | """Handle a stack trace request from the debug adapter client.""" 277 | response_body = {} 278 | stack_frame = {} 279 | stack_frame["id"] = 0 280 | stack_frame["name"] = "source" 281 | source = {} 282 | source["name"] = "Varphi Program" 283 | source["path"] = self.source_path 284 | stack_frame["source"] = source 285 | 286 | stack_frame["line"] = self.current_line_number 287 | stack_frame["column"] = 0 288 | response_body["stackFrames"] = [stack_frame] 289 | response_body["totalFrames"] = 1 290 | self.send_response(json_input["seq"], True, "stackTrace", None, response_body) 291 | 292 | def handle_scopes_request(self, json_input): 293 | """Handle a scopes request from the debug adapter client.""" 294 | scope = {} 295 | scope["name"] = "Machine Variables" 296 | scope["variablesReference"] = ( 297 | 1 # The tape, the index of the head, the index of zero (first head location), state 298 | ) 299 | 300 | response_body = {} 301 | response_body["scopes"] = [scope] 302 | self.send_response(json_input["seq"], True, "scopes", None, response_body) 303 | 304 | def handle_variables_request(self, json_input): 305 | """Handle a variables request from the debug adapter client.""" 306 | tape_variable = {} 307 | tape_variable["name"] = "Tape" 308 | tape_variable["value"] = debug_view_tape_head( 309 | self.turing_machine.tape, self.turing_machine.head 310 | ) 311 | tape_variable["variablesReference"] = 0 312 | 313 | head_variable = {} 314 | head_variable["name"] = "Head" 315 | head_variable["value"] = str(self.turing_machine.head) 316 | head_variable["variablesReference"] = 0 317 | 318 | zero_variable = {} 319 | zero_variable["name"] = "Tape Zero" 320 | zero_variable["value"] = str(-self.turing_machine.tape._minimum_accessed_index) # pylint: disable=W0212 321 | zero_variable["variablesReference"] = 0 322 | 323 | state_variable = {} 324 | state_variable["name"] = "State" 325 | state_variable["value"] = str(self.turing_machine.state.name) 326 | state_variable["variablesReference"] = 0 327 | 328 | response_body = {} 329 | response_body["variables"] = [ 330 | tape_variable, 331 | state_variable, 332 | head_variable, 333 | zero_variable, 334 | ] 335 | self.send_response(json_input["seq"], True, "variables", None, response_body) 336 | 337 | def handle_next_request(self, json_input): 338 | """Handle a next request from the debug adapter client.""" 339 | try: 340 | self.turing_machine.execute_next_instruction() 341 | self.current_line_number = ( 342 | self.turing_machine.determine_next_instruction().line_number 343 | ) 344 | response_body = {} 345 | response_body["allThreadsContinued"] = True 346 | self.send_response(json_input["seq"], True, "next", None, response_body) 347 | 348 | stopped_event = {} 349 | stopped_event["reason"] = "step" 350 | stopped_event["threadId"] = 1 351 | stopped_event["allThreadsStopped"] = True 352 | self.send_event("stopped", stopped_event) 353 | 354 | except VarphiTuringMachineHaltedException: 355 | exited_event = {} 356 | exited_event["exitCode"] = 0 357 | self.send_event("exited", exited_event) 358 | output_event = {} 359 | output_event["category"] = "console" 360 | output_event["output"] = str(self.turing_machine.tape) 361 | self.send_event("output", output_event) 362 | terminated_event = {} 363 | self.send_event("terminated", terminated_event) 364 | 365 | def handle_step_in_request(self, json_input): 366 | """Handle a step in request from the debug adapter client.""" 367 | try: 368 | self.turing_machine.execute_next_instruction() 369 | self.current_line_number = ( 370 | self.turing_machine.determine_next_instruction().line_number 371 | ) 372 | response_body = {} 373 | response_body["allThreadsContinued"] = True 374 | self.send_response(json_input["seq"], True, "stepIn", None, response_body) 375 | 376 | stopped_event = {} 377 | stopped_event["reason"] = "step" 378 | stopped_event["threadId"] = 1 379 | stopped_event["allThreadsStopped"] = True 380 | self.send_event("stopped", stopped_event) 381 | 382 | except VarphiTuringMachineHaltedException: 383 | exited_event = {} 384 | exited_event["exitCode"] = 0 385 | self.send_event("exited", exited_event) 386 | output_event = {} 387 | output_event["category"] = "console" 388 | output_event["output"] = str(self.turing_machine.tape) 389 | self.send_event("output", output_event) 390 | terminated_event = {} 391 | self.send_event("terminated", terminated_event) 392 | 393 | def handle_step_out_request(self, json_input): 394 | """Handle a step out request from the debug adapter client.""" 395 | try: 396 | self.turing_machine.execute_next_instruction() 397 | self.current_line_number = ( 398 | self.turing_machine.determine_next_instruction().line_number 399 | ) 400 | response_body = {} 401 | response_body["allThreadsContinued"] = True 402 | self.send_response(json_input["seq"], True, "stepOut", None, response_body) 403 | 404 | stopped_event = {} 405 | stopped_event["reason"] = "step" 406 | stopped_event["threadId"] = 1 407 | stopped_event["allThreadsStopped"] = True 408 | self.send_event("stopped", stopped_event) 409 | 410 | except VarphiTuringMachineHaltedException: 411 | exited_event = {} 412 | exited_event["exitCode"] = 0 413 | self.send_event("exited", exited_event) 414 | output_event = {} 415 | output_event["category"] = "console" 416 | output_event["output"] = str(self.turing_machine.tape) 417 | self.send_event("output", output_event) 418 | terminated_event = {} 419 | self.send_event("terminated", terminated_event) 420 | 421 | def handle_continue_request(self, json_input): 422 | """Handle a continue request from the debug adapter client.""" 423 | while True: 424 | try: 425 | self.turing_machine.execute_next_instruction() 426 | self.current_line_number = ( 427 | self.turing_machine.determine_next_instruction().line_number 428 | ) 429 | if self.current_line_number in self.breakpoints: 430 | response_body = {} 431 | response_body["allThreadsContinued"] = True 432 | self.send_response( 433 | json_input["seq"], True, "continue", None, response_body 434 | ) 435 | stopped_event = {} 436 | stopped_event["reason"] = "breakpoint" 437 | stopped_event["threadId"] = 1 438 | stopped_event["allThreadsStopped"] = True 439 | self.send_event("stopped", stopped_event) 440 | return 441 | except VarphiTuringMachineHaltedException: 442 | exited_event = {} 443 | exited_event["exitCode"] = 0 444 | self.send_event("exited", exited_event) 445 | output_event = {} 446 | output_event["category"] = "console" 447 | output_event["output"] = str(self.turing_machine.tape) 448 | self.send_event("output", output_event) 449 | terminated_event = {} 450 | self.send_event("terminated", terminated_event) 451 | return 452 | 453 | def handle_disconnect_request(self, json_input): 454 | """Handle a disconnect request from the debug adapter client.""" 455 | self.send_response(json_input["seq"], True, "terminate") 456 | 457 | 458 | def main(initial_state: State | None): 459 | """Construct the Turing machine given an initial state and run it. 460 | 461 | Communicates with a debug adapter client. 462 | """ 463 | VarphiDebugAdapter(initial_state).loop() 464 | -------------------------------------------------------------------------------- /varphi/runtime/exceptions.py: -------------------------------------------------------------------------------- 1 | """varphi/runtime/exceptions.py""" 2 | 3 | 4 | class VarphiNoTallyException(Exception): 5 | """An exception raised when a tally is not found on the initial tape.""" 6 | 7 | 8 | class VarphiInvalidTapeCharacterException(Exception): 9 | """An exception raised when an invalid character is written to a tape.""" 10 | 11 | 12 | class VarphiTuringMachineHaltedException(Exception): 13 | """An exception raised when a Turing machine halts.""" 14 | 15 | 16 | class VarphiDomainError(Exception): 17 | """An exception raised when an input outside the domain of a Turing machine is encountered.""" 18 | -------------------------------------------------------------------------------- /varphi/runtime/types.py: -------------------------------------------------------------------------------- 1 | """varphi/types/varphi_types.py""" 2 | 3 | from __future__ import annotations 4 | import random 5 | from dataclasses import dataclass 6 | from enum import Enum, auto 7 | from typing import List 8 | from collections import defaultdict 9 | from .exceptions import VarphiTuringMachineHaltedException 10 | 11 | 12 | class TapeCharacter(Enum): 13 | """Enumeration representing the possible characters on the tape.""" 14 | 15 | BLANK = auto() 16 | TALLY = auto() 17 | 18 | 19 | class HeadDirection(Enum): 20 | """Enumeration representing the possible directions for the head to move.""" 21 | 22 | LEFT = auto() 23 | RIGHT = auto() 24 | 25 | 26 | @dataclass(frozen=True) 27 | class Instruction: 28 | """Represents an instruction for a Turing machine, detailing the next state, 29 | character to place, and the direction to move the head. 30 | """ 31 | 32 | next_state: State 33 | character_to_place: TapeCharacter 34 | direction_to_move: HeadDirection 35 | 36 | 37 | @dataclass(frozen=True) 38 | class LineNumberedInstruction(Instruction): 39 | """Represents an instruction for a Turing machine, detailing the next state, 40 | character to place, and the direction to move the head. 41 | 42 | This instruction is aware of its line number in the Varphi source. 43 | """ 44 | 45 | line_number: int 46 | 47 | 48 | class State: 49 | """Represents a state in the Turing machine. This includes instructions for 50 | both when the tape head is on a blank or tally. 51 | 52 | Note that a state can have multiple instructions for the same character, 53 | in the case of a non-deterministic machine. 54 | """ 55 | 56 | on_tally_instructions: List[Instruction] 57 | on_blank_instructions: List[Instruction] 58 | 59 | def __init__(self) -> None: 60 | """Initializes a State object.""" 61 | self.on_tally_instructions = [] 62 | self.on_blank_instructions = [] 63 | 64 | def add_on_tally_instruction(self, instruction: Instruction) -> None: 65 | """Adds an instruction for when the tape head is on a tally.""" 66 | if instruction in self.on_tally_instructions: 67 | return 68 | self.on_tally_instructions.append(instruction) 69 | 70 | def add_on_blank_instruction(self, instruction: Instruction) -> None: 71 | """Adds an instruction for when the tape head is on a blank.""" 72 | if instruction in self.on_blank_instructions: 73 | return 74 | self.on_blank_instructions.append(instruction) 75 | 76 | 77 | @dataclass 78 | class NamedState(State): 79 | """Represents a state in the Turing machine. This includes instructions for 80 | both when the tape head is on a blank or tally. 81 | 82 | Note that a state can have multiple instructions for the same character, 83 | in the case of a non-deterministic machine. 84 | 85 | This state is aware of its name. 86 | """ 87 | 88 | name: str 89 | 90 | def __init__(self, name: str) -> None: 91 | """Initializes a State object.""" 92 | self.name = name 93 | super().__init__() 94 | 95 | 96 | class Tape: 97 | """A class representing the tape of a Turing machine.""" 98 | 99 | _tape: defaultdict[int, TapeCharacter] 100 | _maximum_accessed_index: int 101 | _minimum_accessed_index: int 102 | 103 | def __init__(self, initial_values: list[TapeCharacter]) -> None: 104 | self._tape = defaultdict(lambda: TapeCharacter.BLANK) 105 | self._maximum_accessed_index = 0 106 | self._minimum_accessed_index = 0 107 | 108 | i = 0 109 | for initial_value in initial_values: 110 | self[i] = initial_value 111 | i += 1 112 | 113 | def _update_maximum_and_minimum_indices_accessed(self, index: int) -> None: 114 | self._maximum_accessed_index = max(self._maximum_accessed_index, index) 115 | self._minimum_accessed_index = min(self._minimum_accessed_index, index) 116 | 117 | def __getitem__(self, index: int) -> TapeCharacter: 118 | self._update_maximum_and_minimum_indices_accessed(index) 119 | return self._tape[index] 120 | 121 | def __setitem__(self, index: int, value: TapeCharacter) -> None: 122 | self._update_maximum_and_minimum_indices_accessed(index) 123 | self._tape[index] = value 124 | 125 | def __repr__(self) -> str: 126 | representation = "" 127 | for i in range(self._minimum_accessed_index, self._maximum_accessed_index + 1): 128 | representation += "1" if self._tape[i] == TapeCharacter.TALLY else "0" 129 | return representation 130 | 131 | 132 | class Head: 133 | """A class representing the head of a Turing machine.""" 134 | 135 | _tape: Tape 136 | _current_tape_cell_index: int 137 | 138 | def __init__(self, tape: Tape) -> None: 139 | self._tape = tape 140 | self._current_tape_cell_index = 0 141 | 142 | def right(self) -> None: 143 | """Move the head one cell to the right.""" 144 | self._current_tape_cell_index += 1 145 | 146 | def left(self) -> None: 147 | """Move the head one cell to the left.""" 148 | self._current_tape_cell_index -= 1 149 | 150 | def read(self) -> TapeCharacter: 151 | """Read the value of the current cell.""" 152 | return self._tape[self._current_tape_cell_index] 153 | 154 | def write(self, value: TapeCharacter) -> None: 155 | """Write a value to the current cell.""" 156 | self._tape[self._current_tape_cell_index] = value 157 | 158 | def __repr__(self) -> str: 159 | return str(self._current_tape_cell_index) 160 | 161 | 162 | class TuringMachine: # pylint: disable=R0903 163 | """A class representing a Turing machine.""" 164 | 165 | tape: Tape 166 | head: Head 167 | state: State 168 | 169 | def __init__(self, tape: Tape, initial_state: State) -> None: 170 | self.tape = tape 171 | self.head = Head(tape) 172 | self.state = initial_state 173 | 174 | def step(self): 175 | """Execute one step of the Turing machine and return the instruction that was executed. 176 | 177 | Raises `VarphiTuringMachineHaltedException` if the machine halts. 178 | """ 179 | tape_character = self.head.read() 180 | if tape_character == TapeCharacter.TALLY: 181 | possible_instructions_to_follow = self.state.on_tally_instructions 182 | else: 183 | possible_instructions_to_follow = self.state.on_blank_instructions 184 | if len(possible_instructions_to_follow) == 0: 185 | raise VarphiTuringMachineHaltedException() 186 | next_instruction = random.choice(possible_instructions_to_follow) 187 | self.state = next_instruction.next_state 188 | self.head.write(next_instruction.character_to_place) 189 | if next_instruction.direction_to_move == HeadDirection.LEFT: 190 | self.head.left() 191 | else: 192 | self.head.right() 193 | 194 | 195 | class DebugAdapterTuringMachine(TuringMachine): # pylint: disable=R0903 196 | """A class representing a Turing machine.""" 197 | 198 | tape: Tape 199 | head: Head 200 | state: NamedState 201 | _next_instruction: LineNumberedInstruction | None 202 | _armed: bool 203 | 204 | def __init__(self, tape: Tape, initial_state: NamedState) -> None: 205 | super().__init__(tape, initial_state) 206 | self._next_instruction = None 207 | self._armed = False 208 | 209 | def determine_next_instruction(self) -> LineNumberedInstruction: 210 | """Determine the next instruction to be executed by this Turing machine.""" 211 | if self._armed: 212 | raise Exception( # pylint: disable=W0719 213 | "Attempted to determine next instruction when an instruction is already armed." 214 | ) 215 | tape_character = self.head.read() 216 | if tape_character == TapeCharacter.TALLY: 217 | possible_instructions_to_follow = self.state.on_tally_instructions 218 | else: 219 | possible_instructions_to_follow = self.state.on_blank_instructions 220 | if len(possible_instructions_to_follow) == 0: 221 | raise VarphiTuringMachineHaltedException() 222 | self._next_instruction = random.choice(possible_instructions_to_follow) 223 | self._armed = True 224 | return self._next_instruction 225 | 226 | def execute_next_instruction(self): 227 | """Execute one step of the Turing machine and return the instruction that was executed. 228 | 229 | Raises `VarphiTuringMachineHaltedException` if the machine halts. 230 | """ 231 | if not self._armed: 232 | raise Exception( # pylint: disable=W0719 233 | "Attempted to execute next instruction when no instruction armed." 234 | ) 235 | self.state = self._next_instruction.next_state 236 | self.head.write(self._next_instruction.character_to_place) 237 | if self._next_instruction.direction_to_move == HeadDirection.LEFT: 238 | self.head.left() 239 | else: 240 | self.head.right() 241 | self._armed = False 242 | -------------------------------------------------------------------------------- /varphi_interpreter/vpi.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=R0801 2 | """varphi_interpreter/v2p.py""" 3 | 4 | import argparse 5 | 6 | from varphi.compilation import varphi_file_to_python 7 | from varphi.compilation.varphi_translator import ( 8 | VarphiTranslatorCLITarget, 9 | VarphiTranslatorCLIDebuggerTarget, 10 | VarphiTranslatorCLIDebugAdapterTarget, 11 | ) 12 | import varphi.runtime.cli 13 | import varphi.runtime.cli_debugger 14 | import varphi.runtime.debug_adapter 15 | import varphi.runtime.cli_with_complexity 16 | import varphi.runtime.cli_with_prompts 17 | 18 | 19 | VPI_VERSION = "1.1.0" 20 | 21 | def main(): 22 | """The main function for the vpi command line interface.""" 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument( 25 | "program", type=str, help="The path to the Varphi source file to be translated." 26 | ) 27 | 28 | parser.add_argument( 29 | "-d", 30 | "--debug", 31 | action="store_true", 32 | help="Compile the program in debug mode.", 33 | ) 34 | 35 | parser.add_argument( 36 | "-a", 37 | "--debug-adapter", 38 | action="store_true", 39 | help="Compile the program to a debug adapter target.", 40 | ) 41 | 42 | parser.add_argument( 43 | "-c", 44 | "--complexity", 45 | action="store_true", 46 | help="Enable prompting and print the number of steps taken by the machine and the number of tape cells accessed.", 47 | ) 48 | 49 | parser.add_argument( 50 | "-p", 51 | "--enable-prompts", 52 | action="store_true", 53 | help="Prompt explicitly for the input tape and explicitly specify the output tape.", 54 | ) 55 | 56 | args = parser.parse_args() 57 | if args.debug: 58 | exec( # pylint: disable=W0122 59 | varphi_file_to_python( 60 | args.program, 61 | "from varphi.runtime.cli_debugger import main\n", 62 | translator_type=VarphiTranslatorCLIDebuggerTarget, 63 | ), 64 | {"__name__": "__main__"}, 65 | ) 66 | elif args.debug_adapter: 67 | exec( # pylint: disable=W0122 68 | varphi_file_to_python( 69 | args.program, 70 | "from varphi.runtime.debug_adapter import main\n", 71 | translator_type=VarphiTranslatorCLIDebugAdapterTarget, 72 | ), 73 | {"__name__": "__main__"}, 74 | ) 75 | elif args.complexity: 76 | exec( # pylint: disable=W0122 77 | varphi_file_to_python( 78 | args.program, 79 | "from varphi.runtime.cli_with_complexity import main\n", 80 | translator_type=VarphiTranslatorCLITarget, 81 | ), 82 | {"__name__": "__main__"}, 83 | ) 84 | elif args.enable_prompts: 85 | exec( # pylint: disable=W0122 86 | varphi_file_to_python( 87 | args.program, 88 | "from varphi.runtime.cli_with_prompts import main\n", 89 | translator_type=VarphiTranslatorCLITarget, 90 | ), 91 | {"__name__": "__main__"}, 92 | ) 93 | else: 94 | exec( # pylint: disable=W0122 95 | varphi_file_to_python( 96 | args.program, 97 | "from varphi.runtime.cli import main\n", 98 | translator_type=VarphiTranslatorCLITarget, 99 | ), 100 | {"__name__": "__main__"}, 101 | ) 102 | 103 | if __name__ == "__main__": 104 | main() 105 | --------------------------------------------------------------------------------