Skip to content

Latest commit

 

History

History
278 lines (210 loc) · 9.19 KB

File metadata and controls

278 lines (210 loc) · 9.19 KB

Contributing to xDSL

This document describes the steps needed to clone, install, and test the repository, as well as a description of our coding and contributing conventions.

Contents

Developer Installation

We use uv for dependency management of xDSL. See uv's getting started page for more details.

# Ensure uv is installed
uv --version

Then, 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 tests

Please 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 to mlir-opt
  • XDSL_MLIR_TRANSLATE — path to mlir-translate
  • XDSL_LLVM_DIFF — path to llvm-diff
  • XDSL_LLI - path to lli

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).

Alternative installations

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.

Testing and benchmarking

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).

Unit Tests

Python tests in tests/ (excluding tests/filecheck) for testing APIs and logic:

# Run unit tests
uv run pytest
# or via makefile
make pytest

Lit/FileCheck Tests

File-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 filecheck

Note 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.

Bootstrapping and Updating FileCheck Tests with filecheckize

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 filecheckize

This will print the following:

// CHECK:       %c1 = arith.constant 1 : index
// CHECK-NEXT:  %res = arith.addi %c1, %c1 : index

filecheckize 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.

Coverage Tests

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-html

You 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-tests

Benchmarks

Benchmarks 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.

Formatting and Typechecking

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 pyright

Important

Experimental Pyright Features

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 = true

Pre-commit Hooks

To 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 precommit

Code Style

We 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.someprop rather than op.properties["someprop"] or op.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: use is not None instead 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]

Discussion

You can also join the discussion at our Zulip chat room, kindly supported by community hosting from Zulip.