#ledger #finance-ledger #compiler

doppio

A typed compiler pipeline for plain-text Ledger accounting -- parse, resolve, and elaborate .ledger files with a library API built for programmatic use

7 releases (stable)

Uses new Rust 2024

new 2.3.0 May 11, 2026
2.2.0 May 9, 2026
1.0.0 May 6, 2026
0.2.0 Apr 27, 2026
0.1.0 Apr 23, 2026

#36 in Finance


Used in 2 crates

Custom license

1MB
16K SLoC

doppio

A typed compiler pipeline for Ledger plain-text accounting -- built to be embedded.

This is the library crate. For the dop command-line interface, install doppio-cli instead; it depends on this crate and ships the binary.

What it does

doppio parses .ledger (and .hledger, .journal) source files through four sequential stages:

source text -> parse -> resolution -> elaboration -> .dop

It enforces double-entry balance and balance-assertion semantics at elaboration time, then exposes the result as a strongly-typed elaboration::Journal (a prost-generated Protocol Buffers message) ready for queries, reporting, or serialisation to the .dop binary format.

Use cases

  • Bank import pipelines. Build resolution::Transaction values programmatically from imported records (Mercury, Plaid, SimpleFIN, etc.) and run them through eval_transaction to validate balance before writing back to source.
  • Reporting tools. Compile a journal once with compile, then serialise to a .dop file with write_dop and load it in milliseconds with read_dop for repeated queries.
  • Cross-language consumers. The .dop binary format is defined by proto/doppio.proto in the source repo. Any language with a Protocol Buffers binding can read .dop files directly without re-parsing the source.

Quick example

use doppio::resolution::{Context, Transaction, Posting};
use chrono::NaiveDate;
use rust_decimal::Decimal;

let txn = Transaction::new(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(), "Groceries")
    .with_posting(Posting::new("Expenses:Food").with_amount((Decimal::from(50u32), "$")))
    .with_posting(Posting::new("Assets:Checking"));

// Validate and elaborate -- the null posting on Assets:Checking is inferred as -$50.
let journal_txn = doppio::eval_transaction(txn, &Context::default())?;

Public API surface

The intended construction layer is resolution::Transaction / resolution::Posting (builder API). The intended read layer is elaboration::* (the prost-generated proto types) plus their inherent ergonomic methods (Decimal::to_decimal(), Amount::iter(), Posting::amount_in(commodity), etc.).

The Frontend trait pluralises the parser dispatch -- LedgerFrontend and HledgerFrontend are shipped; new formats (e.g. Beancount) can implement the trait without modifying the library.

Using doppio in tests

When writing tests that consume elaboration::Journal directly, use the doppio::testing module to build fixtures without wrestling with proto3 quirks (Option<Amount>, epoch-days dates, state: i32 enum casts). Enable it via the testing feature in your dev-dependencies:

[dev-dependencies]
doppio = { version = "...", features = ["testing"] }

Then construct journals with the fluent builder:

use doppio::testing::{journal, txn, posting};
use chrono::NaiveDate;

let j = journal()
    .with_txn(txn(NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(), "Groceries")
        .with_posting(posting("Expenses:Food").with_amount(50, "$"))
        .with_posting(posting("Assets:Checking").with_amount(-50, "$")))
    .build();
assert_eq!(j.transactions.len(), 1);
assert_eq!(j.transactions[0].postings.len(), 2);

The testing feature is intentionally excluded from default and production builds so it does not bloat binary consumers.

Companion crates

  • doppio-cli -- the dop command-line interface (compile, balance, register, print, stats, accounts, commodities, with --exchange, --cleared, --depth, --format text|json|csv, etc.).
  • doppio-categorize -- counter-account suggestions for new transactions, given an existing journal as the training corpus.

Documentation

  • Repository -- README, CLI reference, supported feature matrix.
  • docs/ -- proto evolution policy, exchange-rate semantics, supported features.
  • proto/doppio.proto -- the canonical wire format with multilingual reassembly recipes.

License

ISC. See LICENSE.

Dependencies

~6–13MB
~165K SLoC