doppio
A typed compiler pipeline and CLI for Ledger plain-text accounting — built to be embedded.
doppio parses .ledger files through a four-stage compiler (parse → resolution → elaboration → binary serialization), enforces double-entry balance and balance assertions, and exposes every stage as a first-class Rust library API. Use the dop CLI to query your journals directly, or embed the library to build importers, reporting tools, and accounting applications on a correct, type-safe foundation.
Why doppio?
Most Ledger tooling treats the format as a parsing problem. doppio treats it as a compilation problem: source text goes in, a fully elaborated, validated journal comes out — along with a compact binary (.dop) for fast repeated queries without re-parsing.
For library users: Construct transactions programmatically with a fluent builder API, run them through elaboration to validate balance, and serialize back to Ledger source text or the binary format. The library exposes the full pipeline at each stage (ast, resolution, elaboration) so you work at the right level of abstraction.
For CLI users: Compile once to .dop, then query balance sheets and posting registers in milliseconds. Accepts both raw .ledger files and pre-compiled .dop files interchangeably.
Quick start
CLI:
Library:
use ;
use NaiveDate;
use Decimal;
// Build a transaction programmatically
let txn = new
.with_posting
.with_posting;
// Validate and elaborate it (balance is checked, null posting inferred)
let resolved = eval_transaction?;
// Or compile a full journal from source
let journal = compile?;
for txn in &journal.transactions
CLI reference
compile — pre-process a journal file
dop compile --output my-journal.dop my-journal.ledger
Parses the source file, runs it through the full compilation pipeline, and writes the result as a postcard-serialized, XZ-compressed .dop file. Use this for large journals to avoid re-parsing on every query.
balance — account balances
dop balance my-journal.ledger
dop balance my-journal.dop --depth 2 --begin 2024-01-01 --cleared
dop balance my-journal.dop --pattern "^Expenses" --format json
Prints account balances grouped by commodity. Flags: --depth N (truncate hierarchy), --flat (single-line output), --begin/--end (date range), --cleared (cleared transactions only), --pattern REGEX (filter accounts), --format text|json|csv.
register — posting register
dop register my-journal.ledger
dop register my-journal.dop Expenses --format csv
Lists individual postings with running totals per commodity, optionally filtered to accounts matching a regex pattern. Flags: --format text|json|csv.
print — re-emit canonical Ledger source
dop print my-journal.ledger
Parses and re-emits the journal in canonical Ledger source format — useful for normalizing formatting or verifying round-trip fidelity.
stats — journal summary
dop stats my-journal.ledger
Prints transaction count, account count, commodity count, and date range.
accounts — list account names
dop accounts my-journal.ledger
Lists all account names found in the journal.
Library API
The library exposes four modules corresponding to the pipeline stages, plus top-level entry points:
| Function | Description |
|---|---|
compile(source, parser) |
Full pipeline: source text → elaborated Journal |
eval_transaction(txn, ctx) |
Elaborate a single resolution::Transaction — validate balance, infer null posting, apply aliases |
write_ledger(txns, writer) |
Serialize resolution::Transaction values to canonical Ledger source text |
dop_write_header / dop_read_header |
Portable .dop header I/O with clear version-mismatch errors |
The resolution::Transaction and resolution::Posting builder APIs are the intended construction layer for programmatic use:
new
.with_state
.with_metadata
.with_posting
.with_posting
Full API documentation:
cargo doc --no-deps --open
Pipeline
doppio processes source text through four sequential stages:
.ledger text
│
▼
┌─────────┐
│ parse │ pest PEG grammar → ast::Journal
└─────────┘
│ unresolved dates, aliases, raw ValueExpr amounts
▼
┌────────────┐
│ resolution │ ast::Journal → resolution::HIR
└────────────┘
│ dates normalized, aliases indexed, notes → tags/metadata
▼
┌─────────────┐
│ elaboration │ resolution::HIR → elaboration::Journal
└─────────────┘
│ amounts evaluated, transactions balanced, accounts registered
▼
┌──────────────┐
│ serialization│ postcard + XZ → .dop
└──────────────┘
Stage details
Parse (src/parser.rs, src/ledger.pest): A pest PEG grammar tokenizes the source into an ast::Journal containing transactions, directives, and comments. Amount expressions are kept as unevaluated ValueExpr trees. include directives are resolved recursively here.
Resolution (src/resolution.rs): Converts ast::Journal to a Higher-level Intermediate Representation (HIR). Dates are resolved to NaiveDate (a full year is required). Commodity and account aliases are accumulated into a versioned Context stack so each transaction sees the aliases that were in effect when it was defined. Structured metadata and tags are extracted from freeform notes.
Elaboration (src/elaboration.rs): Converts HIR to the final elaboration::Journal. ValueExpr trees are evaluated to (Decimal, commodity) pairs, commodity aliases are applied, and each transaction is balanced — if exactly one posting has no explicit amount, its value is inferred as the negation of the sum of the rest. Balance assertions (= amount) and balance assignments (=amount) are checked or applied at this stage.
Serialization: The Journal implements serde::Serialize/Deserialize. The compile command writes it through postcard into an XZ-compressed stream; the balance and register commands decompress and deserialize it in the reverse direction.
Build from source
cargo build --release
The resulting binary is target/release/dop.