daml-fmt 0.2.2

A canonical code formatter for the Daml smart-contract language, built on daml-parser
Documentation

daml-fmt

CI License: AGPL v3

A code formatter for Daml (3.x / SDK 3.4.x), differential-tested against a compiler-verified corpus. No official Daml formatter exists; this project builds one and proves it safe with the compiler itself as the oracle.

Part of the daml-tools workspace. The formatter is Rust on the daml-parser crate (lexer → offside-rule layout → recursive-descent AST). It is an AST-driven, own-design layout — it walks the parse tree, reindents the constructs it models, and passes everything else through verbatim. Every change is gated on the laid-out token stream, so it is desugar-safe by construction: a formatted file desugars byte-identically to the original.

Results over the 924-file corpus (corpus/SCOREBOARD.md is the board):

tier result
desugar byte-identical (semantics proven unchanged) 924 / 924
parses 924 / 924
semantics silently changed 0
idempotent (format(format(x)) == format(x)) 924 / 924

What it lays out today: module/import continuations, do-block indentation, if/then/else clauses, case alternatives, let … in bindings, constructor with fields, record-update fields, template/interface bodies, choice internals, declaration ladders, class/instance body-column alignment, function guards/where bindings, try/catch handlers, explicit tuple/list continuations, trailing-whitespace + blank-line/final-newline normalization, and type-annotation colon spacing (x : Tx: T). Broader expression wrapping (long applications, infix chains, lambdas, and inline forms) remains conservative. It makes its own consistent layout decisions and does not aim to match any other formatter's output.

Documentation

The workspace docs split task guides, reference, and design background:

Build & install

daml-fmt depends only on the daml-parser crate (the shared lexer + offside layout + parser), never on daml-lint. Both live in the daml-tools workspace, so a normal workspace checkout has everything it needs.

For JavaScript/TypeScript projects that want daml-fmt as a dev dependency:

npm install --save-dev @daml-tools/daml-fmt
npx daml-fmt --check ./daml
cargo install daml-fmt                       # from crates.io
# or from the workspace repo:
cargo install --git https://github.com/stevennevins/daml-tools daml-fmt
# or from a local checkout:
git clone https://github.com/stevennevins/daml-tools.git
cd daml-tools
cargo build --release --bin daml-fmt         # target/release/daml-fmt
cargo install --path crates/daml-fmt         # puts daml-fmt on your PATH

Usage

daml-fmt Foo.daml                                  # formatted source to stdout
find src -name '*.daml' -exec daml-fmt -w {} +     # rewrite files in place
find src -name '*.daml' -exec daml-fmt --check {} + # list unformatted files
cat Foo.daml | daml-fmt                            # stdin -> stdout

Exit codes: 0 ok, 1 --check found unformatted files, 2 error.

Workspace-Only Tests

These commands require a full repository checkout. The published crate excludes the corpus, baselines, scripts, and contributor notes.

Fast tier (no SDK, ~seconds — formats all 924 corpus files via the release binary, compares to the committed expected/ baseline, checks idempotence):

npm test                  # node test/diff.js, drives target/release/daml-fmt
cargo test                # unit tests for the layout helpers

The real semantic bar is the desugar oracle: the formatted file must desugar byte-identically to the original (daml damlc desugar). The default verifier runs that oracle on a curated subset and keeps full-corpus idempotence:

tools/verify-rust.sh               # needs Daml SDK 3.4.11 on PATH for desugar
tools/verify-rust.sh --desugar     # full-corpus desugar sweep

Review-oriented full-corpus audit packets:

npm run audit                       # writes target/daml-fmt-audit

See docs/audit-workflow.md for the 25-sample subagent review workflow.

The structural candidate metric (edit candidates over modeled constructs):

cargo run --features dev-tools --bin coverage -- original

Parser round-trip dev tools are also workspace-only and require explicit input paths:

cargo run --features dev-tools --bin lossless-check -- original
cargo run --features dev-tools --bin ast-check -- original

Regenerating the Baseline

expected/ is a snapshot of this formatter's own output over original/. After a deliberate formatter change, regenerate it, review the diff, commit:

tools/gen-expected.sh

Repo layout

  • src/lib.rsformat_source (entry) + the token-gated whitespace/colon normalization. src/layout_ast.rs — the AST-driven layout backend.
  • src/bin/daml-fmt.rs — the CLI (the only published binary). Dev-only bins behind the dev-tools feature: coverage (metric), lossless-check / ast-check (parser round-trip checks). The dev-tool bins require explicit input paths because their default corpus is not included in the package.
  • original/ — 924 corpus files from digital-asset/daml (Apache-2.0 upstream; some example files carry no per-file SPDX header), all verified to desugar clean. The formatting test cases.
  • expected/ — the formatter's output over the corpus, the regression baseline (regenerate with tools/gen-expected.sh).
  • corpus/ — manifests and SCOREBOARD.md (the target board).
  • tools/verify-rust.sh (desugar subset + idempotence by default; full desugar with --desugar), gen-expected.sh.
  • test/diff.js — the differential harness (npm test).

CLAUDE.md holds the full project plan, verification commands, and the formatter rules (the desugar oracle outranks everything; comments are sacred; never dedent an indented line to column 0).

License

AGPL-3.0-only. See LICENSE. The vendored original/ corpus keeps its own upstream Apache-2.0 license (from digital-asset/daml).