doppio 0.2.0

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

**A typed compiler pipeline and CLI for [Ledger](https://ledger-cli.org/) 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:**

```sh
cargo install doppio
dop compile --output my-journal.dop my-journal.ledger
dop balance my-journal.dop
dop register my-journal.dop Expenses
```

**Library:**

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

// Build a transaction programmatically
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 it (balance is checked, null posting inferred)
let resolved = doppio::eval_transaction(txn, &Context::default())?;

// Or compile a full journal from source. The opener returns
// Result<String, Box<dyn Error>> so it can surface I/O failures.
let journal = doppio::compile(&source_text, doppio::parser::Parser {
    opener: doppio::file_opener, // built-in glob-aware opener
    base_path: std::path::PathBuf::from("."),
})?;

for txn in &journal.transactions {
    println!("{}: {}", txn.date, txn.description);
}
```

## 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), `--tag KEY` (transactions tagged with `KEY`), `--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: `--begin`/`--end` (date range), `--cleared`, `--tag KEY`, `--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:

```rust
doppio::resolution::Transaction::new(date, "Payee")
    .with_state(doppio::ast::TransactionState::Cleared)
    .with_metadata("import_id", &bank_transaction_id)
    .with_posting(
        doppio::resolution::Posting::new("Assets:Checking")
            .with_amount((amount, "USD"))
    )
    .with_posting(doppio::resolution::Posting::new("Expenses:Food"))
```

Full API documentation:

```
cargo doc --no-deps --open
```

## Supported Ledger features

doppio supports the subset of ledger-cli syntax needed for typical day-to-day
plain-text accounting, including the patterns used by real downstream books.
At a glance:

| Category | Status |
|---|---|
| Transactions, postings, balance assertions/assignments | Supported |
| Directives — `include` (incl. globs), `account`, `commodity`, `alias`, `define` (with parameters), `tag` (with `assert`/`check`), `P` historical price | Supported |
| Expressions — arithmetic, comparisons, regex `=~`/`!~`, `tag()`, parameterised function calls | Supported |
| CLI — `compile`, `balance`, `register`, `print`, `stats`, `accounts`, `commodities`; text / JSON / CSV output | Supported |
| Library API — `compile`, `eval_transaction`, `write_ledger`, `.dop` binary format | Supported |
| Budgets (`~`), automated transactions (`= payee expr`), Lisp-style scripting | Not supported |

See [`docs/SUPPORTED_FEATURES.md`](./docs/SUPPORTED_FEATURES.md) for the full
matrix with notes on partial support and known limitations.

## 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](https://pest.rs/) 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](https://github.com/jamesmunns/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`.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for release notes.