Skip to main content

rustledger_parser/cst/
ast.rs

1//! Typed AST wrappers over the lossless CST.
2//!
3//! Phase 3 of #1262. The CST (phase 1-2) preserves every byte of
4//! the source as an untyped tree of `SyntaxKind` nodes and tokens.
5//! This module adds a thin typed layer on top: newtype wrappers
6//! around `SyntaxNode` / `SyntaxToken` with `kind()`-gated
7//! constructors (`cast`) and structural accessors (`date()`,
8//! `account()`, `amount()`, etc.).
9//!
10//! Two traits anchor the surface:
11//!
12//! - [`AstNode`]: typed wrapper around a `SyntaxNode`. Each wrapper
13//!   pins its expected `SyntaxKind` via `can_cast` and offers
14//!   accessors that walk direct children.
15//! - [`AstToken`]: typed wrapper around a `SyntaxToken`. Provides
16//!   `text()` for the raw bytes; specific token wrappers (`Date`,
17//!   `Account`, `Number`, ...) can layer parsing on top.
18//!
19//! The wrappers are zero-cost — they store a `SyntaxNode` /
20//! `SyntaxToken` by value and forward to it. Cloning is cheap
21//! (rowan's nodes/tokens are `Arc`-backed). All accessors return
22//! `Option<_>` because the CST is lossless: a malformed input
23//! still produces a tree, just one with missing children.
24//!
25//! # Round-trip
26//!
27//! Every wrapper exposes `syntax()` returning the underlying
28//! `SyntaxNode`/`SyntaxToken`, whose `text()` reproduces the
29//! original bytes exactly. Typed-AST consumers that want to
30//! modify the source can therefore navigate via accessors and
31//! splice via raw text ranges.
32#![allow(missing_docs)] // Accessors are self-documenting via function name + return type.
33
34use crate::cst::syntax_kind::{SyntaxKind, SyntaxNode, SyntaxToken};
35
36/// Re-export of rowan's `SyntaxText` — a rope view over a
37/// `SyntaxNode`'s text without allocation. Returned by
38/// [`ErrorNode::text`] so consumers don't need a direct
39/// `rowan` dependency.
40pub use rowan::SyntaxText;
41
42/// Typed wrapper around a `SyntaxNode` of a specific
43/// `SyntaxKind`.
44pub trait AstNode: Sized {
45    /// Returns true iff `kind` is the wrapper's expected node
46    /// kind. Used by `cast` and by enum dispatch.
47    fn can_cast(kind: SyntaxKind) -> bool;
48
49    /// Wrap `syntax` if its kind matches; otherwise `None`.
50    fn cast(syntax: SyntaxNode) -> Option<Self>;
51
52    /// The underlying CST node. `text()` reproduces the original
53    /// bytes; `children()` / `children_with_tokens()` walk the
54    /// tree.
55    fn syntax(&self) -> &SyntaxNode;
56}
57
58/// Typed wrapper around a `SyntaxToken` of a specific
59/// `SyntaxKind`. Like [`AstNode`] but for leaf tokens.
60pub trait AstToken: Sized {
61    fn can_cast(kind: SyntaxKind) -> bool;
62    fn cast(token: SyntaxToken) -> Option<Self>;
63    fn syntax(&self) -> &SyntaxToken;
64
65    /// The raw token text (borrowed from the green tree, zero
66    /// allocation). Tokens are always contiguous, so a `&str`
67    /// slice is well-defined.
68    fn text(&self) -> &str {
69        self.syntax().text()
70    }
71}
72
73// ---- Helpers --------------------------------------------------
74
75/// First direct-child token of `kind` under `node`, or `None`.
76fn first_token(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
77    node.children_with_tokens()
78        .filter_map(rowan::NodeOrToken::into_token)
79        .find(|t| t.kind() == kind)
80}
81
82/// Nth (0-indexed) direct-child token of `kind` under `node`.
83fn nth_token(node: &SyntaxNode, kind: SyntaxKind, n: usize) -> Option<SyntaxToken> {
84    node.children_with_tokens()
85        .filter_map(rowan::NodeOrToken::into_token)
86        .filter(|t| t.kind() == kind)
87        .nth(n)
88}
89
90/// All direct-child tokens of `kind` under `node`.
91fn tokens_of_kind(node: &SyntaxNode, kind: SyntaxKind) -> impl Iterator<Item = SyntaxToken> + '_ {
92    node.children_with_tokens()
93        .filter_map(rowan::NodeOrToken::into_token)
94        .filter(move |t| t.kind() == kind)
95}
96
97/// First direct-child node castable to `N`.
98fn first_child<N: AstNode>(node: &SyntaxNode) -> Option<N> {
99    node.children().find_map(N::cast)
100}
101
102/// All direct-child nodes castable to `N`.
103fn children<'a, N: AstNode + 'a>(node: &'a SyntaxNode) -> impl Iterator<Item = N> + 'a {
104    node.children().filter_map(N::cast)
105}
106
107// ---- Macros ---------------------------------------------------
108
109macro_rules! ast_node {
110    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
111        $(#[$meta])*
112        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
113        pub struct $name(SyntaxNode);
114
115        impl AstNode for $name {
116            fn can_cast(kind: SyntaxKind) -> bool {
117                kind == SyntaxKind::$kind
118            }
119            fn cast(syntax: SyntaxNode) -> Option<Self> {
120                Self::can_cast(syntax.kind()).then_some(Self(syntax))
121            }
122            fn syntax(&self) -> &SyntaxNode {
123                &self.0
124            }
125        }
126    };
127}
128
129macro_rules! ast_token {
130    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
131        $(#[$meta])*
132        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
133        pub struct $name(SyntaxToken);
134
135        impl AstToken for $name {
136            fn can_cast(kind: SyntaxKind) -> bool {
137                kind == SyntaxKind::$kind
138            }
139            fn cast(token: SyntaxToken) -> Option<Self> {
140                Self::can_cast(token.kind()).then_some(Self(token))
141            }
142            fn syntax(&self) -> &SyntaxToken {
143                &self.0
144            }
145        }
146    };
147}
148
149// ---- Token wrappers -------------------------------------------
150
151ast_token!(
152    /// `DATE` token (e.g., `2024-01-15`).
153    Date, DATE
154);
155ast_token!(
156    /// `ACCOUNT` token (e.g., `Assets:Cash`).
157    Account, ACCOUNT
158);
159ast_token!(
160    /// `CURRENCY` token (e.g., `USD`).
161    CurrencyName, CURRENCY
162);
163ast_token!(
164    /// `STRING` literal (e.g., `"Coffee"`). `text()` includes the
165    /// surrounding quotes; use `text_unquoted()` for the content.
166    StringLit, STRING
167);
168
169impl StringLit {
170    /// String content with surrounding `"` stripped. Returns
171    /// `None` if the raw text isn't a well-formed quoted string.
172    /// Borrowed from the green tree (zero allocation).
173    pub fn text_unquoted(&self) -> Option<&str> {
174        let raw = self.text();
175        let bytes = raw.as_bytes();
176        if bytes.len() < 2 || bytes[0] != b'"' || bytes[bytes.len() - 1] != b'"' {
177            return None;
178        }
179        Some(&raw[1..raw.len() - 1])
180    }
181}
182
183ast_token!(
184    /// `NUMBER` token (e.g., `100.00`).
185    Number, NUMBER
186);
187ast_token!(
188    /// `META_KEY` token (e.g., `note:`). Note the trailing colon
189    /// is part of the token; use `text_without_colon()` to strip it.
190    MetaKey, META_KEY
191);
192
193impl MetaKey {
194    /// Key name with the trailing `:` stripped. Borrowed from the
195    /// green tree (zero allocation).
196    pub fn text_without_colon(&self) -> &str {
197        let raw = self.text();
198        raw.strip_suffix(':').unwrap_or(raw)
199    }
200}
201
202ast_token!(
203    /// `TAG` token (e.g., `#trip`).
204    Tag, TAG
205);
206ast_token!(
207    /// `LINK` token (e.g., `^expense-123`).
208    Link, LINK
209);
210ast_token!(
211    /// `BOOL_TRUE` token literal.
212    BoolTrue, BOOL_TRUE
213);
214ast_token!(
215    /// `BOOL_FALSE` token literal.
216    BoolFalse, BOOL_FALSE
217);
218
219// ---- Heterogeneous flag/sign token wrappers --------------------
220//
221// These wrap a SyntaxToken whose kind is one of several
222// possibilities (a transaction flag may be STAR, PENDING_KW, FLAG
223// letter, HASH, TXN_KW, or single-char CURRENCY). We deliberately
224// do NOT implement AstToken for them: AstToken::can_cast is
225// kind-only, and the CURRENCY case needs a length check (only
226// single-character CURRENCY counts as a ticker-letter flag).
227// Inherent cast() runs the full check.
228//
229// Downstream code that needs exhaustive matching should use the
230// `kind()` method paired with the dedicated `*FlagKind` enum
231// returned by `classify()` (or `Sign::classify()`), which is
232// pinned to the same variant set as `cast`.
233
234/// Exhaustive classification of a [`TransactionFlag`] token.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236pub enum TransactionFlagKind {
237    /// `*` token.
238    Star,
239    /// `!` (the `PENDING_KW` token).
240    Pending,
241    /// Single-letter `FLAG` token (e.g. `P` from `posti P`).
242    Letter,
243    /// `#` token.
244    Hash,
245    /// `txn` keyword.
246    Txn,
247    /// Single-character `CURRENCY` token used as the
248    /// ticker-letter flag.
249    CurrencyLetter,
250}
251
252/// Typed wrapper for the transaction-header flag token.
253///
254/// May be `STAR` (`*`), `PENDING_KW` (`!`), `FLAG` (letter),
255/// `HASH` (`#`), `TXN_KW` (`txn`), or single-character `CURRENCY`
256/// (ticker-letter flag, e.g. `T`). Use [`Self::classify`] for
257/// exhaustive `match` ergonomics, or the `is_*` predicates for
258/// boolean checks.
259///
260/// **Note**: [`Self::cast`] is position-AGNOSTIC — it accepts any
261/// token of a flag-eligible kind regardless of where it sits in
262/// the tree. To get the leading flag of a transaction, use
263/// [`Transaction::flag`] (which scopes the search to the
264/// pre-content header region).
265#[derive(Debug, Clone, PartialEq, Eq, Hash)]
266pub struct TransactionFlag {
267    token: SyntaxToken,
268    classification: TransactionFlagKind,
269}
270
271impl TransactionFlag {
272    /// Wrap the token if its kind is a valid transaction flag.
273    /// For `CURRENCY`, only single-character forms qualify.
274    ///
275    /// Single source of truth: this match also derives
276    /// [`Self::classify`]'s result, so cast + classify cannot
277    /// drift.
278    pub fn cast(token: SyntaxToken) -> Option<Self> {
279        let classification = match token.kind() {
280            SyntaxKind::STAR => TransactionFlagKind::Star,
281            SyntaxKind::PENDING_KW => TransactionFlagKind::Pending,
282            SyntaxKind::FLAG => TransactionFlagKind::Letter,
283            SyntaxKind::HASH => TransactionFlagKind::Hash,
284            SyntaxKind::TXN_KW => TransactionFlagKind::Txn,
285            SyntaxKind::CURRENCY if token.text().len() == 1 => TransactionFlagKind::CurrencyLetter,
286            _ => return None,
287        };
288        Some(Self {
289            token,
290            classification,
291        })
292    }
293
294    pub const fn syntax(&self) -> &SyntaxToken {
295        &self.token
296    }
297    pub fn kind(&self) -> SyntaxKind {
298        self.token.kind()
299    }
300    pub fn text(&self) -> &str {
301        self.token.text()
302    }
303
304    /// Exhaustive classification — pair with a `match` for
305    /// compiler-checked coverage of every variant. Cached at
306    /// `cast()` time; no runtime panic risk.
307    pub const fn classify(&self) -> TransactionFlagKind {
308        self.classification
309    }
310
311    pub const fn is_star(&self) -> bool {
312        matches!(self.classification, TransactionFlagKind::Star)
313    }
314    pub const fn is_pending(&self) -> bool {
315        matches!(self.classification, TransactionFlagKind::Pending)
316    }
317    pub const fn is_hash(&self) -> bool {
318        matches!(self.classification, TransactionFlagKind::Hash)
319    }
320    pub const fn is_txn(&self) -> bool {
321        matches!(self.classification, TransactionFlagKind::Txn)
322    }
323    pub const fn is_letter_flag(&self) -> bool {
324        matches!(self.classification, TransactionFlagKind::Letter)
325    }
326    pub const fn is_currency_letter(&self) -> bool {
327        matches!(self.classification, TransactionFlagKind::CurrencyLetter)
328    }
329}
330
331/// Exhaustive classification of a [`PostingFlag`] token.
332/// Same as [`TransactionFlagKind`] minus `Txn` (postings cannot
333/// carry the `txn` keyword).
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
335pub enum PostingFlagKind {
336    Star,
337    Pending,
338    Letter,
339    Hash,
340    CurrencyLetter,
341}
342
343/// Typed wrapper for a posting-line flag token. Same as
344/// [`TransactionFlag`] minus the `TXN_KW` variant (postings can't
345/// carry the `txn` keyword). Use [`Self::classify`] for
346/// exhaustive `match` ergonomics.
347///
348/// **Note**: [`Self::cast`] is position-AGNOSTIC. To get the
349/// leading flag of a posting, use [`Posting::flag`] (which
350/// scopes the search to the pre-ACCOUNT region of the posting).
351#[derive(Debug, Clone, PartialEq, Eq, Hash)]
352pub struct PostingFlag {
353    token: SyntaxToken,
354    classification: PostingFlagKind,
355}
356
357impl PostingFlag {
358    /// Single source of truth for cast + classify — drift impossible.
359    pub fn cast(token: SyntaxToken) -> Option<Self> {
360        let classification = match token.kind() {
361            SyntaxKind::STAR => PostingFlagKind::Star,
362            SyntaxKind::PENDING_KW => PostingFlagKind::Pending,
363            SyntaxKind::FLAG => PostingFlagKind::Letter,
364            SyntaxKind::HASH => PostingFlagKind::Hash,
365            SyntaxKind::CURRENCY if token.text().len() == 1 => PostingFlagKind::CurrencyLetter,
366            _ => return None,
367        };
368        Some(Self {
369            token,
370            classification,
371        })
372    }
373
374    pub const fn syntax(&self) -> &SyntaxToken {
375        &self.token
376    }
377    pub fn kind(&self) -> SyntaxKind {
378        self.token.kind()
379    }
380    pub fn text(&self) -> &str {
381        self.token.text()
382    }
383
384    /// Exhaustive classification — cached at `cast()` time;
385    /// no runtime panic risk.
386    pub const fn classify(&self) -> PostingFlagKind {
387        self.classification
388    }
389
390    pub const fn is_star(&self) -> bool {
391        matches!(self.classification, PostingFlagKind::Star)
392    }
393    pub const fn is_pending(&self) -> bool {
394        matches!(self.classification, PostingFlagKind::Pending)
395    }
396    pub const fn is_hash(&self) -> bool {
397        matches!(self.classification, PostingFlagKind::Hash)
398    }
399    pub const fn is_letter_flag(&self) -> bool {
400        matches!(self.classification, PostingFlagKind::Letter)
401    }
402    pub const fn is_currency_letter(&self) -> bool {
403        matches!(self.classification, PostingFlagKind::CurrencyLetter)
404    }
405}
406
407/// Typed wrapper for an amount sign token (`PLUS` or `MINUS`).
408///
409/// `Sign::cast` is a position-AGNOSTIC kind check: it accepts ANY
410/// `PLUS` or `MINUS` token, including operator-position signs
411/// inside arithmetic (e.g., the `-` in `10 + -5 USD`). To get the
412/// LEADING sign of an `Amount`, use [`Amount::sign`] which scopes
413/// to the first non-whitespace token of `AMOUNT`. Calling
414/// `Sign::cast` on an arbitrary token does not imply the token
415/// occupies the leading-sign position.
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
417pub struct Sign(SyntaxToken);
418
419impl Sign {
420    pub fn cast(token: SyntaxToken) -> Option<Self> {
421        matches!(token.kind(), SyntaxKind::PLUS | SyntaxKind::MINUS).then_some(Self(token))
422    }
423    pub const fn syntax(&self) -> &SyntaxToken {
424        &self.0
425    }
426    pub fn kind(&self) -> SyntaxKind {
427        self.0.kind()
428    }
429    pub fn text(&self) -> &str {
430        self.0.text()
431    }
432    pub fn is_plus(&self) -> bool {
433        self.kind() == SyntaxKind::PLUS
434    }
435    pub fn is_minus(&self) -> bool {
436        self.kind() == SyntaxKind::MINUS
437    }
438}
439
440// ---- Source file root + Directive enum ------------------------
441
442ast_node!(
443    /// Root of a parsed Beancount file. `SourceFile::parse(src)` is
444    /// the typed-AST entry point — it wraps `parse_structured`.
445    SourceFile, SOURCE_FILE
446);
447
448impl SourceFile {
449    /// Parse `source` into a typed source-file tree.
450    #[must_use]
451    pub fn parse(source: &str) -> Self {
452        let node = crate::cst::parser::parse_structured(source);
453        Self::cast(node).expect("parse_structured always returns a SOURCE_FILE")
454    }
455
456    /// All recognized directives, in source order.
457    pub fn directives(&self) -> impl Iterator<Item = Directive> + '_ {
458        self.syntax().children().filter_map(Directive::cast)
459    }
460
461    /// All `ERROR_NODE` wrappers (unrecognized / malformed lines).
462    pub fn errors(&self) -> impl Iterator<Item = ErrorNode> + '_ {
463        self.syntax().children().filter_map(ErrorNode::cast)
464    }
465}
466
467// Sum-type Directive enum + AstNode impl + per-variant struct
468// declarations, all derived from a single variant list. The
469// macro is the single source of truth for "what directives
470// exist": adding a new directive requires editing exactly one
471// line. Drift between any of {can_cast, cast, syntax, per-variant
472// struct decl, per-variant AstNode impl} is structurally
473// impossible.
474//
475// Per-variant accessor methods (date(), account(), etc.) stay
476// in separate `impl SomeDirective { ... }` blocks below.
477macro_rules! directive_enum {
478    ($($(#[$variant_meta:meta])* $variant:ident($struct:ident, $kind:ident)),* $(,)?) => {
479        // Per-variant struct + AstNode impl, formerly emitted via
480        // `ast_node!` invocations. Folded into directive_enum! so
481        // the variant list is the only source of truth.
482        $(
483            $(#[$variant_meta])*
484            #[derive(Debug, Clone, PartialEq, Eq, Hash)]
485            pub struct $struct(SyntaxNode);
486
487            impl AstNode for $struct {
488                fn can_cast(kind: SyntaxKind) -> bool {
489                    kind == SyntaxKind::$kind
490                }
491                fn cast(syntax: SyntaxNode) -> Option<Self> {
492                    Self::can_cast(syntax.kind()).then_some(Self(syntax))
493                }
494                fn syntax(&self) -> &SyntaxNode {
495                    &self.0
496                }
497            }
498        )*
499
500        /// Sum type over every recognized top-level directive wrapper.
501        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
502        pub enum Directive {
503            $($variant($struct),)*
504        }
505
506        impl AstNode for Directive {
507            fn can_cast(kind: SyntaxKind) -> bool {
508                matches!(kind, $(SyntaxKind::$kind)|*)
509            }
510
511            fn cast(node: SyntaxNode) -> Option<Self> {
512                Some(match node.kind() {
513                    $(SyntaxKind::$kind => Self::$variant($struct(node)),)*
514                    _ => return None,
515                })
516            }
517
518            fn syntax(&self) -> &SyntaxNode {
519                match self {
520                    $(Self::$variant(d) => d.syntax(),)*
521                }
522            }
523        }
524    };
525}
526
527directive_enum!(
528    /// `DATE open ACCOUNT [CURRENCY[,CURRENCY]*] ["BOOKING"]`.
529    Open(OpenDirective, OPEN_DIRECTIVE),
530    /// `DATE close ACCOUNT`.
531    Close(CloseDirective, CLOSE_DIRECTIVE),
532    /// `DATE balance ACCOUNT AMOUNT_TOKENS`. Amount stays flat
533    /// (phase 2.2c scopes AMOUNT wrapping to POSTING only); walk
534    /// `number()` and `currency()` to read it.
535    Balance(BalanceDirective, BALANCE_DIRECTIVE),
536    /// `DATE pad ACCOUNT_TARGET ACCOUNT_SOURCE`.
537    Pad(PadDirective, PAD_DIRECTIVE),
538    /// `DATE event "TYPE" "VALUE"`.
539    Event(EventDirective, EVENT_DIRECTIVE),
540    /// `DATE query "NAME" "QUERY"`.
541    Query(QueryDirective, QUERY_DIRECTIVE),
542    /// `DATE note ACCOUNT "TEXT"`.
543    Note(NoteDirective, NOTE_DIRECTIVE),
544    /// `DATE document ACCOUNT "PATH"`.
545    Document(DocumentDirective, DOCUMENT_DIRECTIVE),
546    /// `DATE price CURRENCY NUMBER CURRENCY`.
547    Price(PriceDirective, PRICE_DIRECTIVE),
548    /// `DATE commodity CURRENCY`.
549    Commodity(CommodityDirective, COMMODITY_DIRECTIVE),
550    /// `pushtag #TAG`.
551    Pushtag(PushtagDirective, PUSHTAG_DIRECTIVE),
552    /// `poptag #TAG`.
553    Poptag(PoptagDirective, POPTAG_DIRECTIVE),
554    /// `pushmeta KEY: VALUE`.
555    Pushmeta(PushmetaDirective, PUSHMETA_DIRECTIVE),
556    /// `popmeta KEY:`.
557    Popmeta(PopmetaDirective, POPMETA_DIRECTIVE),
558    /// `option "KEY" "VALUE"`.
559    Option(OptionDirective, OPTION_DIRECTIVE),
560    /// `include "PATH"`.
561    Include(IncludeDirective, INCLUDE_DIRECTIVE),
562    /// `plugin "MODULE" ["CONFIG"]`.
563    Plugin(PluginDirective, PLUGIN_DIRECTIVE),
564    /// `DATE custom "TYPE" values...`. Heterogeneous value list
565    /// stays flat (phase 2.3); walk the raw token sequence
566    /// via `syntax().children_with_tokens()`.
567    Custom(CustomDirective, CUSTOM_DIRECTIVE),
568    /// `DATE FLAG ["PAYEE"] "NARRATION" #TAG... ^LINK...`
569    /// followed by indented `POSTING` lines and `META_ENTRY`
570    /// sub-lines.
571    Transaction(Transaction, TRANSACTION),
572);
573
574impl Directive {
575    /// Metadata sub-lines attached to this directive (phase 2.2a
576    /// `META_ENTRY` wrapping). Every directive wrapper may carry
577    /// indented metadata.
578    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
579        children(self.syntax())
580    }
581}
582
583ast_node!(
584    /// Wrapper for unrecognized / malformed top-level content
585    /// (PR 2.4 `ERROR_NODE`). Typed-AST consumers can use this to
586    /// surface error regions to users (e.g., LSP diagnostics).
587    ErrorNode, ERROR_NODE
588);
589
590impl ErrorNode {
591    /// The raw bytes of the malformed region as a [`SyntaxText`]
592    /// rope view. Zero allocation; use `.to_string()` on the
593    /// result if you need an owned `String`, or `format!` /
594    /// `Display` for direct output.
595    #[must_use]
596    pub fn text(&self) -> SyntaxText {
597        self.syntax().text()
598    }
599}
600
601// ---- 10 dated single-line directives (PR 2.1a) -----------------
602//
603// The 19 directive struct declarations + AstNode impls are
604// generated by the `directive_enum!` macro invocation above.
605// Per-variant accessor methods live in the `impl` blocks below.
606
607impl OpenDirective {
608    pub fn date(&self) -> Option<Date> {
609        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
610    }
611    pub fn account(&self) -> Option<Account> {
612        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
613    }
614    /// Comma-separated currency constraint list (may be empty).
615    pub fn currencies(&self) -> impl Iterator<Item = CurrencyName> + '_ {
616        tokens_of_kind(self.syntax(), SyntaxKind::CURRENCY).filter_map(CurrencyName::cast)
617    }
618    /// Optional booking-method string (e.g., `"STRICT"`).
619    pub fn booking_method(&self) -> Option<StringLit> {
620        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
621    }
622}
623
624impl CloseDirective {
625    pub fn date(&self) -> Option<Date> {
626        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
627    }
628    pub fn account(&self) -> Option<Account> {
629        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
630    }
631}
632
633impl BalanceDirective {
634    pub fn date(&self) -> Option<Date> {
635        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
636    }
637    pub fn account(&self) -> Option<Account> {
638        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
639    }
640    pub fn number(&self) -> Option<Number> {
641        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
642    }
643    pub fn currency(&self) -> Option<CurrencyName> {
644        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
645    }
646}
647
648impl PadDirective {
649    pub fn date(&self) -> Option<Date> {
650        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
651    }
652    pub fn target_account(&self) -> Option<Account> {
653        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
654    }
655    pub fn source_account(&self) -> Option<Account> {
656        nth_token(self.syntax(), SyntaxKind::ACCOUNT, 1).and_then(Account::cast)
657    }
658}
659
660impl EventDirective {
661    pub fn date(&self) -> Option<Date> {
662        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
663    }
664    pub fn event_type(&self) -> Option<StringLit> {
665        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
666    }
667    pub fn value(&self) -> Option<StringLit> {
668        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
669    }
670}
671
672impl QueryDirective {
673    pub fn date(&self) -> Option<Date> {
674        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
675    }
676    pub fn name(&self) -> Option<StringLit> {
677        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
678    }
679    pub fn query(&self) -> Option<StringLit> {
680        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
681    }
682}
683
684impl NoteDirective {
685    pub fn date(&self) -> Option<Date> {
686        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
687    }
688    pub fn account(&self) -> Option<Account> {
689        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
690    }
691    pub fn text(&self) -> Option<StringLit> {
692        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
693    }
694}
695
696impl DocumentDirective {
697    pub fn date(&self) -> Option<Date> {
698        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
699    }
700    pub fn account(&self) -> Option<Account> {
701        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
702    }
703    pub fn path(&self) -> Option<StringLit> {
704        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
705    }
706}
707
708impl PriceDirective {
709    pub fn date(&self) -> Option<Date> {
710        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
711    }
712    pub fn base_currency(&self) -> Option<CurrencyName> {
713        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
714    }
715    pub fn number(&self) -> Option<Number> {
716        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
717    }
718    pub fn quote_currency(&self) -> Option<CurrencyName> {
719        nth_token(self.syntax(), SyntaxKind::CURRENCY, 1).and_then(CurrencyName::cast)
720    }
721}
722
723impl CommodityDirective {
724    pub fn date(&self) -> Option<Date> {
725        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
726    }
727    pub fn currency(&self) -> Option<CurrencyName> {
728        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
729    }
730}
731
732// ---- 4 standalone-keyword directives (PR 2.1a) -----------------
733
734impl PushtagDirective {
735    pub fn tag(&self) -> Option<Tag> {
736        first_token(self.syntax(), SyntaxKind::TAG).and_then(Tag::cast)
737    }
738}
739
740impl PoptagDirective {
741    pub fn tag(&self) -> Option<Tag> {
742        first_token(self.syntax(), SyntaxKind::TAG).and_then(Tag::cast)
743    }
744}
745
746impl PushmetaDirective {
747    pub fn key(&self) -> Option<MetaKey> {
748        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
749    }
750}
751
752impl PopmetaDirective {
753    pub fn key(&self) -> Option<MetaKey> {
754        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
755    }
756}
757
758// ---- 4 edge directives (PR 2.3) --------------------------------
759
760impl OptionDirective {
761    pub fn key(&self) -> Option<StringLit> {
762        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
763    }
764    pub fn value(&self) -> Option<StringLit> {
765        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
766    }
767}
768
769impl IncludeDirective {
770    pub fn path(&self) -> Option<StringLit> {
771        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
772    }
773}
774
775impl PluginDirective {
776    pub fn module(&self) -> Option<StringLit> {
777        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
778    }
779    pub fn config(&self) -> Option<StringLit> {
780        nth_token(self.syntax(), SyntaxKind::STRING, 1).and_then(StringLit::cast)
781    }
782}
783
784impl CustomDirective {
785    pub fn date(&self) -> Option<Date> {
786        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
787    }
788    /// The type-name string (always the first `STRING` after the
789    /// `custom` keyword).
790    pub fn custom_type(&self) -> Option<StringLit> {
791        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
792    }
793}
794
795// ---- TRANSACTION + body sub-nodes ------------------------------
796
797impl Transaction {
798    /// Direct-child tokens of TRANSACTION in the header region
799    /// only: leading trivia (whitespace, newlines, comments
800    /// attached as inter-directive leading trivia per the
801    /// Directive-Terminator Rule) is skipped, then tokens are
802    /// collected until the first NEWLINE that terminates the
803    /// header line. Body content (`POSTING` / `META_ENTRY` nodes;
804    /// flat tokens emitted by `emit_transaction_body`'s catch-all
805    /// for malformed indented lines) is excluded.
806    fn header_tokens(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
807        self.syntax()
808            .children_with_tokens()
809            .filter_map(rowan::NodeOrToken::into_token)
810            // Skip leading trivia (blank-line newlines, top-of-
811            // directive whitespace, leading comments). The first
812            // non-trivia token marks the start of the header.
813            //
814            // Comment-trivia covers all four comment kinds — ledger-
815            // style `%` comments and org-mode `#!`/`#+` lines are
816            // attached as leading trivia by the Directive-Terminator
817            // Rule the same way `;` comments are, so a transaction
818            // preceded by any of them must skip them too. BOM stays
819            // OUT of the skip set: a mid-file BOM in a transaction
820            // header is a corruption to surface, not trivia.
821            .skip_while(|t| {
822                matches!(
823                    t.kind(),
824                    SyntaxKind::WHITESPACE
825                        | SyntaxKind::NEWLINE
826                        | SyntaxKind::COMMENT
827                        | SyntaxKind::PERCENT_COMMENT
828                        | SyntaxKind::SHEBANG
829                        | SyntaxKind::EMACS_DIRECTIVE
830                )
831            })
832            .take_while(|t| t.kind() != SyntaxKind::NEWLINE)
833    }
834
835    /// Header tokens BEFORE the first STRING/TAG/LINK — i.e., the
836    /// flag-position region (between DATE and the first header
837    /// content token). Used by [`Self::flag`] to scope its search.
838    fn flag_region_tokens(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
839        self.header_tokens().take_while(|t| {
840            !matches!(
841                t.kind(),
842                SyntaxKind::STRING | SyntaxKind::TAG | SyntaxKind::LINK
843            )
844        })
845    }
846
847    pub fn date(&self) -> Option<Date> {
848        // DATE is in the header, so first_token over the whole node
849        // is fine — but for symmetry, scope to header_tokens.
850        self.header_tokens()
851            .find(|t| t.kind() == SyntaxKind::DATE)
852            .and_then(Date::cast)
853    }
854
855    /// Transaction flag token. May be `STAR` (`*`), `PENDING_KW`
856    /// (`!`), `FLAG` letter, `HASH` (`#`), `TXN_KW`
857    /// (the `txn` keyword), single-char `CURRENCY` (ticker-letter
858    /// flag), or absent (implied via a leading `STRING`
859    /// payee/narration).
860    ///
861    /// Scoped to the flag-position region (between `DATE` and the
862    /// first `STRING`/`TAG`/`LINK`) so a stray trailing
863    /// single-char `CURRENCY` after the narration is NOT
864    /// misclassified as a flag.
865    pub fn flag(&self) -> Option<TransactionFlag> {
866        self.flag_region_tokens().find_map(TransactionFlag::cast)
867    }
868
869    /// All `STRING` tokens in the header, in source order.
870    ///
871    /// Scoped to the header (tokens before the terminating
872    /// `NEWLINE`), so `STRING` tokens emitted into TRANSACTION by
873    /// `emit_transaction_body`'s catch-all for malformed indented
874    /// body lines are excluded.
875    ///
876    /// The 2-string convention (`"payee" "narration"`) is the
877    /// canonical form; [`Self::payee`] and [`Self::narration`]
878    /// follow it strictly. For 3+ strings (malformed but
879    /// losslessly parsed), use this method to surface every
880    /// header string.
881    pub fn strings(&self) -> impl Iterator<Item = StringLit> + '_ {
882        self.header_tokens()
883            .filter(|t| t.kind() == SyntaxKind::STRING)
884            .filter_map(StringLit::cast)
885    }
886
887    /// The payee string, if a separate payee + narration pair is
888    /// present. Returns `Some(first)` ONLY when exactly two
889    /// header `STRING` tokens appear (the canonical
890    /// `"payee" "narration"` shape). With 0, 1, or 3+ strings
891    /// the convention is ambiguous and this returns `None` —
892    /// use [`Self::strings`] for lossless access.
893    pub fn payee(&self) -> Option<StringLit> {
894        // Take up to 3 to disambiguate 2 from 3+ without
895        // allocating the whole sequence.
896        let mut iter = self.strings();
897        let first = iter.next()?;
898        let second = iter.next()?;
899        if iter.next().is_some() {
900            None
901        } else {
902            // Exactly 2 strings; first is payee.
903            let _ = second;
904            Some(first)
905        }
906    }
907
908    /// The narration string. Returns `Some(only)` for a single
909    /// header string and `Some(last)` for the 2-string
910    /// `"payee" "narration"` form. Returns `None` for 0 or 3+
911    /// strings — use [`Self::strings`] for lossless access on
912    /// malformed headers.
913    pub fn narration(&self) -> Option<StringLit> {
914        let mut iter = self.strings();
915        let first = iter.next()?;
916        let second = iter.next();
917        let third = iter.next();
918        match (second, third) {
919            (None, _) => Some(first),
920            (Some(s2), None) => Some(s2),
921            _ => None,
922        }
923    }
924
925    /// All `#TAG` tokens attached to the transaction header.
926    /// Scoped to the header region (excludes body tokens).
927    pub fn tags(&self) -> impl Iterator<Item = Tag> + '_ {
928        self.header_tokens()
929            .filter(|t| t.kind() == SyntaxKind::TAG)
930            .filter_map(Tag::cast)
931    }
932
933    /// All `^LINK` tokens attached to the transaction header.
934    /// Scoped to the header region (excludes body tokens).
935    pub fn links(&self) -> impl Iterator<Item = Link> + '_ {
936        self.header_tokens()
937            .filter(|t| t.kind() == SyntaxKind::LINK)
938            .filter_map(Link::cast)
939    }
940
941    /// All `POSTING` sub-lines, in source order.
942    pub fn postings(&self) -> impl Iterator<Item = Posting> + '_ {
943        children(self.syntax())
944    }
945
946    /// Transaction-level `META_ENTRY` sub-lines — those not attached
947    /// to a posting (chiefly metadata preceding the first posting).
948    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
949        children(self.syntax())
950    }
951}
952
953ast_node!(
954    /// `WS [(FLAG | STAR | PENDING_KW | HASH | single-char CURRENCY) WS] ACCOUNT [AMOUNT] [COST_SPEC] [PRICE_ANNOTATION]`.
955    Posting, POSTING
956);
957
958impl Posting {
959    /// Posting flag (optional). Same kinds as
960    /// [`TransactionFlag`] minus `TXN_KW` — indicates whether
961    /// THIS posting is pending, marked, etc.
962    pub fn flag(&self) -> Option<PostingFlag> {
963        // Walk children up to the ACCOUNT; the first non-whitespace
964        // token is the flag iff it's a valid PostingFlag kind.
965        for el in self.syntax().children_with_tokens() {
966            if let rowan::NodeOrToken::Token(t) = el {
967                match t.kind() {
968                    SyntaxKind::WHITESPACE => {}
969                    SyntaxKind::ACCOUNT => return None,
970                    _ => return PostingFlag::cast(t),
971                }
972            }
973        }
974        None
975    }
976
977    pub fn account(&self) -> Option<Account> {
978        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
979    }
980
981    /// Units `AMOUNT` (optional — auto postings have none).
982    pub fn amount(&self) -> Option<Amount> {
983        first_child(self.syntax())
984    }
985
986    /// `COST_SPEC` annotation, if present.
987    pub fn cost_spec(&self) -> Option<CostSpec> {
988        first_child(self.syntax())
989    }
990
991    /// `PRICE_ANNOTATION`, if present.
992    pub fn price_annotation(&self) -> Option<PriceAnnotation> {
993        first_child(self.syntax())
994    }
995
996    /// Posting-attached metadata (`META_ENTRY` sub-lines following
997    /// the posting line at the same or deeper indent).
998    pub fn meta_entries(&self) -> impl Iterator<Item = MetaEntry> + '_ {
999        children(self.syntax())
1000    }
1001}
1002
1003// ---- AMOUNT / COST_SPEC / PRICE_ANNOTATION / META_ENTRY --------
1004
1005ast_node!(
1006    /// Units amount: `[sign] (NUMBER | PAREN_EXPR) ([WS] op
1007    /// [WS] [sign] (NUMBER | PAREN_EXPR))* [WS CURRENCY]`, or a
1008    /// bare `CURRENCY`. Phase 2.4 extension supports arithmetic.
1009    Amount, AMOUNT
1010);
1011
1012impl Amount {
1013    /// Sign token (`MINUS` or `PLUS`), if present as the FIRST
1014    /// non-whitespace child of AMOUNT. Returns `None` if no
1015    /// sign or if the leading non-whitespace token is something
1016    /// else (e.g., `L_PAREN`, `NUMBER`, `CURRENCY`).
1017    pub fn sign(&self) -> Option<Sign> {
1018        let first = self
1019            .syntax()
1020            .children_with_tokens()
1021            .filter_map(rowan::NodeOrToken::into_token)
1022            .find(|t| t.kind() != SyntaxKind::WHITESPACE)?;
1023        Sign::cast(first)
1024    }
1025
1026    /// First `NUMBER` child token (the leading operand). For an
1027    /// arithmetic expression like `10+5 USD`, this is `10`; for
1028    /// a bare CURRENCY amount this is `None`.
1029    pub fn number(&self) -> Option<Number> {
1030        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1031    }
1032
1033    /// The trailing currency at paren-depth 0.
1034    ///
1035    /// For `100 USD`, `100USD`, `(1+2) USD`: returns the trailing
1036    /// `USD`. For bare currency-only `AMOUNT(CURRENCY)`: returns
1037    /// the same token. For malformed `(1 USD)` (CURRENCY inside
1038    /// parens, no outer trailing currency): returns `None`. For
1039    /// unclosed `(1 USD\n` or stray-closer `1 USD)` (unbalanced
1040    /// parens): returns `None`, refusing to surface a possibly
1041    /// paren-internal currency.
1042    ///
1043    /// Single forward pass with paren-depth tracking; no
1044    /// allocation. `emit_amount_operand` keeps paren contents
1045    /// flat under AMOUNT (no `PAREN_EXPR` sub-node), so depth
1046    /// tracking is the only structural disambiguator.
1047    pub fn currency(&self) -> Option<CurrencyName> {
1048        let mut depth: i32 = 0;
1049        let mut last_at_depth_0: Option<SyntaxToken> = None;
1050        for el in self.syntax().children_with_tokens() {
1051            let rowan::NodeOrToken::Token(t) = el else {
1052                continue;
1053            };
1054            match t.kind() {
1055                SyntaxKind::L_PAREN => depth += 1,
1056                SyntaxKind::R_PAREN => depth -= 1,
1057                SyntaxKind::CURRENCY if depth == 0 => last_at_depth_0 = Some(t),
1058                _ => {}
1059            }
1060        }
1061        // Unbalanced parens (unclosed or stray closer): refuse to
1062        // surface a currency rather than guess.
1063        if depth != 0 {
1064            return None;
1065        }
1066        last_at_depth_0.and_then(CurrencyName::cast)
1067    }
1068
1069    /// Returns true iff the amount contains an arithmetic operator
1070    /// (`+`, `-` between operands, `*`, `/`) or a parenthesized
1071    /// sub-expression — useful for typed-AST consumers that need
1072    /// to defer to expression evaluation.
1073    #[must_use]
1074    pub fn is_arithmetic(&self) -> bool {
1075        let mut seen_first_operand = false;
1076        for el in self.syntax().children_with_tokens() {
1077            if let rowan::NodeOrToken::Token(t) = el {
1078                match t.kind() {
1079                    SyntaxKind::NUMBER => seen_first_operand = true,
1080                    SyntaxKind::L_PAREN | SyntaxKind::R_PAREN => return true,
1081                    SyntaxKind::STAR | SyntaxKind::SLASH => return true,
1082                    SyntaxKind::PLUS | SyntaxKind::MINUS if seen_first_operand => return true,
1083                    _ => {}
1084                }
1085            }
1086        }
1087        false
1088    }
1089}
1090
1091ast_node!(
1092    /// Bracketed cost annotation: `{...}` (per-unit), `{#...}`
1093    /// (per-unit + total), or `{{...}}` (total-only). Contents
1094    /// stay flat (phase 2.2c); accessors scan the children.
1095    CostSpec, COST_SPEC
1096);
1097
1098impl CostSpec {
1099    /// Returns true iff the opener is `{{` (total-cost form).
1100    #[must_use]
1101    pub fn is_total(&self) -> bool {
1102        first_token(self.syntax(), SyntaxKind::L_DOUBLE_BRACE).is_some()
1103    }
1104
1105    /// Returns true iff the opener is `{#` (per-unit + total
1106    /// form).
1107    #[must_use]
1108    pub fn is_per_unit_plus_total(&self) -> bool {
1109        first_token(self.syntax(), SyntaxKind::L_BRACE_HASH).is_some()
1110    }
1111
1112    /// Cost number (first NUMBER child token).
1113    pub fn number(&self) -> Option<Number> {
1114        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1115    }
1116
1117    /// Cost currency (first CURRENCY child token).
1118    pub fn currency(&self) -> Option<CurrencyName> {
1119        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
1120    }
1121
1122    /// Cost date (first DATE child token), if present.
1123    pub fn date(&self) -> Option<Date> {
1124        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
1125    }
1126
1127    /// Cost label (first STRING child token), if present.
1128    pub fn label(&self) -> Option<StringLit> {
1129        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
1130    }
1131
1132    /// Returns true iff the opener is immediately followed by a
1133    /// `*` merge marker (e.g., `{*}` or `{* 500 USD}`). A STAR
1134    /// elsewhere in the cost spec is the multiplication operator
1135    /// (e.g., `{500 * 2 USD}`), NOT a merge marker; position
1136    /// matters.
1137    #[must_use]
1138    pub fn is_merge(&self) -> bool {
1139        let mut past_opener = false;
1140        for el in self.syntax().children_with_tokens() {
1141            if let rowan::NodeOrToken::Token(t) = el {
1142                match t.kind() {
1143                    SyntaxKind::L_BRACE | SyntaxKind::L_DOUBLE_BRACE | SyntaxKind::L_BRACE_HASH => {
1144                        past_opener = true;
1145                    }
1146                    SyntaxKind::WHITESPACE if past_opener => {}
1147                    SyntaxKind::STAR if past_opener => return true,
1148                    _ if past_opener => return false,
1149                    _ => {}
1150                }
1151            }
1152        }
1153        false
1154    }
1155}
1156
1157ast_node!(
1158    /// Price annotation: `AT [WS AMOUNT]` (per-unit) or
1159    /// `AT_AT [WS AMOUNT]` (total).
1160    PriceAnnotation, PRICE_ANNOTATION
1161);
1162
1163impl PriceAnnotation {
1164    /// Returns true iff the opener is `@@` (total-price form).
1165    #[must_use]
1166    pub fn is_total(&self) -> bool {
1167        first_token(self.syntax(), SyntaxKind::AT_AT).is_some()
1168    }
1169
1170    /// The price's inner `AMOUNT`, if present.
1171    pub fn amount(&self) -> Option<Amount> {
1172        first_child(self.syntax())
1173    }
1174}
1175
1176ast_node!(
1177    /// Metadata sub-line: `WS META_KEY ... (NEWLINE | EOF)`.
1178    /// Key is the `META_KEY` token; value is the remaining flat
1179    /// content tokens. Use `key()` and `value_*()` accessors.
1180    MetaEntry, META_ENTRY
1181);
1182
1183impl MetaEntry {
1184    pub fn key(&self) -> Option<MetaKey> {
1185        first_token(self.syntax(), SyntaxKind::META_KEY).and_then(MetaKey::cast)
1186    }
1187
1188    /// Value as a typed STRING, if the value is a quoted string.
1189    pub fn value_string(&self) -> Option<StringLit> {
1190        first_token(self.syntax(), SyntaxKind::STRING).and_then(StringLit::cast)
1191    }
1192
1193    /// Value as a NUMBER token, if the value is numeric.
1194    pub fn value_number(&self) -> Option<Number> {
1195        first_token(self.syntax(), SyntaxKind::NUMBER).and_then(Number::cast)
1196    }
1197
1198    /// Value as a DATE token, if the value is a date literal.
1199    pub fn value_date(&self) -> Option<Date> {
1200        first_token(self.syntax(), SyntaxKind::DATE).and_then(Date::cast)
1201    }
1202
1203    /// Value as an ACCOUNT token.
1204    pub fn value_account(&self) -> Option<Account> {
1205        first_token(self.syntax(), SyntaxKind::ACCOUNT).and_then(Account::cast)
1206    }
1207
1208    /// Value as a CURRENCY token.
1209    pub fn value_currency(&self) -> Option<CurrencyName> {
1210        first_token(self.syntax(), SyntaxKind::CURRENCY).and_then(CurrencyName::cast)
1211    }
1212
1213    /// Value as a boolean (true / false token).
1214    pub fn value_bool(&self) -> Option<bool> {
1215        for el in self.syntax().children_with_tokens() {
1216            if let rowan::NodeOrToken::Token(t) = el {
1217                match t.kind() {
1218                    SyntaxKind::BOOL_TRUE => return Some(true),
1219                    SyntaxKind::BOOL_FALSE => return Some(false),
1220                    _ => {}
1221                }
1222            }
1223        }
1224        None
1225    }
1226}