├── .clang-format ├── .github ├── check_license.sh ├── check_python_scripts.sh └── workflows │ ├── check-install.yml │ ├── manylinux-install-cmake.sh │ ├── presubmit.yml │ ├── publish-to-pypi.yml │ └── wheel.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yml ├── .style.yapf ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── _static │ ├── favicon.svg │ ├── image │ │ └── fasm-diagram.png │ └── logo.svg ├── conf.py ├── environment.yml ├── index.rst ├── requirements.txt └── specification │ ├── introduction.rst │ ├── line.rst │ └── syntax.rst ├── environment.yml ├── examples ├── blank.fasm ├── comment.fasm ├── feature_only.fasm └── many.fasm ├── fasm ├── __init__.py ├── model.py ├── output.py ├── parser │ ├── __init__.py │ ├── antlr.py │ ├── antlr_to_tuple.pyx │ ├── fasm.tx │ └── textx.py ├── tool.py └── version.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── src ├── CMakeLists.txt ├── ParseFasm.cpp ├── ParseFasmRun.cpp ├── ParseFasmTests.cpp └── antlr │ ├── FasmLexer.g4 │ └── FasmParser.g4 ├── tests ├── __init__.py └── test_simple.py ├── third_party └── antlr4_lib │ ├── LICENSE.txt │ ├── README.md │ └── antlr-4.9.3-complete.jar ├── tox.ini └── update_version.py /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Chromium 4 | IndentWidth: 8 5 | TabWidth: 4 6 | UseTab: Never 7 | -------------------------------------------------------------------------------- /.github/check_license.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2017-2022 F4PGA Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | echo 19 | echo "===========================" 20 | echo "Check SPDX identifier" 21 | echo "===========================" 22 | echo 23 | 24 | ERROR_FILES="" 25 | FILES_TO_CHECK=`git ls-files | grep -ve '^third_party\|^\.' | grep -e '\.sh$\|\.py$\|\.cpp$\|\.h$\|CMakeLists.txt$\|Makefile$'` 26 | 27 | for file in $FILES_TO_CHECK; do 28 | echo "Checking $file" 29 | grep -q "SPDX-License-Identifier" $file || ERROR_FILES="$ERROR_FILES $file" 30 | done 31 | 32 | if [ ! -z "$ERROR_FILES" ]; then 33 | for file in $ERROR_FILES; do 34 | echo "ERROR: $file does not have license information." 35 | done 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /.github/check_python_scripts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2017-2022 F4PGA Authors 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | echo 19 | echo "===================================" 20 | echo "Check Python UTF coding and shebang" 21 | echo "===================================" 22 | echo 23 | 24 | ERROR_FILES_SHEBANG="" 25 | ERROR_FILES_UTF_CODING="" 26 | FILES_TO_CHECK=`git ls-files | grep -ve '^third_party\|^\.' | grep -e '.py$'` 27 | 28 | for file in $FILES_TO_CHECK; do 29 | echo "Checking $file" 30 | grep -q "\#\!/usr/bin/env python3" $file || ERROR_FILES_SHEBANG="$ERROR_FILES_SHEBANG $file" 31 | grep -q "\#.*coding: utf-8" $file || ERROR_FILES_UTF_CODING="$ERROR_FILES_UTF_CODING $file" 32 | done 33 | 34 | if [ ! -z "$ERROR_FILES_SHEBANG" ]; then 35 | for file in $ERROR_FILES_SHEBANG; do 36 | echo "ERROR: $file does not have the python3 shebang." 37 | done 38 | exit 1 39 | fi 40 | 41 | if [ ! -z "$ERROR_FILES_UTF_CODING" ]; then 42 | for file in $ERROR_FILES_UTF_CODING; do 43 | echo "ERROR: $file does not have the UTF encoding set." 44 | echo "Add # coding: utf-8" 45 | done 46 | exit 1 47 | fi 48 | 49 | echo 50 | -------------------------------------------------------------------------------- /.github/workflows/check-install.yml: -------------------------------------------------------------------------------- 1 | name: Install from 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | 8 | jobs: 9 | 10 | GitHub: 11 | strategy: 12 | matrix: 13 | os: [windows-latest, macos-latest, ubuntu-20.04] 14 | fail-fast: false 15 | 16 | name: GitHub 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Install dependencies (Ubuntu) 21 | if: startsWith(matrix.os, 'ubuntu') 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y cmake default-jre-headless uuid-dev libantlr4-runtime-dev 25 | 26 | - name: Install dependencies (Mac OS X) 27 | if: startsWith(matrix.os, 'macos') 28 | run: | 29 | true 30 | 31 | - name: Install dependencies (Windows) 32 | if: startsWith(matrix.os, 'windows') 33 | run: | 34 | true 35 | 36 | - name: 🐍 Set up Python 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: 3.x 40 | 41 | - name: Install pip+wheel 42 | run: | 43 | python -m pip install --user --upgrade wheel 44 | python -m pip install --user --upgrade pip 45 | 46 | - name: Test installation 47 | shell: bash 48 | run: | 49 | python -m pip install git+https://github.com/${GITHUB_REPOSITORY}.git@${GITHUB_SHA}#egg=fasm 50 | 51 | - name: Run Smoke Test 52 | run: | 53 | fasm --help 54 | 55 | # PyPi: 56 | # runs-on: ubuntu-20.04 57 | # name: PyPi 58 | -------------------------------------------------------------------------------- /.github/workflows/manylinux-install-cmake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | # Remove the ancient version of cmake 7 | yum remove cmake -y 8 | 9 | # Add in curl 10 | yum install wget -y 11 | 12 | yum install java-1.8.0-openjdk uuid uuid-devel libuuid libuuid-devel -y 13 | 14 | # Download new cmake 15 | wget https://github.com/Kitware/CMake/releases/download/v3.19.4/cmake-3.19.4-Linux-x86_64.sh -O /tmp/cmake.sh 16 | chmod a+x /tmp/cmake.sh 17 | 18 | # Install cmake into /usr 19 | /tmp/cmake.sh --prefix=/usr --skip-license 20 | -------------------------------------------------------------------------------- /.github/workflows/presubmit.yml: -------------------------------------------------------------------------------- 1 | # Checks code that code meets requirements for a pull request. 2 | # Any automated checks for code quality and compliance belongs here. 3 | name: presubmit 4 | on: [push, pull_request] 5 | jobs: 6 | check: 7 | name: Source checks 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | # Always clone the full depth so git-describe works. 13 | fetch-depth: 0 14 | submodules: true 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | - name: Install dependencies 18 | run: | 19 | sudo apt update 20 | sudo apt install clang-format 21 | python -m pip install --upgrade pip 22 | python -m pip install -r requirements.txt 23 | - name: Python style check 24 | run: | 25 | make format lint 26 | test $(git status --porcelain | wc -l) -eq 0 || { git diff; false; } 27 | - name: C++ style check 28 | run: | 29 | make format-cpp 30 | test $(git status --porcelain | wc -l) -eq 0 || { git diff; false; } 31 | - name: Check license 32 | run: make check-license 33 | - name: Python checks 34 | run: make check-python-scripts 35 | test: 36 | name: Test Python package 37 | runs-on: ubuntu-20.04 38 | strategy: 39 | matrix: 40 | antlr_runtime_type: [static, shared] 41 | python_version: [3.5, 3.6, 3.7, 3.8, 3.9] 42 | include: 43 | - python-version: 3.5 44 | TOXENV: py35 45 | - python-version: 3.6 46 | TOXENV: py36 47 | - python-version: 3.7 48 | TOXENV: py37 49 | - python-version: 3.8 50 | TOXENV: py38 51 | - python-version: 3.9 52 | TOXENV: py39 53 | fail-fast: false 54 | steps: 55 | - uses: actions/checkout@v2 56 | with: 57 | # Always clone the full depth so git-describe works. 58 | fetch-depth: 0 59 | submodules: true 60 | - name: Set up Python ${{ matrix.python-version }} 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | - name: Install dependencies 65 | run: | 66 | sudo apt update 67 | sudo apt install cmake default-jre-headless uuid-dev libantlr4-runtime-dev 68 | python -m pip install --upgrade pip 69 | python -m pip install -r requirements.txt 70 | python update_version.py 71 | - name: Tox 72 | run: | 73 | export CMAKE_BUILD_PARALLEL_LEVEL=1 74 | export ANTLR4_RUNTIME_TYPE=${{ matrix.antlr_runtime_type }} 75 | tox -e ${{ matrix.TOXENV }} 76 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | Source: 11 | runs-on: ubuntu-20.04 12 | name: Source 13 | 14 | steps: 15 | - name: 🧰 Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | # Always clone the full depth so git-describe works. 19 | fetch-depth: 0 20 | submodules: true 21 | 22 | - name: Install dependencies (Ubuntu) 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install -y cmake default-jre-headless uuid-dev libantlr4-runtime-dev 26 | 27 | - name: 🐍 Set up Python 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: 3.x 31 | 32 | - name: Install build dependencies 33 | run: | 34 | python -m pip install -U pip 35 | python -m pip install -r requirements.txt 36 | python update_version.py 37 | 38 | - name: Install package dependencies 39 | run: | 40 | python setup.py install 41 | 42 | - name: 🚧 Build distribution 📦 43 | run: | 44 | python setup.py sdist 45 | 46 | - name: Check distribution 📦 47 | run: | 48 | twine check dist/* 49 | 50 | - name: 📤 Publish source to Test PyPI 51 | env: 52 | TWINE_USERNAME: __token__ 53 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }} 54 | if: env.TWINE_PASSWORD != null 55 | run: | 56 | twine upload --skip-existing --verbose --repository testpypi dist/*.tar.gz 57 | 58 | - name: 📤 Publish source to PyPI 59 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'chipsalliance/fasm' 60 | env: 61 | TWINE_USERNAME: __token__ 62 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 63 | run: | 64 | twine upload dist/* 65 | 66 | Linux: 67 | strategy: 68 | matrix: 69 | include: 70 | - { name: '3.6', python-version: 'cp36-cp36m' } 71 | - { name: '3.7', python-version: 'cp37-cp37m' } 72 | - { name: '3.8', python-version: 'cp38-cp38' } 73 | - { name: '3.9', python-version: 'cp39-cp39' } 74 | fail-fast: false 75 | 76 | name: '${{ matrix.name }} • manylinux' 77 | runs-on: ubuntu-latest 78 | 79 | steps: 80 | - name: 🧰 Checkout 81 | uses: actions/checkout@v2 82 | with: 83 | # Always clone the full depth so git-describe works. 84 | fetch-depth: 0 85 | submodules: true 86 | 87 | - name: 🐍 Set up Python 88 | uses: actions/setup-python@v2 89 | 90 | - name: Install build dependencies 91 | run: | 92 | python -m pip install -U pip 93 | python -m pip install twine auditwheel 94 | python update_version.py 95 | 96 | - name: 🚧 Build distribution 📦 97 | uses: RalfG/python-wheels-manylinux-build@v0.3.3-manylinux2010_x86_64 98 | with: 99 | build-requirements: 'cython' 100 | pre-build-command: 'bash .github/workflows/manylinux-install-cmake.sh' 101 | python-versions: ${{ matrix.python-version }} 102 | #pip-wheel-args: '-w ./dist --no-deps --verbose' 103 | 104 | - name: List distribution 📦 105 | run: | 106 | # Fix permissions 107 | echo "::group::Fixing permission" 108 | ls -l dist/* 109 | echo $USER 110 | whoami 111 | sudo chown -R $USER dist 112 | ls -l dist/* 113 | echo "::endgroup::" 114 | # Remove the non-manylinux versions 115 | rm -v dist/*linux_x86_64*.whl 116 | ls -l dist/* 117 | 118 | - name: Check distribution 📦 119 | run: | 120 | for WHEEL in dist/*.whl; do 121 | echo 122 | echo "::group::Checking $WHEEL" 123 | echo 124 | python -m zipfile --list $WHEEL 125 | echo 126 | auditwheel show $WHEEL 127 | echo 128 | twine check $WHEEL 129 | echo 130 | echo "::endgroup::" 131 | done 132 | 133 | - name: 📤 Publish wheels to Test PyPI 134 | env: 135 | TWINE_USERNAME: __token__ 136 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }} 137 | if: env.TWINE_PASSWORD != null 138 | run: | 139 | twine upload --skip-existing --verbose --repository testpypi dist/*.whl 140 | 141 | - name: 📤 Publish wheels to PyPI 142 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'chipsalliance/fasm' 143 | env: 144 | TWINE_USERNAME: __token__ 145 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 146 | run: | 147 | twine upload dist/*.whl 148 | 149 | MacAndWindows: 150 | strategy: 151 | matrix: 152 | os: [windows-latest, macos-latest] 153 | python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy-3.6', 'pypy-3.7' ] 154 | fail-fast: false 155 | 156 | name: ${{ matrix.python-version }} • ${{ matrix.os }} 157 | runs-on: ${{ matrix.os }} 158 | 159 | steps: 160 | - name: 🧰 Checkout 161 | uses: actions/checkout@v2 162 | with: 163 | # Always clone the full depth so git-describe works. 164 | fetch-depth: 0 165 | submodules: true 166 | 167 | - name: Install dependencies (Mac OS X) 168 | if: startsWith(matrix.os, 'macos') 169 | run: | 170 | true 171 | 172 | - name: Install dependencies (Windows) 173 | if: startsWith(matrix.os, 'windows') 174 | run: | 175 | true 176 | 177 | - name: 🐍 Set up Python ${{ matrix.python-version }} 178 | uses: actions/setup-python@v2 179 | with: 180 | python-version: ${{ matrix.python-version }} 181 | 182 | - name: Install build dependencies 183 | run: | 184 | python -m pip install -U pip 185 | python -m pip install -r requirements.txt 186 | python update_version.py 187 | 188 | - name: Install package dependencies 189 | run: | 190 | python setup.py install 191 | 192 | - name: 🚧 Build distribution 📦 193 | run: | 194 | python setup.py bdist_wheel 195 | 196 | - name: Check distribution 📦 197 | shell: bash 198 | run: | 199 | python -m zipfile -l dist/* 200 | echo 201 | twine check dist/* 202 | 203 | - name: 📤 Publish wheels to Test PyPI 204 | env: 205 | TWINE_USERNAME: __token__ 206 | TWINE_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }} 207 | if: env.TWINE_PASSWORD != null 208 | run: | 209 | twine upload --skip-existing --verbose --repository testpypi dist/*.whl 210 | 211 | - name: 📤 Publish wheels to PyPI 212 | if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'chipsalliance/fasm' 213 | env: 214 | TWINE_USERNAME: __token__ 215 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 216 | run: | 217 | twine upload dist/*.whl 218 | -------------------------------------------------------------------------------- /.github/workflows/wheel.yml: -------------------------------------------------------------------------------- 1 | # Builds a binary distibutable package. 2 | # Minimal functionality checks may be run as part of the build process, but 3 | # source code checks and extensive functionality checks (e.g. tox) belong in presubmit.yml 4 | name: Python wheels 5 | on: [push, pull_request] 6 | jobs: 7 | wheels: 8 | runs-on: ubuntu-20.04 9 | strategy: 10 | matrix: 11 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 12 | fail-fast: false 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | # Always clone the full depth so git-describe works. 17 | fetch-depth: 0 18 | submodules: true 19 | - name: Install dependencies 20 | run: | 21 | sudo apt update 22 | sudo apt install cmake default-jre-headless uuid-dev libantlr4-runtime-dev 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: python -m pip install --upgrade -r requirements.txt 29 | - name: Generate version 30 | run: python update_version.py 31 | - name: Build wheels 32 | run: | 33 | export CMAKE_BUILD_PARALLEL_LEVEL=1 34 | python setup.py bdist_wheel 35 | - name: Test wheel installation 36 | run: | 37 | python -m pip install dist/*.whl 38 | (cd tests; python test_simple.py) 39 | - uses: actions/upload-artifact@v2 40 | with: 41 | name: fasm 42 | path: dist 43 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | env/ 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # Emacs temporary files 120 | *~ 121 | 122 | # Generated files 123 | /fasm/parser/tags.py 124 | /fasm/parser/antlr_to_tuple.c 125 | /fasm/version.py 126 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/googletest"] 2 | path = third_party/googletest 3 | url = https://github.com/google/googletest.git 4 | [submodule "third_party/antlr4"] 5 | path = third_party/antlr4 6 | url = https://github.com/antlr/antlr4.git 7 | [submodule "third_party/make-env"] 8 | path = third_party/make-env 9 | url = https://github.com/SymbiFlow/make-env.git 10 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | formats: 13 | - htmlzip 14 | 15 | conda: 16 | environment: docs/environment.yml 17 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | split_before_expression_after_opening_paren = True 4 | split_before_first_argument = True 5 | split_complex_comprehension = True 6 | split_penalty_comprehension = 2100 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of fasm's significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Antmicro 8 | Google LLC 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the README 2 | include *.md 3 | 4 | # Include the license file 5 | include LICENSE 6 | include AUTHORS 7 | 8 | # Development support files 9 | include Makefile 10 | include requirements.txt 11 | include environment.yml 12 | include *.yapf 13 | include *.yml 14 | include pyproject.toml 15 | include .clang-format 16 | 17 | # Examples 18 | graft examples 19 | 20 | # Tests 21 | graft tests 22 | 23 | # Docs 24 | recursive-include docs *.py 25 | recursive-include docs *.rst 26 | recursive-include docs *.txt 27 | recursive-include docs *.yml 28 | recursive-include docs *.png 29 | recursive-include docs Makefile 30 | recursive-include tests * 31 | prune docs/_* 32 | prune docs/env 33 | 34 | # textx based parser 35 | recursive-include fasm/parser *.tx 36 | recursive-include fasm/parser *.pyx 37 | exclude fasm/parser/*.c 38 | 39 | # antlr based parser 40 | include src/* 41 | include src/antlr/* 42 | 43 | # Excludes 44 | global-exclude .git 45 | exclude .gitmodules 46 | exclude update_version.py 47 | prune third_party 48 | prune .github 49 | prune __pycache__ 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2022 F4PGA Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | # The top directory where environment will be created. 18 | TOP_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST)))) 19 | 20 | # A pip `requirements.txt` file. 21 | # https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format 22 | REQUIREMENTS_FILE := requirements.txt 23 | 24 | # A conda `environment.yml` file. 25 | # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html 26 | ENVIRONMENT_FILE := environment.yml 27 | 28 | # Rule to checkout the git submodule if it wasn't cloned. 29 | $(TOP_DIR)/third_party/make-env/conda.mk: $(TOP_DIR)/.gitmodules 30 | cd $(TOP_DIR); git submodule update --init third_party/make-env 31 | touch $(TOP_DIR)/third_party/make-env/conda.mk 32 | 33 | -include $(TOP_DIR)/third_party/make-env/conda.mk 34 | 35 | # Update the version file 36 | # ------------------------------------------------------------------------ 37 | fasm/version.py: update_version.py | $(CONDA_ENV_PYTHON) 38 | $(IN_CONDA_ENV) python ./update_version.py 39 | 40 | setup.py: fasm/version.py 41 | touch setup.py --reference fasm/version.py 42 | 43 | # Build/install into the conda environment. 44 | # ------------------------------------------------------------------------ 45 | build-clean: 46 | rm -rf dist fasm.egg-info 47 | 48 | .PHONY: build-clean 49 | 50 | build: setup.py | $(CONDA_ENV_PYTHON) 51 | make build-clean 52 | $(IN_CONDA_ENV) python setup.py sdist bdist_wheel 53 | 54 | .PHONY: build 55 | 56 | # Install into environment 57 | install: setup.py | $(CONDA_ENV_PYTHON) 58 | $(IN_CONDA_ENV) python setup.py develop 59 | 60 | .PHONY: install 61 | 62 | 63 | # Build/install locally rather than inside the environment. 64 | # ------------------------------------------------------------------------ 65 | local-build: setup.py 66 | python setup.py build 67 | 68 | .PHONY: local-build 69 | 70 | local-build-shared: setup.py 71 | python setup.py build --antlr-runtime=shared 72 | 73 | .PHONY: local-build-shared 74 | 75 | local-install: setup.py 76 | python setup.py install 77 | 78 | .PHONY: local-install 79 | 80 | 81 | # Test, lint, auto-format. 82 | # ------------------------------------------------------------------------ 83 | 84 | # Run the tests 85 | test: fasm/version.py | $(CONDA_ENV_PYTHON) 86 | $(IN_CONDA_ENV) py.test -s tests 87 | 88 | .PHONY: test 89 | 90 | # Find files to apply tools to while ignoring files. 91 | define with_files 92 | $(IN_CONDA_ENV) git ls-files | grep -ve '^third_party\|^\.|^env' | grep -e $(1) | xargs -r -P $$(nproc) $(2) 93 | endef 94 | 95 | # Lint the python files 96 | lint: | $(CONDA_ENV_PYTHON) 97 | $(call with_py_files, flake8) 98 | 99 | .PHONY: lint 100 | 101 | # Format the python files 102 | define with_py_files 103 | $(call with_files, '.py$$', $(1)) 104 | endef 105 | 106 | PYTHON_FORMAT ?= yapf 107 | format-py: | $(CONDA_ENV_PYTHON) 108 | $(call with_py_files, yapf -p -i) 109 | 110 | .PHONY: format-py 111 | 112 | # Format the C++ files 113 | define with_cpp_files 114 | $(call with_files, '\.cpp$$\|\.h$$', $(1)) 115 | endef 116 | 117 | format-cpp: 118 | $(call with_cpp_files, clang-format -style=file -i) 119 | 120 | .PHONY: format-cpp 121 | 122 | # Format all the files! 123 | format: format-py format-cpp 124 | true 125 | 126 | # Check - ??? 127 | check: setup.py | $(CONDA_ENV_PYTHON) 128 | $(IN_CONDA_ENV) python setup.py check -m -s 129 | 130 | .PHONY: check 131 | 132 | # Check files have license headers. 133 | check-license: 134 | @./.github/check_license.sh 135 | 136 | .PHONY: check-license 137 | 138 | # Check python scripts have the correct headers. 139 | check-python-scripts: 140 | @./.github/check_python_scripts.sh 141 | 142 | .PHONY: check-python-scripts 143 | 144 | # Upload to PyPI servers 145 | # ------------------------------------------------------------------------ 146 | 147 | # PYPI_TEST = --repository-url https://test.pypi.org/legacy/ 148 | PYPI_TEST = --repository testpypi 149 | 150 | # Check before uploading 151 | upload-check: build | $(CONDA_ENV_PYTHON) 152 | $(IN_CONDA_ENV) twine check dist/* 153 | 154 | .PHONY: upload-check 155 | 156 | # Upload to test.pypi.org 157 | upload-test: check | $(CONDA_ENV_PYTHON) 158 | $(IN_CONDA_ENV) twine upload ${PYPI_TEST} dist/*.tar.gz 159 | $(IN_CONDA_ENV) twine upload ${PYPI_TEST} dist/*.whl 160 | 161 | .PHONY: upload-test 162 | 163 | # Upload to the real pypi.org 164 | upload: check | $(CONDA_ENV_PYTHON) 165 | $(IN_CONDA_ENV) twine upload ${PYPI_TEST} dist/*.tar.gz 166 | $(IN_CONDA_ENV) twine upload ${PYPI_TEST} dist/*.whl 167 | 168 | .PHONY: upload 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FPGA Assembly (FASM) Parser and Generation library 2 | 3 | This repository documents the FASM file format and provides parsing libraries and simple tooling for working with FASM files. 4 | 5 | It provides both a pure Python parser based on `textx` and a significantly faster C parser based on `ANTLR`. The library will try and use the ANTLR parser first and fall back to the `textx` parser if the compiled module is not found. 6 | 7 | Which parsers are supported by your currently install can be found via `python3 -c "import fasm.parser as p; print(p.available)`. The currently in use parser can be found via `fasm.parser.implementation`. 8 | 9 | It is highly recommended to use the ANTLR parser as it is about 15 times faster. 10 | 11 | functions for parsing and generating FASM files. 12 | 13 | ## Build Instructions 14 | 15 | CMake is required, and ANTLR has a few dependencies: 16 | 17 | sudo apt install cmake default-jre-headless uuid-dev libantlr4-runtime-dev 18 | 19 | Pull dependencies in `third_party`: 20 | 21 | git submodule update --init 22 | 23 | Build: 24 | 25 | make build 26 | 27 | Test with: 28 | 29 | python setup.py test 30 | 31 | The ANTLR runtime can either be linked statically or as a shared library. Use the 32 | `--antlr-runtime=[static|shared]` flag to select between the two modes e.g.: 33 | 34 | python setup.py install --antlr-runtime=shared 35 | 36 | Or, using `pip`: 37 | 38 | pip install . --install-option="--antlr-runtime=shared" --no-use-pep517 39 | 40 | The runtime will be built and statically linked by default. This flag is available in the build_ext, build, develop, and install commands. 41 | 42 | The --no-use-pep517 flag is needed because there is currently no way to pass flags with PEP517. 43 | Relevant issue: https://github.com/pypa/pip/issues/5771 44 | 45 | ## FPGA Assembly (FASM) 46 | 47 | FPGA Assembly is a file format designed by the 48 | [F4PGA Project](https://f4pga.org/) developers to provide a plain 49 | text file format for configuring the internals of an FPGA. 50 | 51 | It is designed to allow FPGA place and route to not care about the *actual* 52 | bitstream format used on an FPGA. 53 | 54 | ![FASM Ecosystem Diagram](docs/_static/image/fasm-diagram.png) 55 | 56 | ### Properties 57 | 58 | * Removing a line from a FASM file leaves you with a valid FASM file. 59 | * Allow annotation with human readable comments. 60 | * Allow annotation with "computer readable" comments. 61 | * Has syntactic sugar for expressing memory / lut init bits / other large 62 | arrays of data. 63 | * Has a canonical form. 64 | * Does not require any specific bitstream format. 65 | 66 | ### Supported By 67 | 68 | FASM is currently supported by the 69 | [F4PGA Verilog to Routing fork](https://github.com/f4pga/vtr-verilog-to-routing), 70 | but we hope to get it merged upstream sometime soon. 71 | 72 | It is also used by [Project X-Ray](https://github.com/f4pga/prjxray). 73 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2022 F4PGA Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # Minimal makefile for Sphinx documentation 17 | 18 | 19 | SHELL = /bin/bash 20 | MAKEDIR := $(dir $(lastword $(MAKEFILE_LIST))) 21 | 22 | # You can set these variables from the command line. 23 | SPHINXOPTS = 24 | SPHINXBUILD = [ -e env/bin/activate ] && source env/bin/activate; sphinx-build 25 | SPHINXAUTOBUILD = [ -e env/bin/activate ] && source env/bin/activate; sphinx-autobuild 26 | SPHINXPROJ = F4PGAFASM 27 | SOURCEDIR = . 28 | BUILDDIR = _build 29 | OSFLAG = 30 | UNAME_S := $(shell uname -s) 31 | ifneq (, $(findstring Linux, $(UNAME_S))) 32 | OSFLAG := Linux 33 | endif 34 | ifeq ($(UNAME_S), Darwin) 35 | OSFLAG := MacOSX 36 | endif 37 | ifneq (, $(findstring Cygwin, $(UNAME_S))) 38 | OSFLAG := Linux 39 | endif 40 | ifneq (, $(findstring MINGW, $(UNAME_S))) 41 | OSFLAG := Linux 42 | endif 43 | 44 | 45 | # Put it first so that "make" without argument is like "make help". 46 | help: 47 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 48 | 49 | livehtml: 50 | @$(SPHINXAUTOBUILD) -b html --ignore \*.swp --ignore \*~ $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" 51 | 52 | .PHONY: help livehtml Makefile 53 | 54 | 55 | env/Miniconda3-latest-$(OSFLAG)-x86_64.sh: 56 | mkdir env 57 | wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$(OSFLAG)-x86_64.sh -O env/Miniconda3-latest-$(OSFLAG)-x86_64.sh 58 | chmod a+x env/Miniconda3-latest-$(OSFLAG)-x86_64.sh 59 | 60 | env: 61 | rm -rf env 62 | make env/Miniconda3-latest-$(OSFLAG)-x86_64.sh 63 | ./env/Miniconda3-latest-$(OSFLAG)-x86_64.sh -p $(PWD)/env -b -f 64 | source env/bin/activate; conda config --system --add envs_dirs $(PWD)/env/envs 65 | source env/bin/activate; conda config --system --add pkgs_dirs $(PWD)/env/pkgs 66 | source env/bin/activate; conda env update --name base --file ./environment.yml 67 | 68 | .PHONY: env 69 | 70 | # Catch-all target: route all unknown targets to Sphinx using the new 71 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 72 | %: 73 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 74 | -------------------------------------------------------------------------------- /docs/_static/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/image/fasm-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chipsalliance/fasm/ffafe821bae68637fe46e36bcfd2a01b97cdf6f2/docs/_static/image/fasm-diagram.png -------------------------------------------------------------------------------- /docs/_static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 37 | 44 | 50 | 54 | 55 | 57 | 59 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | # This file only contains a selection of the most common options. For a full 21 | # list see the documentation: 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 23 | 24 | from pathlib import Path 25 | from re import sub as re_sub 26 | from os import environ, path as os_path, popen 27 | # from sys import path as sys_path 28 | 29 | # sys_path.insert(0, os_path.abspath('.')) 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | project = 'FPGA Assembly (FASM)' 34 | author = 'F4PGA Authors' 35 | copyright = f'{author}, 2018 - 2022' 36 | 37 | extensions = [ 38 | 'sphinx.ext.autodoc', 39 | 'sphinx.ext.autosummary', 40 | 'sphinx.ext.doctest', 41 | 'sphinx.ext.imgmath', 42 | 'sphinx.ext.napoleon', 43 | 'sphinx.ext.todo', 44 | 'sphinx_markdown_tables', 45 | ] 46 | 47 | templates_path = ['_templates'] 48 | 49 | source_suffix = ['.rst', '.md'] 50 | 51 | master_doc = 'index' 52 | 53 | on_rtd = environ.get('READTHEDOCS', None) == 'True' 54 | 55 | if on_rtd: 56 | docs_dir = os_path.abspath(os_path.dirname(__file__)) 57 | print("Docs dir is:", docs_dir) 58 | import subprocess 59 | subprocess.call('git fetch origin --unshallow', cwd=docs_dir, shell=True) 60 | subprocess.check_call('git fetch origin --tags', cwd=docs_dir, shell=True) 61 | 62 | release = re_sub('^v', '', popen('git describe').read().strip()) 63 | # The short X.Y version. 64 | version = release 65 | 66 | exclude_patterns = ['_build', 'env', 'Thumbs.db', '.DS_Store'] 67 | 68 | pygments_style = 'default' 69 | 70 | todo_include_todos = True 71 | 72 | # -- Options for HTML output ---------------------------------------------- 73 | 74 | # Enable github links when not on readthedocs 75 | if not on_rtd: 76 | html_context = { 77 | "display_github": True, # Integrate GitHub 78 | "github_user": "chipsalliance", # Username 79 | "github_repo": "fasm", # Repo name 80 | "github_version": "master", # Version 81 | "conf_py_path": "/docs/", 82 | } 83 | 84 | html_show_sourcelink = True 85 | 86 | html_theme = 'sphinx_f4pga_theme' 87 | 88 | html_theme_options = { 89 | 'repo_name': 'chipsalliance/fasm', 90 | 'github_url': 'https://github.com/chipsalliance/fasm', 91 | 'globaltoc_collapse': True, 92 | 'color_primary': 'indigo', 93 | 'color_accent': 'blue', 94 | } 95 | 96 | html_static_path = ['_static'] 97 | 98 | html_logo = str(Path(html_static_path[0]) / 'logo.svg') 99 | html_favicon = str(Path(html_static_path[0]) / 'favicon.svg') 100 | 101 | # -- Options for HTMLHelp output ------------------------------------------ 102 | 103 | htmlhelp_basename = 'f4pga-fasm' 104 | 105 | # -- Options for LaTeX output --------------------------------------------- 106 | 107 | latex_elements = {} 108 | 109 | latex_documents = [ 110 | ( 111 | master_doc, 112 | 'fasm.tex', 113 | u'F4PGA FASM Documentation', 114 | u'F4PGA', 115 | 'manual', 116 | ), 117 | ] 118 | 119 | # -- Options for manual page output --------------------------------------- 120 | 121 | man_pages = [ 122 | ( 123 | master_doc, 124 | 'f4pga-fasm', 125 | u'F4PGA FASM Documentation', 126 | [author], 127 | 1, 128 | ), 129 | ] 130 | 131 | # -- Options for Texinfo output ------------------------------------------- 132 | 133 | texinfo_documents = [ 134 | ( 135 | master_doc, 136 | 'FASM', 137 | u'F4PGA FASM Documentation', 138 | author, 139 | 'FASM', 140 | 'One line description of project.', 141 | 'Miscellaneous', 142 | ), 143 | ] 144 | 145 | # -- Sphinx.Ext.InterSphinx ----------------------------------------------- 146 | 147 | intersphinx_mapping = {'https://docs.python.org/': None} 148 | 149 | 150 | def setup(app): 151 | pass 152 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: fasm-docs 2 | channels: 3 | - SymbiFlow 4 | - conda-forge 5 | - defaults 6 | dependencies: 7 | - python=3.7 8 | - pip 9 | # ReadTheDoc dependencies 10 | - mock 11 | - pillow 12 | - sphinx 13 | # Packages installed from PyPI 14 | - pip: 15 | - -r requirements.txt 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | FPGA Assembly (FASM) 2 | ==================== 3 | 4 | .. toctree:: 5 | :caption: FPGA Assembly (FASM) Specification 6 | 7 | specification/introduction 8 | specification/syntax 9 | specification/line 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | http://github.com/f4pga/sphinx_f4pga_theme/archive/f4pga.zip#sphinx-f4pga-theme 2 | 3 | docutils 4 | sphinx 5 | sphinx-autobuild 6 | 7 | breathe 8 | sphinx-markdown-tables 9 | sphinxcontrib-napoleon 10 | -------------------------------------------------------------------------------- /docs/specification/introduction.rst: -------------------------------------------------------------------------------- 1 | .. _Specification:Introduction: 2 | 3 | Introduction 4 | ------------ 5 | 6 | The FASM is a file format designed to specify the bits in an FPGA bitstream that need to be set (e.g. binary 1) or 7 | cleared (e.g. binary 0). 8 | 9 | A FASM file declares that specific "Features" within the bitstream should be enabled. 10 | Enabling a feature will cause bits within the bitstream to be set or cleared. 11 | 12 | A FASM file is illegal if a bit in the final bitstream must be set and cleared to respect the set of features specified 13 | in the FASM file. 14 | 15 | An empty FASM file will generate a platform specific "default" bitstream. 16 | The FASM file will specify zero or more features that mutate the "default" bitstream into the target bitstream. 17 | 18 | .. image:: ../_static/image/fasm-diagram.png 19 | :width: 100% 20 | -------------------------------------------------------------------------------- /docs/specification/line.rst: -------------------------------------------------------------------------------- 1 | .. _Specification:Line: 2 | 3 | Meaning of a FASM line 4 | ---------------------- 5 | 6 | .. csv-table:: Simplified ``SetFasmFeature`` 7 | :delim: | 8 | :header-rows: 1 9 | 10 | YYYY.XXXXX | [A:B] | = C 11 | ``Feature`` | ``FeatureAddress`` | ``FeatureValue`` 12 | **Required** | *Optional* | *Optional* 13 | 14 | Each line of a FASM file that enables a feature is defined by a ``SetFasmFeature``. Table 1 provides a simplified version of ``SetFasmFeature`` parse. A ``SetFasmFeature`` parse has three parts, the feature to be set (``Feature``), the address within the feature to be set (``FeatureAddress``) and the value of the feature (``FeatureValue``). Both the ``FeatureAddress`` and ``FeatureValue`` are optional. 15 | 16 | When a FASM file declares that a feature is to be enabled or disabled, then specific bits in the bitstream will be cleared or set. 17 | 18 | This section describes how the state of the bits are determined. 19 | 20 | Feature 21 | +++++++ 22 | 23 | The ``Feature`` should uniquely specify a feature within the bitstream. If the feature is repeated across FPGA elements, a prefix identifier is required to uniquely identify where a feature is located. 24 | 25 | For example all SLICEL tiles have ALUT.INIT feature, however each tile CLBLL_L tile actually have two SLICEL, and there are many CLBLL_L tiles with a 7-series FPGA. So a unique path would required to both clarify which tile is being set, and which SLICEL within the tile is being set. 26 | 27 | FeatureAddress and FeatureValue 28 | +++++++++++++++++++++++++++++++ 29 | 30 | If the ``FeatureAddress`` is not specified, then the address selected is 0. 31 | 32 | If the ``FeatureValue`` is not specified, then the value is 1. 33 | 34 | If the ``FeatureAddress`` is specified as a specific bit rather than a range (e.g. "[5]"), then the ``FeatureValue`` width must be 1-bit wide (e.g. 0 or 1). If the ``FeatureAddress`` is a range (e.g. "[15:0]"), then the ``FeatureValue`` width must be equal or less than the ``FeatureAddress`` width. It is invalid to specific a ``FeatureValue`` wider than the ``FeatureAddress``. 35 | 36 | For example, if the ``FeatureAddress`` was [15:0], then the address width is 16 bits, and the ``FeatureValue`` must be 16 bits or less. So a ``FeatureValue`` of 16'hFFFF is valid, but a ``FeatureValue`` of 17'h10000 is invalid. 37 | 38 | When the ``FeatureAddress`` is wider than 1 bit, the ``FeatureValue`` is shifted and masked for each specific address before enabling or disabling the feature. So for a ``FeatureAddress`` of [7:4], the feature at address 4 is set with a value of (``FeatureValue`` >> 0) & 1, and the feature at address 5 is set with a value of (``FeatureValue`` >> 1) & 1, etc. 39 | 40 | If the value of a feature is 1, then the output bitstream must clear and set all bits as specified. 41 | If the value of a feature is 0, then no change to the "default" bitstream is made. 42 | 43 | Note that the absence of a FASM feature line does not imply that the feature is set to 0. It only means that the relevant bits are used from the implementation specific "default" bitstream. 44 | -------------------------------------------------------------------------------- /docs/specification/syntax.rst: -------------------------------------------------------------------------------- 1 | .. _Specification:Syntax: 2 | 3 | File Syntax description 4 | ----------------------- 5 | 6 | FASM is a line oriented format. 7 | 8 | * A single FASM line will do nothing (blank or comments or annotations) or enable a set of features. 9 | * The FASM file format does not support line continuations. Enabling a feature will always be contained within one line. 10 | * Constants and arrays follow verilog syntax 11 | 12 | Due to the line orientated format, a FASM file has the following properties: 13 | 14 | * Removing a line from a FASM file, produces a FASM file. 15 | * Concatenating two FASM files together, produces a FASM file. 16 | * Sorting a FASM file does not change the resulting bitstream. 17 | 18 | If two FASM files are identical, then the resulting bitstream should have identical set of features are enabled. FASM does support various equivalent forms for enabling features, so there is a "canonical" form of a FASM file that removes this variation. If the canonical forms of two FASM files are the same, then they must generate the same bitstream. 19 | 20 | Lines 21 | +++++ 22 | 23 | Examples FASM feature lines 24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | Canonical enabling of feature 27 | ````````````````````````````` 28 | 29 | .. code-block:: text 30 | 31 | # Set a single feature bit to 1 (with an implicit 1) 32 | INT_L_X10Y146.SW6BEG0.WW2END0 33 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] 34 | 35 | Recommended bitarray 36 | ```````````````````` 37 | 38 | .. code-block:: text 39 | 40 | # Setting a bitarray 41 | CLBLL_R_X13Y132.SLICEL_X0.ALUT.INIT[63:32] = 32'b11110000111100001111000011110000 42 | 43 | Permitted advanced variations 44 | ````````````````````````````` 45 | 46 | .. code-block:: text 47 | 48 | # The bitarray syntax also allows explicit 1 and explicit 0, if verbosity is desired. 49 | # An explicit 1 is the same as an implicit 1. 50 | INT_L_X10Y146.SW6BEG0.WW2END0 = 1 51 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] = 1 52 | # Explicit bit range to 1 53 | INT_L_X10Y146.SW6BEG0.WW2END0[0:0] = 1'b1 54 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17:17] = 1'b1 55 | 56 | # An explicit 0 has no effect on the bitstream output. 57 | INT_L_X10Y146.SW6BEG0.WW2END0 = 0 58 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] = 0 59 | # Explicit bit range to 0 60 | INT_L_X10Y146.SW6BEG0.WW2END0[0:0] = 1'b0 61 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17:17] = 1'b0 62 | 63 | Annotations 64 | +++++++++++ 65 | 66 | To allow tools to output machine usable annotations (e.g. compiler metadata), annotations are supported. Annotations can be on the line with a FASM feature, or on a line by itself. An annotations must not change the output bitstream. If an annotation would have an affect on the output bitstream, it should be a FASM feature. 67 | 68 | Annotations that are on the same line as a FASM feature are associated with the feature. Annotations that are on a line with no FASM feature as associated with the FASM file itself. 69 | 70 | Example Annotations 71 | ~~~~~~~~~~~~~~~~~~~ 72 | 73 | .. code-block:: text 74 | 75 | # Annotation on a FASM feature 76 | INT_L_X10Y146.SW6BEG0.WW2END0 { .attr = "" } 77 | INT_L_X10Y146.SW6BEG0.WW2END0 { .filename = "/a/b/c.txt" } 78 | INT_L_X10Y146.SW6BEG0.WW2END0 { module = "top", file = "/a/b/d.txt", line_number = "123" } 79 | 80 | # Annotation by itself 81 | { .top_module = "/a/b/c/d.txt" } 82 | 83 | # Annotation with FASM feature and comment 84 | INT_L_X10Y146.SW6BEG0.WW2END0 { .top_module = "/a/b/c/d.txt" } # This is a comment 85 | 86 | Formal syntax specification of a line of a FASM file 87 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ 88 | 89 | .. code-block:: text 90 | 91 | Identifier ::= [a-zA-Z] [0-9a-zA-Z]* 92 | Feature ::= Identifier ( '.' Identifier )* 93 | S ::= #x9 | #x20 94 | 95 | DecimalValue ::= [0-9_]* 96 | HexidecimalValue ::= [0-9a-fA-F_]+ 97 | OctalValue ::= [0-7_]+ 98 | BinaryValue ::= [01_]+ 99 | 100 | VerilogValue ::= (( DecimalValue? S* "'" ( 'h' S* HexidecimalValue | 'b' S* BinaryValue | 'd' S* DecimalValue | 'o' S* OctalValue ) | DecimalValue ) 101 | 102 | FeatureAddress ::= '[' DecimalValue (':' DecimalValue)? ']' 103 | 104 | Any ::= [^#xA#] 105 | Comment ::= '#' Any* 106 | 107 | AnnotationName ::= [.a-zA-Z] [_0-9a-zA-Z]* 108 | NonEscapeCharacters ::= [^\"] 109 | EscapeSequences ::= '\\' | '\"' 110 | Annotation ::= AnnotationName S* '=' S* '"' (NonEscapeCharacters | EscapeSequences)* '"' 111 | Annotations ::= '{' S* Annotation ( ',' S* Annotation )* S* '}' 112 | 113 | SetFasmFeature ::= Feature FeatureAddress? S* ('=' S* VerilogValue)? 114 | FasmLine ::= S* SetFasmFeature? S* Annotations? S* Comment? 115 | 116 | Canonicalization 117 | ++++++++++++++++ 118 | 119 | If two FASM files have been canonicalized and are identical, then they enable an identical set of features. The canonical FASM file is also equivalent to the FASM file that would be generated by taking the output bitstream and converting it back into a FASM file. 120 | 121 | The canonicalization process is as follows: 122 | 123 | #. Flatten any ``FeatureAddress`` with width greater than 1. 124 | 125 | * For ``SetFasmFeature`` lines with a ``FeatureAddress`` width greater than 1 bit, 1 ``SetFasmFeature`` for the width the original ``FeatureAddress``. 126 | * When flattening, if the flattened address is 0, do not emit the address. 127 | #. Remove all comments and annotations. 128 | #. If the ``FeatureValue`` is 0, remove the FASM line. 129 | #. If the ``FeatureValue`` is 1, only output the ``Feature`` and the ``FeatureAddress`` if the ``Feature`` has addresses other than 0. 130 | #. Remove any lines that do not modify the default bitstream. 131 | 132 | * Example are psuedo-pips in Xilinx parts. 133 | #. Sort the lines in the FASM file. 134 | 135 | Example Canonicalization 136 | ~~~~~~~~~~~~~~~~~~~~~~~~ 137 | 138 | ``ALUT.INIT[0] = 1`` 139 | 140 | becomes 141 | 142 | ``ALUT.INIT`` 143 | 144 | ---- 145 | 146 | ``ALUT.SMALL = 1`` 147 | 148 | becomes 149 | 150 | ``ALUT.SMALL`` 151 | 152 | ---- 153 | 154 | ``ALUT.INIT[3:0] = 4'b1101`` 155 | 156 | becomes 157 | 158 | ``ALUT.INIT`` 159 | 160 | ``ALUT.INIT[2]`` 161 | 162 | ``ALUT.INIT[3]`` 163 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: make-env 2 | dependencies: 3 | - cmake 4 | - pip 5 | - python 6 | - swig 7 | - pip: 8 | - -r requirements.txt 9 | -------------------------------------------------------------------------------- /examples/blank.fasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chipsalliance/fasm/ffafe821bae68637fe46e36bcfd2a01b97cdf6f2/examples/blank.fasm -------------------------------------------------------------------------------- /examples/comment.fasm: -------------------------------------------------------------------------------- 1 | # Only a comment. 2 | -------------------------------------------------------------------------------- /examples/feature_only.fasm: -------------------------------------------------------------------------------- 1 | EXAMPLE_FEATURE.X0.Y0.BLAH 2 | -------------------------------------------------------------------------------- /examples/many.fasm: -------------------------------------------------------------------------------- 1 | # This file should have examples of all FASM lines that should parse. 2 | # If an example is missing, add it to the end with a comment when is being 3 | # demostrated. 4 | # 5 | # Blank line 6 | 7 | # Empty comment 8 | # 9 | # 10 | 11 | # Set a single feature bit to 1 12 | # Implicit 1 13 | INT_L_X10Y146.SW6BEG0.WW2END0 14 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] 15 | # Explicit 1 16 | INT_L_X10Y146.SW6BEG0.WW2END0 = 1 17 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] = 1 18 | # Explicit bit range 19 | INT_L_X10Y146.SW6BEG0.WW2END0[0:0] = 1'b1 20 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17:17] = 1'b1 21 | 22 | # Set a single feature bit to 0 23 | # Explicit 0 24 | INT_L_X10Y146.SW6BEG0.WW2END0 = 0 25 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17] = 0 26 | # Explicit bit range to 0 27 | INT_L_X10Y146.SW6BEG0.WW2END0[0:0] = 1'b0 28 | CLBLL_L_X12Y124.SLICEL_X0.BLUT.INIT[17:17] = 1'b0 29 | 30 | # Set a bitarray 31 | CLBLL_R_X13Y132.SLICEL_X0.ALUT.INIT[63:32] = 32'b11110000_11110000_11110000_11110000 32 | CLBLL_R_X13Y132.SLICEL_X0.ALUT.INIT[63:32] = 32'b11110000_11110000_11110000_11110000 33 | CLBLL_R_X13Y132.SLICEL_X0.ALUT.INIT[63:32] = 5'h1F 34 | CLBLL_R_X13Y132.SLICEL_X0.ALUT.INIT[63:32] = 32'o1234567 35 | 36 | # Annotation on a FASM feature 37 | INT_L_X10Y146.SW6BEG0.WW2END0 { .attr = "" } 38 | INT_L_X10Y146.SW6BEG0.WW2END0 { .filename = "/a/b/c.txt" } 39 | INT_L_X10Y146.SW6BEG0.WW2END0 { module = "top", file = "/a/b/d.txt", line_number = "123" } 40 | INT_L_X10Y146.SW6BEG0.WW2END0{module="top",file="/a/b/d.txt",line_number="123"} 41 | 42 | # Annotation by itself 43 | { .top_module = "/a/b/c/d.txt" } 44 | 45 | # Annotation with FASM feature and comment 46 | INT_L_X10Y146.SW6BEG0.WW2END0 { .top_module = "/a/b/c/d.txt" } # This is a comment 47 | # Comment on the last line! 48 | -------------------------------------------------------------------------------- /fasm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from __future__ import print_function 21 | 22 | import os.path 23 | 24 | from fasm.model import ValueFormat, SetFasmFeature, Annotation, FasmLine 25 | from fasm.parser import parse_fasm_filename, parse_fasm_string 26 | 27 | try: 28 | from fasm.version import version_str 29 | except ImportError: 30 | version_str = "UNKNOWN" 31 | __dir__ = os.path.split(os.path.abspath(os.path.realpath(__file__)))[0] 32 | __version__ = version_str 33 | 34 | 35 | def fasm_value_to_str(value, width, value_format): 36 | """ Convert value from SetFasmFeature to a string. """ 37 | if value_format == ValueFormat.PLAIN: 38 | return '{}'.format(value) 39 | elif value_format == ValueFormat.VERILOG_HEX: 40 | return "{}'h{:X}".format(width, value) 41 | elif value_format == ValueFormat.VERILOG_DECIMAL: 42 | return "{}'d{}".format(width, value) 43 | elif value_format == ValueFormat.VERILOG_OCTAL: 44 | return "{}'o{:o}".format(width, value) 45 | elif value_format == ValueFormat.VERILOG_BINARY: 46 | return "{}'b{:b}".format(width, value) 47 | else: 48 | assert False, value_format 49 | 50 | 51 | def set_feature_width(set_feature): 52 | if set_feature.end is None: 53 | return 1 54 | else: 55 | assert set_feature.start is not None 56 | assert set_feature.start >= 0 57 | assert set_feature.end >= set_feature.start 58 | 59 | return set_feature.end - set_feature.start + 1 60 | 61 | 62 | def set_feature_to_str(set_feature, check_if_canonical=False): 63 | """ Convert SetFasmFeature tuple to string. """ 64 | feature_width = set_feature_width(set_feature) 65 | max_feature_value = 2**feature_width 66 | assert set_feature.value < max_feature_value 67 | 68 | if check_if_canonical: 69 | assert feature_width == 1 70 | assert set_feature.end is None 71 | if set_feature.start is not None: 72 | assert set_feature.start != 0 73 | assert set_feature.value_format is None 74 | 75 | feature = set_feature.feature 76 | address = '' 77 | feature_value = '' 78 | 79 | if set_feature.start is not None: 80 | if set_feature.end is not None: 81 | address = '[{}:{}]'.format(set_feature.end, set_feature.start) 82 | else: 83 | address = '[{}]'.format(set_feature.start) 84 | 85 | if set_feature.value_format is not None: 86 | feature_value = ' = {}'.format( 87 | fasm_value_to_str( 88 | value=set_feature.value, 89 | width=feature_width, 90 | value_format=set_feature.value_format)) 91 | 92 | return '{}{}{}'.format(feature, address, feature_value) 93 | 94 | 95 | def canonical_features(set_feature): 96 | """ Yield SetFasmFeature tuples that are of canonical form. 97 | 98 | EG width 1, and value 1. 99 | """ 100 | if set_feature.value == 0: 101 | return 102 | 103 | if set_feature.start is None: 104 | assert set_feature.value == 1 105 | assert set_feature.end is None 106 | yield SetFasmFeature( 107 | feature=set_feature.feature, 108 | start=None, 109 | end=None, 110 | value=1, 111 | value_format=None, 112 | ) 113 | 114 | return 115 | 116 | if set_feature.start is not None and set_feature.end is None: 117 | assert set_feature.value == 1 118 | 119 | if set_feature.start == 0: 120 | yield SetFasmFeature( 121 | feature=set_feature.feature, 122 | start=None, 123 | end=None, 124 | value=1, 125 | value_format=None, 126 | ) 127 | else: 128 | yield SetFasmFeature( 129 | feature=set_feature.feature, 130 | start=set_feature.start, 131 | end=None, 132 | value=1, 133 | value_format=None, 134 | ) 135 | 136 | return 137 | 138 | assert set_feature.start is not None 139 | assert set_feature.start >= 0 140 | assert set_feature.end >= set_feature.start 141 | 142 | for address in range(set_feature.start, set_feature.end + 1): 143 | value = (set_feature.value >> (address - set_feature.start)) & 1 144 | if value: 145 | if address == 0: 146 | yield SetFasmFeature( 147 | feature=set_feature.feature, 148 | start=None, 149 | end=None, 150 | value=1, 151 | value_format=None, 152 | ) 153 | else: 154 | yield SetFasmFeature( 155 | feature=set_feature.feature, 156 | start=address, 157 | end=None, 158 | value=1, 159 | value_format=None, 160 | ) 161 | 162 | 163 | def fasm_line_to_string(fasm_line, canonical=False): 164 | if canonical: 165 | if fasm_line.set_feature: 166 | for feature in canonical_features(fasm_line.set_feature): 167 | yield set_feature_to_str(feature, check_if_canonical=True) 168 | 169 | return 170 | 171 | parts = [] 172 | 173 | if fasm_line.set_feature: 174 | parts.append(set_feature_to_str(fasm_line.set_feature)) 175 | 176 | if fasm_line.annotations and not canonical: 177 | annotations = '{{ {} }}'.format( 178 | ', '.join( 179 | '{} = "{}"'.format(annotation.name, annotation.value) 180 | for annotation in fasm_line.annotations)) 181 | 182 | parts.append(annotations) 183 | 184 | if fasm_line.comment is not None and not canonical: 185 | comment = '#{}'.format(fasm_line.comment) 186 | parts.append(comment) 187 | 188 | if len(parts) == 0 and canonical: 189 | return 190 | 191 | yield ' '.join(parts) 192 | 193 | 194 | def fasm_tuple_to_string(model, canonical=False): 195 | """ Returns string of FASM file for the model given. 196 | 197 | Note that calling parse_fasm_filename and then calling fasm_tuple_to_string 198 | will result in all optional whitespace replaced with one space. 199 | """ 200 | 201 | lines = [] 202 | for fasm_line in model: 203 | for line in fasm_line_to_string(fasm_line, canonical=canonical): 204 | lines.append(line) 205 | 206 | if canonical: 207 | lines = list(sorted(set(lines))) 208 | 209 | return '\n'.join(lines) + '\n' 210 | -------------------------------------------------------------------------------- /fasm/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from collections import namedtuple 21 | import enum 22 | 23 | 24 | class ValueFormat(enum.Enum): 25 | """ Number format used for a FASM value. """ 26 | PLAIN = 0 27 | VERILOG_DECIMAL = 1 28 | VERILOG_HEX = 2 29 | VERILOG_BINARY = 3 30 | VERILOG_OCTAL = 4 31 | 32 | 33 | ValueFormat.PLAIN.__doc__ = \ 34 | "A decimal number without size or radix e.g. 42" 35 | ValueFormat.VERILOG_DECIMAL.__doc__ = \ 36 | "A decimal number with optional size e.g. 8'd42" 37 | ValueFormat.VERILOG_HEX.__doc__ = \ 38 | "A hexadecimal number with optional size e.g. 8'h2a" 39 | ValueFormat.VERILOG_BINARY.__doc__ = \ 40 | "A binary number with optional size e.g. 8'b00101010" 41 | ValueFormat.VERILOG_OCTAL.__doc__ = \ 42 | "An octal number with optional size e.g. 8'o52" 43 | 44 | SetFasmFeature = namedtuple( 45 | 'SetFasmFeature', 'feature start end value value_format') 46 | SetFasmFeature.__doc__ = """ 47 | Python version of a SetFasmFeature line such as: 48 | feature[31:0] = 42 49 | 50 | feature is a string e.g. 'feature' 51 | 52 | start and end are ints e.g 31, 0 53 | When FeatureAddress is missing, start=None and 54 | end=None. 55 | 56 | value is an int e.g. 42 57 | When FeatureValue is missing, value=1. 58 | 59 | value_format determines how to output the value e.g. ValueFormat.PLAIN 60 | It should be a ValueFormat or None. 61 | If it is None, the value must be 1 and the value will 62 | be omitted from output. 63 | """ 64 | SetFasmFeature.feature.__doc__ = "Feature name (string)" 65 | SetFasmFeature.start.__doc__ = \ 66 | "Starting value of the feature range (int or None)" 67 | SetFasmFeature.end.__doc__ = \ 68 | "Ending value of the feature range (int or None)" 69 | SetFasmFeature.value.__doc__ = \ 70 | "FeatureValue describing the value, or None" 71 | SetFasmFeature.value_format.__doc__ = \ 72 | "ValueFormat describing the format of the value, or None." 73 | 74 | Annotation = namedtuple('Annotation', 'name value') 75 | Annotation.__doc__ = """ 76 | Python version of an Annotation, such as: 77 | { name = "value" } 78 | 79 | Both name and value are strings (not None), 80 | holding the name and value, respectively. 81 | """ 82 | Annotation.name.__doc__ = "Annotation name (string)" 83 | Annotation.value.__doc__ = "Annotation value (string)" 84 | 85 | FasmLine = namedtuple('FasmLine', 'set_feature annotations comment') 86 | FasmLine.__doc__ = """ 87 | Python version of a FasmLine such as: 88 | feature[31:0] = 42 { name = "value" } # comment 89 | 90 | set_feature should be a SetFasmFeature or None. 91 | annotations should be a list of Annotation or None. 92 | comment should a string or None, e.g. " comment" 93 | """ 94 | FasmLine.set_feature.__doc__ = "SetFasmFeature or None" 95 | FasmLine.annotations.__doc__ = "List of Annotation or None" 96 | FasmLine.comment.__doc__ = \ 97 | "String or none containing the line comment (after '#')" 98 | -------------------------------------------------------------------------------- /fasm/output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | """ Output utilities for FASM. 20 | 21 | merge_features - Combines multiple FASM SetFasmFeature into one. 22 | merge_and_sort - Groups and sorts FASM lines, useful for non-canonical output. 23 | 24 | """ 25 | import enum 26 | from fasm import SetFasmFeature, FasmLine, ValueFormat 27 | 28 | 29 | def is_only_comment(line): 30 | """ Returns True if line is only a comment. """ 31 | return not line.set_feature and not line.annotations and line.comment 32 | 33 | 34 | def is_only_annotation(line): 35 | """ Returns True if line is only an annotations. """ 36 | return not line.set_feature and line.annotations and not line.comment 37 | 38 | 39 | def is_blank_line(line): 40 | """ Returns True if line is blank. """ 41 | return not line.set_feature and not line.annotations and not line.comment 42 | 43 | 44 | def merge_features(features): 45 | """ Combines features with varying addresses but same feature. 46 | 47 | A[0] = 1 48 | A[1] = 1 49 | 50 | becomes 51 | 52 | A[1:0] = 2'b11 53 | 54 | and 55 | 56 | A[5] = 1 57 | A[7] = 1 58 | 59 | A[7:0] = 8'b10100000 60 | 61 | """ 62 | # Ensure all features are for the same feature 63 | assert len(set(feature.feature for feature in features)) == 1 64 | 65 | set_bits = set() 66 | cleared_bits = set() 67 | 68 | for feature in features: 69 | start = 0 70 | end = 0 71 | value = 1 72 | 73 | if feature.start is None: 74 | assert feature.end is None 75 | else: 76 | start = feature.start 77 | if feature.end is not None: 78 | end = feature.end 79 | else: 80 | end = start 81 | 82 | if feature.value is not None: 83 | value = feature.value 84 | 85 | for bit in range(start, end + 1): 86 | bit_is_set = ((value >> (bit - start)) & 1) != 0 87 | if bit_is_set: 88 | assert bit not in cleared_bits 89 | set_bits.add(bit) 90 | else: 91 | assert bit not in set_bits 92 | cleared_bits.add(bit) 93 | 94 | max_bit = max(set_bits | cleared_bits) 95 | 96 | final_value = 0 97 | for bit in set_bits: 98 | final_value |= (1 << bit) 99 | 100 | return SetFasmFeature( 101 | feature=features[0].feature, 102 | start=0, 103 | end=max_bit, 104 | value=final_value, 105 | value_format=ValueFormat.VERILOG_BINARY) 106 | 107 | 108 | class MergeModel(object): 109 | """ Groups and merges features. 110 | 111 | Grouping logic: 112 | - Consecutive comments will be grouped. 113 | - Comments groups will attach to the next non-comment entry. 114 | - Consecutive annotations will be grouped. 115 | - Empty lines will be discarded 116 | - Features will be grouped by their first feature part. 117 | - Features within the same feature with different addresses will be 118 | merged. 119 | 120 | If a feature has a comment in its group, it is not eligable for address 121 | merging. 122 | 123 | """ 124 | 125 | class State(enum.Enum): 126 | """ State of grouper. """ 127 | NoGroup = 1 128 | InCommentGroup = 2 129 | InAnnotationGroup = 3 130 | 131 | def __init__(self): 132 | self.state = MergeModel.State.NoGroup 133 | self.groups = [] 134 | self.current_group = None 135 | 136 | def start_comment_group(self, line): 137 | """ Start a new group of comments. 138 | 139 | Requires that input line is a comment and not already in a comment 140 | group. 141 | 142 | """ 143 | assert self.state != MergeModel.State.InCommentGroup 144 | assert is_only_comment(line) 145 | 146 | if self.current_group is not None: 147 | self.groups.append(self.current_group) 148 | 149 | self.state = MergeModel.State.InCommentGroup 150 | self.current_group = [line] 151 | 152 | def add_to_comment_group(self, line): 153 | assert self.state == MergeModel.State.InCommentGroup 154 | 155 | if is_only_comment(line): 156 | self.current_group.append(line) 157 | elif is_only_annotation(line): 158 | self.current_group.append(line) 159 | self.state = MergeModel.State.InAnnotationGroup 160 | else: 161 | if not is_blank_line(line): 162 | self.current_group.append(line) 163 | 164 | self.groups.append(self.current_group) 165 | self.state = MergeModel.State.NoGroup 166 | 167 | def start_annotation_group(self, line): 168 | assert self.state != MergeModel.State.InAnnotationGroup 169 | assert is_only_annotation(line) 170 | 171 | if self.current_group is not None: 172 | self.groups.append(self.current_group) 173 | 174 | self.state = MergeModel.State.InAnnotationGroup 175 | self.current_group = [line] 176 | 177 | def add_to_annotation_group(self, line): 178 | assert self.state == MergeModel.State.InAnnotationGroup 179 | 180 | if is_only_comment(line): 181 | self.start_comment_group(line) 182 | elif is_only_annotation(line): 183 | self.current_group.append(line) 184 | self.state = MergeModel.State.InAnnotationGroup 185 | else: 186 | self.groups.append(self.current_group) 187 | 188 | self.current_group = None 189 | self.state = MergeModel.State.NoGroup 190 | 191 | self.add_to_model(line) 192 | 193 | def add_to_model(self, line): 194 | """ Add a line to the MergeModel. 195 | 196 | Will be grouped per MergeModel rules. 197 | This method is stateful, so order of insert matters, per grouping 198 | rules. 199 | 200 | """ 201 | if self.state == MergeModel.State.NoGroup: 202 | if is_only_comment(line): 203 | self.start_comment_group(line) 204 | elif is_only_annotation(line): 205 | self.start_annotation_group(line) 206 | else: 207 | if not is_blank_line(line): 208 | self.groups.append([line]) 209 | 210 | elif self.state == MergeModel.State.InCommentGroup: 211 | self.add_to_comment_group(line) 212 | 213 | elif self.state == MergeModel.State.InAnnotationGroup: 214 | self.add_to_annotation_group(line) 215 | 216 | else: 217 | assert False 218 | 219 | def merge_addresses(self): 220 | """ Merges address features when possible. 221 | 222 | Call after all lines have been added to the model. 223 | """ 224 | for group in self.groups: 225 | for line in group: 226 | assert not is_blank_line(line) 227 | 228 | def find_eligable_feature(group): 229 | if len(group) > 1: 230 | return None 231 | 232 | if group[0].annotations: 233 | return None 234 | 235 | if group[0].comment: 236 | return None 237 | 238 | return group[0].set_feature 239 | 240 | eligable_address_features = {} 241 | 242 | non_eligable_groups = [] 243 | non_eligable_features = set() 244 | 245 | for group in self.groups: 246 | feature = find_eligable_feature(group) 247 | 248 | if feature is None: 249 | non_eligable_groups.append(group) 250 | for line in group: 251 | if line.set_feature is not None: 252 | non_eligable_features.add(line.set_feature.feature) 253 | else: 254 | if feature.feature not in eligable_address_features: 255 | eligable_address_features[feature.feature] = [] 256 | 257 | eligable_address_features[feature.feature].append(feature) 258 | 259 | self.groups = non_eligable_groups 260 | 261 | for feature_group in eligable_address_features.values(): 262 | if feature_group[0].feature in non_eligable_features: 263 | for feature in feature_group: 264 | self.groups.append( 265 | [ 266 | FasmLine( 267 | set_feature=feature, 268 | annotations=None, 269 | comment=None) 270 | ]) 271 | else: 272 | if len(feature_group) > 1: 273 | self.groups.append( 274 | [ 275 | FasmLine( 276 | set_feature=merge_features(feature_group), 277 | annotations=None, 278 | comment=None) 279 | ]) 280 | else: 281 | for feature in feature_group: 282 | self.groups.append( 283 | [ 284 | FasmLine( 285 | set_feature=feature, 286 | annotations=None, 287 | comment=None) 288 | ]) 289 | 290 | def output_sorted_lines(self, zero_function=None, sort_key=None): 291 | """ Yields sorted FasmLine's. 292 | 293 | zero_function - Function that takes a feature string, and returns true 294 | that feature has no bits set. This allows tiles with 295 | only zero features to be dropped. 296 | 297 | sort_key - Function that takes a string argument and returns a key 298 | for the first feature part. Example: 299 | 300 | """ 301 | feature_groups = {} 302 | non_feature_groups = [] 303 | 304 | for group in self.groups: 305 | is_feature_group = False 306 | for line in group: 307 | if line.set_feature: 308 | group_id = line.set_feature.feature.split('.')[0] 309 | 310 | if group_id not in feature_groups: 311 | feature_groups[group_id] = [] 312 | 313 | feature_groups[group_id].append(group) 314 | is_feature_group = True 315 | break 316 | 317 | if not is_feature_group: 318 | non_feature_groups.append(group) 319 | 320 | output_groups = [] 321 | 322 | def feature_group_key(group): 323 | for line in group: 324 | if line.set_feature: 325 | assert line.set_feature.feature is not None 326 | return line.set_feature.feature 327 | 328 | assert False 329 | 330 | if sort_key is None: 331 | group_ids = sorted(feature_groups.keys()) 332 | else: 333 | group_ids = sorted(feature_groups.keys(), key=sort_key) 334 | 335 | for group_id in group_ids: 336 | flattened_group = [] 337 | for group in sorted(feature_groups[group_id], 338 | key=feature_group_key): 339 | flattened_group.extend(group) 340 | 341 | if zero_function is not None: 342 | if all(zero_function(line.set_feature.feature) 343 | for line in flattened_group 344 | if line.set_feature): 345 | continue 346 | 347 | output_groups.append(flattened_group) 348 | 349 | output_groups.extend(non_feature_groups) 350 | 351 | for idx in range(len(output_groups)): 352 | for line in output_groups[idx]: 353 | yield line 354 | 355 | if idx != len(output_groups) - 1: 356 | yield FasmLine( 357 | set_feature=None, annotations=None, comment=None) 358 | 359 | 360 | def merge_and_sort(model, zero_function=None, sort_key=None): 361 | """ Given a model, groups and sorts entries. 362 | 363 | zero_function - Function that takes a feature string, and returns true 364 | that feature has no bits set. This allows tiles with only 365 | zero features to be dropped. 366 | 367 | sort_key - Function that takes a string argument and returns a key 368 | for the first feature part. Example: 369 | 370 | A_X2Y1, A_X2Y100, A_X2Y2 371 | 372 | could be sorted as 373 | 374 | A_X2Y1, A_X2Y2, A_X2Y100 375 | 376 | with if the key function returns (A, 2, 1) for A_X2Y1. 377 | 378 | Yields FasmLine's. 379 | 380 | Grouping logic: 381 | - Consecutive comments will be grouped. 382 | - Comments groups will attach to the next non-comment entry. 383 | - Consecutive annotations will be grouped. 384 | - Empty lines will be discarded 385 | - Features will be grouped by their first feature part. 386 | - Features within the same feature with different addresses will be 387 | merged. 388 | 389 | Sorting logic: 390 | - Features will appear before raw annotations. 391 | """ 392 | merged_model = MergeModel() 393 | 394 | for line in model: 395 | merged_model.add_to_model(line) 396 | 397 | # Add the last processed annotation or comment blocks to the model 398 | if merged_model.state != MergeModel.State.NoGroup: 399 | if merged_model.current_group is not None: 400 | merged_model.groups.append(merged_model.current_group) 401 | 402 | merged_model.merge_addresses() 403 | return merged_model.output_sorted_lines( 404 | zero_function=zero_function, sort_key=sort_key) 405 | -------------------------------------------------------------------------------- /fasm/parser/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from warnings import warn 21 | 22 | available = [] 23 | """ List of parser submodules available. Strings should match module names. """ 24 | 25 | try: 26 | from fasm.parser.antlr import \ 27 | parse_fasm_filename, parse_fasm_string, implementation 28 | available.append('antlr') 29 | except ImportError as e: 30 | warn( 31 | """Unable to import fast Antlr4 parser implementation. 32 | ImportError: {} 33 | 34 | Falling back to the much slower pure Python textX based parser 35 | implementation. 36 | 37 | Getting the faster antlr parser can normally be done by installing the 38 | required dependencies and then reinstalling the fasm package with: 39 | pip uninstall 40 | pip install -v fasm 41 | """.format(e), RuntimeWarning) 42 | from fasm.parser.textx import \ 43 | parse_fasm_filename, parse_fasm_string, implementation 44 | 45 | # The textx parser is available as a fallback. 46 | available.append('textx') 47 | -------------------------------------------------------------------------------- /fasm/parser/antlr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from ctypes import CDLL, POINTER, CFUNCTYPE, c_char, c_size_t, c_char_p 21 | import os 22 | from fasm.parser import antlr_to_tuple 23 | import platform 24 | from pathlib import Path 25 | 26 | implementation = 'antlr' 27 | """ 28 | Module name of the default parser implementation, accessible as fasm.parser 29 | """ 30 | 31 | try: 32 | if platform.system() == 'Darwin': 33 | parse_fasm_lib = "libparse_fasm.dylib" 34 | else: 35 | parse_fasm_lib = "libparse_fasm.so" 36 | 37 | here = Path(os.path.dirname(os.path.realpath(__file__))) 38 | parse_fasm = CDLL(str(here / parse_fasm_lib)) 39 | except OSError: 40 | raise ImportError('Could not find parse_fasm library.') 41 | 42 | 43 | def parse_fasm_string(s): 44 | """ Parse FASM string, returning list of FasmLine named tuples. 45 | 46 | >>> parse_fasm_string('a.b.c = 1')[0].set_feature.feature 47 | 'a.b.c' 48 | 49 | Args: 50 | s: The string containing FASM source to parse. 51 | 52 | Returns: 53 | A list of fasm.model.FasmLine. 54 | """ 55 | result = [None] 56 | error = [None] 57 | 58 | # Use a closure to parse while allowing C++ to handle memory. 59 | @CFUNCTYPE(None, POINTER(c_char), c_size_t) 60 | def callback(s, n): 61 | data = s[:n] 62 | assert len(data) == n 63 | result[0] = antlr_to_tuple.parse_fasm_data(data) 64 | error[0] = None 65 | 66 | @CFUNCTYPE(None, c_size_t, c_size_t, c_char_p) 67 | def error_callback(line, position, message): 68 | result[0] = None 69 | error[0] = Exception( 70 | 'Parse error at {}:{} - {}'.format( 71 | line, position, message.decode('ascii'))) 72 | 73 | parse_fasm.from_string(bytes(s, 'ascii'), 0, callback, error_callback) 74 | 75 | if error[0] is not None: 76 | raise error[0] 77 | 78 | return result[0] 79 | 80 | 81 | def parse_fasm_filename(filename): 82 | """ Parse FASM file, returning list of FasmLine named tuples. 83 | 84 | >>> parse_fasm_filename('examples/feature_only.fasm')[0]\ 85 | .set_feature.feature 86 | 'EXAMPLE_FEATURE.X0.Y0.BLAH' 87 | 88 | Args: 89 | filename: The file containing FASM source to parse. 90 | 91 | Returns: 92 | A list of fasm.model.FasmLine. 93 | """ 94 | result = [None] 95 | error = [None] 96 | 97 | # Use a closure to parse while allowing C++ to handle memory. 98 | @CFUNCTYPE(None, POINTER(c_char), c_size_t) 99 | def callback(s, n): 100 | data = s[:n] 101 | assert len(data) == n 102 | result[0] = antlr_to_tuple.parse_fasm_data(data) 103 | error[0] = None 104 | 105 | @CFUNCTYPE(None, c_size_t, c_size_t, c_char_p) 106 | def error_callback(line, position, message): 107 | result[0] = None 108 | error[0] = Exception( 109 | 'Parse error at {}:{} - {}'.format( 110 | line, position, message.decode('ascii'))) 111 | 112 | parse_fasm.from_file(bytes(filename, 'ascii'), 0, callback, error_callback) 113 | 114 | if error[0] is not None: 115 | raise error[0] 116 | 117 | return result[0] 118 | -------------------------------------------------------------------------------- /fasm/parser/antlr_to_tuple.pyx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from sys import byteorder 21 | from fasm.model import \ 22 | SetFasmFeature, Annotation, FasmLine, ValueFormat 23 | from fasm.parser import tags 24 | 25 | TAG_TO_VALUE_FORMAT = { 26 | tags.plain: ValueFormat.PLAIN, 27 | tags.decimal: ValueFormat.VERILOG_DECIMAL, 28 | tags.hex: ValueFormat.VERILOG_HEX, 29 | tags.binary: ValueFormat.VERILOG_BINARY, 30 | tags.octal: ValueFormat.VERILOG_OCTAL 31 | } 32 | """ Maps tags from the parse_fasm library to ValueFormats """ 33 | 34 | # The following functions decode the binary format generated by parse_fasm. 35 | # This is a lightweight binary format designed to be fast to decode. 36 | 37 | cdef (int, int) get_header(char* tags, char* data, int i): 38 | """ 39 | Match a tag and retrieve length from the header. 40 | Returns data length and offset, or None and the same offset 41 | if the header doesn't match on of the given tags. 42 | """ 43 | if not data[i] in tags: 44 | return -1, i 45 | 46 | return int.from_bytes(data[i + 1:i + 5], byteorder), i + 5 47 | 48 | 49 | def tagged_string_from_bytes(tag, data, i): 50 | """ Decode a tagged string. """ 51 | length, i = get_header(tag, data, i) 52 | if length == -1: 53 | return None, i 54 | 55 | return data[i:i + length].decode('ascii'), i + length 56 | 57 | 58 | def fasm_address_from_bytes(data, i): 59 | """ Decode a FASM address: [x:y] """ 60 | length, i = get_header(tags.address, data, i) 61 | if length == -1: 62 | return None, None, i 63 | 64 | assert length == 4 or length == 8, length 65 | 66 | end = int.from_bytes(data[i:i + 4], byteorder) 67 | 68 | if length == 8: 69 | start = int.from_bytes(data[i + 4:i + 8], byteorder) 70 | else: # If there is only one value, assign it to start 71 | start = end 72 | end = None 73 | 74 | return start, end, i + length 75 | 76 | 77 | def fasm_value_from_bytes(data, i): 78 | """ Decode a FASM value. """ 79 | tag = data[i:i+1] 80 | if tag == tags.plain: # Matches a plain decimal integer. 81 | length = 4 82 | i = i + 1 83 | else: # Matches Verilog number formats: hex, octal, binary, or decimal. 84 | length, i = get_header( 85 | tags.hex + tags.octal + tags.binary + tags.decimal, data, i) 86 | 87 | if length == -1: 88 | return None, None, i 89 | 90 | assert length % 4 == 0, length 91 | 92 | value = 0 93 | for j in range(i, i + length, 4): 94 | value = value << 32 | int.from_bytes(data[j:j + 4], byteorder) 95 | 96 | return value, TAG_TO_VALUE_FORMAT[tag], i + length 97 | 98 | 99 | def fasm_width_from_bytes(data, i): 100 | """ Decode the width of a Verilog value. """ 101 | if data[i:i+1] != tags.width: 102 | return None, i 103 | else: 104 | return int.from_bytes(data[i + 1:i + 5], byteorder), i + 5 105 | 106 | 107 | def address_width(start, end): 108 | """ Calculate bit width inferred from the address. """ 109 | return 1 if (start is None or end is None) else end - start + 1 110 | 111 | 112 | def fasm_set_feature_from_bytes(data, i): 113 | """ Decode a set feature: feature = value """ 114 | length, i = get_header(tags.set_feature, data, i) 115 | if length == -1: 116 | return None, i 117 | 118 | feature, p = tagged_string_from_bytes(tags.feature, data, i) 119 | start, end, p = fasm_address_from_bytes(data, p) 120 | width, p = fasm_width_from_bytes(data, p) 121 | value, value_format, p = fasm_value_from_bytes(data, p) 122 | 123 | assert p == i + length 124 | 125 | assert feature is not None 126 | 127 | if value is None: 128 | value = 1 129 | 130 | if width: 131 | assert value < 2 ** width, \ 132 | "value {} larger than specified width of {}".format(value, width) 133 | 134 | assert value < 2**address_width(start, end), (value, start, end) 135 | 136 | return SetFasmFeature( 137 | feature=feature, 138 | start=start, 139 | end=end, 140 | value=value, 141 | value_format=value_format), i + length 142 | 143 | 144 | def fasm_annotation_from_bytes(data, i): 145 | """ Decode an annotation: x = "y" """ 146 | length, i = get_header(tags.annotation, data, i) 147 | if length == -1: 148 | return None, i 149 | 150 | name, p = tagged_string_from_bytes(tags.annotation_name, data, i) 151 | value, p = tagged_string_from_bytes(tags.annotation_value, data, p) 152 | 153 | assert p == i + length 154 | 155 | assert name is not None 156 | assert value is not None 157 | 158 | return Annotation(name=name, value=value), i + length 159 | 160 | 161 | def fasm_annotations_from_bytes(data, i): 162 | """ Decode a set of annotations: { ... } """ 163 | length, i = get_header(tags.annotations, data, i) 164 | if length == -1: 165 | return None, i 166 | 167 | annotations = [] 168 | annotation, p = fasm_annotation_from_bytes(data, i) 169 | while annotation: 170 | annotations.append(annotation) 171 | annotation, p = fasm_annotation_from_bytes(data, p) 172 | 173 | assert p == i + length 174 | 175 | return annotations, i + length 176 | 177 | 178 | def fasm_line_from_bytes(data, i): 179 | """ Decode an entire FASM line. """ 180 | length, i = get_header(tags.line, data, i) 181 | if length == -1: 182 | return None, i 183 | 184 | set_feature, p = fasm_set_feature_from_bytes(data, i) 185 | annotations, p = fasm_annotations_from_bytes(data, p) 186 | comment, p = tagged_string_from_bytes(tags.comment, data, p) 187 | 188 | assert p == i + length 189 | 190 | assert ( 191 | set_feature is not None or annotations is not None 192 | or comment is not None) 193 | 194 | return FasmLine( 195 | set_feature=set_feature, annotations=annotations, 196 | comment=comment), i + length 197 | 198 | 199 | def parse_fasm_data(data): 200 | """ Parse FASM string, returning list of FasmLine named tuples.""" 201 | lines = [] 202 | line, p = fasm_line_from_bytes(data, 0) 203 | while line: 204 | lines.append(line) 205 | line, p = fasm_line_from_bytes(data, p) 206 | 207 | # Check that data read, plus the final null header, 208 | # is equal to the buffer size. 209 | assert p + 1 == len(data), p 210 | 211 | return lines 212 | -------------------------------------------------------------------------------- /fasm/parser/fasm.tx: -------------------------------------------------------------------------------- 1 | // FasmFile must be first, it is the root of the parse tree. 2 | FasmFile: (lines*=FasmLine Newline*)* ; 3 | 4 | // None-newline whitespace 5 | S: /[ \t]/ ; 6 | 7 | Newline: /[\n\r]/ ; 8 | 9 | Identifier: /[a-zA-Z][0-9a-zA-Z_]*/ ; 10 | Feature: Identifier ( '.' Identifier )* ; 11 | 12 | DecimalValue: /[0-9_]+/ ; 13 | HexidecimalValue: /[0-9a-fA-F_]+/ ; 14 | OctalValue: /[0-7_]+/ ; 15 | BinaryValue: /[01_]+/ ; 16 | 17 | VerilogValue: ( width=INT? S* "'" ( 'h' S* hex_value=HexidecimalValue | 'b' S* binary_value=BinaryValue | 'd' S* decimal_value=DecimalValue | 'o' S* octal_value=OctalValue ) | plain_decimal=DecimalValue ) ; 18 | 19 | FeatureAddress: '[' address1=DecimalValue (':' address2=DecimalValue)? ']' ; 20 | 21 | CommentCap: '#' comment=/[^\n\r]*/ ; 22 | 23 | AnnotationName: /[.a-zA-Z][0-9a-zA-Z_]*/ ; 24 | NonEscapeCharacters: /[^\"]/ ; 25 | EscapeSequences: /[\\][\\"]/ ; 26 | AnnotationValue: (NonEscapeCharacters | EscapeSequences)* ; 27 | Annotation: name=AnnotationName S* '=' S* '"' value=AnnotationValue '"' ; 28 | Annotations: '{' S* annotations+=Annotation ( ',' S* annotations+=Annotation )* S* '}' ; 29 | 30 | SetFasmFeature: feature=Feature feature_address=FeatureAddress? S* ('=' S* feature_value=VerilogValue)? ; 31 | FasmLine: S* set_feature=SetFasmFeature? S* annotations=Annotations? S* comment=CommentCap?; 32 | -------------------------------------------------------------------------------- /fasm/parser/textx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | from __future__ import print_function 21 | import textx 22 | import os.path 23 | from fasm.model import \ 24 | ValueFormat, SetFasmFeature, Annotation, FasmLine 25 | 26 | implementation = 'textx' 27 | """ 28 | Module name of the default parser implementation, accessible as fasm.parser 29 | """ 30 | 31 | 32 | def assert_max_width(width, value): 33 | """ asserts if the value is greater than the width. """ 34 | assert value < (2**width), (width, value) 35 | 36 | 37 | def verilog_value_to_int(verilog_value): 38 | """ Convert VerilogValue model to width, value, value_format """ 39 | width = None 40 | 41 | if verilog_value.plain_decimal: 42 | return width, int(verilog_value.plain_decimal), ValueFormat.PLAIN 43 | 44 | if verilog_value.width: 45 | width = int(verilog_value.width) 46 | 47 | if verilog_value.hex_value: 48 | value = int(verilog_value.hex_value.replace('_', ''), 16) 49 | value_format = ValueFormat.VERILOG_HEX 50 | elif verilog_value.binary_value: 51 | value = int(verilog_value.binary_value.replace('_', ''), 2) 52 | value_format = ValueFormat.VERILOG_BINARY 53 | elif verilog_value.decimal_value: 54 | value = int(verilog_value.decimal_value.replace('_', ''), 10) 55 | value_format = ValueFormat.VERILOG_DECIMAL 56 | elif verilog_value.octal_value: 57 | value = int(verilog_value.octal_value.replace('_', ''), 8) 58 | value_format = ValueFormat.VERILOG_OCTAL 59 | else: 60 | assert False, verilog_value 61 | 62 | if width is not None: 63 | assert_max_width(width, value) 64 | 65 | return width, value, value_format 66 | 67 | 68 | def set_feature_model_to_tuple(set_feature_model): 69 | start = None 70 | end = None 71 | value = 1 72 | address_width = 1 73 | value_format = None 74 | 75 | if set_feature_model.feature_address: 76 | if set_feature_model.feature_address.address2: 77 | end = int(set_feature_model.feature_address.address1, 10) 78 | start = int(set_feature_model.feature_address.address2, 10) 79 | address_width = end - start + 1 80 | else: 81 | start = int(set_feature_model.feature_address.address1, 10) 82 | end = None 83 | address_width = 1 84 | 85 | if set_feature_model.feature_value: 86 | width, value, value_format = verilog_value_to_int( 87 | set_feature_model.feature_value) 88 | 89 | if width is not None: 90 | assert width <= address_width 91 | 92 | assert value < (2**address_width), (value, address_width) 93 | 94 | return SetFasmFeature( 95 | feature=set_feature_model.feature, 96 | start=start, 97 | end=end, 98 | value=value, 99 | value_format=value_format, 100 | ) 101 | 102 | 103 | def get_fasm_metamodel(): 104 | return textx.metamodel_from_file( 105 | file_name=os.path.join(os.path.dirname(__file__), 'fasm.tx'), 106 | skipws=False) 107 | 108 | 109 | def fasm_model_to_tuple(fasm_model): 110 | """ Converts FasmFile model to list of FasmLine named tuples. """ 111 | if not fasm_model: 112 | return 113 | 114 | for fasm_line in fasm_model.lines: 115 | set_feature = None 116 | annotations = None 117 | comment = None 118 | 119 | if fasm_line.set_feature: 120 | set_feature = set_feature_model_to_tuple(fasm_line.set_feature) 121 | 122 | if fasm_line.annotations: 123 | annotations = tuple( 124 | Annotation( 125 | name=annotation.name, 126 | value=annotation.value if annotation.value else '') 127 | for annotation in fasm_line.annotations.annotations) 128 | 129 | if fasm_line.comment: 130 | comment = fasm_line.comment.comment 131 | 132 | yield FasmLine( 133 | set_feature=set_feature, 134 | annotations=annotations, 135 | comment=comment, 136 | ) 137 | 138 | 139 | def parse_fasm_string(s): 140 | """ Parse FASM string, returning list of FasmLine named tuples. 141 | 142 | >>> parse_fasm_string('a.b.c = 1')[0].set_feature.feature 143 | 'a.b.c' 144 | 145 | Args: 146 | s: The string containing FASM source to parse. 147 | 148 | Returns: 149 | A list of fasm.model.FasmLine. 150 | """ 151 | return fasm_model_to_tuple(get_fasm_metamodel().model_from_str(s)) 152 | 153 | 154 | def parse_fasm_filename(filename): 155 | """ Parse FASM file, returning list of FasmLine named tuples. 156 | 157 | >>> parse_fasm_filename('examples/feature_only.fasm')[0]\ 158 | .set_feature.feature 159 | 'EXAMPLE_FEATURE.X0.Y0.BLAH' 160 | 161 | Args: 162 | filename: The file containing FASM source to parse. 163 | 164 | Returns: 165 | A list of fasm.model.FasmLine. 166 | """ 167 | return fasm_model_to_tuple(get_fasm_metamodel().model_from_file(filename)) 168 | -------------------------------------------------------------------------------- /fasm/tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | import argparse 21 | import importlib 22 | import fasm.parser 23 | from fasm import fasm_tuple_to_string 24 | 25 | 26 | def nullable_string(val): 27 | if not val: 28 | return None 29 | return val 30 | 31 | 32 | def get_fasm_parser(name=None): 33 | module_name = None 34 | if name is None: 35 | module_name = 'fasm.parser' 36 | elif name in fasm.parser.available: 37 | module_name = 'fasm.parser.' + name 38 | else: 39 | raise Exception("Parser '{}' is not available.".format(name)) 40 | return importlib.import_module(module_name) 41 | 42 | 43 | def main(): 44 | parser = argparse.ArgumentParser('FASM tool') 45 | parser.add_argument('file', help='Filename to process') 46 | parser.add_argument( 47 | '--canonical', 48 | action='store_true', 49 | help='Return canonical form of FASM.') 50 | parser.add_argument( 51 | '--parser', 52 | type=nullable_string, 53 | help='Select FASM parser to use. ' 54 | 'Default is to choose the best implementation available.') 55 | 56 | args = parser.parse_args() 57 | 58 | try: 59 | fasm_parser = get_fasm_parser(args.parser) 60 | fasm_tuples = fasm_parser.parse_fasm_filename(args.file) 61 | print(fasm_tuple_to_string(fasm_tuples, args.canonical)) 62 | except Exception as e: 63 | print('Error: ' + str(e)) 64 | 65 | 66 | if __name__ == '__main__': 67 | main() 68 | -------------------------------------------------------------------------------- /fasm/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | # ** WARNING ** 21 | # This file is auto-generated by the update_version.py script. 22 | # ** WARNING ** 23 | 24 | version_str = "0.0.2.post66" 25 | version_tuple = (0, 0, 2, 66) 26 | try: 27 | from packaging.version import Version as V 28 | pversion = V("0.0.2.post66") 29 | except ImportError: 30 | pass 31 | 32 | git_hash = "c0b734e6d373fcffd0522acdd102814bcefb626a" 33 | git_describe = "v0.0.2-66-gc0b734e" 34 | git_msg = """\ 35 | commit c0b734e6d373fcffd0522acdd102814bcefb626a 36 | Author: Tim 'mithro' Ansell 37 | Date: Fri Feb 19 13:05:20 2021 -0800 38 | 39 | Improve the warning message when falling back to the textX. 40 | 41 | Signed-off-by: Tim 'mithro' Ansell 42 | 43 | """ 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "cython"] 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | check-manifest 2 | cython 3 | flake8 4 | pytest 5 | textx 6 | tox 7 | twine 8 | wheel 9 | yapf==0.24.0 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | import os 21 | import re 22 | import setuptools 23 | import shutil 24 | import subprocess 25 | import sys 26 | import traceback 27 | 28 | from Cython.Build import cythonize 29 | from distutils.command.build import build 30 | from distutils.version import LooseVersion 31 | from setuptools import Extension 32 | from setuptools.command.build_ext import build_ext 33 | from setuptools.command.develop import develop 34 | from setuptools.command.install import install 35 | 36 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 37 | 38 | # Read in the description 39 | with open("README.md", "r") as fh: 40 | long_description = fh.read() 41 | 42 | # Read in the version information 43 | FASM_VERSION_FILE = os.path.join(__dir__, 'fasm', 'version.py') 44 | with open(FASM_VERSION_FILE) as f: 45 | if 'UNKNOWN' in f.read(): 46 | print( 47 | "Running update_version.py to generate {}".format( 48 | FASM_VERSION_FILE)) 49 | subprocess.check_call(['python', 'update_version.py'], cwd=__dir__) 50 | with open(FASM_VERSION_FILE) as f: 51 | lines = f.readlines() 52 | version_line = [v.strip() for v in lines if v.startswith('version_str')] 53 | assert len(version_line) == 1, version_line 54 | version_value = version_line[0].split(' = ', 1)[-1] 55 | assert version_value[0] == '"', version_value 56 | assert version_value[-1] == '"', version_value 57 | version = version_value[1:-1] 58 | 59 | 60 | # Based on: https://www.benjack.io/2018/02/02/python-cpp-revisited.html 61 | # GitHub: https://github.com/benjaminjack/python_cpp_example 62 | class CMakeExtension(Extension): 63 | def __init__(self, name, sourcedir='', prefix=''): 64 | Extension.__init__(self, name, sources=[]) 65 | self.sourcedir = os.path.abspath(sourcedir) 66 | self.prefix = prefix 67 | 68 | 69 | # Used to share options between two classes. 70 | class SharedOptions(): 71 | ANTLR_RUNTIMES = ['static', 'shared'] 72 | options = [ 73 | ( 74 | 'antlr-runtime=', None, 75 | "Whether to use a 'static' or 'shared' ANTLR runtime.") 76 | ] 77 | 78 | def __init__(self): 79 | self.antlr_runtime = 'static' 80 | 81 | def initialize(self, other): 82 | other.antlr_runtime = None 83 | 84 | def load(self, other): 85 | if other.antlr_runtime is not None: 86 | self.antlr_runtime = other.antlr_runtime 87 | assert self.antlr_runtime in SharedOptions.ANTLR_RUNTIMES, \ 88 | 'Invalid antlr_runtime {}, expected one of {}'.format( 89 | self.antlr_runtime, SharedOptions.ANTLR_RUNTIMES) 90 | 91 | 92 | # Global to allow sharing options. 93 | shared_options = SharedOptions() 94 | 95 | 96 | class AntlrCMakeBuild(build_ext): 97 | user_options = SharedOptions.options 98 | 99 | def copy_extensions_to_source(self): 100 | original_extensions = list(self.extensions) 101 | self.extensions = [ 102 | ext for ext in self.extensions 103 | if not isinstance(ext, CMakeExtension) 104 | ] 105 | super().copy_extensions_to_source() 106 | self.extensions = original_extensions 107 | 108 | def run(self): 109 | shared_options.load(self) 110 | try: 111 | super().run() 112 | 113 | try: 114 | out = subprocess.check_output(['cmake', '--version']) 115 | except OSError: 116 | raise RuntimeError( 117 | "CMake must be installed to build " 118 | "the following extensions: " + ", ".join( 119 | e.name for e in self.extensions)) 120 | 121 | cmake_version = LooseVersion( 122 | re.search(r'version\s*([\d.]+)', out.decode()).group(1)) 123 | if cmake_version < '3.7.0': 124 | raise RuntimeError("CMake >= 3.7.0 is required.") 125 | 126 | for ext in self.extensions: 127 | self.build_extension(ext) 128 | 129 | except BaseException as e: 130 | print( 131 | "Failed to build ANTLR parser, " 132 | "falling back on slower textX parser. Error:\n", e) 133 | traceback.print_exc() 134 | 135 | # FIXME: Remove this function 136 | # see: https://github.com/chipsalliance/fasm/issues/50 137 | def add_flags(self): 138 | if sys.platform.startswith('win'): 139 | return 140 | 141 | for flag in ["CFLAGS", "CXXFLAGS"]: 142 | flags = [os.environ.get(flag, "")] 143 | if not flags[0]: 144 | flags.pop(0) 145 | 146 | if shared_options.antlr_runtime == 'static': 147 | # When linking the ANTLR runtime statically, -fPIC is 148 | # still necessary because libparse_fasm will be a 149 | # shared library. 150 | flags.append("-fPIC") 151 | 152 | # FIXME: These should be in the cmake config file? 153 | # Disable excessive warnings currently in ANTLR runtime. 154 | # warning: type attributes ignored after type is already defined 155 | # `class ANTLR4CPP_PUBLIC ATN;` 156 | flags.append('-Wno-attributes') 157 | 158 | # Lots of implicit fallthroughs. 159 | # flags.append('-Wimplicit-fallthrough=0') 160 | 161 | if flags: 162 | os.environ[flag] = " ".join(flags) 163 | 164 | def build_extension(self, ext): 165 | if isinstance(ext, CMakeExtension): 166 | extdir = os.path.join( 167 | os.path.abspath( 168 | os.path.dirname(self.get_ext_fullpath(ext.name))), 169 | ext.prefix) 170 | cmake_args = [ 171 | '-DCMAKE_INSTALL_PREFIX=' + extdir, 172 | '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir, 173 | '-DPYTHON_EXECUTABLE=' + sys.executable, 174 | '-DANTLR_RUNTIME_TYPE=' + shared_options.antlr_runtime 175 | ] 176 | 177 | cfg = 'Debug' if self.debug else 'Release' 178 | build_args = ['--config', cfg] 179 | cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] 180 | if not sys.platform.startswith('win') and ( 181 | os.environ.get("CMAKE_BUILD_PARALLEL_LEVEL") is None): 182 | build_args += ['--', '-j'] 183 | 184 | env = os.environ.copy() 185 | env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format( 186 | env.get('CXXFLAGS', ''), self.distribution.get_version()) 187 | 188 | # Remove the existing build_temp directory if it already exists. 189 | if os.path.exists(self.build_temp): 190 | shutil.rmtree(self.build_temp, ignore_errors=True) 191 | os.makedirs(self.build_temp, exist_ok=True) 192 | 193 | self.add_flags() 194 | 195 | subprocess.check_call( 196 | ['cmake', ext.sourcedir] + cmake_args, 197 | cwd=self.build_temp, 198 | env=env) 199 | subprocess.check_call( 200 | ['cmake', '--build', '.'] + build_args, cwd=self.build_temp) 201 | subprocess.check_call( 202 | ['cmake', '--install', '.'], cwd=self.build_temp) 203 | subprocess.check_call(['ctest'], cwd=self.build_temp) 204 | print() # Add an empty line for cleaner output 205 | else: 206 | super().build_extension(ext) 207 | 208 | def initialize_options(self): 209 | super().initialize_options() 210 | shared_options.initialize(self) 211 | 212 | def finalize_options(self): 213 | super().finalize_options() 214 | shared_options.load(self) 215 | 216 | 217 | class BuildCommand(build): 218 | user_options = build.user_options + SharedOptions.options 219 | 220 | def initialize_options(self): 221 | super().initialize_options() 222 | shared_options.initialize(self) 223 | 224 | def finalize_options(self): 225 | super().finalize_options() 226 | shared_options.load(self) 227 | 228 | def run(self): 229 | shared_options.load(self) 230 | super().run() 231 | 232 | 233 | class InstallCommand(install): 234 | user_options = install.user_options + SharedOptions.options 235 | 236 | def initialize_options(self): 237 | super().initialize_options() 238 | shared_options.initialize(self) 239 | 240 | def finalize_options(self): 241 | super().finalize_options() 242 | shared_options.load(self) 243 | 244 | def run(self): 245 | shared_options.load(self) 246 | super().run() 247 | 248 | 249 | class DevelopCommand(develop): 250 | user_options = develop.user_options + SharedOptions.options 251 | 252 | def initialize_options(self): 253 | super().initialize_options() 254 | shared_options.initialize(self) 255 | 256 | def finalize_options(self): 257 | super().finalize_options() 258 | shared_options.load(self) 259 | 260 | def run(self): 261 | shared_options.load(self) 262 | super().run() 263 | 264 | 265 | setuptools.setup( 266 | name="fasm", 267 | version=version, 268 | author="F4PGA Authors", 269 | author_email="f4pga-wg@lists.chipsalliance.org", 270 | description="FPGA Assembly (FASM) Parser and Generation library", 271 | long_description=long_description, 272 | long_description_content_type="text/markdown", 273 | url="https://github.com/chipsalliance/fasm", 274 | packages=setuptools.find_packages(exclude=('tests*', )), 275 | install_requires=['textx'], 276 | include_package_data=True, 277 | classifiers=[ 278 | "Programming Language :: Python :: 3", 279 | "License :: OSI Approved :: Apache Software License", 280 | "Operating System :: OS Independent", 281 | ], 282 | entry_points={ 283 | 'console_scripts': ['fasm=fasm.tool:main'], 284 | }, 285 | ext_modules=[ 286 | CMakeExtension('parse_fasm', sourcedir='src', prefix='fasm/parser') 287 | ] + cythonize("fasm/parser/antlr_to_tuple.pyx"), 288 | cmdclass={ 289 | 'build_ext': AntlrCMakeBuild, 290 | 'build': BuildCommand, 291 | 'develop': DevelopCommand, 292 | 'install': InstallCommand, 293 | }, 294 | ) 295 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2022 F4PGA Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | # minimum required CMAKE version 18 | CMAKE_MINIMUM_REQUIRED(VERSION 3.7 FATAL_ERROR) 19 | 20 | project(parse_fasm) 21 | 22 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/antlr4/runtime/Cpp/cmake) 23 | 24 | set(CMAKE_CXX_STANDARD 17) 25 | set(ANTLR4_TAG e4c1a74) 26 | if (MSVC_VERSION GREATER_EQUAL "1900") 27 | include(CheckCXXCompilerFlag) 28 | CHECK_CXX_COMPILER_FLAG("/std:c++latest" _cpp_latest_flag_supported) 29 | if (_cpp_latest_flag_supported) 30 | add_compile_options("/std:c++latest") 31 | endif() 32 | endif() 33 | 34 | if(ANTLR_RUNTIME_TYPE STREQUAL "static") 35 | # Required if linking to static library 36 | add_definitions(-DANTLR4CPP_STATIC) 37 | 38 | # Build the static library. 39 | include(ExternalAntlr4Cpp) 40 | set(ANTLR4_RUNTIME antlr4_static) 41 | else() 42 | # Look to see if the shared library is already available. 43 | find_library(ANTLR4_RUNTIME NAMES antlr4-runtime REQUIRED) 44 | find_path(ANTLR4_INCLUDE_DIRS antlr4-runtime.h 45 | HINTS /usr/include/antlr4-runtime 46 | $ENV{ANTLR4_RUNTIME_INCLUDE}) 47 | 48 | # If not, build it. 49 | if(NOT ANTLR4_RUNTIME OR NOT ANTLR4_INCLUDE_DIRS) 50 | include(ExternalAntlr4Cpp) 51 | set(ANTLR4_RUNTIME antlr4_shared) 52 | endif() 53 | endif() 54 | 55 | # add antrl4cpp artifacts to project environment 56 | include_directories(${ANTLR4_INCLUDE_DIRS}) 57 | 58 | # set variable pointing to the antlr tool that supports C++ 59 | # this is not required if the jar file can be found under PATH environment 60 | set(ANTLR_EXECUTABLE ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/antlr4_lib/antlr-4.9.3-complete.jar) 61 | 62 | # add macros to generate ANTLR Cpp code from grammar 63 | find_package(ANTLR REQUIRED) 64 | 65 | # Unit testing library 66 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../third_party/googletest EXCLUDE_FROM_ALL googletest) 67 | 68 | # Lexer and parser targets 69 | antlr_target(FasmLexer antlr/FasmLexer.g4 LEXER 70 | OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 71 | COMPILE_FLAGS -Xexact-output-dir 72 | ) 73 | antlr_target(FasmParser antlr/FasmParser.g4 PARSER VISITOR 74 | OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 75 | DEPENDS_ANTLR FasmLexer 76 | COMPILE_FLAGS -Xexact-output-dir -lib ${ANTLR_FasmLexer_OUTPUT_DIR} 77 | ) 78 | 79 | # Explanation: 80 | # line 1: Skip lines starting in #define 81 | # 2: Extract TAGS(...) from dependencies 82 | # 3: Convert from TAGS('c', long_name) -> long_name = b'c, write to file 83 | add_custom_target(tags.py ALL 84 | COMMAND grep -ve ^\#define ${CMAKE_CURRENT_SOURCE_DIR}/ParseFasm.cpp | 85 | grep -hoe TAG\([^\)]*\) | 86 | sed -e s/TAG\(\\\(.*\\\),\ \\\(.*\\\)\)/\\2\ =\ b\\1/ > tags.py 87 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/ParseFasm.cpp 88 | VERBATIM 89 | ) 90 | 91 | # Include generated files in project environment 92 | include_directories(${ANTLR_FasmLexer_OUTPUT_DIR}) 93 | include_directories(${ANTLR_FasmParser_OUTPUT_DIR}) 94 | 95 | # Add generated grammar to binary target 96 | add_library(parse_fasm SHARED ParseFasm.cpp 97 | ${ANTLR_FasmLexer_CXX_OUTPUTS} 98 | ${ANTLR_FasmParser_CXX_OUTPUTS}) 99 | target_link_libraries(parse_fasm ${ANTLR4_RUNTIME}) 100 | #target_compile_options(parse_fasm PRIVATE -Wno-attributes) # Disable warning from antlr4-runtime 101 | 102 | add_executable(parse_fasm_tests 103 | ParseFasmTests.cpp 104 | ${ANTLR_FasmLexer_CXX_OUTPUTS} 105 | ${ANTLR_FasmParser_CXX_OUTPUTS}) 106 | target_link_libraries(parse_fasm_tests ${ANTLR4_RUNTIME}) 107 | target_link_libraries(parse_fasm_tests gtest_main) 108 | #target_compile_options(parse_fasm_tests PRIVATE -Wno-attributes) # Disable warning from antlr4-runtime 109 | 110 | # Standalone executable 111 | add_executable(parse_fasm_run EXCLUDE_FROM_ALL 112 | ParseFasmRun.cpp 113 | ${ANTLR_FasmLexer_CXX_OUTPUTS} 114 | ${ANTLR_FasmParser_CXX_OUTPUTS}) 115 | target_link_libraries(parse_fasm_run ${ANTLR4_RUNTIME}) 116 | set_target_properties(parse_fasm_run PROPERTIES OUTPUT_NAME parse_fasm) 117 | #target_compile_options(parse_fasm_run PRIVATE -Wno-attributes) # Disable warning from antlr4-runtime 118 | 119 | # Unit tests 120 | include(CTest) 121 | 122 | add_test(NAME parse_fasm_tests 123 | COMMAND parse_fasm_tests) 124 | enable_testing() 125 | 126 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/tags.py DESTINATION .) 127 | -------------------------------------------------------------------------------- /src/ParseFasm.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 F4PGA Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | #include "FasmLexer.h" 18 | #include "FasmParser.h" 19 | #include "FasmParserVisitor.h" 20 | #include "antlr4-runtime.h" 21 | 22 | /// This code parses FASM and produces a lightweight binary format that 23 | /// is fast and simple to unpack based on the tag/length/value (TLV) 24 | /// format. 25 | /// 26 | /// For example, for a fixed width integer, this can just be: 27 | /// 28 | /// and for a variable length string: 29 | /// 30 | /// 31 | /// Note that the length itself must be a fixed width value, which does 32 | /// impose a size limit, but this format is more efficient and easier 33 | /// to decode than a UTF-8 style variable length encoding where a bit 34 | /// is reserved per per byte to indicate the end of a variable length 35 | /// value. 36 | /// 37 | /// Each 4-bytes of a numeric value is in native endian order. This 38 | /// format is designed to be produced and consumed on the same machine. 39 | /// 40 | /// TLVs can be nested, with each level adding 5 bytes of header 41 | /// overhead. There is a choice to aggregate values under another header 42 | /// (using withHeader) or not. Even though this encodes redundant size 43 | /// information, it can make the result easier to parse. 44 | /// 45 | /// Example of a nested TLV: 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// Note that there is no need for a closing tag. 51 | /// 52 | /// For example, the consumer can allocate space for results in larger 53 | /// chunks, in this case it can preallocate a line at a time, after 54 | /// reading the first 5 bytes, although this is mostly useful for 55 | /// consumers that can use the data directly without further 56 | /// manipulation. 57 | /// 58 | /// The format used here does not rely on knowing the size of the 59 | /// entire output, which allows streaming line by line, although 60 | /// ANTLR does not yet implement an incremental parser. 61 | /// 62 | /// For a concrete example, see the test case in ParseFasmTests.cpp 63 | 64 | /// External C Interface 65 | /// 66 | /// These functions serialize the FASM parse tree to an easy to parse 67 | /// tag/length/value binary format, where the tag is one byte and 68 | /// the length is 4 bytes, in native endianness (typically little.) 69 | extern "C" { 70 | void from_string(const char* in, 71 | bool hex, 72 | void (*ret)(const char* str, size_t), 73 | void (*err)(size_t, size_t, const char*)); 74 | void from_file(const char* path, 75 | bool hex, 76 | void (*ret)(const char* str, size_t), 77 | void (*err)(size_t, size_t, const char*)); 78 | } 79 | 80 | using namespace antlr4; 81 | using namespace antlrcpp; 82 | 83 | /// Hex mode is useful for debugging. 84 | /// In this mode, binary values are printed as hex values surrounded by < > 85 | bool hex_mode = false; 86 | 87 | /// The Num class provides an << operator that either dumps the raw value 88 | /// or prints the hex value based on the mode. It also can print a tag. 89 | class Num { 90 | public: 91 | /// Constructors, with optional tag character 92 | Num(char tag, uint32_t num) : num(num), tag(tag) {} 93 | Num(uint32_t num) : num(num), tag(0) {} 94 | 95 | uint32_t num; /// The value 96 | char tag; /// Tag character 97 | 98 | /// The bit width 99 | static constexpr int kWidth = sizeof(num) * 8; 100 | }; 101 | 102 | /// Output stream operator for Num. 103 | /// In hex mode, Nums are printed as , otherwise they are 104 | /// copied into the output stream in the underlying representation. 105 | /// As such, this will use the native endianness. 106 | std::ostream& operator<<(std::ostream& s, const Num& num) { 107 | if (num.tag) 108 | s << num.tag; 109 | if (hex_mode) { 110 | s << "<" << std::hex << num.num << ">"; 111 | } else { 112 | s.write(reinterpret_cast(&num.num), 113 | sizeof(num.num)); 114 | } 115 | return s; 116 | } 117 | 118 | /// The Str class wraps a std::string to provide an << operator 119 | /// that includes the tag and length. 120 | struct Str { 121 | /// Takes the tag and string data 122 | Str(char tag, std::string data) : tag(tag), data(data) {} 123 | 124 | char tag; 125 | std::string data; 126 | }; 127 | 128 | /// This output stream operator adds the header needed to decode 129 | /// a string of unknown length and type. 130 | /// Note that some characters are escaped in hex mode to 131 | /// avoid confusion. 132 | std::ostream& operator<<(std::ostream& s, const Str& str) { 133 | s << str.tag << Num(str.data.size()); 134 | if (hex_mode) { /// escape < \ > 135 | for (char c : str.data) { 136 | if (c == '<' || c == '>' || c == '\\') { 137 | s.put('\\'); 138 | } 139 | s.put(c); 140 | } 141 | } else { 142 | s << str.data; 143 | } 144 | return s; 145 | } 146 | 147 | /// Wraps a string in another header, used to aggregate data. 148 | std::string withHeader(char tag, std::string data) { 149 | std::ostringstream header; 150 | header << tag << Num(data.size()); 151 | return header.str() + data; 152 | } 153 | 154 | /// Counts characters that don't match the given character. 155 | /// Used to count digits skipping '_'. 156 | int count_without(std::string::iterator start, 157 | std::string::iterator end, 158 | char c) { 159 | int count = 0; 160 | auto it = start; 161 | while (it != end) { 162 | if (*it != c) { 163 | count++; 164 | } 165 | it++; 166 | } 167 | return count; 168 | } 169 | 170 | /// Calculates the number of leading pad bits are needed 171 | /// so that the rightmost bit will be the LSB of a Num. 172 | /// e.g. This would be 31 for 33'b0. 173 | int lead_bits(int bits) { 174 | return (Num::kWidth - (bits % Num::kWidth)) % Num::kWidth; 175 | } 176 | 177 | // clang-format off 178 | /// Decode a hex digit. 179 | int from_hex_digit(char c) { 180 | int result = 181 | c >= '0' && c <= '9' ? c - '0' : 182 | c >= 'a' && c <= 'f' ? c - 'a' + 10 : 183 | c >= 'A' && c <= 'F' ? c - 'A' + 10: -1; 184 | assert(result >= 0 && result < 16); 185 | return result; 186 | } 187 | // clang-format on 188 | 189 | /// Makes tags easy to extract for documentation and code generation. 190 | /// Use at most once per line to allow simple grepping. 191 | #define TAG(c, long_name) (c) 192 | 193 | /// Raised on parse errors 194 | struct ParseException { 195 | size_t line; ///< Line number of error. 196 | size_t position; ///< Position in that line. 197 | std::string message; ///< A descriptive message. 198 | }; 199 | 200 | /// Helper macro to convert a rule context into a string 201 | /// For use inside FasmParserBaseVisitor 202 | #define GET(x) (context->x() ? visit(context->x()).as() : "") 203 | 204 | /// FasmParserBaseVisitor is a visitor for the parse tree 205 | /// generated by the ANTLR parser. 206 | /// It will encode the tree a line at a time and stream out of 207 | /// the given std::ostream 208 | class FasmParserBaseVisitor : public FasmParserVisitor { 209 | public: 210 | static constexpr size_t kHeaderSize = 5; 211 | 212 | /// The constructor requires a std::ostream to stream encoded lines. 213 | /// This is to avoid storing an entire copy of the parse tree in a 214 | /// different form. 215 | FasmParserBaseVisitor(std::ostream& out) : out(out) {} 216 | 217 | /// Stream out FASM lines. 218 | virtual Any visitFasmFile( 219 | FasmParser::FasmFileContext* context) override { 220 | for (auto& line : context->fasmLine()) { 221 | std::string str = visit(line).as(); 222 | if (!str.empty()) { 223 | out << str; 224 | if (hex_mode) 225 | out << std::endl; 226 | } 227 | } 228 | return {}; 229 | } 230 | 231 | /// This is called for each FASM line. 232 | /// Tag: comment (#) 233 | /// Tag: line (l) 234 | virtual Any visitFasmLine( 235 | FasmParser::FasmLineContext* context) override { 236 | std::ostringstream data; 237 | data << GET(setFasmFeature) << GET(annotations); 238 | 239 | if (context->COMMENT_CAP()) { 240 | std::string c = context->COMMENT_CAP()->getText(); 241 | c.erase(0, 1); /// Remove the leading # 242 | data << Str(TAG('#', comment), c); 243 | } 244 | 245 | if (!data.str().empty()) { 246 | return withHeader(TAG('l', line), data.str()); 247 | } else { 248 | return std::string(); /// Don't emit empty lines. 249 | } 250 | } 251 | 252 | /// The set feature portion of a line (before annotations and comment.) 253 | /// Tag: feature (f) 254 | /// Tag: set feature (s) 255 | virtual Any visitSetFasmFeature( 256 | FasmParser::SetFasmFeatureContext* context) override { 257 | std::ostringstream data; 258 | data << Str(TAG('f', feature), context->FEATURE()->getText()) 259 | << GET(featureAddress) << GET(value); 260 | return withHeader(TAG('s', set_feature), data.str()); 261 | } 262 | 263 | /// The bracketed address, where the second number is optional. 264 | /// Tag: address (:) 265 | virtual Any visitFeatureAddress( 266 | FasmParser::FeatureAddressContext* context) override { 267 | std::ostringstream data; 268 | data << Num(std::stoul(context->INT(0)->getText())); 269 | 270 | if (context->INT(1)) { 271 | data << Num(std::stoul(context->INT(1)->getText())); 272 | } 273 | return withHeader(TAG(':', address), data.str()); 274 | } 275 | 276 | /// A Verilog style number. It can be "plain" (no leading size and 277 | /// base), or hex (h), binary (b), decimal (d), or octal (o). Tag: bit 278 | /// width (') 279 | virtual Any visitVerilogValue( 280 | FasmParser::VerilogValueContext* context) override { 281 | std::ostringstream data; 282 | if (context->verilogDigits()) { 283 | if (context->INT()) { 284 | data << Num( 285 | TAG('\'', width), 286 | std::stoi(context->INT()->getText())); 287 | } 288 | data << visit(context->verilogDigits()) 289 | .as(); 290 | } 291 | return data.str(); 292 | } 293 | 294 | /// A "plain" decimal value. 295 | /// Tag: plain (p) 296 | virtual Any visitPlainDecimal( 297 | FasmParser::PlainDecimalContext* context) override { 298 | std::ostringstream data; 299 | try { 300 | data << Num(TAG('p', plain), 301 | std::stoi(context->INT()->getText())); 302 | } catch (...) { 303 | throw ParseException{ 304 | .line = context->start->getLine(), 305 | .position = context->start->getCharPositionInLine(), 306 | .message = "Could not decode decimal number."}; 307 | } 308 | return data.str(); 309 | } 310 | 311 | /// A Verilog hex value. 312 | /// Tag: hex (h) 313 | virtual Any visitHexValue( 314 | FasmParser::HexValueContext* context) override { 315 | std::ostringstream data; 316 | std::string value = context->HEXADECIMAL_VALUE()->getText(); 317 | auto it = value.begin(); 318 | it += 2; /// skip 'h 319 | 320 | /// Build up Nums 4 bits at a time, skipping '_'. 321 | int bits = lead_bits(count_without(it, value.end(), '_') * 4); 322 | uint32_t word = 0; 323 | while (it != value.end()) { 324 | if (*it != '_') { 325 | word = (word << 4) | from_hex_digit(*it); 326 | bits += 4; 327 | if (bits == Num::kWidth) { 328 | data << Num(word); 329 | word = 0; 330 | bits = 0; 331 | } 332 | } 333 | it++; 334 | } 335 | assert(!word); 336 | return withHeader(TAG('h', hex), data.str()); 337 | } 338 | 339 | /// A Verilog binary value. 340 | /// Tag: binary (b) 341 | virtual Any visitBinaryValue( 342 | FasmParser::BinaryValueContext* context) override { 343 | std::ostringstream data; 344 | std::string value = context->BINARY_VALUE()->getText(); 345 | auto it = value.begin(); 346 | it += 2; /// skip 'b 347 | 348 | /// Build up Nums a bit at a time, skipping '_'. 349 | int bits = lead_bits(count_without(it, value.end(), '_')); 350 | uint32_t word = 0; 351 | while (it != value.end()) { 352 | if (*it != '_') { 353 | word = (word << 1) | (*it == '1' ? 1 : 0); 354 | bits += 1; 355 | if (bits == Num::kWidth) { 356 | data << Num(word); 357 | word = 0; 358 | bits = 0; 359 | } 360 | } 361 | it++; 362 | } 363 | assert(!word); 364 | return withHeader(TAG('b', binary), data.str()); 365 | } 366 | 367 | /// A Verilog decimal value. 368 | /// Tags: decimal (d) 369 | virtual Any visitDecimalValue( 370 | FasmParser::DecimalValueContext* context) override { 371 | long long unsigned integer = 0; 372 | std::string value = context->DECIMAL_VALUE()->getText(); 373 | auto it = value.begin(); 374 | it += 2; /// skip 'd 375 | 376 | /// Build up a Num, skipping '_'. 377 | while (it != value.end()) { 378 | if (*it != '_') { 379 | int digit_value = *it - '0'; 380 | // Check for overflow 381 | if (integer > (std::numeric_limits< 382 | long long unsigned>::max() - 383 | digit_value) / 384 | 10) { 385 | throw ParseException{ 386 | .line = context->start->getLine(), 387 | .position = 388 | context->start 389 | ->getCharPositionInLine(), 390 | .message = 391 | "Could not decode decimal " 392 | "number."}; 393 | } 394 | integer = (integer * 10) + digit_value; 395 | } 396 | it++; 397 | } 398 | 399 | std::ostringstream data; 400 | data << Num(integer); 401 | return withHeader(TAG('d', decimal), data.str()); 402 | } 403 | 404 | /// A Verilog octal value. 405 | /// Tags: octal (o) 406 | virtual Any visitOctalValue( 407 | FasmParser::OctalValueContext* context) override { 408 | std::ostringstream data; 409 | std::string value = context->OCTAL_VALUE()->getText(); 410 | auto it = value.begin(); 411 | it += 2; /// skip 'b 412 | 413 | /// Build up a Num 3 bits at a time. 414 | /// Note that since the word size is not evenly divisible by 3, 415 | /// intermediate values can be greater than the word size. 416 | /// This is why the 'word' below is 64 bits wide. 417 | int bits = lead_bits(count_without(it, value.end(), '_') * 3); 418 | uint64_t word = 0; /// could temporarily overflow uint32_t 419 | while (it != value.end()) { 420 | if (*it != '_') { 421 | word = (word << 3) | (*it - '0'); 422 | bits += 3; 423 | if (bits >= Num::kWidth) { 424 | data << Num(word >> 425 | (bits - Num::kWidth)); 426 | word >>= Num::kWidth; 427 | bits -= Num::kWidth; 428 | } 429 | } 430 | it++; 431 | } 432 | assert(!word); 433 | return withHeader(TAG('o', octal), data.str()); 434 | } 435 | 436 | /// A collection of annotations. { ... } 437 | /// Tags: annotations ({) 438 | virtual Any visitAnnotations( 439 | FasmParser::AnnotationsContext* context) override { 440 | std::ostringstream data; 441 | for (auto& a : context->annotation()) { 442 | data << visit(a).as(); 443 | } 444 | return withHeader(TAG('{', annotations), data.str()); 445 | } 446 | 447 | /// An annotation: x = "y" 448 | /// Tags: annotation (a), annotation name (.), annotation value (=) 449 | virtual Any visitAnnotation( 450 | FasmParser::AnnotationContext* context) override { 451 | std::ostringstream data; 452 | data << Str(TAG('.', annotation_name), 453 | context->ANNOTATION_NAME()->getText()); 454 | if (context->ANNOTATION_VALUE()) { 455 | std::string value = 456 | context->ANNOTATION_VALUE()->getText(); 457 | value.erase(0, 1); /// Convert "value" -> value 458 | value.pop_back(); 459 | data << Str(TAG('=', annotation_value), value); 460 | } 461 | return withHeader(TAG('a', annotation), data.str()); 462 | } 463 | 464 | private: 465 | std::ostream& out; 466 | }; 467 | 468 | // Prevent use of the GET macro outside FasmParseBaseVisitor 469 | #undef GET 470 | 471 | class FasmErrorListener : public BaseErrorListener { 472 | public: 473 | virtual void syntaxError(Recognizer* recognizer, 474 | Token* token, 475 | size_t line, 476 | size_t position, 477 | const std::string& msg, 478 | std::exception_ptr e) override { 479 | throw ParseException{.line = line, 480 | .position = position, 481 | .message = std::string(msg)}; 482 | } 483 | }; 484 | 485 | /// Common portion of 'from_string' and 'from_file'. 486 | /// Consumes an input stream and produces an output stream. 487 | static void parse_fasm(std::istream& in, std::ostream& out) { 488 | ANTLRInputStream stream(in); 489 | FasmLexer lexer(&stream); 490 | FasmErrorListener errorListener; 491 | lexer.removeErrorListeners(); 492 | lexer.addErrorListener(&errorListener); 493 | CommonTokenStream tokens(&lexer); 494 | FasmParser parser(&tokens); 495 | parser.removeErrorListeners(); 496 | parser.addErrorListener(&errorListener); 497 | auto* tree = parser.fasmFile(); 498 | FasmParserBaseVisitor(out).visit(tree); 499 | } 500 | 501 | /// Parse the given input string, returning output. 502 | /// Use hex mode (see above) if hex is true. 503 | /// Use a callback to avoid copying the result. 504 | void from_string(const char* in, 505 | bool hex, 506 | void (*ret)(const char* str, size_t), 507 | void (*err)(size_t, size_t, const char*)) { 508 | hex_mode = hex; 509 | std::istringstream input(in); 510 | std::ostringstream output; 511 | 512 | try { 513 | parse_fasm(input, output); 514 | output.put(0); 515 | std::string result = output.str(); 516 | ret(result.c_str(), result.size()); 517 | } catch (ParseException e) { 518 | // Parse failure will throw this exception. 519 | err(e.line, e.position, e.message.c_str()); 520 | } 521 | } 522 | 523 | /// Parse the given input file, returning output. 524 | /// Use hex mode (see above) if hex is true. 525 | /// Use a callback to avoid copying the result. 526 | void from_file(const char* path, 527 | bool hex, 528 | void (*ret)(const char* str, size_t), 529 | void (*err)(size_t, size_t, const char*)) { 530 | hex_mode = hex; 531 | std::fstream input(std::string(path), input.in); 532 | std::ostringstream output; 533 | if (input.is_open()) { 534 | try { 535 | parse_fasm(input, output); 536 | output.put(0); 537 | std::string result = output.str(); 538 | ret(result.c_str(), result.size()); 539 | } catch (ParseException e) { 540 | // Parse failure will throw this exception. 541 | err(e.line, e.position, e.message.c_str()); 542 | } 543 | } else { 544 | err(0, 0, "Couldn't open file"); 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/ParseFasmRun.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 F4PGA Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | #include "ParseFasm.cpp" 18 | 19 | /// This can be used as a standalone utility. 20 | /// parse_fasm [file] [-hex] 21 | /// file : The file to parse, otherwise use stdin. 22 | /// -hex : Enable hex mode. 23 | int main(int argc, char* argv[]) { 24 | /// If no args say otherwise, 25 | /// run as a filter (stdin -> parse_fasm -> stdout) 26 | bool filter = true; 27 | 28 | /// Parse flags first 29 | for (int i = 1; i < argc; i++) { 30 | std::string arg(argv[i]); 31 | if (arg == "-hex") { 32 | hex_mode = true; 33 | } 34 | } 35 | 36 | /// Run parse on file arguments 37 | for (int i = 1; i < argc; i++) { 38 | std::string arg(argv[i]); 39 | if (arg[0] == '-') { 40 | std::ifstream in; 41 | in.open(arg); 42 | parse_fasm(in, std::cout); 43 | } 44 | } 45 | 46 | /// Run as a filter 47 | if (filter) { 48 | std::istringstream in_line; 49 | for (std::string line; std::getline(std::cin, line);) { 50 | in_line.str(line); 51 | parse_fasm(in_line, std::cout); 52 | } 53 | } 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /src/ParseFasmTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 F4PGA Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | /// Unit tests for ParseFasm 18 | 19 | #include 20 | #include "ParseFasm.cpp" 21 | 22 | // Test the Num class 23 | TEST(ParseFasmTests, Num) { 24 | std::ostringstream str; 25 | str << Num('a', 0x74736554); 26 | if (str.str() == "aTest") { 27 | std::cout << "little endian" << std::endl; 28 | } else if (str.str() == "atseT") { 29 | std::cout << "big endian" << std::endl; 30 | } else { 31 | ASSERT_TRUE(false); 32 | } 33 | } 34 | 35 | // Check that count_without() works 36 | TEST(ParseFasmTests, count_without) { 37 | std::string str = "_01_2_34_"; 38 | EXPECT_EQ(count_without(str.begin(), str.end(), '_'), 5); 39 | } 40 | 41 | // clang-format off 42 | // A test of the full parser 43 | // Formatting is disabled to allow indenting the output string 44 | // to illustrate the structure. 45 | TEST(ParseFasmTests, parse_fasm) { 46 | std::istringstream input("a.b.c[31:0] = 7'o123 { d = \"e\", .f = \"\" } # hello\nthere"); 47 | std::ostringstream output; 48 | bool stored_hex_mode = hex_mode; 49 | hex_mode = true; 50 | parse_fasm(input, output); 51 | 52 | /// The below string is the input above encoded in hex mode. 53 | EXPECT_EQ(output.str(), 54 | "l<50>" /// Line 55 | "s<20>" /// Set feature 56 | "f<5>a.b.c" /// Feature = "a.b.c" 57 | ":<7><1f><0>" /// Address = 31:0 58 | "'<7>" /// Width = 7 59 | "o<4><53>" /// Octal value = 123 60 | "{<1c>" /// Annotations (list) 61 | "a" /// Annotation 62 | ".<1>d" /// Name = "d" 63 | "=<1>e" /// Value = "e" 64 | "a" /// Annotation 65 | ".<2>.f" /// Name = ".f" 66 | "=<0>" /// Value = "" 67 | "#<6> hello" /// Comment 68 | "\n" /// Newline (only in hex mode for readability) 69 | "l" /// Line 70 | "s<9>" /// Set feature 71 | "f<5>there" /// Feature = "there" 72 | "\n" /// Newline (only in hex mode for readability) 73 | ); 74 | hex_mode = stored_hex_mode; 75 | } 76 | // clang-format on 77 | -------------------------------------------------------------------------------- /src/antlr/FasmLexer.g4: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 F4PGA Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | lexer grammar FasmLexer; 18 | 19 | // White space 20 | S : [ \t] -> skip ; // Skip all outer white space 21 | NEWLINE : [\n\r] ; // NEWLINE is not skipped, because it is needed to break lines. 22 | 23 | // Matches a feature identifier 24 | fragment IDENTIFIER : [a-zA-Z] [0-9a-zA-Z_]* ; 25 | FEATURE : IDENTIFIER ('.' IDENTIFIER)* ; 26 | 27 | // Number values 28 | // The leading '[hbdo] is kept in the same token to 29 | // disambiguate the grammar. 30 | HEXADECIMAL_VALUE : '\'h' [ \t]* [0-9a-fA-F_]+ ; 31 | BINARY_VALUE : '\'b' [ \t]* [01_]+ ; 32 | DECIMAL_VALUE : '\'d' [ \t]* [0-9_]+ ; 33 | OCTAL_VALUE : '\'o' [ \t]* [0-7_]+ ; 34 | INT : [0-9]+ ; 35 | 36 | // Everything after a # until a newline. 37 | COMMENT_CAP : '#' (~[\n\r])* ; 38 | 39 | // Simple tokens 40 | // These are not referenced in the parser by name, 41 | // but instead by the character they match. 42 | // They need to be here so the lexer will tokenize them. 43 | // This is needed because otherwise an IDENTIFIER with 44 | // a leading dot, or no dot, could also be an ANNOTATION_NAME. 45 | EQUAL : '=' ; 46 | OPEN_BRACKET : '[' ; 47 | COLON : ':' ; 48 | CLOSE_BRACKET : ']' ; 49 | 50 | // An opening curly bracket enters ANNOTATION_MODE 51 | BEGIN_ANNOTATION : '{' -> pushMode(ANNOTATION_MODE) ; 52 | 53 | // https://github.com/antlr/antlr4/issues/1226#issuecomment-230382658 54 | // Any character which does not match one of the above rules will appear in the token stream as 55 | // an ErrorCharacter token. This ensures the lexer itself will never encounter a syntax error, 56 | // so all error handling may be performed by the parser. 57 | ErrorCharacter 58 | : . 59 | ; 60 | 61 | // Inside an annotation, only the rules below apply. 62 | // That is why there is some duplication. 63 | mode ANNOTATION_MODE; 64 | 65 | // White space is skipped. 66 | ANNOTATION_S : [ \t] -> skip ; 67 | 68 | ANNOTATION_NAME : [.a-zA-Z] [0-9a-zA-Z_]* ; 69 | fragment NON_ESCAPE_CHARACTERS : ~[\\"] ; 70 | fragment ESCAPE_SEQUENCES : [\\] [\\"] ; 71 | ANNOTATION_VALUE : '"' (NON_ESCAPE_CHARACTERS | ESCAPE_SEQUENCES)* '"' ; 72 | 73 | // Simple tokens. 74 | ANNOTATION_EQUAL : '=' ; 75 | ANNOTATION_COMMA : ',' ; 76 | 77 | // A closing curly bracket pops out of ANNOTATION_MODE 78 | END_ANNOTATION : '}' -> popMode ; 79 | -------------------------------------------------------------------------------- /src/antlr/FasmParser.g4: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2022 F4PGA Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | parser grammar FasmParser; 18 | 19 | // Reference tokens from the lexer. 20 | options { tokenVocab=FasmLexer; } 21 | 22 | // fasmFile is the root of the parser 23 | fasmFile : fasmLine (NEWLINE fasmLine)* EOF ; 24 | 25 | // Describes one line 26 | fasmLine : setFasmFeature? 27 | annotations? 28 | COMMENT_CAP? 29 | ; 30 | 31 | // Example: a.b.c[31:0] = 3'o7 32 | setFasmFeature : FEATURE featureAddress? (EQUAL value)? ; 33 | featureAddress : '[' INT (':' INT)? ']' ; 34 | 35 | // Matches an ordinary decimal integer, or a Verilog integer literal. 36 | value : INT? verilogDigits # VerilogValue 37 | | INT # PlainDecimal 38 | ; 39 | 40 | // Matches the portion of a Verilog integer literal starting with 41 | // the quote character (') e.g. 'b1101 42 | verilogDigits : HEXADECIMAL_VALUE # HexValue 43 | | BINARY_VALUE # BinaryValue 44 | | DECIMAL_VALUE # DecimalValue 45 | | OCTAL_VALUE # OctalValue 46 | ; 47 | 48 | // Example: { a = "b", c = "d" } 49 | annotations : BEGIN_ANNOTATION annotation (',' annotation)* END_ANNOTATION ; 50 | annotation : ANNOTATION_NAME ANNOTATION_EQUAL ANNOTATION_VALUE ; 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017-2022 F4PGA Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | import os 21 | import os.path 22 | import importlib 23 | 24 | import unittest 25 | import fasm 26 | import fasm.parser 27 | 28 | parsers = {} 29 | for name in fasm.parser.available: 30 | parsers[name] = importlib.import_module('fasm.parser.' + name) 31 | 32 | 33 | def example(fname): 34 | return os.path.join(os.path.dirname(__file__), '..', 'examples', fname) 35 | 36 | 37 | def check_round_trip(test, parser, result): 38 | s = fasm.fasm_tuple_to_string(result) 39 | test.assertEqual(list(parser.parse_fasm_string(s)), result) 40 | 41 | 42 | class TestFasm(unittest.TestCase): 43 | def test_blank_file(self): 44 | for name, parser in parsers.items(): 45 | with self.subTest(name, parser=name): 46 | result = list( 47 | parser.parse_fasm_filename(example('blank.fasm'))) 48 | self.assertEqual(result, []) 49 | 50 | check_round_trip(self, parser, result) 51 | 52 | def test_comment_file(self): 53 | for name, parser in parsers.items(): 54 | with self.subTest(name, parser=name): 55 | result = list( 56 | parser.parse_fasm_filename(example('comment.fasm'))) 57 | self.assertEqual( 58 | result, [ 59 | fasm.FasmLine( 60 | set_feature=None, 61 | annotations=None, 62 | comment=' Only a comment.', 63 | ) 64 | ]) 65 | 66 | check_round_trip(self, parser, result) 67 | 68 | def test_one_line_feature(self): 69 | for name, parser in parsers.items(): 70 | with self.subTest(name, parser=name): 71 | result = list( 72 | parser.parse_fasm_filename(example('feature_only.fasm'))) 73 | self.assertEqual( 74 | result, [ 75 | fasm.FasmLine( 76 | set_feature=fasm.SetFasmFeature( 77 | feature='EXAMPLE_FEATURE.X0.Y0.BLAH', 78 | start=None, 79 | end=None, 80 | value=1, 81 | value_format=None, 82 | ), 83 | annotations=None, 84 | comment=None, 85 | ) 86 | ]) 87 | 88 | self.assertEqual( 89 | fasm.fasm_tuple_to_string(result), 90 | 'EXAMPLE_FEATURE.X0.Y0.BLAH\n') 91 | check_round_trip(self, parser, result) 92 | 93 | def test_examples_file(self): 94 | for name, parser in parsers.items(): 95 | with self.subTest(name, parser=name): 96 | result = list(parser.parse_fasm_filename(example('many.fasm'))) 97 | check_round_trip(self, parser, result) 98 | 99 | def test_implementations(self): 100 | self.assertTrue('antlr' in fasm.parser.available) 101 | self.assertTrue('textx' in fasm.parser.available) 102 | 103 | 104 | if __name__ == '__main__': 105 | unittest.main() 106 | -------------------------------------------------------------------------------- /third_party/antlr4_lib/LICENSE.txt: -------------------------------------------------------------------------------- 1 | [The "BSD 3-clause license"] 2 | Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | ===== 29 | 30 | MIT License for codepointat.js from https://git.io/codepointat 31 | MIT License for fromcodepoint.js from https://git.io/vDW1m 32 | 33 | Copyright Mathias Bynens 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining 36 | a copy of this software and associated documentation files (the 37 | "Software"), to deal in the Software without restriction, including 38 | without limitation the rights to use, copy, modify, merge, publish, 39 | distribute, sublicense, and/or sell copies of the Software, and to 40 | permit persons to whom the Software is furnished to do so, subject to 41 | the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be 44 | included in all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 47 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 48 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 49 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 50 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 51 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 52 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 53 | -------------------------------------------------------------------------------- /third_party/antlr4_lib/README.md: -------------------------------------------------------------------------------- 1 | # ANTLR v4 2 | 3 | [![Build Travis-CI Status](https://travis-ci.org/antlr/antlr4.svg?branch=master)](https://travis-ci.org/antlr/antlr4) [![Build AppVeyor Status](https://ci.appveyor.com/api/projects/status/5acpbx1pg7bhgh8v/branch/master?svg=true)](https://ci.appveyor.com/project/parrt/antlr4) [![Java 7+](https://img.shields.io/badge/java-7+-4c7e9f.svg)](http://java.oracle.com) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://raw.githubusercontent.com/antlr/antlr4/master/LICENSE.txt) 4 | 5 | **ANTLR** (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It's widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build parse trees and also generates a listener interface (or visitor) that makes it easy to respond to the recognition of phrases of interest. 6 | 7 | *Given day-job constraints, my time working on this project is limited so I'll have to focus first on fixing bugs rather than changing/improving the feature set. Likely I'll do it in bursts every few months. Please do not be offended if your bug or pull request does not yield a response! --parrt* 8 | 9 | [![Donate](https://www.paypal.com/en_US/i/btn/x-click-butcc-donate.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=BF92STRXT8F8Q) 10 | 11 | ## Authors and major contributors 12 | 13 | * [Terence Parr](http://www.cs.usfca.edu/~parrt/), parrt@cs.usfca.edu 14 | ANTLR project lead and supreme dictator for life 15 | [University of San Francisco](http://www.usfca.edu/) 16 | * [Sam Harwell](http://tunnelvisionlabs.com/) (Tool co-author, Java and C# target) 17 | * Eric Vergnaud (Javascript, Python2, Python3 targets and significant work on C# target) 18 | * [Peter Boyer](https://github.com/pboyer) (Go target) 19 | * [Mike Lischke](http://www.soft-gems.net/) (C++ completed target) 20 | * Dan McLaughlin (C++ initial target) 21 | * David Sisson (C++ initial target and test) 22 | * [Janyou](https://github.com/janyou) (Swift target) 23 | * [Ewan Mellor](https://github.com/ewanmellor), [Hanzhou Shi](https://github.com/hanjoes) (Swift target merging) 24 | * [Ben Hamilton](https://github.com/bhamiltoncx) (Full Unicode support in serialized ATN and all languages' runtimes for code points > U+FFFF) 25 | * [Marcos Passos](https://github.com/marcospassos) (PHP target) 26 | * [Lingyu Li](https://github.com/lingyv-li) (Dart target) 27 | 28 | ## Useful information 29 | 30 | * [Release notes](https://github.com/antlr/antlr4/releases) 31 | * [Getting started with v4](https://github.com/antlr/antlr4/blob/master/doc/getting-started.md) 32 | * [Official site](http://www.antlr.org/) 33 | * [Documentation](https://github.com/antlr/antlr4/blob/master/doc/index.md) 34 | * [FAQ](https://github.com/antlr/antlr4/blob/master/doc/faq/index.md) 35 | * [ANTLR code generation targets](https://github.com/antlr/antlr4/blob/master/doc/targets.md)
(Currently: Java, C#, Python2|3, JavaScript, Go, C++, Swift) 36 | * [Java API](http://www.antlr.org/api/Java/index.html) 37 | * [ANTLR v3](http://www.antlr3.org/) 38 | * [v3 to v4 Migration, differences](https://github.com/antlr/antlr4/blob/master/doc/faq/general.md) 39 | 40 | You might also find the following pages useful, particularly if you want to mess around with the various target languages. 41 | 42 | * [How to build ANTLR itself](https://github.com/antlr/antlr4/blob/master/doc/building-antlr.md) 43 | * [How we create and deploy an ANTLR release](https://github.com/antlr/antlr4/blob/master/doc/releasing-antlr.md) 44 | 45 | ## The Definitive ANTLR 4 Reference 46 | 47 | Programmers run into parsing problems all the time. Whether it’s a data format like JSON, a network protocol like SMTP, a server configuration file for Apache, a PostScript/PDF file, or a simple spreadsheet macro language—ANTLR v4 and this book will demystify the process. ANTLR v4 has been rewritten from scratch to make it easier than ever to build parsers and the language applications built on top. This completely rewritten new edition of the bestselling Definitive ANTLR Reference shows you how to take advantage of these new features. 48 | 49 | You can buy the book [The Definitive ANTLR 4 Reference](http://amzn.com/1934356999) at amazon or an [electronic version at the publisher's site](https://pragprog.com/book/tpantlr2/the-definitive-antlr-4-reference). 50 | 51 | You will find the [Book source code](http://pragprog.com/titles/tpantlr2/source_code) useful. 52 | 53 | ## Additional grammars 54 | [This repository](https://github.com/antlr/grammars-v4) is a collection of grammars without actions where the 55 | root directory name is the all-lowercase name of the language parsed 56 | by the grammar. For example, java, cpp, csharp, c, etc... 57 | -------------------------------------------------------------------------------- /third_party/antlr4_lib/antlr-4.9.3-complete.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chipsalliance/fasm/ffafe821bae68637fe46e36bcfd2a01b97cdf6f2/third_party/antlr4_lib/antlr-4.9.3-complete.jar -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35,36,37} 3 | 4 | [testenv] 5 | passenv = 6 | CMAKE_BUILD_PARALLEL_LEVEL 7 | basepython = 8 | py27: python2.7 9 | py34: python3.4 10 | py35: python3.5 11 | py36: python3.6 12 | py37: python3.7 13 | py38: python3.8 14 | py39: python3.9 15 | deps = 16 | -rrequirements.txt 17 | commands = 18 | check-manifest --ignore tox.ini,tests*,.github* 19 | # The --no-use-pep517 flag is needed because there is currently no way to pass flags with PEP517. 20 | # Relevant issue: https://github.com/pypa/pip/issues/5771 21 | pip install -e . --install-option="--antlr-runtime={env:ANTLR4_RUNTIME_TYPE:static}" --no-use-pep517 22 | flake8 --exclude .git,env,dist,build,third_party,.tox,fasm/version.py . 23 | py.test -s tests 24 | 25 | [flake8] 26 | exclude = .tox,*.egg,build,data,third_party 27 | select = E,W,F 28 | ignore = W503 # Deprecated warning contradicts W504 29 | per-file-ignores = 30 | __init__.py: F401 31 | 32 | [check-manifest] 33 | ignore = 34 | fasm/parser/tags.py 35 | fasm/parser/libparse_fasm.* 36 | # This is needed because this is an empty directory, which causes problems: 37 | # https://stackoverflow.com/questions/2640378/include-empty-directory-with-python-setup-py-sdist 38 | third_party/antlr4/runtime/PHP 39 | -------------------------------------------------------------------------------- /update_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import platform 4 | import subprocess 5 | import sys 6 | 7 | VERSION_FILE = 'fasm/version.py' 8 | VERSION_FILE_TEMPLATE = '''\ 9 | #!/usr/bin/env python3 10 | # -*- coding: utf-8 -*- 11 | # 12 | # Copyright 2017-2022 F4PGA Authors 13 | # 14 | # Licensed under the Apache License, Version 2.0 (the "License"); 15 | # you may not use this file except in compliance with the License. 16 | # You may obtain a copy of the License at 17 | # 18 | # http://www.apache.org/licenses/LICENSE-2.0 19 | # 20 | # Unless required by applicable law or agreed to in writing, software 21 | # distributed under the License is distributed on an "AS IS" BASIS, 22 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | # See the License for the specific language governing permissions and 24 | # limitations under the License. 25 | # 26 | # SPDX-License-Identifier: Apache-2.0 27 | 28 | # ** WARNING ** 29 | # This file is auto-generated by the update_version.py script. 30 | # ** WARNING ** 31 | 32 | version_str = "{version}" 33 | version_tuple = {version_tuple} 34 | try: 35 | from packaging.version import Version as V 36 | pversion = V("{version}") 37 | except ImportError: 38 | pass 39 | 40 | git_hash = "{git_hash}" 41 | git_describe = "{git_describe}" 42 | git_msg = """\\ 43 | {git_msg} 44 | """ 45 | ''' 46 | 47 | GIT = 'git' 48 | if platform.system() == 'Windows': 49 | GIT = 'git.exe' 50 | 51 | 52 | def get_hash(): 53 | cmd = [GIT, 'rev-parse', 'HEAD'] 54 | try: 55 | return subprocess.check_output(cmd).decode('utf-8').strip() 56 | except OSError: 57 | print(cmd) 58 | raise 59 | 60 | 61 | def get_describe(): 62 | cmd = [ 63 | GIT, 'describe', '--tags', 'HEAD', '--match', 'v*', '--exclude', '*-r*' 64 | ] 65 | try: 66 | return subprocess.check_output(cmd).decode('utf-8').strip() 67 | except OSError: 68 | print(cmd) 69 | raise 70 | 71 | 72 | def get_msg(): 73 | cmd = [GIT, 'log', '-1', 'HEAD'] 74 | try: 75 | data = subprocess.check_output(cmd).decode('utf-8') 76 | except OSError: 77 | print(cmd) 78 | raise 79 | return '\n'.join(line.rstrip() for line in data.split('\n')) 80 | 81 | 82 | def create_version_tuple(git_describe): 83 | """ 84 | >>> t = '''\\ 85 | ... v0.0 86 | ... v0.0.0 87 | ... v1.0.1-265-g5f0c7a7 88 | ... v0.0-7004-g1cf70ea2 89 | ... ''' 90 | >>> for d in t.splitlines(): 91 | ... v = create_version_tuple(d) 92 | ... print((create_version_str(v), v)) 93 | ('0.0', (0, 0, None)) 94 | ('0.0.0', (0, 0, 0, None)) 95 | ('1.0.1.post265', (1, 0, 1, 265)) 96 | ('0.0.post7004', (0, 0, 7004)) 97 | """ 98 | vtag = git_describe.strip() 99 | if vtag.startswith('v'): 100 | vtag = vtag[1:] 101 | 102 | vbits = vtag.split('.') 103 | vpost = [None] 104 | if '-' in vbits[-1]: 105 | vend = vbits.pop(-1).split('-') 106 | assert len(vend) == 3, (vtag, vbits, vend) 107 | assert len(vend[0]) > 0, (vtag, vbits, vend) 108 | vbits.append(vend.pop(0)) 109 | vpost = [int(vend.pop(0))] 110 | assert vend[-1].startswith('g'), (vtag, vbits, vend, vpost) 111 | vbits = [int(i) for i in vbits] 112 | vbits.extend(vpost) 113 | return tuple(vbits) 114 | 115 | 116 | def create_version_str(version_tuple): 117 | vbits = [str(i) for i in version_tuple] 118 | if version_tuple[-1] is None: 119 | vbits.pop(-1) 120 | else: 121 | vbits[-1] = 'post' + vbits[-1] 122 | return '.'.join(vbits) 123 | 124 | 125 | def update_version_py(args): 126 | output = VERSION_FILE_TEMPLATE.format(**args) 127 | 128 | old = '' 129 | try: 130 | with open(VERSION_FILE) as f: 131 | old = f.read() 132 | except IOError as e: 133 | print(e) 134 | 135 | if old != output: 136 | with open(VERSION_FILE, 'w') as f: 137 | f.write(output) 138 | print('Updated {}'.format(VERSION_FILE)) 139 | 140 | 141 | def main(args): 142 | git_hash = get_hash() 143 | git_describe = get_describe() 144 | git_msg = get_msg() 145 | 146 | version_tuple = create_version_tuple(git_describe) 147 | version = create_version_str(version_tuple) 148 | 149 | update_version_py(locals()) 150 | 151 | return 0 152 | 153 | 154 | if __name__ == "__main__": 155 | import doctest 156 | failure_count, test_count = doctest.testmod() 157 | if failure_count > 0: 158 | sys.exit(-1) 159 | sys.exit(main(sys.argv)) 160 | --------------------------------------------------------------------------------