This document describes the steps needed to clone, install, and test the repository, as well as a description of our coding and contributing conventions.
We use uv for dependency management of xDSL. See uv's getting started page for more details.
# Ensure uv is installed
uv --versionThen, here are the commands to locally set up your development repository:
# Clone repo
git clone https://github.com/xdslproject/xdsl.git
cd xdsl
# Set up local environment with all optional and dev dependencies
# Creates a virtual environment called `.venv`
make venv
# Set up pre-commit hook for automatic formatting
make precommit-install
# Run all tests to verify installation was successful
make testsPlease take a look at the Makefile for the available commands such as running specific tests, running the documentation website locally, and others.
MLIR/LLVM compatibility tests under tests/filecheck/mlir-conversion/with-mlir/
are opt-in and require explicit paths to upstream tools:
XDSL_MLIR_OPT— path tomlir-optXDSL_MLIR_TRANSLATE— path tomlir-translateXDSL_LLVM_DIFF— path tollvm-diffXDSL_LLI- path tolli
If you use Nix, nix develop sets these automatically via the flake
shellHook — no extra configuration is needed.
If you do not use Nix, copy .env.example to .env, set the paths to your
LLVM/MLIR build, and run make filecheck (the Makefile passes .env to uv run
via --env-file when the file exists).
For some use-cases, such as running xDSL with PyPy,
it may be preferable to install a minimal set of dependencies instead.
This can be done with uv sync. Note that Pyright will then complain
about missing dependencies, so run make tests-functional instead of
make tests to test the functionality of xDSL.
The xDSL project uses pytest unit tests, LLVM-style filecheck tests and performance
benchmarks. They can be executed from the root directory with make tests (which runs
everything except benchmarks and also runs pyright for type checking).
Python tests in tests/ (excluding tests/filecheck) for testing APIs and logic:
# Run unit tests
uv run pytest
# or via makefile
make pytestFile-based tests in tests/filecheck using filecheck (a Python reimplementation of
LLVM's FileCheck) to verify tool output. These tests rely on the textual format to
represent and construct IR. They are used to test that custom format implementations
print and parse in the expected way, and to verify transformations such as pattern
rewrites or passes:
# Run filecheck tests
uv run lit tests/filecheck
# or via makefile
make filecheckNote that when a lit test fails, it also prints the command that was run, which can
usually be quickly copy/pasted to the terminal to inspect the unexpected output.
When adding or updating tests, sometimes large chunks of CHECK lines will have to be
added or updated.
Our team created a tool called filecheckize that can sometimes be useful to ease this
work.
It's not added by default to the developer installation, but can still be used via uvx
(installed with uv) like so:
echo "%c1 = arith.constant 1 : index
%res = arith.addi %c1, %c1 : index
" | uvx filecheckizeThis will print the following:
// CHECK: %c1 = arith.constant 1 : index
// CHECK-NEXT: %res = arith.addi %c1, %c1 : indexfilecheckize provides handy options like --mlir-anonymize to convert SSA value names
to {{.*}} and --check-prefix to customize CHECK commands.
It's rarely a good idea to use the ouput of filecheckize directly as the test for your
transformation or printing/parsing test, but it can be handy as a starting point to go
from the output of your command.
To generate coverage reports for both unit tests and filecheck tests:
# Run coverage for unit tests and filecheck tests, then combine data files
make coverage
# Generate a coverage report
make coverage-report
# Or generate an HTML coverage report
make coverage-report-htmlYou can also run coverage for each test type individually:
# Run coverage for unit tests only
make coverage-tests
# Run coverage for filecheck tests only
make coverage-filecheck-testsBenchmarks for the project are tracked in the https://github.com/xdslproject/xdsl-bench repository. These run automatically every day on the main branch, reporting their results to https://xdsl.dev/xdsl-bench/. However, they can also be ran manually by cloning the repository and pointing the submodule at your feature branch to benchmark.
Configuration for linting and formatting is found in pyproject.toml.
Ruff is used for linting and formatting.
Configured in [tool.ruff].
Pyright is used for static type checking.
Configured in [tool.pyright].
# Format code
uv run ruff format
# Type check code
uv run pyright
# or via makefile
make pyrightImportant
xDSL currently relies on an experimental feature of Pyright called TypeForm. TypeForm is in discussion and will likely land in some future version of Python.
For xDSL to type check correctly using Pyright, please add this to your pyproject.toml:
[tool.pyright]
enableExperimentalFeatures = trueTo automate the formatting and type checking, we use pre-commit hooks from the prek package, a drop-in replacement for pre-commit.
# Install the pre-commit on your `.git` folder
make precommit-install
# Run the hooks
make precommitWe aim to follow these rules for all changes in this repository:
-
We aim for consistency in the code style and architectural patterns throughout the codebase in order to make it as easy as possible to understand and modify any part of xDSL.
-
We fix issues immediately rather than relying on future refactoring, as technical debt tends to accumulate and become harder to address over time.
-
We prefer simplicity: no code is better than obvious code, which is better than clever code. Premature abstraction often adds complexity without clear benefit.
-
We prioritize code locality over DRY (Don't Repeat Yourself). Keeping related logic close together - even if it results in slight duplication - makes it easier to understand code in isolation. We minimize variable scope.
-
We write self-describing code by using descriptive variable names and constant intermediary variables rather than relying heavily on comments.
-
We use guard-first logic, handling edge cases, invalid inputs and errors at the start of functions. Returning early keeps the "happy path" at the lowest indentation level, making the main logic easier to follow.
-
We keep if/else blocks small and avoid nesting beyond two levels when possible, as flat structures are easier to read and reason about.
-
We centralize control flow in parent functions, keeping leaf functions as pure logic. This separation makes the codebase more predictable and testable.
-
We access operation properties and attributes via the attribute shortcut
op.someproprather thanop.properties["someprop"]orop.attributes["someprop"], as the shortcut is concise and benefits from static type checking. -
We fail fast by detecting unexpected conditions immediately and raising exceptions rather than corrupting state, as this makes debugging easier.
-
We use truthiness (
if x:) whenever possible, e.g. instead of length checks (len(x) == 0), as__bool__is often O(1). The one exception is Optional values: useis not Noneinstead of truthiness, because many xDSL types define__bool__and truthiness silently skips valid falsy values. -
We follow the Python philosophy of "ask for forgiveness not permission": assume keys and attributes exist and catch exceptions when they don't. For single-value lookups, prefer the walrus operator to avoid a double lookup:
# Good: single lookup if (value := mapping.get(key)) is None: raise MyException() # Good: EAFP, when no sentinel is available try: return mapping[key] except KeyError: return default_value # Bad: LBYL, double lookup if key not in mapping: raise MyException() return mapping[key]
You can also join the discussion at our Zulip chat room, kindly supported by community hosting from Zulip.