Skip to main content

isar-tools

CI Status PyPI conda-forge License

Linting, formatting, and project analysis for Isabelle/Isar. Pure Python: it reads .thy and ROOT files the way isabelle build finds them, without running Isabelle, and answers in seconds.

Terminal recording: isar stats prints session and theory tables for a small demo project, isar check reports an unfinished proof (sorry), isar fmt --diff indents a proof and removes trailing whitespace and extra blank lines, and isar project graph prints the theory import graph.

Why

Isabelle checks that every proof goes through. It has nothing to say about a lemma no proof cites, an import another import already brings in, a constant written out next to the notation the project gave it, a locale assumption no proof uses, or a sorry left in a theory nobody builds. In a development of a few hundred theories these pile up, the build slows down, and reviewers check style by eye. Other languages have formatters, linters, and dead-code finders for this. Large Isabelle projects mostly have conventions in a README.

isar-tools is that missing tooling: a formatter that only touches layout, about fifty checks, statistics, and views of a project's sessions, theories, and declarations. It fits a pre-commit hook or a CI job next to the build.

Status: alpha. Published on PyPI and conda-forge and used by a real project; minor versions may still add commands and JSON columns. See CHANGELOG.md and docs/PLAN.md.

Install

pixi global install isar-tools  # or: conda install -c conda-forge isar-tools
pip install isar-tools          # or: uv tool install isar-tools, pipx install isar-tools
isar --help

Python 3.11 or newer; no runtime dependencies. In a pixi project: pixi add isar-tools.

Commands

Command What it does
isar fmt [PATH...] Format theories: indentation, trailing whitespace, blank lines, and optional line wrapping
isar check [GROUP] [PATH...] Lint ROOT files and theories: proofs, notation, unused and redundant lemmas, and more
isar stats [VIEW] [PATH...] Size, proof, and command statistics
isar stats build BUILD_LOG Where theory elaboration time went in an isabelle build -v log
isar project sessions|theories|graph Sessions, theories, and the session or theory import graph; session layers
isar project hierarchy Class and locale declarations: parents, parameters, assumptions
isar project instances Class instances and locale interpretations, with their class or locale
isar project names Named declarations: qualified name, kind, location, docstring; a Markdown index
isar project extract NAME... The source of a declaration by name; keeps quoted declarations in sync with a manifest
isar project notation TOML The symbols declarations introduce, from their mixfix; keeps a notation table in sync
isar project anchors --browser-info DIR Anchors of a built HTML presentation, HOL included, by name
isar symbols normalize PATH... Rewrite symbols as \<name>, or as Unicode

A path is a project directory, read like isabelle build -D (its ROOT, and ROOTS recursively), or a .thy file. Commands that read a project default to the current directory.

Exit status: 0 success, 1 findings or differences, 2 invalid invocation or unreadable input. Data goes to stdout, diagnostics to stderr. JSON and CSV output use stable snake_case keys and are never coloured.

Checks

isar check                           # groups project, proofs, and syntax
isar check notation unused src/      # name more groups to run them
isar check --ignore oops --format json

A tour, from docs/showcase/Tour.thy:

theory Tour
  imports Base
begin

text \<open>A few findings of \<^verbatim>\<open>isar check\<close>.\<close>

lemma join_comm: "a \<squnion>\<^sub>m b = b \<squnion>\<^sub>m a"
  by (simp add: join_def)

lemma join_comm_again: "x \<squnion>\<^sub>m y = y \<squnion>\<^sub>m x"
  by (simp add:)

lemma join_idem: "join a a = a"
  apply (simp add: join_def)
  done

lemma join_bound: "a \<le> a \<squnion>\<^sub>m b"
  sorry

end
$ isar check proofs notation methods redundant Tour.thy
Tour.thy:10:7: duplicate-lemma: join_comm_again states join_comm (Tour.thy:7) again
Tour.thy:11:12: empty-modifier: add: lists nothing
Tour.thy:13:19: spelled-out-notation: join is written out; its notation is _ \<squnion>\<^sub>m _
Tour.thy:14:3: single-apply: one apply and done: write by
Tour.thy:18:3: unfinished-proof: sorry leaves the goal unproved
Group Codes Runs
project root-syntax, duplicate-session, missing-theory, missing-directory, theory-path, missing-document-file, duplicate-theory-name, unreached-theory by default
proofs unfinished-proof, oops, unclosed-proof by default
syntax lexical-error, document-argument, theory-name, invalid-utf8 by default
symbols non-ascii when named
docs undocumented-theory, undocumented-heading, undocumented-locale, undocumented-class when named
locales locale-free-variable when named
notation spelled-out-notation, spelled-out-abbreviation when named
unused unused-lemma, redundant-import, unused-import, unused-assumption when named
redundant duplicate-lemma, subsumed-lemma when named
hygiene tab, carriage-return, bidi-control, reserved-file-name when named
leftovers proof-search, counterexample-search, goal-reordering, backtracking, diagnostic-command when named
methods empty-modifier, duplicate-fact, single-apply when named
retired retired-identifier when named
prose prose-reference, prose-underscore when named
links broken-link, broken-anchor, anchor-name when named

docs/CHECKS.md explains every group and shows an example of every code, generated from the showcase project. A comment (* isar-ignore: CODE *) silences a finding on its line, or on the next line when it stands alone.

Formatting

isar fmt                          # format every .thy below the current directory
isar fmt --check                  # list files that would change; exit 1 if any
isar fmt --diff Foo.thy           # show the change without writing
isar fmt --max-line-length 100    # also wrap long lines
isar fmt -                        # stdin to stdout, for editors

The formatter only changes layout. It never changes a token, never touches the space between two tokens on a line, and never rewrites strings, cartouches, comments, or ML. By default it only indents lines that are too shallow for their proof structure; --normalize sets indentation exactly. See docs/FORMATTER.md.

As a git hook with lefthook:

pre-commit:
  jobs:
    - name: isar-fmt
      glob: "*.thy"
      run: isar fmt {staged_files}
      stage_fixed: true

Project views

isar project graph --layers          # sessions as strata
isar project names --format markdown > NAMES.md
isar project extract --manifest snippets.toml --out generated/ --check
isar project notation notation.toml --out gen/notation.json --check

docs/PROJECT.md describes session layers, names, quoting declarations, notation tables, anchors of a build, and commands of other sessions (-d).

Statistics

isar stats                               # sessions, then the largest theories
isar stats proofs --top 10               # the longest proofs
isar stats style --max-line-length 100   # long theories, long lines, sorry, watched methods
isar stats commands --by theory --format json   # command counts per theory
isar stats build build.log --budget HOL-Library=0
isar stats build build.log --budget TD=8 --default-budget 0 --project .  # every other library: 0

Configuration

Options a project always wants go in [tool.isar] in pyproject.toml, or in an isar.toml (same keys, at the top level). The file is found by searching upward from the working directory.

[tool.isar]
include = ["$AFP", "vendor/td-verification"]  # like -d; skipped with a note if absent
exclude = ["src/**/generated/**"]             # like --exclude

[tool.isar.fmt]
max-line-length = 100    # also: indent, max-blank-lines, normalize

[tool.isar.check]
groups = ["project", "proofs", "syntax"]      # also: ignore, allow, leaf-sessions
retired-file = "retired_identifiers.txt"      # also: retired = ["name", ...]

[tool.isar.stats]
max-line-length = 100    # also: watch

Paths and globs are relative to the file. Command-line options win; -d and --exclude add to the lists, and --max-line-length 0 turns configured wrapping off. A glob excludes matching files and everything below matching directories, also when a file is named on the command line (as a git hook does).

Limits

  • Nothing runs Isabelle, so nothing is type-checked or proved. A formatted file is guaranteed to contain the same tokens; building it is the project's CI's job.
  • The built-in command table is hand-written, and the AFP table is generated from one AFP revision (scripts/gen_afp_commands.py). A command of a session that is in neither and not passed with -d is read as part of the previous command.
  • project names --derived adds the usual derived facts (f_def, f.simps, P.intros, t.inject, L.intro, ...) and the facts of qualified theory-level interpretations (q.fact), from the interpreted locale's own facts only; facts a locale inherits, and names made by other packages, are not listed.
  • project hierarchy follows declared parents, not sublocale, subclass, or interpretation.
  • stats build reads one log line format, observed in Isabelle2025 logs.

Development

Requires pixi.

pixi run pre-commit-install  # git hooks
pixi run test
pixi run coverage            # 100% line and branch coverage required
pixi run typecheck           # pyright, strict
pixi run lint
pixi run isar --help
pixi run demo                # re-record docs/demo/demo.gif with VHS
pixi run demo-check          # run the demo commands without recording
pixi run checks-doc          # regenerate docs/CHECKS.md and the README's check sections
ISAR_CORPUS=~/afp/thys pixi run corpus   # integration tests over real projects

Expected output lives in golden files under tests/golden/ and tests/formatter/. UPDATE_GOLDEN=1 pixi run test rewrites them, so a change in output shows up as a diff in review.

The demo GIF is generated by VHS from docs/demo/demo.tape, which runs the real commands on the small project in docs/demo/project. The demo task uses its own pixi environment, so VHS, ttyd, and ffmpeg stay out of the default one.

Releasing

  1. Check that formatting cannot break a build: format a real project with the release candidate and build it with Isabelle (for Voblint, its isar fmt round trip workflow or the same steps locally).
  2. In CHANGELOG.md, rename ## Unreleased to ## X.Y.Z (YYYY-MM-DD) and merge.
  3. Tag the merge commit and push the tag (git tag -a vX.Y.Z -m "isar-tools X.Y.Z", git push origin vX.Y.Z). The Build workflow takes the version from the tag and publishes to PyPI; the conda-forge feedstock's bot opens the update.

License

MIT

Metadata

Release files for isar-tools 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for isar-tools 0.6.0
File Size Uploaded
isar_tools-0.6.0.tar.gz 210.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for isar-tools 0.6.0
File Interpreter ABI Platform
isar_tools-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 359.6 kB

Release files / isar_tools-0.6.0.tar.gz

Download URL isar_tools-0.6.0.tar.gz
Size 210.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d97d856ef5279445db9daf672da4a4cc64de910663fa4e5eeaf77b6e355e8a3d
BLAKE2b-256 checksum
How to use checksums
0d6070788f25ecc30c8ad4f90db9a30b9274bc19187b2b564a3a4e2862eac2f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / isar_tools-0.6.0-py3-none-any.whl

Download URL isar_tools-0.6.0-py3-none-any.whl
Size 149.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3967e31f4c3b20e6a50eeffc4e615ffd20f11b2e02a9d74d17d20987f5b1857
BLAKE2b-256 checksum
How to use checksums
a34a6f83881c9ffbe7821f1873afcfa2abc01b695732c2e83c08599272d56032
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page