daml-fmt
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 : T → x: 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:
- Format Daml source for CLI usage patterns
- Verify a formatter change for corpus, baseline, and audit commands
- CLI reference for options and exit codes
- Crate reference for features, binaries, and public API
- Formatter verification model for the token/desugar/idempotence safety story
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:
# or from the workspace repo:
# or from a local checkout:
Usage
|
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):
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:
Review-oriented full-corpus audit packets:
See docs/audit-workflow.md for the 25-sample subagent review workflow.
The structural candidate metric (edit candidates over modeled constructs):
Parser round-trip dev tools are also workspace-only and require explicit input paths:
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:
Repo layout
src/lib.rs—format_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 thedev-toolsfeature: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 withtools/gen-expected.sh).corpus/— manifests andSCOREBOARD.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).