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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
// ============================================================
// ledger.pest — PEG grammar for the Ledger plain-text accounting format
//
// Parsing is handled by pest (https://pest.rs/). This file defines the
// *structure* of the source language. Operator precedence for value
// expressions is NOT encoded here — it is applied by a Pratt parser in
// parser.rs after the grammar has produced a flat token stream.
//
// Rule naming conventions (pest):
//   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
// ============================================================

// --- Top Level ---

// The entire file is a sequence of entries separated by blank lines.
// A compound-atomic ($) root rule is used so that leading/trailing
// whitespace is preserved exactly as written in the source.
journal = ${ (entry | NEWLINE)* ~ EOI }

// An entry is one of: transaction, directive, comment, or budget.
// Silent (_) so the match is transparent — callers see the inner rule.
entry = _{
    assertion_directive
    | transaction
    | directive
    | historical_price
    | c_directive
    | comment_line
    | budget
    | auto_rule
    | apply_tag_directive
    | end_tag_directive
}

// --- Transactions ---

// A transaction is a header line followed by zero or more postings/notes.
// The compound-atomic modifier ($) is critical: it prevents the grammar
// from silently swallowing the indentation that separates postings.
transaction = ${
    header ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}

// Budget entries (e.g. "~ monthly ...") share the same posting structure
// but are not yet elaborated by the compiler.
budget = ${
    "~" ~ ws+ ~ ("yearly" | "monthly" | "weekly") ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}

// Automated posting rules ("= QUERY\n  POSTINGS…"). The query is a
// regex / value-expression that ledger-cli matches against subsequent
// transactions; matched postings synthesise additional postings from
// the rule body. See elaborator.rs for the application semantics (#249).
//
// Multiplier semantics: a body posting amount that is a bare commodity-less
// number (e.g. `0.10`) is interpreted as a multiplier of the matched
// posting's amount in its commodity. The explicit `*N` prefix form
// (e.g. `*-1`, `* 0.5`) is also accepted and lowers to the same bare-number
// multiplier representation — no new semantic path is required in the
// elaborator. Body postings with an explicit commodity symbol (e.g. `$10.00`)
// are taken as literal amounts. See `auto_multiplier` below and #254.
auto_rule = ${
    "=" ~ ws+ ~ rule_query ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}
rule_query = @{ (!(NEWLINE | ";") ~ ANY)+ }

// `apply tag <key>[: <value>]` opens a tag scope; `end tag` (or
// `end apply tag`) closes it. ledger-cli appends the active tag
// stack to every transaction declared between the two markers
// (#222). The Parser threads the stack through recursive include
// resolution; transaction parsing reads it and appends a note per
// active tag.
apply_tag_directive = ${ "apply" ~ ws+ ~ "tag" ~ ws+ ~ apply_tag_body ~ (NEWLINE | EOI) }
apply_tag_body = @{ (!NEWLINE ~ ANY)+ }
end_tag_directive = ${ "end" ~ ws+ ~ ("apply" ~ ws+)? ~ "tag" ~ ws* ~ (NEWLINE | EOI) }

// The header line: date, optional secondary date, state, code, description,
// and an optional trailing note.
header = ${
    date ~ (ws* ~ "=" ~ ws* ~ date)?
    ~ (ws+ ~ state)?
    ~ (ws+ ~ code)?
    ~ (ws+ ~ description)
    ~ (ws* ~ note)?
}

// A note line that is part of the transaction header (indented, starts with ;)
transaction_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }

// A blank indented line allowed between postings — consumed silently.
empty_indented_line = _{ indent+ ~ NEWLINE }

// --- Postings ---

// A posting is an indented account name, optionally followed by an amount
// and/or a trailing note, plus zero or more indented note lines.
//
// The !NEWLINE lookahead prevents a truly empty indented line from being
// matched as a posting with an empty account name.
//
// `ws*` immediately before `(NEWLINE | EOI)` tolerates trailing whitespace
// after the last meaningful token on the line. Real-world journals
// (e.g. ledger-cli's own `test/input/standard.dat`) pad account names to
// a fixed column with trailing spaces, including on null postings; the
// whitespace before EOL is parser-meaningless. See #247.
posting = ${
    indent+ ~ !NEWLINE ~ (state ~ ws+)? ~ posting_account ~ (amount_logic)? ~ (ws* ~ note)? ~ ws* ~ (NEWLINE | EOI) ~
    posting_note*
}

// The account portion of a posting, which may be a bare account name or a
// virtual-posting marker wrapping an account name.
posting_account = ${
    virtual_unbalanced_account
    | virtual_balanced_account
    | account
}

// `(Account)` — virtual unbalanced: the posting is excluded from the
// transaction's balance check. The parens are part of the syntax, not the
// account name; the inner name is captured by `virtual_account_inner`.
virtual_unbalanced_account = ${ "(" ~ virtual_account_inner ~ ")" }

// `[Account]` — virtual balanced: the posting participates in the balance
// check but is flagged so reports can include/exclude it via `--real`.
virtual_balanced_account = ${ "[" ~ virtual_account_inner ~ "]" }

// The bare account name inside a virtual posting marker. The exclusion set
// matches `account` but also stops at `)` and `]` so the closing marker is
// never consumed into the name.
virtual_account_inner = @{ (!("  " | ";" | "\t" | "=" | NEWLINE | ")" | "]") ~ ANY)+ }

// A note line indented beneath a posting.
posting_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }

// The amount field of a posting.
//
// Two or more spaces (or a tab) are REQUIRED before the amount. This is the
// canonical Ledger convention: it unambiguously separates the account name
// from the amount, since account names may contain single spaces (e.g.
// "Equity:Opening Balances").
//
// `auto_multiplier` is tried first so that the `*` prefix in automated-rule
// body postings (e.g. `*-1`, `* 0.5`) is consumed before `value_logic`'s
// expression parser (which also handles infix `*`). Outside auto-rule bodies
// this is harmless: regular postings don't start with `*` anyway.
amount_logic = ${
    (ws{2,} | "\t") ~ (
        auto_multiplier
        | value_logic
        | assertion
    )
}

// Explicit `*N` multiplier form used in automated-rule body postings.
// Matches `*<number>` or `* <number>` with an optional leading sign on the
// number (e.g. `*-1`, `*0.5`, `* 0.12`). The `*` is consumed here and the
// numeric part is lowered to the same bare-number `ValueExpr::Amount` as a
// plain bare decimal — the elaborator's `is_bare_number_expr` treats both
// forms identically (#254).
auto_multiplier = ${ "*" ~ ws* ~ (prefix_op ~ ws*)? ~ number }

// A value expression, optionally followed by lot annotations and/or lot pricing
// and/or a balance assertion check.
//
// Lot annotations may appear in any order between the value and the optional
// `@`/`@@` price. Multiple annotations of different kinds are allowed;
// duplicate kinds take the last value (matching ledger-cli behaviour).
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. Only valid after a posting amount, so there
// is no ambiguity with `[Account]` virtual balanced postings (those appear in
// account position, not after an amount).
lot_date = ${ "[" ~ date ~ "]" }
// `((note))` — free-form lot note. Try `((` before `(` to avoid partial matches.
lot_note = ${ "((" ~ lot_note_inner ~ "))" }
lot_note_inner = @{ (!"))" ~ ANY)* }

// A balance check or balance assignment: `= target` or `== target`.
// Used in two ways within `amount_logic`:
//   - After `value_logic`: a post-posting balance assertion (becomes `balance_assertion`
//     in `AmountDetails::Amount`).
//   - Standalone (no preceding amount): parsed by the parser as
//     `AmountDetails::BalanceAssignment`; the posting amount is inferred to bring
//     the account to the target balance.
assertion = ${ "="{1, 2} ~ ws* ~ value_expr }

// --- Atoms ---

// A full date: YYYY/MM/DD or YYYY-MM-DD. The four-digit year is mandatory.
date = ${ year ~ ("/" | "-") ~ monthdate ~ ("/" | "-") ~ monthdate }
year = @{ ASCII_DIGIT{4} }
monthdate = @{ ASCII_DIGIT{1,2} }

// Cleared (*) or pending (!) state marker.
state = { "*" | "!" }

// An optional reference code in parentheses, e.g. "(INV-042)".
code = { "(" ~ (!")" ~ ANY)* ~ ")" }

// Everything after the header fields up to a semicolon or newline.
description = { (!(";" | NEWLINE) ~ ANY)* }

// An account name: any characters that are not a double-space, semicolon,
// tab, equals sign, or newline. The double-space exclusion is what
// makes the "two-space rule" for amounts work — the account name stops
// as soon as two consecutive spaces are encountered.
account = @{ (!("  " | ";" | "\t" | "=" | NEWLINE) ~ ANY)+ }

// Lot pricing: "@ unit_price" or "@@ total_price".
lot_price = ${ ("@@" | "@") ~ ws* ~ value_expr }

// Whitespace helpers (silent — not included in the pair tree).
indent = _{ " " | "\t" }
ws = _{ " " | "\t" }

// A semicolon-prefixed note. The note_str captures everything up to EOL.
note = ${ ";" ~ note_str }
note_str = ${ (!NEWLINE ~ ANY)* }

// A full-line comment. Ledger accepts several comment characters.
comment_line = @{ (";" | "#" | "*" | "%" | "|") ~ (!NEWLINE ~ ANY)* }

// --- Historical Prices ---

// A price directive: "P date [HH:MM[:SS]] commodity price".
// Records the market price of a commodity at a point in time.
// The time component is optional; when absent the price applies to the whole day.
historical_price = ${ "P" ~ ws+ ~ date ~ ws+ ~ (time ~ ws+)? ~ commodity ~ ws+ ~ value_expr ~ (NEWLINE | EOI) }

// --- Commodity Conversion Directive ---

// `C N1 X = N2 Y` — declares a fixed exchange rate between two commodities.
// The LHS commodity (X) is canonical; the RHS (Y) is the alias.  Amounts in Y
// are divided by (N1 * N2) to obtain their X-equivalent.  Each amount is a
// concrete `<number><commodity>` pair — no arithmetic in the C directive.
//
// Example:  C 1.00s = 100c   (1 silver = 100 copper; canonical is s)
//           C 1.00G = 100s   (1 gold = 100 silver;  canonical is G)
//
// ledger-cli only — hledger has no equivalent directive at the grammar level.
c_directive = ${
    "C" ~ ws+ ~ c_amount ~ ws* ~ "=" ~ ws* ~ c_amount ~ (NEWLINE | EOI)
}

// A bare concrete amount inside a `C` directive: digits (with optional decimal
// point), immediately followed by the commodity symbol (no space allowed).
// Both `100c` and `1.00s` are valid; symbol-first like `$1` is NOT used by
// ledger-cli's C directive in practice.
c_amount = ${ c_number ~ ws* ~ commodity }
c_number = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)? }

// An optional wall-clock time: HH:MM or HH:MM:SS.
time = ${ ASCII_DIGIT{2} ~ ":" ~ ASCII_DIGIT{2} ~ (":" ~ ASCII_DIGIT{2})? }

// --- Standalone Balance Assertion Directives ---

// A standalone balance assertion: `date = account  amount` (weak) or
// `date == account  amount` (strict). The `=`/`==` marker takes the place
// of the cleared-state field that appears in transaction headers.
//
// Two or more spaces (or a tab) separate the account from the amount,
// matching the same convention used for posting amounts.
assertion_directive = ${
    date ~ ws+ ~ assertion_op ~ ws+ ~ account ~ (ws{2,} | "\t") ~ value_expr ~ (NEWLINE | EOI)
}

// `==` must be tried before `=` so that the longer token wins.
assertion_op = @{ "==" | "=" }

// --- Directives ---

// The optional leading "!" is allowed by some Ledger implementations.
// define and tag directives are parsed but not yet elaborated.
directive = _{ "!"? ~ (commodity_directive | account_directive | alias_directive | include_directive | define_directive | tag_directive | default_directive) }

// A bare `D` directive: declares the default commodity AND its display format
// in a single line. `D $1000.00` is equivalent to a `commodity $` block with
// `default` and `format $1,000.00` sub-directives. The amount text supplies
// both the commodity symbol and the format string.
default_directive = ${ "D" ~ ws+ ~ value_expr ~ (NEWLINE | EOI) }

tag_directive = ${
    "tag" ~ ws+ ~ identifier ~ (ws* ~ note)? ~ NEWLINE ~
    ((indent+ ~ note ~ NEWLINE) | tag_assert | tag_check)*
}

// assert / check sub-directives for tag blocks. Separate rules from
// account_assert / account_check so parent context can be determined
// unambiguously by the parser, even though the shape is identical.
tag_assert = ${ indent+ ~ "assert" ~ ws+ ~ bool_expr ~ (NEWLINE | EOI) }
tag_check  = ${ indent+ ~ "check"  ~ ws+ ~ bool_expr ~ (NEWLINE | EOI) }

// A commodity block: declares properties such as aliases, display format,
// and whether market prices are tracked.
commodity_directive = ${
    "commodity" ~ ws+ ~ commodity ~ (ws* ~ note)? ~ NEWLINE ~
    ((indent+ ~ note ~ NEWLINE) | commodity_item)*
}

commodity_item = ${
    indent+ ~ commodity_key ~ (ws+ ~ commodity_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}

// Known commodity keys are enumerated first; anything else is "identifier".
commodity_key = @{ "alias" | "format" | "nomarket" | "default" | identifier }
commodity_val = @{ (!NEWLINE ~ ANY)* }

// An account block: declares an account and its properties (alias, note).
// account_assert / account_check are tried before account_item so the
// "assert" / "check" keywords are not swallowed as unknown keys.
account_directive = ${
    "account" ~ ws+ ~ account ~ (ws* ~ note)? ~ NEWLINE ~
    ((indent+ ~ note ~ NEWLINE) | account_assert | account_check | account_item)*
}

// assert / check take a full boolean expression rather than a raw string.
account_assert = ${ indent+ ~ "assert" ~ ws+ ~ bool_expr ~ (NEWLINE | EOI) }
account_check  = ${ indent+ ~ "check"  ~ ws+ ~ bool_expr ~ (NEWLINE | EOI) }

account_item = ${
    indent+ ~ account_key ~ (ws+ ~ account_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}
account_key = @{ "note" | "alias" | identifier }
account_val = @{ (!NEWLINE ~ ANY)* }

// A top-level "alias short = long" shorthand for account names.
alias_directive = ${ "alias" ~ ws+ ~ account ~ ws* ~ "=" ~ ws* ~ account ~ (ws+ ~ note)? ~ (NEWLINE | EOI) }

// A define directive: "define name[(p1, p2, ...)] = body"
// Associates a name with a value or boolean expression, optionally parameterized.
// The body is tried as a bool_expr first (to support comparisons and logical chains),
// falling back to value_expr for plain arithmetic/amount aliases.
define_directive = ${
    "define" ~ ws+ ~ identifier ~
    (ws* ~ "(" ~ ws* ~ identifier ~ (ws* ~ "," ~ ws* ~ identifier)* ~ ws* ~ ")")? ~
    ws* ~ "=" ~ ws* ~ define_body ~ (NEWLINE | EOI)
}

// A define body is either a full boolean expression (supports comparisons and
// logical chains) or a plain value expression. bool_expr is tried first because
// it is strictly more expressive; value_expr handles plain amounts and arithmetic.
define_body = ${ bool_expr | value_expr }

// Include another file by path (glob patterns are resolved in parser.rs).
include_directive = ${ "include" ~ ws+ ~ filename ~ (NEWLINE | EOI) }
filename = @{ (!NEWLINE ~ ANY)+ }

// --- Expressions (Explicit Whitespace) ---
//
// These rules define the *syntax* of value expressions. Operator PRECEDENCE
// is NOT handled here — the grammar produces a flat left-to-right token
// stream that is then processed by a Pratt parser in parser.rs, which
// applies the correct precedence (* and / bind tighter than + and -).

// A boolean expression used in account assert/check directives.
// Grammar: value_expr (cmp_op value_expr)? (bool_op bool_expr)?
// This is kept intentionally simple: no grouping, left-to-right, since
// the Pratt parser does not yet handle boolean / comparison operators.
// TODO(#74-followup): wire cmp_op/bool_op into the Pratt parser so that
// complex expressions like "a > 0 and b < 10" parse correctly under full
// precedence rules.
// `cmp_op` separators are punctuation so they take ws*; `bool_op` is alphabetic
// (`and`/`or`) and must be space-separated to avoid `xand` parsing as `x and`.
// `bool_expr` supports both ordinary comparisons (cmp_op) and regex matches
// (regex_cmp_op). The RHS of a regex comparison must be a regex_literal;
// ordinary comparisons accept any value_expr.
bool_expr = ${
    value_expr ~ (
        (ws* ~ regex_cmp_op ~ ws* ~ regex_literal)
        | (ws* ~ cmp_op ~ ws* ~ value_expr)
    )? ~ (ws+ ~ bool_op ~ ws+ ~ bool_expr)?
}
cmp_op = @{ "==" | "!=" | "<=" | ">=" | "<" | ">" }
// Regex match operators: must be tried before cmp_op to avoid `=` prefix ambiguity.
regex_cmp_op = @{ "=~" | "!~" }
bool_op = @{ "and" | "or" }

// A regex literal: /pattern/ where \/ is an escaped slash inside the pattern.
// compound-atomic ($) so that regex_body is named and visible to the parser.
regex_literal = ${ "/" ~ regex_body ~ "/" }
// regex_body captures the raw pattern text between the delimiters.
// Allows backslash-escapes (including \/) and any non-slash character.
regex_body = @{ (("\\" ~ ANY) | (!"/" ~ ANY))* }

// A value_expr is an expression optionally followed by a commodity annotation.
// The trailing commodity form "(1 + 2) USD" creates a ValueExpr::Typed node.
// The negative lookahead `!bool_op` prevents `and`/`or` from being consumed as
// a commodity, which would silently drop the boolean chain in `bool_expr`.
value_expr = ${ expr ~ (ws+ ~ !bool_op ~ commodity)? }

// An expression is one or more terms joined by infix operators.
expr = ${ term ~ (ws* ~ infix_op ~ ws* ~ term)* }

// A term is an optional prefix operator followed by a primary atom.
term = ${ (prefix_op ~ ws*)? ~ primary }

// A primary is a base atom optionally followed by dot-access chains.
// base_primary is silent, so the parser sees the child rule directly.
primary = { base_primary ~ access* }

// Ordering note: `amount` is tried before `string` so that commodity-first
// quoted amounts like `"Long Name" 5` are consumed by the `amount` rule
// (commodity ~ ws* ~ number) rather than by `string`.  A quoted string that
// is *not* followed by a number still falls through to `string` because the
// `amount` alternatives all require a numeric component.  `function_call` is
// first because it starts with an identifier, not a quote.
base_primary = _{
    function_call
    | amount
    | string
    | "(" ~ expr ~ ")"
    | "(" ~ bool_expr ~ ")"
    | commodity
}

// A dot-accessor: ".fieldname"
access = ${ "." ~ identifier }

// A double-quoted string literal.
// Atomic string: matches anything between double quotes
string = ${ "\"" ~ (!"\"" ~ ANY)* ~ "\"" }

// A function call: name(arg1, arg2, …). Arguments are full expr trees.
function_call = ${ identifier ~ ws* ~ "(" ~ ws* ~ (expr ~ (ws* ~ "," ~ ws* ~ expr)*)? ~ ws* ~ ")" }

// An amount literal: commodity-prefixed, number-first, or bare number.
//   "$100"        — symbol before number (no space required)
//   "100 USD"     — identifier commodity after number (space required)
//   "100"         — bare number (commodity filled in from context later)
//   `"Foo" -1`    — quoted commodity before a signed number (ledger-cli
//                   accepts optional whitespace between sign and number)
// The negative lookahead `!bool_op` in the number-first alternative prevents
// `and`/`or` from being consumed as a commodity (e.g., `0 and` in `amount > 0 and`).
amount = ${
    (commodity ~ ws* ~ prefix_op? ~ ws* ~ number)
    | (number ~ ws+ ~ !bool_op ~ commodity)
    | (number ~ commodity)
    | number
}

// Numbers may use comma thousands-separators ("1,234.56") or start with a
// decimal point (".5"). Commas are stripped in parser.rs before parsing.
// Comma followed by 3 digits to distinguish from function arguments
number = @{
    ASCII_DIGIT+ ~ ("," ~ ASCII_DIGIT{3})* ~ ("." ~ ASCII_DIGIT+)?
    | "." ~ ASCII_DIGIT+
}

// A commodity is either one or more currency symbols ($, €, £, ¥), a
// double-quoted opaque name (e.g. "Plans: Wildthorn Mail"), or an
// identifier-style name (USD, BTC, AAPL, …).  The quoted alternative is
// listed second so that a leading `"` is always consumed by it rather than
// by the symbol alternative; the identifier alternative is last so that bare
// names still work.  Escape sequences are not supported inside quoted names
// (ledger-cli does not support them either); embedded quotes are disallowed.
commodity = @{
    symbol+
    | "\"" ~ (!"\"" ~ ANY)+ ~ "\""
    | (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "." | ":" | "#")*
}

// Currency symbols treated as single-character commodities.
symbol = _{ "$" | "€" | "£" | "¥" }

// A plain identifier: letter or underscore, then alphanumeric or underscore.
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }

// Infix and prefix operators. These rules are named (not silent) so that
// the Pratt parser in parser.rs can match on their Rule variants.
// Operators must be visible to Pratt
infix_op = _{ add | sub | mul | div }
    add = { "+" }
    sub = { "-" }
    mul = { "*" }
    div = { "/" }
prefix_op = { "-" | "+" }