tilia: A formatter for Haskell source code

[ bsd3, development, formatting, library, program ] [ Propose Tags ] [ Report a vulnerability ]
Versions [RSS] 0.0.1.0
Change log CHANGELOG.md
Dependencies aeson (>=2.1 && <3), base (>=4.14 && <5), base16-bytestring (>=1 && <2), bytestring (>=0.11 && <0.13), Cabal-syntax (>=3.12 && <3.17), choice (>=0.2 && <0.3), containers (>=0.5 && <0.9), cryptohash-sha256 (>=0.11 && <0.12), Diff (>=0.4 && <2), directory (>=1.3 && <1.4), filepath (>=1.4 && <1.6), ghc-lib-parser (>=9.14 && <9.15), optparse-applicative (>=0.14 && <0.20), process (>=1.6 && <1.7), syb (>=0.7 && <0.8), tar (>=0.6 && <0.7), text (>=2.1 && <3), tilia, transformers (>=0.5 && <0.7), zlib (>=0.6 && <0.8) [details]
Tested with ghc ==9.10.3, ghc ==9.12.4, ghc ==9.14.1
License BSD-3-Clause
Author
Maintainer Mark Karpov <markkarpov92@gmail.com>
Uploaded by mrkkrp at 2026-09-16T15:44:16Z
Category Development, Formatting
Home page https://github.com/mrkkrp/tilia
Bug tracker https://github.com/mrkkrp/tilia/issues
Source repo head: git clone https://github.com/mrkkrp/tilia.git
Distributions
Executables tilia
Downloads 0 total (0 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for tilia-0.0.1.0

[back to package description]

Tilia

Tilia is a formatter for Haskell source code. Its primary design choices are:

  • Use ghc-lib-parser for parsing, thus achieving correct parsing at all times.
  • Let single vs multiline layout be influenced by the input.
  • Admit no configuration.
  • Ensure high-quality formatting of comments.
  • Provide first-class support for CPP.
  • Guarantee inference of operator fixity with absolute precision at all times.

Getting started

The two most useful (and only!) commands are inplace and check:

$ tilia inplace [COMPONENT] # format all files of COMPONENT in place
$ tilia check   [COMPONENT] # check that all files of COMPONENT are formatted

COMPONENT may be omitted and in that case it defaults to all. To be precise, the kind of component we are talking about is exactly Cabal's notion of component: libraries, executables, test suites, and benchmarks. For example, in the case of Tilia itself the valid choices are:

  • all
  • tilia, the package, which means every component of it
  • lib:tilia or tilia:lib:tilia
  • exe:tilia or tilia:exe:tilia
  • test:tests or tilia:test:tests, or just tests

It may be surprising that we talk about components rather than individual files. Well, formatting a Haskell module, fortunately or unfortunately, depends on much more than the input text. It depends on things like default-extensions, default-language, and, most importantly, the actual dependencies, because that's where the fixities of the operators you use come from. What all these things have in common is that they are properties of the respective Cabal components your modules belong to. Therefore, it makes sense to consider those components the unit of formatting rather than individual files.

Tilia respects Cabal projects as defined by cabal.project files. It finds the project by starting at the working directory and walking upwards for a cabal.project or a .cabal file. A cabal.project anywhere above wins over a .cabal file that is nearer, so a package inside a multi-package repository resolves to the repository. It is worth pointing out that a package in the tree that the packages field does not name is not part of the project and will not be visited. Within a package, a component's hs-source-dirs say which files belong to it, and every .hs, .hs-boot, and .hsig under them gets formatted.

If there is no build plan yet, or it is older than the .cabal and cabal.project files, or it says nothing about a component you asked for, Tilia has Cabal solve it with cabal build all --dry-run. If the plan is fine but some dependencies have been neither downloaded nor built, it fetches them with cabal build all --only-download. These commands do not build anything, and both are one-time costs, since Cabal's package cache is shared between projects. So do not worry if the first run in a project prints a few lines from Cabal before Tilia starts formatting. Later runs check the plan with a read and a stat per package.

Both of those calls also pass --enable-tests and --enable-benchmarks, because test suites and benchmarks are components Tilia formats, but they are often not enabled by default and that would be confusing. Where a project will not solve with those flags, Tilia settles for what Cabal builds by default, so you get a narrower plan rather than none.

Finally, here are some other flags that may be of interest:

  • --check-ast performs an AST-equivalence check;
  • --check-idempotence performs an idempotence check;
  • --debug-fixity prints information that is useful for debugging formatting of operator chains.

Formatting operator chains

There is nothing you need to know about it or do to make it work. It will just happen, no matter where your operators come from: Hackage, Nix, private repos, or the modules of the project you are formatting.

Formatting CPP

CPP is a first-class formattable object to Tilia. Any Haskell syntactically enclosed in a conditional branch will format, and it does not even need to be self-contained valid Haskell on its own, as long as every configuration of the module is a valid Haskell module.

Development

Enter the development shell by either running direnv allow or nix develop. Once in the shell, the development is ordinary Cabal:

$ cabal build
$ cabal test

All tests are in one test suite and there are a fair number of them. On my machine the full test suite passes in 140 seconds, but it may be different for you, so isolating a subset of the test suite may be helpful:

$ cabal test --test-options='--match "Tilia.Fixity"'

The test suite will perform downloads the first time you run it and so it will be a bit slower on that run. It needs various corpora, such as Hackage packages and GHC's own test suite, which are not checked into this repository.

The Hackage corpus is exercised in order to ensure that every module formats, that its AST is preserved, and that formatting it is idempotent. The results are recorded in corpora/hackage/hackage.manifest. Next to it, hackage.report explains the failing cases. The manifest and the report can be updated like this:

$ TILIA_CORPUS_ACCEPT=1 cabal test

Finally, Tilia formats itself, so make sure to run this command before you open a PR:

$ nix run .#format

Contribution

Issues, bugs, and questions may be reported in the GitHub issue tracker for this project.

Pull requests are also welcome.

License

Copyright © 2026–present Mark Karpov

Distributed under the BSD 3-clause license.