// ============================================================
// hledger.pest — PEG grammar for the hledger 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:
// - Date separators include `.` and `/` in addition to `-`
// - Comment lines also start with `#` (in addition to `;`)
// - `description` is optional (hledger allows bare `DATE *` lines)
// - `account` directives may have a type comment on the header line
// - `commodity` directive accepts a commodity-format string, not just
// a bare commodity name
// - Automated posting rule bodies may reference `*N` multipliers;
// parsing of the multiplier expression is STUBBED out (see TODO below)
//
// NOT YET SUPPORTED (stubbed / ignored):
// - Automated posting arithmetic bodies (`= query\n account *1.1`):
// the auto_rule rule captures the shape but the posting amounts inside
// automated rules that use `*N` will fail to parse. See TODO(#103).
// - `comment` / `end comment` block comment syntax.
// - Full hledger date inference from a preceding `Y year` directive.
// - Directive-attached tags (metadata on the same line as a directive).
// ============================================================
// --- Top Level ---
journal = ${ (entry | NEWLINE)* ~ EOI }
entry = _{
transaction
| periodic_transaction
| auto_rule
| historical_price
| account_directive
| commodity_directive
| default_directive
| include_directive
| comment_line
}
// --- Transactions ---
// A transaction header followed by optional indented notes and postings.
transaction = ${
header ~ NEWLINE ~
(transaction_note | posting | empty_indented_line)*
}
// hledger header:
// DATE [STATUS] [CODE] [DESCRIPTION] [; COMMENT]
//
// Description is optional in hledger (bare `2024-01-01 *` is valid).
header = ${
date
~ (ws+ ~ status)?
~ (ws+ ~ code)?
~ (ws+ ~ description)?
~ (ws* ~ note)?
}
transaction_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }
empty_indented_line = _{ indent+ ~ NEWLINE }
// --- Periodic transactions ---
//
// `~ PERIOD_EXPR\n POSTINGS…`
// The period expression is the rest of the header line; it is stored as a raw
// string and not elaborated (same as the ledger frontend budget entries).
periodic_transaction = ${
"~" ~ ws+ ~ period_expr ~ NEWLINE ~
(transaction_note | posting | empty_indented_line)*
}
period_expr = @{ (!(NEWLINE | ";") ~ ANY)+ }
// --- Automated posting rules ---
//
// `= QUERY\n POSTINGS…`
//
// TODO(#103): Automated posting amounts may use `*N` multiplier expressions
// that reference the matched posting's amount. Parsing these is not yet
// implemented. For now the rule captures the outer shape; any posting whose
// amount body uses `*N` will produce a parse error with a clear message.
auto_rule = ${
"=" ~ ws+ ~ rule_query ~ NEWLINE ~
(transaction_note | posting | empty_indented_line)*
}
rule_query = @{ (!(NEWLINE | ";") ~ ANY)+ }
// --- Postings ---
posting = ${
indent+ ~ !NEWLINE ~
(status ~ ws+)? ~
posting_account ~
(amount_logic)? ~
(ws* ~ note)? ~
(NEWLINE | EOI) ~
posting_note*
}
posting_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }
// The account portion of a posting: bare name or a virtual-posting marker.
posting_account = ${
virtual_unbalanced_account
| virtual_balanced_account
| account
}
// `(Account)` — virtual unbalanced posting.
virtual_unbalanced_account = ${ "(" ~ virtual_account_inner ~ ")" }
// `[Account]` — virtual balanced posting.
virtual_balanced_account = ${ "[" ~ virtual_account_inner ~ "]" }
// Bare account name inside a virtual posting marker (parens/brackets stripped).
virtual_account_inner = @{ (!(" " | ";" | "\t" | "=" | NEWLINE | ")" | "]") ~ ANY)+ }
// The amount field of a posting.
// Two or more spaces (or a tab) separate the account from the amount —
// identical to the ledger-cli convention so that account names containing a
// single space are parsed correctly.
amount_logic = ${
(ws{2,} | "\t") ~ (
value_logic
| assertion
)
}
// A value expression, optionally followed by lot annotations and/or lot pricing
// and/or a balance assertion check. Mirrors the ledger-cli grammar.
value_logic = ${ value_expr ~ (ws* ~ lot_annotation_or_price)* ~ (ws* ~ assertion)? }
lot_annotation_or_price = ${ lot_annotation | lot_price }
lot_annotation = ${ lot_cost | lot_date | lot_note }
// Double-brace `{{expr}}` means total cost; single-brace `{expr}` means per-unit
// cost. Try `{{` before `{` so the longer token wins.
lot_cost = ${ ("{{" ~ ws* ~ value_expr ~ ws* ~ "}}") | ("{" ~ ws* ~ value_expr ~ ws* ~ "}") }
// `[date]` — lot acquisition date.
lot_date = ${ "[" ~ date ~ "]" }
// `((note))` — free-form lot note.
lot_note = ${ "((" ~ lot_note_inner ~ "))" }
lot_note_inner = @{ (!"))" ~ ANY)* }
// hledger uses `==` for "all-commodity" (strict) balance assertions and `=`
// for single-commodity assertions. The longer token must be tried first.
assertion = ${ assertion_op ~ ws* ~ value_expr }
assertion_op = @{ "==" | "=" }
// --- Atoms ---
// hledger accepts YYYY-MM-DD, YYYY/MM/DD, and YYYY.MM.DD.
date = ${ year ~ date_sep ~ monthdate ~ date_sep ~ monthdate }
year = @{ ASCII_DIGIT{4} }
monthdate = @{ ASCII_DIGIT{1,2} }
date_sep = _{ "-" | "/" | "." }
// Cleared (*) or pending (!) state marker.
status = { "*" | "!" }
// An optional reference code in parentheses, e.g. "(INV-042)".
code = { "(" ~ (!")" ~ ANY)* ~ ")" }
// Everything after the header fields up to a semicolon or newline.
// hledger trims trailing whitespace from the description at display time.
description = @{ (!(";" | NEWLINE) ~ ANY)+ }
// An account name: stops at double-space, semicolon, tab, equals, or newline.
account = @{ (!(" " | ";" | "\t" | "=" | NEWLINE) ~ ANY)+ }
// Lot pricing: `@ unit_price` or `@@ total_price`.
lot_price = ${ ("@@" | "@") ~ ws* ~ value_expr }
indent = _{ " " | "\t" }
ws = _{ " " | "\t" }
// hledger comments start with `;` or `#`.
note = ${ ";" ~ note_str }
note_str = ${ (!NEWLINE ~ ANY)* }
comment_line = @{ (";" | "#") ~ (!NEWLINE ~ ANY)* }
// --- Historical prices ---
historical_price = ${
"P" ~ ws+ ~ date ~ ws+ ~ commodity ~ ws+ ~ value_expr ~ (NEWLINE | EOI)
}
// --- Account directive ---
//
// hledger's account directive optionally has indented sub-directives
// (note, alias, type, …) which are treated as key/value account items,
// mirroring the structure of the ledger-cli account block.
account_directive = ${
"account" ~ ws+ ~ account ~ (ws* ~ note)? ~ NEWLINE ~
((indent+ ~ note ~ NEWLINE) | account_item)*
}
account_item = ${
indent+ ~ account_key ~ (ws+ ~ account_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}
account_key = @{ "note" | "alias" | "type" | identifier }
account_val = @{ (!NEWLINE ~ ANY)* }
// --- Commodity directive ---
//
// hledger accepts:
// commodity SYMBOL (bare symbol)
// commodity FORMAT_STRING (e.g. `commodity $1,000.00`)
//
// Both forms are captured as a raw format string; parsing the format is done
// in the Rust layer by inspecting the string.
commodity_directive = ${
"commodity" ~ ws+ ~ commodity_format ~ (ws* ~ note)? ~ (NEWLINE | EOI) ~
((indent+ ~ note ~ NEWLINE) | commodity_item)*
}
// A commodity format string is the rest of the line up to a comment.
commodity_format = @{ (!(";" | NEWLINE) ~ ANY)+ }
commodity_item = ${
indent+ ~ commodity_key ~ (ws+ ~ commodity_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}
commodity_key = @{ "alias" | "format" | "nomarket" | "default" | "note" | identifier }
commodity_val = @{ (!NEWLINE ~ ANY)* }
// --- Default commodity directive ---
//
// hledger supports the same `D <amount>` form as ledger-cli:
// D $1,000.00
// This declares the default commodity and its display format simultaneously.
// It is lowered to the same `Directive::Commodity` representation as a
// full `commodity` block with `default` and `format` sub-directives.
default_directive = ${ "D" ~ ws+ ~ value_expr ~ (NEWLINE | EOI) }
// --- Include directive ---
include_directive = ${ "include" ~ ws+ ~ filename ~ (NEWLINE | EOI) }
filename = @{ (!NEWLINE ~ ANY)+ }
// --- Value expressions ---
//
// Identical in shape to ledger.pest. hledger's posting-amount syntax is a
// subset of ledger-cli's (no `define`, no boolean-expressions in directives),
// so the same Pratt-parsed expression grammar is fully sufficient.
value_expr = ${ expr ~ (ws+ ~ commodity)? }
expr = ${ term ~ (ws* ~ infix_op ~ ws* ~ term)* }
term = ${ (prefix_op ~ ws*)? ~ primary }
primary = { base_primary }
base_primary = _{
"(" ~ expr ~ ")"
| amount
| commodity
}
amount = ${
(commodity ~ ws* ~ number)
| (number ~ ws+ ~ commodity)
| number
}
number = @{
ASCII_DIGIT+ ~ ("," ~ ASCII_DIGIT{3})* ~ ("." ~ ASCII_DIGIT+)?
| "." ~ ASCII_DIGIT+
}
commodity = @{
symbol+
| (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "." | ":" | "#")*
}
symbol = _{ "$" | "€" | "£" | "¥" }
// A plain identifier: letter or underscore, then alphanumeric or underscore.
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
infix_op = _{ add | sub | mul | div }
add = { "+" }
sub = { "-" }
mul = { "*" }
div = { "/" }
prefix_op = { "-" | "+" }