nixfmt_rs 0.1.3

Rust implementation of nixfmt with exact Haskell compatibility
Documentation

nixfmt-rs

crates.io docs.rs license

A drop-in replacement for nixfmt: same binary name, same flags, byte-identical output — just faster and embeddable.

  • Drop-in. Verified byte-for-byte against nixfmt v1.2.0 across all of nixpkgs; swap the binary and nothing in your tree reformats.
  • Fast. Formats the entire nixpkgs checkout in under 2 s — ~130× the Haskell implementation single-threaded (benchmarks).
  • Embeddable. Usable as a Rust library (#![forbid(unsafe_code)], two dependencies) or in the browser via the WebAssembly build.
  • Helpful errors. rustc-style diagnostics with source snippets and fix-it hints (examples).

Install

# Nix
nix run github:Mic92/nixfmt-rs -- --help

# Cargo
cargo install nixfmt_rs

# From source
nix develop -c cargo build --release   # binary at target/release/nixfmt

Prebuilt static binaries are attached to each GitHub release with a SHA256SUMS file and Sigstore-backed provenance:

gh attestation verify ./nixfmt-x86_64-linux -R Mic92/nixfmt-rs

NixOS / home-manager (via flake input):

{
  inputs.nixfmt-rs.url = "github:Mic92/nixfmt-rs";

  outputs = { nixpkgs, nixfmt-rs, ... }: {
    nixosConfigurations.host = nixpkgs.lib.nixosSystem {
      modules = [
        ({ pkgs, ... }: {
          environment.systemPackages = [ nixfmt-rs.packages.${pkgs.system}.default ];
          # or, in home-manager:
          # home.packages = [ nixfmt-rs.packages.${pkgs.system}.default ];
        })
      ];
    };
  };
}

Usage

The binary is named nixfmt and is flag-compatible with upstream.

# stdin → stdout
echo '{a=1;}' | nixfmt

# Format files / directories in place (recurses into *.nix, parallel)
nixfmt path/to/file.nix path/to/dir

# Check only (exit 1 if any file would change)
nixfmt -c path/to/dir

# Layout
nixfmt --width 80 --indent 4 file.nix

# Debugging modes (match `nixfmt --ast` / `nixfmt --ir` exactly)
echo '{a=1;}' | nixfmt --ast
echo '{a=1;}' | nixfmt --ir

treefmt

The binary is a drop-in for nixfmt, so with treefmt-nix just override the package:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    treefmt-nix.url = "github:numtide/treefmt-nix";
    nixfmt-rs.url = "github:Mic92/nixfmt-rs";
  };

  outputs = { nixpkgs, treefmt-nix, nixfmt-rs, ... }:
    let
      forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
    in {
      formatter = forAllSystems (system:
        treefmt-nix.lib.mkWrapper nixpkgs.legacyPackages.${system} {
          programs.nixfmt = {
            enable = true;
            package = nixfmt-rs.packages.${system}.default;
          };
        });
    };
}

Or in a plain treefmt.toml:

[formatter.nixfmt]
command = "nixfmt"      # the nixfmt-rs binary is also called `nixfmt`
includes = ["*.nix"]

Without treefmt at all, point nix fmt straight at the binary (it recurses into directories and formats *.nix in place):

outputs = { nixpkgs, nixfmt-rs, ... }:
  let
    forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
  in {
    formatter = forAllSystems (system: nixfmt-rs.packages.${system}.default);
  };

Editor integration

The binary is named nixfmt and accepts the same flags and stdin/stdout contract as upstream, so any existing nixfmt integration works unchanged once this package is on $PATH (or pointed at explicitly).

VS Code (jnoortheen.nix-ide):

"nix.formatterPath": "nixfmt"

Neovim (conform.nvim):

require("conform").setup({ formatters_by_ft = { nix = { "nixfmt" } } })

Helix (languages.toml):

[[language]]
name = "nix"
formatter = { command = "nixfmt" }

Emacs (apheleia): nixfmt is built in; just ensure the binary resolves to this one.

Anything else: pipe the buffer through nixfmt (reads stdin, writes stdout, exit 1 on parse error).

Error messages

Parse errors come with source snippets, related spans and fix-it hints:

Error[E001]: expected ';', found '='
   ┌─ config.nix:2:27
   │
 1 │ {
 2 │   services.nginx.enable = true
   │                           ^^^^
 3 │   networking.firewall.enable = false;
   = note: missing semicolon after definition
   = help: add a semicolon at the end of the previous line
Error[E002]: unclosed delimiter '{'
   ┌─ config.nix:5:1
   │
 3 │   bar = {
   │         - unclosed delimiter opened here
 4 │     baz = 2;
 5 │ }
   │ ^
   = help: add closing '}'
Error[E005]: commas are not used to separate list elements in Nix
   ┌─ config.nix:1:4
   │
 1 │ [ 1, 2, 3 ]
   │    ^
   = help: use spaces to separate list elements: [1 2 3]

Run cargo run --example error_visualization to see the full catalogue of diagnostics on intentionally broken inputs.

Benchmarks

--check over a full nixpkgs checkout (42 942 .nix files), AMD EPYC 7713P 64-core, nixfmt 1.2.0, treefmt 2.5.0. treefmt runs use --no-cache so every file is actually processed:

command wall time user time vs nixfmt-rs
nixfmt-rs --check . 1.68 s 9.34 s 1.00×
treefmt driving nixfmt-rs 3.35 s 10.14 s 1.99×
nixfmt-tree (treefmt + Haskell) 38.89 s 216.2 s 23.2×
nixfmt --check . (Haskell) 220.67 s 214.4 s 131×

Single large file (all-packages.nix, ~12 k lines): 36.8 ms vs 762.7 ms (20.7×).

Reproduce with scripts/bench.sh; the dev shell provides hyperfine and the script defaults to the nixpkgs revision pinned in flake.lock, so nix develop -c scripts/bench.sh is self-contained.

For parser micro-benchmarks via criterion (the bench feature gates the criterion dependency so cargo test stays lean):

cargo bench --features bench

Library

The formatter is also usable as a library. Disable default features to skip the CLI-only dependencies (ignore, mimalloc):

[dependencies]
nixfmt_rs = { version = "0.1", default-features = false }
let formatted = nixfmt_rs::format("{foo=1;}")?;

let mut opts = nixfmt_rs::Options::default();
opts.width = 80;
let formatted = nixfmt_rs::format_with(src, &opts)?;

On parse failure, render the returned ParseError with source context via nixfmt_rs::format_error. See the API docs.

Design goals

  • Exact behavioural parity. --ast, --ir and formatted output are diffable byte-for-byte against the Haskell implementation, so any divergence can be bisected mechanically.
  • Hand-written recursive-descent parser. No parser-combinator or grammar generator; the structure mirrors Nixfmt/Parser.hs directly, which keeps error messages and trivia handling under our control.
  • Minimal dependencies. The library uses only memchr and compact_str; the binary adds ignore (parallel .nix walking) and mimalloc.

See docs/ARCHITECTURE.md for how the pieces fit together.

Testing

cargo test                       # full suite

# differential check vs. reference `nixfmt` over a nixpkgs checkout
# modes: format | ir | ast; env: NIXPKGS, LIMIT, JOBS, MAX_BYTES, REF, OUT
LIMIT=0 cargo run --release --features sweep --example diff_sweep -- format

The test suite is layered (unit → regression → vendored fixtures → properties); see tests/README.md for where to add new cases.