doppio 2.4.1

A typed compiler pipeline for plain-text Ledger accounting -- parse, resolve, and elaborate .ledger files with a library API built for programmatic use.
Documentation
// ============================================================
// beancount.pest -- PEG grammar for the Beancount journal format.
//
// Conventions from pest (https://pest.rs/):
//   UPPERCASE  -- built-in rules (NEWLINE, EOI, ANY, ASCII_DIGIT, ...)
//   _{ ... }   -- silent rule: matched but not included in the pair tree
//   @{ ... }   -- atomic rule: inner tokens are NOT individually paired
//   ${ ... }   -- compound-atomic: inner named rules ARE paired but
//                 implicit whitespace skipping is disabled
//
// Key differences from ledger.pest / hledger.pest:
//   - Date format is strict ISO `YYYY-MM-DD` (no `/` or `.` separators).
//   - Currencies are bare uppercase identifiers (`USD`, `EUR`, `AAPL`),
//     not `$`-prefixed.
//   - String literals `"..."` are explicit on most directives.
//   - Tags `#tag` and links `^link` appear after a transaction's
//     description.
//   - Comments start with `;` only (Beancount uses `#` for tags).
//   - The directive set is keyword-led after the date:
//     `<date> open|close|commodity|pad|balance|price|note|document
//      |event|query|custom <args...>`.
//   - Transactions are flagged with `*` (complete), `!` (flagged),
//     or the literal `txn`.
//
// NOT YET SUPPORTED:
//   - String escape sequences (`\"`, `\n`, etc) inside quoted strings.
//   - Beancount's lot syntax inside `{...}` is captured as raw text;
//     the AST adapter (#146) is responsible for parsing the inner
//     fields (cost, date, label).
// ============================================================

// --- Top Level ---

journal = ${ (entry | NEWLINE)* ~ EOI }

entry = _{
    transaction
    | open_directive
    | close_directive
    | commodity_directive
    | pad_directive
    | balance_directive
    | price_directive
    | note_directive
    | document_directive
    | event_directive
    | query_directive
    | custom_directive
    | include_directive
    | option_directive
    | plugin_directive
    | pushtag_directive
    | poptag_directive
    | pushmeta_directive
    | popmeta_directive
    | comment_line
    | org_mode_heading
    | shebang_line
    | org_mode_meta
}

// --- Transactions ---
//
// A transaction header is `<date> <flag> [<payee> <description> | <description>]
// [#tag]* [^link]*`, optionally followed by indented metadata and posting lines.

transaction = ${
    txn_header ~ NEWLINE ~
    (metadata_line | posting | empty_indented_line)*
}

txn_header = ${
    date
    ~ ws+ ~ flag
    ~ (ws+ ~ string)?
    ~ (ws+ ~ string)?
    ~ (ws+ ~ tag_or_link)*
    ~ (ws* ~ note)?
}

// Beancount transaction flags. `*` = complete, `!` = flagged for review,
// `txn` = literal keyword form. Single-letter custom flags (P/S/T/C/U/R/M)
// are technically valid but rarely used; not modelled here.
flag = @{ "*" | "!" | "txn" }

tag_or_link = _{ tag | link }
tag  = @{ "#" ~ tag_or_link_chars }
link = @{ "^" ~ tag_or_link_chars }
tag_or_link_chars = @{ (ASCII_ALPHANUMERIC | "_" | "-" | ".")+ }

// --- Postings ---

posting = ${
    indent+ ~ !NEWLINE ~
    (flag ~ ws+)? ~
    posting_account ~
    (amount_logic)? ~
    (ws* ~ note)? ~
    (NEWLINE | EOI) ~
    posting_meta*
}

posting_account = ${ account }

// Two or more spaces (or a tab) separate the account from the amount.
amount_logic = ${
    (ws{2,} | "\t") ~ value_expr ~ (ws* ~ lot_annotation_or_price)*
}

// `{...}` is a Beancount lot specification (cost, date, label).
// `{{...}}` is a total-cost variant. `@`/`@@` are inline price annotations
// (per-unit / total). Try `{{` before `{` and `@@` before `@`.
lot_annotation_or_price = _{ lot_annotation | lot_price }
lot_annotation = ${ ("{{" ~ lot_inner_total ~ "}}") | ("{" ~ lot_inner ~ "}") }
lot_inner       = @{ (!"}"  ~ ANY)* }
lot_inner_total = @{ (!"}}" ~ ANY)* }
lot_price = ${ ("@@" | "@") ~ ws* ~ value_expr }

posting_meta = _{ metadata_line }

// --- Metadata ---
//
// Indented `key: value` lines under a directive or posting. Beancount
// requires the key to be lowercase-led; we enforce that here so a
// posting like `  Assets:Bank:Checking 100 USD` (uppercase-led) is
// not mis-parsed as a metadata line.
metadata_line = ${
    indent+ ~ metadata_key ~ ":" ~ ws* ~ metadata_value ~ (NEWLINE | EOI)
}
metadata_key = @{ (ASCII_ALPHA_LOWER | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }
metadata_value = @{ (!NEWLINE ~ ANY)* }

empty_indented_line = _{ indent+ ~ NEWLINE }

// --- Directives ---
//
// All Beancount directives except `include`/`option`/`plugin` lead with
// the date. Each may carry indented metadata lines under it.

open_directive = ${
    date ~ ws+ ~ "open" ~ ws+ ~ account
    ~ (ws+ ~ commodity_list)?
    ~ (ws+ ~ booking_method)?
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

// Comma-separated currency list on `open Account USD,EUR`.
commodity_list = ${ commodity ~ (ws* ~ "," ~ ws* ~ commodity)* }

// Booking method on `open` directives: `STRICT`, `NONE`, `FIFO`, `LIFO`,
// `AVERAGE`, etc. Quoted because they appear after the currency list.
booking_method = ${ "\"" ~ booking_method_chars ~ "\"" }
booking_method_chars = @{ (!"\"" ~ ANY)* }

close_directive = ${
    date ~ ws+ ~ "close" ~ ws+ ~ account
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

commodity_directive = ${
    date ~ ws+ ~ "commodity" ~ ws+ ~ commodity
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

pad_directive = ${
    date ~ ws+ ~ "pad" ~ ws+ ~ account ~ ws+ ~ account
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

balance_directive = ${
    date ~ ws+ ~ "balance" ~ ws+ ~ account ~ ws+ ~ value_expr
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

price_directive = ${
    date ~ ws+ ~ "price" ~ ws+ ~ commodity ~ ws+ ~ value_expr
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

note_directive = ${
    date ~ ws+ ~ "note" ~ ws+ ~ account ~ ws+ ~ string
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

document_directive = ${
    date ~ ws+ ~ "document" ~ ws+ ~ account ~ ws+ ~ string
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

event_directive = ${
    date ~ ws+ ~ "event" ~ ws+ ~ string ~ ws+ ~ string
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

query_directive = ${
    date ~ ws+ ~ "query" ~ ws+ ~ string ~ ws+ ~ string
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}

// `<date> custom "type-name" <arg> <arg> ...` -- arguments may be strings,
// numbers, accounts, commodities, dates, or booleans.
custom_directive = ${
    date ~ ws+ ~ "custom" ~ ws+ ~ string ~ (ws+ ~ custom_arg)*
    ~ (ws* ~ note)?
    ~ (NEWLINE | EOI)
    ~ metadata_line*
}
custom_arg = _{ string | value_expr | account | commodity | date | bool_literal }
bool_literal = @{ "TRUE" | "FALSE" }

include_directive = ${ "include" ~ ws+ ~ string ~ (NEWLINE | EOI) }
option_directive  = ${ "option"  ~ ws+ ~ string ~ ws+ ~ string ~ (NEWLINE | EOI) }
plugin_directive  = ${
    "plugin" ~ ws+ ~ string ~ (ws+ ~ string)? ~ (NEWLINE | EOI)
}

// --- pushtag / poptag (lexical tag scoping) ---
//
// `pushtag #foo` adds `#foo` to the active set; every subsequent
// transaction parsed before the matching `poptag #foo` inherits it as
// if it were written on the transaction header. `poptag` removes the
// most-recent matching entry from the active set; mismatched pops are
// silently ignored.
pushtag_directive = ${ "pushtag" ~ ws+ ~ tag ~ (NEWLINE | EOI) }
poptag_directive  = ${ "poptag"  ~ ws+ ~ tag ~ (NEWLINE | EOI) }

// --- pushmeta / popmeta (lexical metadata scoping) ---
//
// `pushmeta key: value-expr` adds the key/value pair to the active
// metadata stack; every subsequent transaction inherits it. `popmeta
// key:` removes the most-recent value pushed for that key. The
// trailing colon on `popmeta` matches Beancount's own syntax.
pushmeta_directive = ${
    "pushmeta" ~ ws+ ~ metadata_key ~ ":" ~ ws* ~ metadata_value ~ (NEWLINE | EOI)
}
popmeta_directive = ${
    "popmeta" ~ ws+ ~ metadata_key ~ ":" ~ (NEWLINE | EOI)
}

// --- Atoms ---

date = ${ year ~ "-" ~ monthdate ~ "-" ~ monthdate }
year = @{ ASCII_DIGIT{4} }
monthdate = @{ ASCII_DIGIT{2} }

// String literal: double-quoted form with backslash escape sequences.
// The grammar accepts any byte after a backslash so the inner text
// matches even malformed escapes; the AST adapter is responsible for
// interpreting the recognised sequences (\\, \", \n, \t, \r) and
// passing through unknown escapes verbatim with the leading backslash
// preserved.
string = ${ "\"" ~ string_inner ~ "\"" }
string_inner = @{ (escape_seq | (!"\"" ~ !"\\" ~ ANY))* }
escape_seq = @{ "\\" ~ ANY }

// Account: colon-separated segments. Beancount accepts segments
// starting with an uppercase letter or digit; we accept either case
// to be permissive. Stops at whitespace, semicolon, or newline.
account = @{ account_segment ~ (":" ~ account_segment)+ }
account_segment = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }

indent = _{ " " | "\t" }
ws = _{ " " | "\t" }

note = ${ ";" ~ note_str }
note_str = ${ (!NEWLINE ~ ANY)* }

comment_line = @{ ";" ~ (!NEWLINE ~ ANY)* }

// Org-mode outline headings: `*`, `**`, `***`, ... at column 0,
// followed by free-form heading text. Beancount silently accepts
// these so journals can be edited as Org-mode outlines (and the
// outline structure is preserved for any tool that reads it).
// Single `*` followed by a transaction header would be ambiguous
// only if the next token were a date-like token at column 1; the
// transaction header rule starts with a digit, so a top-level entry
// beginning with `*` cannot be a transaction (transactions are
// `<date> <flag> ...`, not `<flag> <date>`).
org_mode_heading = @{ "*" ~ (!NEWLINE ~ ANY)* }

// Shebang: `#!/usr/bin/env bean-web` etc. Beancount accepts these
// at the top of a file (a journal that is also runnable). We accept
// them anywhere a top-level entry can appear.
shebang_line = @{ "#!" ~ (!NEWLINE ~ ANY)* }

// Org-mode file-level startup directives like `#+STARTUP: showall`
// or `#+TITLE: My Journal`. The file-level analogue of the `*`
// outline headings -- semantically a no-op, useful only when the
// journal is edited in Emacs Org-mode.
org_mode_meta = @{ "#+" ~ (!NEWLINE ~ ANY)* }

identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }

// --- Value expressions ---
//
// Beancount amounts are `<number> <currency>`. Bare numbers and infix
// arithmetic via parentheses are also accepted (Beancount itself
// permits expression-level arithmetic on totals).

value_expr = ${ expr ~ (ws+ ~ commodity)? }
expr = ${ term ~ (ws* ~ infix_op ~ ws* ~ term)* }
term = ${ (prefix_op ~ ws*)? ~ primary }
primary = { base_primary }

base_primary = _{
    "(" ~ expr ~ ")"
    | amount
}

amount = ${
    (number ~ ws+ ~ commodity)
    | number
}

number = @{
    "-"? ~ ASCII_DIGIT+ ~ ("," ~ ASCII_DIGIT{3})* ~ ("." ~ ASCII_DIGIT+)?
    | "-"? ~ "." ~ ASCII_DIGIT+
}

// Beancount currency: must start with an uppercase letter; subsequent
// characters may be uppercase, digits, or any of `'._-`. Length 1+
// (Beancount itself enforces 2..24, but we don't need to here).
commodity = @{ ASCII_ALPHA_UPPER ~ (ASCII_ALPHA_UPPER | ASCII_DIGIT | "'" | "." | "_" | "-")* }

infix_op = _{ add | sub | mul | div }
    add = { "+" }
    sub = { "-" }
    mul = { "*" }
    div = { "/" }
prefix_op = { "-" | "+" }